Skip to content

Commit

Permalink
Add generation of contract clients and an AssembledTransaction abst…
Browse files Browse the repository at this point in the history
…raction (#891)

* feat: spec.generateContractClient; AssembledTx

- new e2e tests copied from cli `ts-tests` for the generated bindings, but
  with TypeScript removed because the ContractClient is generated here
  dynamically at run time, so we cannot know the types at compile time in
  the tests.
- generate JSON specs from local .wasm files during initiaze.sh instead
  of generating TS bindings. As explained in the new wasms/specs/README,
  this is a bummer, but is temporary

* chore: rollback test/contract_spec changes

ContractClient is now fully tested in `tests/e2e` rather than in this
file

Keeping this in a separate commit in case we decide it's better to go
back and do things "The tests/unit way" instead of the new tests/e2e
way. It feels maybe silly to have both.
  • Loading branch information
chadoh committed Jan 5, 2024
1 parent f1358a5 commit 4069314
Show file tree
Hide file tree
Showing 28 changed files with 2,158 additions and 64 deletions.
129 changes: 129 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# paths = ["/path/to/override"] # path dependency overrides

[alias] # command aliases
install_soroban = "install --version 20.1.1 --root ./target soroban-cli --debug"
# b = "build --target wasm32-unknown-unknown --release"
# c = "check"
# t = "test"
# r = "run"
# rr = "run --release"
# recursive_example = "rr --example recursions"
# space_example = ["run", "--release", "--", "\"command list\""]

[build]
# jobs = 1 # number of parallel jobs, defaults to # of CPUs
# rustc = "rustc" # the rust compiler tool
# rustc-wrapper = "…" # run this wrapper instead of `rustc`
# rustc-workspace-wrapper = "…" # run this wrapper instead of `rustc` for workspace members
# rustdoc = "rustdoc" # the doc generator tool
# target = "wasm32-unknown-unknown" # build for the target triple (ignored by `cargo install`)
# target-dir = "target" # path of where to place all generated artifacts
# rustdocflags = ["…", "…"] # custom flags to pass to rustdoc
# incremental = true # whether or not to enable incremental compilation
# dep-info-basedir = "…" # path for the base directory for targets in depfiles

# [doc]
# browser = "chromium" # browser to use with `cargo doc --open`,
# # overrides the `BROWSER` environment variable

# [env]
# # Set ENV_VAR_NAME=value for any process run by Cargo
# ENV_VAR_NAME = "value"
# # Set even if already present in environment
# ENV_VAR_NAME_2 = { value = "value", force = true }
# # Value is relative to .cargo directory containing `config.toml`, make absolute
# ENV_VAR_NAME_3 = { value = "relative/path", relative = true }

# [future-incompat-report]
# frequency = 'always' # when to display a notification about a future incompat report

# [cargo-new]
# vcs = "none" # VCS to use ('git', 'hg', 'pijul', 'fossil', 'none')

# [http]
# debug = false # HTTP debugging
# proxy = "host:port" # HTTP proxy in libcurl format
# ssl-version = "tlsv1.3" # TLS version to use
# ssl-version.max = "tlsv1.3" # maximum TLS version
# ssl-version.min = "tlsv1.1" # minimum TLS version
# timeout = 30 # timeout for each HTTP request, in seconds
# low-speed-limit = 10 # network timeout threshold (bytes/sec)
# cainfo = "cert.pem" # path to Certificate Authority (CA) bundle
# check-revoke = true # check for SSL certificate revocation
# multiplexing = true # HTTP/2 multiplexing
# user-agent = "…" # the user-agent header

# [install]
# root = "/some/path" # `cargo install` destination directory

# [net]
# retry = 2 # network retries
# git-fetch-with-cli = true # use the `git` executable for git operations
# offline = true # do not access the network

# [net.ssh]
# known-hosts = ["..."] # known SSH host keys

# [patch.<registry>]
# # Same keys as for [patch] in Cargo.toml

# [profile.<name>] # Modify profile settings via config.
# inherits = "dev" # Inherits settings from [profile.dev].
# opt-level = 0 # Optimization level.
# debug = true # Include debug info.
# split-debuginfo = '...' # Debug info splitting behavior.
# debug-assertions = true # Enables debug assertions.
# overflow-checks = true # Enables runtime integer overflow checks.
# lto = false # Sets link-time optimization.
# panic = 'unwind' # The panic strategy.
# incremental = true # Incremental compilation.
# codegen-units = 16 # Number of code generation units.
# rpath = false # Sets the rpath linking option.
# [profile.<name>.build-override] # Overrides build-script settings.
# # Same keys for a normal profile.
# [profile.<name>.package.<name>] # Override profile for a package.
# # Same keys for a normal profile (minus `panic`, `lto`, and `rpath`).

# [registries.<name>] # registries other than crates.io
# index = "…" # URL of the registry index
# token = "…" # authentication token for the registry

# [registry]
# default = "…" # name of the default registry
# token = "…" # authentication token for crates.io

# [source.<name>] # source definition and replacement
# replace-with = "…" # replace this source with the given named source
# directory = "…" # path to a directory source
# registry = "…" # URL to a registry source
# local-registry = "…" # path to a local registry source
# git = "…" # URL of a git repository source
# branch = "…" # branch name for the git repository
# tag = "…" # tag name for the git repository
# rev = "…" # revision for the git repository

# [target.<triple>]
# linker = "…" # linker to use
# runner = "…" # wrapper to run executables
# rustflags = ["…", "…"] # custom flags for `rustc`

# [target.<cfg>]
# runner = "…" # wrapper to run executables
# rustflags = ["…", "…"] # custom flags for `rustc`

# [target.<triple>.<links>] # `links` build script override
# rustc-link-lib = ["foo"]
# rustc-link-search = ["/path/to/foo"]
# rustc-flags = ["-L", "/some/path"]
# rustc-cfg = ['key="value"']
# rustc-env = {key = "value"}
# rustc-cdylib-link-arg = ["…"]
# metadata_key1 = "value"
# metadata_key2 = "value"

# [term]
# quiet = false # whether cargo output is quiet
# verbose = false # whether cargo provides verbose output
# color = 'auto' # whether cargo colorizes output
# progress.when = 'auto' # whether cargo shows progress bar
# progress.width = 80 # width of progress bar
2 changes: 2 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
SOROBAN_NETWORK_PASSPHRASE="Standalone Network ; February 2017"
SOROBAN_RPC_URL="http://localhost:8000/soroban/rpc"
39 changes: 39 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: ContractClient

on:
push:
branches: [master, release/**]
pull_request:

jobs:
test:
name: test generated ContractClient
runs-on: ubuntu-22.04
services:
rpc:
image: stellar/quickstart:soroban-dev@sha256:0ad51035cf7caba2fd99c7c1fad0945df6932be7d5c893e1520ccdef7d6a6ffe
ports:
- 8000:8000
env:
ENABLE_LOGS: true
NETWORK: local
ENABLE_SOROBAN_RPC: true
options: >-
--health-cmd "curl --no-progress-meter --fail-with-body -X POST \"http://localhost:8000/soroban/rpc\" -H 'Content-Type: application/json' -d '{\"jsonrpc\":\"2.0\",\"id\":8675309,\"method\":\"getNetwork\"}' && curl --no-progress-meter \"http://localhost:8000/friendbot\" | grep '\"invalid_field\": \"addr\"'"
--health-interval 10s
--health-timeout 5s
--health-retries 50
steps:
- uses: actions/checkout@v3
- uses: actions/cache@v3
with:
path: |
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

# Workaround for some `yarn` nonsense, see:
# https://github.com/yarnpkg/yarn/issues/6312#issuecomment-429685210
- run: yarn install --network-concurrency 1
- run: yarn build:prod
- run: yarn test:e2e

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ js-stellar-base/

test/unit/out/
.vscode/launch.json

target
.soroban
16 changes: 13 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@
"build:prod": "cross-env NODE_ENV=production yarn _build",
"build:node": "yarn _babel && yarn build:ts",
"build:ts": "tsc -p ./config/tsconfig.json",
"build:test": "tsc -p ./test/tsconfig.json",
"build:test": "tsc -p ./test/unit/tsconfig.json",
"build:browser": "webpack -c config/webpack.config.browser.js",
"build:docs": "cross-env NODE_ENV=docs yarn _babel",
"clean": "rm -rf lib/ dist/ coverage/ .nyc_output/ jsdoc/",
"clean": "rm -rf lib/ dist/ coverage/ .nyc_output/ jsdoc/ test/e2e/.soroban test/e2e/contract-*.txt test/e2e/wasms/specs/*.json",
"docs": "yarn build:docs && jsdoc -c ./config/.jsdoc.json --verbose",
"test": "yarn build:test && yarn test:node && yarn test:integration && yarn test:browser",
"test:e2e": "./test/e2e/initialize.sh && ava",
"test:node": "yarn _nyc mocha --recursive 'test/unit/**/*.js'",
"test:integration": "yarn _nyc mocha --recursive 'test/integration/**/*.js'",
"test:browser": "karma start config/karma.conf.js",
Expand Down Expand Up @@ -76,6 +77,8 @@
]
},
"devDependencies": {
"ava": "^5.3.1",
"dotenv": "^16.3.1",
"@babel/cli": "^7.23.0",
"@babel/core": "^7.23.6",
"@babel/eslint-parser": "^7.22.15",
Expand All @@ -96,7 +99,6 @@
"@types/randombytes": "^2.0.1",
"@types/sinon": "^17.0.2",
"@types/urijs": "^1.19.20",
"@typescript-eslint/parser": "^6.14.0",
"axios-mock-adapter": "^1.22.0",
"babel-loader": "^9.1.3",
"babel-plugin-istanbul": "^6.1.1",
Expand Down Expand Up @@ -151,5 +153,13 @@
"randombytes": "^2.1.0",
"toml": "^3.0.0",
"urijs": "^1.19.1"
},
"ava": {
"files": [
"./test/e2e/src/test-*"
],
"require": [
"dotenv/config"
]
}
}
84 changes: 83 additions & 1 deletion src/contract_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import {
Contract,
scValToBigInt,
} from ".";
import {
AssembledTransaction,
ContractClient,
ContractClientOptions,
MethodOptions,
} from './soroban';

