Skip to content

Commit

Permalink
build: move ContractClient & AssembledTransaction
Browse files Browse the repository at this point in the history
These are a bit higher-level and experimental, at this point, so let's
not clutter the global API or the bundle size unless people really want
it.
  • Loading branch information
chadoh committed Mar 5, 2024
1 parent 561eee3 commit 922dfbb
Show file tree
Hide file tree
Showing 7 changed files with 127 additions and 111 deletions.
Original file line number Diff line number Diff line change
@@ -1,56 +1,29 @@
import type {
ContractClientOptions,
XDR_BASE64,
} from ".";
} from "./contract_client";
import {
Account,
BASE_FEE,
Contract,
ContractSpec,
Memo,
MemoType,
Operation,
SorobanRpc,
StrKey,
Transaction,
TransactionBuilder,
authorizeEntry,
hash,
xdr,
} from "..";
import { Memo, MemoType, Transaction } from "..";
} from ".";

type Tx = Transaction<Memo<MemoType>, Operation[]>;

type SendTx = SorobanRpc.Api.SendTransactionResponse;
type GetTx = SorobanRpc.Api.GetTransactionResponse;

/**
* Error interface containing the error message
*/
interface ErrorMessage {
message: string;
}

interface Result<T, E extends ErrorMessage> {
unwrap(): T;
unwrapErr(): E;
isOk(): boolean;
isErr(): boolean;
}

class Ok<T> implements Result<T, never> {
constructor(readonly value: T) {}
unwrapErr(): never { throw new Error("No error") }
unwrap() { return this.value }
isOk() { return true }
isErr() { return false }
}

class Err<E extends ErrorMessage> implements Result<never, E> {
constructor(readonly error: E) {}
unwrapErr() { return this.error }
unwrap(): never { throw new Error(this.error.message) }
isOk() { return false }
isErr() { return true }
}

export const NULL_ACCOUNT =
"GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";

Expand Down Expand Up @@ -143,21 +116,10 @@ export class AssembledTransaction<T> {
}

/**
* A minimal implementation of Rust's `Result` type. Used for contract methods that return Results, to maintain their distinction from methods that simply either return a value or throw.
*/
static Result = {
/**
* A minimal implementation of Rust's `Ok` Result type. Used for contract methods that return successful Results, to maintain their distinction from methods that simply either return a value or throw.
*/
Ok,
/**
* A minimal implementation of Rust's `Error` Result type. Used for contract methods that return unsuccessful Results, to maintain their distinction from methods that simply either return a value or throw.
*/
Err
}

