From 922dfbb91c96cfde1614951c19805cac93082f6a Mon Sep 17 00:00:00 2001 From: Chad Ostrowski <221614+chadoh@users.noreply.github.com> Date: Tue, 5 Mar 2024 15:53:12 -0500 Subject: [PATCH] build: move ContractClient & AssembledTransaction 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. --- src/{soroban => }/assembled_transaction.ts | 63 +++--------- src/{soroban => }/contract_client.ts | 47 ++++++++- src/contract_spec.ts | 114 +++++++++++---------- src/soroban/index.ts | 2 - test/e2e/src/test-custom-types.js | 6 +- test/e2e/src/test-swap.js | 3 +- test/e2e/src/util.js | 3 +- 7 files changed, 127 insertions(+), 111 deletions(-) rename src/{soroban => }/assembled_transaction.ts (93%) rename src/{soroban => }/contract_client.ts (59%) diff --git a/src/soroban/assembled_transaction.ts b/src/assembled_transaction.ts similarity index 93% rename from src/soroban/assembled_transaction.ts rename to src/assembled_transaction.ts index 2e1e633b8..6663b6231 100644 --- a/src/soroban/assembled_transaction.ts +++ b/src/assembled_transaction.ts @@ -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, Operation[]>; type SendTx = SorobanRpc.Api.SendTransactionResponse; type GetTx = SorobanRpc.Api.GetTransactionResponse; -/** - * Error interface containing the error message - */ -interface ErrorMessage { - message: string; -} - -interface Result { - unwrap(): T; - unwrapErr(): E; - isOk(): boolean; - isErr(): boolean; -} - -class Ok implements Result { - constructor(readonly value: T) {} - unwrapErr(): never { throw new Error("No error") } - unwrap() { return this.value } - isOk() { return true } - isErr() { return false } -} - -class Err implements Result { - 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"; @@ -143,21 +116,10 @@ export class AssembledTransaction { } /** - * 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({ @@ -312,14 +274,15 @@ export class AssembledTransaction { } } - parseError(errorMessage: string): Result | 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); } /** diff --git a/src/soroban/contract_client.ts b/src/contract_client.ts similarity index 59% rename from src/soroban/contract_client.ts rename to src/contract_client.ts index 420358fbf..896a1c887 100644 --- a/src/soroban/contract_client.ts +++ b/src/contract_client.ts @@ -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; @@ -54,7 +54,50 @@ export type ContractClientOptions = { errorTypes?: Record; }; +/** + * 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, + 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 + ), + parseResultXdr: (result: xdr.ScVal) => spec.funcResToNative(name, result), + }); + }; + } + return contractClient; + } constructor( public readonly spec: ContractSpec, public readonly options: ContractClientOptions, diff --git a/src/contract_spec.ts b/src/contract_spec.ts index 91829de87..85036eefb 100644 --- a/src/contract_spec.ts +++ b/src/contract_spec.ts @@ -7,12 +7,6 @@ import { Contract, scValToBigInt, } from "."; -import { - AssembledTransaction, - ContractClient, - ContractClientOptions, - MethodOptions, -} from './soroban'; export interface Union { tag: string; @@ -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 { + 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 implements Result { + 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 implements Result { + 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. @@ -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[] = []; /** @@ -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()) ); } @@ -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, - 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 - ), - parseResultXdr: (result: xdr.ScVal) => spec.funcResToNative(name, result), - }); - }; - } - return contractClient; - } - /** * Converts the contract spec to a JSON schema. * @@ -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; diff --git a/src/soroban/index.ts b/src/soroban/index.ts index c269a61a3..36e296ee1 100644 --- a/src/soroban/index.ts +++ b/src/soroban/index.ts @@ -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; diff --git a/test/e2e/src/test-custom-types.js b/test/e2e/src/test-custom-types.js index c810f4469..1bdff913c 100644 --- a/test/e2e/src/test-custom-types.js +++ b/test/e2e/src/test-custom-types.js @@ -1,5 +1,5 @@ const test = require('ava') -const { Address, SorobanRpc } = require('../../..') +const { Address, ContractSpec } = require('../../..') const { clientFor } = require('./util') test.before(async t => { @@ -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" }) ) }) diff --git a/test/e2e/src/test-swap.js b/test/e2e/src/test-swap.js index 967bd6883..780c78658 100644 --- a/test/e2e/src/test-swap.js +++ b/test/e2e/src/test-swap.js @@ -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 @@ -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/) }) diff --git a/test/e2e/src/util.js b/test/e2e/src/util.js index a03a2fd7f..844cb61ae 100644 --- a/test/e2e/src/util.js +++ b/test/e2e/src/util.js @@ -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 = { @@ -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,