Skip to content

Commit

Permalink
Add SorobanDataBuilder to prepare sorobanData easily. (#509)
Browse files Browse the repository at this point in the history
  • Loading branch information
overcat authored Aug 14, 2023
1 parent 6e9badb commit 4181dc0
Show file tree
Hide file tree
Showing 5 changed files with 363 additions and 41 deletions.
166 changes: 166 additions & 0 deletions src/main/java/org/stellar/sdk/SorobanDataBuilder.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.
*
* <p>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.
*
* <p>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<LedgerKey> readOnly) {
if (readOnly != null) {
data.getResources().getFootprint().setReadOnly(readOnly.toArray(new LedgerKey[0]));
}
return this;
}

/**
* Sets the read-write portion of the storage access footprint to be a certain set of ledger keys.
*
* <p>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<LedgerKey> readWrite) {
if (readWrite != null) {
data.getResources().getFootprint().setReadWrite(readWrite.toArray(new LedgerKey[0]));
}
return this;
}

/**
* @return the copy of the final {@link SorobanTransactionData}.
*/
public SorobanTransactionData build() {
try {
return SorobanTransactionData.fromXdrByteArray(data.toXdrByteArray());
} catch (IOException e) {
throw new IllegalArgumentException("Copy SorobanData failed, please report this bug.", e);
}
}

/** Represents the resource metrics of the Soroban data. */
@Builder(toBuilder = true)
@Value
public static class Resources {
// number of CPU instructions (uint32)
@NonNull Long cpuInstructions;
// number of bytes being read (uint32)
@NonNull Long readBytes;
// number of bytes being written (uint32)
@NonNull Long writeBytes;
// number of extended metadata bytes (uint32)
@NonNull Long metadataBytes;
}
}
2 changes: 1 addition & 1 deletion src/main/java/org/stellar/sdk/Transaction.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public class Transaction extends AbstractTransaction {
this.mPreconditions = preconditions;
this.mFee = fee;
this.mMemo = memo != null ? memo : Memo.none();
this.mSorobanData = sorobanData;
this.mSorobanData = sorobanData != null ? new SorobanDataBuilder(sorobanData).build() : null;
}

// setEnvelopeType is only used in tests which is why this method is package protected
Expand Down
28 changes: 17 additions & 11 deletions src/main/java/org/stellar/sdk/TransactionBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import static org.stellar.sdk.TransactionPreconditions.TIMEOUT_INFINITE;

import com.google.common.base.Function;
import java.io.IOException;
import java.math.BigInteger;
import java.util.Collection;
import java.util.List;
Expand Down Expand Up @@ -264,29 +263,36 @@ public static org.stellar.sdk.xdr.TimeBounds buildTimeBounds(long minTime, long
}

/**
* Sets Soroban data to the transaction. TODO: After adding SorobanServer, add more descriptions.
* Sets the transaction's internal Soroban transaction data (resources, footprint, etc.).
*
* <p>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.).
*
* <p>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;
}
}
145 changes: 145 additions & 0 deletions src/test/java/org/stellar/sdk/SorobanDataBuilderTest.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit 4181dc0

Please sign in to comment.