@@ -145,13 +145,19 @@ default BatchFlags flags() {
145145 * The set of accounts that must appear in {@link #batchSigners()}, derived from the inner transactions.
146146 *
147147 * <p>rippled builds the identical set in {@code Batch::preflightSigValidated} and rejects any {@code BatchSigners}
148- * array that does not match it exactly. Each inner contributes its <em>initiator</em> — its {@code Delegate} when
149- * one is present, otherwise its {@code Account} — plus its {@code Sponsor} when the inner also carries a
150- * {@code SponsorSignature}. The outer {@link #account()} is excluded throughout, because it authorises its own
151- * inner transactions with the signature it puts on the Batch itself.
148+ * array that does not match it exactly. Each inner contributes:
149+ * <ul>
150+ * <li>its <em>initiator</em> — its {@code Delegate} when one is present, otherwise its {@code Account};</li>
151+ * <li>its {@code Sponsor}, when the inner also carries a {@code SponsorSignature}; and</li>
152+ * <li>a {@code LoanSet}'s {@code Counterparty}, when present.</li>
153+ * </ul>
154+ * The outer {@link #account()} is excluded throughout, because it authorises its own inner transactions with the
155+ * signature it puts on the Batch itself.
152156 *
153- * <p>rippled additionally reads an inner's {@code Counterparty}, but only {@code LoanSet} carries that field and
154- * {@code LoanSet} may not be an inner transaction, so that case cannot arise today and is not modelled here.
157+ * <p>The {@code Counterparty} branch is unreachable today — {@code LoanSet} is the only type carrying that field,
158+ * and rippled currently bars the entire Lending (XLS-66) and Single Asset Vault (XLS-65) families from being Batch
159+ * inners via its {@code kDisabledTxTypes} list. It is modelled anyway so that this derivation matches rippled's the
160+ * moment those families are permitted in Batches.
155161 *
156162 * @return An unmodifiable {@link Set} of {@link Address}es that must sign this Batch, which may be empty when every
157163 * inner transaction belongs to the outer account.
@@ -161,11 +167,20 @@ default Set<Address> requiredSigners() {
161167 final Address outerAccount = this .account ();
162168 return this .rawTransactions ().stream ()
163169 .map (RawTransactionWrapper ::rawTransaction )
164- .flatMap (innerTransaction -> Stream .concat (
165- Stream .of (innerTransaction .delegate ().orElseGet (innerTransaction ::account )),
166- innerTransaction .sponsorSignature ().isPresent () ?
167- innerTransaction .sponsor ().map (Stream ::of ).orElseGet (Stream ::empty ) : Stream .empty ()
168- ))
170+ .flatMap (innerTransaction -> {
171+ final Stream .Builder <Address > signers = Stream .builder ();
172+ // Initiator: the Delegate signs on behalf of the account holder when present, otherwise the Account.
173+ signers .add (innerTransaction .delegate ().orElseGet (innerTransaction ::account ));
174+ // A Sponsor must also sign when the inner carries a SponsorSignature.
175+ if (innerTransaction .sponsorSignature ().isPresent ()) {
176+ innerTransaction .sponsor ().ifPresent (signers ::add );
177+ }
178+ // A LoanSet's Counterparty must also sign (currently unreachable; see the Javadoc above).
179+ if (innerTransaction instanceof LoanSet ) {
180+ ((LoanSet ) innerTransaction ).counterparty ().ifPresent (signers ::add );
181+ }
182+ return signers .build ();
183+ })
169184 .filter (address -> !address .equals (outerAccount ))
170185 .collect (Collectors .collectingAndThen (Collectors .toCollection (LinkedHashSet ::new ), Collections ::unmodifiableSet ));
171186 }
@@ -338,34 +353,17 @@ default Batch checkBatchSigners() {
338353 firstSignerMatchingOuterAccount .orElse (null )
339354 );
340355
341- // Check 4: When BatchSigners is non-empty, every account required to sign an inner transaction (excluding
342- // the outer account) must have a corresponding BatchSigner entry.
356+ // Check 4: When BatchSigners is non-empty, every account required to sign an inner transaction must have a
357+ // corresponding BatchSigner entry. The required-signer set (initiator, sponsor, and — once permitted —
358+ // LoanSet counterparty, with the outer account excluded) is derived by requiredSigners(), which is the single
359+ // source of truth shared with fee computation.
343360 if (!this .batchSigners ().isEmpty ()) {
344- // Compute the set of accounts that require signatures (excluding the outer account, which signs the Batch
345- // itself). For each inner transaction, the required signer is its Delegate if present (the delegate signs
346- // on behalf of the account holder), otherwise the transaction's Account. A LoanSet's Counterparty must also
347- // sign, if present. Note: rippled also requires a Sponsor to sign when SponsorSignature is present, but
348- // xrpl4j does not yet model sfSponsor/sfSponsorSignature, so that case cannot be checked here.
349- final Set <Address > requiredSignerAccounts = this .rawTransactions ().stream ()
350- .flatMap (wrapper -> {
351- final Transaction innerTransaction = wrapper .rawTransaction ();
352- final Stream .Builder <Address > requiredSigners = Stream .builder ();
353- requiredSigners .add (innerTransaction .delegate ().orElseGet (innerTransaction ::account ));
354- if (innerTransaction instanceof LoanSet ) {
355- ((LoanSet ) innerTransaction ).counterparty ().ifPresent (requiredSigners ::add );
356- }
357- return requiredSigners .build ();
358- })
359- .filter (account -> !account .equals (this .account ()))
360- .collect (Collectors .toSet ());
361-
362- // Compute the set of accounts that actually provided signatures
363361 final Set <Address > actualSignerAccounts = this .batchSigners ().stream ()
364362 .map (wrapper -> wrapper .batchSigner ().account ())
365363 .collect (Collectors .toSet ());
366364
367- // Find the first inner-transaction account (excluding the outer account) that has no BatchSigner entry.
368- final Optional <Address > missingSignerAccount = requiredSignerAccounts .stream ()
365+ // Find the first required signer that has no BatchSigner entry.
366+ final Optional <Address > missingSignerAccount = this . requiredSigners () .stream ()
369367 .filter (account -> !actualSignerAccounts .contains (account ))
370368 .findFirst ();
371369
0 commit comments