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

Update client to support Preview 10 #103

Merged
merged 19 commits into from
Jul 3, 2023
Merged
Show file tree
Hide file tree
Changes from 13 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: 3 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,6 @@ jobs:

- name: Browser Tests
run: yarn test:browser

- name: Run Prettier Checks
run: yarn fmt && (git diff-index --quiet HEAD; git diff)
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,24 @@ 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)).
* The XDR library (`stellar-base`) has been upgraded to Preview 10's protocol format. This includes the following changes:

#### Breaking Changes

- Many XDR structures have changed, please refer to the `types/next.d.ts` diff for details ([#633](https://github.com/stellar/js-stellar-base/pull/633)).
- We have returned to the world in which **one** transaction contains **one** operation which contains **one** host function invocation. This means `Operation.invokeHostFunctions` is gone and `Operation.invokeHostFunction` has changed to accept `{ func, auth }`, where `func` is the correct `xdr.HostFunction` and `auth` is a list of `xdr.SorobanAuthorizationEntry` items that outline the authorization tree for the call stack ([#633](https://github.com/stellar/js-stellar-base/pull/633)). Better abstractions for creating an `xdr.HostFunction` are forthcoming, though you can still refer to `Contract.call()` for help.

#### Added

- A new abstraction for dealing with large integers and `ScVal`s: see `ScInt`, `XdrLargeInt`, and `scValToBigInt` ([#620](https://github.com/stellar/js-stellar-base/pull/620)).
- A new abstraction for converting between native JavaScript types and complex `ScVal`s: see `nativeToScVal` and `scValToNative` ([#630](https://github.com/stellar/js-stellar-base/pull/630)).
- 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

Expand Down
44 changes: 22 additions & 22 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "soroban-client",
"version": "0.8.1",
"version": "0.9.0",
"description": "A library for working with Stellar's Soroban RPC servers.",
"author": "Stellar Development Foundation <hello@stellar.org>",
"homepage": "https://github.com/stellar/js-soroban-client",
Expand Down Expand Up @@ -29,7 +29,7 @@
"build:prod": "cross-env NODE_ENV=production yarn _build",
"build:node": "yarn _babel && yarn build:ts",
"build:ts": "tsc -p ./tsconfig.json",
"build:browser": "webpack --stats-modules-space 999 -c ./webpack.config.browser.js",
"build:browser": "webpack -c ./webpack.config.browser.js",
"build:docs": "cross-env NODE_ENV=docs yarn _babel",
"clean": "rm -rf lib/ dist/ coverage/ .nyc_output/ jsdoc/",
"docs": "yarn build:docs && jsdoc -c .jsdoc.json --verbose",
Expand Down Expand Up @@ -76,31 +76,31 @@
]
},
"devDependencies": {
"@babel/cli": "^7.21.5",
"@babel/core": "^7.22.1",
"@babel/eslint-parser": "^7.21.8",
"@babel/eslint-plugin": "^7.19.1",
"@babel/preset-env": "^7.22.4",
"@babel/preset-typescript": "^7.21.5",
"@babel/register": "^7.21.0",
"@definitelytyped/dtslint": "^0.0.162",
"@babel/cli": "^7.22.5",
"@babel/core": "^7.22.5",
"@babel/eslint-parser": "^7.22.5",
"@babel/eslint-plugin": "^7.22.5",
"@babel/preset-env": "^7.22.5",
"@babel/preset-typescript": "^7.22.5",
"@babel/register": "^7.22.5",
"@definitelytyped/dtslint": "^0.0.163",
"@istanbuljs/nyc-config-babel": "3.0.0",
"@stellar/tsconfig": "^1.0.2",
"@types/detect-node": "^2.0.0",
"@types/eventsource": "^1.1.2",
"@types/lodash": "^4.14.192",
"@types/node": "^20.2.5",
"@types/node": "^20.3.2",
"@types/randombytes": "^2.0.0",
"@types/urijs": "^1.19.6",
"@typescript-eslint/parser": "^5.59.8",
"axios-mock-adapter": "^1.21.4",
"@typescript-eslint/parser": "^5.60.1",
"axios-mock-adapter": "^1.21.5",
"babel-loader": "^9.1.2",
"babel-plugin-istanbul": "^6.1.1",
"chai": "^4.3.7",
"chai-as-promised": "^7.1.1",
"chai-http": "^4.3.0",
"chai-http": "^4.4.0",
"cross-env": "^7.0.3",
"eslint": "^8.40.0",
"eslint": "^8.43.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.25.2",
Expand All @@ -114,27 +114,27 @@
"karma": "^6.4.2",
"karma-chai": "^0.1.0",
"karma-chrome-launcher": "^3.1.0",
"karma-coverage": "^2.2.0",
"karma-coverage": "^2.2.1",
"karma-firefox-launcher": "^2.1.1",
"karma-mocha": "^2.0.0",
"karma-sinon-chai": "^2.0.2",
"karma-webpack": "^5.0.0",
"lint-staged": "^13.2.2",
"lint-staged": "^13.2.3",
"minami": "^1.1.1",
"mocha": "^10.2.0",
"node-polyfill-webpack-plugin": "^2.0.1",
"nyc": "^15.1.0",
"prettier": "^2.8.8",
"randombytes": "^2.1.0",
"sinon": "^15.0.3",
"sinon": "^15.2.0",
"sinon-chai": "^3.7.0",
"taffydb": "^2.7.3",
"terser-webpack-plugin": "^5.3.8",
"ts-node": "^10.9.1",
"typescript": "^5.0.4",
"typescript": "^5.1.5",
"utility-types": "^3.7.0",
"webpack": "^5.85.0",
"webpack-cli": "^5.1.1"
"webpack": "^5.88.1",
"webpack-cli": "^5.1.4"
},
"dependencies": {
"axios": "^1.4.0",
Expand All @@ -145,7 +145,7 @@
"eventsource": "^2.0.2",
"lodash": "^4.17.21",
"randombytes": "^2.1.0",
"stellar-base": "9.0.0-soroban.3",
"stellar-base": "10.0.0-soroban.1",
"toml": "^3.0.0",
"urijs": "^1.19.1"
}
Expand Down
91 changes: 68 additions & 23 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,24 @@ export interface GetEventsRequest {
}

/**
* Server handles the network connection to a [Soroban-RPC](https://soroban.stellar.org/docs)
* instance and exposes an interface for requests to that instance.
* Server handles the network connection to a
* [Soroban-RPC](https://soroban.stellar.org/docs) instance and exposes an
* interface for requests to that instance.
*
* @constructor
* @param {string} serverURL Soroban-RPC Server URL (ex. `http://localhost:8000/soroban/rpc`).
*
* @param {string} serverURL Soroban-RPC Server URL (ex.
* `http://localhost:8000/soroban/rpc`).
* @param {object} [opts] Options object
* @param {boolean} [opts.allowHttp] - Allow connecting to http servers, default: `false`. This must be set to false in production deployments! You can also use {@link Config} class to set this globally.
* @param {string} [opts.appName] - Allow set custom header `X-App-Name`, default: `undefined`.
* @param {string} [opts.appVersion] - Allow set custom header `X-App-Version`, default: `undefined`.
* @param {boolean} [opts.allowHttp] - Allow connecting to http servers,
* default: `false`. This must be set to false in production deployments! You
* can also use {@link Config} class to set this globally.
* @param {string} [opts.appName] - Allow set custom header `X-App-Name`
* @param {string} [opts.appVersion] - Allow set custom header `X-App-Version`
*/
export class Server {
/**
* serverURL Soroban-RPC Server URL (ex. `http://localhost:8000/soroban/rpc`).
* Soroban-RPC Server URL (ex. `http://localhost:8000/soroban/rpc`).
*/
public readonly serverURL: URI;

Expand Down Expand Up @@ -135,49 +141,85 @@ export class Server {
* @example
* const contractId = "CCJZ5DGASBWQXR5MPFCJXMBI333XE5U3FSJTNQU7RIKE3P5GN2K2WYD5";
* const key = xdr.ScVal.scvSymbol("counter");
* server.getContractData(contractId, key).then(data => {
* server.getContractData(contractId, key, 'temporary').then(data => {
* console.log("value:", data.xdr);
* console.log("lastModified:", data.lastModifiedLedgerSeq);
* console.log("latestLedger:", data.latestLedger);
* });
*
* Allows you to directly inspect the current state of a contract. This is a backup way to access your contract data which may not be available via events or simulateTransaction.
* Allows you to directly inspect the current state of a contract. This is a
* backup way to access your contract data which may not be available via
* events or simulateTransaction.
*
* @param {string} contractId - The contract ID containing the data to load. Encoded as Stellar Contract Address e.g. `CCJZ5DGASBWQXR5MPFCJXMBI333XE5U3FSJTNQU7RIKE3P5GN2K2WYD5` or a hex string for backwards compatibility, but will likely be deprecated in the future.
* @param {string} contractId - The contract ID containing the data to load.
* Encoded as Stellar Contract Address e.g.
* `CCJZ5DGASBWQXR5MPFCJXMBI333XE5U3FSJTNQU7RIKE3P5GN2K2WYD5` or a hex
* string for backwards compatibility, but will likely be deprecated in the
* future.
* @param {xdr.ScVal} key - The key of the contract data to load.
* @returns {Promise<SorobanRpc.LedgerEntryResult>} Returns a promise to the {@link SorobanRpc.LedgerEntryResult} object with the current value.
* @param {string} [expirationType] - The "durability keyspace" that this
* ledger key belongs to, which is either 'temporary' (the default) or
* 'persistent'.
*
* @returns {Promise<SorobanRpc.LedgerEntryResult>} Returns a promise to the
* {@link SorobanRpc.LedgerEntryResult} object with the current value.
*
* @warning If the data entry in question is a 'temporary' entry, it's
* entirely possible that it has expired out of existence. Future versions
* of this client may provide abstractions to handle that.
*/
public async getContractData(
contractId: string,
key: xdr.ScVal,
expirationType: 'temporary' | 'persistent' = 'temporary'
Copy link
Contributor

Choose a reason for hiding this comment

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

3 suggestions:

  • Let's call this durability to match the API elsewhere, and in the xdr.
  • wdyt of making persistent the default? That is what we are pushing people towards.
  • Maybe extract the Durability to a type or enum?
Suggested change
expirationType: 'temporary' | 'persistent' = 'temporary'
durability: Durability = 'persistent'

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh really? I thought temporary was actually preferred (i.e. if you want to pay for and worry about persistence, you should know its purpose & understand it better).

Copy link
Contributor

Choose a reason for hiding this comment

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

I think that this dialog suggests that not having a default might be the preferred way to go?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess it begs the question of "how much do we want people to care about storage durability?" is the answer "when it's important", "usually", or "always"?

): Promise<SorobanRpc.LedgerEntryResult> {
const contractKey = xdr.LedgerKey.contractData(
let durability: xdr.ContractDataDurability;
switch (expirationType) {
case 'temporary':
durability = xdr.ContractDataDurability.temporary();
break;

case 'persistent':
durability = xdr.ContractDataDurability.persistent();
break;

default:
throw new TypeError(`invalid expirationType: ${expirationType}`);
}

let contractKey: string = xdr.LedgerKey.contractData(
new xdr.LedgerKeyContractData({
contractId: new Contract(contractId).address().toBuffer(),
contract: new Contract(contractId).address().toScAddress(),
key,
}),
durability,
bodyType: xdr.ContractEntryBodyType.dataEntry() // expirationExtension is internal
})
).toXDR("base64");
const getLedgerEntriesResponse: SorobanRpc.GetLedgerEntriesResponse = await jsonrpc.post(

const getLedgerEntriesResponse = await jsonrpc.post<
SorobanRpc.GetLedgerEntriesResponse
>(
this.serverURL.toString(),
"getLedgerEntries",
[contractKey],
);
if (getLedgerEntriesResponse.entries.length === 0) {

if (getLedgerEntriesResponse.entries.length !== 1) {
return Promise.reject({
code: 404,
message: "Ledger entry not found. Key: " + contractKey,
message: `Ledger entry not found. Key: ${contractKey}`
});
}

return getLedgerEntriesResponse.entries[0];
}

/**
* Reads the current value of ledger entries directly.
*
* Allows you to directly inspect the current state of contracts,
* contract's code, or any other ledger entries. This is a backup way to access
* your contract data which may not be available via events or
* simulateTransaction.
* Allows you to directly inspect the current state of contracts, contract's
* code, or any other ledger entries. This is a backup way to access your
* contract data which may not be available via events or simulateTransaction.
*
* To fetch contract wasm byte-code, use the ContractCode ledger entry key.
*
Expand All @@ -196,7 +238,10 @@ export class Server {
* });
*
* @param {xdr.ScVal} key - The key of the contract data to load.
* @returns {Promise<SorobanRpc.GetLedgerEntriesResponse>} Returns a promise to the {@link SorobanRpc.GetLedgerEntriesResponse} object with the current value.
*
* @returns {Promise<SorobanRpc.GetLedgerEntriesResponse>} Returns a promise
* to the {@link SorobanRpc.GetLedgerEntriesResponse} object with the
* current value.
*/
public async getLedgerEntries(
keys: xdr.LedgerKey[],
Expand Down Expand Up @@ -469,7 +514,7 @@ export class Server {
if (simResponse.error) {
throw simResponse.error;
}
if (!simResponse.results || simResponse.results.length < 1) {
if (!simResponse.results || simResponse.results.length !== 1) {
throw new Error("transaction simulation failed");
}
return assembleTransaction(transaction, passphrase, simResponse);
Expand Down
6 changes: 3 additions & 3 deletions src/soroban_rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ export namespace SorobanRpc {
}

export interface SimulateHostFunctionResult {
// each string is ContractAuth XDR in base64
auth: string[];
// each string is SorobanAuthorizationEntry XDR in base64
auth?: string[];
// function response as SCVal XDR in base64
xdr: string;
}
Expand All @@ -131,7 +131,7 @@ export namespace SorobanRpc {
transactionData: string;
events: string[];
minResourceFee: string;
results: SimulateHostFunctionResult[];
results: SimulateHostFunctionResult[]; // always one element, tho
latestLedger: number;
cost: Cost;
}
Expand Down
61 changes: 20 additions & 41 deletions src/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,29 +32,23 @@ export function assembleTransaction(
);
}

const rawInvokeHostFunctionOp: any = raw.operations[0];

if (
!rawInvokeHostFunctionOp.functions ||
!simulation.results ||
rawInvokeHostFunctionOp.functions.length !== simulation.results.length
) {
throw new Error(
"preflight simulation results do not contain same count of HostFunctions that InvokeHostFunctionOp in the transaction has.",
);
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;
const txnBuilder = new TransactionBuilder(source, {
// automatically update the tx fee that will be set on the resulting tx
// to the sum of 'classic' fee provided from incoming tx.fee
// and minResourceFee provided by simulation.
// automatically update the tx fee that will be set on the resulting tx to
// the sum of 'classic' fee provided from incoming tx.fee and minResourceFee
// provided by simulation.
//
// 'classic' tx fees are measured as the product of tx.fee * 'number of operations', In soroban contract tx,
// there can only be single operation in the tx, so can make simplification
// of total classic fees for the soroban transaction will be equal to incoming tx.fee + minResourceFee.
// 'classic' tx fees are measured as the product of tx.fee * 'number of
// operations', In soroban contract tx, there can only be single operation
// in the tx, so can make simplification of total classic fees for the
// soroban transaction will be equal to incoming tx.fee + minResourceFee.
fee: (classicFeeNum + minResourceFeeNum).toString(),
memo: raw.memo,
networkPassphrase,
Expand All @@ -66,20 +60,17 @@ export function assembleTransaction(
extraSigners: raw.extraSigners,
});

// apply the pre-built Auth from simulation onto each Tx/Op/HostFunction
// invocation
const authDecoratedHostFunctions = simulation.results.map(
(functionSimulationResult, i) => {
const hostFn: xdr.HostFunction = rawInvokeHostFunctionOp.functions[i];
hostFn.auth(buildContractAuth(functionSimulationResult.auth));
return hostFn;
},
);

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

// apply the pre-built Soroban Tx Data from simulation onto the Tx
Expand All @@ -91,15 +82,3 @@ export function assembleTransaction(

return txnBuilder.build();
}

function buildContractAuth(auths: string[]): xdr.ContractAuth[] {
const contractAuths: xdr.ContractAuth[] = [];

if (auths) {
for (const authStr of auths) {
contractAuths.push(xdr.ContractAuth.fromXDR(authStr, "base64"));
}
}

return contractAuths;
}
Loading