Skip to content

Commit aaa00ea

Browse files
ckeshavamvadaricoderabbitai[bot]khancode
authored
Credentials (#759)
* Update definitions.json -- generated from the tip of rippled codebase at commit ea8e77ffec065cf1a8d1cd4517f9cebdab27cc17 Explicity specify featureCredentials inside the conf file. This enables the features inside the genesis ledger * Specify the CredentialCreate transaction * Files relevant to the CredentialAccept transaction * Files pertaining to the CredentialDelete transaction Refactor common elements within Credential-related transactions * Files pertaining to DepositPreauth transaction are included in this commit Deposit_preauth: array length checks on the authcreds and unauthcreds fields * FIX: Update deposit_preauth integration tests to validate the transaction result code * Include account_objects RPC call to verify that cred ledger-object is successfully deleted Updates to Payment transaction model Update AccountDelete transaction model with Credential ID array Update EscrowFinish txn model with CredentialIDs Updates to the PaymentChannelClaim txn model -- Include new unit test file * Updates to DepositAuthorized RPC --------- Co-authored-by: Mayukha Vadari <mvadari@gmail.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Omar Khan <khancodegt@gmail.com>
1 parent b66b14e commit aaa00ea

32 files changed

+1354
-33
lines changed

.ci-config/rippled.cfg

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,11 @@ PriceOracle
182182
fixEmptyDID
183183
fixXChainRewardRounding
184184
fixPreviousTxnID
185-
# 2.3.0-rc1 Amendments
185+
# 2.3.0 Amendments
186186
fixAMMv1_1
187+
fixAMMv1_2
188+
AMMClawback
189+
InvariantsV1_1
187190
Credentials
188191
NFTokenMintOffer
189192
MPTokensV1

.github/workflows/integration_test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Integration test
22

33
env:
44
POETRY_VERSION: 1.8.3
5-
RIPPLED_DOCKER_IMAGE: rippleci/rippled:2.3.0-rc1
5+
RIPPLED_DOCKER_IMAGE: rippleci/rippled:2.3.0
66

77
on:
88
push:

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111
- Support for the Multi-Purpose Tokens (MPT) amendment (XLS-33)
1212
- Add `include_deleted` to ledger_entry request
13+
- Add support for XLS-70d (Credentials)
1314

1415
### BREAKING CHANGE:
1516
- Remove Python 3.7 support to fix dependency installation and use 3.8 as new default.
@@ -92,7 +93,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9293
## [2.0.0] - 2023-07-05
9394
### BREAKING CHANGE
9495
- The default signing algorithm in the `Wallet` was changed from secp256k1 to ed25519
95-
-
9696
### Added:
9797
- Wallet support for regular key compatibility
9898
- Added new ways of wallet generation: `from_seed`, `from_secret`, `from_entropy`, `from_secret_numbers`

CONTRIBUTING.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,17 +90,17 @@ poetry run poe test_unit
9090
To run integration tests, you'll need a standalone rippled node running with WS port `6006` and JSON RPC port `5005`. You can run a docker container for this:
9191

9292
```bash
93-
docker run -p 5005:5005 -p 6006:6006 --interactive -t --volume $PWD/.ci-config:/opt/ripple/etc/ --platform linux/amd64 rippleci/rippled:2.2.0-b3 /opt/ripple/bin/rippled -a --conf /opt/ripple/etc/rippled.cfg
93+
docker run -dit -p 5005:5005 -p 6006:6006 --volume $PWD/.ci-config/:/etc/opt/ripple/ --entrypoint bash rippleci/rippled:develop -c 'rippled -a'
9494
```
9595

9696
Breaking down the command:
9797
* `docker run -p 5005:5005 -p 6006:6006` starts a Docker container with an open port for admin JsonRPC and WebSocket requests.
98-
* `--interactive` allows you to interact with the container.
98+
* `-it` allows you to interact with the container.
99+
* `-d` runs the docker container in detached mode. The container will run in the background and developer gets back control of the terminal
99100
* `-t` starts a terminal in the container for you to send commands to.
100-
* `--volume $PWD/.ci-config:/config/` identifies the `rippled.cfg` and `validators.txt` to import. It must be an absolute path, so we use `$PWD` instead of `./`.
101-
* `xrpllabsofficial/xrpld:1.12.0` is an image that is regularly updated with the latest `rippled` releases and can be found here: https://github.com/WietseWind/docker-rippled
101+
* `--volume $PWD/.ci-config:/etc/opt/ripple/` mounts the directories as indicated. It must be an absolute path, so we use `$PWD` instead of `./`. `rippled` software searches the location `/etc/opt/ripple/` (default behavior) for the config files. Hence there is no need to explicitly specify the config-file path.
102+
* `rippleci/rippled:develop` is an image that is regularly updated with the latest build of the `develop` branch of `rippled`.
102103
* `-a` starts `rippled` in standalone mode
103-
* `--start` signals to start `rippled` with the specified amendments in `rippled.cfg` enabled immediately instead of voting for 2 weeks on them.
104104

105105
Then to actually run the tests, run the command:
106106

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
from tests.integration.integration_test_case import IntegrationTestCase
2+
from tests.integration.it_utils import (
3+
sign_and_reliable_submission_async,
4+
test_async_and_sync,
5+
)
6+
from tests.integration.reusable_values import DESTINATION, WALLET
7+
from xrpl.models import AccountObjects, AccountObjectType
8+
from xrpl.models.requests.ledger_entry import Credential, LedgerEntry
9+
from xrpl.models.response import ResponseStatus
10+
from xrpl.models.transactions.credential_accept import CredentialAccept
11+
from xrpl.models.transactions.credential_create import CredentialCreate
12+
from xrpl.models.transactions.credential_delete import CredentialDelete
13+
from xrpl.utils import str_to_hex
14+
15+
_URI = "www.my-id.com/username"
16+
17+
18+
def is_cred_object_present(
19+
result: dict, issuer: str, subject: str, cred_type: str
20+
) -> bool:
21+
"""
22+
Args:
23+
result: JSON response from account_objects RPC
24+
issuer: Address of the credential issuer
25+
subject: Address of the credential subject
26+
cred_type: Type of the credential
27+
28+
Returns:
29+
bool: True if credential exists, False otherwise
30+
"""
31+
32+
for val in result["account_objects"]:
33+
if (
34+
val["Issuer"] == issuer
35+
and val["Subject"] == subject
36+
and val["CredentialType"] == cred_type
37+
):
38+
return True
39+
40+
return False
41+
42+
43+
class TestCredentials(IntegrationTestCase):
44+
@test_async_and_sync(globals())
45+
async def test_valid(self, client):
46+
# Define entities helpful for Credential lifecycle
47+
_ISSUER = WALLET.address
48+
_SUBJECT = DESTINATION.address
49+
_SUBJECT_WALLET = DESTINATION
50+
51+
# Disambiguate the sync/async, json/websocket tests with different
52+
# credential type values -- this avoids tecDUPLICATE error
53+
# self.value is defined inside the above decorator
54+
cred_type = str_to_hex("Passport_" + str(self.value))
55+
56+
tx = CredentialCreate(
57+
account=_ISSUER,
58+
subject=_SUBJECT,
59+
credential_type=cred_type,
60+
uri=str_to_hex(_URI),
61+
)
62+
response = await sign_and_reliable_submission_async(tx, WALLET, client)
63+
self.assertEqual(response.status, ResponseStatus.SUCCESS)
64+
self.assertEqual(response.result["engine_result"], "tesSUCCESS")
65+
66+
# Use the LedgerEntry RPC to validate the creation of the credential object
67+
ledger_entry_response = await client.request(
68+
LedgerEntry(
69+
credential=Credential(
70+
subject=_SUBJECT, issuer=_ISSUER, credential_type=cred_type
71+
)
72+
)
73+
)
74+
75+
self.assertEqual(ledger_entry_response.status, ResponseStatus.SUCCESS)
76+
77+
# Execute the CredentialAccept transaction on the above Credential ledger object
78+
tx = CredentialAccept(
79+
issuer=_ISSUER, account=_SUBJECT, credential_type=cred_type
80+
)
81+
# CredentialAccept transaction is submitted by SUBJECT
82+
response = await sign_and_reliable_submission_async(tx, _SUBJECT_WALLET, client)
83+
self.assertEqual(response.status, ResponseStatus.SUCCESS)
84+
self.assertEqual(response.result["engine_result"], "tesSUCCESS")
85+
86+
# Execute the CredentialDelete transaction
87+
# Subject initiates the deletion of the Credential ledger object
88+
tx = CredentialDelete(
89+
issuer=_ISSUER, account=_SUBJECT, credential_type=cred_type
90+
)
91+
92+
response = await sign_and_reliable_submission_async(tx, _SUBJECT_WALLET, client)
93+
self.assertEqual(response.status, ResponseStatus.SUCCESS)
94+
self.assertEqual(response.result["engine_result"], "tesSUCCESS")
95+
96+
# The credential ledger object must be deleted from both the Issuer and Subject
97+
# account's directory pages
98+
account_objects_response = await client.request(
99+
AccountObjects(account=_ISSUER, type=AccountObjectType.CREDENTIAL)
100+
)
101+
self.assertFalse(
102+
is_cred_object_present(
103+
account_objects_response.result,
104+
issuer=_ISSUER,
105+
subject=_SUBJECT,
106+
cred_type=cred_type,
107+
)
108+
)
109+
110+
# Verify that the Credential object has been deleted from the Subject's
111+
# directory page as well
112+
account_objects_response = await client.request(
113+
AccountObjects(account=_SUBJECT, type=AccountObjectType.CREDENTIAL)
114+
)
115+
self.assertFalse(
116+
is_cred_object_present(
117+
account_objects_response.result,
118+
issuer=_ISSUER,
119+
subject=_SUBJECT,
120+
cred_type=cred_type,
121+
)
122+
)

tests/integration/transactions/test_deposit_preauth.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,65 @@
33
sign_and_reliable_submission_async,
44
test_async_and_sync,
55
)
6-
from tests.integration.reusable_values import WALLET
6+
from tests.integration.reusable_values import DESTINATION, WALLET
77
from xrpl.models.response import ResponseStatus
88
from xrpl.models.transactions import DepositPreauth
9+
from xrpl.models.transactions.deposit_preauth import Credential
10+
from xrpl.utils import str_to_hex
911

