Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
373fb52
Models and unit tests for LP amendment; TODO: Integ tests are remaini…
ckeshava Sep 8, 2025
aadadbe
update integration test with LoanSet transaction
ckeshava Sep 12, 2025
1db1744
add integ tests for loan-crud operations; update changelog
ckeshava Sep 13, 2025
b062bc3
address first batch of coderabbit AI suggestions
ckeshava Sep 15, 2025
faca6ae
Merge branch 'main' into xls66d
ckeshava Sep 16, 2025
9083a84
fix linter errors
ckeshava Sep 16, 2025
c672015
update Number internal rippled type into str JSON type
ckeshava Sep 17, 2025
029d65c
add unit tests and validation for loan_broker_set txn
ckeshava Sep 18, 2025
1ddf348
loan_set validation and unit tests
ckeshava Sep 18, 2025
4e5cf35
add hex validation for data field
ckeshava Sep 18, 2025
388553d
update tests for LoanSet txn; remove start_date field
ckeshava Sep 18, 2025
31b699e
integ test for Lending Protocol with IOU
ckeshava Sep 18, 2025
e39509f
fix the errors in STIssue codec
ckeshava Sep 19, 2025
88eade6
Merge branch 'updateIssueCodec' into xls66d
ckeshava Sep 19, 2025
3b47b6f
remove debug helper method
ckeshava Sep 22, 2025
9c38b73
integ test for VaultCreate txn with MPToken
ckeshava Sep 23, 2025
f6daf47
feature: allow xrpl-py integ tests to run on XRPL Devnet; This commit…
ckeshava Sep 23, 2025
9f27a07
fix: update the order of the encoding arguments in serialization of I…
ckeshava Sep 23, 2025
d47410a
add SAV integ test with MPToken as Vault asset
ckeshava Sep 24, 2025
bd2f13a
fix: big-endian format to interpret the sequence number in MPTID
ckeshava Sep 24, 2025
fc158fb
Update tests/integration/it_utils.py
ckeshava Sep 24, 2025
2183f0a
address code rabbit suggestions
ckeshava Sep 24, 2025
3162f69
Merge branch 'updateIssueCodec' into xls66d
ckeshava Sep 24, 2025
48bd4e7
integ test: LendingProtocol Vault with MPToken asset
ckeshava Sep 24, 2025
98288b8
Merge branch 'main' into xls66d
ckeshava Sep 29, 2025
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
1 change: 1 addition & 0 deletions .ci-config/rippled.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ PermissionDelegation
PermissionedDEX
Batch
TokenEscrow
LendingProtocol

# This section can be used to simulate various FeeSettings scenarios for rippled node in standalone mode
[voting]
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [[Unreleased]]

### Added
- Support for the Lending Protocol (XLS-66d)

### Fixed

- Removed snippets files from the xrpl-py code repository. Updated the README file to point to the correct location on XRPL.org.
Expand Down
372 changes: 372 additions & 0 deletions tests/integration/transactions/test_lending_protocol.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,372 @@
from tests.integration.integration_test_case import IntegrationTestCase
from tests.integration.it_utils import (
LEDGER_ACCEPT_REQUEST,
fund_wallet_async,
sign_and_reliable_submission_async,
test_async_and_sync,
)
from xrpl.asyncio.transaction import autofill_and_sign, submit
from xrpl.core.binarycodec import encode_for_signing
from xrpl.core.keypairs.main import sign
from xrpl.models import (
AccountObjects,
AccountSet,
AccountSetAsfFlag,
LoanBrokerSet,
LoanDelete,
LoanManage,
LoanPay,
LoanSet,
Payment,
Transaction,
TrustSet,
VaultCreate,
VaultDeposit,
)
from xrpl.models.amounts import IssuedCurrencyAmount
from xrpl.models.currencies.issued_currency import IssuedCurrency
from xrpl.models.currencies.xrp import XRP
from xrpl.models.requests.account_objects import AccountObjectType
from xrpl.models.response import ResponseStatus
from xrpl.models.transactions.loan_manage import LoanManageFlag
from xrpl.models.transactions.loan_set import CounterpartySignature
from xrpl.models.transactions.vault_create import WithdrawalPolicy
from xrpl.wallet import Wallet