/**
* Serialize the AssembledTransaction to a JSON string. This is useful for saving the transaction to a database or sending it over the wire for multi-auth workflows. `fromJSON` can be used to deserialize the transaction. This only works with transactions that have been simulated.
* Serialize the AssembledTransaction to a JSON string. This is useful for
* saving the transaction to a database or sending it over the wire for
* multi-auth workflows. `fromJSON` can be used to deserialize the
* transaction. This only works with transactions that have been simulated.
*/
toJSON() {
return JSON.stringify({
Expand Down Expand Up @@ -312,14 +274,15 @@ export class AssembledTransaction<T> {
}
}

parseError(errorMessage: string): Result<never, ErrorMessage> | undefined {
parseError(errorMessage: string) {
if (!this.options.errorTypes) return undefined;
const match = errorMessage.match(contractErrorPattern);
if (!match) return undefined;
let i = parseInt(match[1], 10);
let err = this.options.errorTypes[i];
if (!err) return undefined;
return new AssembledTransaction.Result.Err(err);
const Err = ContractSpec.Result.Err;
return new Err(err);
}

/**
Expand Down
47 changes: 45 additions & 2 deletions src/soroban/contract_client.ts → src/contract_client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AssembledTransaction } from '.'
import { ContractSpec, xdr } from '..'
import { AssembledTransaction, MethodOptions } from './assembled_transaction'
import { ContractSpec, xdr } from '.'

export type XDR_BASE64 = string;

Expand Down Expand Up @@ -54,7 +54,50 @@ export type ContractClientOptions = {
errorTypes?: Record<number, { message: string }>;
};

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

export class ContractClient {
/**
* 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.
*/
static generate(spec: ContractSpec, options: ContractClientOptions): ContractClient {
let methods = spec.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.build({
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;
}
constructor(
public readonly spec: ContractSpec,
public readonly options: ContractClientOptions,
Expand Down
114 changes: 62 additions & 52 deletions src/contract_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@ import {
Contract,
scValToBigInt,
} from ".";
import {
AssembledTransaction,
ContractClient,
ContractClientOptions,
MethodOptions,
} from './soroban';

export interface Union<T> {
tag: string;
Expand All @@ -28,6 +22,48 @@ function readObj(args: object, input: xdr.ScSpecFunctionInputV0): any {
return entry[1];
}

/**
* A minimal implementation of Rust's `Result` type. Used for contract
* methods that return Results, to maintain their distinction from methods
* that simply either return a value or throw.
*/
interface Result<T, E extends ErrorMessage> {
unwrap(): T;
unwrapErr(): E;
isOk(): boolean;
isErr(): boolean;
}

/**
* Error interface containing the error message. Matches Rust's implementation.
* See reasoning in {@link Result}.
*/
interface ErrorMessage {
message: string;
}

/**
* Part of implementing {@link Result}.
*/
class Ok<T> implements Result<T, never> {
constructor(readonly value: T) {}
unwrapErr(): never { throw new Error("No error") }
unwrap() { return this.value }
isOk() { return true }
isErr() { return false }
}

/**
* Part of implementing {@link Result}.
*/
class Err<E extends ErrorMessage> implements Result<never, E> {
constructor(readonly error: E) {}
unwrapErr() { return this.error }
unwrap(): never { throw new Error(this.error.message) }
isOk() { return false }
isErr() { return true }
}

/**
* Provides a ContractSpec class which can contains the XDR types defined by the contract.
* This allows the class to be used to convert between native and raw `xdr.ScVal`s.
Expand All @@ -54,6 +90,25 @@ function readObj(args: object, input: xdr.ScSpecFunctionInputV0): any {
* ```
*/
export class ContractSpec {
/**
* A minimal implementation of Rust's `Result` type. Used for contract
* methods that return Results, to maintain their distinction from methods
* that simply either return a value or throw.
*/
static Result = {
/**
* A minimal implementation of Rust's `Ok` Result type. Used for contract
* methods that return successful Results, to maintain their distinction
* from methods that simply either return a value or throw.
*/
Ok,
/**
* A minimal implementation of Rust's `Error` Result type. Used for
* contract methods that return unsuccessful Results, to maintain their
* distinction from methods that simply either return a value or throw.
*/
Err
}
public entries: xdr.ScSpecEntry[] = [];

/**
Expand Down Expand Up @@ -169,7 +224,7 @@ export class ContractSpec {
}
let output = outputs[0];
if (output.switch().value === xdr.ScSpecType.scSpecTypeResult().value) {
return new AssembledTransaction.Result.Ok(
return new ContractSpec.Result.Ok(
this.scValToNative(val, output.result().okType())
);
}
Expand Down Expand Up @@ -702,44 +757,6 @@ export class ContractSpec {
.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.build({
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 @@ -1200,13 +1217,6 @@ 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;
Expand Down
2 changes: 0 additions & 2 deletions src/soroban/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,5 @@ export { Server, Durability } from './server';
export { default as AxiosClient } from './axios';
export { parseRawSimulation, parseRawEvents } from './parsers';
export * from './transaction';
export * from './contract_client';
export * from './assembled_transaction';

export default module.exports;
6 changes: 3 additions & 3 deletions test/e2e/src/test-custom-types.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const test = require('ava')
const { Address, SorobanRpc } = require('../../..')
const { Address, ContractSpec } = require('../../..')
const { clientFor } = require('./util')

test.before(async t => {
Expand All @@ -21,11 +21,11 @@ test('woid', async t => {
test('u32_fail_on_even', async t => {
t.deepEqual(
(await t.context.client.u32FailOnEven({ u32_: 1 })).result,
new SorobanRpc.AssembledTransaction.Result.Ok(1)
new ContractSpec.Result.Ok(1)
)
t.deepEqual(
(await t.context.client.u32FailOnEven({ u32_: 2 })).result,
new SorobanRpc.AssembledTransaction.Result.Err({ message: "Please provide an odd number" })
new ContractSpec.Result.Err({ message: "Please provide an odd number" })
)
})

Expand Down
3 changes: 2 additions & 1 deletion test/e2e/src/test-swap.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const test = require('ava')
const { SorobanRpc } = require('../../..')
const { AssembledTransaction } = require('../../../lib/assembled_transaction')
const { clientFor, generateFundedKeypair } = require('./util')

const amountAToSwap = 2n
Expand Down Expand Up @@ -51,7 +52,7 @@ test('calling `signAndSend()` too soon throws descriptive error', async t => {
min_b_for_a: amountBToSwap,
})
const error = await t.throwsAsync(tx.signAndSend())
t.true(error instanceof SorobanRpc.AssembledTransaction.Errors.NeedsMoreSignatures, `error is not of type 'NeedsMoreSignaturesError'; instead it is of type '${error?.constructor.name}'`)
t.true(error instanceof AssembledTransaction.Errors.NeedsMoreSignatures, `error is not of type 'NeedsMoreSignaturesError'; instead it is of type '${error?.constructor.name}'`)
if (error) t.regex(error.message, /needsNonInvokerSigningBy/)
})

Expand Down
3 changes: 2 additions & 1 deletion test/e2e/src/util.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const { spawnSync } = require('node:child_process')
const { ContractSpec, Keypair } = require('../../..')
const { ContractClient } = require('../../../lib/contract_client')
const { ExampleNodeWallet } = require('../../../lib/example_node_wallet')

const contracts = {
Expand Down Expand Up @@ -73,7 +74,7 @@ async function clientFor(contract, { keypair = generateFundedKeypair(), contract
wasmHash,
], { shell: true, encoding: "utf8" }).stdout.trim();

const client = spec.generateContractClient({
const client = ContractClient.generate(spec, {
networkPassphrase,
contractId,
rpcUrl,
Expand Down

0 comments on commit 922dfbb

Please sign in to comment.