@@ -1145,10 +1145,126 @@ void testBatchWithOuterSignerAsMultiSigSignerInBatchSigner() {
11451145 .hasMessageContaining ("The Account submitting a Batch transaction must not sign any inner transactions." );
11461146 }
11471147
1148+ // ///////////////
1149+ // requiredSigners()
1150+ // ///////////////
1151+
1152+ @ Test
1153+ void requiredSignersEmptyWhenAllInnersBelongToOuterAccount () {
1154+ Batch batch = batchWithInners (ACCOUNT ,
1155+ createInnerPayment (ACCOUNT , UnsignedInteger .ONE ),
1156+ createInnerPayment (ACCOUNT , UnsignedInteger .valueOf (2 )));
1157+
1158+ assertThat (batch .requiredSigners ()).isEmpty ();
1159+ }
1160+
1161+ @ Test
1162+ void requiredSignersAreTheDistinctNonOuterInnerAccounts () {
1163+ Address bob = randomAddress ();
1164+ Address carol = randomAddress ();
1165+ Batch batch = batchWithInners (ACCOUNT ,
1166+ createInnerPayment (bob , UnsignedInteger .ONE ),
1167+ createInnerPayment (carol , UnsignedInteger .valueOf (2 )));
1168+
1169+ assertThat (batch .requiredSigners ()).containsExactlyInAnyOrder (bob , carol );
1170+ }
1171+
1172+ @ Test
1173+ void requiredSignersUsesDelegateRatherThanAccount () {
1174+ Address bob = randomAddress ();
1175+ Address delegate = randomAddress ();
1176+ Batch batch = batchWithInners (ACCOUNT ,
1177+ createInnerPayment (bob , delegate , UnsignedInteger .ONE ),
1178+ createInnerPayment (ACCOUNT , UnsignedInteger .valueOf (2 )));
1179+
1180+ // A delegated inner is authorised by its delegate, so bob (the account) is not required, delegate is.
1181+ assertThat (batch .requiredSigners ()).containsExactly (delegate );
1182+ }
1183+
1184+ @ Test
1185+ void requiredSignersIncludesSponsorOnlyWhenSponsorSignatureIsPresent () {
1186+ Address bob = randomAddress ();
1187+ Address sponsor = randomAddress ();
1188+
1189+ Payment outerInner = createInnerPayment (ACCOUNT , UnsignedInteger .valueOf (2 ));
1190+
1191+ Batch withSponsorSignature = batchWithInners (ACCOUNT ,
1192+ sponsoredInner (bob , sponsor , UnsignedInteger .ONE , true ), outerInner );
1193+ assertThat (withSponsorSignature .requiredSigners ()).containsExactlyInAnyOrder (bob , sponsor );
1194+
1195+ Batch withoutSponsorSignature = batchWithInners (ACCOUNT ,
1196+ sponsoredInner (bob , sponsor , UnsignedInteger .ONE , false ), outerInner );
1197+ assertThat (withoutSponsorSignature .requiredSigners ()).containsExactly (bob );
1198+ }
1199+
1200+ @ Test
1201+ void requiredSignersDeduplicatesAcrossInnersAndRoles () {
1202+ Address bob = randomAddress ();
1203+ Batch batch = batchWithInners (ACCOUNT ,
1204+ createInnerPayment (bob , UnsignedInteger .ONE ), // bob as account
1205+ sponsoredInner (randomAddress (), bob , UnsignedInteger .valueOf (2 ), true )); // bob again as sponsor
1206+
1207+ // bob is required via two different inners/roles, but appears once.
1208+ assertThat (batch .requiredSigners ()).hasSize (2 ).contains (bob );
1209+ }
1210+
1211+ @ Test
1212+ void requiredSignersExcludesOuterAccountInEveryRole () {
1213+ Address other = randomAddress ();
1214+ Batch batch = batchWithInners (ACCOUNT ,
1215+ createInnerPayment (ACCOUNT , UnsignedInteger .ONE ), // outer as account
1216+ createInnerPayment (other , ACCOUNT , UnsignedInteger .valueOf (2 )), // outer as delegate
1217+ sponsoredInner (other , ACCOUNT , UnsignedInteger .valueOf (3 ), true )); // outer as sponsor
1218+
1219+ // The outer account authorises its own inners with its Batch signature, so it is never a required signer.
1220+ assertThat (batch .requiredSigners ()).containsExactly (other );
1221+ }
1222+
1223+ @ Test
1224+ void requiredSignersIsUnmodifiable () {
1225+ Batch batch = batchWithInners (ACCOUNT ,
1226+ createInnerPayment (randomAddress (), UnsignedInteger .ONE ),
1227+ createInnerPayment (randomAddress (), UnsignedInteger .valueOf (2 )));
1228+
1229+ assertThatThrownBy (() -> batch .requiredSigners ().clear ())
1230+ .isInstanceOf (UnsupportedOperationException .class );
1231+ }
1232+
11481233 // ///////////////
11491234 // Private Helpers
11501235 // ///////////////
11511236
1237+ private static Address randomAddress () {
1238+ return Seed .ed25519Seed ().deriveKeyPair ().publicKey ().deriveAddress ();
1239+ }
1240+
1241+ private Batch batchWithInners (Address outerAccount , Payment ... inners ) {
1242+ final List <RawTransactionWrapper > wrappers = Lists .newArrayList ();
1243+ for (Payment inner : inners ) {
1244+ wrappers .add (RawTransactionWrapper .of (inner ));
1245+ }
1246+ return Batch .builder ()
1247+ .account (outerAccount )
1248+ .fee (XrpCurrencyAmount .ofDrops (0 ))
1249+ .sequence (UnsignedInteger .ONE )
1250+ .flags (BatchFlags .ALL_OR_NOTHING )
1251+ .rawTransactions (wrappers )
1252+ .build ();
1253+ }
1254+
1255+ private Payment sponsoredInner (
1256+ Address account , Address sponsor , UnsignedInteger sequence , boolean withSponsorSignature
1257+ ) {
1258+ ImmutablePayment .Builder builder = Payment .builder ()
1259+ .from (createInnerPayment (account , sequence ))
1260+ .sponsor (sponsor )
1261+ .sponsorFlags (SponsorFlags .SPONSOR_RESERVE );
1262+ if (withSponsorSignature ) {
1263+ builder .sponsorSignature (SponsorSignature .builder ().build ());
1264+ }
1265+ return builder .build ();
1266+ }
1267+
11521268 private Batch createValidBatch (BatchFlags batchFlags ) {
11531269 return Batch .builder ()
11541270 .account (ACCOUNT )
0 commit comments