Skip to content

Commit

Permalink
feat!: new exports contract and rpc
Browse files Browse the repository at this point in the history
- `SorobanRpc` now also exported as `rpc`

- New main export `contract`

  This allows us to import it the usual way, instead of needing to do
  things like

      import { ContractClient } from "stellar-sdk/lib/contract_client"

  which doesn't work in the browser (because `lib`)

- `ContractSpec` now available at `contract.Spec`

- Also includes other supporting classes, functions, and types:
  - `contract.AssembledTransaction`
  - `contract.ClientOptions`
  - etc

- These are also available at matching entrypoints, if your
node and TypeScript configuration support the `exports` declaration:

    import {
      Client,
      ClientOptions,
      AssembledTransaction,
    } from 'stellar-sdk/contract'

This also attempts to document exported modules. The JSDoc-style
comments in `src/index.ts` don't actually show up when you import these
things. We can figure this out later. Docs here
https://jsdoc.app/howto-es2015-modules. In the meantime, these are still
useful notes that we can use later.

Co-authored-by: George <Shaptic@users.noreply.github.com>
  • Loading branch information
chadoh and Shaptic committed May 15, 2024
1 parent e59a733 commit 7883dc3
Show file tree
Hide file tree
Showing 42 changed files with 367 additions and 249 deletions.
71 changes: 68 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,75 @@ A breaking change will get clearly marked in this log.

## Unreleased

### Breaking Changes

