diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/VaultObject.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/VaultObject.java
index ae3dd5f20..d32da95c3 100644
--- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/VaultObject.java
+++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/ledger/VaultObject.java
@@ -1,10 +1,12 @@
package org.xrpl.xrpl4j.model.ledger;
+import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.google.common.annotations.Beta;
import com.google.common.primitives.UnsignedInteger;
+import com.google.common.primitives.UnsignedLong;
import org.immutables.value.Value;
import org.xrpl.xrpl4j.model.flags.VaultFlags;
import org.xrpl.xrpl4j.model.transactions.Address;
@@ -13,6 +15,7 @@
import org.xrpl.xrpl4j.model.transactions.Hash256;
import org.xrpl.xrpl4j.model.transactions.MpTokenIssuanceId;
import org.xrpl.xrpl4j.model.transactions.VaultData;
+import org.xrpl.xrpl4j.model.transactions.VaultKind;
import org.xrpl.xrpl4j.model.transactions.WithdrawalPolicy;
import java.util.Optional;
@@ -195,6 +198,51 @@ default AssetScale scale() {
return AssetScale.of(UnsignedInteger.ZERO);
}
+ /**
+ * The ledger-entry version of this vault, which selects the accounting rules rippled applies to it. Vaults
+ * created before the {@code LendingProtocolV1_1} amendment activated carry no {@code LEVersion} field and are
+ * treated as version {@code 0} (legacy accounting); vaults created afterwards carry version {@code 1}
+ * (LoanBroker cash-basis accounting).
+ *
+ * @return An {@link UnsignedInteger}, defaulting to {@link UnsignedInteger#ZERO}.
+ */
+ @JsonProperty("LEVersion")
+ @Value.Default
+ default UnsignedInteger leVersion() {
+ return UnsignedInteger.ZERO;
+ }
+
+ /**
+ * Distinguishes a closed-ended vault from the default open-ended kind. Only a closed-ended vault may have a
+ * {@code LoanBroker} attached to it once the {@code LendingProtocolV1_1} amendment is enabled.
+ *
+ * @return A {@link VaultKind}, defaulting to {@link VaultKind#OPEN_ENDED}.
+ */
+ @JsonProperty("VaultKind")
+ @Value.Default
+ @JsonInclude(value = JsonInclude.Include.CUSTOM, valueFilter = VaultKind.OpenEndedFilter.class)
+ default VaultKind vaultKind() {
+ return VaultKind.OPEN_ENDED;
+ }
+
+ /**
+ * The time, in seconds since the Ripple Epoch, after which this closed-ended vault's Subscription phase ends
+ * and its Investment phase begins. Only present when {@link #vaultKind()} is {@link VaultKind#CLOSED_ENDED}.
+ *
+ * @return An {@link Optional} of type {@link UnsignedLong} representing the subscription date.
+ */
+ @JsonProperty("SubscriptionDate")
+ Optional subscriptionDate();
+
+ /**
+ * The time, in seconds since the Ripple Epoch, after which this closed-ended vault's Investment phase ends and
+ * its Redemption phase begins. Only present when {@link #vaultKind()} is {@link VaultKind#CLOSED_ENDED}.
+ *
+ * @return An {@link Optional} of type {@link UnsignedLong} representing the redemption date.
+ */
+ @JsonProperty("RedemptionDate")
+ Optional redemptionDate();
+
/**
* The share MPTokenIssuance information for this vault. Only present in {@code vault_info} RPC responses.
*
diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/LoanBrokerCoverWithdraw.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/LoanBrokerCoverWithdraw.java
index 6e7a62039..5ef3e4587 100644
--- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/LoanBrokerCoverWithdraw.java
+++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/LoanBrokerCoverWithdraw.java
@@ -10,6 +10,8 @@
import org.xrpl.xrpl4j.model.AddressConstants;
import org.xrpl.xrpl4j.model.flags.TransactionFlags;
+import java.util.HashSet;
+import java.util.List;
import java.util.Optional;
/**
@@ -76,6 +78,15 @@ default TransactionFlags flags() {
@JsonProperty("DestinationTag")
Optional destinationTag();
+ /**
+ * Set of Credentials to authorize a deposit made by this transaction. Each member of the array must be the ledger
+ * entry ID of a Credential entry in the ledger. Requires the {@code LendingProtocolV1_1} amendment.
+ *
+ * @return A list of type {@link Hash256}.
+ */
+ @JsonProperty("CredentialIDs")
+ List credentialIds();
+
/**
* Validates LoanBrokerCoverWithdraw data verification preconditions.
*/
@@ -95,5 +106,16 @@ default void check() {
!dest.equals(AddressConstants.ACCOUNT_ZERO),
"Destination must not be the zero account."
));
+
+ if (!credentialIds().isEmpty()) {
+ Preconditions.checkArgument(
+ credentialIds().size() <= 8,
+ "CredentialIDs should have less than or equal to 8 items."
+ );
+ Preconditions.checkArgument(
+ new HashSet<>(credentialIds()).size() == credentialIds().size(),
+ "CredentialIDs should have unique values."
+ );
+ }
}
}
diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/VaultCreate.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/VaultCreate.java
index 4121c2f1f..fa9f092e3 100644
--- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/VaultCreate.java
+++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/VaultCreate.java
@@ -4,6 +4,7 @@
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.google.common.annotations.Beta;
+import com.google.common.primitives.UnsignedLong;
import org.immutables.value.Value;
import org.xrpl.xrpl4j.model.flags.VaultCreateFlags;
import org.xrpl.xrpl4j.model.ledger.Issue;
@@ -99,6 +100,48 @@ default VaultCreateFlags flags() {
@JsonProperty("Scale")
Optional scale();
+ /**
+ * Distinguishes a closed-ended vault from the default open-ended kind. Requires the {@code LendingProtocolV1_1}
+ * amendment to be present on the transaction at all; only a closed-ended vault may later have a
+ * {@code LoanBroker} attached to it.
+ *
+ * @return An optionally-present {@link VaultKind}.
+ */
+ @JsonProperty("VaultKind")
+ Optional vaultKind();
+
+ /**
+ * The time, in seconds since the Ripple Epoch, after which a closed-ended vault's Subscription phase ends and
+ * its Investment phase begins. Required, and only allowed, when {@link #vaultKind()} is
+ * {@link VaultKind#CLOSED_ENDED}.
+ *
+ * @return An {@link Optional} of type {@link UnsignedLong} representing the subscription date.
+ */
+ @JsonProperty("SubscriptionDate")
+ Optional subscriptionDate();
+
+ /**
+ * The time, in seconds since the Ripple Epoch, after which a closed-ended vault's Investment phase ends and its
+ * Redemption phase begins. Required, and only allowed, when {@link #vaultKind()} is
+ * {@link VaultKind#CLOSED_ENDED}.
+ *
+ * @return An {@link Optional} of type {@link UnsignedLong} representing the redemption date.
+ */
+ @JsonProperty("RedemptionDate")
+ Optional redemptionDate();
+
+ /**
+ * The minimum gap, in seconds, allowed between {@link #subscriptionDate()} and {@link #redemptionDate()} for a
+ * closed-ended vault, matching rippled's {@code kMinInvestmentPeriod}.
+ */
+ UnsignedLong MIN_INVESTMENT_PERIOD_SECONDS = UnsignedLong.valueOf(180);
+
+ /**
+ * The maximum gap, in seconds, allowed between {@link #subscriptionDate()} and {@link #redemptionDate()} for a
+ * closed-ended vault, matching rippled's {@code kMaxInvestmentPeriod} (30 x 365.2425-day years).
+ */
+ UnsignedLong MAX_INVESTMENT_PERIOD_SECONDS = UnsignedLong.valueOf(946708560L);
+
/**
* Validate VaultCreate preconditions.
*/
@@ -124,6 +167,32 @@ default void check() {
throw new IllegalArgumentException("Scale is only allowed for IOU assets.");
}
));
+
+ boolean isClosedEnded = vaultKind().map(VaultKind.CLOSED_ENDED::equals).orElse(false);
+ if (!isClosedEnded) {
+ if (subscriptionDate().isPresent() || redemptionDate().isPresent()) {
+ throw new IllegalArgumentException(
+ "SubscriptionDate and RedemptionDate are only allowed when VaultKind is CLOSED_ENDED."
+ );
+ }
+ } else {
+ if (!subscriptionDate().isPresent() || !redemptionDate().isPresent()) {
+ throw new IllegalArgumentException(
+ "SubscriptionDate and RedemptionDate are both required when VaultKind is CLOSED_ENDED."
+ );
+ }
+ UnsignedLong subscription = subscriptionDate().get();
+ UnsignedLong redemption = redemptionDate().get();
+ if (redemption.compareTo(subscription) <= 0) {
+ throw new IllegalArgumentException("RedemptionDate must be strictly after SubscriptionDate.");
+ }
+ UnsignedLong gap = redemption.minus(subscription);
+ if (gap.compareTo(MIN_INVESTMENT_PERIOD_SECONDS) < 0 || gap.compareTo(MAX_INVESTMENT_PERIOD_SECONDS) > 0) {
+ throw new IllegalArgumentException(
+ "Investment period must be between MIN_INVESTMENT_PERIOD_SECONDS and MAX_INVESTMENT_PERIOD_SECONDS."
+ );
+ }
+ }
}
}
diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/VaultDelete.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/VaultDelete.java
index 3cd8cc87a..9e3ca37da 100644
--- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/VaultDelete.java
+++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/VaultDelete.java
@@ -7,6 +7,8 @@
import org.immutables.value.Value;
import org.xrpl.xrpl4j.model.flags.TransactionFlags;
+import java.util.Optional;
+
/**
* Delete a single asset vault. The vault must have no remaining assets or outstanding shares.
*
@@ -47,4 +49,16 @@ default TransactionFlags flags() {
@JsonProperty("VaultID")
Hash256 vaultId();
+ /**
+ * Arbitrary metadata recording why the vault is being deleted, limited to 256 bytes, in hex format. This field
+ * requires the {@code LendingProtocolV1_1} amendment.
+ *
+ * Despite the name, this is a top-level {@code MemoData} field (rippled's {@code sfMemoData}) and is unrelated to
+ * the standard {@link Transaction#memos()} field.
+ *
+ * @return An optionally-present {@link VaultData}.
+ */
+ @JsonProperty("MemoData")
+ Optional memoData();
+
}
diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/VaultKind.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/VaultKind.java
new file mode 100644
index 000000000..1d88b16be
--- /dev/null
+++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/VaultKind.java
@@ -0,0 +1,76 @@
+package org.xrpl.xrpl4j.model.transactions;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+import com.google.common.annotations.Beta;
+
+/**
+ * Distinguishes a closed-ended {@code Vault} from the default open-ended kind. Only a closed-ended vault may
+ * have a {@code LoanBroker} attached to it once the {@code LendingProtocolV1_1} amendment is enabled.
+ */
+@Beta
+public enum VaultKind {
+
+ /**
+ * The default vault kind. Deposits and withdrawals are allowed at any time, and the vault has no defined
+ * lifecycle. An open-ended vault cannot have a {@code LoanBroker} attached once {@code LendingProtocolV1_1}
+ * is enabled.
+ */
+ OPEN_ENDED(0),
+
+ /**
+ * A vault with a defined {@link VaultCreate#subscriptionDate()} and {@link VaultCreate#redemptionDate()},
+ * required in order to attach a {@code LoanBroker} once {@code LendingProtocolV1_1} is enabled.
+ */
+ CLOSED_ENDED(1);
+
+ private final int value;
+
+ VaultKind(int value) {
+ this.value = value;
+ }
+
+ /**
+ * Get the integer value of this {@link VaultKind}.
+ *
+ * @return The integer value.
+ */
+ @JsonValue
+ public int getValue() {
+ return value;
+ }
+
+ /**
+ * Construct a {@link VaultKind} from an integer value.
+ *
+ * @param value The integer value.
+ *
+ * @return The {@link VaultKind} for the given value.
+ */
+ @JsonCreator
+ public static VaultKind forValue(int value) {
+ for (VaultKind kind : VaultKind.values()) {
+ if (kind.value == value) {
+ return kind;
+ }
+ }
+ throw new IllegalArgumentException("No matching VaultKind for value " + value);
+ }
+
+ /**
+ * Used with {@link com.fasterxml.jackson.annotation.JsonInclude.Include#CUSTOM} to omit {@link #OPEN_ENDED} from
+ * serialized JSON, matching how rippled omits {@code VaultKind} for open-ended vaults.
+ */
+ public static class OpenEndedFilter {
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj == VaultKind.OPEN_ENDED;
+ }
+
+ @Override
+ public int hashCode() {
+ return VaultKind.OPEN_ENDED.hashCode();
+ }
+ }
+}
diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/VaultWithdraw.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/VaultWithdraw.java
index 71b1e5bcc..c11b3340c 100644
--- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/VaultWithdraw.java
+++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/VaultWithdraw.java
@@ -4,10 +4,13 @@
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.google.common.annotations.Beta;
+import com.google.common.base.Preconditions;
import com.google.common.primitives.UnsignedInteger;
import org.immutables.value.Value;
import org.xrpl.xrpl4j.model.flags.TransactionFlags;
+import java.util.HashSet;
+import java.util.List;
import java.util.Optional;
/**
@@ -75,4 +78,31 @@ default TransactionFlags flags() {
@JsonProperty("DestinationTag")
Optional destinationTag();
+ /**
+ * Set of Credentials to authorize a deposit made by this transaction. Each member of the array must be the ledger
+ * entry ID of a Credential entry in the ledger. Requires the {@code LendingProtocolV1_1} amendment.
+ *
+ * @return A list of type {@link Hash256}.
+ */
+ @JsonProperty("CredentialIDs")
+ List credentialIds();
+
+ /**
+ * Validate {@link VaultWithdraw#credentialIds} has less than or equal to 8 credentials, and that those
+ * credentials are unique.
+ */
+ @Value.Check
+ default void validateCredentialIds() {
+ if (!credentialIds().isEmpty()) {
+ Preconditions.checkArgument(
+ credentialIds().size() <= 8,
+ "CredentialIDs should have less than or equal to 8 items."
+ );
+ Preconditions.checkArgument(
+ new HashSet<>(credentialIds()).size() == credentialIds().size(),
+ "CredentialIDs should have unique values."
+ );
+ }
+ }
+
}
diff --git a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaVaultObject.java b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaVaultObject.java
index a2b35acbf..9af19434c 100644
--- a/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaVaultObject.java
+++ b/xrpl4j-core/src/main/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaVaultObject.java
@@ -5,6 +5,7 @@
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.google.common.annotations.Beta;
import com.google.common.primitives.UnsignedInteger;
+import com.google.common.primitives.UnsignedLong;
import org.immutables.value.Value.Immutable;
import org.xrpl.xrpl4j.model.flags.VaultFlags;
import org.xrpl.xrpl4j.model.ledger.Issue;
@@ -14,6 +15,7 @@
import org.xrpl.xrpl4j.model.transactions.Hash256;
import org.xrpl.xrpl4j.model.transactions.MpTokenIssuanceId;
import org.xrpl.xrpl4j.model.transactions.VaultData;
+import org.xrpl.xrpl4j.model.transactions.VaultKind;
import org.xrpl.xrpl4j.model.transactions.WithdrawalPolicy;
import java.util.Optional;
@@ -158,4 +160,38 @@ public interface MetaVaultObject extends MetaLedgerObject {
@JsonProperty("Scale")
Optional scale();
+ /**
+ * The ledger-entry version of this vault, which selects the accounting rules rippled applies to it.
+ *
+ * @return An optionally-present {@link UnsignedInteger}.
+ */
+ @JsonProperty("LEVersion")
+ Optional leVersion();
+
+ /**
+ * Distinguishes a closed-ended vault from the default open-ended kind.
+ *
+ * @return An optionally-present {@link VaultKind}.
+ */
+ @JsonProperty("VaultKind")
+ Optional vaultKind();
+
+ /**
+ * The time, in seconds since the Ripple Epoch, after which this closed-ended vault's Subscription phase ends
+ * and its Investment phase begins. Only present when {@link #vaultKind()} is {@link VaultKind#CLOSED_ENDED}.
+ *
+ * @return An optionally-present {@link UnsignedLong}.
+ */
+ @JsonProperty("SubscriptionDate")
+ Optional subscriptionDate();
+
+ /**
+ * The time, in seconds since the Ripple Epoch, after which this closed-ended vault's Investment phase ends and
+ * its Redemption phase begins. Only present when {@link #vaultKind()} is {@link VaultKind#CLOSED_ENDED}.
+ *
+ * @return An optionally-present {@link UnsignedLong}.
+ */
+ @JsonProperty("RedemptionDate")
+ Optional redemptionDate();
+
}
diff --git a/xrpl4j-core/src/main/resources/definitions.json b/xrpl4j-core/src/main/resources/definitions.json
index 672088aef..f7ec2bab5 100644
--- a/xrpl4j-core/src/main/resources/definitions.json
+++ b/xrpl4j-core/src/main/resources/definitions.json
@@ -928,6 +928,26 @@
"type": "UInt32"
}
],
+ [
+ "SubscriptionDate",
+ {
+ "isSerialized": true,
+ "isSigningField": true,
+ "isVLEncoded": false,
+ "nth": 75,
+ "type": "UInt32"
+ }
+ ],
+ [
+ "RedemptionDate",
+ {
+ "isSerialized": true,
+ "isSigningField": true,
+ "isVLEncoded": false,
+ "nth": 76,
+ "type": "UInt32"
+ }
+ ],
[
"IndexNext",
{
@@ -3528,6 +3548,16 @@
"type": "UInt8"
}
],
+ [
+ "LEVersion",
+ {
+ "isSerialized": true,
+ "isSigningField": true,
+ "isVLEncoded": false,
+ "nth": 6,
+ "type": "UInt8"
+ }
+ ],
[
"TickSize",
{
@@ -3578,6 +3608,16 @@
"type": "UInt8"
}
],
+ [
+ "VaultKind",
+ {
+ "isSerialized": true,
+ "isSigningField": true,
+ "isVLEncoded": false,
+ "nth": 22,
+ "type": "UInt8"
+ }
+ ],
[
"TakerPaysCurrency",
{
@@ -5250,6 +5290,22 @@
{
"name": "Scale",
"optionality": 2
+ },
+ {
+ "name": "LEVersion",
+ "optionality": 2
+ },
+ {
+ "name": "VaultKind",
+ "optionality": 2
+ },
+ {
+ "name": "SubscriptionDate",
+ "optionality": 1
+ },
+ {
+ "name": "RedemptionDate",
+ "optionality": 1
}
],
"XChainOwnedClaimID": [
@@ -6101,6 +6157,10 @@
{
"name": "DestinationTag",
"optionality": 1
+ },
+ {
+ "name": "CredentialIDs",
+ "optionality": 1
}
],
"LoanBrokerDelete": [
@@ -6741,6 +6801,18 @@
{
"name": "Scale",
"optionality": 1
+ },
+ {
+ "name": "VaultKind",
+ "optionality": 1
+ },
+ {
+ "name": "SubscriptionDate",
+ "optionality": 1
+ },
+ {
+ "name": "RedemptionDate",
+ "optionality": 1
}
],
"VaultDelete": [
@@ -6797,6 +6869,10 @@
{
"name": "DestinationTag",
"optionality": 1
+ },
+ {
+ "name": "CredentialIDs",
+ "optionality": 1
}
],
"XChainAccountCreateCommit": [
diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtilsTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtilsTest.java
index 25fa568c9..944b26216 100644
--- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtilsTest.java
+++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/signing/SignatureUtilsTest.java
@@ -134,6 +134,7 @@
import org.xrpl.xrpl4j.model.transactions.TrustSet;
import org.xrpl.xrpl4j.model.transactions.VaultClawback;
import org.xrpl.xrpl4j.model.transactions.VaultCreate;
+import org.xrpl.xrpl4j.model.transactions.VaultData;
import org.xrpl.xrpl4j.model.transactions.VaultDelete;
import org.xrpl.xrpl4j.model.transactions.VaultDeposit;
import org.xrpl.xrpl4j.model.transactions.VaultSet;
@@ -1514,6 +1515,7 @@ void addSignatureToVaultDelete() {
VaultDelete transaction = VaultDelete.builder()
.account(sourcePublicKey.deriveAddress())
.vaultId(Hash256.of("0123456789012345678901234567890123456789012345678901234567891234"))
+ .memoData(VaultData.of("48656C6C6F"))
.fee(XrpCurrencyAmount.ofDrops(10))
.sequence(UnsignedInteger.valueOf(391))
.signingPublicKey(sourcePublicKey)
@@ -1550,6 +1552,24 @@ void addSignatureToVaultWithdraw() {
addSignatureToTransactionHelper(transaction);
}
+ @Test
+ void addSignatureToVaultWithdrawWithCredentialIds() {
+ VaultWithdraw transaction = VaultWithdraw.builder()
+ .account(sourcePublicKey.deriveAddress())
+ .vaultId(Hash256.of("0123456789012345678901234567890123456789012345678901234567891234"))
+ .amount(XrpCurrencyAmount.ofDrops(500))
+ .addCredentialIds(
+ Hash256.of("000000000000000000000000000000000000000000000000000000000000000A"),
+ Hash256.of("000000000000000000000000000000000000000000000000000000000000000B")
+ )
+ .fee(XrpCurrencyAmount.ofDrops(10))
+ .sequence(UnsignedInteger.valueOf(391))
+ .signingPublicKey(sourcePublicKey)
+ .build();
+
+ addSignatureToTransactionHelper(transaction);
+ }
+
@Test
void addSignatureToVaultClawback() {
VaultClawback transaction = VaultClawback.builder()
@@ -2083,6 +2103,7 @@ void addMultiSignatureToVaultDelete() {
VaultDelete transaction = VaultDelete.builder()
.account(sourcePublicKey.deriveAddress())
.vaultId(Hash256.of("0123456789012345678901234567890123456789012345678901234567891234"))
+ .memoData(VaultData.of("48656C6C6F"))
.fee(XrpCurrencyAmount.ofDrops(10))
.sequence(UnsignedInteger.valueOf(391))
.build();
diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/ledger/LedgerEntryResultTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/ledger/LedgerEntryResultTest.java
index 59b049110..3bddb9f05 100644
--- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/ledger/LedgerEntryResultTest.java
+++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/ledger/LedgerEntryResultTest.java
@@ -873,6 +873,7 @@ void testVaultResult() throws JSONException, JsonProcessingException {
" \"ShareMPTID\": \"00000005E54ZDVGNGHAOPOPCGVTIQWNQ3DU5Y836\"," +
" \"WithdrawalPolicy\": 1," +
" \"Scale\": 8," +
+ " \"LEVersion\": 0," +
" \"index\": \"6BCD7E451DDA015FB307DAD9208A98A2DC3AC4D1448E624B42C89246DCF08692\"" +
" }," +
" \"status\": \"success\"," +
diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/vault/VaultInfoResultTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/vault/VaultInfoResultTest.java
index 6da53de12..6ad0afc66 100644
--- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/vault/VaultInfoResultTest.java
+++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/client/vault/VaultInfoResultTest.java
@@ -79,6 +79,7 @@ void testJson() throws JSONException, JsonProcessingException {
" \"ShareMPTID\" : \"00000005E54ZDVGNGHAOPOPCGVTIQWNQ3DU5Y836\",\n" +
" \"WithdrawalPolicy\" : 1,\n" +
" \"Scale\" : 6,\n" +
+ " \"LEVersion\" : 0,\n" +
" \"shares\" : {\n" +
" \"Flags\" : 0,\n" +
" \"Issuer\" : \"rE54zDvgnghAoPopCgvtiqWNq3dU5y836S\",\n" +
diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/ledger/VaultObjectTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/ledger/VaultObjectTest.java
index a5a3f6c2d..7c02e85e7 100644
--- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/ledger/VaultObjectTest.java
+++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/ledger/VaultObjectTest.java
@@ -1,9 +1,11 @@
package org.xrpl.xrpl4j.model.ledger;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.xrpl.xrpl4j.crypto.TestConstants.HASH_256;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.primitives.UnsignedInteger;
+import com.google.common.primitives.UnsignedLong;
import org.json.JSONException;
import org.junit.jupiter.api.Test;
import org.xrpl.xrpl4j.model.AbstractJsonTest;
@@ -15,6 +17,7 @@
import org.xrpl.xrpl4j.model.transactions.Hash256;
import org.xrpl.xrpl4j.model.transactions.MpTokenIssuanceId;
import org.xrpl.xrpl4j.model.transactions.VaultData;
+import org.xrpl.xrpl4j.model.transactions.VaultKind;
import org.xrpl.xrpl4j.model.transactions.WithdrawalPolicy;
class VaultObjectTest extends AbstractJsonTest {
@@ -43,9 +46,16 @@ void testJsonWithAllFields() throws JSONException, JsonProcessingException {
.withdrawalPolicy(WithdrawalPolicy.FIRST_COME_FIRST_SERVE)
.scale(AssetScale.of(UnsignedInteger.valueOf(8)))
.flags(VaultFlags.VAULT_PRIVATE)
+ .leVersion(UnsignedInteger.ONE)
+ .vaultKind(VaultKind.CLOSED_ENDED)
+ .subscriptionDate(UnsignedLong.valueOf(1000))
+ .redemptionDate(UnsignedLong.valueOf(2000))
.index(HASH_256)
.build();
+ assertThat(vault.vaultKind()).isEqualTo(VaultKind.CLOSED_ENDED);
+ assertThat(vault.leVersion()).isEqualTo(UnsignedInteger.ONE);
+
String json = String.format("{\n" +
" \"LedgerEntryType\" : \"Vault\",\n" +
" \"Flags\" : 65536,\n" +
@@ -67,6 +77,10 @@ void testJsonWithAllFields() throws JSONException, JsonProcessingException {
" \"ShareMPTID\" : \"00000005E54ZDVGNGHAOPOPCGVTIQWNQ3DU5Y836\",\n" +
" \"WithdrawalPolicy\" : 1,\n" +
" \"Scale\" : 8,\n" +
+ " \"LEVersion\" : 1,\n" +
+ " \"VaultKind\" : 1,\n" +
+ " \"SubscriptionDate\" : 1000,\n" +
+ " \"RedemptionDate\" : 2000,\n" +
" \"index\" : %s\n" +
"}", HASH_256);
@@ -107,6 +121,50 @@ void testJsonWithDefaultFields() throws JSONException, JsonProcessingException {
" \"ShareMPTID\" : \"00000005E54ZDVGNGHAOPOPCGVTIQWNQ3DU5Y836\",\n" +
" \"WithdrawalPolicy\" : 1,\n" +
" \"Scale\" : 0,\n" +
+ " \"LEVersion\" : 0,\n" +
+ " \"index\" : %s\n" +
+ "}", HASH_256);
+
+ assertCanSerializeAndDeserialize(vault, json);
+ }
+
+ @Test
+ void testJsonWithOmittedVaultKind() throws JSONException, JsonProcessingException {
+ VaultObject vault = VaultObject.builder()
+ .previousTransactionId(Hash256.of("0000000000000000000000000000000000000000000000000000000000000010"))
+ .previousTransactionLedgerSequence(UnsignedInteger.valueOf(100))
+ .sequence(UnsignedInteger.valueOf(5))
+ .ownerNode("0")
+ .owner(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm"))
+ .account(Address.of("rE54zDvgnghAoPopCgvtiqWNq3dU5y836S"))
+ .asset(Issue.XRP)
+ .shareMptId(MpTokenIssuanceId.of("00000005E54ZDVGNGHAOPOPCGVTIQWNQ3DU5Y836"))
+ .withdrawalPolicy(WithdrawalPolicy.FIRST_COME_FIRST_SERVE)
+ .index(HASH_256)
+ .build();
+
+ assertThat(vault.vaultKind()).isEqualTo(VaultKind.OPEN_ENDED);
+
+ String json = String.format("{\n" +
+ " \"LedgerEntryType\" : \"Vault\",\n" +
+ " \"Flags\" : 0,\n" +
+ " \"PreviousTxnID\" : \"0000000000000000000000000000000000000000000000000000000000000010\",\n" +
+ " \"PreviousTxnLgrSeq\" : 100,\n" +
+ " \"Sequence\" : 5,\n" +
+ " \"OwnerNode\" : \"0\",\n" +
+ " \"Owner\" : \"rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm\",\n" +
+ " \"Account\" : \"rE54zDvgnghAoPopCgvtiqWNq3dU5y836S\",\n" +
+ " \"Asset\" : {\n" +
+ " \"currency\" : \"XRP\"\n" +
+ " },\n" +
+ " \"AssetsTotal\" : \"0\",\n" +
+ " \"AssetsAvailable\" : \"0\",\n" +
+ " \"AssetsMaximum\" : \"0\",\n" +
+ " \"LossUnrealized\" : \"0\",\n" +
+ " \"ShareMPTID\" : \"00000005E54ZDVGNGHAOPOPCGVTIQWNQ3DU5Y836\",\n" +
+ " \"WithdrawalPolicy\" : 1,\n" +
+ " \"Scale\" : 0,\n" +
+ " \"LEVersion\" : 0,\n" +
" \"index\" : %s\n" +
"}", HASH_256);
diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/VaultCreateTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/VaultCreateTest.java
index 96d5084b0..d0477f8fd 100644
--- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/VaultCreateTest.java
+++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/VaultCreateTest.java
@@ -5,6 +5,7 @@
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import com.google.common.primitives.UnsignedInteger;
+import com.google.common.primitives.UnsignedLong;
import org.junit.jupiter.api.Test;
import org.xrpl.xrpl4j.crypto.keys.PublicKey;
import org.xrpl.xrpl4j.model.flags.VaultCreateFlags;
@@ -104,6 +105,101 @@ void scaleZeroAllowedForIouAsset() {
);
}
+ @Test
+ void closedEndedVaultWithValidDatesAllowed() {
+ assertDoesNotThrow(() -> baseBuilder()
+ .vaultKind(VaultKind.CLOSED_ENDED)
+ .subscriptionDate(UnsignedLong.valueOf(1000))
+ .redemptionDate(UnsignedLong.valueOf(1000).plus(VaultCreate.MIN_INVESTMENT_PERIOD_SECONDS))
+ .build()
+ );
+ }
+
+ @Test
+ void openEndedVaultWithSubscriptionDateNotAllowed() {
+ assertThatThrownBy(() -> baseBuilder()
+ .subscriptionDate(UnsignedLong.valueOf(1000))
+ .build()
+ ).isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("SubscriptionDate and RedemptionDate are only allowed when VaultKind is CLOSED_ENDED.");
+ }
+
+ @Test
+ void openEndedVaultWithRedemptionDateNotAllowed() {
+ assertThatThrownBy(() -> baseBuilder()
+ .redemptionDate(UnsignedLong.valueOf(1000))
+ .build()
+ ).isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("SubscriptionDate and RedemptionDate are only allowed when VaultKind is CLOSED_ENDED.");
+ }
+
+ @Test
+ void closedEndedVaultRequiresSubscriptionDate() {
+ assertThatThrownBy(() -> baseBuilder()
+ .vaultKind(VaultKind.CLOSED_ENDED)
+ .redemptionDate(UnsignedLong.valueOf(1000))
+ .build()
+ ).isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("SubscriptionDate and RedemptionDate are both required when VaultKind is CLOSED_ENDED.");
+ }
+
+ @Test
+ void closedEndedVaultRequiresRedemptionDate() {
+ assertThatThrownBy(() -> baseBuilder()
+ .vaultKind(VaultKind.CLOSED_ENDED)
+ .subscriptionDate(UnsignedLong.valueOf(1000))
+ .build()
+ ).isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("SubscriptionDate and RedemptionDate are both required when VaultKind is CLOSED_ENDED.");
+ }
+
+ @Test
+ void closedEndedVaultRedemptionDateMustBeAfterSubscriptionDate() {
+ assertThatThrownBy(() -> baseBuilder()
+ .vaultKind(VaultKind.CLOSED_ENDED)
+ .subscriptionDate(UnsignedLong.valueOf(1000))
+ .redemptionDate(UnsignedLong.valueOf(1000))
+ .build()
+ ).isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("RedemptionDate must be strictly after SubscriptionDate.");
+ }
+
+ @Test
+ void closedEndedVaultGapBelowMinimumNotAllowed() {
+ UnsignedLong tooSmallGap = VaultCreate.MIN_INVESTMENT_PERIOD_SECONDS.minus(UnsignedLong.ONE);
+ assertThatThrownBy(() -> baseBuilder()
+ .vaultKind(VaultKind.CLOSED_ENDED)
+ .subscriptionDate(UnsignedLong.valueOf(1000))
+ .redemptionDate(UnsignedLong.valueOf(1000).plus(tooSmallGap))
+ .build()
+ ).isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Investment period must be between MIN_INVESTMENT_PERIOD_SECONDS and MAX_INVESTMENT_PERIOD_SECONDS.");
+ }
+
+ @Test
+ void closedEndedVaultGapAtMaximumAllowed() {
+ VaultCreate vaultCreate = baseBuilder()
+ .vaultKind(VaultKind.CLOSED_ENDED)
+ .subscriptionDate(UnsignedLong.valueOf(1000))
+ .redemptionDate(UnsignedLong.valueOf(1000).plus(VaultCreate.MAX_INVESTMENT_PERIOD_SECONDS))
+ .build();
+
+ assertThat(vaultCreate.redemptionDate())
+ .hasValue(UnsignedLong.valueOf(1000).plus(VaultCreate.MAX_INVESTMENT_PERIOD_SECONDS));
+ }
+
+ @Test
+ void closedEndedVaultGapAboveMaximumNotAllowed() {
+ UnsignedLong tooLargeGap = VaultCreate.MAX_INVESTMENT_PERIOD_SECONDS.plus(UnsignedLong.ONE);
+ assertThatThrownBy(() -> baseBuilder()
+ .vaultKind(VaultKind.CLOSED_ENDED)
+ .subscriptionDate(UnsignedLong.valueOf(1000))
+ .redemptionDate(UnsignedLong.valueOf(1000).plus(tooLargeGap))
+ .build()
+ ).isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Investment period must be between MIN_INVESTMENT_PERIOD_SECONDS and MAX_INVESTMENT_PERIOD_SECONDS.");
+ }
+
/**
* Returns a builder pre-populated with required fields for VaultCreate.
*/
diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/VaultKindTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/VaultKindTest.java
new file mode 100644
index 000000000..ce5169faa
--- /dev/null
+++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/VaultKindTest.java
@@ -0,0 +1,29 @@
+package org.xrpl.xrpl4j.model.transactions;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link VaultKind}.
+ */
+class VaultKindTest {
+
+ @Test
+ void forValue() {
+ assertThat(VaultKind.forValue(0)).isEqualTo(VaultKind.OPEN_ENDED);
+ assertThat(VaultKind.forValue(1)).isEqualTo(VaultKind.CLOSED_ENDED);
+ assertThatThrownBy(() -> VaultKind.forValue(2))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("No matching VaultKind for value 2");
+ }
+
+ @Test
+ void openEndedFilter() {
+ VaultKind.OpenEndedFilter filter = new VaultKind.OpenEndedFilter();
+ assertThat(filter.equals(VaultKind.OPEN_ENDED)).isTrue();
+ assertThat(filter.equals(VaultKind.CLOSED_ENDED)).isFalse();
+ assertThat(filter.hashCode()).isEqualTo(VaultKind.OPEN_ENDED.hashCode());
+ }
+}
diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/json/LoanBrokerCoverWithdrawJsonTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/json/LoanBrokerCoverWithdrawJsonTest.java
index ab72c1c4f..58b6421ed 100644
--- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/json/LoanBrokerCoverWithdrawJsonTest.java
+++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/json/LoanBrokerCoverWithdrawJsonTest.java
@@ -1,5 +1,7 @@
package org.xrpl.xrpl4j.model.transactions.json;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.primitives.UnsignedInteger;
import org.json.JSONException;
@@ -9,6 +11,7 @@
import org.xrpl.xrpl4j.model.flags.TransactionFlags;
import org.xrpl.xrpl4j.model.transactions.Address;
import org.xrpl.xrpl4j.model.transactions.Hash256;
+import org.xrpl.xrpl4j.model.transactions.ImmutableLoanBrokerCoverWithdraw;
import org.xrpl.xrpl4j.model.transactions.IssuedCurrencyAmount;
import org.xrpl.xrpl4j.model.transactions.LoanBrokerCoverWithdraw;
import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount;
@@ -147,4 +150,75 @@ public void testLoanBrokerCoverWithdrawJsonWithNonZeroFlags() throws JsonProcess
assertCanSerializeAndDeserialize(withdraw, json);
}
+
+ @Test
+ public void testLoanBrokerCoverWithdrawJsonWithCredentialIds() throws JsonProcessingException, JSONException {
+ LoanBrokerCoverWithdraw withdraw = LoanBrokerCoverWithdraw.builder()
+ .account(Address.of("rU1Cm8GymH5U1WuTcmMTUZ5XjwJbanQoA8"))
+ .fee(XrpCurrencyAmount.ofDrops(15))
+ .sequence(UnsignedInteger.valueOf(192))
+ .signingPublicKey(
+ PublicKey.fromBase16EncodedPublicKey(
+ "ED1F863E4E0957C6965B7B0563D56C77E4F68D571E8026251834F0ADBB0411D6FD"
+ )
+ )
+ .loanBrokerId(Hash256.of("79E25403E9FC010A277D80410EED5494FDD033A09FD4C1432335A1734A1D099D"))
+ .amount(XrpCurrencyAmount.ofDrops(25000))
+ .addCredentialIds(
+ Hash256.of("000000000000000000000000000000000000000000000000000000000000000A"),
+ Hash256.of("000000000000000000000000000000000000000000000000000000000000000B")
+ )
+ .build();
+
+ String json = "{" +
+ " \"Account\": \"rU1Cm8GymH5U1WuTcmMTUZ5XjwJbanQoA8\"," +
+ " \"TransactionType\": \"LoanBrokerCoverWithdraw\"," +
+ " \"Fee\": \"15\"," +
+ " \"Sequence\": 192," +
+ " \"SigningPubKey\": \"ED1F863E4E0957C6965B7B0563D56C77E4F68D571E8026251834F0ADBB0411D6FD\"," +
+ " \"LoanBrokerID\": \"79E25403E9FC010A277D80410EED5494FDD033A09FD4C1432335A1734A1D099D\"," +
+ " \"Amount\": \"25000\"," +
+ " \"CredentialIDs\": [" +
+ " \"000000000000000000000000000000000000000000000000000000000000000A\"," +
+ " \"000000000000000000000000000000000000000000000000000000000000000B\"" +
+ " ]" +
+ "}";
+
+ assertCanSerializeAndDeserialize(withdraw, json);
+ }
+
+ @Test
+ public void testLoanBrokerCoverWithdrawWithTooManyCredentialIds() {
+ ImmutableLoanBrokerCoverWithdraw.Builder builder = LoanBrokerCoverWithdraw.builder()
+ .account(Address.of("rU1Cm8GymH5U1WuTcmMTUZ5XjwJbanQoA8"))
+ .fee(XrpCurrencyAmount.ofDrops(15))
+ .sequence(UnsignedInteger.valueOf(192))
+ .loanBrokerId(Hash256.of("79E25403E9FC010A277D80410EED5494FDD033A09FD4C1432335A1734A1D099D"))
+ .amount(XrpCurrencyAmount.ofDrops(25000));
+
+ for (int i = 0; i < 9; i++) {
+ builder.addCredentialIds(Hash256.of(String.format("%064X", i)));
+ }
+
+ assertThatThrownBy(builder::build)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("CredentialIDs should have less than or equal to 8 items.");
+ }
+
+ @Test
+ public void testLoanBrokerCoverWithdrawWithDuplicateCredentialIds() {
+ assertThatThrownBy(() -> LoanBrokerCoverWithdraw.builder()
+ .account(Address.of("rU1Cm8GymH5U1WuTcmMTUZ5XjwJbanQoA8"))
+ .fee(XrpCurrencyAmount.ofDrops(15))
+ .sequence(UnsignedInteger.valueOf(192))
+ .loanBrokerId(Hash256.of("79E25403E9FC010A277D80410EED5494FDD033A09FD4C1432335A1734A1D099D"))
+ .amount(XrpCurrencyAmount.ofDrops(25000))
+ .addCredentialIds(
+ Hash256.of("000000000000000000000000000000000000000000000000000000000000000A"),
+ Hash256.of("000000000000000000000000000000000000000000000000000000000000000A")
+ )
+ .build())
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("CredentialIDs should have unique values.");
+ }
}
diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/json/VaultCreateJsonTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/json/VaultCreateJsonTest.java
index d230bd833..8fa6422cf 100644
--- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/json/VaultCreateJsonTest.java
+++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/json/VaultCreateJsonTest.java
@@ -2,6 +2,7 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.primitives.UnsignedInteger;
+import com.google.common.primitives.UnsignedLong;
import org.json.JSONException;
import org.junit.jupiter.api.Test;
import org.xrpl.xrpl4j.crypto.keys.PublicKey;
@@ -18,6 +19,7 @@
import org.xrpl.xrpl4j.model.transactions.MpTokenMetadata;
import org.xrpl.xrpl4j.model.transactions.VaultCreate;
import org.xrpl.xrpl4j.model.transactions.VaultData;
+import org.xrpl.xrpl4j.model.transactions.VaultKind;
import org.xrpl.xrpl4j.model.transactions.WithdrawalPolicy;
import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount;
@@ -155,6 +157,38 @@ public void testVaultCreateJsonWithUnknownFields() throws JsonProcessingExceptio
assertCanSerializeAndDeserialize(vaultCreate, json);
}
+ @Test
+ public void testVaultCreateJsonWithClosedEndedVaultKind() throws JsonProcessingException, JSONException {
+ VaultCreate vaultCreate = VaultCreate.builder()
+ .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm"))
+ .fee(XrpCurrencyAmount.ofDrops(10))
+ .sequence(UnsignedInteger.valueOf(1))
+ .asset(Issue.XRP)
+ .vaultKind(VaultKind.CLOSED_ENDED)
+ .subscriptionDate(UnsignedLong.valueOf(1000))
+ .redemptionDate(UnsignedLong.valueOf(1000).plus(VaultCreate.MIN_INVESTMENT_PERIOD_SECONDS))
+ .signingPublicKey(
+ PublicKey.fromBase16EncodedPublicKey("02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC")
+ )
+ .build();
+
+ String json = "{" +
+ " \"Account\": \"rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm\"," +
+ " \"Asset\": {" +
+ " \"currency\": \"XRP\"" +
+ " }," +
+ " \"VaultKind\": 1," +
+ " \"SubscriptionDate\": 1000," +
+ " \"RedemptionDate\": 1180," +
+ " \"Fee\": \"10\"," +
+ " \"Sequence\": 1," +
+ " \"SigningPubKey\": \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\"," +
+ " \"TransactionType\": \"VaultCreate\"" +
+ "}";
+
+ assertCanSerializeAndDeserialize(vaultCreate, json);
+ }
+
@Test
public void testVaultCreateJsonWithMptAsset() throws JsonProcessingException, JSONException {
VaultCreate vaultCreate = VaultCreate.builder()
diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/json/VaultDeleteJsonTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/json/VaultDeleteJsonTest.java
index b086366f2..0b4e317ac 100644
--- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/json/VaultDeleteJsonTest.java
+++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/json/VaultDeleteJsonTest.java
@@ -8,6 +8,7 @@
import org.xrpl.xrpl4j.model.AbstractJsonTest;
import org.xrpl.xrpl4j.model.transactions.Address;
import org.xrpl.xrpl4j.model.transactions.Hash256;
+import org.xrpl.xrpl4j.model.transactions.VaultData;
import org.xrpl.xrpl4j.model.transactions.VaultDelete;
import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount;
@@ -39,5 +40,31 @@ public void testVaultDeleteJson() throws JsonProcessingException, JSONException
assertCanSerializeAndDeserialize(vaultDelete, json);
}
+
+ @Test
+ public void testVaultDeleteJsonWithMemoData() throws JsonProcessingException, JSONException {
+ VaultDelete vaultDelete = VaultDelete.builder()
+ .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm"))
+ .fee(XrpCurrencyAmount.ofDrops(10))
+ .sequence(UnsignedInteger.valueOf(1))
+ .vaultId(Hash256.of("0000000000000000000000000000000000000000000000000000000000000001"))
+ .memoData(VaultData.of("48656C6C6F"))
+ .signingPublicKey(
+ PublicKey.fromBase16EncodedPublicKey("02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC")
+ )
+ .build();
+
+ String json = "{" +
+ " \"Account\": \"rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm\"," +
+ " \"VaultID\": \"0000000000000000000000000000000000000000000000000000000000000001\"," +
+ " \"MemoData\": \"48656C6C6F\"," +
+ " \"Fee\": \"10\"," +
+ " \"Sequence\": 1," +
+ " \"SigningPubKey\": \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\"," +
+ " \"TransactionType\": \"VaultDelete\"" +
+ "}";
+
+ assertCanSerializeAndDeserialize(vaultDelete, json);
+ }
}
diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/json/VaultWithdrawJsonTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/json/VaultWithdrawJsonTest.java
index d4ba8f147..05c03b37b 100644
--- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/json/VaultWithdrawJsonTest.java
+++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/json/VaultWithdrawJsonTest.java
@@ -1,5 +1,7 @@
package org.xrpl.xrpl4j.model.transactions.json;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.primitives.UnsignedInteger;
import org.json.JSONException;
@@ -8,6 +10,7 @@
import org.xrpl.xrpl4j.model.AbstractJsonTest;
import org.xrpl.xrpl4j.model.transactions.Address;
import org.xrpl.xrpl4j.model.transactions.Hash256;
+import org.xrpl.xrpl4j.model.transactions.ImmutableVaultWithdraw;
import org.xrpl.xrpl4j.model.transactions.IssuedCurrencyAmount;
import org.xrpl.xrpl4j.model.transactions.MpTokenIssuanceId;
import org.xrpl.xrpl4j.model.transactions.MptCurrencyAmount;
@@ -146,5 +149,74 @@ public void testVaultWithdrawJsonWithMptAmount() throws JsonProcessingException,
assertCanSerializeAndDeserialize(vaultWithdraw, json);
}
+
+ @Test
+ public void testVaultWithdrawJsonWithCredentialIds() throws JsonProcessingException, JSONException {
+ VaultWithdraw vaultWithdraw = VaultWithdraw.builder()
+ .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm"))
+ .fee(XrpCurrencyAmount.ofDrops(10))
+ .sequence(UnsignedInteger.valueOf(1))
+ .vaultId(Hash256.of("0000000000000000000000000000000000000000000000000000000000000001"))
+ .amount(XrpCurrencyAmount.ofDrops(500000))
+ .addCredentialIds(
+ Hash256.of("000000000000000000000000000000000000000000000000000000000000000A"),
+ Hash256.of("000000000000000000000000000000000000000000000000000000000000000B")
+ )
+ .signingPublicKey(
+ PublicKey.fromBase16EncodedPublicKey("02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC")
+ )
+ .build();
+
+ String json = "{" +
+ " \"Account\": \"rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm\"," +
+ " \"VaultID\": \"0000000000000000000000000000000000000000000000000000000000000001\"," +
+ " \"Amount\": \"500000\"," +
+ " \"CredentialIDs\": [" +
+ " \"000000000000000000000000000000000000000000000000000000000000000A\"," +
+ " \"000000000000000000000000000000000000000000000000000000000000000B\"" +
+ " ]," +
+ " \"Fee\": \"10\"," +
+ " \"Sequence\": 1," +
+ " \"SigningPubKey\": \"02356E89059A75438887F9FEE2056A2890DB82A68353BE9C0C0C8F89C0018B37FC\"," +
+ " \"TransactionType\": \"VaultWithdraw\"" +
+ "}";
+
+ assertCanSerializeAndDeserialize(vaultWithdraw, json);
+ }
+
+ @Test
+ public void testVaultWithdrawWithTooManyCredentialIds() {
+ ImmutableVaultWithdraw.Builder builder = VaultWithdraw.builder()
+ .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm"))
+ .fee(XrpCurrencyAmount.ofDrops(10))
+ .sequence(UnsignedInteger.valueOf(1))
+ .vaultId(Hash256.of("0000000000000000000000000000000000000000000000000000000000000001"))
+ .amount(XrpCurrencyAmount.ofDrops(500000));
+
+ for (int i = 0; i < 9; i++) {
+ builder.addCredentialIds(Hash256.of(String.format("%064X", i)));
+ }
+
+ assertThatThrownBy(builder::build)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("CredentialIDs should have less than or equal to 8 items.");
+ }
+
+ @Test
+ public void testVaultWithdrawWithDuplicateCredentialIds() {
+ assertThatThrownBy(() -> VaultWithdraw.builder()
+ .account(Address.of("rJVUeRqDFNs2xqA7ncVE6ZoAhPUoaJJSQm"))
+ .fee(XrpCurrencyAmount.ofDrops(10))
+ .sequence(UnsignedInteger.valueOf(1))
+ .vaultId(Hash256.of("0000000000000000000000000000000000000000000000000000000000000001"))
+ .amount(XrpCurrencyAmount.ofDrops(500000))
+ .addCredentialIds(
+ Hash256.of("000000000000000000000000000000000000000000000000000000000000000A"),
+ Hash256.of("000000000000000000000000000000000000000000000000000000000000000A")
+ )
+ .build())
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("CredentialIDs should have unique values.");
+ }
}
diff --git a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaVaultObjectTest.java b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaVaultObjectTest.java
index 796c53cb1..2bbe1d6f2 100644
--- a/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaVaultObjectTest.java
+++ b/xrpl4j-core/src/test/java/org/xrpl/xrpl4j/model/transactions/metadata/MetaVaultObjectTest.java
@@ -2,6 +2,7 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.primitives.UnsignedInteger;
+import com.google.common.primitives.UnsignedLong;
import org.json.JSONException;
import org.junit.jupiter.api.Test;
import org.xrpl.xrpl4j.model.AbstractJsonTest;
@@ -13,6 +14,7 @@
import org.xrpl.xrpl4j.model.transactions.Hash256;
import org.xrpl.xrpl4j.model.transactions.MpTokenIssuanceId;
import org.xrpl.xrpl4j.model.transactions.VaultData;
+import org.xrpl.xrpl4j.model.transactions.VaultKind;
import org.xrpl.xrpl4j.model.transactions.WithdrawalPolicy;
class MetaVaultObjectTest extends AbstractJsonTest {
@@ -41,6 +43,10 @@ void testMetaVaultObjectWithAllFields() throws JsonProcessingException, JSONExce
.shareMptId(MpTokenIssuanceId.of("00000005E54ZDVGNGHAOPOPCGVTIQWNQ3DU5Y836"))
.withdrawalPolicy(WithdrawalPolicy.FIRST_COME_FIRST_SERVE)
.scale(AssetScale.of(UnsignedInteger.valueOf(8)))
+ .leVersion(UnsignedInteger.ONE)
+ .vaultKind(VaultKind.CLOSED_ENDED)
+ .subscriptionDate(UnsignedLong.valueOf(1000))
+ .redemptionDate(UnsignedLong.valueOf(2000))
.build();
String json = "{" +
@@ -62,7 +68,11 @@ void testMetaVaultObjectWithAllFields() throws JsonProcessingException, JSONExce
" \"LossUnrealized\": \"100\"," +
" \"ShareMPTID\": \"00000005E54ZDVGNGHAOPOPCGVTIQWNQ3DU5Y836\"," +
" \"WithdrawalPolicy\": 1," +
- " \"Scale\": 8" +
+ " \"Scale\": 8," +
+ " \"LEVersion\": 1," +
+ " \"VaultKind\": 1," +
+ " \"SubscriptionDate\": 1000," +
+ " \"RedemptionDate\": 2000" +
"}";
assertCanSerializeAndDeserialize(metaVaultObject, json, MetaVaultObject.class);
diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/LendingProtocolIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/LendingProtocolIT.java
index fc03b5b31..4dd14e375 100644
--- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/LendingProtocolIT.java
+++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/LendingProtocolIT.java
@@ -32,6 +32,7 @@
import org.xrpl.xrpl4j.model.client.transactions.TransactionResult;
import org.xrpl.xrpl4j.model.flags.LoanManageFlags;
import org.xrpl.xrpl4j.model.flags.LoanPayFlags;
+import org.xrpl.xrpl4j.model.immutables.FluentCompareTo;
import org.xrpl.xrpl4j.model.ledger.IouIssue;
import org.xrpl.xrpl4j.model.ledger.Issue;
import org.xrpl.xrpl4j.model.ledger.LedgerObject;
@@ -64,6 +65,7 @@
import org.xrpl.xrpl4j.model.transactions.SignerListSet;
import org.xrpl.xrpl4j.model.transactions.VaultCreate;
import org.xrpl.xrpl4j.model.transactions.VaultDeposit;
+import org.xrpl.xrpl4j.model.transactions.VaultKind;
import org.xrpl.xrpl4j.model.transactions.WithdrawalPolicy;
import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount;
@@ -117,6 +119,7 @@ void loanSetWithSingleSignBrokerAndSingleSignCounterparty()
.principalRequested(Amount.of("1000000"))
.counterparty(borrowerKeyPair.publicKey().deriveAddress())
.paymentTotal(UnsignedInteger.valueOf(3))
+ .paymentInterval(UnsignedInteger.valueOf(60))
.signingPublicKey(brokerKeyPair.publicKey())
.build();
@@ -208,6 +211,7 @@ void loanSetWithSingleSignBrokerAndRegularKeyCounterparty()
.principalRequested(Amount.of("1000000"))
.counterparty(borrowerKeyPair.publicKey().deriveAddress())
.paymentTotal(UnsignedInteger.valueOf(3))
+ .paymentInterval(UnsignedInteger.valueOf(60))
.signingPublicKey(brokerKeyPair.publicKey())
.build();
@@ -277,6 +281,7 @@ void loanSetWithSingleSignBrokerAndMultiSignCounterparty()
.principalRequested(Amount.of("1000000"))
.counterparty(borrowerKeyPair.publicKey().deriveAddress())
.paymentTotal(UnsignedInteger.valueOf(3))
+ .paymentInterval(UnsignedInteger.valueOf(60))
.signingPublicKey(brokerKeyPair.publicKey())
.build();
@@ -350,6 +355,7 @@ void loanSetWithMultiSignBrokerAndSingleSignCounterparty()
.principalRequested(Amount.of("1000000"))
.counterparty(borrowerKeyPair.publicKey().deriveAddress())
.paymentTotal(UnsignedInteger.valueOf(3))
+ .paymentInterval(UnsignedInteger.valueOf(60))
.build();
final LoanSet unsignedLoanSet = ImmutableLoanSet.copyOf(unpricedLoanSet).withFee(
@@ -421,6 +427,7 @@ void loanSetWithMultiSignBrokerAndMultiSignCounterparty()
.principalRequested(Amount.of("1000000"))
.counterparty(borrowerKeyPair.publicKey().deriveAddress())
.paymentTotal(UnsignedInteger.valueOf(3))
+ .paymentInterval(UnsignedInteger.valueOf(60))
.build();
final LoanSet unsignedLoanSet = ImmutableLoanSet.copyOf(unpricedLoanSet).withFee(
@@ -543,6 +550,15 @@ void lendingProtocolLifecycle() throws Exception {
() -> this.getValidatedAccountInfo(loanBrokerKeyPair.publicKey().deriveAddress())
);
+ // LoanBrokerSet requires a closed-ended Vault once LendingProtocolV1_1 is enabled, so this Vault must define a
+ // SubscriptionDate/RedemptionDate pair, and this method must wait for the ledger to pass SubscriptionDate before
+ // attaching a LoanBroker to it, below. See setupLoanBrokerAndXrpVault() for why SubscriptionDate and
+ // RedemptionDate are padded well past their minimums.
+ final UnsignedLong subscriptionDate = instantToXrpTimestamp(getMinExpirationTime().plus(Duration.ofSeconds(60)));
+ final UnsignedLong redemptionDate = subscriptionDate.plus(
+ VaultCreate.MIN_INVESTMENT_PERIOD_SECONDS.plus(UnsignedLong.valueOf(3600))
+ );
+
VaultCreate vaultCreate = VaultCreate.builder()
.account(loanBrokerKeyPair.publicKey().deriveAddress())
.fee(fee)
@@ -550,6 +566,9 @@ void lendingProtocolLifecycle() throws Exception {
.asset(usdIssue)
.assetsMaximum(Amount.of("500000"))
.withdrawalPolicy(WithdrawalPolicy.FIRST_COME_FIRST_SERVE)
+ .vaultKind(VaultKind.CLOSED_ENDED)
+ .subscriptionDate(subscriptionDate)
+ .redemptionDate(redemptionDate)
.signingPublicKey(loanBrokerKeyPair.publicKey())
.build();
@@ -572,6 +591,10 @@ void lendingProtocolLifecycle() throws Exception {
LedgerSpecifier.VALIDATED
)
);
+ assertThat(vaultLedgerEntry.node().vaultKind()).isEqualTo(VaultKind.CLOSED_ENDED);
+ assertThat(vaultLedgerEntry.node().subscriptionDate()).hasValue(subscriptionDate);
+ assertThat(vaultLedgerEntry.node().redemptionDate()).hasValue(redemptionDate);
+ assertThat(vaultLedgerEntry.node().leVersion()).isEqualTo(UnsignedInteger.ONE);
Hash256 vaultId = vaultLedgerEntry.node().index();
logger.info("Vault created: {}", vaultId);
@@ -598,6 +621,9 @@ void lendingProtocolLifecycle() throws Exception {
() -> this.getValidatedTransaction(signedDeposit.hash(), VaultDeposit.class)
);
+ // LoanBrokerSet requires the Vault to be past its SubscriptionDate once it is closed-ended.
+ awaitSubscriptionDatePassed(subscriptionDate);
+
// ========== LOAN BROKER SET ==========
loanBrokerAccountInfo = this.scanForResult(
() -> this.getValidatedAccountInfo(loanBrokerKeyPair.publicKey().deriveAddress())
@@ -728,6 +754,7 @@ void lendingProtocolLifecycle() throws Exception {
.principalRequested(Amount.of("50000"))
.counterparty(borrowerKeyPair.publicKey().deriveAddress())
.paymentTotal(UnsignedInteger.valueOf(3))
+ .paymentInterval(UnsignedInteger.valueOf(60))
.data(LoanData.of("AABBCC"))
.signingPublicKey(loanBrokerKeyPair.publicKey())
.build();
@@ -1108,9 +1135,11 @@ void loanManageImpairAndUnimpair() throws JsonRpcClientErrorException, JsonProce
* strictly past the loan's {@code NextPaymentDueDate}.
*
* Under the {@code fixCleanup3_4_0} amendment, rippled only permits impairing a loan whose payment is actually
- * late (see rippled's {@code LoanManage::impairLoan}, which otherwise returns {@code tecTOO_SOON}). A standalone
- * rippled's close time advances much faster than wall-clock time, so pinning the loan to the 60-second minimum
- * {@code PaymentInterval} keeps this wait well under a second.
+ * late (see rippled's {@code LoanManage::impairLoan}, which otherwise returns {@code tecTOO_SOON}). A standalone,
+ * local rippled's close time can advance much faster than wall-clock time, so pinning the loan to the 60-second
+ * minimum {@code PaymentInterval} keeps this wait well under a second there. Against a real network (e.g. Devnet),
+ * though, ledger close time tracks wall-clock time roughly 1:1, so this genuinely takes close to a full
+ * {@code PaymentInterval} of real time to resolve; the timeout below is sized for that case.
*
* Note that rippled evaluates lateness against the applying ledger's {@code parentCloseTime}, whereas
* this polls the validated ledger's {@code closeTime}. Waiting on the latter is sufficient because close
@@ -1133,7 +1162,7 @@ private void awaitLoanPaymentLate(final Hash256 loanId) throws JsonRpcClientErro
given()
.pollInterval(Durations.ONE_HUNDRED_MILLISECONDS)
- .atMost(Duration.of(1, ChronoUnit.MINUTES))
+ .atMost(Duration.of(3, ChronoUnit.MINUTES))
.await()
.until(() -> getValidatedLedger().ledger().closeTime()
.orElseThrow(() -> new IllegalStateException("Validated ledger must have a closeTime."))
@@ -1292,6 +1321,27 @@ private Hash256 setupLoanBrokerAndXrpVault(KeyPair brokerKeyPair, XrpCurrencyAmo
() -> this.getValidatedAccountInfo(brokerKeyPair.publicKey().deriveAddress())
);
+ // LoanBrokerSet requires a closed-ended Vault once LendingProtocolV1_1 is enabled, so this Vault must
+ // define a SubscriptionDate/RedemptionDate pair and this method must wait for the ledger to pass
+ // SubscriptionDate before attaching a LoanBroker to it, below.
+ //
+ // SubscriptionDate must also leave enough real time for the VaultDeposit below to land before it -- deposits
+ // into a closed-ended Vault are only accepted before SubscriptionDate. A 5-second buffer is enough on a local
+ // rippled node with near-instant round trips, but against a real network (e.g. Devnet, ~4-5s ledger closes plus
+ // RPC latency for every call between VaultCreate and VaultDeposit) that window routinely closes before the
+ // deposit is submitted, and rippled rejects it with tecEXPIRED.
+ //
+ // RedemptionDate must also clear the Loan's own finalPayment date (StartDate + PaymentTotal * PaymentInterval,
+ // plus rippled's kLoanRedemptionBuffer), or the LoanSet submitted after this setup returns tecNO_PERMISSION.
+ // StartDate is whenever LoanSet actually validates, which is after this method already waited past
+ // SubscriptionDate and closed ledgers for LoanBrokerSet and LoanBrokerCoverDeposit, so a window sized to just
+ // the loan term leaves no slack for that setup latency. Pad it generously; VaultCreate allows up to
+ // MAX_INVESTMENT_PERIOD_SECONDS (~30 years), so there is ample room.
+ final UnsignedLong subscriptionDate = instantToXrpTimestamp(getMinExpirationTime().plus(Duration.ofSeconds(60)));
+ final UnsignedLong redemptionDate = subscriptionDate.plus(
+ VaultCreate.MIN_INVESTMENT_PERIOD_SECONDS.plus(UnsignedLong.valueOf(3600))
+ );
+
final VaultCreate vaultCreate = VaultCreate.builder()
.account(brokerKeyPair.publicKey().deriveAddress())
.fee(fee)
@@ -1299,6 +1349,9 @@ private Hash256 setupLoanBrokerAndXrpVault(KeyPair brokerKeyPair, XrpCurrencyAmo
.asset(Issue.XRP)
.assetsMaximum(Amount.of("10000000000"))
.withdrawalPolicy(WithdrawalPolicy.FIRST_COME_FIRST_SERVE)
+ .vaultKind(VaultKind.CLOSED_ENDED)
+ .subscriptionDate(subscriptionDate)
+ .redemptionDate(redemptionDate)
.signingPublicKey(brokerKeyPair.publicKey())
.build();
@@ -1318,7 +1371,12 @@ private Hash256 setupLoanBrokerAndXrpVault(KeyPair brokerKeyPair, XrpCurrencyAmo
LedgerSpecifier.VALIDATED
)
);
- final Hash256 vaultId = vaultEntry.node().index();
+ final VaultObject vault = vaultEntry.node();
+ assertThat(vault.vaultKind()).isEqualTo(VaultKind.CLOSED_ENDED);
+ assertThat(vault.subscriptionDate()).hasValue(subscriptionDate);
+ assertThat(vault.redemptionDate()).hasValue(redemptionDate);
+ assertThat(vault.leVersion()).isEqualTo(UnsignedInteger.ONE);
+ final Hash256 vaultId = vault.index();
final AccountInfoResult depositorAccountInfo = this.scanForResult(
() -> this.getValidatedAccountInfo(depositorKeyPair.publicKey().deriveAddress())
@@ -1340,6 +1398,9 @@ private Hash256 setupLoanBrokerAndXrpVault(KeyPair brokerKeyPair, XrpCurrencyAmo
assertThat(depositResult.engineResult()).isEqualTo(SUCCESS_STATUS);
this.scanForResult(() -> this.getValidatedTransaction(signedDeposit.hash(), VaultDeposit.class));
+ // LoanBrokerSet requires the Vault to be past its SubscriptionDate once it is closed-ended.
+ awaitSubscriptionDatePassed(subscriptionDate);
+
brokerAccountInfo = this.scanForResult(
() -> this.getValidatedAccountInfo(brokerKeyPair.publicKey().deriveAddress())
);
@@ -1394,6 +1455,24 @@ private Hash256 setupLoanBrokerAndXrpVault(KeyPair brokerKeyPair, XrpCurrencyAmo
return loanBrokerId;
}
+ /**
+ * Waits for the ledger's close time to pass {@code subscriptionDate}. {@link AbstractIT#scanForResult} caps its
+ * wait at {@link AbstractIT#AT_MOST_INTERVAL} (30 seconds), which is too short once SubscriptionDate is padded out
+ * far enough to survive real network latency (see setupLoanBrokerAndXrpVault()), so this polls with its own,
+ * longer timeout instead.
+ */
+ private void awaitSubscriptionDatePassed(UnsignedLong subscriptionDate) {
+ given()
+ .pollInterval(POLL_INTERVAL)
+ .atMost(Duration.ofSeconds(120))
+ .await()
+ .until(
+ this::getValidatedLedger,
+ ledgerResult -> FluentCompareTo.is(ledgerResult.ledger().closeTime().orElse(UnsignedLong.ZERO))
+ .greaterThan(subscriptionDate)
+ );
+ }
+
/**
* Sets up a signer list with two signers on the given account (quorum=2, each weight=1).
*/
diff --git a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/SingleAssetVaultIT.java b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/SingleAssetVaultIT.java
index 64c10cba0..602468795 100644
--- a/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/SingleAssetVaultIT.java
+++ b/xrpl4j-integration-tests/src/test/java/org/xrpl/xrpl4j/tests/SingleAssetVaultIT.java
@@ -1228,6 +1228,11 @@ void vaultWithIouAssetAndDomainId() throws Exception {
.build()
)
.destination(authorizedDepositorKeyPair.publicKey().deriveAddress())
+ .credentialIds(
+ getCredentialObjectIds(
+ credentialIssuerKeyPair, authorizedDepositorKeyPair, new CredentialType[]{credentialType}
+ )
+ )
.signingPublicKey(authorizedDepositorKeyPair.publicKey())
.build();
@@ -1360,6 +1365,7 @@ void vaultWithIouAssetAndDomainId() throws Exception {
.fee(FeeUtils.computeNetworkFees(feeResult).recommendedFee())
.sequence(vaultOwnerAccountInfo.accountData().sequence())
.vaultId(vaultId)
+ .memoData(VaultData.ofPlainText("closing vault"))
.signingPublicKey(vaultOwnerKeyPair.publicKey())
.build();
@@ -1371,9 +1377,10 @@ void vaultWithIouAssetAndDomainId() throws Exception {
assertThat(vaultDeleteSubmitResult.engineResult()).isEqualTo(SUCCESS_STATUS);
// Wait for transaction to be validated
- this.scanForResult(
+ VaultDelete validatedVaultDelete = this.scanForResult(
() -> this.getValidatedTransaction(signedVaultDelete.hash(), VaultDelete.class)
- );
+ ).transaction();
+ assertThat(validatedVaultDelete.memoData()).hasValue(VaultData.ofPlainText("closing vault"));
}
/**
diff --git a/xrpl4j-integration-tests/src/test/resources/xrpld/xrpld.cfg b/xrpl4j-integration-tests/src/test/resources/xrpld/xrpld.cfg
index 85d1e72f0..99eb33cb6 100644
--- a/xrpl4j-integration-tests/src/test/resources/xrpld/xrpld.cfg
+++ b/xrpl4j-integration-tests/src/test/resources/xrpld/xrpld.cfg
@@ -243,6 +243,7 @@ fixTokenEscrowV1
# 3.1.0 Amendments
SingleAssetVault
LendingProtocol
+LendingProtocolV1_1
# 3.1.3 Amendments
fixCleanup3_1_3
# 3.2.0 Amendments