Skip to content

Commit e8fbe0a

Browse files
committed
Initial commit
1 parent 7d46f9b commit e8fbe0a

10 files changed

Lines changed: 1385 additions & 655 deletions

File tree

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
package org.xrpl.xrpl4j.model.client.fees;
2+
3+
/*-
4+
* ========================LICENSE_START=================================
5+
* xrpl4j :: core
6+
* %%
7+
* Copyright (C) 2020 - 2023 XRPL Foundation and its contributors
8+
* %%
9+
* Licensed under the Apache License, Version 2.0 (the "License");
10+
* you may not use this file except in compliance with the License.
11+
* You may obtain a copy of the License at
12+
*
13+
* http://www.apache.org/licenses/LICENSE-2.0
14+
*
15+
* Unless required by applicable law or agreed to in writing, software
16+
* distributed under the License is distributed on an "AS IS" BASIS,
17+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* See the License for the specific language governing permissions and
19+
* limitations under the License.
20+
* =========================LICENSE_END==================================
21+
*/
22+
23+
import com.google.common.annotations.Beta;
24+
import com.google.common.base.Preconditions;
25+
import com.google.common.collect.ImmutableSet;
26+
import com.google.common.primitives.UnsignedInteger;
27+
import org.immutables.value.Value;
28+
import org.xrpl.xrpl4j.model.transactions.Address;
29+
import org.xrpl.xrpl4j.model.transactions.Batch;
30+
import org.xrpl.xrpl4j.model.transactions.RawTransactionWrapper;
31+
import org.xrpl.xrpl4j.model.transactions.Transaction;
32+
import org.xrpl.xrpl4j.model.transactions.TransactionType;
33+
import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount;
34+
35+
import java.util.Map;
36+
import java.util.Optional;
37+
import java.util.Set;
38+
39+
/**
40+
* The inputs to {@link FeeUtils#computeFee(FeeParams)}.
41+
*
42+
* <p>Everything that can be read from the {@link #transaction()} is read from it — the transaction type and therefore
43+
* any per-type surcharge, an {@code EscrowFinish}'s fulfillment, a {@link Batch}'s inner transactions and the accounts
44+
* that must sign it. The remaining fields are the things that are not knowable from the transaction: how each party
45+
* intends to sign, which cannot be derived because the fee is signed over and the signatures therefore do not exist
46+
* yet, plus two values that {@link FeeResult} does not carry.
47+
*
48+
* <p>Every field other than {@link #feeResult()} and {@link #transaction()} has a default, so the common case is
49+
* {@code FeeParams.builder().feeResult(feeResult).transaction(transaction).build()}.
50+
*
51+
* <p>This class will be marked {@link Beta} until the featureBatch and featureSponsorship amendments are enabled on
52+
* mainnet. Its API is subject to change.</p>
53+
*/
54+
@Value.Immutable
55+
@Beta
56+
public interface FeeParams {
57+
58+
/**
59+
* The maximum number of entries in an XRPL signer list, and therefore the most signatures any one party can supply.
60+
*/
61+
UnsignedInteger MAX_SIGNER_LIST_SIZE = UnsignedInteger.valueOf(32);
62+
63+
/**
64+
* The largest number of fee increments a {@code LoanPay} transaction can be charged, being
65+
* {@code kLoanMaximumPaymentsPerTransaction / kLoanPaymentsPerFeeIncrement}.
66+
*/
67+
UnsignedInteger MAX_LOAN_PAYMENT_FEE_INCREMENTS = UnsignedInteger.valueOf(20);
68+
69+
/**
70+
* The {@link TransactionType}s whose fee is one owner reserve increment rather than a multiple of the base fee.
71+
*/
72+
Set<TransactionType> OWNER_RESERVE_TRANSACTION_TYPES = ImmutableSet.of(
73+
TransactionType.ACCOUNT_DELETE,
74+
TransactionType.AMM_CREATE
75+
);
76+
77+
/**
78+
* The pseudo-transaction {@link TransactionType}s, which are created by consensus rather than submitted, and
79+
* therefore carry no fee.
80+
*/
81+
Set<TransactionType> PSEUDO_TRANSACTION_TYPES = ImmutableSet.of(
82+
TransactionType.ENABLE_AMENDMENT,
83+
TransactionType.SET_FEE,
84+
TransactionType.UNL_MODIFY
85+
);
86+
87+
/**
88+
* Construct a builder for this class.
89+
*
90+
* @return An {@link ImmutableFeeParams.Builder}.
91+
*/
92+
static ImmutableFeeParams.Builder builder() {
93+
return ImmutableFeeParams.builder();
94+
}
95+
96+
/**
97+
* The current network fee levels, obtained by querying the ledger (e.g., via {@code XrplClient#fee()}).
98+
*
99+
* @return A {@link FeeResult}.
100+
*/
101+
FeeResult feeResult();
102+
103+
/**
104+
* The transaction being priced. It need not be signed, and its {@code Fee} field is ignored; a placeholder of zero
105+
* is customary while the real fee is being computed.
106+
*
107+
* @return The {@link Transaction} to price.
108+
*/
109+
Transaction transaction();
110+
111+
/**
112+
* The number of signatures the transaction's own account will supply in its {@code Signers} array.
113+
*
114+
* <p>Zero for a single-signed transaction: rippled charges only for the <em>additional</em> signatures of a
115+
* multi-signature, since the first is already covered by the base fee.
116+
*
117+
* @return An {@link UnsignedInteger} number of signatures, defaulting to zero.
118+
*/
119+
@Value.Default
120+
default UnsignedInteger signersCount() {
121+
return UnsignedInteger.ZERO;
122+
}
123+
124+
/**
125+
* The number of signatures the transaction's sponsor will supply in its {@code SponsorSignature.Signers} array.
126+
*
127+
* <p>Zero when the transaction is unsponsored, and also when the sponsor signs with a single key: rippled counts
128+
* only {@code SponsorSignature.Signers} entries, and a lone sponsor signature is carried in
129+
* {@code SponsorSignature.TxnSignature} instead.
130+
*
131+
* @return An {@link UnsignedInteger} number of signatures, defaulting to zero.
132+
*/
133+
@Value.Default
134+
default UnsignedInteger sponsorSignersCount() {
135+
return UnsignedInteger.ZERO;
136+
}
137+
138+
/**
139+
* The total number of signatures in a {@code LoanSet}'s {@code CounterpartySignature}.
140+
*
141+
* <p>Unlike {@link #signersCount()} and {@link #sponsorSignersCount()}, this is a total rather than a count of
142+
* additional signatures: rippled charges a base fee even for a single counterparty signature. One is therefore the
143+
* correct value for a counterparty signing with a single key, and is the default.
144+
*
145+
* @return An {@link UnsignedInteger} number of signatures, defaulting to one.
146+
*/
147+
@Value.Default
148+
default UnsignedInteger counterpartySignatureCount() {
149+
return UnsignedInteger.ONE;
150+
}
151+
152+
/**
153+
* The number of fee increments a {@code LoanPay} transaction will be charged, being one increment per
154+
* {@code kLoanPaymentsPerFeeIncrement} payments the transaction is estimated to make.
155+
*
156+
* <p>Deriving this requires the {@code Loan}, {@code LoanBroker} and {@code Vault} ledger objects, so it is supplied
157+
* rather than computed. One — a single payment — is the default.
158+
*
159+
* @return An {@link UnsignedInteger} number of fee increments, defaulting to one.
160+
*/
161+
@Value.Default
162+
default UnsignedInteger loanPaymentFeeIncrements() {
163+
return UnsignedInteger.ONE;
164+
}
165+
166+
/**
167+
* The owner reserve increment, required when pricing an {@code AccountDelete} or {@code AMMCreate} — including one
168+
* that is an inner transaction of a {@link Batch}.
169+
*
170+
* <p>Those transactions cost exactly one owner reserve increment rather than a multiple of the base fee, and that
171+
* increment is not carried on {@link FeeResult}. It can be read from {@code ServerInfo} (e.g.
172+
* {@code serverInfo.validatedLedger().get().reserveIncXrp()}).
173+
*
174+
* @return An optionally-present {@link XrpCurrencyAmount}.
175+
*/
176+
Optional<XrpCurrencyAmount> ownerReserve();
177+
178+
/**
179+
* How many signatures each {@link Batch} participant will supply, for participants that multi-sign.
180+
*
181+
* <p>This is needed only when pricing a Batch <em>before</em> its {@code BatchSigners} exist — a wallet displaying
182+
* a fee up front, for instance. Once signatures have been collected, the counts are read from
183+
* {@link Batch#batchSigners()} and this map is neither needed nor permitted. Participants absent from the map are
184+
* counted as signing with a single key.
185+
*
186+
* <p>Keys must be members of {@link Batch#requiredSigners()} — the accounts that must sign, derived from the inner
187+
* transactions. Note that a required signer is not always an inner's {@code Account}: a delegated inner is signed by
188+
* its {@code Delegate}, and a sponsored inner also requires its {@code Sponsor}.
189+
*
190+
* @return A {@link Map} from a Batch participant's {@link Address} to the number of signatures it will supply,
191+
* defaulting to empty.
192+
*/
193+
Map<Address, UnsignedInteger> signaturesPerBatchSigner();
194+
195+
/**
196+
* Validates that the supplied fields are consistent with the {@link #transaction()} being priced, so that a value
197+
* which would be silently ignored is rejected instead.
198+
*/
199+
@Value.Check
200+
default void check() {
201+
final TransactionType transactionType = this.transaction().transactionType();
202+
203+
Preconditions.checkArgument(
204+
!PSEUDO_TRANSACTION_TYPES.contains(transactionType),
205+
"%s is a pseudo-transaction. Pseudo-transactions are created by consensus rather than submitted, and carry " +
206+
"no fee.", transactionType
207+
);
208+
Preconditions.checkArgument(
209+
transactionType != TransactionType.UNKNOWN,
210+
"The fee of an unknown transaction type cannot be computed, because its fee rules are not known."
211+
);
212+
213+
checkSignatureCount(this.signersCount(), "signersCount");
214+
checkSignatureCount(this.sponsorSignersCount(), "sponsorSignersCount");
215+
checkSignatureCount(this.counterpartySignatureCount(), "counterpartySignatureCount");
216+
217+
Preconditions.checkArgument(
218+
transactionType == TransactionType.LOAN_SET ||
219+
this.counterpartySignatureCount().equals(UnsignedInteger.ONE),
220+
"counterpartySignatureCount applies only to a LoanSet, but the transaction is a %s.", transactionType
221+
);
222+
223+
Preconditions.checkArgument(
224+
transactionType == TransactionType.LOAN_PAY ||
225+
this.loanPaymentFeeIncrements().equals(UnsignedInteger.ONE),
226+
"loanPaymentFeeIncrements applies only to a LoanPay, but the transaction is a %s.", transactionType
227+
);
228+
Preconditions.checkArgument(
229+
this.loanPaymentFeeIncrements().compareTo(UnsignedInteger.ONE) >= 0 &&
230+
this.loanPaymentFeeIncrements().compareTo(MAX_LOAN_PAYMENT_FEE_INCREMENTS) <= 0,
231+
"loanPaymentFeeIncrements must be between 1 and %s, but was %s.",
232+
MAX_LOAN_PAYMENT_FEE_INCREMENTS, this.loanPaymentFeeIncrements()
233+
);
234+
235+
Preconditions.checkArgument(
236+
this.ownerReserve().isPresent() == this.requiresOwnerReserve(),
237+
"ownerReserve must be supplied for %s, and only for those types, whether standalone or as a Batch inner " +
238+
"transaction. Transaction was a %s.", OWNER_RESERVE_TRANSACTION_TYPES, transactionType
239+
);
240+
241+
this.checkSignaturesPerBatchSigner(transactionType);
242+
}
243+
244+
/**
245+
* Determines whether the transaction being priced costs an owner reserve increment, either because it is one of
246+
* {@link #OWNER_RESERVE_TRANSACTION_TYPES} or because it is a {@link Batch} containing one.
247+
*
248+
* @return {@code true} if {@link #ownerReserve()} is required.
249+
*/
250+
@Value.Derived
251+
default boolean requiresOwnerReserve() {
252+
if (OWNER_RESERVE_TRANSACTION_TYPES.contains(this.transaction().transactionType())) {
253+
return true;
254+
}
255+
return this.transaction() instanceof Batch &&
256+
((Batch) this.transaction()).rawTransactions().stream()
257+
.map(RawTransactionWrapper::rawTransaction)
258+
.map(Transaction::transactionType)
259+
.anyMatch(OWNER_RESERVE_TRANSACTION_TYPES::contains);
260+
}
261+
262+
/**
263+
* Validates {@link #signaturesPerBatchSigner()} against the transaction being priced.
264+
*
265+
* @param transactionType The {@link TransactionType} of {@link #transaction()}.
266+
*/
267+
default void checkSignaturesPerBatchSigner(final TransactionType transactionType) {
268+
if (this.signaturesPerBatchSigner().isEmpty()) {
269+
return;
270+
}
271+
272+
Preconditions.checkArgument(
273+
transactionType == TransactionType.BATCH,
274+
"signaturesPerBatchSigner applies only to a Batch, but the transaction is a %s.", transactionType
275+
);
276+
277+
final Batch batch = (Batch) this.transaction();
278+
Preconditions.checkArgument(
279+
batch.batchSigners().isEmpty(),
280+
"signaturesPerBatchSigner is only for pricing a Batch before its signatures exist. This Batch already has " +
281+
"BatchSigners, so the counts are read from them instead."
282+
);
283+
284+
final Set<Address> requiredSigners = batch.requiredSigners();
285+
this.signaturesPerBatchSigner().forEach((address, count) -> {
286+
Preconditions.checkArgument(
287+
requiredSigners.contains(address),
288+
"%s is not required to sign this Batch, so a signature count for it would be ignored. The accounts that " +
289+
"must sign are %s. Note that a required signer is not always an inner transaction's Account: a delegated " +
290+
"inner is signed by its Delegate, and a sponsored inner also requires its Sponsor.",
291+
address, requiredSigners
292+
);
293+
checkSignatureCount(count, "signaturesPerBatchSigner[" + address + "]");
294+
});
295+
}
296+
297+
/**
298+
* Asserts that a supplied signature count does not exceed the XRPL signer list limit.
299+
*
300+
* @param signatureCount An {@link UnsignedInteger} number of signatures.
301+
* @param fieldName The name of the field being checked, for use in the failure message.
302+
*/
303+
static void checkSignatureCount(final UnsignedInteger signatureCount, final String fieldName) {
304+
Preconditions.checkArgument(
305+
signatureCount.compareTo(MAX_SIGNER_LIST_SIZE) <= 0,
306+
"%s must not exceed %s (the XRPL signer list limit), but was %s.",
307+
fieldName, MAX_SIGNER_LIST_SIZE, signatureCount
308+
);
309+
}
310+
}

0 commit comments

Comments
 (0)