From b8430dd79392d5b08ee3d92daa0a90e4ac2d55e8 Mon Sep 17 00:00:00 2001 From: Blaine Heffron Date: Tue, 25 Jun 2024 16:46:22 -0400 Subject: [PATCH] Running an absolute clinic on appeasing the ES linter (#990) --- .eslintrc.js | 43 + src/.eslintrc.js | 41 - src/browser.ts | 5 +- src/config.ts | 4 +- src/contract/assembled_transaction.ts | 7 +- src/contract/index.ts | 3 +- src/contract/sent_transaction.ts | 3 +- src/contract/spec.ts | 1629 +++++++++-------- src/contract/types.ts | 7 +- src/contract/utils.ts | 13 +- src/errors.ts | 5 + src/federation/server.ts | 5 +- src/horizon/account_call_builder.ts | 4 +- src/horizon/account_response.ts | 3 +- src/horizon/assets_call_builder.ts | 4 +- src/horizon/call_builder.ts | 36 +- .../claimable_balances_call_builder.ts | 6 +- src/horizon/effect_call_builder.ts | 4 +- src/horizon/horizon_axios_client.ts | 15 +- src/horizon/ledger_call_builder.ts | 4 +- src/horizon/liquidity_pool_call_builder.ts | 5 +- src/horizon/offer_call_builder.ts | 4 +- src/horizon/operation_call_builder.ts | 6 +- src/horizon/path_call_builder.ts | 2 +- src/horizon/payment_call_builder.ts | 4 +- src/horizon/server.ts | 50 +- src/horizon/server_api.ts | 1 + .../strict_receive_path_call_builder.ts | 2 +- src/horizon/strict_send_path_call_builder.ts | 2 +- src/horizon/trade_aggregation_call_builder.ts | 25 +- src/horizon/trades_call_builder.ts | 4 +- src/horizon/transaction_call_builder.ts | 6 +- src/horizon/types/assets.ts | 2 +- src/horizon/types/effects.ts | 2 +- src/horizon/types/offer.ts | 2 +- src/rpc/api.ts | 11 +- src/rpc/axios.ts | 4 +- src/rpc/browser.ts | 6 +- src/rpc/jsonrpc.ts | 21 +- src/rpc/parsers.ts | 104 +- src/rpc/server.ts | 104 +- src/rpc/transaction.ts | 80 +- src/rpc/utils.ts | 1 + src/stellartoml/index.ts | 1 + src/webauth/errors.ts | 2 +- .../src/test-contract-client-constructor.js | 13 +- test/e2e/src/test-custom-types.js | 174 +- test/e2e/src/test-hello-world.js | 10 +- test/e2e/src/test-methods-as-args.js | 4 +- test/e2e/src/test-swap.js | 139 +- .../soroban/assembled_transaction_test.js | 102 +- .../soroban/simulate_transaction_test.js | 119 +- 52 files changed, 1514 insertions(+), 1339 deletions(-) delete mode 100644 src/.eslintrc.js diff --git a/.eslintrc.js b/.eslintrc.js index 16fdb5cab..4fcd5f068 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,11 +4,54 @@ module.exports = { }, extends: [ "airbnb-base", + "airbnb-typescript/base", "prettier", "plugin:jsdoc/recommended", ], + parserOptions: { + parser: "@typescript-eslint/parser", + project: "./config/tsconfig.json", + }, plugins: ["@babel", "prettier", "prefer-import"], rules: { + // OFF "node/no-unpublished-require": 0, + "import/prefer-default-export": 0, + "node/no-unsupported-features/es-syntax": 0, + "node/no-unsupported-features/es-builtins": 0, + camelcase: 0, + "class-methods-use-this": 0, + "linebreak-style": 0, + "jsdoc/require-returns": 0, + "jsdoc/require-param": 0, + "jsdoc/require-param-type": 0, + "jsdoc/require-returns-type": 0, + "jsdoc/no-blank-blocks": 0, + "jsdoc/no-multi-asterisks": 0, + "jsdoc/tag-lines": "off", + "jsdoc/require-jsdoc": "off", + "jsdoc/no-defaults": "off", + "valid-jsdoc": "off", + "import/extensions": 0, + "new-cap": 0, + "no-param-reassign": 0, + "no-underscore-dangle": 0, + "no-use-before-define": 0, + "prefer-destructuring": 0, + "lines-between-class-members": 0, + "@typescript-eslint/lines-between-class-members": "off", + "spaced-comment": 0, + + // WARN + "arrow-body-style": 1, + "no-console": ["warn", { allow: ["assert"] }], + "no-debugger": 1, + "object-shorthand": 1, + "prefer-const": 1, + "prefer-import/prefer-import-over-require": [1], + "require-await": 1, + + // ERROR + "no-unused-expressions": [2, { allowTaggedTemplates: true }], }, }; diff --git a/src/.eslintrc.js b/src/.eslintrc.js deleted file mode 100644 index 97b3cc934..000000000 --- a/src/.eslintrc.js +++ /dev/null @@ -1,41 +0,0 @@ -module.exports = { - extends: [ - "airbnb-base", - "airbnb-typescript/base", - "prettier", - "plugin:jsdoc/recommended", - ], - parserOptions: { - parser: "@typescript-eslint/parser", - project: "./config/tsconfig.json", - }, - rules: { - // OFF - "import/prefer-default-export": 0, - "node/no-unsupported-features/es-syntax": 0, - "node/no-unsupported-features/es-builtins": 0, - camelcase: 0, - "class-methods-use-this": 0, - "linebreak-style": 0, - "jsdoc/require-returns": 0, - "jsdoc/require-param": 0, - "new-cap": 0, - "no-param-reassign": 0, - "no-underscore-dangle": 0, - "no-use-before-define": 0, - "prefer-destructuring": 0, - "lines-between-class-members": 0, - - // WARN - "arrow-body-style": 1, - "no-console": ["warn", { allow: ["assert"] }], - "no-debugger": 1, - "object-shorthand": 1, - "prefer-const": 1, - "prefer-import/prefer-import-over-require": [1], - "require-await": 1, - - // ERROR - "no-unused-expressions": [2, { allowTaggedTemplates: true }], - }, -}; diff --git a/src/browser.ts b/src/browser.ts index 6befec18d..2f6034b93 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -1,9 +1,10 @@ /* tslint:disable:no-var-requires */ +/* eslint import/no-import-module-exports: 0 */ + +import axios from "axios"; // idk why axios is weird export * from "./index"; export * as StellarBase from "@stellar/stellar-base"; - -import axios from "axios"; // idk why axios is weird export { axios }; export default module.exports; diff --git a/src/config.ts b/src/config.ts index 946c793d8..4528a0a8a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -18,7 +18,7 @@ const defaultConfig: Configuration = { timeout: 0, }; -let config = Object.assign({}, defaultConfig); +let config = { ...defaultConfig}; /** * Global config class. @@ -82,7 +82,7 @@ class Config { * @returns {void} */ public static setDefault(): void { - config = Object.assign({}, defaultConfig); + config = { ...defaultConfig}; } } diff --git a/src/contract/assembled_transaction.ts b/src/contract/assembled_transaction.ts index e87de33b7..d038051aa 100644 --- a/src/contract/assembled_transaction.ts +++ b/src/contract/assembled_transaction.ts @@ -24,17 +24,14 @@ import { assembleTransaction } from "../rpc/transaction"; import type { Client } from "./client"; import { Err } from "./rust_result"; import { - DEFAULT_TIMEOUT, contractErrorPattern, implementsToString, getAccount } from "./utils"; +import { DEFAULT_TIMEOUT } from "./types"; import { SentTransaction } from "./sent_transaction"; import { Spec } from "./spec"; -export const NULL_ACCOUNT = - "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"; - /** * The main workhorse of {@link Client}. This class is used to wrap a * transaction-under-construction and provide high-level interfaces to the most @@ -909,7 +906,7 @@ export class AssembledTransaction { * Client initialization. * @throws {AssembledTransaction.Errors.RestoreFailure} - Throws a custom error if the * restore transaction fails, providing the details of the failure. - */ + */ async restoreFootprint( /** * The preamble object containing data required to diff --git a/src/contract/index.ts b/src/contract/index.ts index 657bde8cf..4675aacd5 100644 --- a/src/contract/index.ts +++ b/src/contract/index.ts @@ -4,5 +4,4 @@ export * from "./client"; export * from "./rust_result"; export * from "./sent_transaction"; export * from "./spec"; -export * from "./types"; -export { DEFAULT_TIMEOUT } from "./utils"; +export * from "./types"; \ No newline at end of file diff --git a/src/contract/sent_transaction.ts b/src/contract/sent_transaction.ts index 0a449ab3e..0014fa393 100644 --- a/src/contract/sent_transaction.ts +++ b/src/contract/sent_transaction.ts @@ -3,7 +3,8 @@ import type { MethodOptions } from "./types"; import { Server } from "../rpc/server" import { Api } from "../rpc/api" -import { DEFAULT_TIMEOUT, withExponentialBackoff } from "./utils"; +import { withExponentialBackoff } from "./utils"; +import { DEFAULT_TIMEOUT } from "./types"; import type { AssembledTransaction } from "./assembled_transaction"; /** diff --git a/src/contract/spec.ts b/src/contract/spec.ts index 937875fbb..863fbac1d 100644 --- a/src/contract/spec.ts +++ b/src/contract/spec.ts @@ -14,245 +14,659 @@ export interface Union { values?: T; } +function enumToJsonSchema(udt: xdr.ScSpecUdtEnumV0): any { + const description = udt.doc().toString(); + const cases = udt.cases(); + const oneOf: any[] = []; + cases.forEach((aCase) => { + const title = aCase.name().toString(); + const desc = aCase.doc().toString(); + oneOf.push({ + description: desc, + title, + enum: [aCase.value()], + type: "number", + }); + }); + const res: any = { oneOf }; + if (description.length > 0) { + res.description = description; + } + return res; +} + +function isNumeric(field: xdr.ScSpecUdtStructFieldV0) { + return /^\d+$/.test(field.name().toString()); +} + function readObj(args: object, input: xdr.ScSpecFunctionInputV0): any { - let inputName = input.name().toString(); - let entry = Object.entries(args).find(([name, _]) => name === inputName); + const inputName = input.name().toString(); + const entry = Object.entries(args).find(([name]) => name === inputName); if (!entry) { throw new Error(`Missing field ${inputName}`); } return entry[1]; } -/** - * 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. - * - * @example - * ```js - * const specEntries = [...]; // XDR spec entries of a smart contract - * const contractSpec = new ContractSpec(specEntries); - * - * // Convert native value to ScVal - * const args = { - * arg1: 'value1', - * arg2: 1234 - * }; - * const scArgs = contractSpec.funcArgsToScVals('funcName', args); - * - * // Call contract - * const resultScv = await callContract(contractId, 'funcName', scArgs); - * - * // Convert result ScVal back to native value - * const result = contractSpec.funcResToNative('funcName', resultScv); - * - * console.log(result); // {success: true} - * ``` - */ -export class Spec { - public entries: xdr.ScSpecEntry[] = []; - - /** - * Constructs a new ContractSpec from an array of XDR spec entries. - * - * @param {xdr.ScSpecEntry[] | string[]} entries the XDR spec entries - * - * @throws {Error} if entries is invalid - */ - constructor(entries: xdr.ScSpecEntry[] | string[]) { - if (entries.length == 0) { - throw new Error("Contract spec must have at least one entry"); - } - let entry = entries[0]; - if (typeof entry === "string") { - this.entries = (entries as string[]).map((s) => - xdr.ScSpecEntry.fromXDR(s, "base64"), - ); - } else { - this.entries = entries as xdr.ScSpecEntry[]; +function findCase(name: string) { + return function matches(entry: xdr.ScSpecUdtUnionCaseV0) { + switch (entry.switch().value) { + case xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseTupleV0().value: { + const tuple = entry.tupleCase(); + return tuple.name().toString() === name; + } + case xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseVoidV0().value: { + const voidCase = entry.voidCase(); + return voidCase.name().toString() === name; + } + default: + return false; } - } + }; +} - /** - * Gets the XDR functions from the spec. - * - * @returns {xdr.ScSpecFunctionV0[]} all contract functions - * - */ - funcs(): xdr.ScSpecFunctionV0[] { - return this.entries - .filter( - (entry) => - entry.switch().value === - xdr.ScSpecEntryKind.scSpecEntryFunctionV0().value, - ) - .map((entry) => entry.functionV0()); - } - /** - * Gets the XDR function spec for the given function name. - * - * @param {string} name the name of the function - * @returns {xdr.ScSpecFunctionV0} the function spec - * - * @throws {Error} if no function with the given name exists - */ - getFunc(name: string): xdr.ScSpecFunctionV0 { - let entry = this.findEntry(name); - if ( - entry.switch().value !== xdr.ScSpecEntryKind.scSpecEntryFunctionV0().value - ) { - throw new Error(`${name} is not a function`); +function stringToScVal(str: string, ty: xdr.ScSpecType): xdr.ScVal { + switch (ty.value) { + case xdr.ScSpecType.scSpecTypeString().value: + return xdr.ScVal.scvString(str); + case xdr.ScSpecType.scSpecTypeSymbol().value: + return xdr.ScVal.scvSymbol(str); + case xdr.ScSpecType.scSpecTypeAddress().value: { + const addr = Address.fromString(str as string); + return xdr.ScVal.scvAddress(addr.toScAddress()); } - return entry.functionV0(); - } + case xdr.ScSpecType.scSpecTypeU64().value: + return new XdrLargeInt("u64", str).toScVal(); + case xdr.ScSpecType.scSpecTypeI64().value: + return new XdrLargeInt("i64", str).toScVal(); + case xdr.ScSpecType.scSpecTypeU128().value: + return new XdrLargeInt("u128", str).toScVal(); + case xdr.ScSpecType.scSpecTypeI128().value: + return new XdrLargeInt("i128", str).toScVal(); + case xdr.ScSpecType.scSpecTypeU256().value: + return new XdrLargeInt("u256", str).toScVal(); + case xdr.ScSpecType.scSpecTypeI256().value: + return new XdrLargeInt("i256", str).toScVal(); + case xdr.ScSpecType.scSpecTypeBytes().value: + case xdr.ScSpecType.scSpecTypeBytesN().value: + return xdr.ScVal.scvBytes(Buffer.from(str, "base64")); - /** - * Converts native JS arguments to ScVals for calling a contract function. - * - * @param {string} name the name of the function - * @param {Object} args the arguments object - * @returns {xdr.ScVal[]} the converted arguments - * - * @throws {Error} if argument is missing or incorrect type - * - * @example - * ```js - * const args = { - * arg1: 'value1', - * arg2: 1234 - * }; - * const scArgs = contractSpec.funcArgsToScVals('funcName', args); - * ``` - */ - funcArgsToScVals(name: string, args: object): xdr.ScVal[] { - let fn = this.getFunc(name); - return fn - .inputs() - .map((input) => this.nativeToScVal(readObj(args, input), input.type())); + default: + throw new TypeError(`invalid type ${ty.name} specified for string value`); } +} - /** - * Converts the result ScVal of a function call to a native JS value. - * - * @param {string} name the name of the function - * @param {xdr.ScVal | string} val_or_base64 the result ScVal or base64 encoded string - * @returns {any} the converted native value - * - * @throws {Error} if return type mismatch or invalid input - * - * @example - * ```js - * const resultScv = 'AAA=='; // Base64 encoded ScVal - * const result = contractSpec.funcResToNative('funcName', resultScv); - * ``` - */ - funcResToNative(name: string, val_or_base64: xdr.ScVal | string): any { - let val = - typeof val_or_base64 === "string" - ? xdr.ScVal.fromXDR(val_or_base64, "base64") - : val_or_base64; - let func = this.getFunc(name); - let outputs = func.outputs(); - if (outputs.length === 0) { - let type = val.switch(); - if (type.value !== xdr.ScValType.scvVoid().value) { - throw new Error(`Expected void, got ${type.name}`); - } - return null; +const PRIMITIVE_DEFINITONS: { [key: string]: JSONSchema7Definition } = { + U32: { + type: "integer", + minimum: 0, + maximum: 4294967295, + }, + I32: { + type: "integer", + minimum: -2147483648, + maximum: 2147483647, + }, + U64: { + type: "string", + pattern: "^([1-9][0-9]*|0)$", + minLength: 1, + maxLength: 20, // 64-bit max value has 20 digits + }, + I64: { + type: "string", + pattern: "^(-?[1-9][0-9]*|0)$", + minLength: 1, + maxLength: 21, // Includes additional digit for the potential '-' + }, + U128: { + type: "string", + pattern: "^([1-9][0-9]*|0)$", + minLength: 1, + maxLength: 39, // 128-bit max value has 39 digits + }, + I128: { + type: "string", + pattern: "^(-?[1-9][0-9]*|0)$", + minLength: 1, + maxLength: 40, // Includes additional digit for the potential '-' + }, + U256: { + type: "string", + pattern: "^([1-9][0-9]*|0)$", + minLength: 1, + maxLength: 78, // 256-bit max value has 78 digits + }, + I256: { + type: "string", + pattern: "^(-?[1-9][0-9]*|0)$", + minLength: 1, + maxLength: 79, // Includes additional digit for the potential '-' + }, + Address: { + type: "string", + format: "address", + description: "Address can be a public key or contract id", + }, + ScString: { + type: "string", + description: "ScString is a string", + }, + ScSymbol: { + type: "string", + description: "ScString is a string", + }, + DataUrl: { + type: "string", + pattern: + "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=)?$", + }, +}; + +/* eslint-disable default-case */ +/** + * @param typeDef type to convert to json schema reference + * @returns {JSONSchema7} a schema describing the type + */ +function typeRef(typeDef: xdr.ScSpecTypeDef): JSONSchema7 { + const t = typeDef.switch(); + const value = t.value; + let ref; + switch (value) { + case xdr.ScSpecType.scSpecTypeVal().value: { + ref = "Val"; + break; } - if (outputs.length > 1) { - throw new Error(`Multiple outputs not supported`); + case xdr.ScSpecType.scSpecTypeBool().value: { + return { type: "boolean" }; } - let output = outputs[0]; - if (output.switch().value === xdr.ScSpecType.scSpecTypeResult().value) { - return new Ok(this.scValToNative(val, output.result().okType())); + case xdr.ScSpecType.scSpecTypeVoid().value: { + return { type: "null" }; + } + case xdr.ScSpecType.scSpecTypeError().value: { + ref = "Error"; + break; + } + case xdr.ScSpecType.scSpecTypeU32().value: { + ref = "U32"; + break; + } + case xdr.ScSpecType.scSpecTypeI32().value: { + ref = "I32"; + break; + } + case xdr.ScSpecType.scSpecTypeU64().value: { + ref = "U64"; + break; + } + case xdr.ScSpecType.scSpecTypeI64().value: { + ref = "I64"; + break; + } + case xdr.ScSpecType.scSpecTypeTimepoint().value: { + throw new Error("Timepoint type not supported"); + ref = "Timepoint"; + break; + } + case xdr.ScSpecType.scSpecTypeDuration().value: { + throw new Error("Duration not supported"); + ref = "Duration"; + break; + } + case xdr.ScSpecType.scSpecTypeU128().value: { + ref = "U128"; + break; + } + case xdr.ScSpecType.scSpecTypeI128().value: { + ref = "I128"; + break; + } + case xdr.ScSpecType.scSpecTypeU256().value: { + ref = "U256"; + break; + } + case xdr.ScSpecType.scSpecTypeI256().value: { + ref = "I256"; + break; + } + case xdr.ScSpecType.scSpecTypeBytes().value: { + ref = "DataUrl"; + break; + } + case xdr.ScSpecType.scSpecTypeString().value: { + ref = "ScString"; + break; + } + case xdr.ScSpecType.scSpecTypeSymbol().value: { + ref = "ScSymbol"; + break; + } + case xdr.ScSpecType.scSpecTypeAddress().value: { + ref = "Address"; + break; + } + case xdr.ScSpecType.scSpecTypeOption().value: { + const opt = typeDef.option(); + return typeRef(opt.valueType()); + } + case xdr.ScSpecType.scSpecTypeResult().value: { + // throw new Error('Result type not supported'); + break; + } + case xdr.ScSpecType.scSpecTypeVec().value: { + const arr = typeDef.vec(); + const reference = typeRef(arr.elementType()); + return { + type: "array", + items: reference, + }; + } + case xdr.ScSpecType.scSpecTypeMap().value: { + const map = typeDef.map(); + const items = [typeRef(map.keyType()), typeRef(map.valueType())]; + return { + type: "array", + items: { + type: "array", + items, + minItems: 2, + maxItems: 2, + }, + }; + } + case xdr.ScSpecType.scSpecTypeTuple().value: { + const tuple = typeDef.tuple(); + const minItems = tuple.valueTypes().length; + const maxItems = minItems; + const items = tuple.valueTypes().map(typeRef); + return { type: "array", items, minItems, maxItems }; + } + case xdr.ScSpecType.scSpecTypeBytesN().value: { + const arr = typeDef.bytesN(); + return { + $ref: "#/definitions/DataUrl", + maxLength: arr.n(), + }; + } + case xdr.ScSpecType.scSpecTypeUdt().value: { + const udt = typeDef.udt(); + ref = udt.name().toString(); + break; } - return this.scValToNative(val, output); } + return { $ref: `#/definitions/${ref}` }; +} +/* eslint-enable default-case */ - /** - * Finds the XDR spec entry for the given name. - * - * @param {string} name the name to find - * @returns {xdr.ScSpecEntry} the entry - * - * @throws {Error} if no entry with the given name exists - */ - findEntry(name: string): xdr.ScSpecEntry { - let entry = this.entries.find( - (entry) => entry.value().name().toString() === name, - ); - if (!entry) { - throw new Error(`no such entry: ${name}`); +type Func = { input: JSONSchema7; output: JSONSchema7 }; + +function isRequired(typeDef: xdr.ScSpecTypeDef): boolean { + return typeDef.switch().value !== xdr.ScSpecType.scSpecTypeOption().value; +} + +function argsAndRequired( + input: { type: () => xdr.ScSpecTypeDef; name: () => string | Buffer }[], +): { properties: object; required?: string[] } { + const properties: any = {}; + const required: string[] = []; + input.forEach((arg) => { + const aType = arg.type(); + const name = arg.name().toString(); + properties[name] = typeRef(aType); + if (isRequired(aType)) { + required.push(name); } - return entry; + }); + const res: { properties: object; required?: string[] } = { properties }; + if (required.length > 0) { + res.required = required; } + return res; +} - /** - * Converts a native JS value to an ScVal based on the given type. - * - * @param {any} val the native JS value - * @param {xdr.ScSpecTypeDef} [ty] the expected type - * @returns {xdr.ScVal} the converted ScVal - * - * @throws {Error} if value cannot be converted to the given type - */ - nativeToScVal(val: any, ty: xdr.ScSpecTypeDef): xdr.ScVal { - let t: xdr.ScSpecType = ty.switch(); - let value = t.value; - if (t.value === xdr.ScSpecType.scSpecTypeUdt().value) { - let udt = ty.udt(); - return this.nativeToUdt(val, udt.name().toString()); +function structToJsonSchema(udt: xdr.ScSpecUdtStructV0): object { + const fields = udt.fields(); + if (fields.some(isNumeric)) { + if (!fields.every(isNumeric)) { + throw new Error( + "mixed numeric and non-numeric field names are not allowed", + ); } - if (value === xdr.ScSpecType.scSpecTypeOption().value) { - let opt = ty.option(); - if (val === undefined) { - return xdr.ScVal.scvVoid(); + const items = fields.map((_, i) => typeRef(fields[i].type())); + return { + type: "array", + items, + minItems: fields.length, + maxItems: fields.length, + }; + } + const description = udt.doc().toString(); + const { properties, required }: any = argsAndRequired(fields); + properties.additionalProperties = false; + return { + description, + properties, + required, + type: "object", + }; +} + +function functionToJsonSchema(func: xdr.ScSpecFunctionV0): Func { + const { properties, required }: any = argsAndRequired(func.inputs()); + const args: any = { + additionalProperties: false, + properties, + type: "object", + }; + if (required?.length > 0) { + args.required = required; + } + const input: Partial = { + properties: { + args, + }, + }; + const outputs = func.outputs(); + const output: Partial = + outputs.length > 0 + ? typeRef(outputs[0]) + : typeRef(xdr.ScSpecTypeDef.scSpecTypeVoid()); + const description = func.doc().toString(); + if (description.length > 0) { + input.description = description; + } + input.additionalProperties = false; + output.additionalProperties = false; + return { + input, + output, + }; +} + +/* eslint-disable default-case */ +function unionToJsonSchema(udt: xdr.ScSpecUdtUnionV0): any { + const description = udt.doc().toString(); + const cases = udt.cases(); + const oneOf: any[] = []; + cases.forEach((aCase) => { + switch (aCase.switch().value) { + case xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseVoidV0().value: { + const c = aCase.voidCase(); + const title = c.name().toString(); + oneOf.push({ + type: "object", + title, + properties: { + tag: title, + }, + additionalProperties: false, + required: ["tag"], + }); + break; + } + case xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseTupleV0().value: { + const c = aCase.tupleCase(); + const title = c.name().toString(); + oneOf.push({ + type: "object", + title, + properties: { + tag: title, + values: { + type: "array", + items: c.type().map(typeRef), + }, + }, + required: ["tag", "values"], + additionalProperties: false, + }); } - return this.nativeToScVal(val, opt.valueType()); } - switch (typeof val) { - case "object": { - if (val === null) { - switch (value) { - case xdr.ScSpecType.scSpecTypeVoid().value: - return xdr.ScVal.scvVoid(); - default: - throw new TypeError( - `Type ${ty} was not void, but value was null`, - ); - } - } + }); - if (val instanceof xdr.ScVal) { - return val; // should we copy? - } + const res: any = { + oneOf, + }; + if (description.length > 0) { + res.description = description; + } + return res; +} +/* eslint-enable default-case */ - if (val instanceof Address) { - if (ty.switch().value !== xdr.ScSpecType.scSpecTypeAddress().value) { - throw new TypeError( - `Type ${ty} was not address, but value was Address`, - ); - } - return val.toScVal(); - } - if (val instanceof Contract) { - if (ty.switch().value !== xdr.ScSpecType.scSpecTypeAddress().value) { - throw new TypeError( - `Type ${ty} was not address, but value was Address`, - ); - } - return val.address().toScVal(); - } +/** + * 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. + * + * @example + * ```js + * const specEntries = [...]; // XDR spec entries of a smart contract + * const contractSpec = new ContractSpec(specEntries); + * + * // Convert native value to ScVal + * const args = { + * arg1: 'value1', + * arg2: 1234 + * }; + * const scArgs = contractSpec.funcArgsToScVals('funcName', args); + * + * // Call contract + * const resultScv = await callContract(contractId, 'funcName', scArgs); + * + * // Convert result ScVal back to native value + * const result = contractSpec.funcResToNative('funcName', resultScv); + * + * console.log(result); // {success: true} + * ``` + */ +export class Spec { + public entries: xdr.ScSpecEntry[] = []; + + /** + * Constructs a new ContractSpec from an array of XDR spec entries. + * + * @param {xdr.ScSpecEntry[] | string[]} entries the XDR spec entries + * + * @throws {Error} if entries is invalid + */ + constructor(entries: xdr.ScSpecEntry[] | string[]) { + if (entries.length === 0) { + throw new Error("Contract spec must have at least one entry"); + } + const entry = entries[0]; + if (typeof entry === "string") { + this.entries = (entries as string[]).map((s) => + xdr.ScSpecEntry.fromXDR(s, "base64"), + ); + } else { + this.entries = entries as xdr.ScSpecEntry[]; + } + } + + /** + * Gets the XDR functions from the spec. + * + * @returns {xdr.ScSpecFunctionV0[]} all contract functions + * + */ + funcs(): xdr.ScSpecFunctionV0[] { + return this.entries + .filter( + (entry) => + entry.switch().value === + xdr.ScSpecEntryKind.scSpecEntryFunctionV0().value, + ) + .map((entry) => entry.functionV0()); + } + + /** + * Gets the XDR function spec for the given function name. + * + * @param {string} name the name of the function + * @returns {xdr.ScSpecFunctionV0} the function spec + * + * @throws {Error} if no function with the given name exists + */ + getFunc(name: string): xdr.ScSpecFunctionV0 { + const entry = this.findEntry(name); + if ( + entry.switch().value !== xdr.ScSpecEntryKind.scSpecEntryFunctionV0().value + ) { + throw new Error(`${name} is not a function`); + } + return entry.functionV0(); + } + + /** + * Converts native JS arguments to ScVals for calling a contract function. + * + * @param {string} name the name of the function + * @param {object} args the arguments object + * @returns {xdr.ScVal[]} the converted arguments + * + * @throws {Error} if argument is missing or incorrect type + * + * @example + * ```js + * const args = { + * arg1: 'value1', + * arg2: 1234 + * }; + * const scArgs = contractSpec.funcArgsToScVals('funcName', args); + * ``` + */ + funcArgsToScVals(name: string, args: object): xdr.ScVal[] { + const fn = this.getFunc(name); + return fn + .inputs() + .map((input) => this.nativeToScVal(readObj(args, input), input.type())); + } + + /** + * Converts the result ScVal of a function call to a native JS value. + * + * @param {string} name the name of the function + * @param {xdr.ScVal | string} val_or_base64 the result ScVal or base64 encoded string + * @returns {any} the converted native value + * + * @throws {Error} if return type mismatch or invalid input + * + * @example + * ```js + * const resultScv = 'AAA=='; // Base64 encoded ScVal + * const result = contractSpec.funcResToNative('funcName', resultScv); + * ``` + */ + funcResToNative(name: string, val_or_base64: xdr.ScVal | string): any { + const val = + typeof val_or_base64 === "string" + ? xdr.ScVal.fromXDR(val_or_base64, "base64") + : val_or_base64; + const func = this.getFunc(name); + const outputs = func.outputs(); + if (outputs.length === 0) { + const type = val.switch(); + if (type.value !== xdr.ScValType.scvVoid().value) { + throw new Error(`Expected void, got ${type.name}`); + } + return null; + } + if (outputs.length > 1) { + throw new Error(`Multiple outputs not supported`); + } + const output = outputs[0]; + if (output.switch().value === xdr.ScSpecType.scSpecTypeResult().value) { + return new Ok(this.scValToNative(val, output.result().okType())); + } + return this.scValToNative(val, output); + } + + /** + * Finds the XDR spec entry for the given name. + * + * @param {string} name the name to find + * @returns {xdr.ScSpecEntry} the entry + * + * @throws {Error} if no entry with the given name exists + */ + findEntry(name: string): xdr.ScSpecEntry { + const entry = this.entries.find( + (e) => e.value().name().toString() === name, + ); + if (!entry) { + throw new Error(`no such entry: ${name}`); + } + return entry; + } + + + /** + * Converts a native JS value to an ScVal based on the given type. + * + * @param {any} val the native JS value + * @param {xdr.ScSpecTypeDef} [ty] the expected type + * @returns {xdr.ScVal} the converted ScVal + * + * @throws {Error} if value cannot be converted to the given type + */ + nativeToScVal(val: any, ty: xdr.ScSpecTypeDef): xdr.ScVal { + const t: xdr.ScSpecType = ty.switch(); + const value = t.value; + if (t.value === xdr.ScSpecType.scSpecTypeUdt().value) { + const udt = ty.udt(); + return this.nativeToUdt(val, udt.name().toString()); + } + if (value === xdr.ScSpecType.scSpecTypeOption().value) { + const opt = ty.option(); + if (val === undefined) { + return xdr.ScVal.scvVoid(); + } + return this.nativeToScVal(val, opt.valueType()); + } + switch (typeof val) { + case "object": { + if (val === null) { + switch (value) { + case xdr.ScSpecType.scSpecTypeVoid().value: + return xdr.ScVal.scvVoid(); + default: + throw new TypeError( + `Type ${ty} was not void, but value was null`, + ); + } + } + + if (val instanceof xdr.ScVal) { + return val; // should we copy? + } + + if (val instanceof Address) { + if (ty.switch().value !== xdr.ScSpecType.scSpecTypeAddress().value) { + throw new TypeError( + `Type ${ty} was not address, but value was Address`, + ); + } + return val.toScVal(); + } + + if (val instanceof Contract) { + if (ty.switch().value !== xdr.ScSpecType.scSpecTypeAddress().value) { + throw new TypeError( + `Type ${ty} was not address, but value was Address`, + ); + } + return val.address().toScVal(); + } if (val instanceof Uint8Array || Buffer.isBuffer(val)) { const copy = Uint8Array.from(val); switch (value) { case xdr.ScSpecType.scSpecTypeBytesN().value: { - let bytes_n = ty.bytesN(); - if (copy.length !== bytes_n.n()) { + const bytesN = ty.bytesN(); + if (copy.length !== bytesN.n()) { throw new TypeError( - `expected ${bytes_n.n()} bytes, but got ${copy.length}`, + `expected ${bytesN.n()} bytes, but got ${copy.length}`, ); } //@ts-ignore @@ -270,15 +684,15 @@ export class Spec { if (Array.isArray(val)) { switch (value) { case xdr.ScSpecType.scSpecTypeVec().value: { - let vec = ty.vec(); - let elementType = vec.elementType(); + const vec = ty.vec(); + const elementType = vec.elementType(); return xdr.ScVal.scvVec( val.map((v) => this.nativeToScVal(v, elementType)), ); } case xdr.ScSpecType.scSpecTypeTuple().value: { - let tup = ty.tuple(); - let valTypes = tup.valueTypes(); + const tup = ty.tuple(); + const valTypes = tup.valueTypes(); if (val.length !== valTypes.length) { throw new TypeError( `Tuple expects ${valTypes.length} values, but ${val.length} were provided`, @@ -289,14 +703,14 @@ export class Spec { ); } case xdr.ScSpecType.scSpecTypeMap().value: { - let map = ty.map(); - let keyType = map.keyType(); - let valueType = map.valueType(); + const map = ty.map(); + const keyType = map.keyType(); + const valueType = map.valueType(); return xdr.ScVal.scvMap( val.map((entry) => { - let key = this.nativeToScVal(entry[0], keyType); - let val = this.nativeToScVal(entry[1], valueType); - return new xdr.ScMapEntry({ key, val }); + const key = this.nativeToScVal(entry[0], keyType); + const mapVal = this.nativeToScVal(entry[1], valueType); + return new xdr.ScMapEntry({ key, val: mapVal }); }), ); } @@ -311,16 +725,16 @@ export class Spec { if (value !== xdr.ScSpecType.scSpecTypeMap().value) { throw new TypeError(`Type ${ty} was not map, but value was Map`); } - let scMap = ty.map(); - let map = val as Map; - let entries: xdr.ScMapEntry[] = []; - let values = map.entries(); + const scMap = ty.map(); + const map = val as Map; + const entries: xdr.ScMapEntry[] = []; + const values = map.entries(); let res = values.next(); while (!res.done) { - let [k, v] = res.value; - let key = this.nativeToScVal(k, scMap.keyType()); - let val = this.nativeToScVal(v, scMap.valueType()); - entries.push(new xdr.ScMapEntry({ key, val })); + const [k, v] = res.value; + const key = this.nativeToScVal(k, scMap.keyType()); + const mapval = this.nativeToScVal(v, scMap.valueType()); + entries.push(new xdr.ScMapEntry({ key, val: mapval })); res = values.next(); } return xdr.ScVal.scvMap(entries); @@ -391,7 +805,7 @@ export class Spec { } private nativeToUdt(val: any, name: string): xdr.ScVal { - let entry = this.findEntry(name); + const entry = this.findEntry(name); switch (entry.switch()) { case xdr.ScSpecEntryKind.scSpecEntryUdtEnumV0(): if (typeof val !== "number") { @@ -413,34 +827,34 @@ export class Spec { val: Union, union_: xdr.ScSpecUdtUnionV0, ): xdr.ScVal { - let entry_name = val.tag; - let case_ = union_.cases().find((entry) => { - let case_ = entry.value().name().toString(); - return case_ === entry_name; + const entryName = val.tag; + const caseFound = union_.cases().find((entry) => { + const caseN = entry.value().name().toString(); + return caseN === entryName; }); - if (!case_) { - throw new TypeError(`no such enum entry: ${entry_name} in ${union_}`); + if (!caseFound) { + throw new TypeError(`no such enum entry: ${entryName} in ${union_}`); } - let key = xdr.ScVal.scvSymbol(entry_name); - switch (case_.switch()) { + const key = xdr.ScVal.scvSymbol(entryName); + switch (caseFound.switch()) { case xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseVoidV0(): { return xdr.ScVal.scvVec([key]); } case xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseTupleV0(): { - let types = case_.tupleCase().type(); + const types = caseFound.tupleCase().type(); if (Array.isArray(val.values)) { - if (val.values.length != types.length) { + if (val.values.length !== types.length) { throw new TypeError( `union ${union_} expects ${types.length} values, but got ${val.values.length}`, ); } - let scvals = val.values.map((v, i) => + const scvals = val.values.map((v, i) => this.nativeToScVal(v, types[i]), ); scvals.unshift(key); return xdr.ScVal.scvVec(scvals); } - throw new Error(`failed to parse union case ${case_} with ${val}`); + throw new Error(`failed to parse union case ${caseFound} with ${val}`); } default: throw new Error(`failed to parse union ${union_} with ${val}`); @@ -448,7 +862,7 @@ export class Spec { } private nativeToStruct(val: any, struct: xdr.ScSpecUdtStructV0): xdr.ScVal { - let fields = struct.fields(); + const fields = struct.fields(); if (fields.some(isNumeric)) { if (!fields.every(isNumeric)) { throw new Error( @@ -461,7 +875,7 @@ export class Spec { } return xdr.ScVal.scvMap( fields.map((field) => { - let name = field.name().toString(); + const name = field.name().toString(); return new xdr.ScMapEntry({ key: this.nativeToScVal(name, xdr.ScSpecTypeDef.scSpecTypeSymbol()), val: this.nativeToScVal(val[name], field.type()), @@ -500,17 +914,18 @@ export class Spec { * @throws {Error} if ScVal cannot be converted to the given type */ scValToNative(scv: xdr.ScVal, typeDef: xdr.ScSpecTypeDef): T { - let t = typeDef.switch(); - let value = t.value; + const t = typeDef.switch(); + const value = t.value; if (value === xdr.ScSpecType.scSpecTypeUdt().value) { return this.scValUdtToNative(scv, typeDef.udt()); } + /* eslint-disable no-fallthrough*/ // we use the verbose xdr.ScValType..value form here because it's faster // than string comparisons and the underlying constants never need to be // updated switch (scv.switch().value) { case xdr.ScValType.scvVoid().value: - return void 0 as T; + return undefined as T; // these can be converted to bigints directly case xdr.ScValType.scvU64().value: @@ -525,14 +940,14 @@ export class Spec { return scValToBigInt(scv) as T; case xdr.ScValType.scvVec().value: { - if (value == xdr.ScSpecType.scSpecTypeVec().value) { - let vec = typeDef.vec(); + if (value === xdr.ScSpecType.scSpecTypeVec().value) { + const vec = typeDef.vec(); return (scv.vec() ?? []).map((elm) => this.scValToNative(elm, vec.elementType()), ) as T; - } else if (value == xdr.ScSpecType.scSpecTypeTuple().value) { - let tuple = typeDef.tuple(); - let valTypes = tuple.valueTypes(); + } if (value === xdr.ScSpecType.scSpecTypeTuple().value) { + const tuple = typeDef.tuple(); + const valTypes = tuple.valueTypes(); return (scv.vec() ?? []).map((elm, i) => this.scValToNative(elm, valTypes[i]), ) as T; @@ -544,12 +959,12 @@ export class Spec { return Address.fromScVal(scv).toString() as T; case xdr.ScValType.scvMap().value: { - let map = scv.map() ?? []; - if (value == xdr.ScSpecType.scSpecTypeMap().value) { - let type_ = typeDef.map(); - let keyType = type_.keyType(); - let valueType = type_.valueType(); - let res = map.map((entry) => [ + const map = scv.map() ?? []; + if (value === xdr.ScSpecType.scSpecTypeMap().value) { + const typed = typeDef.map(); + const keyType = typed.keyType(); + const valueType = typed.valueType(); + const res = map.map((entry) => [ this.scValToNative(entry.key(), keyType), this.scValToNative(entry.val(), valueType), ]) as T; @@ -575,580 +990,178 @@ export class Spec { case xdr.ScValType.scvSymbol().value: { if ( value !== xdr.ScSpecType.scSpecTypeString().value && - value !== xdr.ScSpecType.scSpecTypeSymbol().value - ) { - throw new Error( - `ScSpecType ${t.name - } was not string or symbol, but ${JSON.stringify(scv, null, 2)} is`, - ); - } - return scv.value()?.toString() as T; - } - - // these can be converted to bigint - case xdr.ScValType.scvTimepoint().value: - case xdr.ScValType.scvDuration().value: - return scValToBigInt(xdr.ScVal.scvU64(scv.u64())) as T; - - // in the fallthrough case, just return the underlying value directly - default: - throw new TypeError( - `failed to convert ${JSON.stringify( - scv, - null, - 2, - )} to native type from type ${t.name}`, - ); - } - } - - private scValUdtToNative(scv: xdr.ScVal, udt: xdr.ScSpecTypeUdt): any { - let entry = this.findEntry(udt.name().toString()); - switch (entry.switch()) { - case xdr.ScSpecEntryKind.scSpecEntryUdtEnumV0(): - return this.enumToNative(scv); - case xdr.ScSpecEntryKind.scSpecEntryUdtStructV0(): - return this.structToNative(scv, entry.udtStructV0()); - case xdr.ScSpecEntryKind.scSpecEntryUdtUnionV0(): - return this.unionToNative(scv, entry.udtUnionV0()); - default: - throw new Error( - `failed to parse udt ${udt.name().toString()}: ${entry}`, - ); - } - } - - private unionToNative(val: xdr.ScVal, udt: xdr.ScSpecUdtUnionV0): any { - let vec = val.vec(); - if (!vec) { - throw new Error(`${JSON.stringify(val, null, 2)} is not a vec`); - } - if (vec.length === 0 && udt.cases.length !== 0) { - throw new Error( - `${val} has length 0, but the there are at least one case in the union`, - ); - } - let name = vec[0].sym().toString(); - if (vec[0].switch().value != xdr.ScValType.scvSymbol().value) { - throw new Error(`{vec[0]} is not a symbol`); - } - let entry = udt.cases().find(findCase(name)); - if (!entry) { - throw new Error( - `failed to find entry ${name} in union {udt.name().toString()}`, - ); - } - let res: Union = { tag: name }; - if ( - entry.switch().value === - xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseTupleV0().value - ) { - let tuple = entry.tupleCase(); - let ty = tuple.type(); - let values = ty.map((entry, i) => this.scValToNative(vec![i + 1], entry)); - res.values = values; - } - return res; - } - private structToNative(val: xdr.ScVal, udt: xdr.ScSpecUdtStructV0): any { - let res: any = {}; - let fields = udt.fields(); - if (fields.some(isNumeric)) { - let r = val - .vec() - ?.map((entry, i) => this.scValToNative(entry, fields[i].type())); - return r; - } - val.map()?.forEach((entry, i) => { - let field = fields[i]; - res[field.name().toString()] = this.scValToNative( - entry.val(), - field.type(), - ); - }); - return res; - } - - private enumToNative(scv: xdr.ScVal): number { - if (scv.switch().value !== xdr.ScValType.scvU32().value) { - throw new Error(`Enum must have a u32 value`); - } - let num = scv.u32(); - 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()); - } - - /** - * Converts the contract spec to a JSON schema. - * - * If `funcName` is provided, the schema will be a reference to the function schema. - * - * @param {string} [funcName] the name of the function to convert - * @returns {JSONSchema7} the converted JSON schema - * - * @throws {Error} if the contract spec is invalid - */ - jsonSchema(funcName?: string): JSONSchema7 { - let definitions: { [key: string]: JSONSchema7Definition } = {}; - for (let entry of this.entries) { - switch (entry.switch().value) { - case xdr.ScSpecEntryKind.scSpecEntryUdtEnumV0().value: { - let udt = entry.udtEnumV0(); - definitions[udt.name().toString()] = enumToJsonSchema(udt); - break; - } - case xdr.ScSpecEntryKind.scSpecEntryUdtStructV0().value: { - let udt = entry.udtStructV0(); - definitions[udt.name().toString()] = structToJsonSchema(udt); - break; - } - case xdr.ScSpecEntryKind.scSpecEntryUdtUnionV0().value: - let udt = entry.udtUnionV0(); - definitions[udt.name().toString()] = unionToJsonSchema(udt); - break; - case xdr.ScSpecEntryKind.scSpecEntryFunctionV0().value: { - let fn = entry.functionV0(); - let fnName = fn.name().toString(); - let { input } = functionToJsonSchema(fn); - // @ts-ignore - definitions[fnName] = input; - break; - } - case xdr.ScSpecEntryKind.scSpecEntryUdtErrorEnumV0().value: { - // console.debug("Error enums not supported yet"); - } - } - } - let res: JSONSchema7 = { - $schema: "http://json-schema.org/draft-07/schema#", - definitions: { ...PRIMITIVE_DEFINITONS, ...definitions }, - }; - if (funcName) { - res["$ref"] = `#/definitions/${funcName}`; - } - return res; - } -} - -function stringToScVal(str: string, ty: xdr.ScSpecType): xdr.ScVal { - switch (ty.value) { - case xdr.ScSpecType.scSpecTypeString().value: - return xdr.ScVal.scvString(str); - case xdr.ScSpecType.scSpecTypeSymbol().value: - return xdr.ScVal.scvSymbol(str); - case xdr.ScSpecType.scSpecTypeAddress().value: { - let addr = Address.fromString(str as string); - return xdr.ScVal.scvAddress(addr.toScAddress()); - } - case xdr.ScSpecType.scSpecTypeU64().value: - return new XdrLargeInt("u64", str).toScVal(); - case xdr.ScSpecType.scSpecTypeI64().value: - return new XdrLargeInt("i64", str).toScVal(); - case xdr.ScSpecType.scSpecTypeU128().value: - return new XdrLargeInt("u128", str).toScVal(); - case xdr.ScSpecType.scSpecTypeI128().value: - return new XdrLargeInt("i128", str).toScVal(); - case xdr.ScSpecType.scSpecTypeU256().value: - return new XdrLargeInt("u256", str).toScVal(); - case xdr.ScSpecType.scSpecTypeI256().value: - return new XdrLargeInt("i256", str).toScVal(); - case xdr.ScSpecType.scSpecTypeBytes().value: - case xdr.ScSpecType.scSpecTypeBytesN().value: - return xdr.ScVal.scvBytes(Buffer.from(str, "base64")); - - default: - throw new TypeError(`invalid type ${ty.name} specified for string value`); - } -} - -function isNumeric(field: xdr.ScSpecUdtStructFieldV0) { - return /^\d+$/.test(field.name().toString()); -} - -function findCase(name: string) { - return function matches(entry: xdr.ScSpecUdtUnionCaseV0) { - switch (entry.switch().value) { - case xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseTupleV0().value: { - let tuple = entry.tupleCase(); - return tuple.name().toString() === name; - } - case xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseVoidV0().value: { - let void_case = entry.voidCase(); - return void_case.name().toString() === name; - } - default: - return false; - } - }; -} - -const PRIMITIVE_DEFINITONS: { [key: string]: JSONSchema7Definition } = { - U32: { - type: "integer", - minimum: 0, - maximum: 4294967295, - }, - I32: { - type: "integer", - minimum: -2147483648, - maximum: 2147483647, - }, - U64: { - type: "string", - pattern: "^([1-9][0-9]*|0)$", - minLength: 1, - maxLength: 20, // 64-bit max value has 20 digits - }, - I64: { - type: "string", - pattern: "^(-?[1-9][0-9]*|0)$", - minLength: 1, - maxLength: 21, // Includes additional digit for the potential '-' - }, - U128: { - type: "string", - pattern: "^([1-9][0-9]*|0)$", - minLength: 1, - maxLength: 39, // 128-bit max value has 39 digits - }, - I128: { - type: "string", - pattern: "^(-?[1-9][0-9]*|0)$", - minLength: 1, - maxLength: 40, // Includes additional digit for the potential '-' - }, - U256: { - type: "string", - pattern: "^([1-9][0-9]*|0)$", - minLength: 1, - maxLength: 78, // 256-bit max value has 78 digits - }, - I256: { - type: "string", - pattern: "^(-?[1-9][0-9]*|0)$", - minLength: 1, - maxLength: 79, // Includes additional digit for the potential '-' - }, - Address: { - type: "string", - format: "address", - description: "Address can be a public key or contract id", - }, - ScString: { - type: "string", - description: "ScString is a string", - }, - ScSymbol: { - type: "string", - description: "ScString is a string", - }, - DataUrl: { - type: "string", - pattern: - "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=)?$", - }, -}; - -/** - * @param typeDef type to convert to json schema reference - * @returns {JSONSchema7} a schema describing the type - */ -function typeRef(typeDef: xdr.ScSpecTypeDef): JSONSchema7 { - let t = typeDef.switch(); - let value = t.value; - let ref; - switch (value) { - case xdr.ScSpecType.scSpecTypeVal().value: { - ref = "Val"; - break; - } - case xdr.ScSpecType.scSpecTypeBool().value: { - return { type: "boolean" }; - } - case xdr.ScSpecType.scSpecTypeVoid().value: { - return { type: "null" }; - } - case xdr.ScSpecType.scSpecTypeError().value: { - ref = "Error"; - break; - } - case xdr.ScSpecType.scSpecTypeU32().value: { - ref = "U32"; - break; - } - case xdr.ScSpecType.scSpecTypeI32().value: { - ref = "I32"; - break; - } - case xdr.ScSpecType.scSpecTypeU64().value: { - ref = "U64"; - break; - } - case xdr.ScSpecType.scSpecTypeI64().value: { - ref = "I64"; - break; - } - case xdr.ScSpecType.scSpecTypeTimepoint().value: { - throw new Error("Timepoint type not supported"); - ref = "Timepoint"; - break; - } - case xdr.ScSpecType.scSpecTypeDuration().value: { - throw new Error("Duration not supported"); - ref = "Duration"; - break; - } - case xdr.ScSpecType.scSpecTypeU128().value: { - ref = "U128"; - break; - } - case xdr.ScSpecType.scSpecTypeI128().value: { - ref = "I128"; - break; - } - case xdr.ScSpecType.scSpecTypeU256().value: { - ref = "U256"; - break; - } - case xdr.ScSpecType.scSpecTypeI256().value: { - ref = "I256"; - break; - } - case xdr.ScSpecType.scSpecTypeBytes().value: { - ref = "DataUrl"; - break; - } - case xdr.ScSpecType.scSpecTypeString().value: { - ref = "ScString"; - break; - } - case xdr.ScSpecType.scSpecTypeSymbol().value: { - ref = "ScSymbol"; - break; - } - case xdr.ScSpecType.scSpecTypeAddress().value: { - ref = "Address"; - break; - } - case xdr.ScSpecType.scSpecTypeOption().value: { - let opt = typeDef.option(); - return typeRef(opt.valueType()); + value !== xdr.ScSpecType.scSpecTypeSymbol().value + ) { + throw new Error( + `ScSpecType ${t.name + } was not string or symbol, but ${JSON.stringify(scv, null, 2)} is`, + ); + } + return scv.value()?.toString() as T; + } + + // these can be converted to bigint + case xdr.ScValType.scvTimepoint().value: + case xdr.ScValType.scvDuration().value: + return scValToBigInt(xdr.ScVal.scvU64(scv.u64())) as T; + + // in the fallthrough case, just return the underlying value directly + default: + throw new TypeError( + `failed to convert ${JSON.stringify( + scv, + null, + 2, + )} to native type from type ${t.name}`, + ); } - case xdr.ScSpecType.scSpecTypeResult().value: { - // throw new Error('Result type not supported'); - break; + /* eslint-enable no-fallthrough*/ + } + + private scValUdtToNative(scv: xdr.ScVal, udt: xdr.ScSpecTypeUdt): any { + const entry = this.findEntry(udt.name().toString()); + switch (entry.switch()) { + case xdr.ScSpecEntryKind.scSpecEntryUdtEnumV0(): + return this.enumToNative(scv); + case xdr.ScSpecEntryKind.scSpecEntryUdtStructV0(): + return this.structToNative(scv, entry.udtStructV0()); + case xdr.ScSpecEntryKind.scSpecEntryUdtUnionV0(): + return this.unionToNative(scv, entry.udtUnionV0()); + default: + throw new Error( + `failed to parse udt ${udt.name().toString()}: ${entry}`, + ); } - case xdr.ScSpecType.scSpecTypeVec().value: { - let arr = typeDef.vec(); - let ref = typeRef(arr.elementType()); - return { - type: "array", - items: ref, - }; + } + + private unionToNative(val: xdr.ScVal, udt: xdr.ScSpecUdtUnionV0): any { + const vec = val.vec(); + if (!vec) { + throw new Error(`${JSON.stringify(val, null, 2)} is not a vec`); } - case xdr.ScSpecType.scSpecTypeMap().value: { - let map = typeDef.map(); - let items = [typeRef(map.keyType()), typeRef(map.valueType())]; - return { - type: "array", - items: { - type: "array", - items, - minItems: 2, - maxItems: 2, - }, - }; + if (vec.length === 0 && udt.cases.length !== 0) { + throw new Error( + `${val} has length 0, but the there are at least one case in the union`, + ); } - case xdr.ScSpecType.scSpecTypeTuple().value: { - let tuple = typeDef.tuple(); - let minItems = tuple.valueTypes().length; - let maxItems = minItems; - let items = tuple.valueTypes().map(typeRef); - return { type: "array", items, minItems, maxItems }; + const name = vec[0].sym().toString(); + if (vec[0].switch().value !== xdr.ScValType.scvSymbol().value) { + throw new Error(`{vec[0]} is not a symbol`); } - case xdr.ScSpecType.scSpecTypeBytesN().value: { - let arr = typeDef.bytesN(); - return { - $ref: "#/definitions/DataUrl", - maxLength: arr.n(), - }; + const entry = udt.cases().find(findCase(name)); + if (!entry) { + throw new Error( + `failed to find entry ${name} in union {udt.name().toString()}`, + ); } - case xdr.ScSpecType.scSpecTypeUdt().value: { - let udt = typeDef.udt(); - ref = udt.name().toString(); - break; + const res: Union = { tag: name }; + if ( + entry.switch().value === + xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseTupleV0().value + ) { + const tuple = entry.tupleCase(); + const ty = tuple.type(); + const values = ty.map((e, i) => this.scValToNative(vec![i + 1], e)); + res.values = values; } + return res; } - return { $ref: `#/definitions/${ref}` }; -} - -type Func = { input: JSONSchema7; output: JSONSchema7 }; - -function isRequired(typeDef: xdr.ScSpecTypeDef): boolean { - return typeDef.switch().value != xdr.ScSpecType.scSpecTypeOption().value; -} -function structToJsonSchema(udt: xdr.ScSpecUdtStructV0): object { - let fields = udt.fields(); - if (fields.some(isNumeric)) { - if (!fields.every(isNumeric)) { - throw new Error( - "mixed numeric and non-numeric field names are not allowed", - ); + private structToNative(val: xdr.ScVal, udt: xdr.ScSpecUdtStructV0): any { + const res: any = {}; + const fields = udt.fields(); + if (fields.some(isNumeric)) { + const r = val + .vec() + ?.map((entry, i) => this.scValToNative(entry, fields[i].type())); + return r; } - let items = fields.map((_, i) => typeRef(fields[i].type())); - return { - type: "array", - items, - minItems: fields.length, - maxItems: fields.length, - }; + val.map()?.forEach((entry, i) => { + const field = fields[i]; + res[field.name().toString()] = this.scValToNative( + entry.val(), + field.type(), + ); + }); + return res; } - let description = udt.doc().toString(); - let { properties, required }: any = args_and_required(fields); - properties["additionalProperties"] = false; - return { - description, - properties, - required, - type: "object", - }; -} -function args_and_required( - input: { type: () => xdr.ScSpecTypeDef; name: () => string | Buffer }[], -): { properties: object; required?: string[] } { - let properties: any = {}; - let required: string[] = []; - for (let arg of input) { - let type_ = arg.type(); - let name = arg.name().toString(); - properties[name] = typeRef(type_); - if (isRequired(type_)) { - required.push(name); + private enumToNative(scv: xdr.ScVal): number { + if (scv.switch().value !== xdr.ScValType.scvU32().value) { + throw new Error(`Enum must have a u32 value`); } + const num = scv.u32(); + return num; } - let res: { properties: object; required?: string[] } = { properties }; - if (required.length > 0) { - res.required = required; - } - return res; -} -function functionToJsonSchema(func: xdr.ScSpecFunctionV0): Func { - let { properties, required }: any = args_and_required(func.inputs()); - let args: any = { - additionalProperties: false, - properties, - type: "object", - }; - if (required?.length > 0) { - args.required = required; - } - let input: Partial = { - properties: { - args, - }, - }; - let outputs = func.outputs(); - let output: Partial = - outputs.length > 0 - ? typeRef(outputs[0]) - : typeRef(xdr.ScSpecTypeDef.scSpecTypeVoid()); - let description = func.doc().toString(); - if (description.length > 0) { - input.description = description; + /** + * 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()); } - input.additionalProperties = false; - output.additionalProperties = false; - return { - input, - output, - }; -} -function unionToJsonSchema(udt: xdr.ScSpecUdtUnionV0): any { - let description = udt.doc().toString(); - let cases = udt.cases(); - let oneOf: any[] = []; - for (let case_ of cases) { - switch (case_.switch().value) { - case xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseVoidV0().value: { - let c = case_.voidCase(); - let title = c.name().toString(); - oneOf.push({ - type: "object", - title, - properties: { - tag: title, - }, - additionalProperties: false, - required: ["tag"], - }); - break; - } - case xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseTupleV0().value: { - let c = case_.tupleCase(); - let title = c.name().toString(); - oneOf.push({ - type: "object", - title, - properties: { - tag: title, - values: { - type: "array", - items: c.type().map(typeRef), - }, - }, - required: ["tag", "values"], - additionalProperties: false, - }); + /** + * Converts the contract spec to a JSON schema. + * + * If `funcName` is provided, the schema will be a reference to the function schema. + * + * @param {string} [funcName] the name of the function to convert + * @returns {JSONSchema7} the converted JSON schema + * + * @throws {Error} if the contract spec is invalid + */ + jsonSchema(funcName?: string): JSONSchema7 { + const definitions: { [key: string]: JSONSchema7Definition } = {}; + /* eslint-disable default-case */ + this.entries.forEach(entry => { + switch (entry.switch().value) { + case xdr.ScSpecEntryKind.scSpecEntryUdtEnumV0().value: { + const udt = entry.udtEnumV0(); + definitions[udt.name().toString()] = enumToJsonSchema(udt); + break; + } + case xdr.ScSpecEntryKind.scSpecEntryUdtStructV0().value: { + const udt = entry.udtStructV0(); + definitions[udt.name().toString()] = structToJsonSchema(udt); + break; + } + case xdr.ScSpecEntryKind.scSpecEntryUdtUnionV0().value: { + const udt = entry.udtUnionV0(); + definitions[udt.name().toString()] = unionToJsonSchema(udt); + break; + } + case xdr.ScSpecEntryKind.scSpecEntryFunctionV0().value: { + const fn = entry.functionV0(); + const fnName = fn.name().toString(); + const { input } = functionToJsonSchema(fn); + // @ts-ignore + definitions[fnName] = input; + break; + } + case xdr.ScSpecEntryKind.scSpecEntryUdtErrorEnumV0().value: { + // console.debug("Error enums not supported yet"); + } } + }); + /* eslint-enable default-case */ + const res: JSONSchema7 = { + $schema: "http://json-schema.org/draft-07/schema#", + definitions: { ...PRIMITIVE_DEFINITONS, ...definitions }, + }; + if (funcName) { + res.$ref = `#/definitions/${funcName}`; } + return res; } - - let res: any = { - oneOf, - }; - if (description.length > 0) { - res.description = description; - } - return res; } -function enumToJsonSchema(udt: xdr.ScSpecUdtEnumV0): any { - let description = udt.doc().toString(); - let cases = udt.cases(); - let oneOf: any[] = []; - for (let case_ of cases) { - let title = case_.name().toString(); - let description = case_.doc().toString(); - oneOf.push({ - description, - title, - enum: [case_.value()], - type: "number", - }); - } - - let res: any = { oneOf }; - if (description.length > 0) { - res.description = description; - } - return res; -} diff --git a/src/contract/types.ts b/src/contract/types.ts index d329bd0da..b97231b55 100644 --- a/src/contract/types.ts +++ b/src/contract/types.ts @@ -3,7 +3,6 @@ import { BASE_FEE, Memo, MemoType, Operation, Transaction, xdr } from "@stellar/stellar-base"; import type { Client } from "./client"; import type { AssembledTransaction } from "./assembled_transaction"; -import { DEFAULT_TIMEOUT } from "./utils"; export type XDR_BASE64 = string; export type u32 = number; @@ -123,3 +122,9 @@ export type AssembledTransactionOptions = MethodOptions & args?: any[]; parseResultXdr: (xdr: xdr.ScVal) => T; }; + +/** + * The default timeout for waiting for a transaction to be included in a block. + */ +export const DEFAULT_TIMEOUT = 5 * 60; +export const NULL_ACCOUNT = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"; diff --git a/src/contract/utils.ts b/src/contract/utils.ts index db99d7cbb..b3a131cb7 100644 --- a/src/contract/utils.ts +++ b/src/contract/utils.ts @@ -1,13 +1,7 @@ import { xdr, cereal, Account } from "@stellar/stellar-base"; import { Server } from "../rpc/server"; -import { NULL_ACCOUNT, type AssembledTransaction } from "./assembled_transaction"; -import { AssembledTransactionOptions } from "./types"; - - -/** - * The default timeout for waiting for a transaction to be included in a block. - */ -export const DEFAULT_TIMEOUT = 5 * 60; +import { type AssembledTransaction } from "./assembled_transaction"; +import { NULL_ACCOUNT , AssembledTransactionOptions } from "./types"; /** * Keep calling a `fn` for `timeoutInSeconds` seconds, if `keepWaitingIf` is @@ -111,11 +105,12 @@ export function processSpecEntryStream(buffer: Buffer) { return res; } +//eslint-disable-next-line require-await export async function getAccount( options: AssembledTransactionOptions, server: Server ): Promise { return options.publicKey - ? await server.getAccount(options.publicKey) + ? server.getAccount(options.publicKey) : new Account(NULL_ACCOUNT, "0"); } diff --git a/src/errors.ts b/src/errors.ts index b84f72162..21f877aff 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,7 +1,9 @@ +/* eslint-disable max-classes-per-file */ import { HorizonApi } from "./horizon/horizon_api"; // For ES5 compatibility (https://stackoverflow.com/a/55066280). /* tslint:disable:variable-name max-classes-per-file */ +/* eslint-disable no-proto */ export class NetworkError extends Error { public response: { @@ -10,6 +12,7 @@ export class NetworkError extends Error { statusText?: string; url?: string; }; + public __proto__: NetworkError; constructor(message: string, response: any) { @@ -72,10 +75,12 @@ export class BadResponseError extends NetworkError { */ export class AccountRequiresMemoError extends Error { public __proto__: AccountRequiresMemoError; + /** * accountId account which requires a memo. */ public accountId: string; + /** * operationIndex operation where accountId is the destination. */ diff --git a/src/federation/server.ts b/src/federation/server.ts index 06d88fc87..5f032c18e 100644 --- a/src/federation/server.ts +++ b/src/federation/server.ts @@ -1,3 +1,4 @@ +/* eslint-disable require-await */ import axios from "axios"; import { StrKey } from "@stellar/stellar-base"; import URI from "urijs"; @@ -15,7 +16,7 @@ export const FEDERATION_RESPONSE_MAX_SIZE = 100 * 1024; * FederationServer handles a network connection to a * [federation server](https://developers.stellar.org/docs/glossary/federation/) * instance and exposes an interface for requests to that instance. - * @constructor + * @class * @param {string} serverURL The federation server URL (ex. `https://acme.com/federation`). * @param {string} domain Domain this server represents * @param {object} [opts] options object @@ -30,6 +31,7 @@ export class FederationServer { * @memberof FederationServer */ private readonly serverURL: URI; // TODO: public or private? readonly? + /** * Domain this server represents. * @@ -37,6 +39,7 @@ export class FederationServer { * @memberof FederationServer */ private readonly domain: string; // TODO: public or private? readonly? + /** * Allow a timeout, default: 0. Allows user to avoid nasty lag due to TOML resolve issue. * diff --git a/src/horizon/account_call_builder.ts b/src/horizon/account_call_builder.ts index 06997053a..58b876c1a 100644 --- a/src/horizon/account_call_builder.ts +++ b/src/horizon/account_call_builder.ts @@ -8,8 +8,8 @@ import { ServerApi } from "./server_api"; * * @see [All Accounts](https://developers.stellar.org/api/resources/accounts/) * @class AccountCallBuilder - * @extends CallBuilder - * @constructor + * @augments CallBuilder + * @class * @param {string} serverUrl Horizon server URL. */ export class AccountCallBuilder extends CallBuilder< diff --git a/src/horizon/account_response.ts b/src/horizon/account_response.ts index 376e9a197..def7ecccb 100644 --- a/src/horizon/account_response.ts +++ b/src/horizon/account_response.ts @@ -1,6 +1,7 @@ /* tslint:disable:variable-name */ import { Account as BaseAccount } from "@stellar/stellar-base"; +import type { TransactionBuilder } from "@stellar/stellar-base"; import { HorizonApi } from "./horizon_api"; import { ServerApi } from "./server_api"; @@ -9,7 +10,7 @@ import { ServerApi } from "./server_api"; * * Returns information and links relating to a single account. * The balances section in the returned JSON will also list all the trust lines this account has set up. - * It also contains {@link Account} object and exposes it's methods so can be used in {@link TransactionBuilder}. + * It also contains {@link BaseAccount} object and exposes it's methods so can be used in {@link TransactionBuilder}. * * @see [Account Details](https://developers.stellar.org/api/resources/accounts/object/) * @param {string} response Response from horizon account endpoint. diff --git a/src/horizon/assets_call_builder.ts b/src/horizon/assets_call_builder.ts index 29d63200a..591bf7a37 100644 --- a/src/horizon/assets_call_builder.ts +++ b/src/horizon/assets_call_builder.ts @@ -6,8 +6,8 @@ import { ServerApi } from "./server_api"; * * Do not create this object directly, use {@link Server#assets}. * @class AssetsCallBuilder - * @constructor - * @extends CallBuilder + * @class + * @augments CallBuilder * @param {string} serverUrl Horizon server URL. */ export class AssetsCallBuilder extends CallBuilder< diff --git a/src/horizon/call_builder.ts b/src/horizon/call_builder.ts index d8c6f8ec4..4006fbf46 100644 --- a/src/horizon/call_builder.ts +++ b/src/horizon/call_builder.ts @@ -6,6 +6,7 @@ import { BadRequestError, NetworkError, NotFoundError } from "../errors"; import { HorizonApi } from "./horizon_api"; import { AxiosClient, version } from "./horizon_axios_client"; import { ServerApi } from "./server_api"; +import type { Server } from "../federation"; // Resources which can be included in the Horizon response via the `join` // query-param. @@ -20,7 +21,9 @@ export interface EventSourceOptions { const anyGlobal = global as any; type Constructable = new (e: string) => T; // require("eventsource") for Node and React Native environment -let EventSource: Constructable = anyGlobal.EventSource ?? +/* eslint-disable global-require */ +/* eslint-disable prefer-import/prefer-import-over-require */ +const EventSource: Constructable = anyGlobal.EventSource ?? anyGlobal.window?.EventSource ?? require("eventsource"); @@ -38,8 +41,11 @@ export class CallBuilder< | ServerApi.CollectionPage > { protected url: URI; + public filter: string[][]; + protected originalSegments: string[]; + protected neighborRoot: string; constructor(serverUrl: URI, neighborRoot: string = "") { @@ -82,10 +88,10 @@ export class CallBuilder< * @see [Horizon Response Format](https://developers.stellar.org/api/introduction/response-format/) * @see [MDN EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) * @param {object} [options] EventSource options. - * @param {function} [options.onmessage] Callback function to handle incoming messages. - * @param {function} [options.onerror] Callback function to handle errors. + * @param {Function} [options.onmessage] Callback function to handle incoming messages. + * @param {Function} [options.onerror] Callback function to handle errors. * @param {number} [options.reconnectTimeout] Custom stream connection timeout in ms, default is 15 seconds. - * @returns {function} Close function. Run to close the connection and stop listening for new events. + * @returns {Function} Close function. Run to close the connection and stop listening for new events. */ public stream(options: EventSourceOptions = {}): () => void { this.checkFilter(); @@ -104,11 +110,12 @@ export class CallBuilder< const createTimeout = () => { timeout = setTimeout(() => { es?.close(); + // eslint-disable-next-line @typescript-eslint/no-use-before-define es = createEventSource(); }, options.reconnectTimeout || 15 * 1000); }; - let createEventSource = (): EventSource => { + const createEventSource = (): EventSource => { try { es = new EventSource(this.url.toString()); } catch (err) { @@ -174,6 +181,8 @@ export class CallBuilder< return es; }; + + createEventSource(); return () => { clearTimeout(timeout); @@ -195,7 +204,7 @@ export class CallBuilder< /** * Sets `limit` parameter for the current call. Returns the CallBuilder object on which this method has been called. * @see [Paging](https://developers.stellar.org/api/introduction/pagination/) - * @param {number} number Number of records the server should return. + * @param {number} recordsNumber Number of records the server should return. * @returns {object} current CallBuilder instance */ public limit(recordsNumber: number): this { @@ -221,7 +230,7 @@ export class CallBuilder< * will include a `transaction` field for each operation in the * response. * - * @param {"transactions"} join Records to be included in the response. + * @param "include" join Records to be included in the response. * @returns {object} current CallBuilder instance. */ public join(include: "transactions"): this { @@ -272,9 +281,9 @@ export class CallBuilder< * Convert a link object to a function that fetches that link. * @private * @param {object} link A link object - * @param {bool} link.href the URI of the link - * @param {bool} [link.templated] Whether the link is templated - * @returns {function} A function that requests the link + * @param {boolean} link.href the URI of the link + * @param {boolean} [link.templated] Whether the link is templated + * @returns {Function} A function that requests the link */ private _requestFnForLink(link: HorizonApi.ResponseLink): (opts?: any) => any { return async (opts: any = {}) => { @@ -303,7 +312,7 @@ export class CallBuilder< if (!json._links) { return json; } - for (const key of Object.keys(json._links)) { + Object.keys(json._links).forEach((key) => { const n = json._links[key]; let included = false; // If the key with the link name already exists, create a copy @@ -323,14 +332,16 @@ export class CallBuilder< const record = this._parseRecord(json[key]); // Maintain a promise based API so the behavior is the same whether you // are loading from the server or in-memory (via join). + // eslint-disable-next-line require-await json[key] = async () => record; } else { json[key] = this._requestFnForLink(n as HorizonApi.ResponseLink); } - } + }); return json; } + // eslint-disable-next-line require-await private async _sendNormalRequest(initialUrl: URI) { let url = initialUrl; @@ -386,6 +397,7 @@ export class CallBuilder< * @param {object} error Network error object * @returns {Promise} Promise that rejects with a human-readable error */ + // eslint-disable-next-line require-await private async _handleNetworkError(error: NetworkError): Promise { if (error.response && error.response.status && error.response.statusText) { switch (error.response.status) { diff --git a/src/horizon/claimable_balances_call_builder.ts b/src/horizon/claimable_balances_call_builder.ts index bf515702a..3d94fd390 100644 --- a/src/horizon/claimable_balances_call_builder.ts +++ b/src/horizon/claimable_balances_call_builder.ts @@ -8,8 +8,8 @@ import { ServerApi } from "./server_api"; * * @see [Claimable Balances](https://developers.stellar.org/api/resources/claimablebalances/) * @class ClaimableBalanceCallBuilder - * @constructor - * @extends CallBuilder + * @class + * @augments CallBuilder * @param {string} serverUrl Horizon server URL. */ export class ClaimableBalanceCallBuilder extends CallBuilder< @@ -65,7 +65,7 @@ export class ClaimableBalanceCallBuilder extends CallBuilder< * Returns all claimable balances which provide a balance for the given asset. * * @see [Claimable Balances](https://developers.stellar.org/api/resources/claimablebalances/list/) - * @param {Asset} The Asset held by the claimable balance + * @param {Asset} asset The Asset held by the claimable balance * @returns {ClaimableBalanceCallBuilder} current ClaimableBalanceCallBuilder instance */ public asset(asset: Asset): this { diff --git a/src/horizon/effect_call_builder.ts b/src/horizon/effect_call_builder.ts index d6db8cc28..01ebaad71 100644 --- a/src/horizon/effect_call_builder.ts +++ b/src/horizon/effect_call_builder.ts @@ -6,9 +6,9 @@ import { ServerApi } from "./server_api"; * Do not create this object directly, use {@link Server#effects}. * * @class EffectCallBuilder - * @extends CallBuilder + * @augments CallBuilder * @see [All Effects](https://developers.stellar.org/api/resources/effects/) - * @constructor + * @class * @param {string} serverUrl Horizon server URL. */ export class EffectCallBuilder extends CallBuilder< diff --git a/src/horizon/horizon_axios_client.ts b/src/horizon/horizon_axios_client.ts index a76da401f..7643b1db5 100644 --- a/src/horizon/horizon_axios_client.ts +++ b/src/horizon/horizon_axios_client.ts @@ -1,7 +1,8 @@ +/* eslint-disable global-require */ import axios, { AxiosResponse } from "axios"; import URI from "urijs"; -/* tslint:disable-next-line:no-var-requires */ +// eslint-disable-next-line prefer-import/prefer-import-over-require export const version = require("../../package.json").version; export interface ServerTime { @@ -30,17 +31,17 @@ export const AxiosClient = axios.create({ }, }); -function _toSeconds(ms: number): number { +function toSeconds(ms: number): number { return Math.floor(ms / 1000); } AxiosClient.interceptors.response.use( - function interceptorHorizonResponse(response: AxiosResponse) { + (response: AxiosResponse) => { const hostname = URI(response.config.url!).hostname(); - const serverTime = _toSeconds(Date.parse(response.headers.date)); - const localTimeRecorded = _toSeconds(new Date().getTime()); + const serverTime = toSeconds(Date.parse(response.headers.date)); + const localTimeRecorded = toSeconds(new Date().getTime()); - if (!isNaN(serverTime)) { + if (!Number.isNaN(serverTime)) { SERVER_TIME_MAP[hostname] = { serverTime, localTimeRecorded, @@ -70,7 +71,7 @@ export function getCurrentServerTime(hostname: string): number | null { } const { serverTime, localTimeRecorded } = entry; - const currentTime = _toSeconds(new Date().getTime()); + const currentTime = toSeconds(new Date().getTime()); // if it's been more than 5 minutes from the last time, then null it out if (currentTime - localTimeRecorded > 60 * 5) { diff --git a/src/horizon/ledger_call_builder.ts b/src/horizon/ledger_call_builder.ts index 5e74a85b5..43ef4aa53 100644 --- a/src/horizon/ledger_call_builder.ts +++ b/src/horizon/ledger_call_builder.ts @@ -6,9 +6,9 @@ import { ServerApi } from "./server_api"; * Do not create this object directly, use {@link Server#ledgers}. * * @see [All Ledgers](https://developers.stellar.org/api/resources/ledgers/list/) - * @constructor + * @class * @class LedgerCallBuilder - * @extends CallBuilder + * @augments CallBuilder * @param {string} serverUrl Horizon server URL. */ export class LedgerCallBuilder extends CallBuilder< diff --git a/src/horizon/liquidity_pool_call_builder.ts b/src/horizon/liquidity_pool_call_builder.ts index f6c24b1f5..25798988e 100644 --- a/src/horizon/liquidity_pool_call_builder.ts +++ b/src/horizon/liquidity_pool_call_builder.ts @@ -8,8 +8,8 @@ import { ServerApi } from "./server_api"; * Do not create this object directly, use {@link Server#liquidityPools}. * * @class LiquidityPoolCallBuilder - * @extends CallBuilder - * @constructor + * @augments CallBuilder + * @class * @param {string} serverUrl Horizon server URL. */ export class LiquidityPoolCallBuilder extends CallBuilder< @@ -24,7 +24,6 @@ export class LiquidityPoolCallBuilder extends CallBuilder< * Filters out pools whose reserves don't exactly match these assets. * * @see Asset - * @param {Asset[]} assets * @returns {LiquidityPoolCallBuilder} current LiquidityPoolCallBuilder instance */ public forAssets(...assets: Asset[]): this { diff --git a/src/horizon/offer_call_builder.ts b/src/horizon/offer_call_builder.ts index 4d40b1a4b..beeb5520f 100644 --- a/src/horizon/offer_call_builder.ts +++ b/src/horizon/offer_call_builder.ts @@ -8,8 +8,8 @@ import { ServerApi } from "./server_api"; * * @see [Offers](https://developers.stellar.org/api/resources/offers/) * @class OfferCallBuilder - * @constructor - * @extends CallBuilder + * @class + * @augments CallBuilder * @param {string} serverUrl Horizon server URL. */ export class OfferCallBuilder extends CallBuilder< diff --git a/src/horizon/operation_call_builder.ts b/src/horizon/operation_call_builder.ts index e7573e7ba..04da7127f 100644 --- a/src/horizon/operation_call_builder.ts +++ b/src/horizon/operation_call_builder.ts @@ -7,8 +7,8 @@ import { ServerApi } from "./server_api"; * * @see [All Operations](https://developers.stellar.org/api/resources/operations/) * @class OperationCallBuilder - * @constructor - * @extends CallBuilder + * @class + * @augments CallBuilder * @param {string} serverUrl Horizon server URL. */ export class OperationCallBuilder extends CallBuilder< @@ -91,7 +91,7 @@ export class OperationCallBuilder extends CallBuilder< * Adds a parameter defining whether to include failed transactions. * By default, only operations of successful transactions are returned. * - * @param {bool} value Set to `true` to include operations of failed transactions. + * @param {boolean} value Set to `true` to include operations of failed transactions. * @returns {OperationCallBuilder} this OperationCallBuilder instance */ public includeFailed(value: boolean): this { diff --git a/src/horizon/path_call_builder.ts b/src/horizon/path_call_builder.ts index 4999e07d8..7b2d40b59 100644 --- a/src/horizon/path_call_builder.ts +++ b/src/horizon/path_call_builder.ts @@ -19,7 +19,7 @@ import { ServerApi } from "./server_api"; * * Do not create this object directly, use {@link Server#paths}. * @see [Find Payment Paths](https://developers.stellar.org/api/aggregations/paths/) - * @extends CallBuilder + * @augments CallBuilder * @param {string} serverUrl Horizon server URL. * @param {string} source The sender's account ID. Any returned path must use a source that the sender can hold. * @param {string} destination The destination account ID that any returned path should use. diff --git a/src/horizon/payment_call_builder.ts b/src/horizon/payment_call_builder.ts index 82e103fb1..1e5c8cb6b 100644 --- a/src/horizon/payment_call_builder.ts +++ b/src/horizon/payment_call_builder.ts @@ -6,8 +6,8 @@ import { ServerApi } from "./server_api"; * * Do not create this object directly, use {@link Server#payments}. * @see [All Payments](https://developers.stellar.org/api/horizon/resources/list-all-payments/) - * @constructor - * @extends CallBuilder + * @class + * @augments CallBuilder * @param {string} serverUrl Horizon server URL. */ export class PaymentCallBuilder extends CallBuilder< diff --git a/src/horizon/server.ts b/src/horizon/server.ts index e039994b3..5e937cd7e 100644 --- a/src/horizon/server.ts +++ b/src/horizon/server.ts @@ -10,6 +10,7 @@ import { } from "@stellar/stellar-base"; import URI from "urijs"; +import type { TransactionBuilder } from "@stellar/stellar-base"; import { CallBuilder } from "./call_builder"; import { Config } from "../config"; import { @@ -37,7 +38,7 @@ import { StrictSendPathCallBuilder } from "./strict_send_path_call_builder"; import { TradeAggregationCallBuilder } from "./trade_aggregation_call_builder"; import { TradesCallBuilder } from "./trades_call_builder"; import { TransactionCallBuilder } from "./transaction_call_builder"; - +// eslint-disable-next-line import/no-named-as-default import AxiosClient, { getCurrentServerTime, } from "./horizon_axios_client"; @@ -50,14 +51,14 @@ const STROOPS_IN_LUMEN = 10000000; // SEP 29 uses this value to define transaction memo requirements for incoming payments. const ACCOUNT_REQUIRES_MEMO = "MQ=="; -function _getAmountInLumens(amt: BigNumber) { +function getAmountInLumens(amt: BigNumber) { return new BigNumber(amt).div(STROOPS_IN_LUMEN).toString(); } /** * Server handles the network connection to a [Horizon](https://developers.stellar.org/api/introduction/) * instance and exposes an interface for requests to that instance. - * @constructor + * @class * @param {string} serverURL Horizon Server URL (ex. `https://horizon-testnet.stellar.org`). * @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. @@ -134,10 +135,10 @@ export class Server { * // earlier does the trick! * .build(); * ``` - * @argument {number} seconds Number of seconds past the current time to wait. - * @argument {bool} [_isRetry=false] True if this is a retry. Only set this internally! + * @param {number} seconds Number of seconds past the current time to wait. + * @param {boolean} [_isRetry] True if this is a retry. Only set this internally! * This is to avoid a scenario where Horizon is horking up the wrong date. - * @returns {Promise} Promise that resolves a `timebounds` object + * @returns {Promise} Promise that resolves a `timebounds` object * (with the shape `{ minTime: 0, maxTime: N }`) that you can set the `timebounds` option to. */ public async fetchTimebounds( @@ -165,7 +166,7 @@ export class Server { // otherwise, retry (by calling the root endpoint) // toString automatically adds the trailing slash await AxiosClient.get(URI(this.serverURL as any).toString()); - return await this.fetchTimebounds(seconds, true); + return this.fetchTimebounds(seconds, true); } /** @@ -185,6 +186,7 @@ export class Server { * @see [Fee Stats](https://developers.stellar.org/api/aggregations/fee-stats/) * @returns {Promise} Promise that resolves to the fee stats returned by Horizon. */ + // eslint-disable-next-line require-await public async feeStats(): Promise { const cb = new CallBuilder( URI(this.serverURL as any), @@ -287,8 +289,7 @@ export class Server { * * If `wasPartiallyFilled` is true, you can tell the user that * `amountBought` or `amountSold` have already been transferred. * - * @see [Post - * Transaction](https://developers.stellar.org/api/resources/transactions/post/) + * @see [PostTransaction](https://developers.stellar.org/api/resources/transactions/post/) * @param {Transaction|FeeBumpTransaction} transaction - The transaction to submit. * @param {object} [opts] Options object * @param {boolean} [opts.skipMemoRequiredCheck] - Allow skipping memo @@ -380,8 +381,8 @@ export class Server { // However, you can never be too careful. default: throw new Error( - "Invalid offer result type: " + - offerClaimedAtom.switch(), + `Invalid offer result type: ${ + offerClaimedAtom.switch()}`, ); } @@ -423,9 +424,9 @@ export class Server { sellerId, offerId: offerClaimed.offerId().toString(), assetSold, - amountSold: _getAmountInLumens(claimedOfferAmountSold), + amountSold: getAmountInLumens(claimedOfferAmountSold), assetBought, - amountBought: _getAmountInLumens(claimedOfferAmountBought), + amountBought: getAmountInLumens(claimedOfferAmountBought), }; }); @@ -443,7 +444,7 @@ export class Server { offerId: offerXDR.offerId().toString(), selling: {}, buying: {}, - amount: _getAmountInLumens(offerXDR.amount().toString()), + amount: getAmountInLumens(offerXDR.amount().toString()), price: { n: offerXDR.price().n(), d: offerXDR.price().d(), @@ -474,8 +475,8 @@ export class Server { currentOffer, // this value is in stroops so divide it out - amountBought: _getAmountInLumens(amountBought), - amountSold: _getAmountInLumens(amountSold), + amountBought: getAmountInLumens(amountBought), + amountSold: getAmountInLumens(amountSold), isFullyOpen: !offersClaimed.length && effect !== "manageOfferDeleted", @@ -491,9 +492,7 @@ export class Server { .filter((result: any) => !!result); } - return Object.assign({}, response.data, { - offerResults: hasManageOffer ? offerResults : undefined, - }); + return { ...response.data, offerResults: hasManageOffer ? offerResults : undefined,}; }) .catch((response) => { if (response instanceof Error) { @@ -713,10 +712,10 @@ export class Server { * * @param {Asset} base base asset * @param {Asset} counter counter asset - * @param {long} start_time lower time boundary represented as millis since epoch - * @param {long} end_time upper time boundary represented as millis since epoch - * @param {long} resolution segment duration as millis since epoch. *Supported values are 5 minutes (300000), 15 minutes (900000), 1 hour (3600000), 1 day (86400000) and 1 week (604800000). - * @param {long} offset segments can be offset using this parameter. Expressed in milliseconds. *Can only be used if the resolution is greater than 1 hour. Value must be in whole hours, less than the provided resolution, and less than 24 hours. + * @param {number} start_time lower time boundary represented as millis since epoch + * @param {number} end_time upper time boundary represented as millis since epoch + * @param {number} resolution segment duration as millis since epoch. *Supported values are 5 minutes (300000), 15 minutes (900000), 1 hour (3600000), 1 day (86400000) and 1 week (604800000). + * @param {number} offset segments can be offset using this parameter. Expressed in milliseconds. *Can only be used if the resolution is greater than 1 hour. Value must be in whole hours, less than the provided resolution, and less than 24 hours. * Returns new {@link TradeAggregationCallBuilder} object configured with the current Horizon server configuration. * @returns {TradeAggregationCallBuilder} New TradeAggregationCallBuilder instance */ @@ -769,7 +768,8 @@ export class Server { const destinations = new Set(); - for (let i = 0; i < transaction.operations.length; i++) { + /* eslint-disable no-continue */ + for (let i = 0; i < transaction.operations.length; i+=1) { const operation = transaction.operations[i]; switch (operation.type) { @@ -793,6 +793,7 @@ export class Server { } try { + // eslint-disable-next-line no-await-in-loop const account = await this.loadAccount(destination); if ( account.data_attr["config.memo_required"] === ACCOUNT_REQUIRES_MEMO @@ -816,6 +817,7 @@ export class Server { continue; } } + /* eslint-enable no-continue */ } } diff --git a/src/horizon/server_api.ts b/src/horizon/server_api.ts index 049f98b04..b567498e7 100644 --- a/src/horizon/server_api.ts +++ b/src/horizon/server_api.ts @@ -182,6 +182,7 @@ export namespace ServerApi { import OperationResponseType = HorizonApi.OperationResponseType; import OperationResponseTypeI = HorizonApi.OperationResponseTypeI; + export interface BaseOperationRecord< T extends OperationResponseType = OperationResponseType, TI extends OperationResponseTypeI = OperationResponseTypeI diff --git a/src/horizon/strict_receive_path_call_builder.ts b/src/horizon/strict_receive_path_call_builder.ts index b44876780..a809c8296 100644 --- a/src/horizon/strict_receive_path_call_builder.ts +++ b/src/horizon/strict_receive_path_call_builder.ts @@ -23,7 +23,7 @@ import { ServerApi } from "./server_api"; * * Do not create this object directly, use {@link Server#strictReceivePaths}. * @see [Find Payment Paths](https://developers.stellar.org/api/aggregations/paths/) - * @extends CallBuilder + * @augments CallBuilder * @param {string} serverUrl Horizon server URL. * @param {string|Asset[]} source The sender's account ID or a list of Assets. Any returned path must use a source that the sender can hold. * @param {Asset} destinationAsset The destination asset. diff --git a/src/horizon/strict_send_path_call_builder.ts b/src/horizon/strict_send_path_call_builder.ts index da840374b..523e700de 100644 --- a/src/horizon/strict_send_path_call_builder.ts +++ b/src/horizon/strict_send_path_call_builder.ts @@ -22,7 +22,7 @@ import { ServerApi } from "./server_api"; * * Do not create this object directly, use {@link Server#strictSendPaths}. * @see [Find Payment Paths](https://developers.stellar.org/api/aggregations/paths/) - * @extends CallBuilder + * @augments CallBuilder * @param {string} serverUrl Horizon server URL. * @param {Asset} sourceAsset The asset to be sent. * @param {string} sourceAmount The amount, denominated in the source asset, that any returned path should be able to satisfy. diff --git a/src/horizon/trade_aggregation_call_builder.ts b/src/horizon/trade_aggregation_call_builder.ts index 8dfff4f69..f20d0a34a 100644 --- a/src/horizon/trade_aggregation_call_builder.ts +++ b/src/horizon/trade_aggregation_call_builder.ts @@ -19,15 +19,15 @@ const allowedResolutions = [ * Do not create this object directly, use {@link Server#tradeAggregation}. * * @class TradeAggregationCallBuilder - * @extends CallBuilder - * @constructor + * @augments CallBuilder + * @class * @param {string} serverUrl serverUrl Horizon server URL. * @param {Asset} base base asset * @param {Asset} counter counter asset - * @param {long} start_time lower time boundary represented as millis since epoch - * @param {long} end_time upper time boundary represented as millis since epoch - * @param {long} resolution segment duration as millis since epoch. *Supported values are 1 minute (60000), 5 minutes (300000), 15 minutes (900000), 1 hour (3600000), 1 day (86400000) and 1 week (604800000). - * @param {long} offset segments can be offset using this parameter. Expressed in milliseconds. *Can only be used if the resolution is greater than 1 hour. Value must be in whole hours, less than the provided resolution, and less than 24 hours. + * @param {number} start_time lower time boundary represented as millis since epoch + * @param {number} end_time upper time boundary represented as millis since epoch + * @param {number} resolution segment duration as millis since epoch. *Supported values are 1 minute (60000), 5 minutes (300000), 15 minutes (900000), 1 hour (3600000), 1 day (86400000) and 1 week (604800000). + * @param {number} offset segments can be offset using this parameter. Expressed in milliseconds. *Can only be used if the resolution is greater than 1 hour. Value must be in whole hours, less than the provided resolution, and less than 24 hours. */ export class TradeAggregationCallBuilder extends CallBuilder< ServerApi.CollectionPage @@ -78,22 +78,17 @@ export class TradeAggregationCallBuilder extends CallBuilder< /** * @private - * @param {long} resolution Trade data resolution in milliseconds + * @param {number} resolution Trade data resolution in milliseconds * @returns {boolean} true if the resolution is allowed */ private isValidResolution(resolution: number): boolean { - for (const allowed of allowedResolutions) { - if (allowed === resolution) { - return true; - } - } - return false; + return allowedResolutions.some((allowed) => allowed === resolution); } /** * @private - * @param {long} offset Time offset in milliseconds - * @param {long} resolution Trade data resolution in milliseconds + * @param {number} offset Time offset in milliseconds + * @param {number} resolution Trade data resolution in milliseconds * @returns {boolean} true if the offset is valid */ private isValidOffset(offset: number, resolution: number): boolean { diff --git a/src/horizon/trades_call_builder.ts b/src/horizon/trades_call_builder.ts index 8c601454d..afddd372c 100644 --- a/src/horizon/trades_call_builder.ts +++ b/src/horizon/trades_call_builder.ts @@ -7,8 +7,8 @@ import { ServerApi } from "./server_api"; * Do not create this object directly, use {@link Server#trades}. * * @class TradesCallBuilder - * @extends CallBuilder - * @constructor + * @augments CallBuilder + * @class * @see [Trades](https://developers.stellar.org/api/resources/trades/) * @param {string} serverUrl serverUrl Horizon server URL. */ diff --git a/src/horizon/transaction_call_builder.ts b/src/horizon/transaction_call_builder.ts index 831c4ee6f..5dd24a3eb 100644 --- a/src/horizon/transaction_call_builder.ts +++ b/src/horizon/transaction_call_builder.ts @@ -6,9 +6,9 @@ import { ServerApi } from "./server_api"; * Do not create this object directly, use {@link Server#transactions}. * * @class TransactionCallBuilder - * @extends CallBuilder + * @augments CallBuilder * @see [All Transactions](https://developers.stellar.org/api/resources/transactions/) - * @constructor + * @class * @param {string} serverUrl Horizon server URL. */ export class TransactionCallBuilder extends CallBuilder< @@ -78,7 +78,7 @@ export class TransactionCallBuilder extends CallBuilder< /** * Adds a parameter defining whether to include failed transactions. By default only successful transactions are * returned. - * @param {bool} value Set to `true` to include failed transactions. + * @param {boolean} value Set to `true` to include failed transactions. * @returns {TransactionCallBuilder} current TransactionCallBuilder instance */ public includeFailed(value: boolean): this { diff --git a/src/horizon/types/assets.ts b/src/horizon/types/assets.ts index 76708718c..844e76d63 100644 --- a/src/horizon/types/assets.ts +++ b/src/horizon/types/assets.ts @@ -1,5 +1,5 @@ import { AssetType } from "@stellar/stellar-base"; -import { HorizonApi } from "./../horizon_api"; +import { HorizonApi } from "../horizon_api"; export interface AssetRecord extends HorizonApi.BaseResponse { asset_type: AssetType.credit4 | AssetType.credit12; diff --git a/src/horizon/types/effects.ts b/src/horizon/types/effects.ts index 7742295d8..f7d0c8af7 100644 --- a/src/horizon/types/effects.ts +++ b/src/horizon/types/effects.ts @@ -1,4 +1,4 @@ -import { HorizonApi } from "./../horizon_api"; +import { HorizonApi } from "../horizon_api"; import { OfferAsset } from "./offer"; // Reference: GO SDK https://github.com/stellar/go/blob/ec5600bd6b2b6900d26988ff670b9ca7992313b8/services/horizon/internal/resourceadapter/effects.go diff --git a/src/horizon/types/offer.ts b/src/horizon/types/offer.ts index 9a0de5000..7331664bb 100644 --- a/src/horizon/types/offer.ts +++ b/src/horizon/types/offer.ts @@ -1,5 +1,5 @@ import { AssetType } from "@stellar/stellar-base"; -import { HorizonApi } from "./../horizon_api"; +import { HorizonApi } from "../horizon_api"; export interface OfferAsset { asset_type: AssetType; diff --git a/src/rpc/api.ts b/src/rpc/api.ts index 25591b379..42c97348b 100644 --- a/src/rpc/api.ts +++ b/src/rpc/api.ts @@ -26,13 +26,14 @@ export namespace Api { key: string; /** a base-64 encoded {@link xdr.LedgerEntryData} instance */ xdr: string; - /** optional, a future ledger number upon which this entry will expire + /** + * optional, a future ledger number upon which this entry will expire * based on https://github.com/stellar/soroban-tools/issues/1010 */ liveUntilLedgerSeq?: number; } - /** An XDR-parsed version of {@link RawLedgerEntryResult} */ + /** An XDR-parsed version of {@link this.RawLedgerEntryResult} */ export interface GetLedgerEntriesResponse { entries: LedgerEntryResult[]; latestLedger: number; @@ -226,7 +227,7 @@ export namespace Api { } /** - * Simplifies {@link RawSimulateTransactionResponse} into separate interfaces + * Simplifies {@link Api.RawSimulateTransactionResponse} into separate interfaces * based on status: * - on success, this includes all fields, though `result` is only present * if an invocation was simulated (since otherwise there's nothing to @@ -253,7 +254,6 @@ export namespace Api { * The field is always present, but may be empty in cases where: * - you didn't simulate an invocation or * - there were no events - * @see {@link humanizeEvents} */ events: xdr.DiagnosticEvent[]; @@ -347,7 +347,8 @@ export namespace Api { /** These are xdr.DiagnosticEvents in base64 */ events?: string[]; minResourceFee?: string; - /** This will only contain a single element if present, because only a single + /** + * This will only contain a single element if present, because only a single * invokeHostFunctionOperation is supported per transaction. * */ results?: RawSimulateHostFunctionResult[]; diff --git a/src/rpc/axios.ts b/src/rpc/axios.ts index bf19ff7d9..e33911202 100644 --- a/src/rpc/axios.ts +++ b/src/rpc/axios.ts @@ -1,7 +1,9 @@ import axios from 'axios'; +/* eslint-disable global-require */ -/* tslint:disable-next-line:no-var-requires */ +// eslint-disable-next-line prefer-import/prefer-import-over-require export const version = require('../../package.json').version; + export const AxiosClient = axios.create({ headers: { 'X-Client-Name': 'js-soroban-client', diff --git a/src/rpc/browser.ts b/src/rpc/browser.ts index a5ce47b04..d382124f1 100644 --- a/src/rpc/browser.ts +++ b/src/rpc/browser.ts @@ -1,9 +1,9 @@ /* tslint:disable:no-var-requires */ +/* eslint-disable import/no-import-module-exports */ +import axios from 'axios'; // idk why axios is weird export * from './index'; -export * as StellarBase from '@stellar/stellar-base'; - -import axios from 'axios'; // idk why axios is weird +export * as StellarBase from '@stellar/stellar-base'; export { axios }; export default module.exports; diff --git a/src/rpc/jsonrpc.ts b/src/rpc/jsonrpc.ts index bd93ebbef..dc29a82c5 100644 --- a/src/rpc/jsonrpc.ts +++ b/src/rpc/jsonrpc.ts @@ -26,6 +26,16 @@ export interface Error { data?: E; } +// Check if the given object X has a field Y, and make that available to +// typescript typing. +function hasOwnProperty( + obj: X, + prop: Y, +): obj is X & Record { + // eslint-disable-next-line no-prototype-builtins + return obj.hasOwnProperty(prop); +} + /** Sends the jsonrpc 'params' as a single 'param' object (no array support). */ export async function postObject( url: string, @@ -44,13 +54,4 @@ export async function postObject( } else { return response.data?.result; } -} - -// Check if the given object X has a field Y, and make that available to -// typescript typing. -function hasOwnProperty( - obj: X, - prop: Y, -): obj is X & Record { - return obj.hasOwnProperty(prop); -} +} \ No newline at end of file diff --git a/src/rpc/parsers.ts b/src/rpc/parsers.ts index a54a83434..076e89e5a 100644 --- a/src/rpc/parsers.ts +++ b/src/rpc/parsers.ts @@ -8,7 +8,7 @@ export function parseRawSendTransaction( delete r.errorResultXdr; delete r.diagnosticEventsXdr; - if (!!errorResultXdr) { + if (errorResultXdr) { return { ...r, ...( @@ -70,49 +70,6 @@ export function parseRawLedgerEntries( }; } -/** - * Converts a raw response schema into one with parsed XDR fields and a - * simplified interface. - * - * @param raw the raw response schema (parsed ones are allowed, best-effort - * detected, and returned untouched) - * - * @returns the original parameter (if already parsed), parsed otherwise - * - * @warning This API is only exported for testing purposes and should not be - * relied on or considered "stable". - */ -export function parseRawSimulation( - sim: - | Api.SimulateTransactionResponse - | Api.RawSimulateTransactionResponse -): Api.SimulateTransactionResponse { - const looksRaw = Api.isSimulationRaw(sim); - if (!looksRaw) { - // Gordon Ramsey in shambles - return sim; - } - - // shared across all responses - let base: Api.BaseSimulateTransactionResponse = { - _parsed: true, - id: sim.id, - latestLedger: sim.latestLedger, - events: - sim.events?.map((evt) => xdr.DiagnosticEvent.fromXDR(evt, 'base64')) ?? [] - }; - - // error type: just has error string - if (typeof sim.error === 'string') { - return { - ...base, - error: sim.error - }; - } - - return parseSuccessful(sim, base); -} - function parseSuccessful( sim: Api.RawSimulateTransactionResponse, partial: Api.BaseSimulateTransactionResponse @@ -127,29 +84,27 @@ function parseSuccessful( cost: sim.cost!, ...// coalesce 0-or-1-element results[] list into a single result struct // with decoded fields if present + // eslint-disable-next-line no-self-compare ((sim.results?.length ?? 0 > 0) && { - result: sim.results!.map((row) => { - return { + result: sim.results!.map((row) => ({ auth: (row.auth ?? []).map((entry) => xdr.SorobanAuthorizationEntry.fromXDR(entry, 'base64') ), // if return value is missing ("falsy") we coalesce to void - retval: !!row.xdr + retval: row.xdr ? xdr.ScVal.fromXDR(row.xdr, 'base64') : xdr.ScVal.scvVoid() - }; - })[0] + }))[0] }), + // eslint-disable-next-line no-self-compare ...(sim.stateChanges?.length ?? 0 > 0) && { - stateChanges: sim.stateChanges?.map((entryChange) => { - return { + stateChanges: sim.stateChanges?.map((entryChange) => ({ type: entryChange.type, key: xdr.LedgerKey.fromXDR(entryChange.key, 'base64'), before: entryChange.before ? xdr.LedgerEntry.fromXDR(entryChange.before, 'base64') : null, after: entryChange.after ? xdr.LedgerEntry.fromXDR(entryChange.after, 'base64') : null, - }; - }) + })) } }; @@ -169,3 +124,46 @@ function parseSuccessful( } }; } + +/** + * Converts a raw response schema into one with parsed XDR fields and a + * simplified interface. + * Warning: This API is only exported for testing purposes and should not be + * relied on or considered "stable". + * + * @param {Api.SimulateTransactionResponse|Api.RawSimulateTransactionResponse} sim the raw response schema (parsed ones are allowed, best-effort + * detected, and returned untouched) + * + * @returns the original parameter (if already parsed), parsed otherwise + * + */ +export function parseRawSimulation( + sim: + | Api.SimulateTransactionResponse + | Api.RawSimulateTransactionResponse +): Api.SimulateTransactionResponse { + const looksRaw = Api.isSimulationRaw(sim); + if (!looksRaw) { + // Gordon Ramsey in shambles + return sim; + } + + // shared across all responses + const base: Api.BaseSimulateTransactionResponse = { + _parsed: true, + id: sim.id, + latestLedger: sim.latestLedger, + events: + sim.events?.map((evt) => xdr.DiagnosticEvent.fromXDR(evt, 'base64')) ?? [] + }; + + // error type: just has error string + if (typeof sim.error === 'string') { + return { + ...base, + error: sim.error + }; + } + + return parseSuccessful(sim, base); +} diff --git a/src/rpc/server.ts b/src/rpc/server.ts index a0d09f2ba..1ec1a9804 100644 --- a/src/rpc/server.ts +++ b/src/rpc/server.ts @@ -11,6 +11,8 @@ import { xdr } from '@stellar/stellar-base'; +import type { TransactionBuilder } from '@stellar/stellar-base'; +// eslint-disable-next-line import/no-named-as-default import AxiosClient from './axios'; import { Api as FriendbotApi } from '../friendbot'; import * as jsonrpc from './jsonrpc'; @@ -52,11 +54,44 @@ export namespace Server { } } +function findCreatedAccountSequenceInTransactionMeta( + meta: xdr.TransactionMeta +): string { + let operations: xdr.OperationMeta[] = []; + switch (meta.switch()) { + case 0: + operations = meta.operations(); + break; + case 1: + case 2: + case 3: // all three have the same interface + operations = (meta.value() as xdr.TransactionMetaV3).operations(); + break; + default: + throw new Error('Unexpected transaction meta switch value'); + } + const sequenceNumber = operations + .flatMap(op => op.changes()) + .find(c => c.switch() === xdr.LedgerEntryChangeType.ledgerEntryCreated() && + c.created().data().switch() === xdr.LedgerEntryType.account()) + ?.created() + ?.data() + ?.account() + ?.seqNum() + ?.toString(); + + if (sequenceNumber) { + return sequenceNumber; + } + throw new Error('No account created in transaction'); +} + +/* eslint-disable jsdoc/no-undefined-types */ /** * Handles the network connection to a Soroban RPC instance, exposing an * interface for requests to that instance. * - * @constructor + * @class * * @param {string} serverURL Soroban-RPC Server URL (ex. * `http://localhost:8000/soroban/rpc`). @@ -117,6 +152,7 @@ export class Server { const resp = await this.getLedgerEntries(ledgerKey); if (resp.entries.length === 0) { + // eslint-disable-next-line prefer-promise-reject-errors return Promise.reject({ code: 404, message: `Account not found: ${address}` @@ -140,6 +176,7 @@ export class Server { * console.log("status:", health.status); * }); */ + // eslint-disable-next-line require-await public async getHealth(): Promise { return jsonrpc.postObject( this.serverURL.toString(), @@ -153,6 +190,8 @@ export class Server { * 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 {@link Server.simulateTransaction}. + * Warning: If the data entry in question is a 'temporary' entry, it's + * entirely possible that it has expired out of existence. * * @param {string|Address|Contract} contract the contract ID containing the * data to load as a strkey (`C...` form), a {@link Contract}, or an @@ -164,9 +203,6 @@ export class Server { * * @returns {Promise} the current data value * - * @warning If the data entry in question is a 'temporary' entry, it's - * entirely possible that it has expired out of existence. - * * @see https://soroban.stellar.org/api/methods/getLedgerEntries * @example * const contractId = "CCJZ5DGASBWQXR5MPFCJXMBI333XE5U3FSJTNQU7RIKE3P5GN2K2WYD5"; @@ -178,6 +214,7 @@ export class Server { * console.log("latestLedger:", data.latestLedger); * }); */ + // eslint-disable-next-line require-await public async getContractData( contract: string | Address | Contract, key: xdr.ScVal, @@ -209,7 +246,7 @@ export class Server { throw new TypeError(`invalid durability: ${durability}`); } - let contractKey = xdr.LedgerKey.contractData( + const contractKey = xdr.LedgerKey.contractData( new xdr.LedgerKeyContractData({ key, contract: scAddress, @@ -220,6 +257,7 @@ export class Server { return this.getLedgerEntries(contractKey).then( (r: Api.GetLedgerEntriesResponse) => { if (r.entries.length === 0) { + // eslint-disable-next-line prefer-promise-reject-errors return Promise.reject({ code: 404, message: `Contract data not found. Contract: ${Address.fromScAddress( @@ -265,6 +303,7 @@ export class Server { const contractLedgerKey = new Contract(contractId).getFootprint(); const response = await this.getLedgerEntries(contractLedgerKey); if (!response.entries.length || !response.entries[0]?.val) { + // eslint-disable-next-line prefer-promise-reject-errors return Promise.reject({code: 404, message: `Could not obtain contract hash from server`}); } @@ -315,6 +354,7 @@ export class Server { const responseWasm = await this.getLedgerEntries(ledgerKeyWasmHash); if (!responseWasm.entries.length || !responseWasm.entries[0]?.val) { + // eslint-disable-next-line prefer-promise-reject-errors return Promise.reject({ code: 404, message: "Could not obtain contract wasm from server" }); } const wasmBuffer = responseWasm.entries[0].val.contractCode().code(); @@ -355,12 +395,14 @@ export class Server { * console.log("latestLedger:", response.latestLedger); * }); */ + // eslint-disable-next-line require-await public async getLedgerEntries( ...keys: xdr.LedgerKey[] ): Promise { return this._getLedgerEntries(...keys).then(parseRawLedgerEntries); } + // eslint-disable-next-line require-await public async _getLedgerEntries(...keys: xdr.LedgerKey[]) { return jsonrpc .postObject( @@ -392,6 +434,7 @@ export class Server { * console.log("resultXdr:", tx.resultXdr); * }); */ + // eslint-disable-next-line require-await public async getTransaction( hash: string ): Promise { @@ -435,6 +478,7 @@ export class Server { }); } + // eslint-disable-next-line require-await public async _getTransaction( hash: string ): Promise { @@ -479,12 +523,14 @@ export class Server { * limit: 10, * }); */ + // eslint-disable-next-line require-await public async getEvents( request: Server.GetEventsRequest ): Promise { return this._getEvents(request).then(parseRawEvents); } + // eslint-disable-next-line require-await public async _getEvents( request: Server.GetEventsRequest ): Promise { @@ -514,8 +560,9 @@ export class Server { * console.log("protocolVersion:", network.protocolVersion); * }); */ + // eslint-disable-next-line require-await public async getNetwork(): Promise { - return await jsonrpc.postObject(this.serverURL.toString(), 'getNetwork'); + return jsonrpc.postObject(this.serverURL.toString(), 'getNetwork'); } /** @@ -533,6 +580,7 @@ export class Server { * console.log("protocolVersion:", response.protocolVersion); * }); */ + // eslint-disable-next-line require-await public async getLatestLedger(): Promise { return jsonrpc.postObject(this.serverURL.toString(), 'getLatestLedger'); } @@ -541,7 +589,7 @@ export class Server { * Submit a trial contract invocation to get back return values, expected * ledger footprint, expected authorizations, and expected costs. * - * @param {Transaction | FeeBumpTransaction} transaction the transaction to + * @param {Transaction | FeeBumpTransaction} tx the transaction to * simulate, which should include exactly one operation (one of * {@link xdr.InvokeHostFunctionOp}, {@link xdr.ExtendFootprintTTLOp}, or * {@link xdr.RestoreFootprintOp}). Any provided footprint or auth @@ -579,6 +627,7 @@ export class Server { * console.log("latestLedger:", sim.latestLedger); * }); */ + // eslint-disable-next-line require-await public async simulateTransaction( tx: Transaction | FeeBumpTransaction, addlResources?: Server.ResourceLeeway @@ -587,6 +636,7 @@ export class Server { .then(parseRawSimulation); } + // eslint-disable-next-line require-await public async _simulateTransaction( transaction: Transaction | FeeBumpTransaction, addlResources?: Server.ResourceLeeway @@ -622,7 +672,7 @@ export class Server { * if you want to inspect estimated fees for a given transaction in detail * first, then re-assemble it manually or via {@link assembleTransaction}. * - * @param {Transaction | FeeBumpTransaction} transaction the transaction to + * @param {Transaction | FeeBumpTransaction} tx the transaction to * prepare. It should include exactly one operation, which must be one of * {@link xdr.InvokeHostFunctionOp}, {@link xdr.ExtendFootprintTTLOp}, * or {@link xdr.RestoreFootprintOp}. @@ -677,7 +727,7 @@ export class Server { public async prepareTransaction(tx: Transaction | FeeBumpTransaction) { const simResponse = await this.simulateTransaction(tx); if (Api.isSimulationError(simResponse)) { - throw simResponse.error; + throw new Error(simResponse.error); } return assembleTransaction(tx, simResponse).build(); @@ -726,12 +776,14 @@ export class Server { * console.log("errorResultXdr:", result.errorResultXdr); * }); */ + // eslint-disable-next-line require-await public async sendTransaction( transaction: Transaction | FeeBumpTransaction ): Promise { return this._sendTransaction(transaction).then(parseRawSendTransaction); } + // eslint-disable-next-line require-await public async _sendTransaction( transaction: Transaction | FeeBumpTransaction ): Promise { @@ -803,37 +855,3 @@ export class Server { } } } - -function findCreatedAccountSequenceInTransactionMeta( - meta: xdr.TransactionMeta -): string { - let operations: xdr.OperationMeta[] = []; - switch (meta.switch()) { - case 0: - operations = meta.operations(); - break; - case 1: - case 2: - case 3: // all three have the same interface - operations = (meta.value() as xdr.TransactionMetaV3).operations(); - break; - default: - throw new Error('Unexpected transaction meta switch value'); - } - - for (const op of operations) { - for (const c of op.changes()) { - if (c.switch() !== xdr.LedgerEntryChangeType.ledgerEntryCreated()) { - continue; - } - const data = c.created().data(); - if (data.switch() !== xdr.LedgerEntryType.account()) { - continue; - } - - return data.account().seqNum().toString(); - } - } - - throw new Error('No account created in transaction'); -} diff --git a/src/rpc/transaction.ts b/src/rpc/transaction.ts index c75f333b4..8ec79d252 100644 --- a/src/rpc/transaction.ts +++ b/src/rpc/transaction.ts @@ -7,9 +7,30 @@ import { import { Api } from './api'; import { parseRawSimulation } from './parsers'; +import type { Server } from './server'; + +function isSorobanTransaction(tx: Transaction): boolean { + if (tx.operations.length !== 1) { + return false; + } + + switch (tx.operations[0].type) { + case 'invokeHostFunction': + case 'extendFootprintTtl': + case 'restoreFootprint': + return true; + + default: + return false; + } +} + /** * Combines the given raw transaction alongside the simulation results. + * If the given transaction already has authorization entries in a host + * function invocation (see {@link Operation.invokeHostFunction}), **the + * simulation entries are ignored**. * * @param raw the initial transaction, w/o simulation applied * @param simulation the Soroban RPC simulation result (see @@ -18,10 +39,6 @@ import { parseRawSimulation } from './parsers'; * @returns a new, cloned transaction with the proper auth and resource (fee, * footprint) simulation data applied * - * @note if the given transaction already has authorization entries in a host - * function invocation (see {@link Operation.invokeHostFunction}), **the - * simulation entries are ignored**. - * * @see {Server.simulateTransaction} * @see {Server.prepareTransaction} */ @@ -47,11 +64,12 @@ export function assembleTransaction( ); } - let success = parseRawSimulation(simulation); + const success = parseRawSimulation(simulation); if (!Api.isSimulationSuccess(success)) { throw new Error(`simulation incorrect: ${JSON.stringify(success)}`); } + /* eslint-disable radix */ const classicFeeNum = parseInt(raw.fee) || 0; const minResourceFeeNum = parseInt(success.minResourceFee) || 0; const txnBuilder = TransactionBuilder.cloneFrom(raw, { @@ -69,43 +87,25 @@ export function assembleTransaction( networkPassphrase: raw.networkPassphrase }); - switch (raw.operations[0].type) { - case 'invokeHostFunction': - // In this case, we don't want to clone the operation, so we drop it. - txnBuilder.clearOperations(); + if (raw.operations[0].type === 'invokeHostFunction') { + // In this case, we don't want to clone the operation, so we drop it. + txnBuilder.clearOperations(); - const invokeOp: Operation.InvokeHostFunction = raw.operations[0]; - const existingAuth = invokeOp.auth ?? []; - txnBuilder.addOperation( - Operation.invokeHostFunction({ - source: invokeOp.source, - func: invokeOp.func, - // if auth entries are already present, we consider this "advanced - // usage" and disregard ALL auth entries from the simulation - // - // the intuition is "if auth exists, this tx has probably been - // simulated before" - auth: existingAuth.length > 0 ? existingAuth : success.result!.auth - }) - ); - break; + const invokeOp: Operation.InvokeHostFunction = raw.operations[0]; + const existingAuth = invokeOp.auth ?? []; + txnBuilder.addOperation( + Operation.invokeHostFunction({ + source: invokeOp.source, + func: invokeOp.func, + // if auth entries are already present, we consider this "advanced + // usage" and disregard ALL auth entries from the simulation + // + // the intuition is "if auth exists, this tx has probably been + // simulated before" + auth: existingAuth.length > 0 ? existingAuth : success.result!.auth + }) + ); } return txnBuilder; } - -function isSorobanTransaction(tx: Transaction): boolean { - if (tx.operations.length !== 1) { - return false; - } - - switch (tx.operations[0].type) { - case 'invokeHostFunction': - case 'extendFootprintTtl': - case 'restoreFootprint': - return true; - - default: - return false; - } -} diff --git a/src/rpc/utils.ts b/src/rpc/utils.ts index af4bb76a2..7691f4e7f 100644 --- a/src/rpc/utils.ts +++ b/src/rpc/utils.ts @@ -4,5 +4,6 @@ export function hasOwnProperty( obj: X, prop: Y, ): obj is X & Record { + // eslint-disable-next-line no-prototype-builtins return obj.hasOwnProperty(prop); } diff --git a/src/stellartoml/index.ts b/src/stellartoml/index.ts index ba0c965bf..96d4bb755 100644 --- a/src/stellartoml/index.ts +++ b/src/stellartoml/index.ts @@ -31,6 +31,7 @@ export class Resolver { * @param {number} [opts.timeout] - Allow a timeout, default: 0. Allows user to avoid nasty lag due to TOML resolve issue. * @returns {Promise} A `Promise` that resolves to the parsed stellar.toml object */ + // eslint-disable-next-line require-await public static async resolve( domain: string, opts: Api.StellarTomlResolveOptions = {}, diff --git a/src/webauth/errors.ts b/src/webauth/errors.ts index 5ba7bca43..4126ea6e0 100644 --- a/src/webauth/errors.ts +++ b/src/webauth/errors.ts @@ -1,4 +1,4 @@ - +/* eslint-disable no-proto */ export class InvalidChallengeError extends Error { public __proto__: InvalidChallengeError; diff --git a/test/e2e/src/test-contract-client-constructor.js b/test/e2e/src/test-contract-client-constructor.js index da40738a7..c0eb015c8 100644 --- a/test/e2e/src/test-contract-client-constructor.js +++ b/test/e2e/src/test-contract-client-constructor.js @@ -89,9 +89,8 @@ async function clientForFromTest(contractId, publicKey, keypair) { return contract.Client.from(options); } -describe('Client', function() { - - before(async function() { +describe("Client", function () { + before(async function () { const { client, keypair, contractId } = await clientFromConstructor("customTypes"); const publicKey = keypair.publicKey(); @@ -99,12 +98,12 @@ describe('Client', function() { this.context = { client, publicKey, addr, contractId, keypair }; }); - it("can be constructed with `new Client`", async function() { + it("can be constructed with `new Client`", async function () { const { result } = await this.context.client.hello({ hello: "tests" }); expect(result).to.equal("tests"); }); - it("can be constructed with `from`", async function() { + it("can be constructed with `from`", async function () { // objects with different constructors will not pass deepEqual check function constructorWorkaround(object) { return JSON.parse(JSON.stringify(object)); @@ -118,6 +117,8 @@ describe('Client', function() { expect(constructorWorkaround(clientFromFrom)).to.deep.equal( constructorWorkaround(this.context.client), ); - expect(this.context.client.spec.entries).to.deep.equal(clientFromFrom.spec.entries); + expect(this.context.client.spec.entries).to.deep.equal( + clientFromFrom.spec.entries, + ); }); }); diff --git a/test/e2e/src/test-custom-types.js b/test/e2e/src/test-custom-types.js index 778a4eeb0..b5c253653 100644 --- a/test/e2e/src/test-custom-types.js +++ b/test/e2e/src/test-custom-types.js @@ -1,83 +1,100 @@ -const { expect } = require('chai'); +const { expect } = require("chai"); const { Address, contract } = require("../../.."); const { clientFor } = require("./util"); - -describe("Custom Types Tests", function() { - before(async function() { +describe("Custom Types Tests", function () { + before(async function () { const { client, keypair, contractId } = await clientFor("customTypes"); const publicKey = keypair.publicKey(); const addr = Address.fromString(publicKey); this.context = { client, publicKey, addr, contractId, keypair }; }); - it("hello", async function() { - expect((await this.context.client.hello({ hello: "tests" })).result).to.equal("tests"); + it("hello", async function () { + expect( + (await this.context.client.hello({ hello: "tests" })).result, + ).to.equal("tests"); }); - it("view method with empty keypair", async function() { + it("view method with empty keypair", async function () { const { client: client2 } = await clientFor("customTypes", { keypair: undefined, contractId: this.context.contractId, }); - expect((await client2.hello({ hello: "anonymous" })).result).to.equal("anonymous"); + expect((await client2.hello({ hello: "anonymous" })).result).to.equal( + "anonymous", + ); }); - it("woid", async function() { + it("woid", async function () { expect((await this.context.client.woid()).result).to.be.null; }); - it("u32_fail_on_even", async function() { + it("u32_fail_on_even", async function () { let response = await this.context.client.u32_fail_on_even({ u32_: 1 }); expect(response.result).to.deep.equal(new contract.Ok(1)); response = await this.context.client.u32_fail_on_even({ u32_: 2 }); - expect(response.result).to.deep.equal(new contract.Err({ message: "Please provide an odd number" })); + expect(response.result).to.deep.equal( + new contract.Err({ message: "Please provide an odd number" }), + ); }); - it("u32", async function() { + it("u32", async function () { expect((await this.context.client.u32_({ u32_: 1 })).result).to.equal(1); }); - it("i32", async function() { + it("i32", async function () { expect((await this.context.client.i32_({ i32_: 1 })).result).to.equal(1); }); - it("i64", async function() { + it("i64", async function () { expect((await this.context.client.i64_({ i64_: 1n })).result).to.equal(1n); }); - it("strukt_hel", async function() { + it("strukt_hel", async function () { const strukt = { a: 0, b: true, c: "world" }; - expect((await this.context.client.strukt_hel({ strukt })).result).to.deep.equal(["Hello", "world"]); + expect( + (await this.context.client.strukt_hel({ strukt })).result, + ).to.deep.equal(["Hello", "world"]); }); - it("strukt", async function() { + it("strukt", async function () { const strukt = { a: 0, b: true, c: "hello" }; - expect((await this.context.client.strukt({ strukt })).result).to.deep.equal(strukt); + expect((await this.context.client.strukt({ strukt })).result).to.deep.equal( + strukt, + ); }); - it("simple first", async function() { + it("simple first", async function () { const simple = { tag: "First", values: undefined }; - expect((await this.context.client.simple({ simple })).result).to.deep.equal({ tag: "First" }); + expect((await this.context.client.simple({ simple })).result).to.deep.equal( + { tag: "First" }, + ); }); - it("simple second", async function() { + it("simple second", async function () { const simple = { tag: "Second", values: undefined }; - expect((await this.context.client.simple({ simple })).result).to.deep.equal({ tag: "Second" }); + expect((await this.context.client.simple({ simple })).result).to.deep.equal( + { tag: "Second" }, + ); }); - it("simple third", async function() { + it("simple third", async function () { const simple = { tag: "Third", values: undefined }; - expect((await this.context.client.simple({ simple })).result).to.deep.equal({ tag: "Third" }); + expect((await this.context.client.simple({ simple })).result).to.deep.equal( + { tag: "Third" }, + ); }); - it("complex with struct", async function() { + it("complex with struct", async function () { const arg = { tag: "Struct", values: [{ a: 0, b: true, c: "hello" }] }; - expect((await this.context.client.complex({ complex: arg })).result).to.deep.equal(arg); + expect( + (await this.context.client.complex({ complex: arg })).result, + ).to.deep.equal(arg); }); - it("complex with tuple", async function() { + it("complex with tuple", async function () { const arg = { tag: "Tuple", values: [ @@ -91,62 +108,83 @@ describe("Custom Types Tests", function() { tag: "Tuple", values: [[{ a: 0, b: true, c: "hello" }, { tag: "First" }]], }; - expect((await this.context.client.complex({ complex: arg })).result).to.deep.equal(ret); + expect( + (await this.context.client.complex({ complex: arg })).result, + ).to.deep.equal(ret); }); - it("complex with enum", async function() { + it("complex with enum", async function () { const arg = { tag: "Enum", values: [{ tag: "First", values: undefined }] }; const ret = { tag: "Enum", values: [{ tag: "First" }] }; - expect((await this.context.client.complex({ complex: arg })).result).to.deep.equal(ret); + expect( + (await this.context.client.complex({ complex: arg })).result, + ).to.deep.equal(ret); }); - it("complex with asset", async function() { + it("complex with asset", async function () { const arg = { tag: "Asset", values: [this.context.publicKey, 1n] }; - expect((await this.context.client.complex({ complex: arg })).result).to.deep.equal(arg); + expect( + (await this.context.client.complex({ complex: arg })).result, + ).to.deep.equal(arg); }); - it("complex with void", async function() { + it("complex with void", async function () { const complex = { tag: "Void", values: undefined }; const ret = { tag: "Void" }; - expect((await this.context.client.complex({ complex })).result).to.deep.equal(ret); + expect( + (await this.context.client.complex({ complex })).result, + ).to.deep.equal(ret); }); - it("addresse", async function() { - expect((await this.context.client.addresse({ addresse: this.context.publicKey })).result).to.equal(this.context.addr.toString()); + it("addresse", async function () { + expect( + (await this.context.client.addresse({ addresse: this.context.publicKey })) + .result, + ).to.equal(this.context.addr.toString()); }); - it("bytes", async function() { + it("bytes", async function () { const bytes = Buffer.from("hello"); - expect((await this.context.client.bytes({ bytes })).result).to.deep.equal(bytes); + expect((await this.context.client.bytes({ bytes })).result).to.deep.equal( + bytes, + ); }); - it("bytesN", async function() { + it("bytesN", async function () { const bytesN = Buffer.from("123456789"); // what's the correct way to construct bytesN? - expect((await this.context.client.bytes_n({ bytes_n: bytesN })).result).to.deep.equal(bytesN); + expect( + (await this.context.client.bytes_n({ bytes_n: bytesN })).result, + ).to.deep.equal(bytesN); }); - it("card", async function() { + it("card", async function () { const card = 11; expect((await this.context.client.card({ card })).result).to.equal(card); }); - it("boolean", async function() { - expect((await this.context.client.boolean({ boolean: true })).result).to.equal(true); + it("boolean", async function () { + expect( + (await this.context.client.boolean({ boolean: true })).result, + ).to.equal(true); }); - it("not", async function() { - expect((await this.context.client.not({ boolean: true })).result).to.equal(false); + it("not", async function () { + expect((await this.context.client.not({ boolean: true })).result).to.equal( + false, + ); }); - it("i128", async function() { - expect((await this.context.client.i128({ i128: -1n })).result).to.equal(-1n); + it("i128", async function () { + expect((await this.context.client.i128({ i128: -1n })).result).to.equal( + -1n, + ); }); - it("u128", async function() { + it("u128", async function () { expect((await this.context.client.u128({ u128: 1n })).result).to.equal(1n); }); - it("multi_args", async function() { + it("multi_args", async function () { let response = await this.context.client.multi_args({ a: 1, b: true }); expect(response.result).to.equal(1); @@ -154,24 +192,28 @@ describe("Custom Types Tests", function() { expect(response.result).to.equal(0); }); - it("map", async function() { + it("map", async function () { const map = new Map(); map.set(1, true); map.set(2, false); - expect((await this.context.client.map({ map })).result).to.deep.equal(Array.from(map.entries())); + expect((await this.context.client.map({ map })).result).to.deep.equal( + Array.from(map.entries()), + ); }); - it("vec", async function() { + it("vec", async function () { const vec = [1, 2, 3]; expect((await this.context.client.vec({ vec })).result).to.deep.equal(vec); }); - it("tuple", async function() { + it("tuple", async function () { const tuple = ["hello", 1]; - expect((await this.context.client.tuple({ tuple })).result).to.deep.equal(tuple); + expect((await this.context.client.tuple({ tuple })).result).to.deep.equal( + tuple, + ); }); - it("option", async function() { + it("option", async function () { let response = await this.context.client.option({ option: 1 }); expect(response.result).to.equal(1); @@ -183,24 +225,30 @@ describe("Custom Types Tests", function() { // t.deepEqual((await t.context.client.option({ option: undefined })).result, undefined) }); - it("u256", async function() { + it("u256", async function () { expect((await this.context.client.u256({ u256: 1n })).result).to.equal(1n); }); - it("i256", async function() { - expect((await this.context.client.i256({ i256: -1n })).result).to.equal(-1n); + it("i256", async function () { + expect((await this.context.client.i256({ i256: -1n })).result).to.equal( + -1n, + ); }); - it("string", async function() { - expect((await this.context.client.string({ string: "hello" })).result).to.equal("hello"); + it("string", async function () { + expect( + (await this.context.client.string({ string: "hello" })).result, + ).to.equal("hello"); }); - it("tuple strukt", async function() { + it("tuple strukt", async function () { const arg = [ { a: 0, b: true, c: "hello" }, { tag: "First", values: undefined }, ]; const res = [{ a: 0, b: true, c: "hello" }, { tag: "First" }]; - expect((await this.context.client.tuple_strukt({ tuple_strukt: arg })).result).to.deep.equal(res); + expect( + (await this.context.client.tuple_strukt({ tuple_strukt: arg })).result, + ).to.deep.equal(res); }); -}); \ No newline at end of file +}); diff --git a/test/e2e/src/test-hello-world.js b/test/e2e/src/test-hello-world.js index 50b50f4f7..ac74a4629 100644 --- a/test/e2e/src/test-hello-world.js +++ b/test/e2e/src/test-hello-world.js @@ -1,21 +1,21 @@ const { expect } = require("chai"); const { clientFor } = require("./util"); -describe("helloWorld client", function() { - it("should return properly formed hello response", async function() { +describe("helloWorld client", function () { + it("should return properly formed hello response", async function () { const { client } = await clientFor("helloWorld"); const response = await client.hello({ world: "tests" }); expect(response.result).to.deep.equal(["Hello", "tests"]); }); - it("should authenticate the user correctly", async function() { + it("should authenticate the user correctly", async function () { const { client, keypair } = await clientFor("helloWorld"); const publicKey = keypair.publicKey(); const { result } = await client.auth({ addr: publicKey, world: "lol" }); expect(result).to.equal(publicKey); }); - it("should increment the counter correctly", async function() { + it("should increment the counter correctly", async function () { const { client } = await clientFor("helloWorld"); const { result: startingBalance } = await client.get_count(); const inc = await client.inc(); @@ -26,7 +26,7 @@ describe("helloWorld client", function() { expect(newBalance).to.equal(startingBalance + 1); }); - it("should accept only options object for methods with no arguments", async function() { + it("should accept only options object for methods with no arguments", async function () { const { client } = await clientFor("helloWorld"); const inc = await client.inc({ simulate: false }); expect(inc.simulation).to.be.undefined; diff --git a/test/e2e/src/test-methods-as-args.js b/test/e2e/src/test-methods-as-args.js index 1f4b5b5ef..3308e907d 100644 --- a/test/e2e/src/test-methods-as-args.js +++ b/test/e2e/src/test-methods-as-args.js @@ -6,8 +6,8 @@ function callMethod(method, args) { return method(args); } -describe("methods-as-args", function() { - it("should pass methods as arguments and have them still work", async function() { +describe("methods-as-args", function () { + it("should pass methods as arguments and have them still work", async function () { const { client } = await clientFor("helloWorld"); const { result } = await callMethod(client.hello, { world: "tests" }); expect(result).to.deep.equal(["Hello", "tests"]); diff --git a/test/e2e/src/test-swap.js b/test/e2e/src/test-swap.js index a55497071..b46a4eec0 100644 --- a/test/e2e/src/test-swap.js +++ b/test/e2e/src/test-swap.js @@ -17,7 +17,6 @@ const amountAToSwap = 2n; const amountBToSwap = 1n; describe("Swap Contract Tests", function () { - before(async function () { const alice = await generateFundedKeypair(); const bob = await generateFundedKeypair(); @@ -46,12 +45,14 @@ describe("Swap Contract Tests", function () { await tokenA.mint({ amount: amountAToSwap, to: alice.publicKey() }) ).signAndSend(); - await tokenB.initialize({ - admin: root.publicKey(), - decimal: 0, - name: "Token B", - symbol: "B", - }).then(t => t.signAndSend()); + await tokenB + .initialize({ + admin: root.publicKey(), + decimal: 0, + name: "Token B", + symbol: "B", + }) + .then((t) => t.signAndSend()); await ( await tokenB.mint({ amount: amountBToSwap, to: bob.publicKey() }) ).signAndSend(); @@ -69,7 +70,36 @@ describe("Swap Contract Tests", function () { }; }); - it("calling `signAndSend()` too soon throws descriptive error", async function() { + it("calling `signAndSend()` too soon throws descriptive error", async function () { + const tx = await this.context.swapContractAsRoot.swap({ + a: this.context.alice.publicKey(), + b: this.context.bob.publicKey(), + token_a: this.context.tokenAId, + token_b: this.context.tokenBId, + amount_a: amountAToSwap, + min_a_for_b: amountAToSwap, + amount_b: amountBToSwap, + min_b_for_a: amountBToSwap, + }); + await expect(tx.signAndSend()) + .to.be.rejectedWith( + contract.AssembledTransaction.Errors.NeedsMoreSignatures, + ) + .then((error) => { + // Further assertions on the error object + expect(error).to.be.instanceOf( + contract.AssembledTransaction.Errors.NeedsMoreSignatures, + `error is not of type 'NeedsMoreSignaturesError'; instead it is of type '${error?.constructor.name}'`, + ); + + if (error) { + // Using regex to check the error message + expect(error.message).to.match(/needsNonInvokerSigningBy/); + } + }); + }); + + it("modified & re-simulated transactions show updated data", async function () { const tx = await this.context.swapContractAsRoot.swap({ a: this.context.alice.publicKey(), b: this.context.bob.publicKey(), @@ -80,16 +110,39 @@ describe("Swap Contract Tests", function () { amount_b: amountBToSwap, min_b_for_a: amountBToSwap, }); - await expect(tx.signAndSend()).to.be.rejectedWith(contract.AssembledTransaction.Errors.NeedsMoreSignatures).then((error) => { - // Further assertions on the error object - expect(error).to.be.instanceOf(contract.AssembledTransaction.Errors.NeedsMoreSignatures, - `error is not of type 'NeedsMoreSignaturesError'; instead it is of type '${error?.constructor.name}'`); + await tx.signAuthEntries({ + publicKey: this.context.alice.publicKey(), + ...contract.basicNodeSigner(this.context.alice, networkPassphrase), + }); + await tx.signAuthEntries({ + publicKey: this.context.bob.publicKey(), + ...contract.basicNodeSigner(this.context.bob, networkPassphrase), + }); - if (error) { - // Using regex to check the error message - expect(error.message).to.match(/needsNonInvokerSigningBy/); - } + const originalResourceFee = Number( + tx.simulationData.transactionData.resourceFee(), + ); + const bumpedResourceFee = originalResourceFee + 10000; + + tx.raw = TransactionBuilder.cloneFrom(tx.built, { + fee: tx.built.fee, + sorobanData: new SorobanDataBuilder( + tx.simulationData.transactionData.toXDR(), + ) + .setResourceFee( + xdr.Int64.fromString(bumpedResourceFee.toString()).toBigInt(), + ) + .build(), }); + + await tx.simulate(); + + const newSimulatedResourceFee = Number( + tx.simulationData.transactionData.resourceFee(), + ); + + expect(originalResourceFee).to.not.equal(newSimulatedResourceFee); + expect(newSimulatedResourceFee).to.be.greaterThan(bumpedResourceFee); }); it("modified & re-simulated transactions show updated data", async function () { @@ -113,17 +166,17 @@ describe("Swap Contract Tests", function () { }); const originalResourceFee = Number( - tx.simulationData.transactionData.resourceFee() + tx.simulationData.transactionData.resourceFee(), ); const bumpedResourceFee = originalResourceFee + 10000; tx.raw = TransactionBuilder.cloneFrom(tx.built, { fee: tx.built.fee, sorobanData: new SorobanDataBuilder( - tx.simulationData.transactionData.toXDR() + tx.simulationData.transactionData.toXDR(), ) .setResourceFee( - xdr.Int64.fromString(bumpedResourceFee.toString()).toBigInt() + xdr.Int64.fromString(bumpedResourceFee.toString()).toBigInt(), ) .build(), }); @@ -131,14 +184,14 @@ describe("Swap Contract Tests", function () { await tx.simulate(); const newSimulatedResourceFee = Number( - tx.simulationData.transactionData.resourceFee() + tx.simulationData.transactionData.resourceFee(), ); expect(originalResourceFee).to.not.equal(newSimulatedResourceFee); expect(newSimulatedResourceFee).to.be.greaterThan(bumpedResourceFee); }); - it("alice swaps bob 10 A for 1 B", async function() { + it("alice swaps bob 10 A for 1 B", async function () { const tx = await this.context.swapContractAsRoot.swap({ a: this.context.alice.publicKey(), b: this.context.bob.publicKey(), @@ -152,8 +205,12 @@ describe("Swap Contract Tests", function () { const needsNonInvokerSigningBy = await tx.needsNonInvokerSigningBy(); expect(needsNonInvokerSigningBy).to.have.lengthOf(2); - expect(needsNonInvokerSigningBy.indexOf(this.context.alice.publicKey())).to.equal(0, "needsNonInvokerSigningBy does not have alice's public key!"); - expect(needsNonInvokerSigningBy.indexOf(this.context.bob.publicKey())).to.equal(1, "needsNonInvokerSigningBy does not have bob's public key!"); + expect( + needsNonInvokerSigningBy.indexOf(this.context.alice.publicKey()), + ).to.equal(0, "needsNonInvokerSigningBy does not have alice's public key!"); + expect( + needsNonInvokerSigningBy.indexOf(this.context.bob.publicKey()), + ).to.equal(1, "needsNonInvokerSigningBy does not have bob's public key!"); // root serializes & sends to alice const xdrFromRoot = tx.toXDR(); @@ -184,16 +241,34 @@ describe("Swap Contract Tests", function () { await txRoot.simulate(); const result = await txRoot.signAndSend(); - expect(result).to.have.property('sendTransactionResponse'); - expect(result.sendTransactionResponse).to.have.property('status', 'PENDING'); - expect(result).to.have.property('getTransactionResponseAll').that.is.an('array').that.is.not.empty; - expect(result.getTransactionResponse).to.have.property('status').that.is.not.equal('FAILED'); - expect(result.getTransactionResponse).to.have.property('status', rpc.Api.GetTransactionStatus.SUCCESS); + expect(result).to.have.property("sendTransactionResponse"); + expect(result.sendTransactionResponse).to.have.property( + "status", + "PENDING", + ); + expect(result) + .to.have.property("getTransactionResponseAll") + .that.is.an("array").that.is.not.empty; + expect(result.getTransactionResponse) + .to.have.property("status") + .that.is.not.equal("FAILED"); + expect(result.getTransactionResponse).to.have.property( + "status", + rpc.Api.GetTransactionStatus.SUCCESS, + ); - const aliceTokenABalance = await this.context.tokenA.balance({ id: this.context.alice.publicKey() }); - const aliceTokenBBalance = await this.context.tokenB.balance({ id: this.context.alice.publicKey() }); - const bobTokenABalance = await this.context.tokenA.balance({ id: this.context.bob.publicKey() }); - const bobTokenBBalance = await this.context.tokenB.balance({ id: this.context.bob.publicKey() }); + const aliceTokenABalance = await this.context.tokenA.balance({ + id: this.context.alice.publicKey(), + }); + const aliceTokenBBalance = await this.context.tokenB.balance({ + id: this.context.alice.publicKey(), + }); + const bobTokenABalance = await this.context.tokenA.balance({ + id: this.context.bob.publicKey(), + }); + const bobTokenBBalance = await this.context.tokenB.balance({ + id: this.context.bob.publicKey(), + }); expect(aliceTokenABalance.result).to.equal(0n); expect(aliceTokenBBalance.result).to.equal(amountBToSwap); diff --git a/test/unit/server/soroban/assembled_transaction_test.js b/test/unit/server/soroban/assembled_transaction_test.js index 6b433fae6..35e966fb5 100644 --- a/test/unit/server/soroban/assembled_transaction_test.js +++ b/test/unit/server/soroban/assembled_transaction_test.js @@ -1,15 +1,10 @@ -const { - Account, - Keypair, - Networks, - rpc, - SorobanDataBuilder, - xdr, - contract, -} = StellarSdk; +const { Account, Keypair, Networks, rpc, SorobanDataBuilder, xdr, contract } = + StellarSdk; const { Server, AxiosClient, parseRawSimulation } = StellarSdk.rpc; -const restoreTxnData = StellarSdk.SorobanDataBuilder.fromXDR("AAAAAAAAAAAAAAAEAAAABgAAAAHZ4Y4l0GNoS97QH0fa5Jbbm61Ou3t9McQ09l7wREKJYwAAAA8AAAAJUEVSU19DTlQxAAAAAAAAAQAAAAYAAAAB2eGOJdBjaEve0B9H2uSW25utTrt7fTHENPZe8ERCiWMAAAAPAAAACVBFUlNfQ05UMgAAAAAAAAEAAAAGAAAAAdnhjiXQY2hL3tAfR9rkltubrU67e30xxDT2XvBEQoljAAAAFAAAAAEAAAAH+BoQswzzGTKRzrdC6axxKaM4qnyDP8wgQv8Id3S4pbsAAAAAAAAGNAAABjQAAAAAAADNoQ=="); +const restoreTxnData = StellarSdk.SorobanDataBuilder.fromXDR( + "AAAAAAAAAAAAAAAEAAAABgAAAAHZ4Y4l0GNoS97QH0fa5Jbbm61Ou3t9McQ09l7wREKJYwAAAA8AAAAJUEVSU19DTlQxAAAAAAAAAQAAAAYAAAAB2eGOJdBjaEve0B9H2uSW25utTrt7fTHENPZe8ERCiWMAAAAPAAAACVBFUlNfQ05UMgAAAAAAAAEAAAAGAAAAAdnhjiXQY2hL3tAfR9rkltubrU67e30xxDT2XvBEQoljAAAAFAAAAAEAAAAH+BoQswzzGTKRzrdC6axxKaM4qnyDP8wgQv8Id3S4pbsAAAAAAAAGNAAABjQAAAAAAADNoQ==", +); describe("AssembledTransaction.buildFootprintRestoreTransaction", () => { const keypair = Keypair.random(); @@ -17,13 +12,13 @@ describe("AssembledTransaction.buildFootprintRestoreTransaction", () => { const networkPassphrase = "Standalone Network ; February 2017"; const wallet = contract.basicNodeSigner(keypair, networkPassphrase); const options = { - networkPassphrase, - contractId, - rpcUrl: serverUrl, - allowHttp: true, - publicKey: keypair.publicKey(), - ...wallet, - } + networkPassphrase, + contractId, + rpcUrl: serverUrl, + allowHttp: true, + publicKey: keypair.publicKey(), + ...wallet, + }; beforeEach(function () { this.server = new Server(serverUrl); @@ -35,8 +30,6 @@ describe("AssembledTransaction.buildFootprintRestoreTransaction", () => { this.axiosMock.restore(); }); - - it("makes expected RPC calls", function (done) { const simulateTransactionResponse = { transactionData: restoreTxnData, @@ -46,10 +39,10 @@ describe("AssembledTransaction.buildFootprintRestoreTransaction", () => { }; const sendTransactionResponse = { - "status": "PENDING", - "hash": "05870e35fc94e5424f72d125959760b5f60631d91452bde2d11126fb5044e35d", - "latestLedger": 17034, - "latestLedgerCloseTime": "1716483573" + status: "PENDING", + hash: "05870e35fc94e5424f72d125959760b5f60631d91452bde2d11126fb5044e35d", + latestLedger: 17034, + latestLedgerCloseTime: "1716483573", }; const getTransactionResponse = { status: "SUCCESS", @@ -58,9 +51,11 @@ describe("AssembledTransaction.buildFootprintRestoreTransaction", () => { oldestLedger: 15598, oldestLedgerCloseTime: "1716482133", applicationOrder: 1, - envelopeXdr: "AAAAAgAAAAARwpJYOq4lKj/RdtS7ds3ciGSMfZUp+7d4xgg9vsN7qQABm0IAAAvWAAAAAwAAAAEAAAAAAAAAAAAAAABmT3cbAAAAAAAAAAEAAAAAAAAAGgAAAAAAAAABAAAAAAAAAAAAAAAEAAAABgAAAAHZ4Y4l0GNoS97QH0fa5Jbbm61Ou3t9McQ09l7wREKJYwAAAA8AAAAJUEVSU19DTlQxAAAAAAAAAQAAAAYAAAAB2eGOJdBjaEve0B9H2uSW25utTrt7fTHENPZe8ERCiWMAAAAPAAAACVBFUlNfQ05UMgAAAAAAAAEAAAAGAAAAAdnhjiXQY2hL3tAfR9rkltubrU67e30xxDT2XvBEQoljAAAAFAAAAAEAAAAH+BoQswzzGTKRzrdC6axxKaM4qnyDP8wgQv8Id3S4pbsAAAAAAAAGNAAABjQAAAAAAADNoQAAAAG+w3upAAAAQGBfsx+gyi/2Dh6i+7Vbb6Ongw3HDcFDZ48eoadkUUvkq97zdPe3wYGFswZgT5/GXPqGDBi+iqHuZiYx5eSy3Qk=", + envelopeXdr: + "AAAAAgAAAAARwpJYOq4lKj/RdtS7ds3ciGSMfZUp+7d4xgg9vsN7qQABm0IAAAvWAAAAAwAAAAEAAAAAAAAAAAAAAABmT3cbAAAAAAAAAAEAAAAAAAAAGgAAAAAAAAABAAAAAAAAAAAAAAAEAAAABgAAAAHZ4Y4l0GNoS97QH0fa5Jbbm61Ou3t9McQ09l7wREKJYwAAAA8AAAAJUEVSU19DTlQxAAAAAAAAAQAAAAYAAAAB2eGOJdBjaEve0B9H2uSW25utTrt7fTHENPZe8ERCiWMAAAAPAAAACVBFUlNfQ05UMgAAAAAAAAEAAAAGAAAAAdnhjiXQY2hL3tAfR9rkltubrU67e30xxDT2XvBEQoljAAAAFAAAAAEAAAAH+BoQswzzGTKRzrdC6axxKaM4qnyDP8wgQv8Id3S4pbsAAAAAAAAGNAAABjQAAAAAAADNoQAAAAG+w3upAAAAQGBfsx+gyi/2Dh6i+7Vbb6Ongw3HDcFDZ48eoadkUUvkq97zdPe3wYGFswZgT5/GXPqGDBi+iqHuZiYx5eSy3Qk=", resultXdr: "AAAAAAAAiRkAAAAAAAAAAQAAAAAAAAAaAAAAAAAAAAA=", - resultMetaXdr: "AAAAAwAAAAAAAAACAAAAAwAAQowAAAAAAAAAABHCklg6riUqP9F21Lt2zdyIZIx9lSn7t3jGCD2+w3upAAAAF0h1Pp0AAAvWAAAAAgAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAAAAMMQAAAABmTz9yAAAAAAAAAAEAAEKMAAAAAAAAAAARwpJYOq4lKj/RdtS7ds3ciGSMfZUp+7d4xgg9vsN7qQAAABdIdT6dAAAL1gAAAAMAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAAAQowAAAAAZk919wAAAAAAAAABAAAACAAAAAMAAAwrAAAACc4pIDe7y0sRFHAghrdpB7ypfj4BVuZStvX4u0BC1S/YAAANVgAAAAAAAAABAABCjAAAAAnOKSA3u8tLERRwIIa3aQe8qX4+AVbmUrb1+LtAQtUv2AAAQ7cAAAAAAAAAAwAADCsAAAAJikpmJa7Pr3lTb+dhRP2N4TOYCqK4tL4tQhDYnNEijtgAAA1WAAAAAAAAAAEAAEKMAAAACYpKZiWuz695U2/nYUT9jeEzmAqiuLS+LUIQ2JzRIo7YAABDtwAAAAAAAAADAAAMMQAAAAlT7LdEin/CaQA3iscHqkwnEFlSh8jfTPTIhSQ5J8Ao0wAADVwAAAAAAAAAAQAAQowAAAAJU+y3RIp/wmkAN4rHB6pMJxBZUofI30z0yIUkOSfAKNMAAEO3AAAAAAAAAAMAAAwxAAAACQycyCYjh7j9CHnTm9OKCYXhgmXw6jdtoMsGHyPk8Aa+AAANXAAAAAAAAAABAABCjAAAAAkMnMgmI4e4/Qh505vTigmF4YJl8Oo3baDLBh8j5PAGvgAAQ7cAAAAAAAAAAgAAAAMAAEKMAAAAAAAAAAARwpJYOq4lKj/RdtS7ds3ciGSMfZUp+7d4xgg9vsN7qQAAABdIdT6dAAAL1gAAAAMAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAAAQowAAAAAZk919wAAAAAAAAABAABCjAAAAAAAAAAAEcKSWDquJSo/0XbUu3bN3IhkjH2VKfu3eMYIPb7De6kAAAAXSHWDiQAAC9YAAAADAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAwAAAAAAAEKMAAAAAGZPdfcAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAA", + resultMetaXdr: + "AAAAAwAAAAAAAAACAAAAAwAAQowAAAAAAAAAABHCklg6riUqP9F21Lt2zdyIZIx9lSn7t3jGCD2+w3upAAAAF0h1Pp0AAAvWAAAAAgAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAAAAMMQAAAABmTz9yAAAAAAAAAAEAAEKMAAAAAAAAAAARwpJYOq4lKj/RdtS7ds3ciGSMfZUp+7d4xgg9vsN7qQAAABdIdT6dAAAL1gAAAAMAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAAAQowAAAAAZk919wAAAAAAAAABAAAACAAAAAMAAAwrAAAACc4pIDe7y0sRFHAghrdpB7ypfj4BVuZStvX4u0BC1S/YAAANVgAAAAAAAAABAABCjAAAAAnOKSA3u8tLERRwIIa3aQe8qX4+AVbmUrb1+LtAQtUv2AAAQ7cAAAAAAAAAAwAADCsAAAAJikpmJa7Pr3lTb+dhRP2N4TOYCqK4tL4tQhDYnNEijtgAAA1WAAAAAAAAAAEAAEKMAAAACYpKZiWuz695U2/nYUT9jeEzmAqiuLS+LUIQ2JzRIo7YAABDtwAAAAAAAAADAAAMMQAAAAlT7LdEin/CaQA3iscHqkwnEFlSh8jfTPTIhSQ5J8Ao0wAADVwAAAAAAAAAAQAAQowAAAAJU+y3RIp/wmkAN4rHB6pMJxBZUofI30z0yIUkOSfAKNMAAEO3AAAAAAAAAAMAAAwxAAAACQycyCYjh7j9CHnTm9OKCYXhgmXw6jdtoMsGHyPk8Aa+AAANXAAAAAAAAAABAABCjAAAAAkMnMgmI4e4/Qh505vTigmF4YJl8Oo3baDLBh8j5PAGvgAAQ7cAAAAAAAAAAgAAAAMAAEKMAAAAAAAAAAARwpJYOq4lKj/RdtS7ds3ciGSMfZUp+7d4xgg9vsN7qQAAABdIdT6dAAAL1gAAAAMAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAAAQowAAAAAZk919wAAAAAAAAABAABCjAAAAAAAAAAAEcKSWDquJSo/0XbUu3bN3IhkjH2VKfu3eMYIPb7De6kAAAAXSHWDiQAAC9YAAAADAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAwAAAAAAAEKMAAAAAGZPdfcAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAA", ledger: 17036, createdAt: "1716483575", }; @@ -72,49 +67,54 @@ describe("AssembledTransaction.buildFootprintRestoreTransaction", () => { jsonrpc: "2.0", id: 1, method: "simulateTransaction", - }) + }), ) - .returns(Promise.resolve({ data: { result: simulateTransactionResponse } })); + .returns( + Promise.resolve({ data: { result: simulateTransactionResponse } }), + ); this.axiosMock .expects("post") - .withArgs(serverUrl, + .withArgs( + serverUrl, sinon.match({ jsonrpc: "2.0", id: 1, method: "getTransaction", - }) + }), ) .returns(Promise.resolve({ data: { result: getTransactionResponse } })); this.axiosMock .expects("post") - .withArgs(serverUrl, + .withArgs( + serverUrl, sinon.match({ jsonrpc: "2.0", id: 1, method: "sendTransaction", - }) + }), ) .returns(Promise.resolve({ data: { result: sendTransactionResponse } })); - contract.AssembledTransaction.buildFootprintRestoreTransaction( - options, - restoreTxnData, - new StellarSdk.Account( - "GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI", - "1", - ), - 52641, - ) - .then((txn) => txn.signAndSend({ ...wallet })) - .then((result) => { - expect(result.getTransactionResponse.status).to.equal(rpc.Api.GetTransactionStatus.SUCCESS); - done(); - }) - .catch((error) => { - // handle any errors that occurred during the promise chain - done(error); - }); - - }) + contract.AssembledTransaction.buildFootprintRestoreTransaction( + options, + restoreTxnData, + new StellarSdk.Account( + "GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI", + "1", + ), + 52641, + ) + .then((txn) => txn.signAndSend({ ...wallet })) + .then((result) => { + expect(result.getTransactionResponse.status).to.equal( + rpc.Api.GetTransactionStatus.SUCCESS, + ); + done(); + }) + .catch((error) => { + // handle any errors that occurred during the promise chain + done(error); + }); + }); }); diff --git a/test/unit/server/soroban/simulate_transaction_test.js b/test/unit/server/soroban/simulate_transaction_test.js index 863ad6b13..a48817877 100644 --- a/test/unit/server/soroban/simulate_transaction_test.js +++ b/test/unit/server/soroban/simulate_transaction_test.js @@ -18,12 +18,11 @@ describe("Server#simulateTransaction", async function (done) { let contract = new StellarSdk.Contract(contractId); let address = contract.address().toScAddress(); - const accountId = - "GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI"; + const accountId = "GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI"; const accountKey = xdr.LedgerKey.account( - new xdr.LedgerKeyAccount({ - accountId: Keypair.fromPublicKey(accountId).xdrPublicKey(), - }), + new xdr.LedgerKeyAccount({ + accountId: Keypair.fromPublicKey(accountId).xdrPublicKey(), + }), ); const simulationResponse = await invokeSimulationResponse(address); @@ -192,48 +191,48 @@ describe("Server#simulateTransaction", async function (done) { it("works with state changes", async function () { return invokeSimulationResponseWithStateChanges(address).then( - (simResponse) => { - const expected = cloneSimulation(parsedSimulationResponse); - expected.stateChanges = [ - { - type: 2, - key: accountKey, - before: new xdr.LedgerEntry({ - lastModifiedLedgerSeq: 0, - data: new xdr.LedgerEntryData(), - ext: new xdr.LedgerEntryExt(), - }), - after: new xdr.LedgerEntry({ - lastModifiedLedgerSeq: 0, - data: new xdr.LedgerEntryData(), - ext: new xdr.LedgerEntryExt(), - }), - }, - { - type: 1, - key: accountKey, - before: null, - after: new xdr.LedgerEntry({ - lastModifiedLedgerSeq: 0, - data: new xdr.LedgerEntryData(), - ext: new xdr.LedgerEntryExt(), - }), - }, - { - type: 3, - key: accountKey, - before: new xdr.LedgerEntry({ - lastModifiedLedgerSeq: 0, - data: new xdr.LedgerEntryData(), - ext: new xdr.LedgerEntryExt(), - }), - after: null, - }, - ] + (simResponse) => { + const expected = cloneSimulation(parsedSimulationResponse); + expected.stateChanges = [ + { + type: 2, + key: accountKey, + before: new xdr.LedgerEntry({ + lastModifiedLedgerSeq: 0, + data: new xdr.LedgerEntryData(), + ext: new xdr.LedgerEntryExt(), + }), + after: new xdr.LedgerEntry({ + lastModifiedLedgerSeq: 0, + data: new xdr.LedgerEntryData(), + ext: new xdr.LedgerEntryExt(), + }), + }, + { + type: 1, + key: accountKey, + before: null, + after: new xdr.LedgerEntry({ + lastModifiedLedgerSeq: 0, + data: new xdr.LedgerEntryData(), + ext: new xdr.LedgerEntryExt(), + }), + }, + { + type: 3, + key: accountKey, + before: new xdr.LedgerEntry({ + lastModifiedLedgerSeq: 0, + data: new xdr.LedgerEntryData(), + ext: new xdr.LedgerEntryExt(), + }), + after: null, + }, + ]; - const parsed = parseRawSimulation(simResponse); - expect(parsed).to.be.deep.equal(expected); - }, + const parsed = parseRawSimulation(simResponse); + expect(parsed).to.be.deep.equal(expected); + }, ); }); @@ -341,9 +340,9 @@ function baseSimulationResponse(results) { { type: 2, key: xdr.LedgerKey.account( - new xdr.LedgerKeyAccount({ - accountId: Keypair.fromPublicKey(accountId).xdrPublicKey(), - }), + new xdr.LedgerKeyAccount({ + accountId: Keypair.fromPublicKey(accountId).xdrPublicKey(), + }), ).toXDR("base64"), before: new xdr.LedgerEntry({ lastModifiedLedgerSeq: 0, @@ -355,7 +354,7 @@ function baseSimulationResponse(results) { data: new xdr.LedgerEntryData(), ext: new xdr.LedgerEntryExt(), }).toXDR("base64"), - } + }, ], }; } @@ -374,15 +373,14 @@ async function invokeSimulationResponseWithStateChanges(address) { const accountId = "GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI"; return { - ...(await invokeSimulationResponse(address)), stateChanges: [ { type: 2, key: xdr.LedgerKey.account( - new xdr.LedgerKeyAccount({ - accountId: Keypair.fromPublicKey(accountId).xdrPublicKey(), - }), + new xdr.LedgerKeyAccount({ + accountId: Keypair.fromPublicKey(accountId).xdrPublicKey(), + }), ).toXDR("base64"), before: new xdr.LedgerEntry({ lastModifiedLedgerSeq: 0, @@ -398,9 +396,9 @@ async function invokeSimulationResponseWithStateChanges(address) { { type: 1, key: xdr.LedgerKey.account( - new xdr.LedgerKeyAccount({ - accountId: Keypair.fromPublicKey(accountId).xdrPublicKey(), - }), + new xdr.LedgerKeyAccount({ + accountId: Keypair.fromPublicKey(accountId).xdrPublicKey(), + }), ).toXDR("base64"), before: null, after: new xdr.LedgerEntry({ @@ -412,9 +410,9 @@ async function invokeSimulationResponseWithStateChanges(address) { { type: 3, key: xdr.LedgerKey.account( - new xdr.LedgerKeyAccount({ - accountId: Keypair.fromPublicKey(accountId).xdrPublicKey(), - }), + new xdr.LedgerKeyAccount({ + accountId: Keypair.fromPublicKey(accountId).xdrPublicKey(), + }), ).toXDR("base64"), before: new xdr.LedgerEntry({ lastModifiedLedgerSeq: 0, @@ -427,7 +425,6 @@ async function invokeSimulationResponseWithStateChanges(address) { }; } - describe("works with real responses", function () { const schema = { transactionData: