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 SorobanDataBuilder to prepare sorobanData easily. #509

Merged
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
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