export interface Union<T> {
tag: string;
Expand Down Expand Up @@ -163,7 +169,9 @@ export class ContractSpec {
}
let output = outputs[0];
if (output.switch().value === xdr.ScSpecType.scSpecTypeResult().value) {
return this.scValToNative(val, output.result().okType());
return new AssembledTransaction.Result.Ok(
this.scValToNative(val, output.result().okType())
);
}
return this.scValToNative(val, output);
}
Expand Down Expand Up @@ -678,6 +686,60 @@ export class ContractSpec {
return num;
}

/**
* Gets the XDR error cases from the spec.
*
* @returns {xdr.ScSpecFunctionV0[]} all contract functions
*
*/
errorCases(): xdr.ScSpecUdtErrorEnumCaseV0[] {
return this.entries
.filter(
(entry) =>
entry.switch().value ===
xdr.ScSpecEntryKind.scSpecEntryUdtErrorEnumV0().value
)
.flatMap((entry) => (entry.value() as xdr.ScSpecUdtErrorEnumV0).cases());
}

/**
* Generate a class from the contract spec that where each contract method gets included with a possibly-JSified name.
*
* Each method returns an AssembledTransaction object that can be used to sign and submit the transaction.
*/
generateContractClient(options: ContractClientOptions): ContractClient {
const spec = this;
let methods = this.funcs();
const contractClient = new ContractClient(spec, options);
for (let method of methods) {
let name = method.name().toString();
let jsName = toLowerCamelCase(name);
// @ts-ignore
contractClient[jsName] = async (
args: Record<string, any>,
options: MethodOptions
) => {
return await AssembledTransaction.fromSimulation({
method: name,
args: spec.funcArgsToScVals(name, args),
...options,
...contractClient.options,
errorTypes: spec
.errorCases()
.reduce(
(acc, curr) => ({
...acc,
[curr.value()]: { message: curr.doc().toString() },
}),
{} as Pick<ContractClientOptions, "errorTypes">
),
parseResultXdr: (result: xdr.ScVal) => spec.funcResToNative(name, result),
});
};
}
return contractClient;
}

/**
* Converts the contract spec to a JSON schema.
*
Expand Down Expand Up @@ -1138,3 +1200,23 @@ function enumToJsonSchema(udt: xdr.ScSpecUdtEnumV0): any {
}
return res;
}

/**
* converts a snake_case string to camelCase
*/
export function toLowerCamelCase(str: string): string {
return str.replace(/_\w/g, (m) => m[1].toUpperCase());
}

export type u32 = number;
export type i32 = number;
export type u64 = bigint;
export type i64 = bigint;
export type u128 = bigint;
export type i128 = bigint;
export type u256 = bigint;
export type i256 = bigint;
export type Option<T> = T | undefined;
export type Typepoint = bigint;
export type Duration = bigint;

2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export * as Horizon from './horizon';

// Soroban RPC-related classes to expose
export * as SorobanRpc from './soroban';
export { ContractSpec } from './contract_spec';
export * from './contract_spec';

// expose classes and functions from stellar-base
export * from '@stellar/stellar-base';
Expand Down
Loading

0 comments on commit 4069314

Please sign in to comment.