class TestLendingProtocolLifecycle(IntegrationTestCase):
@test_async_and_sync(
globals(), ["xrpl.transaction.autofill_and_sign", "xrpl.transaction.submit"]
)
async def test_lending_protocol_lifecycle(self, client):

loan_issuer = Wallet.create()
await fund_wallet_async(loan_issuer)

depositor_wallet = Wallet.create()
await fund_wallet_async(depositor_wallet)
borrower_wallet = Wallet.create()
await fund_wallet_async(borrower_wallet)

# Step-1: Create a vault
tx = VaultCreate(
account=loan_issuer.address,
asset=XRP(),
assets_maximum="1000",
withdrawal_policy=WithdrawalPolicy.VAULT_STRATEGY_FIRST_COME_FIRST_SERVE,
)
response = await sign_and_reliable_submission_async(tx, loan_issuer, client)
self.assertEqual(response.status, ResponseStatus.SUCCESS)
self.assertEqual(response.result["engine_result"], "tesSUCCESS")

account_objects_response = await client.request(
AccountObjects(account=loan_issuer.address, type=AccountObjectType.VAULT)
)
self.assertEqual(len(account_objects_response.result["account_objects"]), 1)
VAULT_ID = account_objects_response.result["account_objects"][0]["index"]

# Step-2: Create a loan broker
tx = LoanBrokerSet(
account=loan_issuer.address,
vault_id=VAULT_ID,
)
response = await sign_and_reliable_submission_async(tx, loan_issuer, client)
self.assertEqual(response.status, ResponseStatus.SUCCESS)
self.assertEqual(response.result["engine_result"], "tesSUCCESS")

# Step-2.1: Verify that the LoanBroker was successfully created
response = await client.request(
AccountObjects(
account=loan_issuer.address, type=AccountObjectType.LOAN_BROKER
)
)
self.assertEqual(len(response.result["account_objects"]), 1)
LOAN_BROKER_ID = response.result["account_objects"][0]["index"]

# Step-3: Deposit funds into the vault
tx = VaultDeposit(
account=depositor_wallet.address,
vault_id=VAULT_ID,
amount="100",
)
response = await sign_and_reliable_submission_async(
tx, depositor_wallet, client
)
self.assertEqual(response.status, ResponseStatus.SUCCESS)
self.assertEqual(response.result["engine_result"], "tesSUCCESS")

# Step-5: The Loan Broker and Borrower create a Loan object with a LoanSet
# transaction and the requested principal (excluding fees) is transered to
# the Borrower.

loan_issuer_signed_txn = await autofill_and_sign(
LoanSet(
account=loan_issuer.address,
loan_broker_id=LOAN_BROKER_ID,
principal_requested="100",
counterparty=borrower_wallet.address,
),
client,
loan_issuer,
)

# borrower agrees to the terms of the loan
borrower_txn_signature = sign(
encode_for_signing(loan_issuer_signed_txn.to_xrpl()),
borrower_wallet.private_key,
)

loan_issuer_and_borrower_signature = loan_issuer_signed_txn.to_dict()
loan_issuer_and_borrower_signature["counterparty_signature"] = (
CounterpartySignature(
signing_pub_key=borrower_wallet.public_key,
txn_signature=borrower_txn_signature,
)
)

response = await submit(
Transaction.from_dict(loan_issuer_and_borrower_signature),
client,
fail_hard=True,
)

self.assertEqual(response.status, ResponseStatus.SUCCESS)
self.assertEqual(response.result["engine_result"], "tesSUCCESS")

# Wait for the validation of the latest ledger
await client.request(LEDGER_ACCEPT_REQUEST)

# fetch the Loan object
response = await client.request(
AccountObjects(account=borrower_wallet.address, type=AccountObjectType.LOAN)
)
self.assertEqual(len(response.result["account_objects"]), 1)
LOAN_ID = response.result["account_objects"][0]["index"]

# Delete the Loan object
tx = LoanDelete(
account=loan_issuer.address,
loan_id=LOAN_ID,
)
response = await sign_and_reliable_submission_async(tx, loan_issuer, client)
self.assertEqual(response.status, ResponseStatus.SUCCESS)
# Loan cannot be deleted until all the remaining payments are completed
self.assertEqual(response.result["engine_result"], "tecHAS_OBLIGATIONS")

