Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Verify credential tutorial (Python) #3030

Merged
merged 2 commits into from
Mar 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions _code-samples/verify-credential/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Verify Credential

Check whether a specific account holds a specific credential, and the credential is currently valid.
33 changes: 33 additions & 0 deletions _code-samples/verify-credential/py/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Verify Credential - Python sample code

Verifies that a specific credential exists on the XRPL and is valid.

Quick install & usage:

```sh
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
./verify_credential.py
```

`verify_credential.py` can also be used as a commandline utility. Full usage statement:

```sh
$ ./verify_credential.py -h
usage: verify_credential.py [-h] [-b] [-n {devnet,testnet,mainnet}]
[issuer] [subject] [credential_type]

Verify an XRPL credential

positional arguments:
issuer Credential issuer address as base58.
subject Credential subject (holder) address as base58.
credential_type Credential type as string

options:
-h, --help show this help message and exit
-b, --binary Use binary (hexadecimal) for credential_type
-n, --network {devnet,testnet,mainnet}
Use the specified network for lookup
```
1 change: 1 addition & 0 deletions _code-samples/verify-credential/py/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
xrpl-py==4.0.0
167 changes: 167 additions & 0 deletions _code-samples/verify-credential/py/verify_credential.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
#!/usr/bin/env python

import argparse
import logging
import sys
from binascii import hexlify
from re import match

from xrpl.clients import JsonRpcClient
from xrpl.models.requests import LedgerEntry, Ledger
from xrpl.utils import ripple_time_to_datetime

# Set up logging --------------------------------------------------------------
# Use WARNING by default in case verify_credential is called from elsewhere.
logger = logging.getLogger("verify_credential")
logger.setLevel(logging.WARNING)
logger.addHandler(logging.StreamHandler(sys.stderr))

# Define an error to throw when XRPL lookup fails unexpectedly ----------------
class XRPLLookupError(Exception):
def __init__(self, xrpl_response):
self.body = xrpl_response.result

# Main function ---------------------------------------------------------------
def verify_credential(client:JsonRpcClient,
issuer:str,
subject:str,
credential_type:str="",
credential_type_hex:str=""):
"""
Check whether an XRPL account holds a specified credential,
as of the most recently validated ledger.

Paramters:
client - JsonRpcClient for the XRPL network to use.
issuer - Address of the credential issuer, in base58
subject - Address of the credential holder/subject, in base58
credential_type - Credential type to check for as a string,
which will be encoded as UTF-8 (1-64 bytes long).
credential_type_hex - Credential type (binary) as hexadecimal.
verbose - If true, print details to stdout during lookup.
You must provide either credential_type or credential_type_hex.

Returns True if the account holds the specified, valid credential.
Returns False if the credential is missing, expired, or not accepted.
"""
# Handle function inputs --------------------------------------------------
if not (credential_type or credential_type_hex):
raise ValueError("Provide a non-empty credential_type or " +
"credential_type_hex")
if credential_type and credential_type_hex:
raise ValueError("Provide either credential_type or " +
"credential_type_hex, but not both")

# Encode credential_type as uppercase hex, if needed
if credential_type:
credential_type_hex = hexlify(credential_type.encode("utf-8")
).decode("ascii")
logger.info("Encoded credential_type as hex: "+credential_type_hex.upper())
credential_type_hex = credential_type_hex.upper()

if len(credential_type_hex) % 2 or \
not match(r"[0-9A-F]{2,128}", credential_type_hex):
# Hexadecimal is always 2 chars per byte, so an odd length is invalid.
raise ValueError("credential_type_hex must be 1-64 bytes as hexadecimal.")

# Perform XRPL lookup of Credential ledger entry --------------------------
ledger_entry_request = LedgerEntry(
credential={
"subject": subject,
"issuer": issuer,
"credential_type": credential_type_hex
},
ledger_index="validated"
)
logger.info("Looking up credential...")
logger.info(ledger_entry_request.to_dict())
xrpl_response = client.request(ledger_entry_request)

if xrpl_response.status != "success":
if xrpl_response.result["error"] == "entryNotFound":
logger.info("Credential was not found")
return False
# Other errors, for example invalidly-specified addresses.
raise XRPLLookupError(xrpl_response)

