Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for unknown TransactionType (#521) #522

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public interface Transaction {
.put(ImmutableXChainModifyBridge.class, TransactionType.XCHAIN_MODIFY_BRIDGE)
.put(ImmutableDidSet.class, TransactionType.DID_SET)
.put(ImmutableDidDelete.class, TransactionType.DID_DELETE)
.put(UnknownTransaction.class, TransactionType.UNKNOWN)
.build();

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,12 @@ public enum TransactionType {
* is subject to change.</p>
*/
@Beta
DID_DELETE("DIDDelete");
DID_DELETE("DIDDelete"),

/**
* The {@link TransactionType} for an unknown transaction.
*/
UNKNOWN("");

private final String value;

Expand All @@ -331,7 +336,7 @@ public enum TransactionType {
*
* @param value The {@link String} value corresponding to a {@link TransactionType}.
*
* @return The {@link TransactionType} with the corresponding value.
* @return The {@link TransactionType} with the corresponding value or {@link TransactionType#UNKNOWN} if the given string value is unknown.
*/
public static TransactionType forValue(String value) {
for (TransactionType transactionType : TransactionType.values()) {
Expand All @@ -340,7 +345,7 @@ public static TransactionType forValue(String value) {
}
}

throw new IllegalArgumentException("No matching TransactionType enum value for String value " + value);
return TransactionType.UNKNOWN;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
package org.xrpl.xrpl4j.model.transactions;

import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.primitives.UnsignedInteger;
import com.google.common.primitives.UnsignedLong;
import org.immutables.value.Value;
import org.xrpl.xrpl4j.crypto.keys.PublicKey;
import org.xrpl.xrpl4j.crypto.signing.Signature;
import org.xrpl.xrpl4j.model.client.common.LedgerIndex;
import org.xrpl.xrpl4j.model.client.common.TimeUtils;
import org.xrpl.xrpl4j.model.flags.Flags;

import java.time.ZonedDateTime;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public class UnknownTransaction implements Transaction {
private Address account;
private String transactionTypeText;
private XrpCurrencyAmount fee;
private UnsignedInteger sequence;
private Optional<UnsignedInteger> ticketSequence;
private Optional<Hash256> accountTransactionId;
private Flags flags;
private Optional<UnsignedInteger> lastLedgerSequence;
private List<MemoWrapper> memos;
private List<SignerWrapper> signers;
private Optional<UnsignedInteger> sourceTag;
private Optional<Signature> transactionSignature;
private Optional<NetworkId> networkId;
private PublicKey signingPublicKey;
private Hash256 hash;
private LedgerIndex ledgerIndex;
private Optional<UnsignedLong> closeDate;

private final Map<String, Object> unknowns = new LinkedHashMap<>();

/**
* The unique {@link Address} of the account that initiated this transaction.
*
* @return The {@link Address} of the account submitting this transaction.
*/
@Override
public Address account() {
return account;
}

/**
* The type as text of transaction.
*/
@JsonProperty("TransactionType")
public String transactionTypeText() {
return transactionTypeText;
}

/**
* The {@link String} representation of an integer amount of XRP, in drops, to be destroyed as a cost for distributing
* this Payment transaction to the network.
*
* @return An {@link XrpCurrencyAmount} representing the transaction cost.
*/
@Override
public XrpCurrencyAmount fee() {
return fee;
}

/**
* The sequence number of the account submitting the {@link Transaction}. A {@link Transaction} is only valid if the
* Sequence number is exactly 1 greater than the previous transaction from the same account.
*
* @return An {@link UnsignedInteger} representing the sequence of the transaction.
*/
@JsonProperty("Sequence")
public UnsignedInteger sequence() {
return sequence;
}

/**
* The sequence number of the {@link org.xrpl.xrpl4j.model.ledger.TicketObject} to use in place of a
* {@link #sequence()} number. If this is provided, {@link #sequence()} must be 0. Cannot be used with
* {@link #accountTransactionId()}.
*
* @return An {@link UnsignedInteger} representing the ticket sequence of the transaction.
*/
@Override
public Optional<UnsignedInteger> ticketSequence() {
return ticketSequence;
}

/**
* Hash value identifying another transaction. If provided, this {@link Transaction} is only valid if the sending
* account's previously-sent transaction matches the provided hash.
*
* @return An {@link Optional} of type {@link Hash256} containing the account transaction ID.
*/
@Override
public Optional<Hash256> accountTransactionId() {
return accountTransactionId;
}

/**
* A bit-map of boolean flags.
*/
@JsonProperty("Flags")
public Flags flags() {
return flags;
}

/**
* Highest ledger index this transaction can appear in. Specifying this field places a strict upper limit on how long
* the transaction can wait to be validated or rejected.
*
* @return An {@link Optional} of type {@link UnsignedInteger} representing the last ledger sequence.
*/
@Override
public Optional<UnsignedInteger> lastLedgerSequence() {
return lastLedgerSequence;
}

/**
* Additional arbitrary information used to identify this {@link Transaction}.
*
* @return A {@link List} of {@link MemoWrapper}s.
*/
@Override
public List<MemoWrapper> memos() {
return memos;
}

/**
* Array of {@link SignerWrapper}s that represent a multi-signature which authorizes this {@link Transaction}.
*
* @return A {@link List} of {@link SignerWrapper}s.
*/
@Override
public List<SignerWrapper> signers() {
return signers;
}

/**
* Arbitrary {@link UnsignedInteger} used to identify the reason for this {@link Transaction}, or a sender on whose
* behalf this {@link Transaction} is made.
*
* @return An {@link Optional} {@link UnsignedInteger} representing the source account's tag.
*/
@Override
public Optional<UnsignedInteger> sourceTag() {
return sourceTag;
}

/**
* The {@link PublicKey} that corresponds to the private key used to sign this transaction. If an empty string, ie
* {@link PublicKey#MULTI_SIGN_PUBLIC_KEY}, indicates a multi-signature is present in the
* {@link Transaction#signers()} field instead.
*
* @return A {@link PublicKey} containing the public key of the account submitting the transaction, or
* {@link PublicKey#MULTI_SIGN_PUBLIC_KEY} if the transaction is multi-signed.
*/
@JsonProperty("SigningPubKey")
public PublicKey signingPublicKey() {
return signingPublicKey;
}

/**
* The signature that verifies this transaction as originating from the account it says it is from.
*
* @return An {@link Optional} {@link String} containing the transaction signature.
*/
@Override
public Optional<Signature> transactionSignature() {
return transactionSignature;
}

@Override
public Optional<NetworkId> networkId() {
return networkId;
}

/**
* Unique hash for the ledger, as hexadecimal.
*
* @return A {@link Hash256} containing the ledger hash.
*/
@JsonProperty("hash")
public Hash256 hash() {
return hash;
}

/**
* The index of the ledger that this transaction was included in.
*
* @return The {@link LedgerIndex} that this transaction was included in.
*/
@JsonProperty("ledger_index")
public LedgerIndex ledgerIndex() {
return ledgerIndex;
}

/**
* The approximate close time (using Ripple Epoch) of the ledger containing this transaction.
* This is an undocumented field.
*
* @return An optionally-present {@link UnsignedLong}.
*/
@JsonProperty("date")
public Optional<UnsignedLong> closeDate() {
return closeDate;
}

/**
* The approximate close time in UTC offset.
* This is derived from undocumented field.
*
* @return An optionally-present {@link ZonedDateTime}.
*/
public Optional<ZonedDateTime> closeDateHuman() {
return closeDate().map(TimeUtils::xrplTimeToZonedDateTime);
}

@JsonAnySetter
private void putUnknown(String key, Object value) {
unknowns.put(key, value);
}

/**
* Map of all unknown and not mapped JSON nodes.
*/
@JsonIgnore
public Map<String, Object> unknowns() {
return unknowns;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
import org.xrpl.xrpl4j.model.transactions.SignerListSet;
import org.xrpl.xrpl4j.model.transactions.Transaction;
import org.xrpl.xrpl4j.model.transactions.TrustSet;
import org.xrpl.xrpl4j.model.transactions.UnknownTransaction;
import org.xrpl.xrpl4j.model.transactions.XChainAccountCreateCommit;
import org.xrpl.xrpl4j.model.transactions.XChainAddAccountCreateAttestation;
import org.xrpl.xrpl4j.model.transactions.XChainAddClaimAttestation;
Expand All @@ -100,6 +101,8 @@
import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Map;

public class BinarySerializationTests {

Expand Down Expand Up @@ -2120,6 +2123,62 @@ void serializeDidDelete() throws JsonProcessingException {
assertSerializesAndDeserializes(transaction, binary);
}

@Test
void deserializeUnknown() throws JsonProcessingException {
String json = "{" +
" \"Account\": \"rfmDuhDyLGgx94qiwf3YF8BUV5j6KSvE8\"," +
" \"Fee\": \"10\"," +
" \"Sequence\": 4," +
" \"TransactionType\": \"UnknownCustom\"," +
" \"UnknownProperty\": \"value123\"," +
" \"UnknownObject\": {" +
" \"name\": \"value0\"," +
" \"key1\": 1" +
" }," +
" \"UnknownArray\": [" +
" {" +
" \"UnknownDetail\": {" +
" \"Amount\": \"1666671963\"," +
" \"Destination\": \"rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL\"" +
" }" +
" }," +
" {" +
" \"UnknownDetail\": {" +
" \"Amount\": \"83333166\"," +
" \"Destination\": \"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV\"" +
" }" +
" }" +
" ]" +
"}";

UnknownTransaction transaction = objectMapper.readValue(json, UnknownTransaction.class);

assertThat(transaction.account().value()).isEqualTo("rfmDuhDyLGgx94qiwf3YF8BUV5j6KSvE8");
assertThat(transaction.fee()).isEqualTo(XrpCurrencyAmount.ofDrops(10));
assertThat(transaction.sequence()).isEqualTo(UnsignedInteger.valueOf(4));
assertThat(transaction.transactionTypeText()).isEqualTo("UnknownCustom");

assertThat(transaction.unknowns().size()).isEqualTo(3);
assertThat(transaction.unknowns().get("UnknownProperty")).isEqualTo("value123");

Map<?, ?> unknownObject = (Map<?, ?>)transaction.unknowns().get("UnknownObject");
assertThat(unknownObject).isNotNull();
assertThat(unknownObject.get("name")).isEqualTo("value0");
assertThat(unknownObject.get("key1")).isEqualTo(1);

ArrayList<?> unknownArray = (ArrayList<?>)transaction.unknowns().get("UnknownArray");
assertThat(unknownArray).isNotNull();
assertThat(unknownArray.size()).isEqualTo(2);
Map<?, ?> elem0 = (Map<?, ?>)unknownArray.get(0);
Map<?, ?> elem0Detail = (Map<?, ?>)elem0.get("UnknownDetail");
assertThat(elem0Detail.get("Amount")).isEqualTo("1666671963");
assertThat(elem0Detail.get("Destination")).isEqualTo("rGzx83BVoqTYbGn7tiVAnFw7cbxjin13jL");
Map<?, ?> elem1 = (Map<?, ?>)unknownArray.get(1);
Map<?, ?> elem1Detail = (Map<?, ?>)elem1.get("UnknownDetail");
assertThat(elem1Detail.get("Amount")).isEqualTo("83333166");
assertThat(elem1Detail.get("Destination")).isEqualTo("r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV");
}

private <T extends Transaction> void assertSerializesAndDeserializes(
T transaction,
String expectedBinary
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,10 @@ public void shouldReturnTransactionTypeForValidValues(String value) {
@NullSource
@ParameterizedTest
@ArgumentsSource(value = TransactionTypeInvalidArgumentProvider.class)
public void shouldThrowIllegalArgumentExceptionForInvalidValues(String value) {
assertThrows(IllegalArgumentException.class, () -> TransactionType.forValue(value),
"No matching TransactionType enum value for String value " + value);
public void shouldReturnUnknownForInvalidValues(String value) {
TransactionType transactionType = TransactionType.forValue(value);
assertNotNull(transactionType);
assertThat(transactionType).isEqualTo(TransactionType.UNKNOWN);
}

public static class TransactionTypeValidArgumentProvider implements ArgumentsProvider {
Expand Down
Loading