# Test the LoanManage transaction
tx = LoanManage(
account=loan_issuer.address,
loan_id=LOAN_ID,
flags=LoanManageFlag.TF_LOAN_IMPAIR,
)
response = await sign_and_reliable_submission_async(tx, loan_issuer, client)
self.assertEqual(response.status, ResponseStatus.SUCCESS)
self.assertEqual(response.result["engine_result"], "tesSUCCESS")

# Test the LoanPay transaction
tx = LoanPay(
account=borrower_wallet.address,
loan_id=LOAN_ID,
amount="100",
)
response = await sign_and_reliable_submission_async(tx, borrower_wallet, client)
self.assertEqual(response.status, ResponseStatus.SUCCESS)
self.assertEqual(response.result["engine_result"], "tesSUCCESS")

@test_async_and_sync(
globals(), ["xrpl.transaction.autofill_and_sign", "xrpl.transaction.submit"]
)
async def test_lending_protocol_lifecycle_with_iou_asset(self, client):
loan_issuer = Wallet.create()
await fund_wallet_async(loan_issuer)

depositor_wallet = Wallet.create()
await fund_wallet_async(depositor_wallet)
borrower_wallet = Wallet.create()
await fund_wallet_async(borrower_wallet)

# Step-0: Set up the relevant flags on the loan_issuer account -- This is
# a pre-requisite for a Vault to hold the Issued Currency Asset
response = await sign_and_reliable_submission_async(
AccountSet(
account=loan_issuer.classic_address,
set_flag=AccountSetAsfFlag.ASF_DEFAULT_RIPPLE,
),
loan_issuer,
)
self.assertTrue(response.is_successful())
self.assertEqual(response.result["engine_result"], "tesSUCCESS")

# Step 0.1: Set up trustlines required for the transferring the IOU token
tx = TrustSet(
account=depositor_wallet.address,
limit_amount=IssuedCurrencyAmount(
currency="USD", issuer=loan_issuer.address, value="1000"
),
)
response = await sign_and_reliable_submission_async(
tx, depositor_wallet, client
)
self.assertEqual(response.status, ResponseStatus.SUCCESS)
self.assertEqual(response.result["engine_result"], "tesSUCCESS")

tx = TrustSet(
account=borrower_wallet.address,
limit_amount=IssuedCurrencyAmount(
currency="USD", issuer=loan_issuer.address, value="1000"
),
)
response = await sign_and_reliable_submission_async(tx, borrower_wallet, client)
self.assertEqual(response.status, ResponseStatus.SUCCESS)
self.assertEqual(response.result["engine_result"], "tesSUCCESS")

# Step 0.2: Transfer the `USD` IOU to depositor_wallet and borrower_wallet
tx = Payment(
account=loan_issuer.address,
destination=depositor_wallet.address,
amount=IssuedCurrencyAmount(
currency="USD", issuer=loan_issuer.address, value="1000"
),
)
response = await sign_and_reliable_submission_async(tx, loan_issuer, client)
self.assertEqual(response.status, ResponseStatus.SUCCESS)
self.assertEqual(response.result["engine_result"], "tesSUCCESS")

tx = Payment(
account=loan_issuer.address,
destination=borrower_wallet.address,
amount=IssuedCurrencyAmount(
currency="USD", issuer=loan_issuer.address, value="1000"
),
)
response = await sign_and_reliable_submission_async(tx, loan_issuer, client)
self.assertEqual(response.status, ResponseStatus.SUCCESS)
self.assertEqual(response.result["engine_result"], "tesSUCCESS")

# Step-1: Create a vault
tx = VaultCreate(
account=loan_issuer.address,
asset=IssuedCurrency(currency="USD", issuer=loan_issuer.address),
assets_maximum="1000",
withdrawal_policy=WithdrawalPolicy.VAULT_STRATEGY_FIRST_COME_FIRST_SERVE,
)
response = await sign_and_reliable_submission_async(tx, loan_issuer, client)
self.assertEqual(response.status, ResponseStatus.SUCCESS)
self.assertEqual(response.result["engine_result"], "tesSUCCESS")

account_objects_response = await client.request(
AccountObjects(account=loan_issuer.address, type=AccountObjectType.VAULT)
)
self.assertEqual(len(account_objects_response.result["account_objects"]), 1)
VAULT_ID = account_objects_response.result["account_objects"][0]["index"]