1012
ACCOUNT = WALLET.address
11-
ADDRESS = "rEhxGqkqPPSxQ3P25J66ft5TwpzV14k2de"
1213

1314

1415
class TestDepositPreauth(IntegrationTestCase):
1516
@test_async_and_sync(globals())
16-
async def test_authorize(self, client):
17+
async def test_authorize_unauthorize_fields(self, client):
1718
deposit_preauth = DepositPreauth(
1819
account=ACCOUNT,
19-
authorize=ADDRESS,
20+
authorize=DESTINATION.address,
2021
)
2122
response = await sign_and_reliable_submission_async(
2223
deposit_preauth, WALLET, client
2324
)
2425
self.assertEqual(response.status, ResponseStatus.SUCCESS)
26+
self.assertEqual(response.result["engine_result"], "tesSUCCESS")
27+
28+
# validate the un-authorization of the same address as above
29+
deposit_preauth = DepositPreauth(
30+
account=ACCOUNT,
31+
unauthorize=DESTINATION.address,
32+
)
33+
response = await sign_and_reliable_submission_async(
34+
deposit_preauth, WALLET, client
35+
)
36+
self.assertEqual(response.status, ResponseStatus.SUCCESS)
37+
self.assertEqual(response.result["engine_result"], "tesSUCCESS")
2538

