Skip to content

Commit

Permalink
Credentials (#759)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Omar Khan <[email protected]>
  • Loading branch information
4 people authored Dec 21, 2024
1 parent b66b14e commit aaa00ea
Show file tree
Hide file tree
Showing 32 changed files with 1,354 additions and 33 deletions.
5 changes: 4 additions & 1 deletion .ci-config/rippled.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,11 @@ PriceOracle
fixEmptyDID
fixXChainRewardRounding
fixPreviousTxnID
# 2.3.0-rc1 Amendments
# 2.3.0 Amendments
fixAMMv1_1
fixAMMv1_2
AMMClawback
InvariantsV1_1
Credentials
NFTokenMintOffer
MPTokensV1
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/integration_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Integration test

env:
POETRY_VERSION: 1.8.3
RIPPLED_DOCKER_IMAGE: rippleci/rippled:2.3.0-rc1
RIPPLED_DOCKER_IMAGE: rippleci/rippled:2.3.0

on:
push:
Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Support for the Multi-Purpose Tokens (MPT) amendment (XLS-33)
- Add `include_deleted` to ledger_entry request
- Add support for XLS-70d (Credentials)

### BREAKING CHANGE:
- Remove Python 3.7 support to fix dependency installation and use 3.8 as new default.
Expand Down Expand Up @@ -92,7 +93,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [2.0.0] - 2023-07-05
### BREAKING CHANGE
- The default signing algorithm in the `Wallet` was changed from secp256k1 to ed25519
-
### Added:
- Wallet support for regular key compatibility
- Added new ways of wallet generation: `from_seed`, `from_secret`, `from_entropy`, `from_secret_numbers`
Expand Down
10 changes: 5 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,17 @@ poetry run poe test_unit
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:

```bash
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
docker run -dit -p 5005:5005 -p 6006:6006 --volume $PWD/.ci-config/:/etc/opt/ripple/ --entrypoint bash rippleci/rippled:develop -c 'rippled -a'
```

Breaking down the command:
* `docker run -p 5005:5005 -p 6006:6006` starts a Docker container with an open port for admin JsonRPC and WebSocket requests.
* `--interactive` allows you to interact with the container.
* `-it` allows you to interact with the container.
* `-d` runs the docker container in detached mode. The container will run in the background and developer gets back control of the terminal
* `-t` starts a terminal in the container for you to send commands to.
* `--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 `./`.
* `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
* `--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.
* `rippleci/rippled:develop` is an image that is regularly updated with the latest build of the `develop` branch of `rippled`.
* `-a` starts `rippled` in standalone mode
* `--start` signals to start `rippled` with the specified amendments in `rippled.cfg` enabled immediately instead of voting for 2 weeks on them.

Then to actually run the tests, run the command:

Expand Down
122 changes: 122 additions & 0 deletions tests/integration/transactions/test_credential.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
from tests.integration.integration_test_case import IntegrationTestCase
from tests.integration.it_utils import (
sign_and_reliable_submission_async,
test_async_and_sync,
)
from tests.integration.reusable_values import DESTINATION, WALLET
from xrpl.models import AccountObjects, AccountObjectType
from xrpl.models.requests.ledger_entry import Credential, LedgerEntry
from xrpl.models.response import ResponseStatus
from xrpl.models.transactions.credential_accept import CredentialAccept
from xrpl.models.transactions.credential_create import CredentialCreate
from xrpl.models.transactions.credential_delete import CredentialDelete
from xrpl.utils import str_to_hex

_URI = "www.my-id.com/username"


def is_cred_object_present(
result: dict, issuer: str, subject: str, cred_type: str
) -> bool:
"""
Args:
result: JSON response from account_objects RPC
issuer: Address of the credential issuer
subject: Address of the credential subject
cred_type: Type of the credential
Returns:
bool: True if credential exists, False otherwise
"""

for val in result["account_objects"]:
if (
val["Issuer"] == issuer
and val["Subject"] == subject
and val["CredentialType"] == cred_type
):
return True

return False


class TestCredentials(IntegrationTestCase):
@test_async_and_sync(globals())
async def test_valid(self, client):
# Define entities helpful for Credential lifecycle
_ISSUER = WALLET.address
_SUBJECT = DESTINATION.address
_SUBJECT_WALLET = DESTINATION

# Disambiguate the sync/async, json/websocket tests with different
# credential type values -- this avoids tecDUPLICATE error
# self.value is defined inside the above decorator
cred_type = str_to_hex("Passport_" + str(self.value))

tx = CredentialCreate(
account=_ISSUER,
subject=_SUBJECT,
credential_type=cred_type,
uri=str_to_hex(_URI),
)
response = await sign_and_reliable_submission_async(tx, WALLET, client)
self.assertEqual(response.status, ResponseStatus.SUCCESS)
self.assertEqual(response.result["engine_result"], "tesSUCCESS")

# Use the LedgerEntry RPC to validate the creation of the credential object
ledger_entry_response = await client.request(
LedgerEntry(
credential=Credential(
subject=_SUBJECT, issuer=_ISSUER, credential_type=cred_type
)
)
)

self.assertEqual(ledger_entry_response.status, ResponseStatus.SUCCESS)

# Execute the CredentialAccept transaction on the above Credential ledger object
tx = CredentialAccept(
issuer=_ISSUER, account=_SUBJECT, credential_type=cred_type
)
# CredentialAccept transaction is submitted by SUBJECT
response = await sign_and_reliable_submission_async(tx, _SUBJECT_WALLET, client)
self.assertEqual(response.status, ResponseStatus.SUCCESS)
self.assertEqual(response.result["engine_result"], "tesSUCCESS")

# Execute the CredentialDelete transaction
# Subject initiates the deletion of the Credential ledger object
tx = CredentialDelete(
issuer=_ISSUER, account=_SUBJECT, credential_type=cred_type
)

response = await sign_and_reliable_submission_async(tx, _SUBJECT_WALLET, client)
self.assertEqual(response.status, ResponseStatus.SUCCESS)
self.assertEqual(response.result["engine_result"], "tesSUCCESS")

# The credential ledger object must be deleted from both the Issuer and Subject
# account's directory pages
account_objects_response = await client.request(
AccountObjects(account=_ISSUER, type=AccountObjectType.CREDENTIAL)
)
self.assertFalse(
is_cred_object_present(
account_objects_response.result,
issuer=_ISSUER,
subject=_SUBJECT,
cred_type=cred_type,
)
)

# Verify that the Credential object has been deleted from the Subject's
# directory page as well
account_objects_response = await client.request(
AccountObjects(account=_SUBJECT, type=AccountObjectType.CREDENTIAL)
)
self.assertFalse(
is_cred_object_present(
account_objects_response.result,
issuer=_ISSUER,
subject=_SUBJECT,
cred_type=cred_type,
)
)
44 changes: 38 additions & 6 deletions tests/integration/transactions/test_deposit_preauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,65 @@
sign_and_reliable_submission_async,
test_async_and_sync,
)
from tests.integration.reusable_values import WALLET
from tests.integration.reusable_values import DESTINATION, WALLET
from xrpl.models.response import ResponseStatus
from xrpl.models.transactions import DepositPreauth
from xrpl.models.transactions.deposit_preauth import Credential
from xrpl.utils import str_to_hex

ACCOUNT = WALLET.address
ADDRESS = "rEhxGqkqPPSxQ3P25J66ft5TwpzV14k2de"


class TestDepositPreauth(IntegrationTestCase):
@test_async_and_sync(globals())
async def test_authorize(self, client):
async def test_authorize_unauthorize_fields(self, client):
deposit_preauth = DepositPreauth(
account=ACCOUNT,
authorize=ADDRESS,
authorize=DESTINATION.address,
)
response = await sign_and_reliable_submission_async(
deposit_preauth, WALLET, client
)
self.assertEqual(response.status, ResponseStatus.SUCCESS)
self.assertEqual(response.result["engine_result"], "tesSUCCESS")

# validate the un-authorization of the same address as above
deposit_preauth = DepositPreauth(
account=ACCOUNT,
unauthorize=DESTINATION.address,
)
response = await sign_and_reliable_submission_async(
deposit_preauth, WALLET, client
)
self.assertEqual(response.status, ResponseStatus.SUCCESS)
self.assertEqual(response.result["engine_result"], "tesSUCCESS")

@test_async_and_sync(globals())
async def test_unauthorize(self, client):
async def test_credentials_array_input_fields(self, client):
sample_credentials = [
Credential(
issuer=DESTINATION.address, credential_type=str_to_hex("SampleCredType")
)
]

# Test the authorize_credentials input field
deposit_preauth = DepositPreauth(
account=ACCOUNT,
authorize_credentials=sample_credentials,
)
response = await sign_and_reliable_submission_async(
deposit_preauth, WALLET, client
)
self.assertEqual(response.status, ResponseStatus.SUCCESS)
self.assertEqual(response.result["engine_result"], "tesSUCCESS")

# Test the unauthorize_credentials input field
deposit_preauth = DepositPreauth(
account=ACCOUNT,
unauthorize=ADDRESS,
unauthorize_credentials=sample_credentials,
)
response = await sign_and_reliable_submission_async(
deposit_preauth, WALLET, client
)
self.assertEqual(response.status, ResponseStatus.SUCCESS)
self.assertEqual(response.result["engine_result"], "tesSUCCESS")
15 changes: 15 additions & 0 deletions tests/unit/models/requests/test_deposit_authorized.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from unittest import TestCase

from xrpl.models import DepositAuthorized


class TestDepositAuthorized(TestCase):
def test_valid(self):
req = DepositAuthorized(
source_account="srcAccount",
destination_account="dstAccount",
credentials=[
"EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66A"
],
)
self.assertTrue(req.is_valid())
19 changes: 18 additions & 1 deletion tests/unit/models/requests/test_ledger_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from xrpl.models import XRP, LedgerEntry, XChainBridge
from xrpl.models.exceptions import XRPLModelException
from xrpl.models.requests.ledger_entry import MPToken, Oracle, RippleState
from xrpl.models.requests.ledger_entry import Credential, MPToken, Oracle, RippleState


class TestLedgerEntry(TestCase):
Expand All @@ -18,6 +18,23 @@ def test_has_only_account_root_is_valid(self):
)
self.assertTrue(req.is_valid())

def test_query_credential_object_id(self):
self.assertTrue(
LedgerEntry(
credential="EA85602C1B41F6F1F5E83C0E6B87142FB8957B"
"D209469E4CC347BA2D0C26F66A"
).is_valid()
)

def test_query_credential_by_object_params(self):
self.assertTrue(
LedgerEntry(
credential=Credential(
subject="rSubject", issuer="rIssuer", credential_type="ABCDE"
)
).is_valid()
)

def test_has_only_directory_is_valid(self):
req = LedgerEntry(
directory="hello",
Expand Down
65 changes: 65 additions & 0 deletions tests/unit/models/transactions/test_account_delete.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from unittest import TestCase

from xrpl.models.exceptions import XRPLModelException
from xrpl.models.transactions import AccountDelete
from xrpl.models.utils import MAX_CREDENTIAL_ARRAY_LENGTH

_ACCOUNT = "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ"
_DESTINATION = "rf7HPydP4ihkFkSRHWFq34b4SXRc7GvPCR"


class TestAccountDelete(TestCase):
def test_creds_list_too_long(self):
"""Test that AccountDelete raises exception when credential_ids exceeds max
length."""
with self.assertRaises(XRPLModelException) as err:
AccountDelete(
account=_ACCOUNT,
destination=_DESTINATION,
credential_ids=[
"credential_index_" + str(i)
for i in range(MAX_CREDENTIAL_ARRAY_LENGTH + 1)
],
)

self.assertEqual(
err.exception.args[0],
"{'credential_ids': 'CredentialIDs list cannot exceed "
+ str(MAX_CREDENTIAL_ARRAY_LENGTH)
+ " elements.'}",
)

def test_creds_list_empty(self):
with self.assertRaises(XRPLModelException) as err:
AccountDelete(
account=_ACCOUNT,
destination=_DESTINATION,
credential_ids=[],
)
self.assertEqual(
err.exception.args[0],
"{'credential_ids': 'CredentialIDs list cannot be empty.'}",
)

def test_creds_list_duplicates(self):
with self.assertRaises(XRPLModelException) as err:
AccountDelete(
account=_ACCOUNT,
destination=_DESTINATION,
credential_ids=["credential_index" for _ in range(5)],
)
self.assertEqual(
err.exception.args[0],
"{'credential_ids_duplicates': 'CredentialIDs list cannot contain duplicate"
+ " values.'}",
)

def test_valid_account_delete_txn(self):
tx = AccountDelete(
account=_ACCOUNT,
destination=_DESTINATION,
credential_ids=[
"EA85602C1B41F6F1F5E83C0E6B87142FB8957BD209469E4CC347BA2D0C26F66A"
],
)
self.assertTrue(tx.is_valid())
Loading

0 comments on commit aaa00ea

Please sign in to comment.