# Step-2: Create a loan broker
tx = LoanBrokerSet(
account=loan_issuer.address,
vault_id=VAULT_ID,
)
response = await sign_and_reliable_submission_async(tx, loan_issuer, client)
self.assertEqual(response.status, ResponseStatus.SUCCESS)
self.assertEqual(response.result["engine_result"], "tesSUCCESS")

# Step-2.1: Verify that the LoanBroker was successfully created
response = await client.request(
AccountObjects(
account=loan_issuer.address, type=AccountObjectType.LOAN_BROKER
)
)
self.assertEqual(len(response.result["account_objects"]), 1)
LOAN_BROKER_ID = response.result["account_objects"][0]["index"]

# Step-3: Deposit funds into the vault
tx = VaultDeposit(
account=depositor_wallet.address,
vault_id=VAULT_ID,
amount=IssuedCurrencyAmount(
currency="USD", issuer=loan_issuer.address, value="1000"
),
)
response = await sign_and_reliable_submission_async(
tx, depositor_wallet, client
)
self.assertEqual(response.status, ResponseStatus.SUCCESS)
self.assertEqual(response.result["engine_result"], "tesSUCCESS")

# Step-5: The Loan Broker and Borrower create a Loan object with a LoanSet
# transaction and the requested principal (excluding fees) is transered to
# the Borrower.
loan_issuer_signed_txn = await autofill_and_sign(
LoanSet(
account=loan_issuer.address,
loan_broker_id=LOAN_BROKER_ID,
principal_requested="100",
counterparty=borrower_wallet.address,
),
client,
loan_issuer,
)

# borrower agrees to the terms of the loan
borrower_txn_signature = sign(
encode_for_signing(loan_issuer_signed_txn.to_xrpl()),
borrower_wallet.private_key,
)

loan_issuer_and_borrower_signature = loan_issuer_signed_txn.to_dict()
loan_issuer_and_borrower_signature["counterparty_signature"] = (
CounterpartySignature(
signing_pub_key=borrower_wallet.public_key,
txn_signature=borrower_txn_signature,
)
)

response = await submit(
Transaction.from_dict(loan_issuer_and_borrower_signature),
client,
fail_hard=True,
)

self.assertEqual(response.status, ResponseStatus.SUCCESS)
self.assertEqual(response.result["engine_result"], "tesSUCCESS")

# Wait for the validation of the latest ledger
await client.request(LEDGER_ACCEPT_REQUEST)

# fetch the Loan object
response = await client.request(
AccountObjects(account=borrower_wallet.address, type=AccountObjectType.LOAN)
)
self.assertEqual(len(response.result["account_objects"]), 1)
LOAN_ID = response.result["account_objects"][0]["index"]

# Delete the Loan object
tx = LoanDelete(
account=loan_issuer.address,
loan_id=LOAN_ID,
)
response = await sign_and_reliable_submission_async(tx, loan_issuer, client)
self.assertEqual(response.status, ResponseStatus.SUCCESS)
# Loan cannot be deleted until all the remaining payments are completed
self.assertEqual(response.result["engine_result"], "tecHAS_OBLIGATIONS")

# Test the LoanManage transaction
tx = LoanManage(
account=loan_issuer.address,
loan_id=LOAN_ID,
flags=LoanManageFlag.TF_LOAN_IMPAIR,
)
response = await sign_and_reliable_submission_async(tx, loan_issuer, client)
self.assertEqual(response.status, ResponseStatus.SUCCESS)
self.assertEqual(response.result["engine_result"], "tesSUCCESS")

# Test the LoanPay transaction
tx = LoanPay(
account=borrower_wallet.address,
loan_id=LOAN_ID,
amount=IssuedCurrencyAmount(
currency="USD", issuer=loan_issuer.address, value="100"
),
)
response = await sign_and_reliable_submission_async(tx, borrower_wallet, client)
self.assertEqual(response.status, ResponseStatus.SUCCESS)
self.assertEqual(response.result["engine_result"], "tesSUCCESS")
4 changes: 4 additions & 0 deletions tests/unit/core/binarycodec/types/test_blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@ def test_from_value(self):
def test_raises_invalid_value_type(self):
invalid_value = [1, 2, 3]
self.assertRaises(XRPLBinaryCodecException, Blob.from_value, invalid_value)

def test_raises_invalid_non_hex_input(self):
invalid_value = "Z"
self.assertRaises(ValueError, Blob.from_value, invalid_value)
Loading
Loading