-
Notifications
You must be signed in to change notification settings - Fork 117
Support for Lending Protocol (XLS-66d) #866
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
base: main
Are you sure you want to change the base?
Changes from 4 commits
373fb52
aadadbe
1db1744
b062bc3
faca6ae
9083a84
c672015
029d65c
1ddf348
4e5cf35
388553d
31b699e
e39509f
88eade6
3b47b6f
9c38b73
f6daf47
9f27a07
d47410a
bd2f13a
fc158fb
2183f0a
3162f69
48bd4e7
98288b8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,186 @@ | ||
| import datetime | ||
|
|
||
| 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, | ||
| Transaction, | ||
| VaultCreate, | ||
| VaultDeposit, | ||
| ) | ||
| 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-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( | ||
Patel-Raj11 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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-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", | ||
| start_date=int(datetime.datetime.now().timestamp()), | ||
| counterparty=borrower_wallet.address, | ||
| ), | ||
ckeshava marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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, | ||
| ) | ||
ckeshava marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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) | ||
| # The borrower cannot pay the loan before the start date | ||
| self.assertEqual(response.result["engine_result"], "tecTOO_SOON") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| from unittest import TestCase | ||
kuan121 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| from xrpl.models.amounts import IssuedCurrencyAmount, MPTAmount | ||
| from xrpl.models.exceptions import XRPLModelException | ||
| from xrpl.models.transactions import LoanBrokerCoverClawback | ||
|
|
||
| _SOURCE = "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ" | ||
| _ISSUER = "rHxTJLqdVUxjJuZEZvajXYYQJ7q8p4DhHy" | ||
|
|
||
|
|
||
| class TestLoanBrokerCoverClawback(TestCase): | ||
| def test_invalid_no_amount_nor_loan_broker_id_specified(self): | ||
| with self.assertRaises(XRPLModelException) as error: | ||
| LoanBrokerCoverClawback(account=_SOURCE) | ||
| self.assertEqual( | ||
| error.exception.args[0], | ||
| "{'LoanBrokerCoverClawback': 'No amount or loan broker ID specified.'}", | ||
| ) | ||
|
|
||
| def test_invalid_xrp_amount(self): | ||
| with self.assertRaises(XRPLModelException) as error: | ||
| LoanBrokerCoverClawback(account=_SOURCE, amount="10.20") | ||
| self.assertEqual( | ||
| error.exception.args[0], | ||
| "{'LoanBrokerCoverClawback:Amount': 'Amount cannot be XRP.'}", | ||
| ) | ||
|
|
||
| def test_invalid_negative_amount(self): | ||
| with self.assertRaises(XRPLModelException) as error: | ||
| LoanBrokerCoverClawback( | ||
| account=_SOURCE, | ||
| amount=IssuedCurrencyAmount( | ||
| issuer=_ISSUER, | ||
| currency="USD", | ||
| value="-10", | ||
| ), | ||
| ) | ||
| self.assertEqual( | ||
| error.exception.args[0], | ||
| "{'LoanBrokerCoverClawback:Amount': 'Amount must be greater than 0.'}", | ||
| ) | ||
|
|
||
| def test_valid_loan_broker_cover_clawback(self): | ||
| tx = LoanBrokerCoverClawback( | ||
| account=_SOURCE, | ||
| amount=MPTAmount( | ||
| mpt_issuance_id=_ISSUER, | ||
| value="10.20", | ||
| ), | ||
| loan_broker_id=_ISSUER, | ||
| ) | ||
| self.assertTrue(tx.is_valid()) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import datetime | ||
| from unittest import TestCase | ||
|
|
||
| from xrpl.models.exceptions import XRPLModelException | ||
| from xrpl.models.transactions import LoanSet | ||
|
|
||
| _SOURCE = "r9LqNeG6qHxjeUocjvVki2XR35weJ9mZgQ" | ||
| _ISSUER = "rHxTJLqdVUxjJuZEZvajXYYQJ7q8p4DhHy" | ||
|
|
||
|
|
||
| class TestLoanSet(TestCase): | ||
| def test_invalid_payment_interval_shorter_than_grace_period(self): | ||
| with self.assertRaises(XRPLModelException) as error: | ||
| LoanSet( | ||
| account=_SOURCE, | ||
| loan_broker_id=_ISSUER, | ||
| principal_requested="100000000", | ||
| start_date=int(datetime.datetime.now().timestamp()), | ||
| payment_interval=65, | ||
| grace_period=70, | ||
| ) | ||
| self.assertEqual( | ||
| error.exception.args[0], | ||
| "{'LoanSet:GracePeriod': 'Grace period must be less than the payment " | ||
| + "interval.'}", | ||
| ) | ||
|
|
||
| def test_invalid_payment_interval_too_short(self): | ||
| with self.assertRaises(XRPLModelException) as error: | ||
| LoanSet( | ||
| account=_SOURCE, | ||
| loan_broker_id=_ISSUER, | ||
| principal_requested="100000000", | ||
| start_date=int(datetime.datetime.now().timestamp()), | ||
| payment_interval=59, | ||
| ) | ||
| self.assertEqual( | ||
| error.exception.args[0], | ||
| "{'LoanSet:PaymentInterval': 'Payment interval must be at least 60 seconds." | ||
| + "'}", | ||
| ) | ||
|
|
||
| def test_valid_loan_set(self): | ||
| tx = LoanSet( | ||
| account=_SOURCE, | ||
| loan_broker_id=_ISSUER, | ||
| principal_requested="100000000", | ||
| start_date=int(datetime.datetime.now().timestamp()), | ||
| ) | ||
| self.assertTrue(tx.is_valid()) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,7 @@ | |
| from xrpl.models import ( | ||
| Batch, | ||
| EscrowFinish, | ||
| LoanSet, | ||
| Response, | ||
| ServerState, | ||
| Simulate, | ||
|
|
@@ -516,6 +517,25 @@ async def _calculate_fee_per_transaction_type( | |
| for raw_txn in batch.raw_transactions | ||
| ] | ||
| ) | ||
| elif transaction.transaction_type == TransactionType.LOAN_SET: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should add unit/integration test to verify this logic.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The existing integ test already uses this piece of code to automatically calculate the transaction fees
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I highly doubt that they will check
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Which transaction-type are you concerned about? The integration test invokes The All the integration tests already test this code-path because
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am referring to a similar test like
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Patel-Raj11 Are you suggesting that we remove the What would be an appropriate interface for the fee-calculation method? Should we let the first party specify the "number of second-party-signers"? This info is necessary for the fee calculation.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TransactionType.LOAN_SET should not have any special handling of fees calculation. So, I was suggesting to remove all changes in this file. As explained in earlier message, we would not hit this code-path. If you want to try it, maybe try writing an integration test that hits this codepath with multi-signing enabled. As far as I understand counterparty_signature cannot be included in a transaction that needs to be signed. So counterparty_signature would always be None when this method is executed.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To auto-fill the approach should be to use loan_set.counterparty instead of loan_set.counterparty_signature:
We should write integration tests, if we implement this logic. I am planning to write such integration test in xrpl.js once, the multi-signing starts working in cpp implementation for LoanSet transaction.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1.a: Even if we get the minimum quorum required for the counterparty, the actual number of signers can always exceed the minimum number of signers. In this case, the transaction will fail due to insufficient fee. Will users want this feature at all? It is easier to communicate the exact number of signers offline between the loan-broker and the borrower. This autofill procedure involves significant network communication and associated delays. If a user wants to use the library for time-sensitive tasks (high frequency trading, flash loans, etc), won't the delays be unacceptable?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
You made a valid point that if multi-signing is setup as three signers (1+1+1) with quorum as 2, it could happen that even though only two needs to sign, but all three can sign and the transaction is still valid. So, we need to find a balance between making the autofill feature usable and not over-estimate the fees. Using total number of signers will always ensure that the transaction passes (at a cost of increased transaction fees) and using quorum will give optimal cost but transaction might fail if more than the quorum signs that transaction. I would lean towards using quorum to begin with and adjust to max signers if we receive many issues from library users. This calculations can always be changed without incurring a breaking change. Does this makes sense? With the current setup, the autofill feature is totally not usable when multi-signing gets involved and I feel that Lending Protocol will be used by intuitions where multi-signing will be the norm.
Autofilling is optional and they can completely skip it for such use cases. And on another note, we do call |
||
| # Compute the additional cost of each signature in the | ||
| # CounterpartySignature, whether a single signature or a multisignature | ||
| loan_set = cast(LoanSet, transaction) | ||
| if loan_set.counterparty_signature is not None: | ||
| signer_count = ( | ||
| len(loan_set.counterparty_signature.signers) | ||
| if loan_set.counterparty_signature.signers is not None | ||
| else 1 | ||
| ) | ||
| base_fee += net_fee * signer_count | ||
| else: | ||
| # Note: Due to lack of information, the client-library assumes that | ||
| # there is only one signer. However, the LoanIssuer and Borrower need to | ||
| # communicate the number of CounterpartySignature.signers | ||
| # (or the appropriate transaction-fee) | ||
| # with each other off-chain. This helps with efficient fee-calculation for | ||
| # the LoanSet transaction. | ||
| base_fee += net_fee | ||
|
|
||
| # Multi-signed/Multi-Account Batch Transactions | ||
| # BaseFee × (1 + Number of Signatures Provided) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.