From c814b0d4bc5d632507173c19d339dbc6c048ff74 Mon Sep 17 00:00:00 2001 From: Blaine Heffron Date: Mon, 10 Jun 2024 18:23:07 -0400 Subject: [PATCH 01/13] linter adjustments for nitpicky things and linter autofixes for others --- config/tsconfig.json | 5 +- package.json | 2 +- src/.eslintrc.js | 10 + src/browser.ts | 4 +- src/config.ts | 4 +- src/contract/spec.ts | 252 +++++++++--------- src/errors.ts | 3 + src/federation/server.ts | 4 +- src/horizon/account_call_builder.ts | 4 +- src/horizon/account_response.ts | 22 ++ src/horizon/assets_call_builder.ts | 4 +- src/horizon/call_builder.ts | 13 +- .../claimable_balances_call_builder.ts | 4 +- src/horizon/effect_call_builder.ts | 4 +- src/horizon/horizon_axios_client.ts | 2 +- src/horizon/ledger_call_builder.ts | 4 +- src/horizon/liquidity_pool_call_builder.ts | 4 +- src/horizon/offer_call_builder.ts | 4 +- src/horizon/operation_call_builder.ts | 4 +- src/horizon/path_call_builder.ts | 2 +- src/horizon/payment_call_builder.ts | 4 +- src/horizon/server.ts | 16 +- 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 | 4 +- src/horizon/trades_call_builder.ts | 4 +- src/horizon/transaction_call_builder.ts | 4 +- src/horizon/types/assets.ts | 2 +- src/horizon/types/effects.ts | 2 +- src/horizon/types/offer.ts | 2 +- src/rpc/api.ts | 6 +- src/rpc/axios.ts | 1 + src/rpc/browser.ts | 6 +- src/rpc/parsers.ts | 18 +- src/rpc/server.ts | 8 +- src/rpc/transaction.ts | 2 +- src/webauth/utils.ts | 10 +- 38 files changed, 246 insertions(+), 203 deletions(-) diff --git a/config/tsconfig.json b/config/tsconfig.json index 76fdf22b3..12b4ea6b7 100644 --- a/config/tsconfig.json +++ b/config/tsconfig.json @@ -11,5 +11,8 @@ "outDir": "../lib", "target": "es6" }, - "include": ["../src"] + "include": [ + "../src", + "../src/.eslintrc.js" + ] } \ No newline at end of file diff --git a/package.json b/package.json index 3d1cc2e23..6cc182c92 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "test:node": "yarn _nyc mocha --recursive 'test/unit/**/*.js'", "test:integration": "yarn _nyc mocha --recursive 'test/integration/**/*.js'", "test:browser": "karma start config/karma.conf.js", - "fmt": "yarn eslint -c .eslintrc.js src/ --fix && yarn _prettier", + "fmt": "yarn eslint -c src/.eslintrc.js src/ --fix && yarn _prettier", "preversion": "yarn clean && yarn _prettier && yarn build:prod && yarn test", "prepare": "yarn build:prod", "_build": "yarn build:node && yarn build:test && yarn build:browser", diff --git a/src/.eslintrc.js b/src/.eslintrc.js index 97b3cc934..5754b371a 100644 --- a/src/.eslintrc.js +++ b/src/.eslintrc.js @@ -14,17 +14,27 @@ module.exports = { "import/prefer-default-export": 0, "node/no-unsupported-features/es-syntax": 0, "node/no-unsupported-features/es-builtins": 0, + "no-proto": 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", + "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, + "spaced-comment": 0, // WARN "arrow-body-style": 1, diff --git a/src/browser.ts b/src/browser.ts index 6befec18d..cccf17356 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -1,9 +1,9 @@ /* tslint:disable:no-var-requires */ +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/spec.ts b/src/contract/spec.ts index 937875fbb..6a438cc35 100644 --- a/src/contract/spec.ts +++ b/src/contract/spec.ts @@ -15,8 +15,8 @@ export interface Union { } 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}`); } @@ -62,7 +62,7 @@ export class Spec { if (entries.length == 0) { throw new Error("Contract spec must have at least one entry"); } - let entry = entries[0]; + const entry = entries[0]; if (typeof entry === "string") { this.entries = (entries as string[]).map((s) => xdr.ScSpecEntry.fromXDR(s, "base64"), @@ -87,6 +87,7 @@ export class Spec { ) .map((entry) => entry.functionV0()); } + /** * Gets the XDR function spec for the given function name. * @@ -96,7 +97,7 @@ export class Spec { * @throws {Error} if no function with the given name exists */ getFunc(name: string): xdr.ScSpecFunctionV0 { - let entry = this.findEntry(name); + const entry = this.findEntry(name); if ( entry.switch().value !== xdr.ScSpecEntryKind.scSpecEntryFunctionV0().value ) { @@ -109,7 +110,7 @@ export class Spec { * 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 + * @param {object} args the arguments object * @returns {xdr.ScVal[]} the converted arguments * * @throws {Error} if argument is missing or incorrect type @@ -124,7 +125,7 @@ export class Spec { * ``` */ funcArgsToScVals(name: string, args: object): xdr.ScVal[] { - let fn = this.getFunc(name); + const fn = this.getFunc(name); return fn .inputs() .map((input) => this.nativeToScVal(readObj(args, input), input.type())); @@ -146,14 +147,14 @@ export class Spec { * ``` */ funcResToNative(name: string, val_or_base64: xdr.ScVal | string): any { - let val = + const 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(); + const func = this.getFunc(name); + const outputs = func.outputs(); if (outputs.length === 0) { - let type = val.switch(); + const type = val.switch(); if (type.value !== xdr.ScValType.scvVoid().value) { throw new Error(`Expected void, got ${type.name}`); } @@ -162,7 +163,7 @@ export class Spec { if (outputs.length > 1) { throw new Error(`Multiple outputs not supported`); } - let output = outputs[0]; + const output = outputs[0]; if (output.switch().value === xdr.ScSpecType.scSpecTypeResult().value) { return new Ok(this.scValToNative(val, output.result().okType())); } @@ -178,7 +179,7 @@ export class Spec { * @throws {Error} if no entry with the given name exists */ findEntry(name: string): xdr.ScSpecEntry { - let entry = this.entries.find( + const entry = this.entries.find( (entry) => entry.value().name().toString() === name, ); if (!entry) { @@ -197,14 +198,14 @@ export class Spec { * @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; + const t: xdr.ScSpecType = ty.switch(); + const value = t.value; if (t.value === xdr.ScSpecType.scSpecTypeUdt().value) { - let udt = ty.udt(); + const udt = ty.udt(); return this.nativeToUdt(val, udt.name().toString()); } if (value === xdr.ScSpecType.scSpecTypeOption().value) { - let opt = ty.option(); + const opt = ty.option(); if (val === undefined) { return xdr.ScVal.scvVoid(); } @@ -249,7 +250,7 @@ export class Spec { const copy = Uint8Array.from(val); switch (value) { case xdr.ScSpecType.scSpecTypeBytesN().value: { - let bytes_n = ty.bytesN(); + const bytes_n = ty.bytesN(); if (copy.length !== bytes_n.n()) { throw new TypeError( `expected ${bytes_n.n()} bytes, but got ${copy.length}`, @@ -270,15 +271,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,13 +290,13 @@ 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); + const key = this.nativeToScVal(entry[0], keyType); + const val = this.nativeToScVal(entry[1], valueType); return new xdr.ScMapEntry({ key, val }); }), ); @@ -311,15 +312,15 @@ 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()); + const [k, v] = res.value; + const key = this.nativeToScVal(k, scMap.keyType()); + const val = this.nativeToScVal(v, scMap.valueType()); entries.push(new xdr.ScMapEntry({ key, val })); res = values.next(); } @@ -391,7 +392,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,28 +414,28 @@ 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(); + const entry_name = val.tag; + const case_ = union_.cases().find((entry) => { + const case_ = entry.value().name().toString(); return case_ === entry_name; }); if (!case_) { throw new TypeError(`no such enum entry: ${entry_name} in ${union_}`); } - let key = xdr.ScVal.scvSymbol(entry_name); + const key = xdr.ScVal.scvSymbol(entry_name); switch (case_.switch()) { case xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseVoidV0(): { return xdr.ScVal.scvVec([key]); } case xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseTupleV0(): { - let types = case_.tupleCase().type(); + const types = case_.tupleCase().type(); if (Array.isArray(val.values)) { 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); @@ -448,7 +449,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 +462,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,8 +501,8 @@ 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()); } @@ -526,13 +527,13 @@ export class Spec { case xdr.ScValType.scvVec().value: { if (value == xdr.ScSpecType.scSpecTypeVec().value) { - let vec = typeDef.vec(); + 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 +545,12 @@ export class Spec { return Address.fromScVal(scv).toString() as T; case xdr.ScValType.scvMap().value: { - let map = scv.map() ?? []; + const 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 type_ = typeDef.map(); + const keyType = type_.keyType(); + const valueType = type_.valueType(); + const res = map.map((entry) => [ this.scValToNative(entry.key(), keyType), this.scValToNative(entry.val(), valueType), ]) as T; @@ -603,7 +604,7 @@ export class Spec { } private scValUdtToNative(scv: xdr.ScVal, udt: xdr.ScSpecTypeUdt): any { - let entry = this.findEntry(udt.name().toString()); + const entry = this.findEntry(udt.name().toString()); switch (entry.switch()) { case xdr.ScSpecEntryKind.scSpecEntryUdtEnumV0(): return this.enumToNative(scv); @@ -619,7 +620,7 @@ export class Spec { } private unionToNative(val: xdr.ScVal, udt: xdr.ScSpecUdtUnionV0): any { - let vec = val.vec(); + const vec = val.vec(); if (!vec) { throw new Error(`${JSON.stringify(val, null, 2)} is not a vec`); } @@ -628,39 +629,40 @@ export class Spec { `${val} has length 0, but the there are at least one case in the union`, ); } - let name = vec[0].sym().toString(); + const 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)); + const 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 }; + const 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)); + const tuple = entry.tupleCase(); + const ty = tuple.type(); + const 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(); + const res: any = {}; + const fields = udt.fields(); if (fields.some(isNumeric)) { - let r = val + const r = val .vec() ?.map((entry, i) => this.scValToNative(entry, fields[i].type())); return r; } val.map()?.forEach((entry, i) => { - let field = fields[i]; + const field = fields[i]; res[field.name().toString()] = this.scValToNative( entry.val(), field.type(), @@ -673,7 +675,7 @@ export class Spec { if (scv.switch().value !== xdr.ScValType.scvU32().value) { throw new Error(`Enum must have a u32 value`); } - let num = scv.u32(); + const num = scv.u32(); return num; } @@ -704,27 +706,27 @@ export class Spec { * @throws {Error} if the contract spec is invalid */ jsonSchema(funcName?: string): JSONSchema7 { - let definitions: { [key: string]: JSONSchema7Definition } = {}; - for (let entry of this.entries) { + const definitions: { [key: string]: JSONSchema7Definition } = {}; + for (const entry of this.entries) { switch (entry.switch().value) { case xdr.ScSpecEntryKind.scSpecEntryUdtEnumV0().value: { - let udt = entry.udtEnumV0(); + const udt = entry.udtEnumV0(); definitions[udt.name().toString()] = enumToJsonSchema(udt); break; } case xdr.ScSpecEntryKind.scSpecEntryUdtStructV0().value: { - let udt = entry.udtStructV0(); + const udt = entry.udtStructV0(); definitions[udt.name().toString()] = structToJsonSchema(udt); break; } case xdr.ScSpecEntryKind.scSpecEntryUdtUnionV0().value: - let udt = entry.udtUnionV0(); + const 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); + const fn = entry.functionV0(); + const fnName = fn.name().toString(); + const { input } = functionToJsonSchema(fn); // @ts-ignore definitions[fnName] = input; break; @@ -734,12 +736,12 @@ export class Spec { } } } - let res: JSONSchema7 = { + const res: JSONSchema7 = { $schema: "http://json-schema.org/draft-07/schema#", definitions: { ...PRIMITIVE_DEFINITONS, ...definitions }, }; if (funcName) { - res["$ref"] = `#/definitions/${funcName}`; + res.$ref = `#/definitions/${funcName}`; } return res; } @@ -752,7 +754,7 @@ function stringToScVal(str: string, ty: xdr.ScSpecType): xdr.ScVal { case xdr.ScSpecType.scSpecTypeSymbol().value: return xdr.ScVal.scvSymbol(str); case xdr.ScSpecType.scSpecTypeAddress().value: { - let addr = Address.fromString(str as string); + const addr = Address.fromString(str as string); return xdr.ScVal.scvAddress(addr.toScAddress()); } case xdr.ScSpecType.scSpecTypeU64().value: @@ -784,11 +786,11 @@ function findCase(name: string) { return function matches(entry: xdr.ScSpecUdtUnionCaseV0) { switch (entry.switch().value) { case xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseTupleV0().value: { - let tuple = entry.tupleCase(); + const tuple = entry.tupleCase(); return tuple.name().toString() === name; } case xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseVoidV0().value: { - let void_case = entry.voidCase(); + const void_case = entry.voidCase(); return void_case.name().toString() === name; } default: @@ -869,8 +871,8 @@ const PRIMITIVE_DEFINITONS: { [key: string]: JSONSchema7Definition } = { * @returns {JSONSchema7} a schema describing the type */ function typeRef(typeDef: xdr.ScSpecTypeDef): JSONSchema7 { - let t = typeDef.switch(); - let value = t.value; + const t = typeDef.switch(); + const value = t.value; let ref; switch (value) { case xdr.ScSpecType.scSpecTypeVal().value: { @@ -946,7 +948,7 @@ function typeRef(typeDef: xdr.ScSpecTypeDef): JSONSchema7 { break; } case xdr.ScSpecType.scSpecTypeOption().value: { - let opt = typeDef.option(); + const opt = typeDef.option(); return typeRef(opt.valueType()); } case xdr.ScSpecType.scSpecTypeResult().value: { @@ -954,16 +956,16 @@ function typeRef(typeDef: xdr.ScSpecTypeDef): JSONSchema7 { break; } case xdr.ScSpecType.scSpecTypeVec().value: { - let arr = typeDef.vec(); - let ref = typeRef(arr.elementType()); + const arr = typeDef.vec(); + const ref = typeRef(arr.elementType()); return { type: "array", items: ref, }; } case xdr.ScSpecType.scSpecTypeMap().value: { - let map = typeDef.map(); - let items = [typeRef(map.keyType()), typeRef(map.valueType())]; + const map = typeDef.map(); + const items = [typeRef(map.keyType()), typeRef(map.valueType())]; return { type: "array", items: { @@ -975,21 +977,21 @@ function typeRef(typeDef: xdr.ScSpecTypeDef): JSONSchema7 { }; } case xdr.ScSpecType.scSpecTypeTuple().value: { - let tuple = typeDef.tuple(); - let minItems = tuple.valueTypes().length; - let maxItems = minItems; - let items = tuple.valueTypes().map(typeRef); + 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: { - let arr = typeDef.bytesN(); + const arr = typeDef.bytesN(); return { $ref: "#/definitions/DataUrl", maxLength: arr.n(), }; } case xdr.ScSpecType.scSpecTypeUdt().value: { - let udt = typeDef.udt(); + const udt = typeDef.udt(); ref = udt.name().toString(); break; } @@ -1004,14 +1006,14 @@ function isRequired(typeDef: xdr.ScSpecTypeDef): boolean { } function structToJsonSchema(udt: xdr.ScSpecUdtStructV0): object { - let fields = udt.fields(); + 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", ); } - let items = fields.map((_, i) => typeRef(fields[i].type())); + const items = fields.map((_, i) => typeRef(fields[i].type())); return { type: "array", items, @@ -1019,9 +1021,9 @@ function structToJsonSchema(udt: xdr.ScSpecUdtStructV0): object { maxItems: fields.length, }; } - let description = udt.doc().toString(); - let { properties, required }: any = args_and_required(fields); - properties["additionalProperties"] = false; + const description = udt.doc().toString(); + const { properties, required }: any = args_and_required(fields); + properties.additionalProperties = false; return { description, properties, @@ -1033,17 +1035,17 @@ function structToJsonSchema(udt: xdr.ScSpecUdtStructV0): 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(); + const properties: any = {}; + const required: string[] = []; + for (const arg of input) { + const type_ = arg.type(); + const name = arg.name().toString(); properties[name] = typeRef(type_); if (isRequired(type_)) { required.push(name); } } - let res: { properties: object; required?: string[] } = { properties }; + const res: { properties: object; required?: string[] } = { properties }; if (required.length > 0) { res.required = required; } @@ -1051,8 +1053,8 @@ function args_and_required( } function functionToJsonSchema(func: xdr.ScSpecFunctionV0): Func { - let { properties, required }: any = args_and_required(func.inputs()); - let args: any = { + const { properties, required }: any = args_and_required(func.inputs()); + const args: any = { additionalProperties: false, properties, type: "object", @@ -1060,17 +1062,17 @@ function functionToJsonSchema(func: xdr.ScSpecFunctionV0): Func { if (required?.length > 0) { args.required = required; } - let input: Partial = { + const input: Partial = { properties: { args, }, }; - let outputs = func.outputs(); - let output: Partial = + const outputs = func.outputs(); + const output: Partial = outputs.length > 0 ? typeRef(outputs[0]) : typeRef(xdr.ScSpecTypeDef.scSpecTypeVoid()); - let description = func.doc().toString(); + const description = func.doc().toString(); if (description.length > 0) { input.description = description; } @@ -1083,14 +1085,14 @@ function functionToJsonSchema(func: xdr.ScSpecFunctionV0): Func { } function unionToJsonSchema(udt: xdr.ScSpecUdtUnionV0): any { - let description = udt.doc().toString(); - let cases = udt.cases(); - let oneOf: any[] = []; - for (let case_ of cases) { + const description = udt.doc().toString(); + const cases = udt.cases(); + const oneOf: any[] = []; + for (const case_ of cases) { switch (case_.switch().value) { case xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseVoidV0().value: { - let c = case_.voidCase(); - let title = c.name().toString(); + const c = case_.voidCase(); + const title = c.name().toString(); oneOf.push({ type: "object", title, @@ -1103,8 +1105,8 @@ function unionToJsonSchema(udt: xdr.ScSpecUdtUnionV0): any { break; } case xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseTupleV0().value: { - let c = case_.tupleCase(); - let title = c.name().toString(); + const c = case_.tupleCase(); + const title = c.name().toString(); oneOf.push({ type: "object", title, @@ -1122,7 +1124,7 @@ function unionToJsonSchema(udt: xdr.ScSpecUdtUnionV0): any { } } - let res: any = { + const res: any = { oneOf, }; if (description.length > 0) { @@ -1132,12 +1134,12 @@ function unionToJsonSchema(udt: xdr.ScSpecUdtUnionV0): any { } 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(); + const description = udt.doc().toString(); + const cases = udt.cases(); + const oneOf: any[] = []; + for (const case_ of cases) { + const title = case_.name().toString(); + const description = case_.doc().toString(); oneOf.push({ description, title, @@ -1146,7 +1148,7 @@ function enumToJsonSchema(udt: xdr.ScSpecUdtEnumV0): any { }); } - let res: any = { oneOf }; + const res: any = { oneOf }; if (description.length > 0) { res.description = description; } diff --git a/src/errors.ts b/src/errors.ts index b84f72162..38a70e85e 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -10,6 +10,7 @@ export class NetworkError extends Error { statusText?: string; url?: string; }; + public __proto__: NetworkError; constructor(message: string, response: any) { @@ -72,10 +73,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..c0c6412be 100644 --- a/src/federation/server.ts +++ b/src/federation/server.ts @@ -15,7 +15,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 +30,7 @@ export class FederationServer { * @memberof FederationServer */ private readonly serverURL: URI; // TODO: public or private? readonly? + /** * Domain this server represents. * @@ -37,6 +38,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..de103381c 100644 --- a/src/horizon/account_response.ts +++ b/src/horizon/account_response.ts @@ -17,39 +17,61 @@ import { ServerApi } from "./server_api"; */ export class AccountResponse { public readonly id!: string; + public readonly paging_token!: string; + public readonly account_id!: string; + public sequence!: string; + public readonly sequence_ledger?: number; + public readonly sequence_time?: string; + public readonly subentry_count!: number; + public readonly home_domain?: string; + public readonly inflation_destination?: string; + public readonly last_modified_ledger!: number; + public readonly last_modified_time!: string; + public readonly thresholds!: HorizonApi.AccountThresholds; + public readonly flags!: HorizonApi.Flags; + public readonly balances!: HorizonApi.BalanceLine[]; + public readonly signers!: ServerApi.AccountRecordSigners[]; + public readonly data!: (options: { value: string; }) => Promise<{ value: string }>; + public readonly data_attr!: Record; + public readonly effects!: ServerApi.CallCollectionFunction< ServerApi.EffectRecord >; + public readonly offers!: ServerApi.CallCollectionFunction< ServerApi.OfferRecord >; + public readonly operations!: ServerApi.CallCollectionFunction< ServerApi.OperationRecord >; + public readonly payments!: ServerApi.CallCollectionFunction< ServerApi.PaymentOperationRecord >; + public readonly trades!: ServerApi.CallCollectionFunction< ServerApi.TradeRecord >; + private readonly _baseAccount: BaseAccount; constructor(response: ServerApi.AccountRecord) { 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..7710f3b0a 100644 --- a/src/horizon/call_builder.ts +++ b/src/horizon/call_builder.ts @@ -20,7 +20,7 @@ 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 ?? +const EventSource: Constructable = anyGlobal.EventSource ?? anyGlobal.window?.EventSource ?? require("eventsource"); @@ -38,8 +38,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 +85,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(); @@ -274,7 +277,7 @@ export class CallBuilder< * @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 + * @returns {Function} A function that requests the link */ private _requestFnForLink(link: HorizonApi.ResponseLink): (opts?: any) => any { return async (opts: any = {}) => { diff --git a/src/horizon/claimable_balances_call_builder.ts b/src/horizon/claimable_balances_call_builder.ts index bf515702a..019edbd71 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< 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..9fe3b279a 100644 --- a/src/horizon/horizon_axios_client.ts +++ b/src/horizon/horizon_axios_client.ts @@ -35,7 +35,7 @@ function _toSeconds(ms: number): number { } 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()); 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..be38666be 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< 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..debdf6331 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< 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 48c92106d..d36dd00f1 100644 --- a/src/horizon/server.ts +++ b/src/horizon/server.ts @@ -57,7 +57,7 @@ function _getAmountInLumens(amt: BigNumber) { /** * 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. @@ -131,8 +131,8 @@ 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 {bool} [_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 * (with the shape `{ minTime: 0, maxTime: N }`) that you can set the `timebounds` option to. @@ -162,7 +162,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); } /** @@ -377,8 +377,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()}`, ); } @@ -488,9 +488,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) { 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..b42b05372 100644 --- a/src/horizon/trade_aggregation_call_builder.ts +++ b/src/horizon/trade_aggregation_call_builder.ts @@ -19,8 +19,8 @@ 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 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..39071818c 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< 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..03f6d407d 100644 --- a/src/rpc/api.ts +++ b/src/rpc/api.ts @@ -26,7 +26,8 @@ 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; @@ -347,7 +348,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..5c98be46f 100644 --- a/src/rpc/axios.ts +++ b/src/rpc/axios.ts @@ -2,6 +2,7 @@ import axios from 'axios'; /* tslint:disable-next-line:no-var-requires */ 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..6f321c2e4 100644 --- a/src/rpc/browser.ts +++ b/src/rpc/browser.ts @@ -1,9 +1,9 @@ /* tslint:disable:no-var-requires */ -export * from './index'; -export * as StellarBase from '@stellar/stellar-base'; - import axios from 'axios'; // idk why axios is weird + +export * from './index'; +export * as StellarBase from '@stellar/stellar-base'; export { axios }; export default module.exports; diff --git a/src/rpc/parsers.ts b/src/rpc/parsers.ts index a54a83434..3ebbcf21a 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, ...( @@ -94,7 +94,7 @@ export function parseRawSimulation( } // shared across all responses - let base: Api.BaseSimulateTransactionResponse = { + const base: Api.BaseSimulateTransactionResponse = { _parsed: true, id: sim.id, latestLedger: sim.latestLedger, @@ -128,28 +128,24 @@ function parseSuccessful( ...// coalesce 0-or-1-element results[] list into a single result struct // with decoded fields if present ((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] }), ...(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, - }; - }) + })) } }; diff --git a/src/rpc/server.ts b/src/rpc/server.ts index e3c518efb..f0fa7e7be 100644 --- a/src/rpc/server.ts +++ b/src/rpc/server.ts @@ -56,7 +56,7 @@ export namespace Server { * 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`). @@ -158,7 +158,7 @@ export class Server { * data to load as a strkey (`C...` form), a {@link Contract}, or an * {@link Address} instance * @param {xdr.ScVal} key the key of the contract data to load - * @param {Durability} [durability=Durability.Persistent] the "durability + * @param {Durability} [durability] the "durability * keyspace" that this ledger key belongs to, which is either 'temporary' * or 'persistent' (the default), see {@link Durability}. * @@ -209,7 +209,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, @@ -515,7 +515,7 @@ export class Server { * }); */ public async getNetwork(): Promise { - return await jsonrpc.postObject(this.serverURL.toString(), 'getNetwork'); + return jsonrpc.postObject(this.serverURL.toString(), 'getNetwork'); } /** diff --git a/src/rpc/transaction.ts b/src/rpc/transaction.ts index c75f333b4..c48162f24 100644 --- a/src/rpc/transaction.ts +++ b/src/rpc/transaction.ts @@ -47,7 +47,7 @@ export function assembleTransaction( ); } - let success = parseRawSimulation(simulation); + const success = parseRawSimulation(simulation); if (!Api.isSimulationSuccess(success)) { throw new Error(`simulation incorrect: ${JSON.stringify(success)}`); } diff --git a/src/webauth/utils.ts b/src/webauth/utils.ts index 3e6d13c73..852994b21 100644 --- a/src/webauth/utils.ts +++ b/src/webauth/utils.ts @@ -29,7 +29,7 @@ import { ServerApi } from "../horizon/server_api"; * (M...) that the wallet wishes to authenticate with the server. * @param {string} homeDomain The fully qualified domain name of the service * requiring authentication - * @param {number} [timeout=300] Challenge duration (default to 5 minutes). + * @param {number} [timeout] Challenge duration (default to 5 minutes). * @param {string} networkPassphrase The network passphrase. If you pass this * argument then timeout is required. * @param {string} webAuthDomain The fully qualified domain name of the service @@ -572,8 +572,8 @@ export function verifyChallengeTxSigners( serverKP = Keypair.fromPublicKey(serverAccountID); // can throw 'Invalid Stellar public key' } catch (err: any) { throw new Error( - "Couldn't infer keypair from the provided 'serverAccountID': " + - err.message, + `Couldn't infer keypair from the provided 'serverAccountID': ${ + err.message}`, ); } @@ -645,7 +645,7 @@ export function verifyChallengeTxSigners( // Confirm we matched a signature to the server signer. if (!serverSignatureFound) { throw new InvalidChallengeError( - "Transaction not signed by server: '" + serverKP.publicKey() + "'", + `Transaction not signed by server: '${ serverKP.publicKey() }'`, ); } @@ -751,7 +751,7 @@ export function gatherTxSigners( keypair = Keypair.fromPublicKey(signer); // This can throw a few different errors } catch (err: any) { throw new InvalidChallengeError( - "Signer is not a valid address: " + err.message, + `Signer is not a valid address: ${ err.message}`, ); } From 14db848df4d5662ff381caa8ad138ab7ef943c28 Mon Sep 17 00:00:00 2001 From: Blaine Heffron Date: Mon, 10 Jun 2024 20:36:02 -0400 Subject: [PATCH 02/13] fix errors in browser and spec --- src/browser.ts | 1 + src/contract/spec.ts | 2108 +++++++++++++++++++++--------------------- 2 files changed, 1058 insertions(+), 1051 deletions(-) diff --git a/src/browser.ts b/src/browser.ts index cccf17356..2f6034b93 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -1,4 +1,5 @@ /* tslint:disable:no-var-requires */ +/* eslint import/no-import-module-exports: 0 */ import axios from "axios"; // idk why axios is weird diff --git a/src/contract/spec.ts b/src/contract/spec.ts index 6a438cc35..01f8f841d 100644 --- a/src/contract/spec.ts +++ b/src/contract/spec.ts @@ -1,3 +1,5 @@ +/* eslint-disable no-fallthrough */ +/* eslint-disable default-case */ import type { JSONSchema7, JSONSchema7Definition } from "json-schema"; import { ScIntType, @@ -14,1143 +16,1147 @@ 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 { const inputName = input.name().toString(); - const entry = Object.entries(args).find(([name, _]) => name === inputName); + 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"); - } - 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}`); +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; } - 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( - (entry) => entry.value().name().toString() === name, - ); - if (!entry) { - throw new Error(`no such entry: ${name}`); + case xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseVoidV0().value: { + const voidCase = entry.voidCase(); + return voidCase.name().toString() === name; + } + default: + return false; } - 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()); +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()); } - 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`, - ); - } - } + 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")); - if (val instanceof xdr.ScVal) { - return val; // should we copy? - } + default: + throw new TypeError(`invalid type ${ty.name} specified for string value`); + } +} - 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: { - const bytes_n = ty.bytesN(); - if (copy.length !== bytes_n.n()) { - throw new TypeError( - `expected ${bytes_n.n()} bytes, but got ${copy.length}`, - ); - } - //@ts-ignore - return xdr.ScVal.scvBytes(copy); - } - case xdr.ScSpecType.scSpecTypeBytes().value: - //@ts-ignore - return xdr.ScVal.scvBytes(copy); - default: - throw new TypeError( - `invalid type (${ty}) specified for Bytes and BytesN`, - ); - } - } - if (Array.isArray(val)) { - switch (value) { - case xdr.ScSpecType.scSpecTypeVec().value: { - const vec = ty.vec(); - const elementType = vec.elementType(); - return xdr.ScVal.scvVec( - val.map((v) => this.nativeToScVal(v, elementType)), - ); - } - case xdr.ScSpecType.scSpecTypeTuple().value: { - 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`, - ); - } - return xdr.ScVal.scvVec( - val.map((v, i) => this.nativeToScVal(v, valTypes[i])), - ); - } - case xdr.ScSpecType.scSpecTypeMap().value: { - const map = ty.map(); - const keyType = map.keyType(); - const valueType = map.valueType(); - return xdr.ScVal.scvMap( - val.map((entry) => { - const key = this.nativeToScVal(entry[0], keyType); - const val = this.nativeToScVal(entry[1], valueType); - return new xdr.ScMapEntry({ key, val }); - }), - ); - } - - default: - throw new TypeError( - `Type ${ty} was not vec, but value was Array`, - ); - } - } - if (val.constructor === Map) { - if (value !== xdr.ScSpecType.scSpecTypeMap().value) { - throw new TypeError(`Type ${ty} was not map, but value was Map`); - } - const scMap = ty.map(); - const map = val as Map; - const entries: xdr.ScMapEntry[] = []; - const values = map.entries(); - let res = values.next(); - while (!res.done) { - const [k, v] = res.value; - const key = this.nativeToScVal(k, scMap.keyType()); - const val = this.nativeToScVal(v, scMap.valueType()); - entries.push(new xdr.ScMapEntry({ key, val })); - res = values.next(); - } - return xdr.ScVal.scvMap(entries); - } - - if ((val.constructor?.name ?? "") !== "Object") { - throw new TypeError( - `cannot interpret ${val.constructor?.name - } value as ScVal (${JSON.stringify(val)})`, - ); - } - - throw new TypeError( - `Received object ${val} did not match the provided type ${ty}`, - ); - } - - case "number": - case "bigint": { - switch (value) { - case xdr.ScSpecType.scSpecTypeU32().value: - return xdr.ScVal.scvU32(val as number); - case xdr.ScSpecType.scSpecTypeI32().value: - return xdr.ScVal.scvI32(val as number); - case xdr.ScSpecType.scSpecTypeU64().value: - case xdr.ScSpecType.scSpecTypeI64().value: - case xdr.ScSpecType.scSpecTypeU128().value: - case xdr.ScSpecType.scSpecTypeI128().value: - case xdr.ScSpecType.scSpecTypeU256().value: - case xdr.ScSpecType.scSpecTypeI256().value: { - const intType = t.name.substring(10).toLowerCase() as ScIntType; - return new XdrLargeInt(intType, val as bigint).toScVal(); - } - default: - throw new TypeError(`invalid type (${ty}) specified for integer`); - } - } - case "string": - return stringToScVal(val, t); - - case "boolean": { - if (value !== xdr.ScSpecType.scSpecTypeBool().value) { - throw TypeError(`Type ${ty} was not bool, but value was bool`); - } - return xdr.ScVal.scvBool(val); - } - case "undefined": { - if (!ty) { - return xdr.ScVal.scvVoid(); - } - switch (value) { - case xdr.ScSpecType.scSpecTypeVoid().value: - case xdr.ScSpecType.scSpecTypeOption().value: - return xdr.ScVal.scvVoid(); - default: - throw new TypeError( - `Type ${ty} was not void, but value was undefined`, - ); - } - } - - case "function": // FIXME: Is this too helpful? - return this.nativeToScVal(val(), ty); +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}=)?$", + }, +}; - default: - throw new TypeError(`failed to convert typeof ${typeof val} (${val})`); +/** + * @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; } - } - - private nativeToUdt(val: any, name: string): xdr.ScVal { - const entry = this.findEntry(name); - switch (entry.switch()) { - case xdr.ScSpecEntryKind.scSpecEntryUdtEnumV0(): - if (typeof val !== "number") { - throw new TypeError( - `expected number for enum ${name}, but got ${typeof val}`, - ); - } - return this.nativeToEnum(val as number, entry.udtEnumV0()); - case xdr.ScSpecEntryKind.scSpecEntryUdtStructV0(): - return this.nativeToStruct(val, entry.udtStructV0()); - case xdr.ScSpecEntryKind.scSpecEntryUdtUnionV0(): - return this.nativeToUnion(val, entry.udtUnionV0()); - default: - throw new Error(`failed to parse udt ${name}`); + case xdr.ScSpecType.scSpecTypeBool().value: { + return { type: "boolean" }; } - } - - private nativeToUnion( - val: Union, - union_: xdr.ScSpecUdtUnionV0, - ): xdr.ScVal { - const entry_name = val.tag; - const case_ = union_.cases().find((entry) => { - const case_ = entry.value().name().toString(); - return case_ === entry_name; - }); - if (!case_) { - throw new TypeError(`no such enum entry: ${entry_name} in ${union_}`); + case xdr.ScSpecType.scSpecTypeVoid().value: { + return { type: "null" }; } - const key = xdr.ScVal.scvSymbol(entry_name); - switch (case_.switch()) { - case xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseVoidV0(): { - return xdr.ScVal.scvVec([key]); - } - case xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseTupleV0(): { - const types = case_.tupleCase().type(); - if (Array.isArray(val.values)) { - if (val.values.length != types.length) { - throw new TypeError( - `union ${union_} expects ${types.length} values, but got ${val.values.length}`, - ); - } - 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}`); - } - default: - throw new Error(`failed to parse union ${union_} with ${val}`); + case xdr.ScSpecType.scSpecTypeError().value: { + ref = "Error"; + break; } - } - - private nativeToStruct(val: any, struct: xdr.ScSpecUdtStructV0): xdr.ScVal { - const fields = struct.fields(); - if (fields.some(isNumeric)) { - if (!fields.every(isNumeric)) { - throw new Error( - "mixed numeric and non-numeric field names are not allowed", - ); - } - return xdr.ScVal.scvVec( - fields.map((_, i) => this.nativeToScVal(val[i], fields[i].type())), - ); + case xdr.ScSpecType.scSpecTypeU32().value: { + ref = "U32"; + break; } - return xdr.ScVal.scvMap( - fields.map((field) => { - const name = field.name().toString(); - return new xdr.ScMapEntry({ - key: this.nativeToScVal(name, xdr.ScSpecTypeDef.scSpecTypeSymbol()), - val: this.nativeToScVal(val[name], field.type()), - }); - }), - ); - } - - private nativeToEnum(val: number, enum_: xdr.ScSpecUdtEnumV0): xdr.ScVal { - if (enum_.cases().some((entry) => entry.value() === val)) { - return xdr.ScVal.scvU32(val); + case xdr.ScSpecType.scSpecTypeI32().value: { + ref = "I32"; + break; } - throw new TypeError(`no such enum entry: ${val} in ${enum_}`); - } - - /** - * Converts an base64 encoded ScVal back to a native JS value based on the given type. - * - * @param {string} scv the base64 encoded ScVal - * @param {xdr.ScSpecTypeDef} typeDef the expected type - * @returns {any} the converted native JS value - * - * @throws {Error} if ScVal cannot be converted to the given type - */ - scValStrToNative(scv: string, typeDef: xdr.ScSpecTypeDef): T { - return this.scValToNative(xdr.ScVal.fromXDR(scv, "base64"), typeDef); - } - - /** - * Converts an ScVal back to a native JS value based on the given type. - * - * @param {xdr.ScVal} scv the ScVal - * @param {xdr.ScSpecTypeDef} typeDef the expected type - * @returns {any} the converted native JS value - * - * @throws {Error} if ScVal cannot be converted to the given type - */ - scValToNative(scv: xdr.ScVal, typeDef: xdr.ScSpecTypeDef): T { - const t = typeDef.switch(); - const value = t.value; - if (value === xdr.ScSpecType.scSpecTypeUdt().value) { - return this.scValUdtToNative(scv, typeDef.udt()); + case xdr.ScSpecType.scSpecTypeU64().value: { + ref = "U64"; + break; } - // 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; - - // these can be converted to bigints directly - case xdr.ScValType.scvU64().value: - case xdr.ScValType.scvI64().value: - // these can be parsed by internal abstractions note that this can also - // handle the above two cases, but it's not as efficient (another - // type-check, parsing, etc.) - case xdr.ScValType.scvU128().value: - case xdr.ScValType.scvI128().value: - case xdr.ScValType.scvU256().value: - case xdr.ScValType.scvI256().value: - return scValToBigInt(scv) as T; - - case xdr.ScValType.scvVec().value: { - if (value == xdr.ScSpecType.scSpecTypeVec().value) { - const vec = typeDef.vec(); - return (scv.vec() ?? []).map((elm) => - this.scValToNative(elm, vec.elementType()), - ) as T; - } 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; - } - throw new TypeError(`Type ${typeDef} was not vec, but ${scv} is`); - } - - case xdr.ScValType.scvAddress().value: - return Address.fromScVal(scv).toString() as T; - - case xdr.ScValType.scvMap().value: { - const map = scv.map() ?? []; - if (value == xdr.ScSpecType.scSpecTypeMap().value) { - const type_ = typeDef.map(); - const keyType = type_.keyType(); - const valueType = type_.valueType(); - const res = map.map((entry) => [ - this.scValToNative(entry.key(), keyType), - this.scValToNative(entry.val(), valueType), - ]) as T; - return res; - } - throw new TypeError( - `ScSpecType ${t.name} was not map, but ${JSON.stringify( - scv, - null, - 2, - )} is`, - ); - } - - // these return the primitive type directly - case xdr.ScValType.scvBool().value: - case xdr.ScValType.scvU32().value: - case xdr.ScValType.scvI32().value: - case xdr.ScValType.scvBytes().value: - return scv.value() as T; - - case xdr.ScValType.scvString().value: - 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}`, - ); + 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 { $ref: `#/definitions/${ref}` }; +} - 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}`, - ); +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); } + }); + const res: { properties: object; required?: string[] } = { properties }; + if (required.length > 0) { + res.required = required; } + return res; +} - 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`); - } - 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`, - ); - } - const name = vec[0].sym().toString(); - if (vec[0].switch().value != xdr.ScValType.scvSymbol().value) { - throw new Error(`{vec[0]} is not a symbol`); - } - const entry = udt.cases().find(findCase(name)); - if (!entry) { +function structToJsonSchema(udt: xdr.ScSpecUdtStructV0): object { + const fields = udt.fields(); + if (fields.some(isNumeric)) { + if (!fields.every(isNumeric)) { throw new Error( - `failed to find entry ${name} in union {udt.name().toString()}`, + "mixed numeric and non-numeric field names are not allowed", ); } - 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((entry, i) => this.scValToNative(vec![i + 1], entry)); - res.values = values; + 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, + }; +} + +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 res; + }); + + const res: any = { + oneOf, + }; + if (description.length > 0) { + res.description = description; } + return res; +} - 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; + + +/** + * 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"); } - val.map()?.forEach((entry, i) => { - const field = fields[i]; - res[field.name().toString()] = this.scValToNative( - entry.val(), - field.type(), + const entry = entries[0]; + if (typeof entry === "string") { + this.entries = (entries as string[]).map((s) => + xdr.ScSpecEntry.fromXDR(s, "base64"), ); - }); - 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`); + } else { + this.entries = entries as xdr.ScSpecEntry[]; } - const num = scv.u32(); - return num; } /** - * Gets the XDR error cases from the spec. + * Gets the XDR functions from the spec. * * @returns {xdr.ScSpecFunctionV0[]} all contract functions * */ - errorCases(): xdr.ScSpecUdtErrorEnumCaseV0[] { + funcs(): xdr.ScSpecFunctionV0[] { return this.entries .filter( (entry) => entry.switch().value === - xdr.ScSpecEntryKind.scSpecEntryUdtErrorEnumV0().value, + xdr.ScSpecEntryKind.scSpecEntryFunctionV0().value, ) - .flatMap((entry) => (entry.value() as xdr.ScSpecUdtErrorEnumV0).cases()); + .map((entry) => entry.functionV0()); } /** - * Converts the contract spec to a JSON schema. - * - * If `funcName` is provided, the schema will be a reference to the function schema. + * Gets the XDR function spec for the given function name. * - * @param {string} [funcName] the name of the function to convert - * @returns {JSONSchema7} the converted JSON schema + * @param {string} name the name of the function + * @returns {xdr.ScSpecFunctionV0} the function spec * - * @throws {Error} if the contract spec is invalid + * @throws {Error} if no function with the given name exists */ - jsonSchema(funcName?: string): JSONSchema7 { - const definitions: { [key: string]: JSONSchema7Definition } = {}; - for (const entry of this.entries) { - 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"); - } - } - } - const 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: { - const 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: { - const tuple = entry.tupleCase(); - return tuple.name().toString() === name; - } - case xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseVoidV0().value: { - const 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 { - const t = typeDef.switch(); - const 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; + 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`); } - case xdr.ScSpecType.scSpecTypeI128().value: { - ref = "I128"; - break; + 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; } - case xdr.ScSpecType.scSpecTypeU256().value: { - ref = "U256"; - break; + if (outputs.length > 1) { + throw new Error(`Multiple outputs not supported`); } - case xdr.ScSpecType.scSpecTypeI256().value: { - ref = "I256"; - break; + const output = outputs[0]; + if (output.switch().value === xdr.ScSpecType.scSpecTypeResult().value) { + return new Ok(this.scValToNative(val, output.result().okType())); } - case xdr.ScSpecType.scSpecTypeBytes().value: { - ref = "DataUrl"; - break; + 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}`); } - case xdr.ScSpecType.scSpecTypeString().value: { - ref = "ScString"; - break; + 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()); } - case xdr.ScSpecType.scSpecTypeSymbol().value: { - ref = "ScSymbol"; - break; + if (value === xdr.ScSpecType.scSpecTypeOption().value) { + const opt = ty.option(); + if (val === undefined) { + return xdr.ScVal.scvVoid(); + } + return this.nativeToScVal(val, opt.valueType()); } - case xdr.ScSpecType.scSpecTypeAddress().value: { - ref = "Address"; - break; + 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: { + const bytesN = ty.bytesN(); + if (copy.length !== bytesN.n()) { + throw new TypeError( + `expected ${bytesN.n()} bytes, but got ${copy.length}`, + ); + } + //@ts-ignore + return xdr.ScVal.scvBytes(copy); + } + case xdr.ScSpecType.scSpecTypeBytes().value: + //@ts-ignore + return xdr.ScVal.scvBytes(copy); + default: + throw new TypeError( + `invalid type (${ty}) specified for Bytes and BytesN`, + ); + } + } + if (Array.isArray(val)) { + switch (value) { + case xdr.ScSpecType.scSpecTypeVec().value: { + const vec = ty.vec(); + const elementType = vec.elementType(); + return xdr.ScVal.scvVec( + val.map((v) => this.nativeToScVal(v, elementType)), + ); + } + case xdr.ScSpecType.scSpecTypeTuple().value: { + 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`, + ); + } + return xdr.ScVal.scvVec( + val.map((v, i) => this.nativeToScVal(v, valTypes[i])), + ); + } + case xdr.ScSpecType.scSpecTypeMap().value: { + const map = ty.map(); + const keyType = map.keyType(); + const valueType = map.valueType(); + return xdr.ScVal.scvMap( + val.map((entry) => { + const key = this.nativeToScVal(entry[0], keyType); + const mapVal = this.nativeToScVal(entry[1], valueType); + return new xdr.ScMapEntry({ key, val: mapVal }); + }), + ); + } + + default: + throw new TypeError( + `Type ${ty} was not vec, but value was Array`, + ); + } + } + if (val.constructor === Map) { + if (value !== xdr.ScSpecType.scSpecTypeMap().value) { + throw new TypeError(`Type ${ty} was not map, but value was Map`); + } + const scMap = ty.map(); + const map = val as Map; + const entries: xdr.ScMapEntry[] = []; + const values = map.entries(); + let res = values.next(); + while (!res.done) { + 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); + } + + if ((val.constructor?.name ?? "") !== "Object") { + throw new TypeError( + `cannot interpret ${val.constructor?.name + } value as ScVal (${JSON.stringify(val)})`, + ); + } + + throw new TypeError( + `Received object ${val} did not match the provided type ${ty}`, + ); + } + + case "number": + case "bigint": { + switch (value) { + case xdr.ScSpecType.scSpecTypeU32().value: + return xdr.ScVal.scvU32(val as number); + case xdr.ScSpecType.scSpecTypeI32().value: + return xdr.ScVal.scvI32(val as number); + case xdr.ScSpecType.scSpecTypeU64().value: + case xdr.ScSpecType.scSpecTypeI64().value: + case xdr.ScSpecType.scSpecTypeU128().value: + case xdr.ScSpecType.scSpecTypeI128().value: + case xdr.ScSpecType.scSpecTypeU256().value: + case xdr.ScSpecType.scSpecTypeI256().value: { + const intType = t.name.substring(10).toLowerCase() as ScIntType; + return new XdrLargeInt(intType, val as bigint).toScVal(); + } + default: + throw new TypeError(`invalid type (${ty}) specified for integer`); + } + } + case "string": + return stringToScVal(val, t); + + case "boolean": { + if (value !== xdr.ScSpecType.scSpecTypeBool().value) { + throw TypeError(`Type ${ty} was not bool, but value was bool`); + } + return xdr.ScVal.scvBool(val); + } + case "undefined": { + if (!ty) { + return xdr.ScVal.scvVoid(); + } + switch (value) { + case xdr.ScSpecType.scSpecTypeVoid().value: + case xdr.ScSpecType.scSpecTypeOption().value: + return xdr.ScVal.scvVoid(); + default: + throw new TypeError( + `Type ${ty} was not void, but value was undefined`, + ); + } + } + + case "function": // FIXME: Is this too helpful? + return this.nativeToScVal(val(), ty); + + default: + throw new TypeError(`failed to convert typeof ${typeof val} (${val})`); } - case xdr.ScSpecType.scSpecTypeOption().value: { - const opt = typeDef.option(); - return typeRef(opt.valueType()); + } + + private nativeToUdt(val: any, name: string): xdr.ScVal { + const entry = this.findEntry(name); + switch (entry.switch()) { + case xdr.ScSpecEntryKind.scSpecEntryUdtEnumV0(): + if (typeof val !== "number") { + throw new TypeError( + `expected number for enum ${name}, but got ${typeof val}`, + ); + } + return this.nativeToEnum(val as number, entry.udtEnumV0()); + case xdr.ScSpecEntryKind.scSpecEntryUdtStructV0(): + return this.nativeToStruct(val, entry.udtStructV0()); + case xdr.ScSpecEntryKind.scSpecEntryUdtUnionV0(): + return this.nativeToUnion(val, entry.udtUnionV0()); + default: + throw new Error(`failed to parse udt ${name}`); } - case xdr.ScSpecType.scSpecTypeResult().value: { - // throw new Error('Result type not supported'); - break; + } + + private nativeToUnion( + val: Union, + union_: xdr.ScSpecUdtUnionV0, + ): xdr.ScVal { + const entryName = val.tag; + const caseFound = union_.cases().find((entry) => { + const caseN = entry.value().name().toString(); + return caseN === entryName; + }); + if (!caseFound) { + throw new TypeError(`no such enum entry: ${entryName} in ${union_}`); } - case xdr.ScSpecType.scSpecTypeVec().value: { - const arr = typeDef.vec(); - const ref = typeRef(arr.elementType()); - return { - type: "array", - items: ref, - }; + const key = xdr.ScVal.scvSymbol(entryName); + switch (caseFound.switch()) { + case xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseVoidV0(): { + return xdr.ScVal.scvVec([key]); + } + case xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseTupleV0(): { + const types = caseFound.tupleCase().type(); + if (Array.isArray(val.values)) { + if (val.values.length !== types.length) { + throw new TypeError( + `union ${union_} expects ${types.length} values, but got ${val.values.length}`, + ); + } + 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 ${caseFound} with ${val}`); + } + default: + throw new Error(`failed to parse union ${union_} with ${val}`); } - 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, - }, - }; + } + + private nativeToStruct(val: any, struct: xdr.ScSpecUdtStructV0): xdr.ScVal { + const fields = struct.fields(); + if (fields.some(isNumeric)) { + if (!fields.every(isNumeric)) { + throw new Error( + "mixed numeric and non-numeric field names are not allowed", + ); + } + return xdr.ScVal.scvVec( + fields.map((_, i) => this.nativeToScVal(val[i], fields[i].type())), + ); } - 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 }; + return xdr.ScVal.scvMap( + fields.map((field) => { + const name = field.name().toString(); + return new xdr.ScMapEntry({ + key: this.nativeToScVal(name, xdr.ScSpecTypeDef.scSpecTypeSymbol()), + val: this.nativeToScVal(val[name], field.type()), + }); + }), + ); + } + + private nativeToEnum(val: number, enum_: xdr.ScSpecUdtEnumV0): xdr.ScVal { + if (enum_.cases().some((entry) => entry.value() === val)) { + return xdr.ScVal.scvU32(val); } - case xdr.ScSpecType.scSpecTypeBytesN().value: { - const arr = typeDef.bytesN(); - return { - $ref: "#/definitions/DataUrl", - maxLength: arr.n(), - }; + throw new TypeError(`no such enum entry: ${val} in ${enum_}`); + } + + /** + * Converts an base64 encoded ScVal back to a native JS value based on the given type. + * + * @param {string} scv the base64 encoded ScVal + * @param {xdr.ScSpecTypeDef} typeDef the expected type + * @returns {any} the converted native JS value + * + * @throws {Error} if ScVal cannot be converted to the given type + */ + scValStrToNative(scv: string, typeDef: xdr.ScSpecTypeDef): T { + return this.scValToNative(xdr.ScVal.fromXDR(scv, "base64"), typeDef); + } + + /** + * Converts an ScVal back to a native JS value based on the given type. + * + * @param {xdr.ScVal} scv the ScVal + * @param {xdr.ScSpecTypeDef} typeDef the expected type + * @returns {any} the converted native JS value + * + * @throws {Error} if ScVal cannot be converted to the given type + */ + scValToNative(scv: xdr.ScVal, typeDef: xdr.ScSpecTypeDef): T { + const t = typeDef.switch(); + const value = t.value; + if (value === xdr.ScSpecType.scSpecTypeUdt().value) { + return this.scValUdtToNative(scv, typeDef.udt()); } - case xdr.ScSpecType.scSpecTypeUdt().value: { - const udt = typeDef.udt(); - ref = udt.name().toString(); - break; + // 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 undefined as T; + + // these can be converted to bigints directly + case xdr.ScValType.scvU64().value: + case xdr.ScValType.scvI64().value: + // these can be parsed by internal abstractions note that this can also + // handle the above two cases, but it's not as efficient (another + // type-check, parsing, etc.) + case xdr.ScValType.scvU128().value: + case xdr.ScValType.scvI128().value: + case xdr.ScValType.scvU256().value: + case xdr.ScValType.scvI256().value: + return scValToBigInt(scv) as T; + + case xdr.ScValType.scvVec().value: { + if (value === xdr.ScSpecType.scSpecTypeVec().value) { + const vec = typeDef.vec(); + return (scv.vec() ?? []).map((elm) => + this.scValToNative(elm, vec.elementType()), + ) as T; + } 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; + } + throw new TypeError(`Type ${typeDef} was not vec, but ${scv} is`); + } + + case xdr.ScValType.scvAddress().value: + return Address.fromScVal(scv).toString() as T; + + case xdr.ScValType.scvMap().value: { + 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; + return res; + } + throw new TypeError( + `ScSpecType ${t.name} was not map, but ${JSON.stringify( + scv, + null, + 2, + )} is`, + ); + } + + // these return the primitive type directly + case xdr.ScValType.scvBool().value: + case xdr.ScValType.scvU32().value: + case xdr.ScValType.scvI32().value: + case xdr.ScValType.scvBytes().value: + return scv.value() as T; + + case xdr.ScValType.scvString().value: + 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}`, + ); } } - return { $ref: `#/definitions/${ref}` }; -} - -type Func = { input: JSONSchema7; output: JSONSchema7 }; -function isRequired(typeDef: xdr.ScSpecTypeDef): boolean { - return typeDef.switch().value != xdr.ScSpecType.scSpecTypeOption().value; -} + 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}`, + ); + } + } -function structToJsonSchema(udt: xdr.ScSpecUdtStructV0): object { - const fields = udt.fields(); - if (fields.some(isNumeric)) { - if (!fields.every(isNumeric)) { + 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`); + } + if (vec.length === 0 && udt.cases.length !== 0) { throw new Error( - "mixed numeric and non-numeric field names are not allowed", + `${val} has length 0, but the there are at least one case in the union`, ); } - 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 = 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[] } { - const properties: any = {}; - const required: string[] = []; - for (const arg of input) { - const type_ = arg.type(); - const name = arg.name().toString(); - properties[name] = typeRef(type_); - if (isRequired(type_)) { - required.push(name); + const name = vec[0].sym().toString(); + if (vec[0].switch().value !== xdr.ScValType.scvSymbol().value) { + throw new Error(`{vec[0]} is not a symbol`); } + const entry = udt.cases().find(findCase(name)); + if (!entry) { + throw new Error( + `failed to find entry ${name} in union {udt.name().toString()}`, + ); + } + 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; } - const res: { properties: object; required?: string[] } = { properties }; - if (required.length > 0) { - res.required = required; - } - return res; -} -function functionToJsonSchema(func: xdr.ScSpecFunctionV0): Func { - const { properties, required }: any = args_and_required(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; + 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; + } + val.map()?.forEach((entry, i) => { + const field = fields[i]; + res[field.name().toString()] = this.scValToNative( + entry.val(), + field.type(), + ); + }); + return res; } - input.additionalProperties = false; - output.additionalProperties = false; - return { - input, - output, - }; -} -function unionToJsonSchema(udt: xdr.ScSpecUdtUnionV0): any { - const description = udt.doc().toString(); - const cases = udt.cases(); - const oneOf: any[] = []; - for (const case_ of cases) { - switch (case_.switch().value) { - case xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseVoidV0().value: { - const c = case_.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 = case_.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, - }); - } + 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; } - const res: any = { - oneOf, - }; - if (description.length > 0) { - res.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()); } - return res; -} -function enumToJsonSchema(udt: xdr.ScSpecUdtEnumV0): any { - const description = udt.doc().toString(); - const cases = udt.cases(); - const oneOf: any[] = []; - for (const case_ of cases) { - const title = case_.name().toString(); - const description = case_.doc().toString(); - oneOf.push({ - description, - title, - enum: [case_.value()], - type: "number", + /** + * 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 } = {}; + 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"); + } + } }); + const res: JSONSchema7 = { + $schema: "http://json-schema.org/draft-07/schema#", + definitions: { ...PRIMITIVE_DEFINITONS, ...definitions }, + }; + if (funcName) { + res.$ref = `#/definitions/${funcName}`; + } + return res; } - - const res: any = { oneOf }; - if (description.length > 0) { - res.description = description; - } - return res; } + From 7b31f8454ff13fdd0c7892b80c80237025d24a0c Mon Sep 17 00:00:00 2001 From: Blaine Heffron Date: Mon, 10 Jun 2024 21:10:49 -0400 Subject: [PATCH 03/13] more eslint fixes --- src/errors.ts | 2 ++ src/federation/server.ts | 1 + src/horizon/account_response.ts | 3 ++- src/horizon/call_builder.ts | 23 ++++++++++++++++------- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/errors.ts b/src/errors.ts index 38a70e85e..d97b118d1 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,8 +1,10 @@ +/* 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 */ + export class NetworkError extends Error { public response: { data?: HorizonApi.ErrorResponseData; diff --git a/src/federation/server.ts b/src/federation/server.ts index c0c6412be..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"; diff --git a/src/horizon/account_response.ts b/src/horizon/account_response.ts index de103381c..932d14915 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/call_builder.ts b/src/horizon/call_builder.ts index 7710f3b0a..5c9a0d96b 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,6 +21,8 @@ export interface EventSourceOptions { const anyGlobal = global as any; type Constructable = new (e: string) => T; // require("eventsource") for Node and React Native environment +/* eslint-disable global-require */ +/* eslint-disable prefer-import/prefer-import-over-require */ const EventSource: Constructable = anyGlobal.EventSource ?? anyGlobal.window?.EventSource ?? require("eventsource"); @@ -107,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) { @@ -177,6 +181,8 @@ export class CallBuilder< return es; }; + + createEventSource(); return () => { clearTimeout(timeout); @@ -198,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 "recordsNumber" number Number of records the server should return. * @returns {object} current CallBuilder instance */ public limit(recordsNumber: number): this { @@ -224,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 { @@ -275,8 +281,8 @@ 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 + * @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 { @@ -306,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 @@ -326,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; @@ -389,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) { From 9060be1f288a78e1ad8ce1cf151cae0357cbe896 Mon Sep 17 00:00:00 2001 From: Blaine Heffron Date: Tue, 11 Jun 2024 11:03:06 -0400 Subject: [PATCH 04/13] final eslint fixes --- .../claimable_balances_call_builder.ts | 2 +- src/horizon/horizon_axios_client.ts | 13 +- src/horizon/liquidity_pool_call_builder.ts | 1 - src/horizon/operation_call_builder.ts | 2 +- src/horizon/server.ts | 36 +- src/horizon/trade_aggregation_call_builder.ts | 19 +- src/horizon/transaction_call_builder.ts | 2 +- src/rpc/api.ts | 5 +- src/rpc/axios.ts | 3 +- src/rpc/browser.ts | 2 +- src/rpc/jsonrpc.ts | 21 +- src/rpc/parsers.ts | 88 ++-- src/rpc/server.ts | 99 ++-- src/rpc/transaction.ts | 78 ++-- src/rpc/utils.ts | 1 + src/stellartoml/index.ts | 1 + src/webauth/utils.ts | 439 +++++++++--------- .../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 | 109 ++++- .../soroban/simulate_transaction_test.js | 119 +++-- 23 files changed, 691 insertions(+), 550 deletions(-) diff --git a/src/horizon/claimable_balances_call_builder.ts b/src/horizon/claimable_balances_call_builder.ts index 019edbd71..3d94fd390 100644 --- a/src/horizon/claimable_balances_call_builder.ts +++ b/src/horizon/claimable_balances_call_builder.ts @@ -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/horizon_axios_client.ts b/src/horizon/horizon_axios_client.ts index 9fe3b279a..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( (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/liquidity_pool_call_builder.ts b/src/horizon/liquidity_pool_call_builder.ts index be38666be..25798988e 100644 --- a/src/horizon/liquidity_pool_call_builder.ts +++ b/src/horizon/liquidity_pool_call_builder.ts @@ -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/operation_call_builder.ts b/src/horizon/operation_call_builder.ts index debdf6331..04da7127f 100644 --- a/src/horizon/operation_call_builder.ts +++ b/src/horizon/operation_call_builder.ts @@ -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/server.ts b/src/horizon/server.ts index d36dd00f1..7306fe08d 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,7 +51,7 @@ 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(); } @@ -132,9 +133,9 @@ export class Server { * .build(); * ``` * @param {number} seconds Number of seconds past the current time to wait. - * @param {bool} [_isRetry] True if this is a retry. Only set this internally! + * @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( @@ -182,6 +183,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), @@ -284,8 +286,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 @@ -420,9 +421,9 @@ export class Server { sellerId, offerId: offerClaimed.offerId().toString(), assetSold, - amountSold: _getAmountInLumens(claimedOfferAmountSold), + amountSold: getAmountInLumens(claimedOfferAmountSold), assetBought, - amountBought: _getAmountInLumens(claimedOfferAmountBought), + amountBought: getAmountInLumens(claimedOfferAmountBought), }; }); @@ -440,7 +441,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(), @@ -471,8 +472,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", @@ -708,10 +709,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 */ @@ -764,7 +765,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) { @@ -788,6 +790,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 @@ -811,6 +814,7 @@ export class Server { continue; } } + /* eslint-enable no-continue */ } } diff --git a/src/horizon/trade_aggregation_call_builder.ts b/src/horizon/trade_aggregation_call_builder.ts index b42b05372..8072bc31f 100644 --- a/src/horizon/trade_aggregation_call_builder.ts +++ b/src/horizon/trade_aggregation_call_builder.ts @@ -24,10 +24,10 @@ const allowedResolutions = [ * @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,23 @@ 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) { + // eslint-disable-next-line consistent-return + allowedResolutions.forEach((allowed) => { if (allowed === resolution) { return true; } - } + }); return false; } /** * @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/transaction_call_builder.ts b/src/horizon/transaction_call_builder.ts index 39071818c..5dd24a3eb 100644 --- a/src/horizon/transaction_call_builder.ts +++ b/src/horizon/transaction_call_builder.ts @@ -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/rpc/api.ts b/src/rpc/api.ts index 03f6d407d..42c97348b 100644 --- a/src/rpc/api.ts +++ b/src/rpc/api.ts @@ -33,7 +33,7 @@ export namespace Api { liveUntilLedgerSeq?: number; } - /** An XDR-parsed version of {@link RawLedgerEntryResult} */ + /** An XDR-parsed version of {@link this.RawLedgerEntryResult} */ export interface GetLedgerEntriesResponse { entries: LedgerEntryResult[]; latestLedger: number; @@ -227,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 @@ -254,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[]; diff --git a/src/rpc/axios.ts b/src/rpc/axios.ts index 5c98be46f..e33911202 100644 --- a/src/rpc/axios.ts +++ b/src/rpc/axios.ts @@ -1,6 +1,7 @@ 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({ diff --git a/src/rpc/browser.ts b/src/rpc/browser.ts index 6f321c2e4..d382124f1 100644 --- a/src/rpc/browser.ts +++ b/src/rpc/browser.ts @@ -1,5 +1,5 @@ /* tslint:disable:no-var-requires */ - +/* eslint-disable import/no-import-module-exports */ import axios from 'axios'; // idk why axios is weird export * from './index'; 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 3ebbcf21a..076e89e5a 100644 --- a/src/rpc/parsers.ts +++ b/src/rpc/parsers.ts @@ -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 - 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); -} - function parseSuccessful( sim: Api.RawSimulateTransactionResponse, partial: Api.BaseSimulateTransactionResponse @@ -127,6 +84,7 @@ 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) => ({ auth: (row.auth ?? []).map((entry) => @@ -139,6 +97,7 @@ function parseSuccessful( }))[0] }), + // eslint-disable-next-line no-self-compare ...(sim.stateChanges?.length ?? 0 > 0) && { stateChanges: sim.stateChanges?.map((entryChange) => ({ type: entryChange.type, @@ -165,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 f0fa7e7be..42a46cab9 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'; @@ -22,6 +24,8 @@ import { parseRawLedgerEntries, parseRawEvents } from './parsers'; +import type { Config } from '../config'; + export const SUBMIT_TRANSACTION_TIMEOUT = 60 * 1000; @@ -52,6 +56,38 @@ 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'); +} + /** * Handles the network connection to a Soroban RPC instance, exposing an * interface for requests to that instance. @@ -117,6 +153,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 +177,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 +191,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 +204,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 +215,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, @@ -220,6 +258,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 +304,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 +355,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 +396,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 +435,7 @@ export class Server { * console.log("resultXdr:", tx.resultXdr); * }); */ + // eslint-disable-next-line require-await public async getTransaction( hash: string ): Promise { @@ -435,6 +479,7 @@ export class Server { }); } + // eslint-disable-next-line require-await public async _getTransaction( hash: string ): Promise { @@ -479,12 +524,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,6 +561,7 @@ export class Server { * console.log("protocolVersion:", network.protocolVersion); * }); */ + // eslint-disable-next-line require-await public async getNetwork(): Promise { return jsonrpc.postObject(this.serverURL.toString(), 'getNetwork'); } @@ -533,6 +581,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 +590,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 +628,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 +637,7 @@ export class Server { .then(parseRawSimulation); } + // eslint-disable-next-line require-await public async _simulateTransaction( transaction: Transaction | FeeBumpTransaction, addlResources?: Server.ResourceLeeway @@ -622,7 +673,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 +728,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 +777,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 +856,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 c48162f24..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} */ @@ -52,6 +69,7 @@ export function assembleTransaction( 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/utils.ts b/src/webauth/utils.ts index 852994b21..359be9079 100644 --- a/src/webauth/utils.ts +++ b/src/webauth/utils.ts @@ -13,10 +13,13 @@ import { TransactionBuilder, } from "@stellar/stellar-base"; +import type { Networks } from "@stellar/stellar-base"; import { Utils } from "../utils"; import { InvalidChallengeError } from "./errors"; import { ServerApi } from "../horizon/server_api"; +/* eslint-disable jsdoc/no-undefined-types */ + /** * Returns a valid [SEP-10](https://stellar.org/protocol/sep-10) challenge * transaction which you can use for Stellar Web Authentication. @@ -63,6 +66,7 @@ export function buildChallengeTx( serverKeypair: Keypair, clientAccountID: string, homeDomain: string, + // eslint-disable-next-line @typescript-eslint/default-param-last timeout: number = 300, networkPassphrase: string, webAuthDomain: string, @@ -133,6 +137,97 @@ export function buildChallengeTx( .toString(); } +/** + * Checks if a transaction has been signed by one or more of the given signers, + * returning a list of non-repeated signers that were found to have signed the + * given transaction. + * + * @function + * @memberof WebAuth + * @param {Transaction} transaction the signed transaction. + * @param {string[]} signers The signers public keys. + * @returns {string[]} a list of signers that were found to have signed the + * transaction. + * + * @example + * let keypair1 = Keypair.random(); + * let keypair2 = Keypair.random(); + * const account = new StellarSdk.Account(keypair1.publicKey(), "-1"); + * + * const transaction = new TransactionBuilder(account, { fee: 100 }) + * .setTimeout(30) + * .build(); + * + * transaction.sign(keypair1, keypair2) + * WebAuth.gatherTxSigners(transaction, [keypair1.publicKey(), keypair2.publicKey()]) + */ +export function gatherTxSigners( + transaction: FeeBumpTransaction | Transaction, + signers: string[], +): string[] { + const hashedSignatureBase = transaction.hash(); + + const txSignatures = [...transaction.signatures]; // shallow copy for safe splicing + const signersFound = new Set(); + + // eslint-disable-next-line no-restricted-syntax + for (const signer of signers) { + if (txSignatures.length === 0) { + break; + } + + let keypair: Keypair; + try { + keypair = Keypair.fromPublicKey(signer); // This can throw a few different errors + } catch (err: any) { + throw new InvalidChallengeError( + `Signer is not a valid address: ${ err.message}`, + ); + } + + for (let i = 0; i < txSignatures.length; i+=1) { + const decSig = txSignatures[i]; + + if (!decSig.hint().equals(keypair.signatureHint())) { + // eslint-disable-next-line no-continue + continue; + } + + if (keypair.verify(hashedSignatureBase, decSig.signature())) { + signersFound.add(signer); + txSignatures.splice(i, 1); + break; + } + } + } + + return Array.from(signersFound); +} + +/** + * Verifies if a transaction was signed by the given account id. + * + * @function + * @memberof WebAuth + * + * @example + * let keypair = Keypair.random(); + * const account = new StellarSdk.Account(keypair.publicKey(), "-1"); + * + * const transaction = new TransactionBuilder(account, { fee: 100 }) + * .setTimeout(30) + * .build(); + * + * transaction.sign(keypair) + * WebAuth.verifyTxSignedBy(transaction, keypair.publicKey()) + */ +export function verifyTxSignedBy( + transaction: FeeBumpTransaction | Transaction, + accountID: string, +): boolean { + return gatherTxSigners(transaction, [accountID]).length !== 0; +} + /** * Reads a SEP 10 challenge transaction and returns the decoded transaction and * client account ID contained within. @@ -317,7 +412,7 @@ export function readChallengeTx( } // verify any subsequent operations are manage data ops and source account is the server - for (const op of subsequentOperations) { + subsequentOperations.forEach((op) => { if (op.type !== "manageData") { throw new InvalidChallengeError( "The transaction has operations that are not of type 'manageData'", @@ -340,7 +435,7 @@ export function readChallengeTx( ); } } - } + }); if (!verifyTxSignedBy(transaction, serverAccountID)) { throw new InvalidChallengeError( @@ -351,131 +446,6 @@ export function readChallengeTx( return { tx: transaction, clientAccountID, matchedHomeDomain, memo }; } -/** - * Verifies that for a SEP-10 challenge transaction all signatures on the - * transaction are accounted for and that the signatures meet a threshold on an - * account. A transaction is verified if it is signed by the server account, and - * all other signatures match a signer that has been provided as an argument, - * and those signatures meet a threshold on the account. - * - * Signers that are not prefixed as an address/account ID strkey (G...) will be - * ignored. - * - * Errors will be raised if: - * - The transaction is invalid according to {@link readChallengeTx}. - * - No client signatures are found on the transaction. - * - One or more signatures in the transaction are not identifiable as the - * server account or one of the signers provided in the arguments. - * - The signatures are all valid but do not meet the threshold. - * - * @function - * @memberof WebAuth - * - * @param {string} challengeTx SEP0010 challenge transaction in base64. - * @param {string} serverAccountID The server's stellar account (public key). - * @param {string} networkPassphrase The network passphrase, e.g.: 'Test SDF - * Network ; September 2015' (see {@link Networks}). - * @param {number} threshold The required signatures threshold for verifying - * this transaction. - * @param {ServerApi.AccountRecordSigners[]} signerSummary a map of all - * authorized signers to their weights. It's used to validate if the - * transaction signatures have met the given threshold. - * @param {string|string[]} [homeDomains] The home domain(s) that should be - * included in the first Manage Data operation's string key. Required in - * verifyChallengeTxSigners() => readChallengeTx(). - * @param {string} webAuthDomain The home domain that is expected to be included - * as the value of the Manage Data operation with the 'web_auth_domain' key, - * if present. Used in verifyChallengeTxSigners() => readChallengeTx(). - * - * @returns {string[]} The list of signers public keys that have signed the - * transaction, excluding the server account ID, given that the threshold was - * met. - * - * @see [SEP-10: Stellar Web Auth](https://stellar.org/protocol/sep-10). - * @example - * import { Networks, TransactionBuilder, WebAuth } from 'stellar-sdk'; - * - * const serverKP = Keypair.random(); - * const clientKP1 = Keypair.random(); - * const clientKP2 = Keypair.random(); - * - * // Challenge, possibly built in the server side - * const challenge = WebAuth.buildChallengeTx( - * serverKP, - * clientKP1.publicKey(), - * "SDF", - * 300, - * Networks.TESTNET - * ); - * - * // clock.tick(200); // Simulates a 200 ms delay when communicating from server to client - * - * // Transaction gathered from a challenge, possibly from the client side - * const transaction = TransactionBuilder.fromXDR(challenge, Networks.TESTNET); - * transaction.sign(clientKP1, clientKP2); - * const signedChallenge = transaction - * .toEnvelope() - * .toXDR("base64") - * .toString(); - * - * // Defining the threshold and signerSummary - * const threshold = 3; - * const signerSummary = [ - * { - * key: this.clientKP1.publicKey(), - * weight: 1, - * }, - * { - * key: this.clientKP2.publicKey(), - * weight: 2, - * }, - * ]; - * - * // The result below should be equal to [clientKP1.publicKey(), clientKP2.publicKey()] - * WebAuth.verifyChallengeTxThreshold( - * signedChallenge, - * serverKP.publicKey(), - * Networks.TESTNET, - * threshold, - * signerSummary - * ); - */ -export function verifyChallengeTxThreshold( - challengeTx: string, - serverAccountID: string, - networkPassphrase: string, - threshold: number, - signerSummary: ServerApi.AccountRecordSigners[], - homeDomains: string | string[], - webAuthDomain: string, -): string[] { - const signers = signerSummary.map((signer) => signer.key); - - const signersFound = verifyChallengeTxSigners( - challengeTx, - serverAccountID, - networkPassphrase, - signers, - homeDomains, - webAuthDomain, - ); - - let weight = 0; - for (const signer of signersFound) { - const sigWeight = - signerSummary.find((s) => s.key === signer)?.weight || 0; - weight += sigWeight; - } - - if (weight < threshold) { - throw new InvalidChallengeError( - `signers with weight ${weight} do not meet threshold ${threshold}"`, - ); - } - - return signersFound; -} - /** * Verifies that for a SEP 10 challenge transaction all signatures on the * transaction are accounted for. A transaction is verified if it is signed by @@ -579,24 +549,16 @@ export function verifyChallengeTxSigners( // Deduplicate the client signers and ensure the server is not included // anywhere we check or output the list of signers. - const clientSigners = new Set(); - for (const signer of signers) { - // Ignore the server signer if it is in the signers list. It's - // important when verifying signers of a challenge transaction that we - // only verify and return client signers. If an account has the server - // as a signer the server should not play a part in the authentication - // of the client. - if (signer === serverKP.publicKey()) { - continue; - } - - // Ignore non-G... account/address signers. - if (signer.charAt(0) !== "G") { - continue; - } - - clientSigners.add(signer); - } + // Ignore the server signer if it is in the signers list. It's + // important when verifying signers of a challenge transaction that we + // only verify and return client signers. If an account has the server + // as a signer the server should not play a part in the authentication + // of the client. + const clientSigners = new Set( + signers.filter( + (signer) => signer !== serverKP.publicKey() && signer.charAt(0) === "G" + ) + ); // Don't continue if none of the signers provided are in the final list. if (clientSigners.size === 0) { @@ -606,7 +568,7 @@ export function verifyChallengeTxSigners( } let clientSigningKey; - for (const op of tx.operations) { + tx.operations.forEach((op) => { if (op.type === "manageData" && op.name === "client_domain") { if (clientSigningKey) { throw new InvalidChallengeError( @@ -615,7 +577,7 @@ export function verifyChallengeTxSigners( } clientSigningKey = op.source; } - } + }); // Verify all the transaction's signers (server and client) in one // hit. We do this in one hit here even though the server signature was @@ -633,14 +595,14 @@ export function verifyChallengeTxSigners( let serverSignatureFound = false; let clientSigningKeySignatureFound = false; - for (const signer of signersFound) { + signersFound.forEach((signer) => { if (signer === serverKP.publicKey()) { serverSignatureFound = true; } if (signer === clientSigningKey) { clientSigningKeySignatureFound = true; } - } + }); // Confirm we matched a signature to the server signer. if (!serverSignatureFound) { @@ -681,94 +643,129 @@ export function verifyChallengeTxSigners( return signersFound; } + /** - * Verifies if a transaction was signed by the given account id. + * Verifies that for a SEP-10 challenge transaction all signatures on the + * transaction are accounted for and that the signatures meet a threshold on an + * account. A transaction is verified if it is signed by the server account, and + * all other signatures match a signer that has been provided as an argument, + * and those signatures meet a threshold on the account. + * + * Signers that are not prefixed as an address/account ID strkey (G...) will be + * ignored. + * + * Errors will be raised if: + * - The transaction is invalid according to {@link readChallengeTx}. + * - No client signatures are found on the transaction. + * - One or more signatures in the transaction are not identifiable as the + * server account or one of the signers provided in the arguments. + * - The signatures are all valid but do not meet the threshold. * * @function * @memberof WebAuth - * @param {Transaction} transaction - * @param {string} accountID - * @returns {boolean}. * + * @param {string} challengeTx SEP0010 challenge transaction in base64. + * @param {string} serverAccountID The server's stellar account (public key). + * @param {string} networkPassphrase The network passphrase, e.g.: 'Test SDF + * Network ; September 2015' (see {@link Networks}). + * @param {number} threshold The required signatures threshold for verifying + * this transaction. + * @param {ServerApi.AccountRecordSigners[]} signerSummary a map of all + * authorized signers to their weights. It's used to validate if the + * transaction signatures have met the given threshold. + * @param {string|string[]} [homeDomains] The home domain(s) that should be + * included in the first Manage Data operation's string key. Required in + * verifyChallengeTxSigners() => readChallengeTx(). + * @param {string} webAuthDomain The home domain that is expected to be included + * as the value of the Manage Data operation with the 'web_auth_domain' key, + * if present. Used in verifyChallengeTxSigners() => readChallengeTx(). + * + * @returns {string[]} The list of signers public keys that have signed the + * transaction, excluding the server account ID, given that the threshold was + * met. + * + * @see [SEP-10: Stellar Web Auth](https://stellar.org/protocol/sep-10). * @example - * let keypair = Keypair.random(); - * const account = new StellarSdk.Account(keypair.publicKey(), "-1"); + * import { Networks, TransactionBuilder, WebAuth } from 'stellar-sdk'; * - * const transaction = new TransactionBuilder(account, { fee: 100 }) - * .setTimeout(30) - * .build(); + * const serverKP = Keypair.random(); + * const clientKP1 = Keypair.random(); + * const clientKP2 = Keypair.random(); * - * transaction.sign(keypair) - * WebAuth.verifyTxSignedBy(transaction, keypair.publicKey()) - */ -export function verifyTxSignedBy( - transaction: FeeBumpTransaction | Transaction, - accountID: string, -): boolean { - return gatherTxSigners(transaction, [accountID]).length !== 0; -} - -/** - * Checks if a transaction has been signed by one or more of the given signers, - * returning a list of non-repeated signers that were found to have signed the - * given transaction. + * // Challenge, possibly built in the server side + * const challenge = WebAuth.buildChallengeTx( + * serverKP, + * clientKP1.publicKey(), + * "SDF", + * 300, + * Networks.TESTNET + * ); * - * @function - * @memberof WebAuth - * @param {Transaction} transaction the signed transaction. - * @param {string[]} signers The signers public keys. - * @returns {string[]} a list of signers that were found to have signed the - * transaction. + * // clock.tick(200); // Simulates a 200 ms delay when communicating from server to client * - * @example - * let keypair1 = Keypair.random(); - * let keypair2 = Keypair.random(); - * const account = new StellarSdk.Account(keypair1.publicKey(), "-1"); + * // Transaction gathered from a challenge, possibly from the client side + * const transaction = TransactionBuilder.fromXDR(challenge, Networks.TESTNET); + * transaction.sign(clientKP1, clientKP2); + * const signedChallenge = transaction + * .toEnvelope() + * .toXDR("base64") + * .toString(); * - * const transaction = new TransactionBuilder(account, { fee: 100 }) - * .setTimeout(30) - * .build(); + * // Defining the threshold and signerSummary + * const threshold = 3; + * const signerSummary = [ + * { + * key: this.clientKP1.publicKey(), + * weight: 1, + * }, + * { + * key: this.clientKP2.publicKey(), + * weight: 2, + * }, + * ]; * - * transaction.sign(keypair1, keypair2) - * WebAuth.gatherTxSigners(transaction, [keypair1.publicKey(), keypair2.publicKey()]) + * // The result below should be equal to [clientKP1.publicKey(), clientKP2.publicKey()] + * WebAuth.verifyChallengeTxThreshold( + * signedChallenge, + * serverKP.publicKey(), + * Networks.TESTNET, + * threshold, + * signerSummary + * ); */ -export function gatherTxSigners( - transaction: FeeBumpTransaction | Transaction, - signers: string[], +export function verifyChallengeTxThreshold( + challengeTx: string, + serverAccountID: string, + networkPassphrase: string, + threshold: number, + signerSummary: ServerApi.AccountRecordSigners[], + homeDomains: string | string[], + webAuthDomain: string, ): string[] { - const hashedSignatureBase = transaction.hash(); - - const txSignatures = [...transaction.signatures]; // shallow copy for safe splicing - const signersFound = new Set(); - - for (const signer of signers) { - if (txSignatures.length === 0) { - break; - } - - let keypair: Keypair; - try { - keypair = Keypair.fromPublicKey(signer); // This can throw a few different errors - } catch (err: any) { - throw new InvalidChallengeError( - `Signer is not a valid address: ${ err.message}`, - ); - } + const signers = signerSummary.map((signer) => signer.key); - for (let i = 0; i < txSignatures.length; i++) { - const decSig = txSignatures[i]; + const signersFound = verifyChallengeTxSigners( + challengeTx, + serverAccountID, + networkPassphrase, + signers, + homeDomains, + webAuthDomain, + ); - if (!decSig.hint().equals(keypair.signatureHint())) { - continue; - } + let weight = 0; + signersFound.forEach((signer) => { + const sigWeight = + signerSummary.find((s) => s.key === signer)?.weight || 0; + weight += sigWeight; + }); - if (keypair.verify(hashedSignatureBase, decSig.signature())) { - signersFound.add(signer); - txSignatures.splice(i, 1); - break; - } - } + if (weight < threshold) { + throw new InvalidChallengeError( + `signers with weight ${weight} do not meet threshold ${threshold}"`, + ); } - return Array.from(signersFound); -} \ No newline at end of file + return signersFound; +} + 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..215041208 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,7 @@ 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(), @@ -138,7 +139,53 @@ describe("Swap Contract Tests", function () { expect(newSimulatedResourceFee).to.be.greaterThan(bumpedResourceFee); }); - it("alice swaps bob 10 A for 1 B", async function() { + 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(), + 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 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), + }); + + 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("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 +199,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 +235,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/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: From d0e3ff8bc183977e61add5a48496c6243043d461 Mon Sep 17 00:00:00 2001 From: Blaine Heffron Date: Tue, 11 Jun 2024 11:17:18 -0400 Subject: [PATCH 05/13] compiler fixes --- src/horizon/trade_aggregation_call_builder.ts | 8 +------- src/rpc/server.ts | 2 -- src/webauth/utils.ts | 2 +- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/horizon/trade_aggregation_call_builder.ts b/src/horizon/trade_aggregation_call_builder.ts index 8072bc31f..f20d0a34a 100644 --- a/src/horizon/trade_aggregation_call_builder.ts +++ b/src/horizon/trade_aggregation_call_builder.ts @@ -82,13 +82,7 @@ export class TradeAggregationCallBuilder extends CallBuilder< * @returns {boolean} true if the resolution is allowed */ private isValidResolution(resolution: number): boolean { - // eslint-disable-next-line consistent-return - allowedResolutions.forEach((allowed) => { - if (allowed === resolution) { - return true; - } - }); - return false; + return allowedResolutions.some((allowed) => allowed === resolution); } /** diff --git a/src/rpc/server.ts b/src/rpc/server.ts index 42a46cab9..b00594364 100644 --- a/src/rpc/server.ts +++ b/src/rpc/server.ts @@ -24,8 +24,6 @@ import { parseRawLedgerEntries, parseRawEvents } from './parsers'; -import type { Config } from '../config'; - export const SUBMIT_TRANSACTION_TIMEOUT = 60 * 1000; diff --git a/src/webauth/utils.ts b/src/webauth/utils.ts index 359be9079..3c58a1c07 100644 --- a/src/webauth/utils.ts +++ b/src/webauth/utils.ts @@ -567,7 +567,7 @@ export function verifyChallengeTxSigners( ); } - let clientSigningKey; + let clientSigningKey: string | undefined; tx.operations.forEach((op) => { if (op.type === "manageData" && op.name === "client_domain") { if (clientSigningKey) { From ddb5ed2f503ff25d65078db7faa29d32acdb874d Mon Sep 17 00:00:00 2001 From: Blaine Heffron Date: Tue, 11 Jun 2024 11:58:42 -0400 Subject: [PATCH 06/13] cleanup --- src/.eslintrc.js | 1 - src/errors.ts | 2 +- src/rpc/server.ts | 1 + src/webauth/errors.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/.eslintrc.js b/src/.eslintrc.js index 5754b371a..b110c461a 100644 --- a/src/.eslintrc.js +++ b/src/.eslintrc.js @@ -14,7 +14,6 @@ module.exports = { "import/prefer-default-export": 0, "node/no-unsupported-features/es-syntax": 0, "node/no-unsupported-features/es-builtins": 0, - "no-proto": 0, camelcase: 0, "class-methods-use-this": 0, "linebreak-style": 0, diff --git a/src/errors.ts b/src/errors.ts index d97b118d1..21f877aff 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -3,7 +3,7 @@ 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: { diff --git a/src/rpc/server.ts b/src/rpc/server.ts index b00594364..482ac0223 100644 --- a/src/rpc/server.ts +++ b/src/rpc/server.ts @@ -86,6 +86,7 @@ function findCreatedAccountSequenceInTransactionMeta( 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. 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; From e489abc6a7dffabdc71f7a56f31f718511dfdc15 Mon Sep 17 00:00:00 2001 From: Blaine Heffron Date: Tue, 11 Jun 2024 12:12:13 -0400 Subject: [PATCH 07/13] merge eslintrc files --- .eslintrc.js | 41 +++++++++++++++++++++++++++++++++++++++ package.json | 2 +- src/.eslintrc.js | 50 ------------------------------------------------ 3 files changed, 42 insertions(+), 51 deletions(-) delete mode 100644 src/.eslintrc.js diff --git a/.eslintrc.js b/.eslintrc.js index 16fdb5cab..116970f49 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,11 +4,52 @@ 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", + "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, + "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/package.json b/package.json index 6cc182c92..3d1cc2e23 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "test:node": "yarn _nyc mocha --recursive 'test/unit/**/*.js'", "test:integration": "yarn _nyc mocha --recursive 'test/integration/**/*.js'", "test:browser": "karma start config/karma.conf.js", - "fmt": "yarn eslint -c src/.eslintrc.js src/ --fix && yarn _prettier", + "fmt": "yarn eslint -c .eslintrc.js src/ --fix && yarn _prettier", "preversion": "yarn clean && yarn _prettier && yarn build:prod && yarn test", "prepare": "yarn build:prod", "_build": "yarn build:node && yarn build:test && yarn build:browser", diff --git a/src/.eslintrc.js b/src/.eslintrc.js deleted file mode 100644 index b110c461a..000000000 --- a/src/.eslintrc.js +++ /dev/null @@ -1,50 +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, - "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", - "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, - "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 }], - }, -}; From e53d9078df700b4083ad7dac40c299aa2b30efa5 Mon Sep 17 00:00:00 2001 From: Blaine Heffron Date: Mon, 17 Jun 2024 15:26:52 -0400 Subject: [PATCH 08/13] remove unneeded config, more targed ignores --- config/tsconfig.json | 5 +---- src/contract/spec.ts | 11 ++++++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/config/tsconfig.json b/config/tsconfig.json index 12b4ea6b7..76fdf22b3 100644 --- a/config/tsconfig.json +++ b/config/tsconfig.json @@ -11,8 +11,5 @@ "outDir": "../lib", "target": "es6" }, - "include": [ - "../src", - "../src/.eslintrc.js" - ] + "include": ["../src"] } \ No newline at end of file diff --git a/src/contract/spec.ts b/src/contract/spec.ts index 01f8f841d..863fbac1d 100644 --- a/src/contract/spec.ts +++ b/src/contract/spec.ts @@ -1,5 +1,3 @@ -/* eslint-disable no-fallthrough */ -/* eslint-disable default-case */ import type { JSONSchema7, JSONSchema7Definition } from "json-schema"; import { ScIntType, @@ -165,6 +163,7 @@ const PRIMITIVE_DEFINITONS: { [key: string]: JSONSchema7Definition } = { }, }; +/* eslint-disable default-case */ /** * @param typeDef type to convert to json schema reference * @returns {JSONSchema7} a schema describing the type @@ -297,6 +296,7 @@ function typeRef(typeDef: xdr.ScSpecTypeDef): JSONSchema7 { } return { $ref: `#/definitions/${ref}` }; } +/* eslint-enable default-case */ type Func = { input: JSONSchema7; output: JSONSchema7 }; @@ -383,6 +383,7 @@ function functionToJsonSchema(func: xdr.ScSpecFunctionV0): Func { }; } +/* eslint-disable default-case */ function unionToJsonSchema(udt: xdr.ScSpecUdtUnionV0): any { const description = udt.doc().toString(); const cases = udt.cases(); @@ -431,7 +432,7 @@ function unionToJsonSchema(udt: xdr.ScSpecUdtUnionV0): any { } return res; } - +/* eslint-enable default-case */ /** @@ -918,6 +919,7 @@ export class Spec { 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 @@ -1013,6 +1015,7 @@ export class Spec { )} to native type from type ${t.name}`, ); } + /* eslint-enable no-fallthrough*/ } private scValUdtToNative(scv: xdr.ScVal, udt: xdr.ScSpecTypeUdt): any { @@ -1119,6 +1122,7 @@ export class Spec { */ 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: { @@ -1149,6 +1153,7 @@ export class Spec { } } }); + /* eslint-enable default-case */ const res: JSONSchema7 = { $schema: "http://json-schema.org/draft-07/schema#", definitions: { ...PRIMITIVE_DEFINITONS, ...definitions }, From 81d56318402408a75279386ca40198ade8b63be0 Mon Sep 17 00:00:00 2001 From: Blaine Heffron Date: Mon, 17 Jun 2024 15:47:11 -0400 Subject: [PATCH 09/13] fix dependency cyle and other fmt fixes --- src/contract/assembled_transaction.ts | 7 +- src/contract/index.ts | 3 +- src/contract/sent_transaction.ts | 3 +- src/contract/types.ts | 10 +- src/contract/utils.ts | 13 +-- test/e2e/src/test-swap.js | 40 ++++--- .../soroban/assembled_transaction_test.js | 102 +++++++++--------- 7 files changed, 91 insertions(+), 87 deletions(-) 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/types.ts b/src/contract/types.ts index d329bd0da..68ef9b24e 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; @@ -122,4 +121,11 @@ export type AssembledTransactionOptions = MethodOptions & method: string; 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/test/e2e/src/test-swap.js b/test/e2e/src/test-swap.js index 215041208..b46a4eec0 100644 --- a/test/e2e/src/test-swap.js +++ b/test/e2e/src/test-swap.js @@ -81,16 +81,22 @@ 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 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/); - } - }); + 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 () { @@ -114,17 +120,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(), }); @@ -132,7 +138,7 @@ 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); @@ -160,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(), }); @@ -178,7 +184,7 @@ 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); 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); + }); + }); }); From 52a3acae6817bb1e2e298c63cfad7f652ffff35c Mon Sep 17 00:00:00 2001 From: Blaine Heffron Date: Mon, 17 Jun 2024 15:53:24 -0400 Subject: [PATCH 10/13] minor formatting --- src/contract/types.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/contract/types.ts b/src/contract/types.ts index 68ef9b24e..b97231b55 100644 --- a/src/contract/types.ts +++ b/src/contract/types.ts @@ -121,11 +121,10 @@ export type AssembledTransactionOptions = MethodOptions & method: string; args?: any[]; parseResultXdr: (xdr: xdr.ScVal) => T; - };/** - * The default timeout for waiting for a transaction to be included in a block. - */ - - + }; + +/** + * 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"; - From 4c5c19a4a763186f1fae845cdb3d08bdcc12fbfd Mon Sep 17 00:00:00 2001 From: Blaine Heffron Date: Thu, 20 Jun 2024 16:14:50 -0400 Subject: [PATCH 11/13] fix jsdoc typo and rule for line breaks --- .eslintrc.js | 1 + src/horizon/account_response.ts | 22 ---------------------- src/horizon/call_builder.ts | 2 +- 3 files changed, 2 insertions(+), 23 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 116970f49..e6d0223b0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -38,6 +38,7 @@ module.exports = { "no-use-before-define": 0, "prefer-destructuring": 0, "lines-between-class-members": 0, + "@typescript-eslint/lines-between-class-members": "off", "spaced-comment": 0, // WARN diff --git a/src/horizon/account_response.ts b/src/horizon/account_response.ts index 932d14915..def7ecccb 100644 --- a/src/horizon/account_response.ts +++ b/src/horizon/account_response.ts @@ -18,61 +18,39 @@ import { ServerApi } from "./server_api"; */ export class AccountResponse { public readonly id!: string; - public readonly paging_token!: string; - public readonly account_id!: string; - public sequence!: string; - public readonly sequence_ledger?: number; - public readonly sequence_time?: string; - public readonly subentry_count!: number; - public readonly home_domain?: string; - public readonly inflation_destination?: string; - public readonly last_modified_ledger!: number; - public readonly last_modified_time!: string; - public readonly thresholds!: HorizonApi.AccountThresholds; - public readonly flags!: HorizonApi.Flags; - public readonly balances!: HorizonApi.BalanceLine[]; - public readonly signers!: ServerApi.AccountRecordSigners[]; - public readonly data!: (options: { value: string; }) => Promise<{ value: string }>; - public readonly data_attr!: Record; - public readonly effects!: ServerApi.CallCollectionFunction< ServerApi.EffectRecord >; - public readonly offers!: ServerApi.CallCollectionFunction< ServerApi.OfferRecord >; - public readonly operations!: ServerApi.CallCollectionFunction< ServerApi.OperationRecord >; - public readonly payments!: ServerApi.CallCollectionFunction< ServerApi.PaymentOperationRecord >; - public readonly trades!: ServerApi.CallCollectionFunction< ServerApi.TradeRecord >; - private readonly _baseAccount: BaseAccount; constructor(response: ServerApi.AccountRecord) { diff --git a/src/horizon/call_builder.ts b/src/horizon/call_builder.ts index 5c9a0d96b..4006fbf46 100644 --- a/src/horizon/call_builder.ts +++ b/src/horizon/call_builder.ts @@ -204,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 "recordsNumber" 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 { From d996afbe5004a22613ef7e724721d9d287e94b96 Mon Sep 17 00:00:00 2001 From: Blaine Heffron Date: Thu, 20 Jun 2024 16:42:33 -0400 Subject: [PATCH 12/13] remove no-default and add back Durability.Persistent default --- .eslintrc.js | 1 + src/rpc/server.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index e6d0223b0..4fcd5f068 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -30,6 +30,7 @@ module.exports = { "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, diff --git a/src/rpc/server.ts b/src/rpc/server.ts index 482ac0223..7a63d6bb3 100644 --- a/src/rpc/server.ts +++ b/src/rpc/server.ts @@ -197,7 +197,7 @@ export class Server { * data to load as a strkey (`C...` form), a {@link Contract}, or an * {@link Address} instance * @param {xdr.ScVal} key the key of the contract data to load - * @param {Durability} [durability] the "durability + * @param {Durability} [durability=Durability.Persistent] the "durability * keyspace" that this ledger key belongs to, which is either 'temporary' * or 'persistent' (the default), see {@link Durability}. * From 966e4e9f203e0107567b07568ce6e1e1d00225fc Mon Sep 17 00:00:00 2001 From: Blaine Heffron Date: Thu, 20 Jun 2024 16:44:25 -0400 Subject: [PATCH 13/13] revert webauth/utils.ts changes for now --- src/webauth/utils.ts | 449 ++++++++++++++++++++++--------------------- 1 file changed, 226 insertions(+), 223 deletions(-) diff --git a/src/webauth/utils.ts b/src/webauth/utils.ts index 3c58a1c07..3e6d13c73 100644 --- a/src/webauth/utils.ts +++ b/src/webauth/utils.ts @@ -13,13 +13,10 @@ import { TransactionBuilder, } from "@stellar/stellar-base"; -import type { Networks } from "@stellar/stellar-base"; import { Utils } from "../utils"; import { InvalidChallengeError } from "./errors"; import { ServerApi } from "../horizon/server_api"; -/* eslint-disable jsdoc/no-undefined-types */ - /** * Returns a valid [SEP-10](https://stellar.org/protocol/sep-10) challenge * transaction which you can use for Stellar Web Authentication. @@ -32,7 +29,7 @@ import { ServerApi } from "../horizon/server_api"; * (M...) that the wallet wishes to authenticate with the server. * @param {string} homeDomain The fully qualified domain name of the service * requiring authentication - * @param {number} [timeout] Challenge duration (default to 5 minutes). + * @param {number} [timeout=300] Challenge duration (default to 5 minutes). * @param {string} networkPassphrase The network passphrase. If you pass this * argument then timeout is required. * @param {string} webAuthDomain The fully qualified domain name of the service @@ -66,7 +63,6 @@ export function buildChallengeTx( serverKeypair: Keypair, clientAccountID: string, homeDomain: string, - // eslint-disable-next-line @typescript-eslint/default-param-last timeout: number = 300, networkPassphrase: string, webAuthDomain: string, @@ -137,97 +133,6 @@ export function buildChallengeTx( .toString(); } -/** - * Checks if a transaction has been signed by one or more of the given signers, - * returning a list of non-repeated signers that were found to have signed the - * given transaction. - * - * @function - * @memberof WebAuth - * @param {Transaction} transaction the signed transaction. - * @param {string[]} signers The signers public keys. - * @returns {string[]} a list of signers that were found to have signed the - * transaction. - * - * @example - * let keypair1 = Keypair.random(); - * let keypair2 = Keypair.random(); - * const account = new StellarSdk.Account(keypair1.publicKey(), "-1"); - * - * const transaction = new TransactionBuilder(account, { fee: 100 }) - * .setTimeout(30) - * .build(); - * - * transaction.sign(keypair1, keypair2) - * WebAuth.gatherTxSigners(transaction, [keypair1.publicKey(), keypair2.publicKey()]) - */ -export function gatherTxSigners( - transaction: FeeBumpTransaction | Transaction, - signers: string[], -): string[] { - const hashedSignatureBase = transaction.hash(); - - const txSignatures = [...transaction.signatures]; // shallow copy for safe splicing - const signersFound = new Set(); - - // eslint-disable-next-line no-restricted-syntax - for (const signer of signers) { - if (txSignatures.length === 0) { - break; - } - - let keypair: Keypair; - try { - keypair = Keypair.fromPublicKey(signer); // This can throw a few different errors - } catch (err: any) { - throw new InvalidChallengeError( - `Signer is not a valid address: ${ err.message}`, - ); - } - - for (let i = 0; i < txSignatures.length; i+=1) { - const decSig = txSignatures[i]; - - if (!decSig.hint().equals(keypair.signatureHint())) { - // eslint-disable-next-line no-continue - continue; - } - - if (keypair.verify(hashedSignatureBase, decSig.signature())) { - signersFound.add(signer); - txSignatures.splice(i, 1); - break; - } - } - } - - return Array.from(signersFound); -} - -/** - * Verifies if a transaction was signed by the given account id. - * - * @function - * @memberof WebAuth - * - * @example - * let keypair = Keypair.random(); - * const account = new StellarSdk.Account(keypair.publicKey(), "-1"); - * - * const transaction = new TransactionBuilder(account, { fee: 100 }) - * .setTimeout(30) - * .build(); - * - * transaction.sign(keypair) - * WebAuth.verifyTxSignedBy(transaction, keypair.publicKey()) - */ -export function verifyTxSignedBy( - transaction: FeeBumpTransaction | Transaction, - accountID: string, -): boolean { - return gatherTxSigners(transaction, [accountID]).length !== 0; -} - /** * Reads a SEP 10 challenge transaction and returns the decoded transaction and * client account ID contained within. @@ -412,7 +317,7 @@ export function readChallengeTx( } // verify any subsequent operations are manage data ops and source account is the server - subsequentOperations.forEach((op) => { + for (const op of subsequentOperations) { if (op.type !== "manageData") { throw new InvalidChallengeError( "The transaction has operations that are not of type 'manageData'", @@ -435,7 +340,7 @@ export function readChallengeTx( ); } } - }); + } if (!verifyTxSignedBy(transaction, serverAccountID)) { throw new InvalidChallengeError( @@ -446,6 +351,131 @@ export function readChallengeTx( return { tx: transaction, clientAccountID, matchedHomeDomain, memo }; } +/** + * Verifies that for a SEP-10 challenge transaction all signatures on the + * transaction are accounted for and that the signatures meet a threshold on an + * account. A transaction is verified if it is signed by the server account, and + * all other signatures match a signer that has been provided as an argument, + * and those signatures meet a threshold on the account. + * + * Signers that are not prefixed as an address/account ID strkey (G...) will be + * ignored. + * + * Errors will be raised if: + * - The transaction is invalid according to {@link readChallengeTx}. + * - No client signatures are found on the transaction. + * - One or more signatures in the transaction are not identifiable as the + * server account or one of the signers provided in the arguments. + * - The signatures are all valid but do not meet the threshold. + * + * @function + * @memberof WebAuth + * + * @param {string} challengeTx SEP0010 challenge transaction in base64. + * @param {string} serverAccountID The server's stellar account (public key). + * @param {string} networkPassphrase The network passphrase, e.g.: 'Test SDF + * Network ; September 2015' (see {@link Networks}). + * @param {number} threshold The required signatures threshold for verifying + * this transaction. + * @param {ServerApi.AccountRecordSigners[]} signerSummary a map of all + * authorized signers to their weights. It's used to validate if the + * transaction signatures have met the given threshold. + * @param {string|string[]} [homeDomains] The home domain(s) that should be + * included in the first Manage Data operation's string key. Required in + * verifyChallengeTxSigners() => readChallengeTx(). + * @param {string} webAuthDomain The home domain that is expected to be included + * as the value of the Manage Data operation with the 'web_auth_domain' key, + * if present. Used in verifyChallengeTxSigners() => readChallengeTx(). + * + * @returns {string[]} The list of signers public keys that have signed the + * transaction, excluding the server account ID, given that the threshold was + * met. + * + * @see [SEP-10: Stellar Web Auth](https://stellar.org/protocol/sep-10). + * @example + * import { Networks, TransactionBuilder, WebAuth } from 'stellar-sdk'; + * + * const serverKP = Keypair.random(); + * const clientKP1 = Keypair.random(); + * const clientKP2 = Keypair.random(); + * + * // Challenge, possibly built in the server side + * const challenge = WebAuth.buildChallengeTx( + * serverKP, + * clientKP1.publicKey(), + * "SDF", + * 300, + * Networks.TESTNET + * ); + * + * // clock.tick(200); // Simulates a 200 ms delay when communicating from server to client + * + * // Transaction gathered from a challenge, possibly from the client side + * const transaction = TransactionBuilder.fromXDR(challenge, Networks.TESTNET); + * transaction.sign(clientKP1, clientKP2); + * const signedChallenge = transaction + * .toEnvelope() + * .toXDR("base64") + * .toString(); + * + * // Defining the threshold and signerSummary + * const threshold = 3; + * const signerSummary = [ + * { + * key: this.clientKP1.publicKey(), + * weight: 1, + * }, + * { + * key: this.clientKP2.publicKey(), + * weight: 2, + * }, + * ]; + * + * // The result below should be equal to [clientKP1.publicKey(), clientKP2.publicKey()] + * WebAuth.verifyChallengeTxThreshold( + * signedChallenge, + * serverKP.publicKey(), + * Networks.TESTNET, + * threshold, + * signerSummary + * ); + */ +export function verifyChallengeTxThreshold( + challengeTx: string, + serverAccountID: string, + networkPassphrase: string, + threshold: number, + signerSummary: ServerApi.AccountRecordSigners[], + homeDomains: string | string[], + webAuthDomain: string, +): string[] { + const signers = signerSummary.map((signer) => signer.key); + + const signersFound = verifyChallengeTxSigners( + challengeTx, + serverAccountID, + networkPassphrase, + signers, + homeDomains, + webAuthDomain, + ); + + let weight = 0; + for (const signer of signersFound) { + const sigWeight = + signerSummary.find((s) => s.key === signer)?.weight || 0; + weight += sigWeight; + } + + if (weight < threshold) { + throw new InvalidChallengeError( + `signers with weight ${weight} do not meet threshold ${threshold}"`, + ); + } + + return signersFound; +} + /** * Verifies that for a SEP 10 challenge transaction all signatures on the * transaction are accounted for. A transaction is verified if it is signed by @@ -542,23 +572,31 @@ export function verifyChallengeTxSigners( serverKP = Keypair.fromPublicKey(serverAccountID); // can throw 'Invalid Stellar public key' } catch (err: any) { throw new Error( - `Couldn't infer keypair from the provided 'serverAccountID': ${ - err.message}`, + "Couldn't infer keypair from the provided 'serverAccountID': " + + err.message, ); } // Deduplicate the client signers and ensure the server is not included // anywhere we check or output the list of signers. - // Ignore the server signer if it is in the signers list. It's - // important when verifying signers of a challenge transaction that we - // only verify and return client signers. If an account has the server - // as a signer the server should not play a part in the authentication - // of the client. - const clientSigners = new Set( - signers.filter( - (signer) => signer !== serverKP.publicKey() && signer.charAt(0) === "G" - ) - ); + const clientSigners = new Set(); + for (const signer of signers) { + // Ignore the server signer if it is in the signers list. It's + // important when verifying signers of a challenge transaction that we + // only verify and return client signers. If an account has the server + // as a signer the server should not play a part in the authentication + // of the client. + if (signer === serverKP.publicKey()) { + continue; + } + + // Ignore non-G... account/address signers. + if (signer.charAt(0) !== "G") { + continue; + } + + clientSigners.add(signer); + } // Don't continue if none of the signers provided are in the final list. if (clientSigners.size === 0) { @@ -567,8 +605,8 @@ export function verifyChallengeTxSigners( ); } - let clientSigningKey: string | undefined; - tx.operations.forEach((op) => { + let clientSigningKey; + for (const op of tx.operations) { if (op.type === "manageData" && op.name === "client_domain") { if (clientSigningKey) { throw new InvalidChallengeError( @@ -577,7 +615,7 @@ export function verifyChallengeTxSigners( } clientSigningKey = op.source; } - }); + } // Verify all the transaction's signers (server and client) in one // hit. We do this in one hit here even though the server signature was @@ -595,19 +633,19 @@ export function verifyChallengeTxSigners( let serverSignatureFound = false; let clientSigningKeySignatureFound = false; - signersFound.forEach((signer) => { + for (const signer of signersFound) { if (signer === serverKP.publicKey()) { serverSignatureFound = true; } if (signer === clientSigningKey) { clientSigningKeySignatureFound = true; } - }); + } // Confirm we matched a signature to the server signer. if (!serverSignatureFound) { throw new InvalidChallengeError( - `Transaction not signed by server: '${ serverKP.publicKey() }'`, + "Transaction not signed by server: '" + serverKP.publicKey() + "'", ); } @@ -643,129 +681,94 @@ export function verifyChallengeTxSigners( return signersFound; } - /** - * Verifies that for a SEP-10 challenge transaction all signatures on the - * transaction are accounted for and that the signatures meet a threshold on an - * account. A transaction is verified if it is signed by the server account, and - * all other signatures match a signer that has been provided as an argument, - * and those signatures meet a threshold on the account. - * - * Signers that are not prefixed as an address/account ID strkey (G...) will be - * ignored. - * - * Errors will be raised if: - * - The transaction is invalid according to {@link readChallengeTx}. - * - No client signatures are found on the transaction. - * - One or more signatures in the transaction are not identifiable as the - * server account or one of the signers provided in the arguments. - * - The signatures are all valid but do not meet the threshold. + * Verifies if a transaction was signed by the given account id. * * @function * @memberof WebAuth + * @param {Transaction} transaction + * @param {string} accountID + * @returns {boolean}. * - * @param {string} challengeTx SEP0010 challenge transaction in base64. - * @param {string} serverAccountID The server's stellar account (public key). - * @param {string} networkPassphrase The network passphrase, e.g.: 'Test SDF - * Network ; September 2015' (see {@link Networks}). - * @param {number} threshold The required signatures threshold for verifying - * this transaction. - * @param {ServerApi.AccountRecordSigners[]} signerSummary a map of all - * authorized signers to their weights. It's used to validate if the - * transaction signatures have met the given threshold. - * @param {string|string[]} [homeDomains] The home domain(s) that should be - * included in the first Manage Data operation's string key. Required in - * verifyChallengeTxSigners() => readChallengeTx(). - * @param {string} webAuthDomain The home domain that is expected to be included - * as the value of the Manage Data operation with the 'web_auth_domain' key, - * if present. Used in verifyChallengeTxSigners() => readChallengeTx(). - * - * @returns {string[]} The list of signers public keys that have signed the - * transaction, excluding the server account ID, given that the threshold was - * met. - * - * @see [SEP-10: Stellar Web Auth](https://stellar.org/protocol/sep-10). * @example - * import { Networks, TransactionBuilder, WebAuth } from 'stellar-sdk'; + * let keypair = Keypair.random(); + * const account = new StellarSdk.Account(keypair.publicKey(), "-1"); * - * const serverKP = Keypair.random(); - * const clientKP1 = Keypair.random(); - * const clientKP2 = Keypair.random(); + * const transaction = new TransactionBuilder(account, { fee: 100 }) + * .setTimeout(30) + * .build(); * - * // Challenge, possibly built in the server side - * const challenge = WebAuth.buildChallengeTx( - * serverKP, - * clientKP1.publicKey(), - * "SDF", - * 300, - * Networks.TESTNET - * ); + * transaction.sign(keypair) + * WebAuth.verifyTxSignedBy(transaction, keypair.publicKey()) + */ +export function verifyTxSignedBy( + transaction: FeeBumpTransaction | Transaction, + accountID: string, +): boolean { + return gatherTxSigners(transaction, [accountID]).length !== 0; +} + +/** + * Checks if a transaction has been signed by one or more of the given signers, + * returning a list of non-repeated signers that were found to have signed the + * given transaction. * - * // clock.tick(200); // Simulates a 200 ms delay when communicating from server to client + * @function + * @memberof WebAuth + * @param {Transaction} transaction the signed transaction. + * @param {string[]} signers The signers public keys. + * @returns {string[]} a list of signers that were found to have signed the + * transaction. * - * // Transaction gathered from a challenge, possibly from the client side - * const transaction = TransactionBuilder.fromXDR(challenge, Networks.TESTNET); - * transaction.sign(clientKP1, clientKP2); - * const signedChallenge = transaction - * .toEnvelope() - * .toXDR("base64") - * .toString(); + * @example + * let keypair1 = Keypair.random(); + * let keypair2 = Keypair.random(); + * const account = new StellarSdk.Account(keypair1.publicKey(), "-1"); * - * // Defining the threshold and signerSummary - * const threshold = 3; - * const signerSummary = [ - * { - * key: this.clientKP1.publicKey(), - * weight: 1, - * }, - * { - * key: this.clientKP2.publicKey(), - * weight: 2, - * }, - * ]; + * const transaction = new TransactionBuilder(account, { fee: 100 }) + * .setTimeout(30) + * .build(); * - * // The result below should be equal to [clientKP1.publicKey(), clientKP2.publicKey()] - * WebAuth.verifyChallengeTxThreshold( - * signedChallenge, - * serverKP.publicKey(), - * Networks.TESTNET, - * threshold, - * signerSummary - * ); + * transaction.sign(keypair1, keypair2) + * WebAuth.gatherTxSigners(transaction, [keypair1.publicKey(), keypair2.publicKey()]) */ -export function verifyChallengeTxThreshold( - challengeTx: string, - serverAccountID: string, - networkPassphrase: string, - threshold: number, - signerSummary: ServerApi.AccountRecordSigners[], - homeDomains: string | string[], - webAuthDomain: string, +export function gatherTxSigners( + transaction: FeeBumpTransaction | Transaction, + signers: string[], ): string[] { - const signers = signerSummary.map((signer) => signer.key); + const hashedSignatureBase = transaction.hash(); - const signersFound = verifyChallengeTxSigners( - challengeTx, - serverAccountID, - networkPassphrase, - signers, - homeDomains, - webAuthDomain, - ); + const txSignatures = [...transaction.signatures]; // shallow copy for safe splicing + const signersFound = new Set(); - let weight = 0; - signersFound.forEach((signer) => { - const sigWeight = - signerSummary.find((s) => s.key === signer)?.weight || 0; - weight += sigWeight; - }); + for (const signer of signers) { + if (txSignatures.length === 0) { + break; + } - if (weight < threshold) { - throw new InvalidChallengeError( - `signers with weight ${weight} do not meet threshold ${threshold}"`, - ); - } + let keypair: Keypair; + try { + keypair = Keypair.fromPublicKey(signer); // This can throw a few different errors + } catch (err: any) { + throw new InvalidChallengeError( + "Signer is not a valid address: " + err.message, + ); + } - return signersFound; -} + for (let i = 0; i < txSignatures.length; i++) { + const decSig = txSignatures[i]; + + if (!decSig.hint().equals(keypair.signatureHint())) { + continue; + } + + if (keypair.verify(hashedSignatureBase, decSig.signature())) { + signersFound.add(signer); + txSignatures.splice(i, 1); + break; + } + } + } + return Array.from(signersFound); +} \ No newline at end of file