2639
@test_async_and_sync(globals())
27-
async def test_unauthorize(self, client):
40+
async def test_credentials_array_input_fields(self, client):
41+
sample_credentials = [
42+
Credential(
43+
issuer=DESTINATION.address, credential_type=str_to_hex("SampleCredType")
44+
)
45+
]
46+
47+
# Test the authorize_credentials input field
48+
deposit_preauth = DepositPreauth(
49+
account=ACCOUNT,
50+
authorize_credentials=sample_credentials,
51+
)
52+
response = await sign_and_reliable_submission_async(
53+
deposit_preauth, WALLET, client
54+
)
55+
self.assertEqual(response.status, ResponseStatus.SUCCESS)
56+
self.assertEqual(response.result["engine_result"], "tesSUCCESS")
57+
58+
# Test the unauthorize_credentials input field
2859
deposit_preauth = DepositPreauth(
2960
account=ACCOUNT,
30-
unauthorize=ADDRESS,
61+
unauthorize_credentials=sample_credentials,
3162
)
3263
response = await sign_and_reliable_submission_async(
3364
deposit_preauth, WALLET, client
3465
)
3566
self.assertEqual(response.status, ResponseStatus.SUCCESS)
67+
self.assertEqual(response.result["engine_result"], "tesSUCCESS")
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from unittest import TestCase
2+
3+
from xrpl.models import DepositAuthorized
4+
5+
6+
class TestDepositAuthorized(TestCase):
7+
def test_valid(self):
8+
req = DepositAuthorized(
9+
source_account="srcAccount",
10+
destination_account="dstAccount",
11+
credentials=[
12+
"EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66A"
13+
],
14+
)
15+
self.assertTrue(req.is_valid())

