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 support for Preview 10 operations when assembling Soroban transactions. #108

Merged
merged 8 commits into from
Jul 11, 2023
Merged
Show file tree
Hide file tree
Changes from 7 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: 1 addition & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ A breaking change should be clearly marked in this log.

## Unreleased

## v0.9.0

### Updated
* `Server.getContractData` has an additional, optional parameter: `expirationType?: string` which should be set to either `'temporary'` or `'persistent'` depending on the type of ledger key. By default, it will attempt to fetch both, returning whichever one it finds ([#103](https://github.com/stellar/js-soroban-client/pull/103)).
* `assembleTransaction` now accepts simulation results for the new `BumpFootprintExpirationOp`s and `RestoreFootprintOp`s ([#108](https://github.com/stellar/js-soroban-client/pull/108)).
* The XDR library (`stellar-base`) has been upgraded to Preview 10's protocol format. This includes the following changes:

#### Breaking Changes
Expand All @@ -25,7 +25,6 @@ A breaking change should be clearly marked in this log.
- We have added two new operations related to state expiration in Soroban: `BumpFootprintExpiration` and `RestoreFootprint`. Please refer to their docstrings for details ([#633](https://github.com/stellar/js-stellar-base/pull/633)).



## v0.8.1

### Fix
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@
"eventsource": "^2.0.2",
"lodash": "^4.17.21",
"randombytes": "^2.1.0",
"stellar-base": "10.0.0-soroban.1",
"stellar-base": "10.0.0-soroban.2",
"toml": "^3.0.0",
"urijs": "^1.19.1"
}
Expand Down
54 changes: 31 additions & 23 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ export class Server {
* // Uncomment the following line to build transactions for the live network. Be
* // sure to also change the horizon hostname.
* // networkPassphrase: SorobanClient.Networks.PUBLIC,
* networkPassphrase: SorobanClient.Networks.STANDALONE
* networkPassphrase: SorobanClient.Networks.FUTURENET
* })
* // Add a contract.increment soroban contract invocation operation
* .addOperation(contract.call("increment"))
Expand All @@ -434,11 +434,15 @@ export class Server {
* });
*
* @param {Transaction | FeeBumpTransaction} transaction - The transaction to
* simulate. It should include exactly one operation, which must be a
* {@link InvokeHostFunctionOp}. Any provided footprint will be ignored.
* simulate. It should include exactly one operation, which must be one of
* {@link xdr.InvokeHostFunctionOp}, {@link xdr.BumpFootprintExpirationOp},
* or {@link xdr.RestoreFootprintOp}. Any provided footprint will be
* ignored.
*
* @returns {Promise<SorobanRpc.SimulateTransactionResponse>} Returns a
* promise to the {@link SorobanRpc.SimulateTransactionResponse} object
* with the cost, result, footprint, auth, and error of the transaction.
* with the cost, footprint, result/auth requirements (if applicable), and
* error of the transaction.
*/
public async simulateTransaction(
transaction: Transaction | FeeBumpTransaction,
Expand All @@ -452,18 +456,20 @@ export class Server {

/**
* Submit a trial contract invocation, first run a simulation of the contract
* invocation as defined on the incoming transaction, and apply the results
* to a new copy of the transaction which is then returned. Setting the ledger
* footprint and authorization, so the resulting transaction is ready for signing & sending.
* invocation as defined on the incoming transaction, and apply the results to
* a new copy of the transaction which is then returned. Setting the ledger
* footprint and authorization, so the resulting transaction is ready for
* signing & sending.
*
* The returned transaction will also have an updated fee that is the sum of fee set
* on incoming transaction with the contract resource fees estimated from simulation. It is
* adviseable to check the fee on returned transaction and validate or take appropriate
* measures for interaction with user to confirm it is acceptable.
* The returned transaction will also have an updated fee that is the sum of
* fee set on incoming transaction with the contract resource fees estimated
* from simulation. It is adviseable to check the fee on returned transaction
* and validate or take appropriate measures for interaction with user to
* confirm it is acceptable.
*
* You can call the {simulateTransaction(transaction)} method directly first if you
* want to inspect estimated fees for a given transaction in detail first if that is
* of importance.
* You can call the {@link Server.simulateTransaction} method directly first
* if you want to inspect estimated fees for a given transaction in detail
* first, if that is of importance.
*
* @example
* const contractId = '0000000000000000000000000000000000000000000000000000000000000001';
Expand Down Expand Up @@ -503,17 +509,19 @@ export class Server {
* });
*
* @param {Transaction | FeeBumpTransaction} transaction - The transaction to
* prepare. It should include exactly one operation, which must be a
* {@link InvokeHostFunctionOp}. Any provided footprint will be overwritten.
* prepare. It should include exactly one operation, which must be one of
* {@link xdr.InvokeHostFunctionOp}, {@link xdr.BumpFootprintExpirationOp},
* or {@link xdr.RestoreFootprintOp}. Any provided footprint will be
* overwritten.
* @param {string} [networkPassphrase] - Explicitly provide a network
* passphrase. If not passed, the current network passphrase will be requested
* from the server via `getNetwork`.
* @returns {Promise<Transaction | FeeBumpTransaction>} Returns a copy of the
* transaction, with the expected ledger footprint and authorizations added
* and the transaction fee will automatically be adjusted to the sum of
* the incoming transaction fee and the contract minimum resource fees
* discovered from the simulation,
* passphrase. If not passed, the current network passphrase will be
* requested from the server via {@link Server.getNetwork}.
*
* @returns {Promise<Transaction | FeeBumpTransaction>} Returns a copy of the
* transaction, with the expected authorizations (in the case of
* invocation) and ledger footprint added. The transaction fee will also
* automatically be padded with the contract's minimum resource fees
* discovered from the simulation.
*/
public async prepareTransaction(
transaction: Transaction | FeeBumpTransaction,
Expand Down
82 changes: 59 additions & 23 deletions src/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,29 @@ import { SorobanRpc } from "./soroban_rpc";
export function assembleTransaction(
raw: Transaction | FeeBumpTransaction,
networkPassphrase: string,
simulation: SorobanRpc.SimulateTransactionResponse,
simulation: SorobanRpc.SimulateTransactionResponse
): Transaction {
if ("innerTransaction" in raw) {
// TODO: Handle feebump transactions
return assembleTransaction(
raw.innerTransaction,
networkPassphrase,
simulation,
simulation
);
}

if (
raw.operations.length !== 1 ||
raw.operations[0].type !== "invokeHostFunction"
) {
throw new Error(
"unsupported operation type, must be only one InvokeHostFunctionOp in the transaction.",
if (!isSorobanTransaction(raw)) {
throw new TypeError(
"unsupported transaction: must contain exactly one " +
"invokeHostFunction, bumpFootprintExpiration, or restoreFootprint " +
"operation"
);
}

if (simulation.results.length !== 1) {
throw new Error(`simulation results invalid: ${simulation.results}`);
}


const source = new Account(raw.source, `${parseInt(raw.sequence, 10) - 1}`);
const classicFeeNum = parseInt(raw.fee, 10) || 0;
const minResourceFeeNum = parseInt(simulation.minResourceFee, 10) || 0;
Expand All @@ -61,25 +59,63 @@ export function assembleTransaction(
extraSigners: raw.extraSigners,
});

// apply the auth from the simulation to the invokeHostFunction op's props
const invokeOp: Operation.InvokeHostFunction = raw.operations[0];
txnBuilder.addOperation(
Operation.invokeHostFunction({
func: invokeOp.func,
auth: (invokeOp.auth ?? []).concat(
simulation.results[0].auth?.map((a: string) =>
xdr.SorobanAuthorizationEntry.fromXDR(a, "base64")
) ?? []
),
})
);
switch (raw.operations[0].type) {
case "invokeHostFunction":
const invokeOp: Operation.InvokeHostFunction = raw.operations[0];
txnBuilder.addOperation(
Operation.invokeHostFunction({
source: invokeOp.source,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note that previously we weren't copying the source

func: invokeOp.func,
// apply the auth from the simulation
auth: (invokeOp.auth ?? []).concat(
simulation.results[0].auth?.map((a: string) =>
xdr.SorobanAuthorizationEntry.fromXDR(a, "base64")
) ?? []
),
})
);
break;

case "bumpFootprintExpiration":
const bumpOp: Operation.BumpFootprintExpiration = raw.operations[0];
txnBuilder.addOperation(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since this and restore case are not merging any data from simulation, can it reduce to txnBuilder.addOperation(raw.operations[0]), or I may be missing a subtlety.

Copy link
Contributor Author

@Shaptic Shaptic Jul 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought so, too, but kept getting some TypeScript errors... tried it again in ea8ccb3 and it worked 🤦 Great call, thank you!

Operation.bumpFootprintExpiration({
source: bumpOp.source,
ledgersToExpire: bumpOp.ledgersToExpire,
})
);
break;

case "restoreFootprint":
const restoreOp: Operation.RestoreFootprint = raw.operations[0];
txnBuilder.addOperation(
Operation.restoreFootprint({ source: restoreOp.source })
);
break;
}

// apply the pre-built Soroban Tx Data from simulation onto the Tx
const sorobanTxData = xdr.SorobanTransactionData.fromXDR(
simulation.transactionData,
"base64",
"base64"
);
txnBuilder.setSorobanData(sorobanTxData);

return txnBuilder.build();
}
}

function isSorobanTransaction(tx: Transaction): boolean {
if (tx.operations.length !== 1) {
return false;
}

switch (tx.operations[0].type) {
case "invokeHostFunction":
case "bumpFootprintExpiration":
case "restoreFootprint":
return true;

default:
return false;
}
}
4 changes: 1 addition & 3 deletions test/unit/transaction_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,7 @@ describe("assembleTransaction", () => {
});
expect.fail();
} catch (err) {
expect(err.toString()).to.equal(
"Error: unsupported operation type, must be only one InvokeHostFunctionOp in the transaction."
);
expect(err.toString()).to.match(/TypeError: unsupported transaction/i);
}
});
});
Expand Down
Loading