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

feat: FeeBumpTransaction supports transactions that include Soroban operations. #617

Merged
merged 3 commits into from
Jul 17, 2024
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ As this project is pre 1.0, breaking changes may happen for minor version bumps.
- refactor!: `Transaction.Builder` has been removed, use `TransactionBuilder` instead.
- refactor!: refactor asset classes. `LiquidityPoolParameters`, `LiquidityPoolConstantProductParameters`, `AssetTypePoolShare`, `LiquidityPoolShareChangeTrustAsset` and `LiquidityPoolShareTrustLineAsset` have been removed. Use `ChangeTrustAsset` and `TrustLineAsset` instead.
- refactor!: `Asset.getType()` returns `org.stellar.sdk.xdr.AssetType` instead of `String`.
- refactor!: `FeeBumpTransaction.Builder` has been removed, use `FeeBumpTransaction#FeeBumpTransaction(String, long, Transaction)` instead.
- refactor!: `FeeBumpTransaction.Builder` has been removed, use `FeeBumpTransaction#createWithBaseFee(String, long, Transaction)` or `FeeBumpTransaction#createWithFee(String, long, Transaction)` instead.
- refactor!: `FeeBumpTransaction.getFeeAccount` has been removed, use `FeeBumpTransaction.getFeeSource` instead.
- refactor!: remove `AccountConverter`, this means that we no longer support disabling support for MuxedAccount.
- refactor!: refactor the way of constructing `Predicate.Or` and `Predicate.And`. The `inner` inside has been removed, and in its place are `left` and `right`, used to represent two predicates.
Expand All @@ -48,6 +48,7 @@ As this project is pre 1.0, breaking changes may happen for minor version bumps.
- feat: add `MuxedAccount` class to represent a multiplexed account on Stellar's network.
- feat: Add `Server.loadAccount` to load the `Account` object used for building transactions, supporting `MuxedAccount`.
- feat: Add support for `MuxedAccount` to `SorobanServer.getAccount`.
- feat: `FeeBumpTransaction` supports transactions that include Soroban operations.

