Skip to content

Commit d5ce28f

Browse files
authored
Merge branch 'main' into account-permission-delegation
2 parents af2fc8f + 3bda0db commit d5ce28f

38 files changed

+5752
-114
lines changed

xrpl4j-client/src/test/java/org/xrpl/xrpl4j/client/XrplClientTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -659,7 +659,7 @@ public <T extends XrplResult> T send(
659659
JsonRpcRequest request,
660660
JavaType resultType
661661
) {
662-
SubmitMultiSignedResult submitMultiSignedResult = SubmitMultiSignedResult.builder()
662+
SubmitMultiSignedResult<?> submitMultiSignedResult = SubmitMultiSignedResult.builder()
663663
.engineResult("tesSUCCESS")
664664
.engineResultCode(200)
665665
.engineResultMessage("Submitted")

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/binary/XrplBinaryCodec.java

Lines changed: 120 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@
3434
import org.xrpl.xrpl4j.codec.binary.types.AccountIdType;
3535
import org.xrpl.xrpl4j.codec.binary.types.STObjectType;
3636
import org.xrpl.xrpl4j.codec.binary.types.UInt64Type;
37+
import org.xrpl.xrpl4j.crypto.HashingUtils;
38+
import org.xrpl.xrpl4j.crypto.signing.SignedTransaction;
39+
import org.xrpl.xrpl4j.model.transactions.Address;
40+
import org.xrpl.xrpl4j.model.transactions.Batch;
41+
import org.xrpl.xrpl4j.model.transactions.Hash256;
42+
import org.xrpl.xrpl4j.model.transactions.RawTransactionWrapper;
3743

3844
import java.util.Map;
3945
import java.util.Objects;
@@ -47,11 +53,13 @@ public class XrplBinaryCodec {
4753
public static final String TRX_MULTI_SIGNATURE_PREFIX = "534D5400";
4854

4955
public static final String PAYMENT_CHANNEL_CLAIM_SIGNATURE_PREFIX = "434C4D00";
56+
public static final String BATCH_SIGNATURE_PREFIX = "42434800"; // "BCH\0" per XLS-0056
57+
5058
public static final String CHANNEL_FIELD_NAME = "Channel";
5159
public static final String AMOUNT_FIELD_NAME = "Amount";
5260

53-
private static final DefinitionsService definitionsService = DefinitionsService.getInstance();
54-
private static final ObjectMapper objectMapper = BinaryCodecObjectMapperFactory.getObjectMapper();
61+
private static final DefinitionsService DEFINITIONS_SERVICE = DefinitionsService.getInstance();
62+
private static final ObjectMapper BINARY_CODEC_OBJECT_MAPPER = BinaryCodecObjectMapperFactory.getObjectMapper();
5563

5664
private static final XrplBinaryCodec INSTANCE = new XrplBinaryCodec();
5765

@@ -70,11 +78,12 @@ public static XrplBinaryCodec getInstance() {
7078
* @param json A {@link String} containing JSON to be encoded.
7179
*
7280
* @return A {@link String} containing the hex-encoded representation of {@code json}.
81+
*
7382
* @throws JsonProcessingException if {@code json} is not valid JSON.
7483
*/
7584
public String encode(String json) throws JsonProcessingException {
7685
Objects.requireNonNull(json);
77-
JsonNode node = BinaryCodecObjectMapperFactory.getObjectMapper().readTree(json);
86+
JsonNode node = BINARY_CODEC_OBJECT_MAPPER.readTree(json);
7887
return encode(node);
7988
}
8089

@@ -84,7 +93,6 @@ public String encode(String json) throws JsonProcessingException {
8493
* @param jsonNode A {@link JsonNode} containing JSON to be encoded.
8594
*
8695
* @return A {@link String} containing the hex-encoded representation of {@code jsonNode}.
87-
* @throws JsonProcessingException if {@code jsonNode} is not valid JSON.
8896
*/
8997
private String encode(final JsonNode jsonNode) {
9098
Objects.requireNonNull(jsonNode);
@@ -99,10 +107,11 @@ private String encode(final JsonNode jsonNode) {
99107
* @param json String containing JSON to be encoded.
100108
*
101109
* @return hex encoded representations
110+
*
102111
* @throws JsonProcessingException if JSON is not valid.
103112
*/
104113
public String encodeForSigning(String json) throws JsonProcessingException {
105-
JsonNode node = BinaryCodecObjectMapperFactory.getObjectMapper().readTree(json);
114+
JsonNode node = BINARY_CODEC_OBJECT_MAPPER.readTree(json);
106115
return TRX_SIGNATURE_PREFIX + encode(removeNonSigningFields(node));
107116
}
108117

@@ -113,10 +122,11 @@ public String encodeForSigning(String json) throws JsonProcessingException {
113122
* @param xrpAccountId A {@link String} containing the XRPL AccountId.
114123
*
115124
* @return hex encoded representations
125+
*
116126
* @throws JsonProcessingException if JSON is not valid.
117127
*/
118128
public String encodeForMultiSigning(String json, String xrpAccountId) throws JsonProcessingException {
119-
JsonNode node = BinaryCodecObjectMapperFactory.getObjectMapper().readTree(json);
129+
JsonNode node = BINARY_CODEC_OBJECT_MAPPER.readTree(json);
120130
if (!node.isObject()) {
121131
throw new IllegalArgumentException("JSON object required for signing");
122132
}
@@ -126,17 +136,90 @@ public String encodeForMultiSigning(String json, String xrpAccountId) throws Jso
126136
return TRX_MULTI_SIGNATURE_PREFIX + encode(removeNonSigningFields(node)) + suffix;
127137
}
128138

139+
/**
140+
* Encodes a {@link Batch} transaction to canonical XRPL binary as a hex string for signing purposes. Note that this
141+
* function slightly diverges from the pattern of the other encodeForSigning functions because the bytes to be signed
142+
* for a Batch transaction are not simply the canonical binary representation of the JSON. Instead, we have distinct
143+
* portions of the Batch transaction that are signed. Also, unlike {@link #encodeForSigning(String)}, which accepts
144+
* JSON and then checks for JsonNode values, this implementation instead accepts a well-typed Java object and operates
145+
* on that, for safety and correctness.
146+
*
147+
* @param batch A {@link Batch} containing JSON to be encoded.
148+
*
149+
* @return hex encoded representations
150+
*
151+
* @throws JsonProcessingException if JSON is not valid.
152+
*/
153+
public UnsignedByteArray encodeForBatchInnerSigning(Batch batch) throws JsonProcessingException {
154+
Objects.requireNonNull(batch);
155+
try {
156+
// Start with batch prefix (0x42434800 = "BCH\0")
157+
UnsignedByteArray signableBytes = UnsignedByteArray.fromHex(XrplBinaryCodec.BATCH_SIGNATURE_PREFIX);
158+
159+
// Add flags (4 bytes, big-endian)
160+
HashingUtils.addUInt32(signableBytes, (int) batch.flags().getValue());
161+
162+
// Add count of inner transactions (4 bytes, big-endian)
163+
HashingUtils.addUInt32(signableBytes, batch.rawTransactions().size());
164+
165+
// Add each inner transaction ID (32 bytes each)
166+
for (RawTransactionWrapper wrapper : batch.rawTransactions()) {
167+
final UnsignedByteArray transactionId = computeInnerBatchTransactionId(wrapper);
168+
signableBytes.append(transactionId);
169+
}
170+
171+
return signableBytes;
172+
} catch (JsonProcessingException e) {
173+
// Test Coverage Note: this catch block is for defensive error handling and is otherwise challenging to test
174+
// in a unit test without mocking static fields or using reflection to create malformed objects, which would
175+
// not be representative of real usage scenarios. In practice, JsonProcessingException should never be thrown
176+
// during normal operation with valid objects, which immutables typically will enforce.
177+
throw new RuntimeException(e.getMessage(), e);
178+
}
179+
}
180+
181+
/**
182+
* Encode a {@link Batch} for multi-signing by a specific signer. This is used when a multi-sig account acts as a
183+
* BatchSigner with nested Signers. Per rippled's checkBatchMultiSign, this uses batch serialization (serializeBatch)
184+
* followed by appending the signer's account ID (finishMultiSigningData).
185+
*
186+
* @param batch The {@link Batch} to encode.
187+
* @param signerAddress The address of the signer (will be appended as account ID suffix).
188+
*
189+
* @return An {@link UnsignedByteArray} containing the batch serialization with account ID suffix.
190+
*
191+
* @throws JsonProcessingException if there is an error processing the JSON.
192+
*/
193+
public UnsignedByteArray encodeForBatchInnerMultiSigning(Batch batch, Address signerAddress)
194+
throws JsonProcessingException {
195+
Objects.requireNonNull(batch);
196+
Objects.requireNonNull(signerAddress);
197+
198+
// Start with batch serialization (HashPrefix::batch + flags + count + tx IDs)
199+
UnsignedByteArray batchBytes = encodeForBatchInnerSigning(batch);
200+
201+
// Create a copy to avoid mutating the original (since UnsignedByteArray.append() mutates)
202+
UnsignedByteArray result = UnsignedByteArray.of(batchBytes.toByteArray());
203+
204+
// Append the signer's account ID (like finishMultiSigningData does in rippled)
205+
String accountIdHex = new AccountIdType().fromJson(new TextNode(signerAddress.value())).toHex();
206+
result.append(UnsignedByteArray.fromHex(accountIdHex));
207+
208+
return result;
209+
}
210+
129211
/**
130212
* Encodes JSON to canonical XRPL binary as a hex string for signing payment channel claims. The only JSON fields
131213
* which will be encoded are "Channel" and "Amount".
132214
*
133215
* @param json String containing JSON to be encoded.
134216
*
135217
* @return The binary encoded JSON in hexadecimal form.
218+
*
136219
* @throws JsonProcessingException If the JSON is not valid.
137220
*/
138221
public String encodeForSigningClaim(String json) throws JsonProcessingException {
139-
JsonNode node = BinaryCodecObjectMapperFactory.getObjectMapper().readTree(json);
222+
JsonNode node = BINARY_CODEC_OBJECT_MAPPER.readTree(json);
140223
if (!node.isObject()) {
141224
throw new IllegalArgumentException("JSON object required for signing");
142225
}
@@ -201,12 +284,40 @@ private JsonNode removeNonSigningFields(JsonNode node) {
201284
.filter(this::isSigningField)
202285
.collect(Collectors.toMap(Function.identity(), node::get));
203286

204-
return new ObjectNode(objectMapper.getNodeFactory(), signingFields);
287+
return new ObjectNode(BINARY_CODEC_OBJECT_MAPPER.getNodeFactory(), signingFields);
205288
}
206289

207290
private Boolean isSigningField(String fieldName) {
208-
return definitionsService.getFieldInstance(fieldName).map(FieldInstance::isSigningField).orElse(false);
291+
return DEFINITIONS_SERVICE.getFieldInstance(fieldName).map(FieldInstance::isSigningField).orElse(false);
209292
}
210293

294+
/**
295+
* Computes the transaction ID for an unsigned inner transaction.
296+
*
297+
* @param rawTransactionWrapper A {@link RawTransactionWrapper} with an unsigned inner transaction, used for Batch.
298+
*
299+
* @return A {@link Hash256} containing the transaction's transaction ID.
300+
*/
301+
private UnsignedByteArray computeInnerBatchTransactionId(final RawTransactionWrapper rawTransactionWrapper)
302+
throws JsonProcessingException {
303+
Objects.requireNonNull(rawTransactionWrapper);
211304

305+
String txJson = BINARY_CODEC_OBJECT_MAPPER.writeValueAsString(rawTransactionWrapper.rawTransaction());
306+
// Parse the JSON and ensure SigningPubKey is set to empty string for inner transactions
307+
JsonNode txNode = removeNonSigningFields(BINARY_CODEC_OBJECT_MAPPER.readTree(txJson));
308+
if (txNode.isObject()) {
309+
final ObjectNode objNode = (ObjectNode) txNode;
310+
// Fix Flags field if it's serialized as an object instead of a number
311+
// NOTE: Once https://github.com/XRPLF/xrpl4j/issues/649 is implemented, this line can be removed.
312+
final JsonNode flagsNode = objNode.get("Flags");
313+
if (flagsNode != null && flagsNode.isObject() && flagsNode.has("value")) {
314+
objNode.put("Flags", flagsNode.get("value").asLong());
315+
}
316+
}
317+
final String txHex = this.encode(txNode);
318+
final UnsignedByteArray txBytes = UnsignedByteArray.fromHex(
319+
SignedTransaction.SIGNED_TRANSACTION_HASH_PREFIX + txHex
320+
);
321+
return HashingUtils.sha512Half(txBytes);
322+
}
212323
}

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/HashingUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
import java.util.Objects;
3030

3131
/**
32-
* Hashing utilities for XRPL related hashing algorithms.
32+
* Hashing utilities for XRPL-related hashing algorithms.
3333
*/
3434
public class HashingUtils {
3535

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/AbstractSignatureService.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.xrpl.xrpl4j.crypto.keys.PublicKey;
2828
import org.xrpl.xrpl4j.model.client.channels.UnsignedClaim;
2929
import org.xrpl.xrpl4j.model.ledger.Attestation;
30+
import org.xrpl.xrpl4j.model.transactions.Batch;
3031
import org.xrpl.xrpl4j.model.transactions.Signer;
3132
import org.xrpl.xrpl4j.model.transactions.Transaction;
3233

@@ -97,21 +98,31 @@ public <T extends Transaction> SingleSignedTransaction<T> sign(final P privateKe
9798
return this.abstractTransactionSigner.sign(privateKeyable, transaction);
9899
}
99100

101+
@Override
102+
public Signature sign(final P privateKeyable, final Attestation attestation) {
103+
return this.abstractTransactionSigner.sign(privateKeyable, attestation);
104+
}
105+
100106
@Override
101107
public Signature sign(final P privateKeyable, final UnsignedClaim unsignedClaim) {
102108
return this.abstractTransactionSigner.sign(privateKeyable, unsignedClaim);
103109
}
104110

105111
@Override
106-
public Signature sign(P privateKeyable, Attestation attestation) {
107-
return this.abstractTransactionSigner.sign(privateKeyable, attestation);
112+
public Signature signInner(final P privateKeyable, final Batch batchTransaction) {
113+
return this.abstractTransactionSigner.signInner(privateKeyable, batchTransaction);
108114
}
109115

110116
@Override
111117
public <T extends Transaction> Signature multiSign(final P privateKeyable, final T transaction) {
112118
return abstractTransactionSigner.multiSign(privateKeyable, transaction);
113119
}
114120

121+
@Override
122+
public Signature multiSignInner(final P privateKeyable, final Batch batchTransaction) {
123+
return abstractTransactionSigner.multiSignInner(privateKeyable, batchTransaction);
124+
}
125+
115126
@Override
116127
public <T extends Transaction> boolean verify(final Signer signer, final T unsignedTransaction) {
117128
return abstractTransactionVerifier.verify(signer, unsignedTransaction);

xrpl4j-core/src/main/java/org/xrpl/xrpl4j/crypto/signing/AbstractTransactionSigner.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.xrpl.xrpl4j.model.client.channels.UnsignedClaim;
2828
import org.xrpl.xrpl4j.model.ledger.Attestation;
2929
import org.xrpl.xrpl4j.model.transactions.Address;
30+
import org.xrpl.xrpl4j.model.transactions.Batch;
3031
import org.xrpl.xrpl4j.model.transactions.Transaction;
3132

3233
import java.util.Objects;
@@ -77,17 +78,35 @@ public Signature sign(P privateKeyable, Attestation attestation) {
7778
return this.signingHelper(privateKeyable, signableBytes);
7879
}
7980

81+
@Override
82+
public Signature signInner(final P privateKeyable, final Batch batchTransaction) {
83+
Objects.requireNonNull(privateKeyable);
84+
Objects.requireNonNull(batchTransaction);
85+
final UnsignedByteArray signableBytes = this.signatureUtils.toSignableInnerBytes(batchTransaction);
86+
return this.signingHelper(privateKeyable, signableBytes);
87+
}
88+
8089
@Override
8190
public <T extends Transaction> Signature multiSign(final P privateKeyable, final T transaction) {
8291
Objects.requireNonNull(privateKeyable);
8392
Objects.requireNonNull(transaction);
8493

8594
final Address address = derivePublicKey(privateKeyable).deriveAddress();
8695
final UnsignedByteArray signableTransactionBytes = this.signatureUtils.toMultiSignableBytes(transaction, address);
87-
8896
return this.signingHelper(privateKeyable, signableTransactionBytes);
8997
}
9098

99+
@Override
100+
public Signature multiSignInner(final P privateKeyable, final Batch batchTransaction) {
101+
Objects.requireNonNull(privateKeyable);
102+
Objects.requireNonNull(batchTransaction);
103+
104+
final Address address = derivePublicKey(privateKeyable).deriveAddress();
105+
final UnsignedByteArray signableBytes = this.signatureUtils.toMultiSignableInnerBytes(batchTransaction, address);
106+
107+
return this.signingHelper(privateKeyable, signableBytes);
108+
}
109+
91110
/**
92111
* Helper to generate a signature based upon an {@link UnsignedByteArray} of transaction bytes.
93112
*

0 commit comments

Comments
 (0)