Skip to content

Commit c9a7676

Browse files
Patel-Raj11ckeshavacoderabbitai[bot]
authored
Implement account permission delegation - XLS-74d and XLS-75d amendments (#840)
* Transaction model, unit and integ tests for Delegate XLS-74d amendment * fix linter errors * Update xrpl/models/transactions/transaction.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * address PR comments from Phu and Rabbit * fix linter errors * add DelegatableTransaction and GranularPermission string enums * handle PermissionValue serialization * add unit test for DelegateSet serialization * fix integration tests * refactor PermissionValue binary codec * remove DelegatableTransaction enum * refactor based on pr comments * refactor definitions.py * fix lint issue * refactor integration test case * refactor nit comment * add TransactionType and GranularPermission for types * pr review refactors * pr review comments --------- Co-authored-by: Chenna Keshava B S <[email protected]> Co-authored-by: Chenna Keshava B S <[email protected]> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent ba3cbe5 commit c9a7676

File tree

16 files changed

+635
-1
lines changed

16 files changed

+635
-1
lines changed

.ci-config/rippled.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ fixEnforceNFTokenTrustline
196196
fixReducedOffersV2
197197
DeepFreeze
198198
PermissionedDomains
199+
PermissionDelegation
199200

200201
# This section can be used to simulate various FeeSettings scenarios for rippled node in standalone mode
201202
[voting]

CHANGELOG.md

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

1616
### Added
1717
- Improved validation for models to also check param types
18+
- Support for `Account Permission` and `Account Permission Delegation` (XLS-74d, XLS-75d)
1819

1920
## [4.1.0] - 2025-2-13
2021

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
from tests.integration.integration_test_case import IntegrationTestCase
2+
from tests.integration.it_utils import (
3+
fund_wallet_async,
4+
sign_and_reliable_submission_async,
5+
test_async_and_sync,
6+
)
7+
from xrpl.models.requests import AccountObjects, AccountObjectType, LedgerEntry
8+
from xrpl.models.requests.ledger_entry import Delegate
9+
from xrpl.models.response import ResponseStatus
10+
from xrpl.models.transactions import (
11+
AccountSet,
12+
DelegateSet,
13+
GranularPermission,
14+
Payment,
15+
)
16+
from xrpl.models.transactions.delegate_set import Permission
17+
from xrpl.models.transactions.types import TransactionType
18+
from xrpl.utils import xrp_to_drops
19+
from xrpl.wallet.main import Wallet
20+
21+
22+
class TestDelegateSet(IntegrationTestCase):
23+
@test_async_and_sync(globals())
24+
async def test_delegation_with_no_permission(self, client):
25+
# Note: Using WALLET, DESTINATION accounts could pollute the test results
26+
alice = Wallet.create()
27+
await fund_wallet_async(alice)
28+
bob = Wallet.create()
29+
await fund_wallet_async(bob)
30+
carol = Wallet.create()
31+
await fund_wallet_async(carol)
32+
33+
# Use bob's account to execute a transaction on behalf of alice
34+
payment = Payment(
35+
account=alice.address,
36+
amount=xrp_to_drops(1),
37+
destination=carol.address,
38+
delegate=bob.address,
39+
)
40+
response = await sign_and_reliable_submission_async(
41+
payment, bob, client, check_fee=False
42+
)
43+
self.assertEqual(response.status, ResponseStatus.SUCCESS)
44+
45+
# The lack of AccountPermissionSet transaction will result in a tecNO_PERMISSION
46+
self.assertEqual(response.result["engine_result"], "tecNO_PERMISSION")
47+
48+
@test_async_and_sync(globals())
49+
async def test_delegate_set_workflow(self, client):
50+
# Note: Using WALLET, DESTINATION accounts could pollute the test results
51+
alice = Wallet.create()
52+
await fund_wallet_async(alice)
53+
bob = Wallet.create()
54+
await fund_wallet_async(bob)
55+
carol = Wallet.create()
56+
await fund_wallet_async(carol)
57+
58+
delegate_set = DelegateSet(
59+
account=alice.address,
60+
authorize=bob.address,
61+
# Authorize bob account to execute Payment transactions and
62+
# modify the domain of an account behalf of alice's account.
63+
permissions=[
64+
Permission(permission_value=TransactionType.PAYMENT),
65+
Permission(permission_value=GranularPermission.ACCOUNT_DOMAIN_SET),
66+
],
67+
)
68+
response = await sign_and_reliable_submission_async(
69+
delegate_set, alice, client, check_fee=False
70+
)
71+
self.assertEqual(response.status, ResponseStatus.SUCCESS)
72+
self.assertEqual(response.result["engine_result"], "tesSUCCESS")
73+
74+
# Use the bob's account to execute a transaction on behalf of alice
75+
payment = Payment(
76+
account=alice.address,
77+
amount=xrp_to_drops(1),
78+
destination=carol.address,
79+
delegate=bob.address,
80+
)
81+
response = await sign_and_reliable_submission_async(
82+
payment, bob, client, check_fee=False
83+
)
84+
self.assertEqual(response.status, ResponseStatus.SUCCESS)
85+
self.assertEqual(response.result["engine_result"], "tesSUCCESS")
86+
87+
# Validate that the transaction was signed by bob
88+
self.assertEqual(response.result["tx_json"]["Account"], alice.address)
89+
self.assertEqual(response.result["tx_json"]["Delegate"], bob.address)
90+
self.assertEqual(response.result["tx_json"]["SigningPubKey"], bob.public_key)
91+
92+
# Use the bob's account to execute a transaction on behalf of alice
93+
account_set = AccountSet(
94+
account=alice.address,
95+
delegate=bob.address,
96+
email_hash="10000000002000000000300000000012",
97+
)
98+
response = await sign_and_reliable_submission_async(
99+
account_set, bob, client, check_fee=False
100+
)
101+
self.assertEqual(response.status, ResponseStatus.SUCCESS)
102+
self.assertEqual(response.result["engine_result"], "tecNO_PERMISSION")
103+
104+
# test ledger entry
105+
ledger_entry_response = await client.request(
106+
LedgerEntry(
107+
delegate=Delegate(
108+
account=alice.address,
109+
authorize=bob.address,
110+
),
111+
)
112+
)
113+
self.assertTrue(ledger_entry_response.is_successful())
114+
self.assertEqual(
115+
ledger_entry_response.result["node"]["LedgerEntryType"],
116+
"Delegate",
117+
)
118+
self.assertEqual(ledger_entry_response.result["node"]["Account"], alice.address)
119+
self.assertEqual(ledger_entry_response.result["node"]["Authorize"], bob.address)
120+
self.assertEqual(len(ledger_entry_response.result["node"]["Permissions"]), 2)
121+
122+
@test_async_and_sync(globals())
123+
async def test_fetch_delegate_account_objects(self, client):
124+
# Note: Using WALLET, DESTINATION accounts could pollute the test results
125+
alice = Wallet.create()
126+
await fund_wallet_async(alice)
127+
bob = Wallet.create()
128+
await fund_wallet_async(bob)
129+
130+
delegate_set = DelegateSet(
131+
account=alice.address,
132+
authorize=bob.address,
133+
# Authorize bob's account to execute Payment transactions
134+
# and authorize a trustline on behalf of alice's account.
135+
permissions=[
136+
Permission(permission_value=TransactionType.PAYMENT),
137+
Permission(permission_value=GranularPermission.TRUSTLINE_AUTHORIZE),
138+
],
139+
)
140+
response = await sign_and_reliable_submission_async(
141+
delegate_set, alice, client, check_fee=False
142+
)
143+
144+
self.assertEqual(response.status, ResponseStatus.SUCCESS)
145+
self.assertEqual(response.result["engine_result"], "tesSUCCESS")
146+
147+
account_objects_response = await client.request(
148+
AccountObjects(account=alice.address, type=AccountObjectType.DELEGATE)
149+
)
150+
151+
granted_permission = {
152+
obj["Permission"]["PermissionValue"]
153+
for obj in account_objects_response.result["account_objects"][0][
154+
"Permissions"
155+
]
156+
}
157+
158+
self.assertEqual(len(granted_permission), 2)
159+
self.assertTrue(TransactionType.PAYMENT.value in granted_permission)
160+
self.assertTrue(
161+
GranularPermission.TRUSTLINE_AUTHORIZE.value in granted_permission
162+
)

tests/unit/core/binarycodec/fixtures/data/codec-fixtures.json

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4839,6 +4839,51 @@
48394839
"SigningPubKey": "ED7453D2572A2104E7B266A45888C53F503CEB1F11DC4BB3710EB2995238EC65B8",
48404840
"TxnSignature": "BC2F6E76969E3747E9BDE183C97573B086212F09D5387460E6EE2F32953E85EAEB9618FBBEF077276E30E59D619FCF7C7BDCDDDD9EB94D7CE1DD5CE9246B2107"
48414841
}
4842+
},
4843+
{
4844+
"binary": "120007220000000024000195F964400000170A53AC2065D5460561EC9DE000000000000000000000000000494C53000000000092D705968936C419CE614BF264B5EEB1CEA47FF468400000000000000A7321028472865AF4CB32AA285834B57576B7290AA8C31B459047DB27E16F418D6A71667447304502202ABE08D5E78D1E74A4C18F2714F64E87B8BD57444AFA5733109EB3C077077520022100DB335EE97386E4C0591CAC024D50E9230D8F171EEB901B5E5E4BD6D1E0AEF98C811439408A69F0895E62149CFCC006FB89FA7D1E6E5D",
4845+
"json": {
4846+
"Account": "raD5qJMAShLeHZXf9wjUmo6vRK4arj9cF3",
4847+
"Fee": "10",
4848+
"Flags": 0,
4849+
"Sequence": 103929,
4850+
"SigningPubKey":
4851+
"028472865AF4CB32AA285834B57576B7290AA8C31B459047DB27E16F418D6A7166",
4852+
"TakerGets": {
4853+
"value": "1694.768",
4854+
"currency": "ILS",
4855+
"issuer": "rNPRNzBB92BVpAhhZr4iXDTveCgV5Pofm9"
4856+
},
4857+
"TakerPays": "98957503520",
4858+
"TransactionType": "OfferCreate",
4859+
"TxnSignature": "304502202ABE08D5E78D1E74A4C18F2714F64E87B8BD57444AFA5733109EB3C077077520022100DB335EE97386E4C0591CAC024D50E9230D8F171EEB901B5E5E4BD6D1E0AEF98C"
4860+
}
4861+
},
4862+
{
4863+
"binary": "120040210000F7E0228000000024000009186840000000000000C87321ED510865F867CDFCB944D435812ACF23D231E5C14534B146BCE46A2F794D198B777440D05A89D0B489DEC1CECBE0D33BA656C929CDCCC75D4D41B282B378544975B87A70C3E42147D980D1F6E2E4DC6316C99D7E2D4F6335F147C71C0DAA0D6516150D8114DB9157872FA63FAF7432CD300911A43B981B85A28514EBA79C385B47C50D52445DF2676EEC0231F732CEF01DEF203400000001E1EF203400000015E1F1",
4864+
"json": {
4865+
"Account": "rMryaYXZMchTWBovAzEsMzid9FUwmrmcH7",
4866+
"Authorize": "r4Vp2qvKR59guHDn4Yzw9owTzDVnt6TJZA",
4867+
"Fee": "200",
4868+
"Flags": 2147483648,
4869+
"NetworkID": 63456,
4870+
"Permissions": [
4871+
{
4872+
"Permission": {
4873+
"PermissionValue": "Payment"
4874+
}
4875+
},
4876+
{
4877+
"Permission": {
4878+
"PermissionValue": "TrustSet"
4879+
}
4880+
}
4881+
],
4882+
"Sequence": 2328,
4883+
"SigningPubKey": "ED510865F867CDFCB944D435812ACF23D231E5C14534B146BCE46A2F794D198B77",
4884+
"TransactionType": "DelegateSet",
4885+
"TxnSignature": "D05A89D0B489DEC1CECBE0D33BA656C929CDCCC75D4D41B282B378544975B87A70C3E42147D980D1F6E2E4DC6316C99D7E2D4F6335F147C71C0DAA0D6516150D"
4886+
}
48424887
}
48434888
],
48444889
"ledgerData": [{
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
from unittest import TestCase
2+
3+
from xrpl.models.exceptions import XRPLModelException
4+
from xrpl.models.transactions import DelegateSet
5+
from xrpl.models.transactions.delegate_set import (
6+
PERMISSIONS_MAX_LENGTH,
7+
GranularPermission,
8+
Permission,
9+
)
10+
from xrpl.models.transactions.types import TransactionType
11+
12+
_ACCOUNT = "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ"
13+
_DELEGATED_ACCOUNT = "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW"
14+
_MORE_THAN_10_PERMISSIONS = [
15+
GranularPermission.PAYMENT_MINT,
16+
GranularPermission.ACCOUNT_MESSAGE_KEY_SET,
17+
GranularPermission.ACCOUNT_TICK_SIZE_SET,
18+
GranularPermission.ACCOUNT_DOMAIN_SET,
19+
TransactionType.PAYMENT,
20+
TransactionType.AMM_CLAWBACK,
21+
TransactionType.AMM_BID,
22+
TransactionType.ORACLE_DELETE,
23+
TransactionType.MPTOKEN_AUTHORIZE,
24+
TransactionType.MPTOKEN_ISSUANCE_DESTROY,
25+
TransactionType.CREDENTIAL_ACCEPT,
26+
]
27+
28+
29+
class TestDelegateSet(TestCase):
30+
def test_delegate_set(self):
31+
tx = DelegateSet(
32+
account=_ACCOUNT,
33+
authorize=_DELEGATED_ACCOUNT,
34+
permissions=[
35+
Permission(permission_value=GranularPermission.TRUSTLINE_AUTHORIZE),
36+
Permission(permission_value=TransactionType.PAYMENT),
37+
],
38+
)
39+
self.assertTrue(tx.is_valid())
40+
41+
def test_delegate_set_granular_permission(self):
42+
tx = DelegateSet(
43+
account=_ACCOUNT,
44+
authorize=_DELEGATED_ACCOUNT,
45+
permissions=[Permission(permission_value=GranularPermission.PAYMENT_MINT)],
46+
)
47+
self.assertTrue(tx.is_valid())
48+
49+
def test_long_permissions_list(self):
50+
with self.assertRaises(XRPLModelException) as error:
51+
DelegateSet(
52+
account=_ACCOUNT,
53+
authorize=_DELEGATED_ACCOUNT,
54+
permissions=[
55+
Permission(permission_value=_MORE_THAN_10_PERMISSIONS[i])
56+
for i in range(len(_MORE_THAN_10_PERMISSIONS))
57+
],
58+
)
59+
self.assertEqual(
60+
error.exception.args[0],
61+
"{'permissions': 'Length of `permissions` list is greater than "
62+
+ str(PERMISSIONS_MAX_LENGTH)
63+
+ ".'}",
64+
)
65+
66+
def test_duplicate_permission_value(self):
67+
with self.assertRaises(XRPLModelException) as error:
68+
DelegateSet(
69+
account=_ACCOUNT,
70+
authorize=_DELEGATED_ACCOUNT,
71+
permissions=[
72+
Permission(permission_value=TransactionType.ORACLE_DELETE),
73+
Permission(permission_value=TransactionType.ORACLE_DELETE),
74+
],
75+
)
76+
self.assertEqual(
77+
error.exception.args[0],
78+
"{'permissions': 'Duplicate permission value in `permissions` list.'}",
79+
)
80+
81+
def test_account_and_delegate_are_the_same(self):
82+
with self.assertRaises(XRPLModelException) as error:
83+
DelegateSet(
84+
account=_ACCOUNT,
85+
authorize=_ACCOUNT,
86+
permissions=[
87+
Permission(
88+
permission_value=GranularPermission.MPTOKEN_ISSUANCE_LOCK
89+
),
90+
],
91+
)
92+
self.assertEqual(
93+
error.exception.args[0],
94+
"{'account_addresses': 'Field `authorize` and `account` must be different."
95+
+ "'}",
96+
)
97+
98+
def test_non_delegatable_transactions(self):
99+
with self.assertRaises(XRPLModelException) as error:
100+
DelegateSet(
101+
account=_ACCOUNT,
102+
authorize=_DELEGATED_ACCOUNT,
103+
permissions=[
104+
Permission(
105+
permission_value=GranularPermission.MPTOKEN_ISSUANCE_LOCK
106+
),
107+
Permission(permission_value=TransactionType.ACCOUNT_DELETE),
108+
],
109+
)
110+
self.assertEqual(
111+
error.exception.args[0],
112+
"{'permissions': \"Non-delegatable transactions found in `permissions` "
113+
"list: {<TransactionType.ACCOUNT_DELETE: 'AccountDelete'>}.\"}",
114+
)

tests/unit/models/transactions/test_transaction.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,31 @@ def test_payment_txn_API_identical_amount_and_deliver_max(self):
223223

224224
payment_txn = Payment.from_xrpl(payment_tx_json)
225225
self.assertEqual(delivered_amount, payment_txn.to_dict()["amount"])
226+
227+
def test_duplicate_account_and_delegate_account(self):
228+
with self.assertRaises(XRPLModelException) as err:
229+
Transaction(
230+
account=_ACCOUNT,
231+
delegate=_ACCOUNT,
232+
transaction_type=TransactionType.PAYMENT,
233+
)
234+
235+
self.assertEqual(
236+
err.exception.args[0],
237+
"{'delegate': 'Account and delegate addresses cannot be the same'}",
238+
)
239+
240+
def test_payment_with_delegate_account(self):
241+
payment_tx_json = {
242+
"Account": "rGWTUVmm1fB5QUjMYn8KfnyrFNgDiD9H9e",
243+
"Destination": "rw71Qs1UYQrSQ9hSgRohqNNQcyjCCfffkQ",
244+
"Delegate": "rJ73aumLPTQQmy5wnGhvrogqf5DDhjuzc9",
245+
"TransactionType": "Payment",
246+
"Amount": "1000000",
247+
"Fee": "15",
248+
"Flags": 0,
249+
"Sequence": 144,
250+
"LastLedgerSequence": 6220218,
251+
}
252+
payment_txn = Payment.from_xrpl(payment_tx_json)
253+
self.assertTrue(payment_txn.is_valid())

xrpl/core/binarycodec/definitions/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
get_field_name_from_header,
77
get_ledger_entry_type_code,
88
get_ledger_entry_type_name,
9+
get_permission_value_type_code,
10+
get_permission_value_type_name,
911
get_transaction_result_code,
1012
get_transaction_result_name,
1113
get_transaction_type_code,
@@ -30,4 +32,6 @@
3032
"get_transaction_result_name",
3133
"get_transaction_type_code",
3234
"get_transaction_type_name",
35+
"get_permission_value_type_code",
36+
"get_permission_value_type_name",
3337
]

0 commit comments

Comments
 (0)