tests/unit/models/requests/test_ledger_entry.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from xrpl.models import XRP, LedgerEntry, XChainBridge
44
from xrpl.models.exceptions import XRPLModelException
5-
from xrpl.models.requests.ledger_entry import MPToken, Oracle, RippleState
5+
from xrpl.models.requests.ledger_entry import Credential, MPToken, Oracle, RippleState
66

77

88
class TestLedgerEntry(TestCase):
@@ -18,6 +18,23 @@ def test_has_only_account_root_is_valid(self):
1818
)
1919
self.assertTrue(req.is_valid())
2020

21+
def test_query_credential_object_id(self):
22+
self.assertTrue(
23+
LedgerEntry(
24+
credential="EA85602C1B41F6F1F5E83C0E6B87142FB8957B"
25+
"D209469E4CC347BA2D0C26F66A"
26+
).is_valid()
27+
)
28+
29+
def test_query_credential_by_object_params(self):
30+
self.assertTrue(
31+
LedgerEntry(
32+
credential=Credential(
33+
subject="rSubject", issuer="rIssuer", credential_type="ABCDE"
34+
)
35+
).is_valid()
36+
)
37+
2138
def test_has_only_directory_is_valid(self):
2239
req = LedgerEntry(
2340
directory="hello",
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from unittest import TestCase
2+
3+
from xrpl.models.exceptions import XRPLModelException
4+
from xrpl.models.transactions import AccountDelete
5+
from xrpl.models.utils import MAX_CREDENTIAL_ARRAY_LENGTH
6+
7+
_ACCOUNT = "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ"
8+
_DESTINATION = "rf7HPydP4ihkFkSRHWFq34b4SXRc7GvPCR"
9+
10+
11+
class TestAccountDelete(TestCase):
12+
def test_creds_list_too_long(self):
13+
"""Test that AccountDelete raises exception when credential_ids exceeds max
14+
length."""
15+
with self.assertRaises(XRPLModelException) as err:
16+
AccountDelete(
17+
account=_ACCOUNT,
18+
destination=_DESTINATION,
19+
credential_ids=[
20+
"credential_index_" + str(i)
21+
for i in range(MAX_CREDENTIAL_ARRAY_LENGTH + 1)
22+
],
23+
)
24+
25+
self.assertEqual(
26+
err.exception.args[0],
27+
"{'credential_ids': 'CredentialIDs list cannot exceed "
28+
+ str(MAX_CREDENTIAL_ARRAY_LENGTH)
29+
+ " elements.'}",
30+
)
31+
32+
def test_creds_list_empty(self):
33+
with self.assertRaises(XRPLModelException) as err:
34+
AccountDelete(
35+
account=_ACCOUNT,
36+
destination=_DESTINATION,
37+
credential_ids=[],
38+
)
39+
self.assertEqual(
40+
err.exception.args[0],
41+
"{'credential_ids': 'CredentialIDs list cannot be empty.'}",
42+
)
43+
44+
def test_creds_list_duplicates(self):
45+
with self.assertRaises(XRPLModelException) as err:
46+
AccountDelete(
47+
account=_ACCOUNT,
48+
destination=_DESTINATION,
49+
credential_ids=["credential_index" for _ in range(5)],
50+
)
51+
self.assertEqual(
52+
err.exception.args[0],
53+
"{'credential_ids_duplicates': 'CredentialIDs list cannot contain duplicate"
54+
+ " values.'}",
55+
)
56+
57+
def test_valid_account_delete_txn(self):
58+
tx = AccountDelete(
59+
account=_ACCOUNT,
60+
destination=_DESTINATION,
61+
credential_ids=[
62+
"EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66A"
63+
],
64+
)
65+
self.assertTrue(tx.is_valid())

0 commit comments

Comments
 (0)