- `ContractClient` functionality previously added in [v11.3.0](https://github.com/stellar/js-stellar-sdk/releases/tag/v11.3.0) was exported in a non-standard way. You can now import it as any other stellar-sdk module.

```diff
- import { ContractClient } from '@stellar/stellar-sdk/lib/contract_client'
+ import { contract } from '@stellar/stellar-sdk'
+ const { Client } = contract
```

Note that this top-level `contract` export is a container for ContractClient and related functionality. The ContractClient class is now available at `contract.Client`, as shown. Further note that there is a capitalized `Contract` export as well, which comes [from stellar-base](https://github.com/stellar/js-stellar-base/blob/b96281b9b3f94af23a913f93bdb62477f5434ccc/src/contract.js#L6-L19). You can remember which is which because capital-C `Contract` is a class, whereas lowercase-c `contract` is a container/module with a bunch of classes, functions, and types.

Additionally, this is available from the `/contract` entrypoint, if your version of Node [and TypeScript](https://stackoverflow.com/a/70020984/249801) support [the `exports` declaration](https://nodejs.org/api/packages.html#exports). Finally, some of its exports have been renamed.

```diff
import {
- ContractClient,
+ Client,
AssembledTransaction,
- ContractClientOptions,
+ ClientOptions,
SentTransaction,
-} from '@stellar/stellar-sdk/lib/contract_client'
+} from '@stellar/stellar-sdk/contract'
```


- The `ContractSpec` class is now nested under the `contract` module, and has been renamed to `Spec`.

```diff
-import { ContractSpec } from '@stellar/stellar-sdk'
+import { contract } from '@stellar/stellar-sdk'
+const { Spec } = contract
```

Alternatively, you can import this from the `contract` entrypoint, if your version of Node [and TypeScript](https://stackoverflow.com/a/70020984/249801) support [the `exports` declaration](https://nodejs.org/api/packages.html#exports).

```diff
-import { ContractSpec } from '@stellar/stellar-sdk'
+import { Spec } from '@stellar/stellar-sdk/contract'
```

- Previously, `AssembledTransaction.signAndSend()` would return a `SentTransaction` even if the transaction never finalized. That is, if it successfully sent the transaction to the network, but the transaction was still `status: 'PENDING'`, then it would `console.error` an error message, but return the indeterminate transaction anyhow.

It now throws a `SentTransaction.Errors.TransactionStillPending` error with that error message instead.

### Deprecated

- `SorobanRpc` module is now also exported as `rpc`. You can import it with either name for now, but `SorobanRpc` will be removed in a future release.

```diff
import { SorobanRpc } from '@stellar/stellar-sdk'
+// OR
+import { rpc } from '@stellar/stellar-sdk'
```

You can also now import it at the `/rpc` entrypoint, if your version of Node [and TypeScript](https://stackoverflow.com/a/70020984/249801) support [the `exports` declaration](https://nodejs.org/api/packages.html#exports).

```diff
-import { SorobanRpc } from '@stellar/stellar-sdk'
-const { Api } = SorobanRpc
+import { Api } from '@stellar/stellar-sdk/rpc'
```

### Added
* Added a from method in `ContractClient` which takes the `ContractClientOptions` and instantiates the `ContractClient` by utilizing the `contractId` to retrieve the contract wasm from the blockchain. The custom section is then extracted and used to create a `ContractSpec` which is then used to create the client.
* Similarly adds `fromWasm` and `fromWasmHash` methods in `ContractClient` which can be used to initialize a `ContractClient` if you already have the wasm bytes or the wasm hash along with the `ContractClientOptions`.
* Added `getContractWasmByContractId` and `getContractWasmByHash` methods in `Server` which can be used to retrieve the wasm bytecode of a contract via its `contractId` and wasm hash respectively.

* Added a `from` method in `contract.Client` which takes the `ClientOptions` and instantiates the `Client` by utilizing the `contractId` to retrieve the contract wasm from the blockchain. The custom section is then extracted and used to create a `contract.Spec` which is then used to create the client.
* Similarly adds `fromWasm` and `fromWasmHash` methods in `Client` which can be used to initialize a `Client` if you already have the wasm bytes or the wasm hash along with the `ClientOptions`.
* Added `getContractWasmByContractId` and `getContractWasmByHash` methods in `rpc.Server` which can be used to retrieve the wasm bytecode of a contract via its `contractId` and wasm hash respectively.

## [v12.0.0-rc.2](https://github.com/stellar/js-stellar-sdk/compare/v11.3.0...v12.0.0-rc.2)

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ The usage documentation for this library lives in a handful of places:
You can also refer to:

* the [documentation](https://developers.stellar.org/network/horizon) for the Horizon REST API (if using the `Horizon` module) and
* the [documentation](https://soroban.stellar.org/docs/reference/rpc) for Soroban RPC's API (if using the `SorobanRpc` module)
* the [documentation](https://soroban.stellar.org/docs/reference/rpc) for Soroban RPC's API (if using the `rpc` module)

### Usage with React-Native

Expand Down
15 changes: 15 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,21 @@
"/lib",
"/dist"
],
"exports": {
".": {
"browser": "./dist/stellar-sdk.min.js",
"types": "./lib/index.d.ts",
"default": "./lib/index.js"
},
"./contract": {
"types": "./lib/contract/index.d.ts",
"default": "./lib/contract/index.js"
},
"./rpc": {
"types": "./lib/rpc/index.d.ts",
"default": "./lib/rpc/index.js"
}
},
"scripts": {
"build": "cross-env NODE_ENV=development yarn _build",
"build:prod": "cross-env NODE_ENV=production yarn _build",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
/* disable max-classes rule, because extending error shouldn't count! */
/* eslint max-classes-per-file: 0 */
import type {
AssembledTransactionOptions,
ContractClientOptions,
MethodOptions,
Tx,
XDR_BASE64,
} from "./types";
import type { ContractClient } from "./client";
import {
Account,
BASE_FEE,
Contract,
Operation,
SorobanRpc,
StrKey,
TransactionBuilder,
authorizeEntry,
xdr,
} from "..";
import { Err } from "../rust_types";
} from "@stellar/stellar-base";
import type {
AssembledTransactionOptions,
ClientOptions,
MethodOptions,
Tx,
XDR_BASE64,
} from "./types";
import { Server } from "../rpc/server";
import { Api } from "../rpc/api";
import { assembleTransaction } from "../rpc/transaction";
import type { Client } from "./client";
import { Err } from "./rust_result";
import {
DEFAULT_TIMEOUT,
contractErrorPattern,
Expand All @@ -31,15 +33,15 @@ export const NULL_ACCOUNT =
"GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";

/**
* The main workhorse of {@link ContractClient}. This class is used to wrap a
* The main workhorse of {@link Client}. This class is used to wrap a
* transaction-under-construction and provide high-level interfaces to the most
* common workflows, while still providing access to low-level stellar-sdk
* transaction manipulation.
*
* Most of the time, you will not construct an `AssembledTransaction` directly,
* but instead receive one as the return value of a `ContractClient` method. If
* but instead receive one as the return value of a `Client` method. If
* you're familiar with the libraries generated by soroban-cli's `contract
* bindings typescript` command, these also wraps `ContractClient` and return
* bindings typescript` command, these also wraps `Client` and return
* `AssembledTransaction` instances.
*
* Let's look at examples of how to use `AssembledTransaction` for a variety of
Expand Down Expand Up @@ -68,7 +70,7 @@ export const NULL_ACCOUNT =
* ```
*
* While that looks pretty complicated, most of the time you will use this in
* conjunction with {@link ContractClient}, which simplifies it to:
* conjunction with {@link Client}, which simplifies it to:
*
* ```ts
* const { result } = await client.myReadMethod({
Expand All @@ -92,7 +94,7 @@ export const NULL_ACCOUNT =
* const sentTx = await assembledTx.signAndSend()
* ```
*
* Here we're assuming that you're using a {@link ContractClient}, rather than
* Here we're assuming that you're using a {@link Client}, rather than
* constructing `AssembledTransaction`'s directly.
*
* Note that `sentTx`, the return value of `signAndSend`, is a
Expand All @@ -116,7 +118,7 @@ export const NULL_ACCOUNT =
*
* If you need more control over the transaction before simulating it, you can
* set various {@link MethodOptions} when constructing your
* `AssembledTransaction`. With a {@link ContractClient}, this is passed as a
* `AssembledTransaction`. With a {@link Client}, this is passed as a
* second object after the arguments (or the only object, if the method takes
* no arguments):
*
Expand Down Expand Up @@ -223,7 +225,7 @@ export const NULL_ACCOUNT =
* ```
*
* Under the hood, this uses `signAuthEntry`, which you either need to inject
* during initial construction of the `ContractClient`/`AssembledTransaction`,
* during initial construction of the `Client`/`AssembledTransaction`,
* or which you can pass directly to `signAuthEntries`.
*
* Now Bob can again serialize the transaction and send back to Alice, where
Expand Down Expand Up @@ -264,7 +266,7 @@ export class AssembledTransaction<T> {
* cached, serializable access to the data needed by AssembledTransaction
* logic.
*/
public simulation?: SorobanRpc.Api.SimulateTransactionResponse;
public simulation?: Api.SimulateTransactionResponse;

/**
* Cached simulation result. This is set after the first call to
Expand All @@ -277,7 +279,7 @@ export class AssembledTransaction<T> {
* If you need access to this data after a transaction has been serialized
* and then deserialized, you can call `simulationData.result`.
*/
private simulationResult?: SorobanRpc.Api.SimulateHostFunctionResult;
private simulationResult?: Api.SimulateHostFunctionResult;

/**
* Cached simulation transaction data. This is set after the first call to
Expand All @@ -296,21 +298,21 @@ export class AssembledTransaction<T> {
* The Soroban server to use for all RPC calls. This is constructed from the
* `rpcUrl` in the options.
*/
private server: SorobanRpc.Server;
private server: Server;

/**
* A list of the most important errors that various AssembledTransaction
* methods can throw. Feel free to catch specific errors in your application
* logic.
*/
static Errors = {
ExpiredState: class ExpiredStateError extends Error {},
NeedsMoreSignatures: class NeedsMoreSignaturesError extends Error {},
NoSignatureNeeded: class NoSignatureNeededError extends Error {},
NoUnsignedNonInvokerAuthEntries: class NoUnsignedNonInvokerAuthEntriesError extends Error {},
NoSigner: class NoSignerError extends Error {},
NotYetSimulated: class NotYetSimulatedError extends Error {},
FakeAccount: class FakeAccountError extends Error {},
ExpiredState: class ExpiredStateError extends Error { },
NeedsMoreSignatures: class NeedsMoreSignaturesError extends Error { },
NoSignatureNeeded: class NoSignatureNeededError extends Error { },
NoUnsignedNonInvokerAuthEntries: class NoUnsignedNonInvokerAuthEntriesError extends Error { },
NoSigner: class NoSignerError extends Error { },
NotYetSimulated: class NotYetSimulatedError extends Error { },
FakeAccount: class FakeAccountError extends Error { },
};

/**
Expand Down Expand Up @@ -364,7 +366,7 @@ export class AssembledTransaction<T> {

private constructor(public options: AssembledTransactionOptions<T>) {
this.options.simulate = this.options.simulate ?? true;
this.server = new SorobanRpc.Server(this.options.rpcUrl, {
this.server = new Server(this.options.rpcUrl, {
allowHttp: this.options.allowHttp ?? false,
});
}
Expand Down Expand Up @@ -413,15 +415,15 @@ export class AssembledTransaction<T> {
if (!this.raw) {
throw new Error(
"Transaction has not yet been assembled; " +
"call `AssembledTransaction.build` first.",
"call `AssembledTransaction.build` first.",
);
}

this.built = this.raw.build();
this.simulation = await this.server.simulateTransaction(this.built);

if (SorobanRpc.Api.isSimulationSuccess(this.simulation)) {
this.built = SorobanRpc.assembleTransaction(
if (Api.isSimulationSuccess(this.simulation)) {
this.built = assembleTransaction(
this.built,
this.simulation,
).build();
Expand All @@ -431,7 +433,7 @@ export class AssembledTransaction<T> {
};

get simulationData(): {
result: SorobanRpc.Api.SimulateHostFunctionResult;
result: Api.SimulateHostFunctionResult;
transactionData: xdr.SorobanTransactionData;
} {
if (this.simulationResult && this.simulationTransactionData) {
Expand All @@ -446,11 +448,11 @@ export class AssembledTransaction<T> {
"Transaction has not yet been simulated",
);
}
if (SorobanRpc.Api.isSimulationError(simulation)) {
if (Api.isSimulationError(simulation)) {
throw new Error(`Transaction simulation failed: "${simulation.error}"`);
}

if (SorobanRpc.Api.isSimulationRestore(simulation)) {
if (Api.isSimulationRestore(simulation)) {
throw new AssembledTransaction.Errors.ExpiredState(
`You need to restore some contract state before you can invoke this method. ${JSON.stringify(
simulation,
Expand Down Expand Up @@ -513,13 +515,13 @@ export class AssembledTransaction<T> {
signTransaction = this.options.signTransaction,
}: {
/**
* TSDoc: If `true`, sign and send the transaction even if it is a read call
* If `true`, sign and send the transaction even if it is a read call
*/
force?: boolean;
/**
* TSDoc: You must provide this here if you did not provide one before
* You must provide this here if you did not provide one before
*/
signTransaction?: ContractClientOptions["signTransaction"];
signTransaction?: ClientOptions["signTransaction"];
} = {}): Promise<SentTransaction<T>> => {
if (!this.built) {
throw new Error("Transaction has not yet been simulated");
Expand All @@ -528,21 +530,21 @@ export class AssembledTransaction<T> {
if (!force && this.isReadCall) {
throw new AssembledTransaction.Errors.NoSignatureNeeded(
"This is a read call. It requires no signature or sending. " +
"Use `force: true` to sign and send anyway.",
"Use `force: true` to sign and send anyway.",
);
}

if (!signTransaction) {
throw new AssembledTransaction.Errors.NoSigner(
"You must provide a signTransaction function, either when calling " +
"`signAndSend` or when initializing your ContractClient",
"`signAndSend` or when initializing your Client",
);
}

if (this.needsNonInvokerSigningBy().length) {
throw new AssembledTransaction.Errors.NeedsMoreSignatures(
"Transaction requires more signatures. " +
"See `needsNonInvokerSigningBy` for details.",
"See `needsNonInvokerSigningBy` for details.",
);
}

Expand Down Expand Up @@ -615,10 +617,10 @@ export class AssembledTransaction<T> {
.filter(
(entry) =>
entry.credentials().switch() ===
xdr.SorobanCredentialsType.sorobanCredentialsAddress() &&
xdr.SorobanCredentialsType.sorobanCredentialsAddress() &&
(includeAlreadySigned ||
entry.credentials().address().signature().switch().name ===
"scvVoid"),
"scvVoid"),
)
.map((entry) =>
StrKey.encodeEd25519PublicKey(
Expand Down Expand Up @@ -664,10 +666,10 @@ export class AssembledTransaction<T> {
publicKey?: string;
/**
* You must provide this here if you did not provide one before. Default:
* the `signAuthEntry` function from the `ContractClient` options. Must
* the `signAuthEntry` function from the `Client` options. Must
* sign things as the given `publicKey`.
*/
signAuthEntry?: ContractClientOptions["signAuthEntry"];
signAuthEntry?: ClientOptions["signAuthEntry"];
} = {}): Promise<void> => {
if (!this.built)
throw new Error("Transaction has not yet been assembled or simulated");
Expand All @@ -686,7 +688,7 @@ export class AssembledTransaction<T> {
if (!signAuthEntry) {
throw new AssembledTransaction.Errors.NoSigner(
"You must provide `signAuthEntry` when calling `signAuthEntries`, " +
"or when constructing the `ContractClient` or `AssembledTransaction`",
"or when constructing the `Client` or `AssembledTransaction`",
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Keypair, TransactionBuilder, hash } from "..";
import { Keypair, TransactionBuilder, hash } from "@stellar/stellar-base";
import type { AssembledTransaction } from "./assembled_transaction";
import type { ContractClient } from "./client";
import type { Client } from "./client";

/**
* For use with {@link ContractClient} and {@link AssembledTransaction}.
* For use with {@link Client} and {@link AssembledTransaction}.
* Implements `signTransaction` and `signAuthEntry` with signatures expected by
* those classes. This is useful for testing and maybe some simple Node
* applications. Feel free to use this as a starting point for your own
Expand Down

0 comments on commit 7883dc3

Please sign in to comment.