diff --git a/src/main/java/org/stellar/sdk/SorobanDataBuilder.java b/src/main/java/org/stellar/sdk/SorobanDataBuilder.java new file mode 100644 index 000000000..23480f4ca --- /dev/null +++ b/src/main/java/org/stellar/sdk/SorobanDataBuilder.java @@ -0,0 +1,166 @@ +package org.stellar.sdk; + +import java.io.IOException; +import java.util.Collection; +import javax.annotation.Nullable; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import org.stellar.sdk.xdr.ExtensionPoint; +import org.stellar.sdk.xdr.Int64; +import org.stellar.sdk.xdr.LedgerFootprint; +import org.stellar.sdk.xdr.LedgerKey; +import org.stellar.sdk.xdr.SorobanResources; +import org.stellar.sdk.xdr.SorobanTransactionData; +import org.stellar.sdk.xdr.Uint32; +import org.stellar.sdk.xdr.XdrUnsignedInteger; + +/** + * Supports building {@link SorobanTransactionData} structures with various items set to specific + * values. + * + *
This is recommended for when you are building {@link BumpFootprintExpirationOperation} and + * {@link RestoreFootprintOperation} operations to avoid (re)building the entire data structure from + * scratch. + */ +public class SorobanDataBuilder { + private final SorobanTransactionData data; + + /** Creates a new builder with an empty {@link SorobanTransactionData}. */ + public SorobanDataBuilder() { + data = + new SorobanTransactionData.Builder() + .resources( + new SorobanResources.Builder() + .footprint( + new LedgerFootprint.Builder() + .readOnly(new LedgerKey[] {}) + .readWrite(new LedgerKey[] {}) + .build()) + .instructions(new Uint32(new XdrUnsignedInteger(0))) + .readBytes(new Uint32(new XdrUnsignedInteger(0))) + .writeBytes(new Uint32(new XdrUnsignedInteger(0))) + .extendedMetaDataSizeBytes(new Uint32(new XdrUnsignedInteger(0))) + .build()) + .refundableFee(new Int64(0L)) + .ext(new ExtensionPoint.Builder().discriminant(0).build()) + .build(); + } + + /** + * Creates a new builder from a base64 representation of {@link SorobanTransactionData}. + * + * @param sorobanData base64 representation of {@link SorobanTransactionData} + */ + public SorobanDataBuilder(String sorobanData) { + try { + data = SorobanTransactionData.fromXdrBase64(sorobanData); + } catch (IOException e) { + throw new IllegalArgumentException("Invalid SorobanData: " + sorobanData, e); + } + } + + /** + * Creates a new builder from a {@link SorobanTransactionData}. + * + * @param sorobanData {@link SorobanTransactionData}. + */ + public SorobanDataBuilder(SorobanTransactionData sorobanData) { + try { + data = SorobanTransactionData.fromXdrByteArray(sorobanData.toXdrByteArray()); + } catch (IOException e) { + throw new IllegalArgumentException("Invalid SorobanData: " + sorobanData, e); + } + } + + /** + * Sets the "refundable" fee portion of the Soroban data. + * + * @param fee the refundable fee to set (int64) + * @return this builder instance + */ + public SorobanDataBuilder setRefundableFee(long fee) { + data.setRefundableFee(new Int64(fee)); + return this; + } + + /** + * Sets up the resource metrics. + * + *
You should almost NEVER need this, as its often generated/provided to you by transaction + * simulation/preflight from a Soroban RPC server. + * + * @param resources the resource metrics to set + * @return this builder instance + */ + public SorobanDataBuilder setResources(Resources resources) { + data.getResources() + .setInstructions(new Uint32(new XdrUnsignedInteger(resources.getCpuInstructions()))); + data.getResources().setReadBytes(new Uint32(new XdrUnsignedInteger(resources.getReadBytes()))); + data.getResources() + .setWriteBytes(new Uint32(new XdrUnsignedInteger(resources.getWriteBytes()))); + data.getResources() + .setExtendedMetaDataSizeBytes( + new Uint32(new XdrUnsignedInteger(resources.getMetadataBytes()))); + return this; + } + + /** + * Sets the read-only portion of the storage access footprint to be a certain set of ledger keys. + * + *
Passing {@code null} will leave that portion of the footprint untouched. If you want to
+ * clear a portion of the footprint, pass an empty collection.
+ *
+ * @param readOnly the set of ledger keys to set in the read-only portion of the transaction's
+ * sorobanData
+ * @return this builder instance
+ */
+ public SorobanDataBuilder setReadOnly(@Nullable Collection Passing {@code null} will leave that portion of the footprint untouched. If you want to
+ * clear a portion of the footprint, pass an empty collection.
+ *
+ * @param readWrite the set of ledger keys to set in the read-write portion of the transaction's
+ * sorobanData
+ * @return this builder instance
+ */
+ public SorobanDataBuilder setReadWrite(@Nullable Collection For non-contract(non-Soroban) transactions, this setting has no effect. In the case of
+ * Soroban transactions, this is either an instance of {@link SorobanTransactionData} or a
+ * base64-encoded string of said structure. This is usually obtained from the simulation response
+ * based on a transaction with a Soroban operation (e.g. {@link InvokeHostFunctionOperation},
+ * providing necessary resource and storage footprint estimations for contract invocation.
*
* @param sorobanData Soroban data to set
* @return Builder object so you can chain methods.
*/
public TransactionBuilder setSorobanData(SorobanTransactionData sorobanData) {
- this.mSorobanData = sorobanData;
+ this.mSorobanData = new SorobanDataBuilder(sorobanData).build();
return this;
}
/**
- * Sets Soroban data to the transaction. TODO: After adding SorobanServer, add more descriptions.
+ * Sets the transaction's internal Soroban transaction data (resources, footprint, etc.).
+ *
+ * For non-contract(non-Soroban) transactions, this setting has no effect. In the case of
+ * Soroban transactions, this is either an instance of {@link SorobanTransactionData} or a
+ * base64-encoded string of said structure. This is usually obtained from the simulation response
+ * based on a transaction with a Soroban operation (e.g. {@link InvokeHostFunctionOperation},
+ * providing necessary resource and storage footprint estimations for contract invocation.
*
* @param sorobanData Soroban data to set
* @return Builder object so you can chain methods.
*/
public TransactionBuilder setSorobanData(String sorobanData) {
- SorobanTransactionData data;
- try {
- data = SorobanTransactionData.fromXdrBase64(sorobanData);
- } catch (IOException e) {
- throw new IllegalArgumentException("Invalid Soroban data: " + sorobanData, e);
- }
- return setSorobanData(data);
+ this.mSorobanData = new SorobanDataBuilder(sorobanData).build();
+ return this;
}
}
diff --git a/src/test/java/org/stellar/sdk/SorobanDataBuilderTest.java b/src/test/java/org/stellar/sdk/SorobanDataBuilderTest.java
new file mode 100644
index 000000000..f55a4ae74
--- /dev/null
+++ b/src/test/java/org/stellar/sdk/SorobanDataBuilderTest.java
@@ -0,0 +1,145 @@
+package org.stellar.sdk;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotSame;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import org.junit.Test;
+import org.stellar.sdk.xdr.ExtensionPoint;
+import org.stellar.sdk.xdr.Int64;
+import org.stellar.sdk.xdr.LedgerEntryType;
+import org.stellar.sdk.xdr.LedgerFootprint;
+import org.stellar.sdk.xdr.LedgerKey;
+import org.stellar.sdk.xdr.SorobanResources;
+import org.stellar.sdk.xdr.SorobanTransactionData;
+import org.stellar.sdk.xdr.Uint32;
+import org.stellar.sdk.xdr.XdrUnsignedInteger;
+
+public class SorobanDataBuilderTest {
+ LedgerKey readOnly =
+ new LedgerKey.Builder()
+ .discriminant(LedgerEntryType.ACCOUNT)
+ .account(
+ new LedgerKey.LedgerKeyAccount.Builder()
+ .accountID(
+ KeyPair.fromAccountId(
+ "GB7TAYRUZGE6TVT7NHP5SMIZRNQA6PLM423EYISAOAP3MKYIQMVYP2JO")
+ .getXdrAccountId())
+ .build())
+ .build();
+ LedgerKey readWrite =
+ new LedgerKey.Builder()
+ .discriminant(LedgerEntryType.ACCOUNT)
+ .account(
+ new LedgerKey.LedgerKeyAccount.Builder()
+ .accountID(
+ KeyPair.fromAccountId(
+ "GAHJJJKMOKYE4RVPZEWZTKH5FVI4PA3VL7GK2LFNUBSGBV6OJP7TQSLX")
+ .getXdrAccountId())
+ .build())
+ .build();
+
+ SorobanTransactionData emptySorobanData =
+ new SorobanTransactionData.Builder()
+ .resources(
+ new SorobanResources.Builder()
+ .footprint(
+ new LedgerFootprint.Builder()
+ .readOnly(new LedgerKey[] {})
+ .readWrite(new LedgerKey[] {})
+ .build())
+ .instructions(new Uint32(new XdrUnsignedInteger(0)))
+ .readBytes(new Uint32(new XdrUnsignedInteger(0)))
+ .writeBytes(new Uint32(new XdrUnsignedInteger(0)))
+ .extendedMetaDataSizeBytes(new Uint32(new XdrUnsignedInteger(0)))
+ .build())
+ .refundableFee(new Int64(0L))
+ .ext(new ExtensionPoint.Builder().discriminant(0).build())
+ .build();
+
+ SorobanTransactionData presetSorobanData =
+ new SorobanTransactionData.Builder()
+ .resources(
+ new SorobanResources.Builder()
+ .footprint(
+ new LedgerFootprint.Builder()
+ .readOnly(new LedgerKey[] {readOnly})
+ .readWrite(new LedgerKey[] {readWrite})
+ .build())
+ .instructions(new Uint32(new XdrUnsignedInteger(1)))
+ .readBytes(new Uint32(new XdrUnsignedInteger(2)))
+ .writeBytes(new Uint32(new XdrUnsignedInteger(3)))
+ .extendedMetaDataSizeBytes(new Uint32(new XdrUnsignedInteger(4)))
+ .build())
+ .refundableFee(new Int64(5L))
+ .ext(new ExtensionPoint.Builder().discriminant(0).build())
+ .build();
+
+ @Test
+ public void testConstructorFromEmpty() {
+ SorobanTransactionData actualData = new SorobanDataBuilder().build();
+ assertEquals(emptySorobanData, actualData);
+ }
+
+ @Test
+ public void testConstructorFromBase64() throws IOException {
+ String base64 = presetSorobanData.toXdrBase64();
+ SorobanTransactionData actualData = new SorobanDataBuilder(base64).build();
+ assertEquals(presetSorobanData, actualData);
+ }
+
+ @Test
+ public void testConstructorFromSorobanTransactionData() {
+ SorobanTransactionData actualData = new SorobanDataBuilder(presetSorobanData).build();
+ assertEquals(presetSorobanData, actualData);
+ }
+
+ @Test
+ public void testSetProperties() {
+ SorobanTransactionData actualData =
+ new SorobanDataBuilder()
+ .setReadOnly(of(readOnly))
+ .setReadWrite(of(readWrite))
+ .setRefundableFee(5)
+ .setResources(
+ new SorobanDataBuilder.Resources.ResourcesBuilder()
+ .cpuInstructions(1L)
+ .readBytes(2L)
+ .writeBytes(3L)
+ .metadataBytes(4L)
+ .build())
+ .build();
+ assertEquals(presetSorobanData, actualData);
+ }
+
+ @Test
+ public void testLeavesUntouchedFootprintsUntouched() {
+ SorobanTransactionData data0 =
+ new SorobanDataBuilder(presetSorobanData).setReadOnly(null).build();
+ assertArrayEquals(
+ new LedgerKey[] {readOnly}, data0.getResources().getFootprint().getReadOnly());
+
+ SorobanTransactionData data1 =
+ new SorobanDataBuilder(presetSorobanData).setReadOnly(new ArrayList<>()).build();
+ assertArrayEquals(new LedgerKey[] {}, data1.getResources().getFootprint().getReadOnly());
+
+ SorobanTransactionData data3 =
+ new SorobanDataBuilder(presetSorobanData).setReadWrite(null).build();
+ assertArrayEquals(
+ new LedgerKey[] {readWrite}, data3.getResources().getFootprint().getReadWrite());
+
+ SorobanTransactionData data4 =
+ new SorobanDataBuilder(presetSorobanData).setReadWrite(new ArrayList<>()).build();
+ assertArrayEquals(new LedgerKey[] {}, data4.getResources().getFootprint().getReadWrite());
+ }
+
+ @Test
+ public void testBuildCopy() {
+ SorobanTransactionData actualData = new SorobanDataBuilder(presetSorobanData).build();
+ assertEquals(presetSorobanData, actualData);
+ assertNotSame(presetSorobanData, actualData);
+ }
+}
diff --git a/src/test/java/org/stellar/sdk/SorobanServerTest.java b/src/test/java/org/stellar/sdk/SorobanServerTest.java
index 57bba78ee..58cf86bfd 100644
--- a/src/test/java/org/stellar/sdk/SorobanServerTest.java
+++ b/src/test/java/org/stellar/sdk/SorobanServerTest.java
@@ -1362,35 +1362,40 @@ private Transaction buildSorobanTransaction(
auth = new ArrayList<>();
}
- return new TransactionBuilder(AccountConverter.enableMuxed(), source, Network.STANDALONE)
- .setBaseFee(50000)
- .addPreconditions(
- TransactionPreconditions.builder().timeBounds(new TimeBounds(0, 0)).build())
- .addOperation(
- InvokeHostFunctionOperation.builder()
- .sourceAccount(opInvokerKp.getAccountId())
- .hostFunction(
- new HostFunction.Builder()
- .discriminant(HostFunctionType.HOST_FUNCTION_TYPE_INVOKE_CONTRACT)
- .invokeContract(
- new SCVec(
- new SCVal[] {
- new Address(contractId).toSCVal(),
- new SCVal.Builder()
- .discriminant(SCValType.SCV_SYMBOL)
- .sym(new SCSymbol(new XdrString("increment")))
- .build(),
- new Address(opInvokerKp.getAccountId()).toSCVal(),
- new SCVal.Builder()
- .discriminant(SCValType.SCV_U32)
- .u32(new Uint32(new XdrUnsignedInteger(10)))
- .build()
- }))
- .build())
- .auth(auth)
- .build())
- .setSorobanData(sorobanData)
- .build();
+ TransactionBuilder transactionBuilder =
+ new TransactionBuilder(AccountConverter.enableMuxed(), source, Network.STANDALONE)
+ .setBaseFee(50000)
+ .addPreconditions(
+ TransactionPreconditions.builder().timeBounds(new TimeBounds(0, 0)).build())
+ .addOperation(
+ InvokeHostFunctionOperation.builder()
+ .sourceAccount(opInvokerKp.getAccountId())
+ .hostFunction(
+ new HostFunction.Builder()
+ .discriminant(HostFunctionType.HOST_FUNCTION_TYPE_INVOKE_CONTRACT)
+ .invokeContract(
+ new SCVec(
+ new SCVal[] {
+ new Address(contractId).toSCVal(),
+ new SCVal.Builder()
+ .discriminant(SCValType.SCV_SYMBOL)
+ .sym(new SCSymbol(new XdrString("increment")))
+ .build(),
+ new Address(opInvokerKp.getAccountId()).toSCVal(),
+ new SCVal.Builder()
+ .discriminant(SCValType.SCV_U32)
+ .u32(new Uint32(new XdrUnsignedInteger(10)))
+ .build()
+ }))
+ .build())
+ .auth(auth)
+ .build());
+
+ if (sorobanData != null) {
+ transactionBuilder.setSorobanData(sorobanData);
+ }
+
+ return transactionBuilder.build();
}
private static SorobanAuthorizationEntry sorobanAuthorizationEntryFromXdrBase64(