Skip to content
Merged
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 @@ -49,6 +49,7 @@ public LocalsReference(Address address, long appId) {
private List<HoldingReference> holdings;
private List<LocalsReference> locals;
private Long applicationId;
private Long rejectVersion;
private boolean useAccess = false;

/**
Expand Down Expand Up @@ -129,6 +130,7 @@ protected void applyTo(Transaction txn) {
if (applicationId != null) txn.applicationId = applicationId;
if (onCompletion != null) txn.onCompletion = onCompletion;
if (applicationArgs != null) txn.applicationArgs = applicationArgs;
if (rejectVersion != null) txn.rejectVersion = rejectVersion;
}

@Override
Expand Down Expand Up @@ -224,28 +226,41 @@ public T locals(List<LocalsReference> locals) {

/**
* Enable or disable translation of foreign references into the access field.
*
*
* When useAccess=true:
* - All foreign references (accounts, foreignApps, foreignAssets, boxReferences) are translated
* into a unified access field instead of using separate legacy fields
* - You can still use the same methods (accounts(), foreignApps(), etc.) - they will be translated
* - Advanced features (holdings(), locals()) are also available
* - Compatible with networks that support the access field consensus upgrade
*
*
* When useAccess=false (default):
* - Uses legacy separate fields (accounts, foreignApps, foreignAssets, boxReferences)
* - No translation occurs - references are placed directly in their respective fields
* - Maintains backward compatibility with pre-consensus upgrade networks
* - Advanced features (holdings(), locals()) are not allowed
*
*
* This design allows easy migration - just add .useAccess(true) to enable access field mode
* while keeping your existing foreign reference method calls.
*
*
* @param useAccess true to translate references to access field, false to use legacy fields
* @return this builder instance
*/
public T useAccess(boolean useAccess) {
this.useAccess = useAccess;
return (T) this;
}

/**
* Set the reject version for the application call.
* The lowest application version for which this transaction should immediately fail.
* 0 indicates that no version check should be performed.
*
* @param rejectVersion the minimum application version to reject
* @return this builder instance
*/
public T rejectVersion(Long rejectVersion) {
this.rejectVersion = rejectVersion;
return (T) this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ public class Transaction implements Serializable {
@JsonProperty("apep")
public Long extraPages = 0L;

@JsonProperty("aprv")
public Long rejectVersion = 0L;

/* access field - unifies accounts, foreignApps, foreignAssets, and boxReferences */
@JsonProperty("al")
public List<ResourceRef> access = new ArrayList<>();
Expand Down Expand Up @@ -246,6 +249,7 @@ private Transaction(@JsonProperty("type") Type type,
@JsonProperty("apls") StateSchema localStateSchema,
@JsonProperty("apsu") byte[] clearStateProgram,
@JsonProperty("apep") Long extraPages,
@JsonProperty("aprv") Long rejectVersion,
// access fields
@JsonProperty("al") List<ResourceRef> access,
// heartbeat fields
Expand Down Expand Up @@ -301,6 +305,7 @@ private Transaction(@JsonProperty("type") Type type,
localStateSchema,
clearStateProgram == null ? null : new TEALProgram(clearStateProgram),
extraPages,
rejectVersion,
access == null ? new ArrayList<>() : access,
heartbeatFields
);
Expand Down Expand Up @@ -362,6 +367,7 @@ private Transaction(
StateSchema localStateSchema,
TEALProgram clearStateProgram,
Long extraPages,
Long rejectVersion,
List<ResourceRef> access,
HeartbeatTxnFields heartbeatFields
) {
Expand Down Expand Up @@ -408,6 +414,7 @@ private Transaction(
if (localStateSchema != null) this.localStateSchema = localStateSchema;
if (clearStateProgram != null) this.clearStateProgram = clearStateProgram;
if (extraPages != null) this.extraPages = extraPages;
if (rejectVersion != null) this.rejectVersion = rejectVersion;
if (access != null) this.access = access;
if (heartbeatFields != null) this.heartbeatFields = heartbeatFields;
}
Expand Down Expand Up @@ -622,6 +629,7 @@ public boolean equals(Object o) {
freezeState == that.freezeState &&
rekeyTo.equals(that.rekeyTo) &&
extraPages.equals(that.extraPages) &&
rejectVersion.equals(that.rejectVersion) &&
boxReferences.equals(that.boxReferences) &&
heartbeatFields.equals(that.heartbeatFields);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1053,4 +1053,72 @@ public void TxnEstimatedDefaultFee() throws Exception {
}
}

@Test
public void testApplicationCallRejectVersion() throws Exception {
Address from = new Address("VKM6KSCTDHEM6KGEAMSYCNEGIPFJMHDSEMIRAQLK76CJDIRMMDHKAIRMFQ");
byte[] gh = Encoder.decodeFromBase64("SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=");

// Test with specific rejectVersion
Transaction txWithReject = Transaction.ApplicationCallTransactionBuilder()
.sender(from)
.applicationId(123L)
.firstValid(1000)
.lastValid(2000)
.genesisHash(gh)
.rejectVersion(5L)
.build();

// Verify rejectVersion is set
assertThat(txWithReject.rejectVersion).isEqualTo(5L);

// Test serialization/deserialization with msgpack
byte[] encodedTxn = Encoder.encodeToMsgPack(txWithReject);
Transaction decodedTxn = Encoder.decodeFromMsgPack(encodedTxn, Transaction.class);
assertThat(decodedTxn.rejectVersion).isEqualTo(5L);
assertEqual(txWithReject, decodedTxn);

// Test serialization/deserialization with JSON
ObjectMapper objectMapper = new ObjectMapper();
String transactionJson = objectMapper.writeValueAsString(txWithReject);
Transaction jsonDecodedTxn = objectMapper.readValue(transactionJson, Transaction.class);
assertThat(jsonDecodedTxn.rejectVersion).isEqualTo(5L);
assertEqual(txWithReject, jsonDecodedTxn);

// Verify aprv field is present in JSON when rejectVersion is non-zero
assertThat(transactionJson).contains("\"aprv\":5");

// Test with default rejectVersion (should be 0)
Transaction txDefault = Transaction.ApplicationCallTransactionBuilder()
.sender(from)
.applicationId(456L)
.firstValid(1000)
.lastValid(2000)
.genesisHash(gh)
.build();

// Verify default rejectVersion is 0
assertThat(txDefault.rejectVersion).isEqualTo(0L);

// Verify aprv field is NOT present in JSON when rejectVersion is 0 (default omission)
String txDefaultJson = objectMapper.writeValueAsString(txDefault);
assertThat(txDefaultJson).doesNotContain("aprv");

// Test explicit 0 rejectVersion
Transaction txExplicitZero = Transaction.ApplicationCallTransactionBuilder()
.sender(from)
.applicationId(456L)
.firstValid(1000)
.lastValid(2000)
.genesisHash(gh)
.rejectVersion(0L)
.build();

assertThat(txExplicitZero.rejectVersion).isEqualTo(0L);
assertEqual(txDefault, txExplicitZero);

// Verify aprv field is NOT present in JSON when rejectVersion is explicitly 0
String txExplicitZeroJson = objectMapper.writeValueAsString(txExplicitZero);
assertThat(txExplicitZeroJson).doesNotContain("aprv");
}

}