diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 000000000..f2f82b746
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,100 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Build and Development Commands
+
+### Build
+```bash
+mvn package
+```
+
+### Testing
+```bash
+# Run unit tests only
+make unit
+# or: mvn test -Dcucumber.filter.tags="@unit"
+
+# Run integration tests only
+make integration
+# or: mvn test -Dtest=com.algorand.algosdk.integration.RunCucumberIntegrationTest -Dcucumber.filter.tags="@integration"
+
+# Run all tests (unit + integration)
+make ci-test
+
+# Run tests with Docker test harness
+make docker-test
+
+# Start/stop test harness manually
+make harness
+make harness-down
+```
+
+### Examples
+The `examples/` directory contains example code:
+```bash
+cd examples/
+mvn package
+java -cp target/sdk-extras-1.0-SNAPSHOT.jar com.algorand.examples.Example
+```
+
+## Architecture Overview
+
+This is the official Java SDK for Algorand blockchain, providing client libraries for interacting with algod and indexer nodes.
+
+### Core Package Structure
+
+- **`account/`** - Account management, keypair generation, and logic signature accounts
+- **`crypto/`** - Core cryptographic primitives (Ed25519, addresses, signatures, multisig)
+- **`transaction/`** - Transaction types, builders, and atomic transaction composer
+- **`v2/client/`** - REST clients for algod and indexer APIs (auto-generated from OpenAPI specs)
+- **`builder/transaction/`** - Fluent transaction builders for all transaction types
+- **`abi/`** - Application Binary Interface support for smart contracts
+- **`mnemonic/`** - BIP39 mnemonic phrase utilities
+- **`util/`** - Encoding/decoding utilities (MessagePack, Base64, etc.)
+
+### Key Components
+
+#### Transaction System
+- **Transaction Builder Pattern**: All transaction types use fluent builders (e.g., `PaymentTransactionBuilder`, `ApplicationCallTransactionBuilder`)
+- **Atomic Transaction Composer**: High-level interface for building transaction groups with ABI method calls
+- **Transaction Types**: Payment, asset transfer, application calls, key registration, state proof, heartbeat
+
+#### Client Architecture
+- **AlgodClient**: REST client for algod daemon (node operations, transaction submission)
+- **IndexerClient**: REST client for indexer service (historical queries, account lookups)
+- **Generated Code**: Most v2 client classes are auto-generated from OpenAPI specifications
+
+#### Cryptography
+- **Ed25519**: Primary signature algorithm using BouncyCastle
+- **Multisig**: M-of-N threshold signatures
+- **Logic Signatures**: Delegated signing using TEAL programs
+- **Address**: 32-byte public key hash with checksum
+
+## Code Generation
+
+The v2 client code (`v2.client.algod.*`, `v2.client.indexer.*`, `v2.client.model.*`) is generated from OpenAPI specs:
+- algod.oas2.json - algod daemon API
+- indexer.oas2.json - indexer service API
+
+To regenerate clients, use the `generate_java.sh` script from the [generator](https://github.com/algorand/generator/) repository.
+
+## Testing Framework
+
+Uses Cucumber BDD testing with separate unit and integration test suites:
+- **Unit tests**: Fast tests using mocked clients (`@unit` tags)
+- **Integration tests**: Full integration with test harness (`@integration` tags)
+- **Test harness**: Dockerized Algorand network for integration testing
+
+Test files are organized under:
+- `src/test/java/com/algorand/algosdk/unit/` - Unit test step definitions
+- `src/test/java/com/algorand/algosdk/integration/` - Integration test step definitions
+- `src/test/resources/` - Test data and response fixtures
+- `test-harness/features/` - Cucumber feature files
+
+## Development Notes
+
+- **Java 8+ Compatibility**: Maintains Java 8 compatibility with Android support (minSdkVersion 26+)
+- **Dependencies**: Uses Jackson for JSON, MessagePack for serialization, BouncyCastle for crypto
+- **Maven Profiles**: Includes IDE profile for IntelliJ compatibility with mixed Java versions
+- **Generated Content**: Do not manually edit generated client code - regenerate from specs instead
\ No newline at end of file
diff --git a/README.md b/README.md
index e4eddbe27..16e8c12d2 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,5 @@
# java-algorand-sdk
-[](https://dl.circleci.com/status-badge/redirect/gh/algorand/java-algorand-sdk/tree/develop)
diff --git a/examples/pom.xml b/examples/pom.xml
index 21bec1b2f..da9c8b0d3 100644
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -96,7 +96,7 @@
com.algorand
algosdk
- 2.0.0
+ 2.9.0
diff --git a/examples/src/main/java/com/algorand/algosdk/example/UseAccessFlagExample.java b/examples/src/main/java/com/algorand/algosdk/example/UseAccessFlagExample.java
new file mode 100644
index 000000000..a6a888a75
--- /dev/null
+++ b/examples/src/main/java/com/algorand/algosdk/example/UseAccessFlagExample.java
@@ -0,0 +1,135 @@
+package com.algorand.algosdk.example;
+
+import com.algorand.algosdk.account.Account;
+import com.algorand.algosdk.builder.transaction.ApplicationCallTransactionBuilder;
+import com.algorand.algosdk.builder.transaction.ApplicationBaseTransactionBuilder;
+import com.algorand.algosdk.crypto.Address;
+import com.algorand.algosdk.transaction.AppBoxReference;
+import com.algorand.algosdk.transaction.ResourceRef;
+import com.algorand.algosdk.transaction.Transaction;
+import com.algorand.algosdk.util.Encoder;
+
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Demonstrates the useAccess flag for handling consensus upgrade compatibility.
+ *
+ * The useAccess flag controls how foreign references are handled:
+ * - useAccess=false: Uses legacy fields (accounts, foreignApps, foreignAssets, boxReferences)
+ * - useAccess=true: Translates the same references into a unified access field
+ *
+ * Key insight: You use the SAME builder methods in both modes - just add .useAccess(true)!
+ * No API changes needed for migration, making upgrades simple and safe.
+ */
+public class UseAccessFlagExample {
+
+ public static void main(String[] args) throws Exception {
+ Account sender = new Account();
+ Account otherAccount = new Account();
+
+ System.out.println("=== useAccess Flag Examples ===\n");
+ System.out.println("Shows how easy it is to migrate to access field mode:");
+ System.out.println("Just add .useAccess(true) - no other code changes needed!\n");
+
+ // Example 1: Legacy mode (useAccess=false, default)
+ demonstrateLegacyMode(sender, otherAccount);
+
+ System.out.println("\n" + "=".repeat(50) + "\n");
+
+ // Example 2: Same code with useAccess=true - easy migration!
+ demonstrateEasyMigration(sender, otherAccount);
+
+ System.out.println("\n" + "=".repeat(50) + "\n");
+
+ System.out.println("\nš That's it! Migration to access field mode is just one line.");
+ System.out.println("Advanced features like holdings() and locals() are also available with useAccess=true.");
+ }
+
+ private static void demonstrateLegacyMode(Account sender, Account otherAccount) throws Exception {
+ System.out.println("Example 1: Legacy Mode (useAccess=false)");
+ System.out.println("-----------------------------------------");
+ System.out.println("Using standard foreign reference methods with legacy field output");
+
+ // Build transaction using foreign reference methods (default behavior)
+ Transaction txn = ApplicationCallTransactionBuilder.Builder()
+ .sender(sender.getAddress())
+ .applicationId(12345L)
+ .useAccess(false) // Default mode - puts references in separate fields
+ .accounts(Arrays.asList(otherAccount.getAddress()))
+ .foreignApps(Arrays.asList(67890L))
+ .foreignAssets(Arrays.asList(999L))
+ .firstValid(BigInteger.valueOf(1000))
+ .lastValid(BigInteger.valueOf(2000))
+ .genesisHash(Encoder.decodeFromBase64("SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI="))
+ .build();
+
+ // Show how references are stored in separate legacy fields
+ System.out.printf("Result: References stored in separate fields%n");
+ System.out.printf(" - accounts: %d entries%n", txn.accounts.size());
+ System.out.printf(" - foreignApps: %d entries%n", txn.foreignApps.size());
+ System.out.printf(" - foreignAssets: %d entries%n", txn.foreignAssets.size());
+ System.out.printf(" - access field: %d entries (empty)%n", txn.access.size());
+ System.out.println("ā Compatible with pre-consensus upgrade networks");
+ }
+
+ private static void demonstrateEasyMigration(Account sender, Account otherAccount) throws Exception {
+ System.out.println("Example 2: Easy Migration (useAccess=true)");
+ System.out.println("-------------------------------------------");
+ System.out.println("SAME CODE as Example 1 - just add .useAccess(true)!");
+
+ // Build transaction using the EXACT SAME builder method calls as Example 1
+ // The only difference is useAccess=true, which translates these into access field
+ Transaction txn = ApplicationCallTransactionBuilder.Builder()
+ .sender(sender.getAddress())
+ .applicationId(12345L)
+ .accounts(Arrays.asList(otherAccount.getAddress())) // Same as Example 1
+ .foreignApps(Arrays.asList(67890L)) // Same as Example 1
+ .foreignAssets(Arrays.asList(999L)) // Same as Example 1
+ .firstValid(BigInteger.valueOf(1000))
+ .lastValid(BigInteger.valueOf(2000))
+ .genesisHash(Encoder.decodeFromBase64("SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI="))
+ .useAccess(true) // š„ ONLY CHANGE: Add this one line!
+ .build();
+
+ // Show how the same references are now translated to access field
+ System.out.printf("Result: Same references, different internal format%n");
+ System.out.printf(" - accounts: %d entries (empty - translated to access)%n", txn.accounts.size());
+ System.out.printf(" - foreignApps: %d entries (empty - translated to access)%n", txn.foreignApps.size());
+ System.out.printf(" - foreignAssets: %d entries (empty - translated to access)%n", txn.foreignAssets.size());
+ System.out.printf(" - access field: %d entries (contains all references)%n", txn.access.size());
+
+ System.out.println("\nš Migration is just one line: .useAccess(true)");
+ System.out.println("ā No API changes required");
+ System.out.println("ā Same builder methods work in both modes");
+ System.out.println("ā Ready for post-consensus upgrade networks");
+ }
+
+
+
+ private static String formatAccessEntry(ResourceRef ref) {
+ if (ref.address != null) {
+ return "Address: " + ref.address.toString().substring(0, 10) + "...";
+ }
+ if (ref.asset != null) {
+ return "Asset: " + ref.asset;
+ }
+ if (ref.app != null) {
+ return "App: " + ref.app;
+ }
+ if (ref.holding != null) {
+ return String.format("Holding: addr_idx=%d, asset_idx=%d",
+ ref.holding.addressIndex, ref.holding.assetIndex);
+ }
+ if (ref.locals != null) {
+ return String.format("Locals: addr_idx=%d, app_idx=%d",
+ ref.locals.addressIndex, ref.locals.appIndex);
+ }
+ if (ref.box != null) {
+ return String.format("Box: app_idx=%d, name='%s'",
+ ref.box.index, new String(ref.box.name));
+ }
+ return "Empty";
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/algorand/algosdk/builder/transaction/ApplicationBaseTransactionBuilder.java b/src/main/java/com/algorand/algosdk/builder/transaction/ApplicationBaseTransactionBuilder.java
index a0d4fb496..489806a6b 100644
--- a/src/main/java/com/algorand/algosdk/builder/transaction/ApplicationBaseTransactionBuilder.java
+++ b/src/main/java/com/algorand/algosdk/builder/transaction/ApplicationBaseTransactionBuilder.java
@@ -1,7 +1,10 @@
package com.algorand.algosdk.builder.transaction;
import com.algorand.algosdk.crypto.Address;
+import com.algorand.algosdk.transaction.AccessConverter;
import com.algorand.algosdk.transaction.AppBoxReference;
+import com.algorand.algosdk.transaction.AppResourceRef;
+import com.algorand.algosdk.transaction.ResourceRef;
import com.algorand.algosdk.transaction.Transaction;
import com.algorand.algosdk.util.Encoder;
@@ -11,13 +14,42 @@
@SuppressWarnings("unchecked")
public abstract class ApplicationBaseTransactionBuilder> extends TransactionBuilder implements ApplicationCallReferencesSetter {
+
+ /**
+ * Represents a holding reference for asset holdings of an account.
+ */
+ public static class HoldingReference {
+ public final Address address;
+ public final long assetId;
+
+ public HoldingReference(Address address, long assetId) {
+ this.address = address;
+ this.assetId = assetId;
+ }
+ }
+
+ /**
+ * Represents a locals reference for local state of an account in an app.
+ */
+ public static class LocalsReference {
+ public final Address address;
+ public final long appId;
+
+ public LocalsReference(Address address, long appId) {
+ this.address = address;
+ this.appId = appId;
+ }
+ }
private Transaction.OnCompletion onCompletion;
private List applicationArgs;
private List accounts;
private List foreignApps;
private List foreignAssets;
private List appBoxReferences;
+ private List holdings;
+ private List locals;
private Long applicationId;
+ private boolean useAccess = false;
/**
* All application calls use this type, so no need to make this private. This constructor should always be called.
@@ -32,13 +64,71 @@ protected void applyTo(Transaction txn) {
Objects.requireNonNull(onCompletion, "OnCompletion is required, please file a bug report.");
Objects.requireNonNull(applicationId);
+ // Check if advanced features are being used
+ boolean hasAdvancedFeatures = (holdings != null && !holdings.isEmpty()) ||
+ (locals != null && !locals.isEmpty());
+
+ if (useAccess) {
+ // Using access field mode - translate all references into access list
+ List allRefs = new ArrayList<>();
+
+ if (accounts != null && !accounts.isEmpty()) {
+ for (Address account : accounts) {
+ allRefs.add(AppResourceRef.forAddress(account));
+ }
+ }
+
+ if (foreignApps != null && !foreignApps.isEmpty()) {
+ for (Long appId : foreignApps) {
+ allRefs.add(AppResourceRef.forApp(appId));
+ }
+ }
+
+ if (foreignAssets != null && !foreignAssets.isEmpty()) {
+ for (Long assetId : foreignAssets) {
+ allRefs.add(AppResourceRef.forAsset(assetId));
+ }
+ }
+
+ if (appBoxReferences != null && !appBoxReferences.isEmpty()) {
+ for (AppBoxReference boxRef : appBoxReferences) {
+ allRefs.add(AppResourceRef.forBox(boxRef.getAppId(), boxRef.getName()));
+ }
+ }
+
+ if (holdings != null && !holdings.isEmpty()) {
+ for (HoldingReference holdingRef : holdings) {
+ allRefs.add(AppResourceRef.forHolding(holdingRef.address, holdingRef.assetId));
+ }
+ }
+
+ if (locals != null && !locals.isEmpty()) {
+ for (LocalsReference localsRef : locals) {
+ allRefs.add(AppResourceRef.forLocals(localsRef.address, localsRef.appId));
+ }
+ }
+
+ txn.access = AccessConverter.convertToResourceRefs(allRefs, sender, applicationId);
+
+ } else {
+ // Using legacy fields mode
+ if (hasAdvancedFeatures) {
+ throw new IllegalArgumentException(
+ "Holdings and locals references require useAccess=true as they cannot be represented in legacy transaction format"
+ );
+ }
+
+ // Use legacy fields directly
+ if (accounts != null) txn.accounts = accounts;
+ if (foreignApps != null) txn.foreignApps = foreignApps;
+ if (foreignAssets != null) txn.foreignAssets = foreignAssets;
+ if (appBoxReferences != null) txn.boxReferences = convertBoxes(appBoxReferences, foreignApps, applicationId);
+ }
+
+ // Set common fields
if (applicationId != null) txn.applicationId = applicationId;
if (onCompletion != null) txn.onCompletion = onCompletion;
if (applicationArgs != null) txn.applicationArgs = applicationArgs;
- if (accounts != null) txn.accounts = accounts;
- if (foreignApps != null) txn.foreignApps = foreignApps;
- if (foreignAssets != null) txn.foreignAssets = foreignAssets;
- if (appBoxReferences != null) txn.boxReferences = convertBoxes(appBoxReferences, foreignApps, applicationId);
}
@Override
@@ -107,4 +197,55 @@ public T boxReferences(List boxReferences) {
this.appBoxReferences = boxReferences;
return (T) this;
}
+
+ /**
+ * Set asset holding references that need to be accessible in this transaction.
+ * Holdings references allow the transaction to access asset balances of specific accounts.
+ *
+ * Note: Holdings references are only available when useAccess=true as they cannot be
+ * represented in legacy transaction format.
+ */
+ public T holdings(List holdings) {
+ this.holdings = holdings;
+ return (T) this;
+ }
+
+ /**
+ * Set local state references that need to be accessible in this transaction.
+ * Locals references allow the transaction to access local state of specific accounts in specific apps.
+ *
+ * Note: Locals references are only available when useAccess=true as they cannot be
+ * represented in legacy transaction format.
+ */
+ public T locals(List locals) {
+ this.locals = locals;
+ return (T) this;
+ }
+
+ /**
+ * 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;
+ }
}
diff --git a/src/main/java/com/algorand/algosdk/transaction/AccessConverter.java b/src/main/java/com/algorand/algosdk/transaction/AccessConverter.java
new file mode 100644
index 000000000..904103d2b
--- /dev/null
+++ b/src/main/java/com/algorand/algosdk/transaction/AccessConverter.java
@@ -0,0 +1,144 @@
+package com.algorand.algosdk.transaction;
+
+import com.algorand.algosdk.crypto.Address;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * AccessConverter handles the conversion from high-level AppResourceRef instances
+ * to index-based ResourceRef instances that go-algorand expects.
+ *
+ * This follows the same pattern as BoxReference.fromAppBoxReference() method.
+ */
+public class AccessConverter {
+
+ /**
+ * Convert a list of high-level AppResourceRef to index-based ResourceRef.
+ * This handles index 0 special cases and ensures proper referencing.
+ *
+ * @param appRefs High-level resource references
+ * @param sender Transaction sender (used for index 0 address references)
+ * @param currentAppId Current application ID (used for index 0 app references)
+ * @return List of index-based ResourceRef for serialization
+ */
+ public static List convertToResourceRefs(
+ List appRefs,
+ Address sender,
+ Long currentAppId) {
+
+ if (appRefs == null || appRefs.isEmpty()) {
+ return new ArrayList<>();
+ }
+
+ List result = new ArrayList<>();
+
+ // First pass: Create basic ResourceRef entries for addresses, assets, and apps
+ for (AppResourceRef appRef : appRefs) {
+ if (appRef instanceof AppResourceRef.AddressRef) {
+ AppResourceRef.AddressRef addrRef = (AppResourceRef.AddressRef) appRef;
+ result.add(ResourceRef.forAddress(addrRef.getAddress()));
+ } else if (appRef instanceof AppResourceRef.AssetRef) {
+ AppResourceRef.AssetRef assetRef = (AppResourceRef.AssetRef) appRef;
+ result.add(ResourceRef.forAsset(assetRef.getAssetId()));
+ } else if (appRef instanceof AppResourceRef.AppRef) {
+ AppResourceRef.AppRef appRefInner = (AppResourceRef.AppRef) appRef;
+ result.add(ResourceRef.forApp(appRefInner.getAppId()));
+ }
+ }
+
+ // Second pass: Handle compound references (holding, locals, box) with proper indices
+ for (AppResourceRef appRef : appRefs) {
+ if (appRef instanceof AppResourceRef.HoldingRef) {
+ AppResourceRef.HoldingRef holdingRef = (AppResourceRef.HoldingRef) appRef;
+ long addressIndex = findOrAddAddressIndex(
+ holdingRef.getAddress(), sender, result);
+ long assetIndex = findOrAddAssetIndex(
+ holdingRef.getAssetId(), result);
+ result.add(ResourceRef.forHolding(
+ new ResourceRef.HoldingRef(addressIndex, assetIndex)));
+
+ } else if (appRef instanceof AppResourceRef.LocalsRef) {
+ AppResourceRef.LocalsRef localsRef = (AppResourceRef.LocalsRef) appRef;
+ long addressIndex = findOrAddAddressIndex(
+ localsRef.getAddress(), sender, result);
+ long appIndex = findOrAddAppIndex(
+ localsRef.getAppId(), currentAppId, result);
+ result.add(ResourceRef.forLocals(
+ new ResourceRef.LocalsRef(addressIndex, appIndex)));
+
+ } else if (appRef instanceof AppResourceRef.BoxRef) {
+ AppResourceRef.BoxRef boxRef = (AppResourceRef.BoxRef) appRef;
+ long appIndex = findOrAddAppIndex(
+ boxRef.getAppId(), currentAppId, result);
+ result.add(ResourceRef.forBox(
+ new ResourceRef.BoxRef(appIndex, boxRef.getName())));
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Find or add an address to the resource list and return its index.
+ * Handles index 0 special case (sender).
+ */
+ private static long findOrAddAddressIndex(Address address, Address sender, List resources) {
+ // Special case: index 0 = sender
+ if (address == null || address.equals(sender)) {
+ return 0;
+ }
+
+ // Look for existing address in the list
+ for (int i = 0; i < resources.size(); i++) {
+ ResourceRef ref = resources.get(i);
+ if (ref.address != null && ref.address.equals(address)) {
+ return i + 1; // 1-based indexing (0 is special)
+ }
+ }
+
+ // Add address if not found
+ resources.add(ResourceRef.forAddress(address));
+ return resources.size(); // 1-based indexing
+ }
+
+ /**
+ * Find or add an asset to the resource list and return its index.
+ */
+ private static long findOrAddAssetIndex(long assetId, List resources) {
+ // Look for existing asset in the list
+ for (int i = 0; i < resources.size(); i++) {
+ ResourceRef ref = resources.get(i);
+ if (ref.asset != null && ref.asset.equals(assetId)) {
+ return i + 1; // 1-based indexing
+ }
+ }
+
+ // Add asset if not found
+ resources.add(ResourceRef.forAsset(assetId));
+ return resources.size(); // 1-based indexing
+ }
+
+ /**
+ * Find or add an app to the resource list and return its index.
+ * Handles index 0 special case (current app).
+ */
+ private static long findOrAddAppIndex(long appId, Long currentAppId, List resources) {
+ // Special case: index 0 = current app
+ if (currentAppId != null && appId == currentAppId) {
+ return 0;
+ }
+
+ // Look for existing app in the list
+ for (int i = 0; i < resources.size(); i++) {
+ ResourceRef ref = resources.get(i);
+ if (ref.app != null && ref.app.equals(appId)) {
+ return i + 1; // 1-based indexing (0 is special)
+ }
+ }
+
+ // Add app if not found
+ resources.add(ResourceRef.forApp(appId));
+ return resources.size(); // 1-based indexing
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/algorand/algosdk/transaction/AppResourceRef.java b/src/main/java/com/algorand/algosdk/transaction/AppResourceRef.java
new file mode 100644
index 000000000..46f474517
--- /dev/null
+++ b/src/main/java/com/algorand/algosdk/transaction/AppResourceRef.java
@@ -0,0 +1,256 @@
+package com.algorand.algosdk.transaction;
+
+import com.algorand.algosdk.crypto.Address;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * AppResourceRef represents a high-level resource reference that will be converted
+ * to index-based ResourceRef entries. This provides a user-friendly API.
+ *
+ * This follows the same pattern as AppBoxReference -> BoxReference conversion.
+ */
+public abstract class AppResourceRef {
+
+ /**
+ * Address reference.
+ */
+ public static class AddressRef extends AppResourceRef {
+ private final Address address;
+
+ public AddressRef(Address address) {
+ this.address = address;
+ }
+
+ public Address getAddress() {
+ return address;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ AddressRef that = (AddressRef) o;
+ return Objects.equals(address, that.address);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(address);
+ }
+
+ @Override
+ public String toString() {
+ return "AddressRef{address=" + address + '}';
+ }
+ }
+
+ /**
+ * Asset reference.
+ */
+ public static class AssetRef extends AppResourceRef {
+ private final long assetId;
+
+ public AssetRef(long assetId) {
+ this.assetId = assetId;
+ }
+
+ public long getAssetId() {
+ return assetId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ AssetRef assetRef = (AssetRef) o;
+ return assetId == assetRef.assetId;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(assetId);
+ }
+
+ @Override
+ public String toString() {
+ return "AssetRef{assetId=" + assetId + '}';
+ }
+ }
+
+ /**
+ * Application reference.
+ */
+ public static class AppRef extends AppResourceRef {
+ private final long appId;
+
+ public AppRef(long appId) {
+ this.appId = appId;
+ }
+
+ public long getAppId() {
+ return appId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ AppRef appRef = (AppRef) o;
+ return appId == appRef.appId;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(appId);
+ }
+
+ @Override
+ public String toString() {
+ return "AppRef{appId=" + appId + '}';
+ }
+ }
+
+ /**
+ * Holding reference (account + asset).
+ */
+ public static class HoldingRef extends AppResourceRef {
+ private final Address address;
+ private final long assetId;
+
+ public HoldingRef(Address address, long assetId) {
+ this.address = address;
+ this.assetId = assetId;
+ }
+
+ public Address getAddress() {
+ return address;
+ }
+
+ public long getAssetId() {
+ return assetId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ HoldingRef that = (HoldingRef) o;
+ return assetId == that.assetId && Objects.equals(address, that.address);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(address, assetId);
+ }
+
+ @Override
+ public String toString() {
+ return "HoldingRef{address=" + address + ", assetId=" + assetId + '}';
+ }
+ }
+
+ /**
+ * Locals reference (account + app).
+ */
+ public static class LocalsRef extends AppResourceRef {
+ private final Address address;
+ private final long appId;
+
+ public LocalsRef(Address address, long appId) {
+ this.address = address;
+ this.appId = appId;
+ }
+
+ public Address getAddress() {
+ return address;
+ }
+
+ public long getAppId() {
+ return appId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ LocalsRef localsRef = (LocalsRef) o;
+ return appId == localsRef.appId && Objects.equals(address, localsRef.address);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(address, appId);
+ }
+
+ @Override
+ public String toString() {
+ return "LocalsRef{address=" + address + ", appId=" + appId + '}';
+ }
+ }
+
+ /**
+ * Box reference.
+ */
+ public static class BoxRef extends AppResourceRef {
+ private final long appId;
+ private final byte[] name;
+
+ public BoxRef(long appId, byte[] name) {
+ this.appId = appId;
+ this.name = name == null ? new byte[0] : Arrays.copyOf(name, name.length);
+ }
+
+ public long getAppId() {
+ return appId;
+ }
+
+ public byte[] getName() {
+ return Arrays.copyOf(name, name.length);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ BoxRef boxRef = (BoxRef) o;
+ return appId == boxRef.appId && Arrays.equals(name, boxRef.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(appId, Arrays.hashCode(name));
+ }
+
+ @Override
+ public String toString() {
+ return "BoxRef{appId=" + appId + ", name=" + Arrays.toString(name) + '}';
+ }
+ }
+
+ // Factory methods
+ public static AddressRef forAddress(Address address) {
+ return new AddressRef(address);
+ }
+
+ public static AssetRef forAsset(long assetId) {
+ return new AssetRef(assetId);
+ }
+
+ public static AppRef forApp(long appId) {
+ return new AppRef(appId);
+ }
+
+ public static HoldingRef forHolding(Address address, long assetId) {
+ return new HoldingRef(address, assetId);
+ }
+
+ public static LocalsRef forLocals(Address address, long appId) {
+ return new LocalsRef(address, appId);
+ }
+
+ public static BoxRef forBox(long appId, byte[] name) {
+ return new BoxRef(appId, name);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/algorand/algosdk/transaction/ResourceRef.java b/src/main/java/com/algorand/algosdk/transaction/ResourceRef.java
new file mode 100644
index 000000000..7a6e422e5
--- /dev/null
+++ b/src/main/java/com/algorand/algosdk/transaction/ResourceRef.java
@@ -0,0 +1,331 @@
+package com.algorand.algosdk.transaction;
+
+import com.algorand.algosdk.crypto.Address;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * ResourceRef is a reference to a resource in an application call transaction.
+ * It can reference different types of resources like accounts, assets, applications, holdings, locals, or boxes.
+ * Only one resource type should be set per ResourceRef instance.
+ */
+@JsonInclude(JsonInclude.Include.NON_DEFAULT)
+public class ResourceRef {
+ @JsonProperty("d")
+ public Address address;
+
+ @JsonProperty("s")
+ public Long asset;
+
+ @JsonProperty("p")
+ public Long app;
+
+ @JsonProperty("h")
+ public HoldingRef holding;
+
+ @JsonProperty("l")
+ public LocalsRef locals;
+
+ @JsonProperty("b")
+ public BoxRef box;
+
+ /**
+ * Default constructor for ResourceRef.
+ */
+ public ResourceRef() {}
+
+ /**
+ * JsonCreator constructor for ResourceRef deserialization.
+ */
+ @JsonCreator
+ public ResourceRef(
+ @JsonProperty("d") byte[] address,
+ @JsonProperty("s") Long asset,
+ @JsonProperty("p") Long app,
+ @JsonProperty("h") HoldingRef holding,
+ @JsonProperty("l") LocalsRef locals,
+ @JsonProperty("b") BoxRef box) {
+ if (address != null) {
+ this.address = new Address(address);
+ }
+ this.asset = asset;
+ this.app = app;
+ this.holding = holding;
+ this.locals = locals;
+ this.box = box;
+ }
+
+ /**
+ * Create a ResourceRef for an account address.
+ */
+ public static ResourceRef forAddress(Address address) {
+ ResourceRef ref = new ResourceRef();
+ ref.address = address;
+ return ref;
+ }
+
+ /**
+ * Create a ResourceRef for an asset.
+ */
+ public static ResourceRef forAsset(long assetId) {
+ ResourceRef ref = new ResourceRef();
+ ref.asset = assetId;
+ return ref;
+ }
+
+ /**
+ * Create a ResourceRef for an application.
+ */
+ public static ResourceRef forApp(long appId) {
+ ResourceRef ref = new ResourceRef();
+ ref.app = appId;
+ return ref;
+ }
+
+ /**
+ * Create a ResourceRef for a holding reference.
+ */
+ public static ResourceRef forHolding(HoldingRef holdingRef) {
+ ResourceRef ref = new ResourceRef();
+ ref.holding = holdingRef;
+ return ref;
+ }
+
+ /**
+ * Create a ResourceRef for a locals reference.
+ */
+ public static ResourceRef forLocals(LocalsRef localsRef) {
+ ResourceRef ref = new ResourceRef();
+ ref.locals = localsRef;
+ return ref;
+ }
+
+ /**
+ * Create a ResourceRef for a box reference.
+ */
+ public static ResourceRef forBox(BoxRef boxRef) {
+ ResourceRef ref = new ResourceRef();
+ ref.box = boxRef;
+ return ref;
+ }
+
+ /**
+ * Check if this ResourceRef is empty (no resource type is set).
+ */
+ @JsonIgnore
+ public boolean isEmpty() {
+ return address == null && asset == null && app == null &&
+ holding == null && locals == null && box == null;
+ }
+
+ /**
+ * Validate that only one resource type is set.
+ * @throws IllegalStateException if multiple resource types are set
+ */
+ @JsonIgnore
+ public void validate() {
+ int setCount = 0;
+ if (address != null) setCount++;
+ if (asset != null) setCount++;
+ if (app != null) setCount++;
+ if (holding != null) setCount++;
+ if (locals != null) setCount++;
+ if (box != null) setCount++;
+
+ if (setCount > 1) {
+ throw new IllegalStateException("ResourceRef can only have one resource type set");
+ }
+ if (setCount == 0) {
+ throw new IllegalStateException("ResourceRef must have one resource type set");
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ResourceRef that = (ResourceRef) o;
+ return Objects.equals(address, that.address) &&
+ Objects.equals(asset, that.asset) &&
+ Objects.equals(app, that.app) &&
+ Objects.equals(holding, that.holding) &&
+ Objects.equals(locals, that.locals) &&
+ Objects.equals(box, that.box);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(address, asset, app, holding, locals, box);
+ }
+
+ @Override
+ public String toString() {
+ return "ResourceRef{" +
+ "address=" + address +
+ ", asset=" + asset +
+ ", app=" + app +
+ ", holding=" + holding +
+ ", locals=" + locals +
+ ", box=" + box +
+ '}';
+ }
+
+ /**
+ * HoldingRef represents a reference to an asset holding of an account.
+ * Both fields are indices into the Access array, matching the Go implementation.
+ */
+ @JsonInclude(JsonInclude.Include.NON_DEFAULT)
+ public static class HoldingRef {
+ @JsonProperty("d")
+ public Long addressIndex; // Index into Access array (0 = sender)
+
+ @JsonProperty("s")
+ public Long assetIndex; // Index into Access array
+
+ public HoldingRef() {}
+
+ @JsonCreator
+ public HoldingRef(
+ @JsonProperty("d") Long addressIndex,
+ @JsonProperty("s") Long assetIndex) {
+ this.addressIndex = addressIndex;
+ this.assetIndex = assetIndex;
+ }
+
+ public HoldingRef(long addressIndex, long assetIndex) {
+ this.addressIndex = addressIndex;
+ this.assetIndex = assetIndex;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ HoldingRef that = (HoldingRef) o;
+ return Objects.equals(addressIndex, that.addressIndex) && Objects.equals(assetIndex, that.assetIndex);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(addressIndex, assetIndex);
+ }
+
+ @Override
+ public String toString() {
+ return "HoldingRef{" +
+ "addressIndex=" + addressIndex +
+ ", assetIndex=" + assetIndex +
+ '}';
+ }
+ }
+
+ /**
+ * LocalsRef represents a reference to the local state of an account for an application.
+ * Both fields are indices into the Access array, matching the Go implementation.
+ */
+ @JsonInclude(JsonInclude.Include.NON_DEFAULT)
+ public static class LocalsRef {
+ @JsonProperty("d")
+ public Long addressIndex; // Index into Access array (0 = sender)
+
+ @JsonProperty("p")
+ public Long appIndex; // Index into Access array (0 = current app)
+
+ public LocalsRef() {}
+
+ @JsonCreator
+ public LocalsRef(
+ @JsonProperty("d") Long addressIndex,
+ @JsonProperty("p") Long appIndex) {
+ this.addressIndex = addressIndex;
+ this.appIndex = appIndex;
+ }
+
+ public LocalsRef(long addressIndex, long appIndex) {
+ this.addressIndex = addressIndex;
+ this.appIndex = appIndex;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ LocalsRef that = (LocalsRef) o;
+ return Objects.equals(addressIndex, that.addressIndex) && Objects.equals(appIndex, that.appIndex);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(addressIndex, appIndex);
+ }
+
+ @Override
+ public String toString() {
+ return "LocalsRef{" +
+ "addressIndex=" + addressIndex +
+ ", appIndex=" + appIndex +
+ '}';
+ }
+ }
+
+ /**
+ * BoxRef represents a reference to a box of an application.
+ * The index field references the application in the Access array.
+ */
+ @JsonInclude(JsonInclude.Include.NON_DEFAULT)
+ public static class BoxRef {
+ @JsonProperty("i")
+ public Long index; // Index into Access array (0 = current app)
+
+ @JsonProperty("n")
+ public byte[] name;
+
+ public BoxRef() {}
+
+ @JsonCreator
+ public BoxRef(
+ @JsonProperty("i") Long index,
+ @JsonProperty("n") byte[] name) {
+ this.index = index;
+ this.name = name == null ? new byte[0] : Arrays.copyOf(name, name.length);
+ }
+
+ public BoxRef(long index, byte[] name) {
+ this.index = index;
+ this.name = name == null ? new byte[0] : Arrays.copyOf(name, name.length);
+ }
+
+ @JsonIgnore
+ public byte[] getName() {
+ return name == null ? new byte[0] : Arrays.copyOf(name, name.length);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ BoxRef boxRef = (BoxRef) o;
+ return Objects.equals(index, boxRef.index) && Arrays.equals(name, boxRef.name);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = Objects.hash(index);
+ result = 31 * result + Arrays.hashCode(name);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "BoxRef{" +
+ "index=" + index +
+ ", name=" + Arrays.toString(name) +
+ '}';
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/algorand/algosdk/transaction/Transaction.java b/src/main/java/com/algorand/algosdk/transaction/Transaction.java
index 51f4058f2..16c12bacf 100644
--- a/src/main/java/com/algorand/algosdk/transaction/Transaction.java
+++ b/src/main/java/com/algorand/algosdk/transaction/Transaction.java
@@ -165,6 +165,10 @@ public class Transaction implements Serializable {
@JsonProperty("apep")
public Long extraPages = 0L;
+ /* access field - unifies accounts, foreignApps, foreignAssets, and boxReferences */
+ @JsonProperty("al")
+ public List access = new ArrayList<>();
+
/* state proof fields */
@JsonProperty("sptype")
public Integer stateProofType = null;
@@ -242,6 +246,8 @@ private Transaction(@JsonProperty("type") Type type,
@JsonProperty("apls") StateSchema localStateSchema,
@JsonProperty("apsu") byte[] clearStateProgram,
@JsonProperty("apep") Long extraPages,
+ // access fields
+ @JsonProperty("al") List access,
// heartbeat fields
@JsonProperty("hb") HeartbeatTxnFields heartbeatFields
) throws IOException {
@@ -295,6 +301,7 @@ private Transaction(@JsonProperty("type") Type type,
localStateSchema,
clearStateProgram == null ? null : new TEALProgram(clearStateProgram),
extraPages,
+ access == null ? new ArrayList<>() : access,
heartbeatFields
);
}
@@ -355,6 +362,7 @@ private Transaction(
StateSchema localStateSchema,
TEALProgram clearStateProgram,
Long extraPages,
+ List access,
HeartbeatTxnFields heartbeatFields
) {
if (type != null) this.type = type;
@@ -400,6 +408,7 @@ private Transaction(
if (localStateSchema != null) this.localStateSchema = localStateSchema;
if (clearStateProgram != null) this.clearStateProgram = clearStateProgram;
if (extraPages != null) this.extraPages = extraPages;
+ if (access != null) this.access = access;
if (heartbeatFields != null) this.heartbeatFields = heartbeatFields;
}
diff --git a/src/test/java/com/algorand/algosdk/transaction/TestResourceRef.java b/src/test/java/com/algorand/algosdk/transaction/TestResourceRef.java
new file mode 100644
index 000000000..961e5e838
--- /dev/null
+++ b/src/test/java/com/algorand/algosdk/transaction/TestResourceRef.java
@@ -0,0 +1,228 @@
+package com.algorand.algosdk.transaction;
+
+import com.algorand.algosdk.crypto.Address;
+import org.junit.jupiter.api.Test;
+
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class TestResourceRef {
+
+ @Test
+ public void testResourceRefForAddress() throws NoSuchAlgorithmException {
+ Address addr = new Address("XBYLS2E6YI6XXL5BWCAMOA4GTWHXWENZMX5UHXMRNWWUQ7BXCY5WC5TEPA");
+ ResourceRef ref = ResourceRef.forAddress(addr);
+
+ assertNotNull(ref.address);
+ assertEquals(addr, ref.address);
+ assertNull(ref.asset);
+ assertNull(ref.app);
+ assertNull(ref.holding);
+ assertNull(ref.locals);
+ assertNull(ref.box);
+ assertFalse(ref.isEmpty());
+ assertDoesNotThrow(ref::validate);
+ }
+
+ @Test
+ public void testResourceRefForAsset() {
+ ResourceRef ref = ResourceRef.forAsset(123L);
+
+ assertNull(ref.address);
+ assertEquals(Long.valueOf(123L), ref.asset);
+ assertNull(ref.app);
+ assertNull(ref.holding);
+ assertNull(ref.locals);
+ assertNull(ref.box);
+ assertFalse(ref.isEmpty());
+ assertDoesNotThrow(ref::validate);
+ }
+
+ @Test
+ public void testResourceRefForApp() {
+ ResourceRef ref = ResourceRef.forApp(456L);
+
+ assertNull(ref.address);
+ assertNull(ref.asset);
+ assertEquals(Long.valueOf(456L), ref.app);
+ assertNull(ref.holding);
+ assertNull(ref.locals);
+ assertNull(ref.box);
+ assertFalse(ref.isEmpty());
+ assertDoesNotThrow(ref::validate);
+ }
+
+ @Test
+ public void testResourceRefForHolding() {
+ // Index-based approach: addressIndex=0 (sender), assetIndex=1 (first asset in access list)
+ ResourceRef.HoldingRef holding = new ResourceRef.HoldingRef(0L, 1L);
+ ResourceRef ref = ResourceRef.forHolding(holding);
+
+ assertNull(ref.address);
+ assertNull(ref.asset);
+ assertNull(ref.app);
+ assertNotNull(ref.holding);
+ assertEquals(Long.valueOf(0L), ref.holding.addressIndex);
+ assertEquals(Long.valueOf(1L), ref.holding.assetIndex);
+ assertNull(ref.locals);
+ assertNull(ref.box);
+ assertFalse(ref.isEmpty());
+ assertDoesNotThrow(ref::validate);
+ }
+
+ @Test
+ public void testResourceRefForLocals() {
+ // Index-based approach: addressIndex=0 (sender), appIndex=0 (current app)
+ ResourceRef.LocalsRef locals = new ResourceRef.LocalsRef(0L, 0L);
+ ResourceRef ref = ResourceRef.forLocals(locals);
+
+ assertNull(ref.address);
+ assertNull(ref.asset);
+ assertNull(ref.app);
+ assertNull(ref.holding);
+ assertNotNull(ref.locals);
+ assertEquals(Long.valueOf(0L), ref.locals.addressIndex);
+ assertEquals(Long.valueOf(0L), ref.locals.appIndex);
+ assertNull(ref.box);
+ assertFalse(ref.isEmpty());
+ assertDoesNotThrow(ref::validate);
+ }
+
+ @Test
+ public void testResourceRefForBox() {
+ byte[] boxName = "test-box".getBytes();
+ ResourceRef.BoxRef box = new ResourceRef.BoxRef(0L, boxName); // Use index 0 for current app
+ ResourceRef ref = ResourceRef.forBox(box);
+
+ assertNull(ref.address);
+ assertNull(ref.asset);
+ assertNull(ref.app);
+ assertNull(ref.holding);
+ assertNull(ref.locals);
+ assertNotNull(ref.box);
+ assertEquals(Long.valueOf(0L), ref.box.index);
+ assertArrayEquals(boxName, ref.box.name);
+ assertFalse(ref.isEmpty());
+ assertDoesNotThrow(ref::validate);
+ }
+
+ @Test
+ public void testEmptyResourceRef() {
+ ResourceRef ref = new ResourceRef();
+
+ assertNull(ref.address);
+ assertNull(ref.asset);
+ assertNull(ref.app);
+ assertNull(ref.holding);
+ assertNull(ref.locals);
+ assertNull(ref.box);
+ assertTrue(ref.isEmpty());
+ }
+
+ @Test
+ public void testResourceRefValidationFailsWhenEmpty() {
+ ResourceRef ref = new ResourceRef();
+
+ IllegalStateException exception = assertThrows(IllegalStateException.class, ref::validate);
+ assertEquals("ResourceRef must have one resource type set", exception.getMessage());
+ }
+
+ @Test
+ public void testResourceRefValidationFailsWhenMultipleSet() throws NoSuchAlgorithmException {
+ ResourceRef ref = new ResourceRef();
+ ref.address = new Address("XBYLS2E6YI6XXL5BWCAMOA4GTWHXWENZMX5UHXMRNWWUQ7BXCY5WC5TEPA");
+ ref.asset = 123L;
+
+ IllegalStateException exception = assertThrows(IllegalStateException.class, ref::validate);
+ assertEquals("ResourceRef can only have one resource type set", exception.getMessage());
+ }
+
+ @Test
+ public void testHoldingRefConstructorAndMethods() {
+ // Index-based approach: addressIndex=2, assetIndex=3 (arbitrary indices)
+ ResourceRef.HoldingRef holding = new ResourceRef.HoldingRef(2L, 3L);
+
+ assertEquals(Long.valueOf(2L), holding.addressIndex);
+ assertEquals(Long.valueOf(3L), holding.assetIndex);
+
+ // Test equality
+ ResourceRef.HoldingRef holding2 = new ResourceRef.HoldingRef(2L, 3L);
+ assertEquals(holding, holding2);
+ assertEquals(holding.hashCode(), holding2.hashCode());
+
+ // Test toString
+ assertTrue(holding.toString().contains("HoldingRef"));
+ }
+
+ @Test
+ public void testLocalsRefConstructorAndMethods() {
+ // Index-based approach: addressIndex=1, appIndex=2 (arbitrary indices)
+ ResourceRef.LocalsRef locals = new ResourceRef.LocalsRef(1L, 2L);
+
+ assertEquals(Long.valueOf(1L), locals.addressIndex);
+ assertEquals(Long.valueOf(2L), locals.appIndex);
+
+ // Test equality
+ ResourceRef.LocalsRef locals2 = new ResourceRef.LocalsRef(1L, 2L);
+ assertEquals(locals, locals2);
+ assertEquals(locals.hashCode(), locals2.hashCode());
+
+ // Test toString
+ assertTrue(locals.toString().contains("LocalsRef"));
+ }
+
+ @Test
+ public void testBoxRefConstructorAndMethods() {
+ byte[] boxName = "my-box".getBytes();
+ ResourceRef.BoxRef box = new ResourceRef.BoxRef(101L, boxName);
+
+ assertEquals(Long.valueOf(101L), box.index);
+ assertArrayEquals(boxName, box.name);
+ assertArrayEquals(boxName, box.getName()); // Test getter makes defensive copy
+
+ // Test equality
+ ResourceRef.BoxRef box2 = new ResourceRef.BoxRef(101L, "my-box".getBytes());
+ assertEquals(box, box2);
+ assertEquals(box.hashCode(), box2.hashCode());
+
+ // Test toString
+ assertTrue(box.toString().contains("BoxRef"));
+ }
+
+ @Test
+ public void testBoxRefWithNullName() {
+ ResourceRef.BoxRef box = new ResourceRef.BoxRef(102L, null);
+
+ assertEquals(Long.valueOf(102L), box.index);
+ assertArrayEquals(new byte[0], box.name);
+ assertArrayEquals(new byte[0], box.getName());
+ }
+
+ @Test
+ public void testResourceRefEquality() throws NoSuchAlgorithmException {
+ Address addr = new Address("XBYLS2E6YI6XXL5BWCAMOA4GTWHXWENZMX5UHXMRNWWUQ7BXCY5WC5TEPA");
+
+ ResourceRef ref1 = ResourceRef.forAddress(addr);
+ ResourceRef ref2 = ResourceRef.forAddress(addr);
+ ResourceRef ref3 = ResourceRef.forAsset(123L);
+
+ assertEquals(ref1, ref2);
+ assertEquals(ref1.hashCode(), ref2.hashCode());
+ assertNotEquals(ref1, ref3);
+ assertNotEquals(ref1.hashCode(), ref3.hashCode());
+
+ // Test toString
+ assertTrue(ref1.toString().contains("ResourceRef"));
+ }
+
+ @Test
+ public void testResourceRefEqualityWithNull() throws NoSuchAlgorithmException {
+ ResourceRef ref = ResourceRef.forAsset(123L);
+
+ assertNotEquals(ref, null);
+ assertNotEquals(ref, "not a ResourceRef");
+ assertEquals(ref, ref); // self equality
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/algorand/algosdk/transaction/TestUseAccess.java b/src/test/java/com/algorand/algosdk/transaction/TestUseAccess.java
new file mode 100644
index 000000000..4a66129f7
--- /dev/null
+++ b/src/test/java/com/algorand/algosdk/transaction/TestUseAccess.java
@@ -0,0 +1,224 @@
+package com.algorand.algosdk.transaction;
+
+import com.algorand.algosdk.builder.transaction.ApplicationCallTransactionBuilder;
+import com.algorand.algosdk.crypto.Address;
+import com.algorand.algosdk.util.Encoder;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigInteger;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class TestUseAccess {
+
+ private static final String SENDER_ADDR = "XBYLS2E6YI6XXL5BWCAMOA4GTWHXWENZMX5UHXMRNWWUQ7BXCY5WC5TEPA";
+ private static final String ACCOUNT_ADDR = "47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU";
+
+ @Test
+ public void testUseAccessDefaultIsFalse() throws NoSuchAlgorithmException {
+ Address sender = new Address(SENDER_ADDR);
+ Address account = new Address(ACCOUNT_ADDR);
+
+ // Default behavior should be useAccess=false (legacy fields)
+ Transaction txn = ApplicationCallTransactionBuilder.Builder()
+ .sender(sender)
+ .applicationId(1001L)
+ // No .useAccess() call - should default to false
+ .accounts(Collections.singletonList(account))
+ .foreignApps(Collections.singletonList(456L))
+ .firstValid(BigInteger.valueOf(1000))
+ .lastValid(BigInteger.valueOf(2000))
+ .genesisHash(Encoder.decodeFromBase64("SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI="))
+ .build();
+
+ // Should use legacy fields
+ assertEquals(1, txn.accounts.size());
+ assertEquals(account, txn.accounts.get(0));
+ assertEquals(1, txn.foreignApps.size());
+ assertEquals(Long.valueOf(456L), txn.foreignApps.get(0));
+ assertTrue(txn.access.isEmpty());
+ }
+
+ @Test
+ public void testUseAccessFalseUsesLegacyFields() throws NoSuchAlgorithmException {
+ Address sender = new Address(SENDER_ADDR);
+ Address account = new Address(ACCOUNT_ADDR);
+
+ // Explicit useAccess=false should use legacy fields
+ Transaction txn = ApplicationCallTransactionBuilder.Builder()
+ .sender(sender)
+ .applicationId(1001L)
+ .useAccess(false)
+ .accounts(Collections.singletonList(account))
+ .foreignApps(Collections.singletonList(456L))
+ .foreignAssets(Collections.singletonList(123L))
+ .firstValid(BigInteger.valueOf(1000))
+ .lastValid(BigInteger.valueOf(2000))
+ .genesisHash(Encoder.decodeFromBase64("SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI="))
+ .build();
+
+ // Verify legacy fields are populated
+ assertEquals(1, txn.accounts.size());
+ assertEquals(account, txn.accounts.get(0));
+ assertEquals(1, txn.foreignApps.size());
+ assertEquals(Long.valueOf(456L), txn.foreignApps.get(0));
+ assertEquals(1, txn.foreignAssets.size());
+ assertEquals(Long.valueOf(123L), txn.foreignAssets.get(0));
+
+ // Verify access field is empty
+ assertTrue(txn.access.isEmpty());
+ }
+
+ @Test
+ public void testUseAccessTrueTranslatesFields() throws NoSuchAlgorithmException {
+ Address sender = new Address(SENDER_ADDR);
+ Address account = new Address(ACCOUNT_ADDR);
+
+ // useAccess=true should translate same method calls into access field
+ Transaction txn = ApplicationCallTransactionBuilder.Builder()
+ .sender(sender)
+ .applicationId(1001L)
+ .useAccess(true) // Translation mode
+ .accounts(Collections.singletonList(account))
+ .foreignApps(Collections.singletonList(456L))
+ .foreignAssets(Collections.singletonList(123L))
+ .firstValid(BigInteger.valueOf(1000))
+ .lastValid(BigInteger.valueOf(2000))
+ .genesisHash(Encoder.decodeFromBase64("SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI="))
+ .build();
+
+ // Verify legacy fields are empty (translated)
+ assertTrue(txn.accounts.isEmpty());
+ assertTrue(txn.foreignApps.isEmpty());
+ assertTrue(txn.foreignAssets.isEmpty());
+
+ // Verify access field has translated references
+ assertNotNull(txn.access);
+ assertEquals(3, txn.access.size());
+
+ // Check each reference type exists in access field
+ boolean foundAccount = false, foundApp = false, foundAsset = false;
+ for (ResourceRef ref : txn.access) {
+ if (ref.address != null && ref.address.equals(account)) foundAccount = true;
+ if (ref.app != null && ref.app.equals(456L)) foundApp = true;
+ if (ref.asset != null && ref.asset.equals(123L)) foundAsset = true;
+ }
+ assertTrue(foundAccount, "Account reference should be in access field");
+ assertTrue(foundApp, "App reference should be in access field");
+ assertTrue(foundAsset, "Asset reference should be in access field");
+ }
+
+ @Test
+ public void testBoxReferencesWorkWithBothModes() throws NoSuchAlgorithmException {
+ Address sender = new Address(SENDER_ADDR);
+
+ AppBoxReference boxRef = new AppBoxReference(1001L, "test-box".getBytes());
+
+ // Test with useAccess=false
+ Transaction legacyTxn = ApplicationCallTransactionBuilder.Builder()
+ .sender(sender)
+ .applicationId(1001L)
+ .useAccess(false)
+ .boxReferences(Collections.singletonList(boxRef))
+ .firstValid(BigInteger.valueOf(1000))
+ .lastValid(BigInteger.valueOf(2000))
+ .genesisHash(Encoder.decodeFromBase64("SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI="))
+ .build();
+
+ assertEquals(1, legacyTxn.boxReferences.size());
+ assertTrue(legacyTxn.access.isEmpty());
+
+ // Test with useAccess=true
+ Transaction accessTxn = ApplicationCallTransactionBuilder.Builder()
+ .sender(sender)
+ .applicationId(1001L)
+ .useAccess(true)
+ .boxReferences(Arrays.asList(boxRef))
+ .firstValid(BigInteger.valueOf(1000))
+ .lastValid(BigInteger.valueOf(2000))
+ .genesisHash(Encoder.decodeFromBase64("SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI="))
+ .build();
+
+ assertTrue(accessTxn.boxReferences.isEmpty());
+ assertEquals(1, accessTxn.access.size());
+ assertNotNull(accessTxn.access.get(0).box);
+ }
+
+ @Test
+ public void testMixedReferencesTranslateCorrectly() throws NoSuchAlgorithmException {
+ Address sender = new Address(SENDER_ADDR);
+ Address account1 = new Address(ACCOUNT_ADDR);
+ Address account2 = new Address(SENDER_ADDR);
+
+ // Test that multiple references of different types all get translated
+ Transaction txn = ApplicationCallTransactionBuilder.Builder()
+ .sender(sender)
+ .applicationId(1001L)
+ .useAccess(true)
+ .accounts(Arrays.asList(account1, account2)) // Multiple accounts
+ .foreignApps(Arrays.asList(456L, 789L)) // Multiple apps
+ .foreignAssets(Arrays.asList(123L, 999L)) // Multiple assets
+ .boxReferences(Arrays.asList(new AppBoxReference(1001L, "box1".getBytes())))
+ .firstValid(BigInteger.valueOf(1000))
+ .lastValid(BigInteger.valueOf(2000))
+ .genesisHash(Encoder.decodeFromBase64("SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI="))
+ .build();
+
+ // All legacy fields should be empty
+ assertTrue(txn.accounts.isEmpty());
+ assertTrue(txn.foreignApps.isEmpty());
+ assertTrue(txn.foreignAssets.isEmpty());
+ assertTrue(txn.boxReferences.isEmpty());
+
+ // Access field should contain all references
+ assertNotNull(txn.access);
+ assertEquals(7, txn.access.size()); // 2 accounts + 2 apps + 2 assets + 1 box = 7
+
+ // Verify we have references of each type
+ int addressCount = 0, appCount = 0, assetCount = 0, boxCount = 0;
+ for (ResourceRef ref : txn.access) {
+ if (ref.address != null) addressCount++;
+ if (ref.app != null) appCount++;
+ if (ref.asset != null) assetCount++;
+ if (ref.box != null) boxCount++;
+ }
+
+ assertEquals(2, addressCount, "Should have 2 address references");
+ assertEquals(2, appCount, "Should have 2 app references");
+ assertEquals(2, assetCount, "Should have 2 asset references");
+ assertEquals(1, boxCount, "Should have 1 box reference");
+ }
+
+ @Test
+ public void testEmptyReferencesWork() throws NoSuchAlgorithmException {
+ Address sender = new Address(SENDER_ADDR);
+
+ // Test both modes with no references
+ Transaction legacyTxn = ApplicationCallTransactionBuilder.Builder()
+ .sender(sender)
+ .applicationId(1001L)
+ .useAccess(false)
+ .firstValid(BigInteger.valueOf(1000))
+ .lastValid(BigInteger.valueOf(2000))
+ .genesisHash(Encoder.decodeFromBase64("SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI="))
+ .build();
+
+ assertTrue(legacyTxn.accounts.isEmpty());
+ assertTrue(legacyTxn.access.isEmpty());
+
+ Transaction accessTxn = ApplicationCallTransactionBuilder.Builder()
+ .sender(sender)
+ .applicationId(1001L)
+ .useAccess(true)
+ .firstValid(BigInteger.valueOf(1000))
+ .lastValid(BigInteger.valueOf(2000))
+ .genesisHash(Encoder.decodeFromBase64("SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI="))
+ .build();
+
+ assertTrue(accessTxn.accounts.isEmpty());
+ assertTrue(accessTxn.access.isEmpty());
+ }
+}
\ No newline at end of file