## 0.44.0
### Update
Expand Down
57 changes: 40 additions & 17 deletions src/main/java/org/stellar/sdk/FeeBumpTransaction.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,29 @@ public class FeeBumpTransaction extends AbstractTransaction {
/** The inner transaction that is being wrapped by this fee bump transaction. */
@NonNull private final Transaction innerTransaction;

private FeeBumpTransaction(
@NonNull String feeSource, long fee, @NonNull Transaction innerTransaction) {
super(innerTransaction.getNetwork());
this.feeSource = feeSource;
this.fee = fee;
this.innerTransaction = innerTransaction;
}

/**
* Creates a new FeeBumpTransaction object, enabling you to resubmit an existing transaction with
* a higher fee.
*
* @param feeSource The account paying for the transaction fee.
* @param fee Max fee willing to pay for this transaction (in stroops)
* @param innerTransaction The inner transaction that is being wrapped by this fee bump
* transaction.
* @return {@link FeeBumpTransaction}
*/
public static FeeBumpTransaction createWithFee(
@NonNull String feeSource, long fee, @NonNull Transaction innerTransaction) {
return new FeeBumpTransaction(feeSource, fee, innerTransaction);
}

/**
* Creates a new FeeBumpTransaction object, enabling you to resubmit an existing transaction with
* a higher fee.
Expand All @@ -37,39 +60,42 @@ public class FeeBumpTransaction extends AbstractTransaction {
* @param baseFee Max fee willing to pay per operation in inner transaction (in stroops)
* @param innerTransaction The inner transaction that is being wrapped by this fee bump
* transaction.
* @return {@link FeeBumpTransaction}
*/
public FeeBumpTransaction(
public static FeeBumpTransaction createWithBaseFee(
@NonNull String feeSource, long baseFee, @NonNull Transaction innerTransaction) {
super(innerTransaction.getNetwork());
this.feeSource = feeSource;

// set fee
if (baseFee < MIN_BASE_FEE) {
throw new IllegalArgumentException(
"baseFee cannot be smaller than the BASE_FEE (" + MIN_BASE_FEE + "): " + baseFee);
}

long innerBaseFee = innerTransaction.getFee();
long innerSorobanResourceFee = 0;
if (innerTransaction.getSorobanData() != null) {
innerSorobanResourceFee = innerTransaction.getSorobanData().getResourceFee().getInt64();
}

long innerBaseFee =
innerTransaction.getFee() - innerSorobanResourceFee; // dont include soroban resource fee
long numOperations = innerTransaction.getOperations().length;
if (numOperations > 0) {
innerBaseFee = innerBaseFee / numOperations;
innerBaseFee = (long) Math.ceil((double) innerBaseFee / numOperations);
}

if (baseFee < innerBaseFee) {
throw new IllegalArgumentException(
"base fee cannot be lower than provided inner transaction base fee");
}

long maxFee = baseFee * (numOperations + 1);
long maxFee = (baseFee * (numOperations + 1)) + innerSorobanResourceFee;
if (maxFee < 0) {
throw new IllegalArgumentException("fee overflows 64 bit int");
}
fee = maxFee;

// set inner transaction
Transaction tx;
EnvelopeType txType = innerTransaction.toEnvelopeXdr().getDiscriminant();
if (txType == EnvelopeType.ENVELOPE_TYPE_TX_V0) {
this.innerTransaction =
tx =
new TransactionBuilder(
new Account(
innerTransaction.getSourceAccount(),
Expand All @@ -83,24 +109,21 @@ public FeeBumpTransaction(
.timeBounds(innerTransaction.getTimeBounds())
.build())
.build();
this.innerTransaction.signatures = new ArrayList<>(innerTransaction.signatures);
tx.signatures = new ArrayList<>(innerTransaction.signatures);
} else {
this.innerTransaction = innerTransaction;
tx = innerTransaction;
}
return new FeeBumpTransaction(feeSource, maxFee, tx);
}

public static FeeBumpTransaction fromFeeBumpTransactionEnvelope(
FeeBumpTransactionEnvelope envelope, Network network) {
Transaction inner =
Transaction.fromV1EnvelopeXdr(envelope.getTx().getInnerTx().getV1(), network);
String feeSource = StrKey.encodeMuxedAccount(envelope.getTx().getFeeSource());

long fee = envelope.getTx().getFee().getInt64();
long baseFee = fee / (inner.getOperations().length + 1);

FeeBumpTransaction feeBump = new FeeBumpTransaction(feeSource, baseFee, inner);
FeeBumpTransaction feeBump = new FeeBumpTransaction(feeSource, fee, inner);
feeBump.signatures.addAll(Arrays.asList(envelope.getSignatures()));

return feeBump;
}

Expand Down
1 change: 1 addition & 0 deletions src/main/java/org/stellar/sdk/SorobanServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,7 @@ private Transaction assembleTransaction(
"unsupported transaction: must contain exactly one InvokeHostFunctionOperation, BumpSequenceOperation, or RestoreFootprintOperation");
}

// TODO: exclude exists soroban resource fee from tx fee
long classicFeeNum = transaction.getFee();
long minResourceFeeNum =
Optional.ofNullable(simulateTransactionResponse.getMinResourceFee()).orElse(0L);
Expand Down
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 @@ -36,7 +36,7 @@
* target="_blank">Transaction</a> in Stellar network.
*/
public class Transaction extends AbstractTransaction {
/** fee paid for transaction in stroops (1 stroop = 0.0000001 XLM). */
/** Max fee paid for transaction in stroops (1 stroop = 0.0000001 XLM). */
@Getter private final long fee;

/** The source account for this transaction. */
Expand Down
150 changes: 140 additions & 10 deletions src/test/java/org/stellar/sdk/FeeBumpTransactionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,30 @@
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.fail;

import java.math.BigInteger;
import java.util.ArrayList;
import org.junit.Test;
import org.stellar.sdk.operations.InvokeHostFunctionOperation;
import org.stellar.sdk.operations.PaymentOperation;
import org.stellar.sdk.xdr.ContractExecutable;
import org.stellar.sdk.xdr.ContractExecutableType;
import org.stellar.sdk.xdr.ContractIDPreimage;
import org.stellar.sdk.xdr.ContractIDPreimageType;
import org.stellar.sdk.xdr.CreateContractArgs;
import org.stellar.sdk.xdr.EnvelopeType;
import org.stellar.sdk.xdr.ExtensionPoint;
import org.stellar.sdk.xdr.HostFunction;
import org.stellar.sdk.xdr.HostFunctionType;
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.SignerKey;
import org.stellar.sdk.xdr.SorobanResources;
import org.stellar.sdk.xdr.SorobanTransactionData;
import org.stellar.sdk.xdr.Uint256;
import org.stellar.sdk.xdr.Uint32;
import org.stellar.sdk.xdr.XdrUnsignedInteger;

public class FeeBumpTransactionTest {

Expand Down Expand Up @@ -49,7 +70,7 @@ public void testSetBaseFeeBelowNetworkMinimum() {
Transaction inner = createInnerTransaction();

try {
new FeeBumpTransaction(
FeeBumpTransaction.createWithBaseFee(
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3",
Transaction.MIN_BASE_FEE - 1,
inner);
Expand All @@ -64,7 +85,7 @@ public void testSetBaseFeeBelowInner() {
Transaction inner = createInnerTransaction(Transaction.MIN_BASE_FEE + 1);

try {
new FeeBumpTransaction(
FeeBumpTransaction.createWithBaseFee(
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3",
Transaction.MIN_BASE_FEE,
inner);
Expand All @@ -80,7 +101,7 @@ public void testSetBaseFeeOverflowsLong() {
Transaction inner = createInnerTransaction(Transaction.MIN_BASE_FEE + 1);

try {
new FeeBumpTransaction(
FeeBumpTransaction.createWithBaseFee(
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3", Long.MAX_VALUE, inner);
fail();
} catch (RuntimeException e) {
Expand All @@ -93,7 +114,7 @@ public void testSetBaseFeeEqualToInner() {
Transaction inner = createInnerTransaction();

FeeBumpTransaction feeBump =
new FeeBumpTransaction(
FeeBumpTransaction.createWithBaseFee(
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3",
Transaction.MIN_BASE_FEE,
inner);
Expand All @@ -108,7 +129,7 @@ public void testHash() {
"2a8ead3351faa7797b284f59027355ddd69c21adb8e4da0b9bb95531f7f32681", inner.hashHex());

FeeBumpTransaction feeBump =
new FeeBumpTransaction(
FeeBumpTransaction.createWithBaseFee(
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3",
Transaction.MIN_BASE_FEE * 2,
inner);
Expand All @@ -121,7 +142,7 @@ public void testRoundTripXdr() {
Transaction inner = createInnerTransaction();

FeeBumpTransaction feeBump =
new FeeBumpTransaction(
FeeBumpTransaction.createWithBaseFee(
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3",
Transaction.MIN_BASE_FEE * 2,
inner);
Expand Down Expand Up @@ -156,7 +177,7 @@ public void testFeeBumpUpgradesInnerToV1() {
innerV0.setEnvelopeType(EnvelopeType.ENVELOPE_TYPE_TX_V0);

FeeBumpTransaction feeBump =
new FeeBumpTransaction(
FeeBumpTransaction.createWithBaseFee(
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3",
Transaction.MIN_BASE_FEE * 2,
innerV0);
Expand Down Expand Up @@ -192,14 +213,14 @@ public void testFeeBumpUpgradesInnerToV1() {
public void testHashCodeAndEquals() {
Transaction inner = createInnerTransaction();
FeeBumpTransaction feeBump0 =
new FeeBumpTransaction(
FeeBumpTransaction.createWithBaseFee(
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3",
Transaction.MIN_BASE_FEE * 2,
inner);

// they get different base fee
FeeBumpTransaction feeBump2 =
new FeeBumpTransaction(
FeeBumpTransaction.createWithBaseFee(
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3",
Transaction.MIN_BASE_FEE * 3,
createInnerTransaction(Network.PUBLIC));
Expand All @@ -208,11 +229,120 @@ public void testHashCodeAndEquals() {

// they get different network
FeeBumpTransaction feeBump3 =
new FeeBumpTransaction(
FeeBumpTransaction.createWithBaseFee(
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3",
Transaction.MIN_BASE_FEE * 2,
createInnerTransaction(Network.PUBLIC));

assertNotEquals(feeBump0, feeBump3);
}

@Test
public void testCreateWithBaseFee() {
Transaction inner = createInnerTransaction(300);
FeeBumpTransaction feeBumpTx =
FeeBumpTransaction.createWithBaseFee(
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3", 400, inner);

assertEquals(
feeBumpTx.getFeeSource(), "GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3");
assertEquals(feeBumpTx.getFee(), 800);
assertEquals(feeBumpTx.getInnerTransaction(), inner);
}

@Test
public void testCreateWithFee() {
Transaction inner = createInnerTransaction(300);
FeeBumpTransaction feeBumpTx =
FeeBumpTransaction.createWithFee(
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3", 1100, inner);

assertEquals(
feeBumpTx.getFeeSource(), "GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3");
assertEquals(feeBumpTx.getFee(), 1100);
assertEquals(feeBumpTx.getInnerTransaction(), inner);
}

@Test
public void testCreateWithBaseFeeWithSorobanOp() {
long sorobanResourceFee = 346546L;
KeyPair source =
KeyPair.fromSecretSeed("SCH27VUZZ6UAKB67BDNF6FA42YMBMQCBKXWGMFD5TZ6S5ZZCZFLRXKHS");

Account account = new Account(source.getAccountId(), 2908908335136768L);
LedgerKey ledgerKey =
LedgerKey.builder()
.discriminant(LedgerEntryType.ACCOUNT)
.account(
LedgerKey.LedgerKeyAccount.builder()
.accountID(
KeyPair.fromAccountId(
"GB7TAYRUZGE6TVT7NHP5SMIZRNQA6PLM423EYISAOAP3MKYIQMVYP2JO")
.getXdrAccountId())
.build())
.build();
SorobanTransactionData sorobanData =
SorobanTransactionData.builder()
.resources(
SorobanResources.builder()
.footprint(
LedgerFootprint.builder()
.readOnly(new LedgerKey[] {ledgerKey})
.readWrite(new LedgerKey[] {})
.build())
.readBytes(new Uint32(new XdrUnsignedInteger(699)))
.writeBytes(new Uint32(new XdrUnsignedInteger(0)))
.instructions(new Uint32(new XdrUnsignedInteger(34567)))
.build())
.resourceFee(new Int64(sorobanResourceFee))
.ext(ExtensionPoint.builder().discriminant(0).build())
.build();

CreateContractArgs createContractArgs =
CreateContractArgs.builder()
.contractIDPreimage(
ContractIDPreimage.builder()
.discriminant(ContractIDPreimageType.CONTRACT_ID_PREIMAGE_FROM_ADDRESS)
.fromAddress(
ContractIDPreimage.ContractIDPreimageFromAddress.builder()
.address(
new Address(
"GB7TAYRUZGE6TVT7NHP5SMIZRNQA6PLM423EYISAOAP3MKYIQMVYP2JO")
.toSCAddress())
.salt(new Uint256(new byte[32]))
.build())
.build())
.executable(
ContractExecutable.builder()
.discriminant(ContractExecutableType.CONTRACT_EXECUTABLE_STELLAR_ASSET)
.build())
.build();
HostFunction hostFunction =
HostFunction.builder()
.discriminant(HostFunctionType.HOST_FUNCTION_TYPE_CREATE_CONTRACT)
.createContract(createContractArgs)
.build();
InvokeHostFunctionOperation invokeHostFunctionOperation =
InvokeHostFunctionOperation.builder().hostFunction(hostFunction).build();
Transaction transaction =
new Transaction(
account.getAccountId(),
Transaction.MIN_BASE_FEE,
account.getIncrementedSequenceNumber(),
new org.stellar.sdk.operations.Operation[] {invokeHostFunctionOperation},
null,
new TransactionPreconditions(
null, null, BigInteger.ZERO, 0, new ArrayList<SignerKey>(), null),
sorobanData,
Network.TESTNET);

FeeBumpTransaction feeBumpTransaction =
FeeBumpTransaction.createWithBaseFee(
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3", 200, transaction);
assertEquals(
feeBumpTransaction.getFeeSource(),
"GDQNY3PBOJOKYZSRMK2S7LHHGWZIUISD4QORETLMXEWXBI7KFZZMKTL3");
assertEquals(feeBumpTransaction.getFee(), 200 * (1 + 1) + sorobanResourceFee);
assertEquals(feeBumpTransaction.getInnerTransaction(), transaction);
}
}
2 changes: 1 addition & 1 deletion src/test/java/org/stellar/sdk/Sep10ChallengeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1523,7 +1523,7 @@ public void testReadChallengeTransactionRejectFeeBumpTransaction() throws IOExce

transaction.sign(server);
FeeBumpTransaction feeBumpTransaction =
new FeeBumpTransaction(server.getAccountId(), 500, transaction);
FeeBumpTransaction.createWithBaseFee(server.getAccountId(), 500, transaction);
String challenge = feeBumpTransaction.toEnvelopeXdrBase64();
try {
Sep10Challenge.readChallengeTransaction(
Expand Down
3 changes: 2 additions & 1 deletion src/test/java/org/stellar/sdk/ServerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,8 @@ private FeeBumpTransaction feeBump(Transaction inner) {
KeyPair signer =
KeyPair.fromSecretSeed("SA5ZEFDVFZ52GRU7YUGR6EDPBNRU2WLA6IQFQ7S2IH2DG3VFV3DOMV2Q");
FeeBumpTransaction tx =
new FeeBumpTransaction(signer.getAccountId(), FeeBumpTransaction.MIN_BASE_FEE * 10, inner);
FeeBumpTransaction.createWithBaseFee(
signer.getAccountId(), FeeBumpTransaction.MIN_BASE_FEE * 10, inner);
tx.sign(signer);
return tx;
}
Expand Down
Loading