credential = xrpl_response.result["node"]
logger.info("Found credential:")
logger.info(credential)

# Confirm that the credential has been accepted ---------------------------
lsfAccepted = 0x00010000
if not credential["Flags"] & lsfAccepted:
logger.info("Credential is not accepted.")
return False

# Confirm that the credential is not expired ------------------------------
if credential.get("Expiration"):
expiration_time = ripple_time_to_datetime(credential["Expiration"])
logger.info("Credential has expiration: "+expiration_time.isoformat())
logger.info("Looking up validated ledger to check for expiration.")

ledger_response = client.request(Ledger(ledger_index="validated"))
if ledger_response.status != "success":
raise XRPLLookupError(ledger_response)
close_time = ripple_time_to_datetime(
ledger_response.result["ledger"]["close_time"]
)
logger.info("Most recent validated ledger is: "+close_time.isoformat())

if close_time > expiration_time:
logger.info("Credential is expired.")
return False

# Credential has passed all checks. ---------------------------------------
logger.info("Credential is valid.")
return True

# Commandline usage -----------------------------------------------------------
if __name__=="__main__":
NETWORKS = {
# JSON-RPC URLs of public servers
"devnet": "https://s.devnet.rippletest.net:51234/",
"testnet": "https://s.altnet.rippletest.net:51234/",
"mainnet": "https://xrplcluster.com/"
}

# Parse arguments ---------------------------------------------------------
parser = argparse.ArgumentParser(description="Verify an XRPL credential")
parser.add_argument("issuer", type=str, nargs="?",
help="Credential issuer address as base58.",
default="rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS")
parser.add_argument("subject", type=str, nargs="?",
help="Credential subject (holder) address as base58.",
default="rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA")
parser.add_argument("credential_type", type=str, nargs="?",
help="Credential type as string",
default="my_credential")
parser.add_argument("-b", "--binary", action="store_true",
help="Use binary (hexadecimal) for credential_type")
parser.add_argument("-n", "--network", choices=NETWORKS.keys(),
help="Use the specified network for lookup",
default="devnet")
parser.add_argument("-q", "--quiet", action="store_true",
help="Don't print log messages.")
args = parser.parse_args()

# Call verify_credential with appropriate args ----------------------------
client = JsonRpcClient(NETWORKS[args.network])
if not args.quiet:
# Use INFO level by default when called from the commandline.
logger.setLevel(logging.INFO)

if args.binary:
result = verify_credential(client,
issuer=args.issuer,
subject=args.subject,
credential_type_hex=args.credential_type)
else:
result = verify_credential(client,
issuer=args.issuer,
subject=args.subject,
credential_type=args.credential_type)

# Return a nonzero exit code if credential verification failed. -----------
if not result:
exit(1)
12 changes: 12 additions & 0 deletions docs/tutorials/python/build-apps/credential-issuing-service.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,4 +335,16 @@ Finally, the `CredentialRequest` class inherits from the `Credential` class but

{% code-snippet file="/_code-samples/issue-credentials/py/credential_model.py" language="py" from="class CredentialRequest(Credential):" /%}

## Next Steps

Using this service as a base, you can extend the service with more features, such as:

- Security/authentication to protect API methods from unauthorized use.
- Actually checking user documents to decide if you should issue a credential.

Alternatively, you can use credentials to for various purposes, such as:

- Define a [Permissioned Domain](/docs/concepts/tokens/decentralized-exchange/permissioned-domains) that uses your credentials to grant access to features on the XRP Ledger.
- [Verify credentials](../compliance/verify-credential.md) manually to grant access to services that exist off-ledger.

{% raw-partial file="/docs/_snippets/common-links.md" /%}
13 changes: 13 additions & 0 deletions docs/tutorials/python/compliance/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
metadata:
indexPage: true
seo:
description: Transact with confidence using the XRP Ledger's suite of compliance features for following government regulations and security practices.
---
# Transact with Confidence Using Compliance Features

The XRP Ledger has a rich suite of features designed to help financial institutions of all sizes engage with DeFi technology while complying with government regulations domestically and internationally.

See the following tutorials for examples of how to put these features to work:

{% child-pages /%}
Loading