diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 514f4173f..b3a08c969 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,7 @@ jobs: fail-fast: false max-parallel: 4 matrix: - node-version: [16, 18, 20] + node-version: [16, 18] steps: - name: Checkout diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b0c06473..133b2b77b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ A breaking change will get clearly marked in this log. ## Unreleased +### Breaking Changes +* The `soroban-client` library ([stellar/js-soroban-client](https://github.com/stellar/js-soroban-client)) has been merged into this package, causing significant breaking changes in the module structure ([#860](https://github.com/stellar/js-stellar-sdk/pull/860)): + + - The namespaces have changed to move each server-dependent component into its own module. Shared components (e.g. `TransactionBuilder`) are still in the top level, Horizon-specific interactions are in the `Horizon` namespace (i.e. `Server` is now `Horizon.Server`), and new Soroban RPC interactions are in the `SorobanRpc` namespace. + - There is a [detailed migration guide](https://gist.github.com/Shaptic/5ce4f16d9cce7118f391fbde398c2f30) available to outline both the literal (i.e. necessary code changes) and philosophical (i.e. how to find certain functionality) changes needed to adapt to this merge. + ## [v11.0.0-beta.4](https://github.com/stellar/js-stellar-sdk/compare/v11.0.0-beta.3...v11.0.0-beta.4) diff --git a/package.json b/package.json index cf4780f36..9293b307c 100644 --- a/package.json +++ b/package.json @@ -28,21 +28,22 @@ "build:prod": "cross-env NODE_ENV=production yarn _build", "build:node": "yarn _babel && yarn build:ts", "build:ts": "tsc -p ./config/tsconfig.json", + "build:test": "tsc -p ./test/tsconfig.json", "build:browser": "webpack -c config/webpack.config.browser.js", "build:docs": "cross-env NODE_ENV=docs yarn _babel", "clean": "rm -rf lib/ dist/ coverage/ .nyc_output/ jsdoc/", "docs": "yarn build:docs && jsdoc -c ./config/.jsdoc.json --verbose", - "test": "yarn test:node && yarn test:integration && yarn test:browser", + "test": "yarn build:test && yarn test:node && yarn test:integration && yarn test:browser", "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", "preversion": "yarn clean && yarn fmt && yarn build:prod && yarn test", "prepare": "yarn build:prod", - "_build": "yarn build:node && yarn build:browser", + "_build": "yarn build:node && yarn build:test && yarn build:browser", "_babel": "babel --extensions '.ts' --out-dir lib/ src/", "_nyc": "nyc --nycrc-path config/.nycrc", - "_prettier": "prettier --ignore-path config/.prettierignore --write './**/*.js'" + "_prettier": "prettier --ignore-path config/.prettierignore --write './test/**/*.js'" }, "husky": { "hooks": { @@ -51,8 +52,7 @@ }, "lint-staged": { "**/*.{js,json,ts}": [ - "yarn fmt", - "yarn lint" + "yarn fmt" ] }, "mocha": { @@ -66,7 +66,7 @@ ], "sort": true, "recursive": true, - "timeout": 60000 + "timeout": 30000 }, "nyc": { "instrument": false, @@ -86,11 +86,14 @@ "@definitelytyped/dtslint": "^0.0.182", "@istanbuljs/nyc-config-babel": "3.0.0", "@stellar/tsconfig": "^1.0.2", + "@types/chai": "^4.3.6", "@types/detect-node": "^2.0.0", "@types/eventsource": "^1.1.12", "@types/lodash": "^4.14.199", + "@types/mocha": "^10.0.2", "@types/node": "^20.8.2", "@types/randombytes": "^2.0.1", + "@types/sinon": "^10.0.19", "@types/urijs": "^1.19.20", "@typescript-eslint/parser": "^6.7.4", "axios-mock-adapter": "^1.22.0", @@ -143,7 +146,7 @@ "bignumber.js": "^9.1.2", "eventsource": "^2.0.2", "randombytes": "^2.1.0", - "stellar-base": "10.0.0-beta.2", + "stellar-base": "10.0.0-beta.3", "toml": "^3.0.0", "urijs": "^1.19.1" } diff --git a/src/contract_spec.ts b/src/contract_spec.ts new file mode 100644 index 000000000..eb48559b8 --- /dev/null +++ b/src/contract_spec.ts @@ -0,0 +1,700 @@ +import { + Address, + Contract, + ScIntType, + XdrLargeInt, + scValToBigInt, + xdr +} from 'stellar-base'; + +export interface Union { + tag: string; + values?: T; +} + +/** + * 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 + * 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 ContractSpec { + public entries: xdr.ScSpecEntry[] = []; + + /** + * Constructs a new ContractSpec from an array of XDR spec entries. + * + * @param {xdr.ScSpecEntry[] | string[]} entries the XDR spec entries + * + * @throws {Error} if entries is invalid + */ + constructor(entries: xdr.ScSpecEntry[] | string[]) { + if (entries.length == 0) { + throw new Error('Contract spec must have at least one entry'); + } + let entry = entries[0]; + if (typeof entry === 'string') { + this.entries = (entries as string[]).map((s) => + xdr.ScSpecEntry.fromXDR(s, 'base64') + ); + } else { + this.entries = entries as xdr.ScSpecEntry[]; + } + } + /** + * Gets the XDR function spec for the given function name. + * + * @param {string} name the name of the function + * @returns {xdr.ScSpecFunctionV0} the function spec + * + * @throws {Error} if no function with the given name exists + */ + getFunc(name: string): xdr.ScSpecFunctionV0 { + let entry = this.findEntry(name); + if ( + entry.switch().value !== xdr.ScSpecEntryKind.scSpecEntryFunctionV0().value + ) { + throw new Error(`${name} is not a function`); + } + return entry.value() as xdr.ScSpecFunctionV0; + } + + /** + * Converts native JS arguments to ScVals for calling a contract function. + * + * @param {string} name the name of the function + * @param {Object} args the arguments object + * @returns {xdr.ScVal[]} the converted arguments + * + * @throws {Error} if argument is missing or incorrect type + * + * @example + * ```js + * const args = { + * arg1: 'value1', + * arg2: 1234 + * }; + * const scArgs = contractSpec.funcArgsToScVals('funcName', args); + * ``` + */ + funcArgsToScVals(name: string, args: object): xdr.ScVal[] { + let fn = this.getFunc(name); + return fn + .inputs() + .map((input) => this.nativeToScVal(readObj(args, input), input.type())); + } + + /** + * Converts the result ScVal of a function call to a native JS value. + * + * @param {string} name the name of the function + * @param {xdr.ScVal | string} val_or_base64 the result ScVal or base64 encoded string + * @returns {any} the converted native value + * + * @throws {Error} if return type mismatch or invalid input + * + * @example + * ```js + * const resultScv = 'AAA=='; // Base64 encoded ScVal + * const result = contractSpec.funcResToNative('funcName', resultScv); + * ``` + */ + funcResToNative(name: string, val_or_base64: xdr.ScVal | string): any { + let val = + typeof val_or_base64 === 'string' + ? xdr.ScVal.fromXDR(val_or_base64, 'base64') + : val_or_base64; + let func = this.getFunc(name); + let outputs = func.outputs(); + if (outputs.length === 0) { + let type = val.switch(); + if (type.value !== xdr.ScValType.scvVoid().value) { + throw new Error(`Expected void, got ${type.name}`); + } + return null; + } + if (outputs.length > 1) { + throw new Error(`Multiple outputs not supported`); + } + let output = outputs[0]; + if (output.switch().value === xdr.ScSpecType.scSpecTypeResult().value) { + return 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 { + let entry = this.entries.find( + (entry) => entry.value().name().toString() === name + ); + if (!entry) { + throw new Error(`no such entry: ${name}`); + } + return entry; + } + + /** + * Converts a native JS value to an ScVal based on the given type. + * + * @param {any} val the native JS value + * @param {xdr.ScSpecTypeDef} [ty] the expected type + * @returns {xdr.ScVal} the converted ScVal + * + * @throws {Error} if value cannot be converted to the given type + */ + nativeToScVal(val: any, ty: xdr.ScSpecTypeDef): xdr.ScVal { + let t: xdr.ScSpecType = ty.switch(); + let value = t.value; + if (t.value === xdr.ScSpecType.scSpecTypeUdt().value) { + let udt = ty.value() as xdr.ScSpecTypeUdt; + return this.nativeToUdt(val, udt.name().toString()); + } + if (value === xdr.ScSpecType.scSpecTypeOption().value) { + let opt = ty.value() as xdr.ScSpecTypeOption; + if (val === undefined) { + return xdr.ScVal.scvVoid(); + } + return this.nativeToScVal(val, opt.valueType()); + } + + switch (typeof val) { + case 'object': { + if (val === null) { + switch (value) { + case xdr.ScSpecType.scSpecTypeVoid().value: + return xdr.ScVal.scvVoid(); + default: + throw new TypeError( + `Type ${ty} was not void, but value was null` + ); + } + } + + if (val instanceof xdr.ScVal) { + return val; // should we copy? + } + + if (val instanceof Address) { + if (ty.switch().value !== xdr.ScSpecType.scSpecTypeAddress().value) { + throw new TypeError( + `Type ${ty} was not address, but value was Address` + ); + } + return val.toScVal(); + } + + if (val instanceof Contract) { + if (ty.switch().value !== xdr.ScSpecType.scSpecTypeAddress().value) { + throw new TypeError( + `Type ${ty} was not address, but value was Address` + ); + } + return val.address().toScVal(); + } + + if (val instanceof Uint8Array || Buffer.isBuffer(val)) { + const copy = Uint8Array.from(val); + switch (value) { + case xdr.ScSpecType.scSpecTypeBytesN().value: { + let bytes_n = ty.value() as xdr.ScSpecTypeBytesN; + 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)) { + if (xdr.ScSpecType.scSpecTypeVec().value === value) { + let vec = ty.value() as xdr.ScSpecTypeVec; + let elementType = vec.elementType(); + return xdr.ScVal.scvVec( + val.map((v) => this.nativeToScVal(v, elementType)) + ); + } else if (xdr.ScSpecType.scSpecTypeTuple().value === value) { + let tup = ty.value() as xdr.ScSpecTypeTuple; + let 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])) + ); + } else { + 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`); + } + let scMap = ty.value() as xdr.ScSpecTypeMap; + let map = val as Map; + let entries: xdr.ScMapEntry[] = []; + let values = map.entries(); + let res = values.next(); + while (!res.done) { + let [k, v] = res.value; + let key = this.nativeToScVal(k, scMap.keyType()); + let val = this.nativeToScVal(v, scMap.valueType()); + entries.push(new xdr.ScMapEntry({ key, val })); + 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})`); + } + } + + private nativeToUdt(val: any, name: string): xdr.ScVal { + let 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.value() as xdr.ScSpecUdtEnumV0 + ); + case xdr.ScSpecEntryKind.scSpecEntryUdtStructV0(): + return this.nativeToStruct(val, entry.value() as xdr.ScSpecUdtStructV0); + case xdr.ScSpecEntryKind.scSpecEntryUdtUnionV0(): + return this.nativeToUnion(val, entry.value() as xdr.ScSpecUdtUnionV0); + default: + throw new Error(`failed to parse udt ${name}`); + } + } + + private nativeToUnion( + val: Union, + union_: xdr.ScSpecUdtUnionV0 + ): xdr.ScVal { + let entry_name = val.tag; + let case_ = union_.cases().find((entry) => { + let case_ = entry.value().name().toString(); + return case_ === entry_name; + }); + if (!case_) { + throw new TypeError(`no such enum entry: ${entry_name} in ${union_}`); + } + let 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_.value() as xdr.ScSpecUdtUnionCaseTupleV0).type(); + // if (types.length == 1) { + // return xdr.ScVal.scvVec([ + // key, + // this.nativeToScVal(val.values, types[0]), + // ]); + // } + 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) => + 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}`); + } + + // enum_.cases() + } + + private nativeToStruct(val: any, struct: xdr.ScSpecUdtStructV0): xdr.ScVal { + let 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())) + ); + } + return xdr.ScVal.scvMap( + fields.map((field) => { + let 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); + } + 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 { + let t = typeDef.switch(); + let value = t.value; + if (value === xdr.ScSpecType.scSpecTypeUdt().value) { + return this.scValUdtToNative(scv, typeDef.value() as xdr.ScSpecTypeUdt); + } + // 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) { + let vec = typeDef.value() as xdr.ScSpecTypeVec; + return (scv.vec() ?? []).map((elm) => + this.scValToNative(elm, vec.elementType()) + ) as T; + } else if (value == xdr.ScSpecType.scSpecTypeTuple().value) { + let tuple = typeDef.value() as xdr.ScSpecTypeTuple; + let 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) as T; + + case xdr.ScValType.scvMap().value: { + let map = scv.map() ?? []; + if (value == xdr.ScSpecType.scSpecTypeMap().value) { + let type_ = typeDef.value() as xdr.ScSpecTypeMap; + let keyType = type_.keyType(); + let valueType = type_.valueType(); + return new Map( + map.map((entry) => [ + this.scValToNative(entry.key(), keyType), + this.scValToNative(entry.val(), valueType) + ]) + ) as T; + } + 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.value() as xdr.Uint64)) as T; + + // in the fallthrough case, just return the underlying value directly + default: + throw new TypeError( + `failed to convert ${JSON.stringify( + scv, + null, + 2 + )} to native type from type ${t.name}` + ); + } + } + + private scValUdtToNative(scv: xdr.ScVal, udt: xdr.ScSpecTypeUdt): any { + let entry = this.findEntry(udt.name().toString()); + switch (entry.switch()) { + case xdr.ScSpecEntryKind.scSpecEntryUdtEnumV0(): + return this.enumToNative(scv, entry.value() as xdr.ScSpecUdtEnumV0); + case xdr.ScSpecEntryKind.scSpecEntryUdtStructV0(): + return this.structToNative(scv, entry.value() as xdr.ScSpecUdtStructV0); + case xdr.ScSpecEntryKind.scSpecEntryUdtUnionV0(): + return this.unionToNative(scv, entry.value() as xdr.ScSpecUdtUnionV0); + default: + throw new Error( + `failed to parse udt ${udt.name().toString()}: ${entry}` + ); + } + } + + private unionToNative(val: xdr.ScVal, udt: xdr.ScSpecUdtUnionV0): any { + let vec = val.vec(); + if (!vec) { + throw new Error(`${JSON.stringify(val, null, 2)} is not a vec`); + } + if (vec.length === 0 && udt.cases.length !== 0) { + throw new Error( + `${val} has length 0, but the there are at least one case in the union` + ); + } + let name = vec[0].sym().toString(); + if (vec[0].switch().value != xdr.ScValType.scvSymbol().value) { + throw new Error(`{vec[0]} is not a symbol`); + } + let entry = udt.cases().find(findCase(name)); + if (!entry) { + throw new Error( + `failed to find entry ${name} in union {udt.name().toString()}` + ); + } + let res: Union = { tag: name, values: undefined }; + if ( + entry.switch().value === + xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseTupleV0().value + ) { + let tuple = entry.value() as xdr.ScSpecUdtUnionCaseTupleV0; + let ty = tuple.type(); + //@ts-ignore + let values = []; + for (let i = 0; i < ty.length; i++) { + let v = this.scValToNative(vec[i + 1], ty[i]); + values.push(v); + } + let r = { tag: name, values }; + return r; + } + return res; + } + private structToNative(val: xdr.ScVal, udt: xdr.ScSpecUdtStructV0): any { + let res: any = {}; + let fields = udt.fields(); + if (fields.some(isNumeric)) { + let r = val + .vec() + ?.map((entry, i) => this.scValToNative(entry, fields[i].type())); + return r; + } + val.map()?.forEach((entry, i) => { + let field = fields[i]; + res[field.name().toString()] = this.scValToNative( + entry.val(), + field.type() + ); + }); + return res; + } + + private enumToNative(scv: xdr.ScVal, udt: xdr.ScSpecUdtEnumV0): any { + if (scv.switch().value !== xdr.ScValType.scvU32().value) { + throw new Error(`Enum must have a u32 value`); + } + let num = scv.value() as number; + if (udt.cases().some((entry) => entry.value() === num)) { + } + return num; + } +} + +function stringToScVal(str: string, ty: xdr.ScSpecType): xdr.ScVal { + switch (ty.value) { + case xdr.ScSpecType.scSpecTypeString().value: + return xdr.ScVal.scvString(str); + case xdr.ScSpecType.scSpecTypeSymbol().value: + return xdr.ScVal.scvSymbol(str); + case xdr.ScSpecType.scSpecTypeAddress().value: { + let addr = Address.fromString(str as string); + return xdr.ScVal.scvAddress(addr.toScAddress()); + } + + default: + throw new TypeError(`invalid type ${ty.name} specified for string value`); + } +} + +function isNumeric(field: xdr.ScSpecUdtStructFieldV0) { + return /^\d+$/.test(field.name().toString()); +} + +function findCase(name: string) { + return function matches(entry: xdr.ScSpecUdtUnionCaseV0) { + switch (entry.switch().value) { + case xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseTupleV0().value: { + let tuple = entry.value() as xdr.ScSpecUdtUnionCaseTupleV0; + return tuple.name().toString() === name; + } + case xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseVoidV0().value: { + let void_case = entry.value() as xdr.ScSpecUdtUnionCaseVoidV0; + return void_case.name().toString() === name; + } + default: + return false; + } + }; +} + +function readObj(args: object, input: xdr.ScSpecFunctionInputV0): any { + let inputName = input.name().toString(); + let entry = Object.entries(args).find(([name, _]) => name === inputName); + if (!entry) { + throw new Error(`Missing field ${inputName}`); + } + return entry[1]; +} diff --git a/src/errors.ts b/src/errors.ts index 7cc3cf4eb..b84f72162 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,11 +1,11 @@ -import { Horizon } from "./horizon_api"; +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?: Horizon.ErrorResponseData; + data?: HorizonApi.ErrorResponseData; status?: number; statusText?: string; url?: string; @@ -55,18 +55,6 @@ export class BadResponseError extends NetworkError { } } -export class InvalidSep10ChallengeError extends Error { - public __proto__: InvalidSep10ChallengeError; - - constructor(message: string) { - const trueProto = new.target.prototype; - super(message); - this.__proto__ = trueProto; - this.constructor = InvalidSep10ChallengeError; - this.name = "InvalidSep10ChallengeError"; - } -} - /** * AccountRequiresMemoError is raised when a transaction is trying to submit an * operation to an account which requires a memo. See diff --git a/src/federation/api.ts b/src/federation/api.ts new file mode 100644 index 000000000..65c3e98c3 --- /dev/null +++ b/src/federation/api.ts @@ -0,0 +1,13 @@ +/* tslint:disable-next-line:no-namespace */ +export namespace Api { + export interface Record { + account_id: string; + memo_type?: string; + memo?: string; + } + + export interface Options { + allowHttp?: boolean; + timeout?: number; + } + } diff --git a/src/federation/index.ts b/src/federation/index.ts new file mode 100644 index 000000000..b7c92ee06 --- /dev/null +++ b/src/federation/index.ts @@ -0,0 +1,2 @@ +export { FederationServer as Server, FEDERATION_RESPONSE_MAX_SIZE } from './server'; +export * as Api from './api'; \ No newline at end of file diff --git a/src/federation_server.ts b/src/federation/server.ts similarity index 92% rename from src/federation_server.ts rename to src/federation/server.ts index 70eda449e..a262aa29a 100644 --- a/src/federation_server.ts +++ b/src/federation/server.ts @@ -2,9 +2,11 @@ import axios from "axios"; import { StrKey } from "stellar-base"; import URI from "urijs"; -import { Config } from "./config"; -import { BadResponseError } from "./errors"; -import { StellarTomlResolver } from "./stellar_toml_resolver"; +import { Config } from "../config"; +import { BadResponseError } from "../errors"; +import { Resolver } from "../stellartoml"; + +import { Api } from "./api"; // FEDERATION_RESPONSE_MAX_SIZE is the maximum size of response from a federation server export const FEDERATION_RESPONSE_MAX_SIZE = 100 * 1024; @@ -79,8 +81,8 @@ export class FederationServer { */ public static async resolve( value: string, - opts: FederationServer.Options = {}, - ): Promise { + opts: Api.Options = {}, + ): Promise { // Check if `value` is in account ID format if (value.indexOf("*") < 0) { if (!StrKey.isValidEd25519PublicKey(value)) { @@ -127,9 +129,9 @@ export class FederationServer { */ public static async createForDomain( domain: string, - opts: FederationServer.Options = {}, + opts: Api.Options = {}, ): Promise { - const tomlObject = await StellarTomlResolver.resolve(domain, opts); + const tomlObject = await Resolver.resolve(domain, opts); if (!tomlObject.FEDERATION_SERVER) { return Promise.reject( new Error("stellar.toml does not contain FEDERATION_SERVER field"), @@ -141,7 +143,7 @@ export class FederationServer { public constructor( serverURL: string, domain: string, - opts: FederationServer.Options = {}, + opts: Api.Options = {}, ) { // TODO `domain` regexp this.serverURL = URI(serverURL); @@ -168,7 +170,7 @@ export class FederationServer { */ public async resolveAddress( address: string, - ): Promise { + ): Promise { let stellarAddress = address; if (address.indexOf("*") < 0) { if (!this.domain) { @@ -192,7 +194,7 @@ export class FederationServer { */ public async resolveAccountId( accountId: string, - ): Promise { + ): Promise { const url = this.serverURL.query({ type: "id", q: accountId }); return this._sendRequest(url); } @@ -205,7 +207,7 @@ export class FederationServer { */ public async resolveTransactionId( transactionId: string, - ): Promise { + ): Promise { const url = this.serverURL.query({ type: "txid", q: transactionId }); return this._sendRequest(url); } @@ -247,17 +249,3 @@ export class FederationServer { }); } } - -/* tslint:disable-next-line:no-namespace */ -export namespace FederationServer { - export interface Record { - account_id: string; - memo_type?: string; - memo?: string; - } - - export interface Options { - allowHttp?: boolean; - timeout?: number; - } -} diff --git a/src/friendbot/index.ts b/src/friendbot/index.ts new file mode 100644 index 000000000..f7e15a14e --- /dev/null +++ b/src/friendbot/index.ts @@ -0,0 +1,6 @@ +export namespace Api { + // Just the fields we are interested in + export interface Response { + result_meta_xdr: string; + } +} diff --git a/src/account_call_builder.ts b/src/horizon/account_call_builder.ts similarity index 99% rename from src/account_call_builder.ts rename to src/horizon/account_call_builder.ts index ba840cf18..43303f533 100644 --- a/src/account_call_builder.ts +++ b/src/horizon/account_call_builder.ts @@ -78,4 +78,4 @@ export class AccountCallBuilder extends CallBuilder< this.url.setQuery("liquidity_pool", id); return this; } -} +} \ No newline at end of file diff --git a/src/account_response.ts b/src/horizon/account_response.ts similarity index 93% rename from src/account_response.ts rename to src/horizon/account_response.ts index 59ebeac26..1cff48f17 100644 --- a/src/account_response.ts +++ b/src/horizon/account_response.ts @@ -1,7 +1,7 @@ /* tslint:disable:variable-name */ import { Account as BaseAccount } from "stellar-base"; -import { Horizon } from "./horizon_api"; +import { HorizonApi } from "./horizon_api"; import { ServerApi } from "./server_api"; /** @@ -27,9 +27,9 @@ export class AccountResponse { public readonly inflation_destination?: string; public readonly last_modified_ledger!: number; public readonly last_modified_time!: string; - public readonly thresholds!: Horizon.AccountThresholds; - public readonly flags!: Horizon.Flags; - public readonly balances!: Horizon.BalanceLine[]; + 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; diff --git a/src/assets_call_builder.ts b/src/horizon/assets_call_builder.ts similarity index 100% rename from src/assets_call_builder.ts rename to src/horizon/assets_call_builder.ts diff --git a/src/call_builder.ts b/src/horizon/call_builder.ts similarity index 95% rename from src/call_builder.ts rename to src/horizon/call_builder.ts index 88b490500..59ff97393 100644 --- a/src/call_builder.ts +++ b/src/horizon/call_builder.ts @@ -1,13 +1,11 @@ import URI from "urijs"; import URITemplate from "urijs/src/URITemplate"; -import { BadRequestError, NetworkError, NotFoundError } from "./errors"; -import { Horizon } from "./horizon_api"; -import HorizonAxiosClient from "./horizon_axios_client"; -import { ServerApi } from "./server_api"; +import { BadRequestError, NetworkError, NotFoundError } from "../errors"; -/* tslint:disable-next-line:no-var-requires */ -const version = require("../package.json").version; +import { HorizonApi } from "./horizon_api"; +import { AxiosClient, version } from "./horizon_axios_client"; +import { ServerApi } from "./server_api"; // Resources which can be included in the Horizon response via the `join` // query-param. @@ -35,9 +33,9 @@ let EventSource: Constructable = anyGlobal.EventSource ?? */ export class CallBuilder< T extends - | Horizon.FeeStatsResponse - | Horizon.BaseResponse - | ServerApi.CollectionPage + | HorizonApi.FeeStatsResponse + | HorizonApi.BaseResponse + | ServerApi.CollectionPage > { protected url: URI; public filter: string[][]; @@ -278,7 +276,7 @@ export class CallBuilder< * @param {bool} [link.templated] Whether the link is templated * @returns {function} A function that requests the link */ - private _requestFnForLink(link: Horizon.ResponseLink): (opts?: any) => any { + private _requestFnForLink(link: HorizonApi.ResponseLink): (opts?: any) => any { return async (opts: any = {}) => { let uri; @@ -327,7 +325,7 @@ export class CallBuilder< // are loading from the server or in-memory (via join). json[key] = async () => record; } else { - json[key] = this._requestFnForLink(n as Horizon.ResponseLink); + json[key] = this._requestFnForLink(n as HorizonApi.ResponseLink); } } return json; @@ -344,7 +342,7 @@ export class CallBuilder< url = url.protocol(this.url.protocol()); } - return HorizonAxiosClient.get(url.toString()) + return AxiosClient.get(url.toString()) .then((response) => response.data) .catch(this._handleNetworkError); } @@ -404,4 +402,4 @@ export class CallBuilder< return Promise.reject(new Error(error.message)); } } -} +} \ No newline at end of file diff --git a/src/claimable_balances_call_builder.ts b/src/horizon/claimable_balances_call_builder.ts similarity index 100% rename from src/claimable_balances_call_builder.ts rename to src/horizon/claimable_balances_call_builder.ts diff --git a/src/effect_call_builder.ts b/src/horizon/effect_call_builder.ts similarity index 100% rename from src/effect_call_builder.ts rename to src/horizon/effect_call_builder.ts diff --git a/src/friendbot_builder.ts b/src/horizon/friendbot_builder.ts similarity index 100% rename from src/friendbot_builder.ts rename to src/horizon/friendbot_builder.ts diff --git a/src/horizon_api.ts b/src/horizon/horizon_api.ts similarity index 99% rename from src/horizon_api.ts rename to src/horizon/horizon_api.ts index cb4116195..d6e50780f 100644 --- a/src/horizon_api.ts +++ b/src/horizon/horizon_api.ts @@ -1,7 +1,7 @@ import { AssetType, MemoType } from "stellar-base"; /* tslint:disable-next-line:no-namespace */ -export namespace Horizon { +export namespace HorizonApi { export interface ResponseLink { href: string; templated?: boolean; diff --git a/src/horizon_axios_client.ts b/src/horizon/horizon_axios_client.ts similarity index 92% rename from src/horizon_axios_client.ts rename to src/horizon/horizon_axios_client.ts index 8f105829f..a76da401f 100644 --- a/src/horizon_axios_client.ts +++ b/src/horizon/horizon_axios_client.ts @@ -2,7 +2,7 @@ import axios, { AxiosResponse } from "axios"; import URI from "urijs"; /* tslint:disable-next-line:no-var-requires */ -const version = require("../package.json").version; +export const version = require("../../package.json").version; export interface ServerTime { serverTime: number; @@ -23,7 +23,7 @@ export interface ServerTime { */ export const SERVER_TIME_MAP: Record = {}; -const HorizonAxiosClient = axios.create({ +export const AxiosClient = axios.create({ headers: { "X-Client-Name": "js-stellar-sdk", "X-Client-Version": version, @@ -34,7 +34,7 @@ function _toSeconds(ms: number): number { return Math.floor(ms / 1000); } -HorizonAxiosClient.interceptors.response.use( +AxiosClient.interceptors.response.use( function interceptorHorizonResponse(response: AxiosResponse) { const hostname = URI(response.config.url!).hostname(); const serverTime = _toSeconds(Date.parse(response.headers.date)); @@ -51,7 +51,7 @@ HorizonAxiosClient.interceptors.response.use( }, ); -export default HorizonAxiosClient; +export default AxiosClient; /** * Given a hostname, get the current time of that server (i.e., use the last- diff --git a/src/horizon/index.ts b/src/horizon/index.ts new file mode 100644 index 000000000..2a690dda7 --- /dev/null +++ b/src/horizon/index.ts @@ -0,0 +1,15 @@ +// Expose all types +export * from "./horizon_api"; +export * from "./server_api"; + +// stellar-sdk classes to expose +export * from "./account_response"; + +export { Server } from "./server"; +export { + default as AxiosClient, + SERVER_TIME_MAP, + getCurrentServerTime +} from "./horizon_axios_client"; + +export default module.exports; \ No newline at end of file diff --git a/src/ledger_call_builder.ts b/src/horizon/ledger_call_builder.ts similarity index 100% rename from src/ledger_call_builder.ts rename to src/horizon/ledger_call_builder.ts diff --git a/src/liquidity_pool_call_builder.ts b/src/horizon/liquidity_pool_call_builder.ts similarity index 100% rename from src/liquidity_pool_call_builder.ts rename to src/horizon/liquidity_pool_call_builder.ts diff --git a/src/offer_call_builder.ts b/src/horizon/offer_call_builder.ts similarity index 100% rename from src/offer_call_builder.ts rename to src/horizon/offer_call_builder.ts diff --git a/src/operation_call_builder.ts b/src/horizon/operation_call_builder.ts similarity index 100% rename from src/operation_call_builder.ts rename to src/horizon/operation_call_builder.ts diff --git a/src/orderbook_call_builder.ts b/src/horizon/orderbook_call_builder.ts similarity index 100% rename from src/orderbook_call_builder.ts rename to src/horizon/orderbook_call_builder.ts diff --git a/src/path_call_builder.ts b/src/horizon/path_call_builder.ts similarity index 100% rename from src/path_call_builder.ts rename to src/horizon/path_call_builder.ts diff --git a/src/payment_call_builder.ts b/src/horizon/payment_call_builder.ts similarity index 100% rename from src/payment_call_builder.ts rename to src/horizon/payment_call_builder.ts diff --git a/src/server.ts b/src/horizon/server.ts similarity index 96% rename from src/server.ts rename to src/horizon/server.ts index 70f384388..ac0692d2b 100644 --- a/src/server.ts +++ b/src/horizon/server.ts @@ -11,12 +11,12 @@ import { import URI from "urijs"; import { CallBuilder } from "./call_builder"; -import { Config } from "./config"; +import { Config } from "../config"; import { AccountRequiresMemoError, BadResponseError, NotFoundError, -} from "./errors"; +} from "../errors"; import { AccountCallBuilder } from "./account_call_builder"; import { AccountResponse } from "./account_response"; @@ -24,7 +24,7 @@ import { AssetsCallBuilder } from "./assets_call_builder"; import { ClaimableBalanceCallBuilder } from "./claimable_balances_call_builder"; import { EffectCallBuilder } from "./effect_call_builder"; import { FriendbotBuilder } from "./friendbot_builder"; -import { Horizon } from "./horizon_api"; +import { HorizonApi } from "./horizon_api"; import { LedgerCallBuilder } from "./ledger_call_builder"; import { LiquidityPoolCallBuilder } from "./liquidity_pool_call_builder"; import { OfferCallBuilder } from "./offer_call_builder"; @@ -38,7 +38,7 @@ import { TradeAggregationCallBuilder } from "./trade_aggregation_call_builder"; import { TradesCallBuilder } from "./trades_call_builder"; import { TransactionCallBuilder } from "./transaction_call_builder"; -import HorizonAxiosClient, { +import AxiosClient, { getCurrentServerTime, } from "./horizon_axios_client"; @@ -93,7 +93,7 @@ export class Server { customHeaders["X-Auth-Token"] = opts.authToken; } if (Object.keys(customHeaders).length > 0) { - HorizonAxiosClient.interceptors.request.use((config) => { + AxiosClient.interceptors.request.use((config) => { // merge the custom headers with an existing headers, where customs // override defaults config.headers = Object.assign(config.headers, customHeaders); @@ -141,7 +141,7 @@ export class Server { seconds: number, _isRetry: boolean = false, ): Promise { - // HorizonAxiosClient instead of this.ledgers so we can get at them headers + // AxiosClient instead of this.ledgers so we can get at them headers const currentTime = getCurrentServerTime(this.serverURL.hostname()); if (currentTime) { @@ -161,7 +161,7 @@ export class Server { // otherwise, retry (by calling the root endpoint) // toString automatically adds the trailing slash - await HorizonAxiosClient.get(URI(this.serverURL as any).toString()); + await AxiosClient.get(URI(this.serverURL as any).toString()); return await this.fetchTimebounds(seconds, true); } @@ -180,10 +180,10 @@ export class Server { /** * Fetch the fee stats endpoint. * @see [Fee Stats](https://developers.stellar.org/api/aggregations/fee-stats/) - * @returns {Promise} Promise that resolves to the fee stats returned by Horizon. + * @returns {Promise} Promise that resolves to the fee stats returned by Horizon. */ - public async feeStats(): Promise { - const cb = new CallBuilder( + public async feeStats(): Promise { + const cb = new CallBuilder( URI(this.serverURL as any), ); cb.filter.push(["fee_stats"]); @@ -297,7 +297,7 @@ export class Server { public async submitTransaction( transaction: Transaction | FeeBumpTransaction, opts: Server.SubmitTransactionOptions = { skipMemoRequiredCheck: false }, - ): Promise { + ): Promise { // only check for memo required if skipMemoRequiredCheck is false and the transaction doesn't include a memo. if (!opts.skipMemoRequiredCheck) { await this.checkMemoRequired(transaction); @@ -310,7 +310,7 @@ export class Server { .toString("base64"), ); - return HorizonAxiosClient.post( + return AxiosClient.post( URI(this.serverURL as any) .segment("transactions") .toString(), @@ -742,15 +742,14 @@ export class Server { * Check if any of the destination accounts requires a memo. * * This function implements a memo required check as defined in - * [SEP0029](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0029.md). - * It will load each account which is the destination and check if it has the - * data field `config.memo_required` set to `"MQ=="`. + * [SEP-29](https://stellar.org/protocol/sep-29). It will load each account + * which is the destination and check if it has the data field + * `config.memo_required` set to `"MQ=="`. * * Each account is checked sequentially instead of loading multiple accounts * at the same time from Horizon. * - * @see - * [SEP0029](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0029.md) + * @see https://stellar.org/protocol/sep-29 * @param {Transaction} transaction - The transaction to check. * @returns {Promise} - If any of the destination account * requires a memo, the promise will throw {@link AccountRequiresMemoError}. diff --git a/src/server_api.ts b/src/horizon/server_api.ts similarity index 84% rename from src/server_api.ts rename to src/horizon/server_api.ts index 05f18a20a..f0a58f85a 100644 --- a/src/server_api.ts +++ b/src/horizon/server_api.ts @@ -1,6 +1,6 @@ import { Asset } from "stellar-base"; import { Omit } from "utility-types"; -import { Horizon } from "./horizon_api"; +import { HorizonApi } from "./horizon_api"; // more types import { AccountRecordSigners as AccountRecordSignersType } from "./types/account"; @@ -15,7 +15,7 @@ export namespace ServerApi { export type AccountRecordSigners = AccountRecordSignersType; export type AssetRecord = AssetRecordType; export interface CollectionPage< - T extends Horizon.BaseResponse = Horizon.BaseResponse + T extends HorizonApi.BaseResponse = HorizonApi.BaseResponse > { records: T[]; next: () => Promise>; @@ -29,10 +29,10 @@ export namespace ServerApi { } export type CallFunction< - T extends Horizon.BaseResponse = Horizon.BaseResponse + T extends HorizonApi.BaseResponse = HorizonApi.BaseResponse > = () => Promise; export type CallCollectionFunction< - T extends Horizon.BaseResponse = Horizon.BaseResponse + T extends HorizonApi.BaseResponse = HorizonApi.BaseResponse > = (options?: CallFunctionTemplateOptions) => Promise>; type BaseEffectRecordFromTypes = @@ -84,16 +84,16 @@ export namespace ServerApi { | Trade; export type EffectRecord = BaseEffectRecordFromTypes & EffectRecordMethods; - export interface ClaimableBalanceRecord extends Horizon.BaseResponse { + export interface ClaimableBalanceRecord extends HorizonApi.BaseResponse { id: string; paging_token: string; asset: string; amount: string; sponsor?: string; last_modified_ledger: number; - claimants: Horizon.Claimant[]; + claimants: HorizonApi.Claimant[]; } - export interface AccountRecord extends Horizon.BaseResponse { + export interface AccountRecord extends HorizonApi.BaseResponse { id: string; paging_token: string; account_id: string; @@ -105,9 +105,9 @@ export namespace ServerApi { inflation_destination?: string; last_modified_ledger: number; last_modified_time: string; - thresholds: Horizon.AccountThresholds; - flags: Horizon.Flags; - balances: Horizon.BalanceLine[]; + thresholds: HorizonApi.AccountThresholds; + flags: HorizonApi.Flags; + balances: HorizonApi.BalanceLine[]; signers: AccountRecordSigners[]; data: (options: { value: string }) => Promise<{ value: string }>; data_attr: { @@ -122,14 +122,14 @@ export namespace ServerApi { payments: CallCollectionFunction; trades: CallCollectionFunction; } - export interface LiquidityPoolRecord extends Horizon.BaseResponse { + export interface LiquidityPoolRecord extends HorizonApi.BaseResponse { id: string; paging_token: string; fee_bp: number; - type: Horizon.LiquidityPoolType; + type: HorizonApi.LiquidityPoolType; total_trustlines: string; total_shares: string; - reserves: Horizon.Reserve[]; + reserves: HorizonApi.Reserve[]; } export enum TradeType { all = "all", @@ -141,7 +141,7 @@ export namespace ServerApi { precedes?: CallFunction; succeeds?: CallFunction; } - export interface LedgerRecord extends Horizon.BaseResponse { + export interface LedgerRecord extends HorizonApi.BaseResponse { id: string; paging_token: string; hash: string; @@ -181,12 +181,12 @@ export namespace ServerApi { transactions: CallCollectionFunction; } - import OperationResponseType = Horizon.OperationResponseType; - import OperationResponseTypeI = Horizon.OperationResponseTypeI; + import OperationResponseType = HorizonApi.OperationResponseType; + import OperationResponseTypeI = HorizonApi.OperationResponseTypeI; export interface BaseOperationRecord< T extends OperationResponseType = OperationResponseType, TI extends OperationResponseTypeI = OperationResponseTypeI - > extends Horizon.BaseOperationResponse { + > extends HorizonApi.BaseOperationResponse { self: CallFunction; succeeds: CallFunction; precedes: CallFunction; @@ -198,13 +198,13 @@ export namespace ServerApi { OperationResponseType.createAccount, OperationResponseTypeI.createAccount >, - Horizon.CreateAccountOperationResponse {} + HorizonApi.CreateAccountOperationResponse {} export interface PaymentOperationRecord extends BaseOperationRecord< OperationResponseType.payment, OperationResponseTypeI.payment >, - Horizon.PaymentOperationResponse { + HorizonApi.PaymentOperationResponse { sender: CallFunction; receiver: CallFunction; } @@ -213,145 +213,145 @@ export namespace ServerApi { OperationResponseType.pathPayment, OperationResponseTypeI.pathPayment >, - Horizon.PathPaymentOperationResponse {} + HorizonApi.PathPaymentOperationResponse {} export interface PathPaymentStrictSendOperationRecord extends BaseOperationRecord< OperationResponseType.pathPaymentStrictSend, OperationResponseTypeI.pathPaymentStrictSend >, - Horizon.PathPaymentStrictSendOperationResponse {} + HorizonApi.PathPaymentStrictSendOperationResponse {} export interface ManageOfferOperationRecord extends BaseOperationRecord< OperationResponseType.manageOffer, OperationResponseTypeI.manageOffer >, - Horizon.ManageOfferOperationResponse {} + HorizonApi.ManageOfferOperationResponse {} export interface PassiveOfferOperationRecord extends BaseOperationRecord< OperationResponseType.createPassiveOffer, OperationResponseTypeI.createPassiveOffer >, - Horizon.PassiveOfferOperationResponse {} + HorizonApi.PassiveOfferOperationResponse {} export interface SetOptionsOperationRecord extends BaseOperationRecord< OperationResponseType.setOptions, OperationResponseTypeI.setOptions >, - Horizon.SetOptionsOperationResponse {} + HorizonApi.SetOptionsOperationResponse {} export interface ChangeTrustOperationRecord extends BaseOperationRecord< OperationResponseType.changeTrust, OperationResponseTypeI.changeTrust >, - Horizon.ChangeTrustOperationResponse {} + HorizonApi.ChangeTrustOperationResponse {} export interface AllowTrustOperationRecord extends BaseOperationRecord< OperationResponseType.allowTrust, OperationResponseTypeI.allowTrust >, - Horizon.AllowTrustOperationResponse {} + HorizonApi.AllowTrustOperationResponse {} export interface AccountMergeOperationRecord extends BaseOperationRecord< OperationResponseType.accountMerge, OperationResponseTypeI.accountMerge >, - Horizon.AccountMergeOperationResponse {} + HorizonApi.AccountMergeOperationResponse {} export interface InflationOperationRecord extends BaseOperationRecord< OperationResponseType.inflation, OperationResponseTypeI.inflation >, - Horizon.InflationOperationResponse {} + HorizonApi.InflationOperationResponse {} export interface ManageDataOperationRecord extends BaseOperationRecord< OperationResponseType.manageData, OperationResponseTypeI.manageData >, - Horizon.ManageDataOperationResponse {} + HorizonApi.ManageDataOperationResponse {} export interface BumpSequenceOperationRecord extends BaseOperationRecord< OperationResponseType.bumpSequence, OperationResponseTypeI.bumpSequence >, - Horizon.BumpSequenceOperationResponse {} + HorizonApi.BumpSequenceOperationResponse {} export interface CreateClaimableBalanceOperationRecord extends BaseOperationRecord< OperationResponseType.createClaimableBalance, OperationResponseTypeI.createClaimableBalance >, - Horizon.CreateClaimableBalanceOperationResponse {} + HorizonApi.CreateClaimableBalanceOperationResponse {} export interface ClaimClaimableBalanceOperationRecord extends BaseOperationRecord< OperationResponseType.claimClaimableBalance, OperationResponseTypeI.claimClaimableBalance >, - Horizon.ClaimClaimableBalanceOperationResponse {} + HorizonApi.ClaimClaimableBalanceOperationResponse {} export interface BeginSponsoringFutureReservesOperationRecord extends BaseOperationRecord< OperationResponseType.beginSponsoringFutureReserves, OperationResponseTypeI.beginSponsoringFutureReserves >, - Horizon.BeginSponsoringFutureReservesOperationResponse {} + HorizonApi.BeginSponsoringFutureReservesOperationResponse {} export interface EndSponsoringFutureReservesOperationRecord extends BaseOperationRecord< OperationResponseType.endSponsoringFutureReserves, OperationResponseTypeI.endSponsoringFutureReserves >, - Horizon.EndSponsoringFutureReservesOperationResponse {} + HorizonApi.EndSponsoringFutureReservesOperationResponse {} export interface RevokeSponsorshipOperationRecord extends BaseOperationRecord< OperationResponseType.revokeSponsorship, OperationResponseTypeI.revokeSponsorship >, - Horizon.RevokeSponsorshipOperationResponse {} + HorizonApi.RevokeSponsorshipOperationResponse {} export interface ClawbackOperationRecord extends BaseOperationRecord< OperationResponseType.clawback, OperationResponseTypeI.clawback >, - Horizon.ClawbackOperationResponse {} + HorizonApi.ClawbackOperationResponse {} export interface ClawbackClaimableBalanceOperationRecord extends BaseOperationRecord< OperationResponseType.clawbackClaimableBalance, OperationResponseTypeI.clawbackClaimableBalance >, - Horizon.ClawbackClaimableBalanceOperationResponse {} + HorizonApi.ClawbackClaimableBalanceOperationResponse {} export interface SetTrustLineFlagsOperationRecord extends BaseOperationRecord< OperationResponseType.setTrustLineFlags, OperationResponseTypeI.setTrustLineFlags >, - Horizon.SetTrustLineFlagsOperationResponse {} + HorizonApi.SetTrustLineFlagsOperationResponse {} export interface DepositLiquidityOperationRecord extends BaseOperationRecord< OperationResponseType.liquidityPoolDeposit, OperationResponseTypeI.liquidityPoolDeposit >, - Horizon.DepositLiquidityOperationResponse {} + HorizonApi.DepositLiquidityOperationResponse {} export interface WithdrawLiquidityOperationRecord extends BaseOperationRecord< OperationResponseType.liquidityPoolWithdraw, OperationResponseTypeI.liquidityPoolWithdraw >, - Horizon.WithdrawLiquidityOperationResponse {} + HorizonApi.WithdrawLiquidityOperationResponse {} export interface InvokeHostFunctionOperationRecord extends BaseOperationRecord< OperationResponseType.invokeHostFunction, OperationResponseTypeI.invokeHostFunction >, - Horizon.InvokeHostFunctionOperationResponse {} + HorizonApi.InvokeHostFunctionOperationResponse {} export interface BumpFootprintExpirationOperationRecord extends BaseOperationRecord< OperationResponseType.bumpFootprintExpiration, OperationResponseTypeI.bumpFootprintExpiration >, - Horizon.BumpFootprintExpirationOperationResponse {} + HorizonApi.BumpFootprintExpirationOperationResponse {} export interface RestoreFootprintOperationRecord extends BaseOperationRecord< OperationResponseType.restoreFootprint, OperationResponseTypeI.restoreFootprint >, - Horizon.RestoreFootprintOperationResponse {} + HorizonApi.RestoreFootprintOperationResponse {} export type OperationRecord = | CreateAccountOperationRecord @@ -382,7 +382,7 @@ export namespace ServerApi { | RestoreFootprintOperationRecord; export namespace TradeRecord { - interface Base extends Horizon.BaseResponse { + interface Base extends HorizonApi.BaseResponse { id: string; paging_token: string; ledger_close_time: string; @@ -427,8 +427,8 @@ export namespace ServerApi { } export type TradeRecord = TradeRecord.Orderbook | TradeRecord.LiquidityPool; export interface TransactionRecord - extends Omit { - ledger_attr: Horizon.TransactionResponse["ledger"]; + extends Omit { + ledger_attr: HorizonApi.TransactionResponse["ledger"]; account: CallFunction; effects: CallCollectionFunction; @@ -438,7 +438,7 @@ export namespace ServerApi { self: CallFunction; succeeds: CallFunction; } - export interface OrderbookRecord extends Horizon.BaseResponse { + export interface OrderbookRecord extends HorizonApi.BaseResponse { bids: Array<{ price_r: { d: number; @@ -458,7 +458,7 @@ export namespace ServerApi { base: Asset; counter: Asset; } - export interface PaymentPathRecord extends Horizon.BaseResponse { + export interface PaymentPathRecord extends HorizonApi.BaseResponse { path: Array<{ asset_code: string; asset_issuer: string; @@ -473,4 +473,4 @@ export namespace ServerApi { destination_asset_code: string; destination_asset_issuer: string; } -} +} \ No newline at end of file diff --git a/src/strict_receive_path_call_builder.ts b/src/horizon/strict_receive_path_call_builder.ts similarity index 100% rename from src/strict_receive_path_call_builder.ts rename to src/horizon/strict_receive_path_call_builder.ts diff --git a/src/strict_send_path_call_builder.ts b/src/horizon/strict_send_path_call_builder.ts similarity index 100% rename from src/strict_send_path_call_builder.ts rename to src/horizon/strict_send_path_call_builder.ts diff --git a/src/trade_aggregation_call_builder.ts b/src/horizon/trade_aggregation_call_builder.ts similarity index 96% rename from src/trade_aggregation_call_builder.ts rename to src/horizon/trade_aggregation_call_builder.ts index f5f9f139e..b35fad91e 100644 --- a/src/trade_aggregation_call_builder.ts +++ b/src/horizon/trade_aggregation_call_builder.ts @@ -1,8 +1,8 @@ /* tslint:disable: variable-name */ import { Asset } from "stellar-base"; import { CallBuilder } from "./call_builder"; -import { BadRequestError } from "./errors"; -import { Horizon } from "./horizon_api"; +import { BadRequestError } from "../errors"; +import { HorizonApi } from "./horizon_api"; import { ServerApi } from "./server_api"; const allowedResolutions = [ @@ -102,7 +102,7 @@ export class TradeAggregationCallBuilder extends CallBuilder< } } -interface TradeAggregationRecord extends Horizon.BaseResponse { +interface TradeAggregationRecord extends HorizonApi.BaseResponse { timestamp: number | string; trade_count: number | string; base_volume: string; diff --git a/src/trades_call_builder.ts b/src/horizon/trades_call_builder.ts similarity index 100% rename from src/trades_call_builder.ts rename to src/horizon/trades_call_builder.ts diff --git a/src/transaction_call_builder.ts b/src/horizon/transaction_call_builder.ts similarity index 100% rename from src/transaction_call_builder.ts rename to src/horizon/transaction_call_builder.ts diff --git a/src/types/account.ts b/src/horizon/types/account.ts similarity index 100% rename from src/types/account.ts rename to src/horizon/types/account.ts diff --git a/src/types/assets.ts b/src/horizon/types/assets.ts similarity index 65% rename from src/types/assets.ts rename to src/horizon/types/assets.ts index 5c831408e..1ea450d25 100644 --- a/src/types/assets.ts +++ b/src/horizon/types/assets.ts @@ -1,13 +1,13 @@ import { AssetType } from "stellar-base"; -import { Horizon } from "./../horizon_api"; +import { HorizonApi } from "./../horizon_api"; -export interface AssetRecord extends Horizon.BaseResponse { +export interface AssetRecord extends HorizonApi.BaseResponse { asset_type: AssetType.credit4 | AssetType.credit12; asset_code: string; asset_issuer: string; paging_token: string; - accounts: Horizon.AssetAccounts; - balances: Horizon.AssetBalances; + accounts: HorizonApi.AssetAccounts; + balances: HorizonApi.AssetBalances; num_claimable_balances: number; num_liquidity_pools: number; num_contracts: number; @@ -16,5 +16,5 @@ export interface AssetRecord extends Horizon.BaseResponse { claimable_balances_amount: string; liquidity_pools_amount: string; contracts_amount: string; - flags: Horizon.Flags; + flags: HorizonApi.Flags; } diff --git a/src/types/effects.ts b/src/horizon/types/effects.ts similarity index 95% rename from src/types/effects.ts rename to src/horizon/types/effects.ts index 4fa5999c9..7742295d8 100644 --- a/src/types/effects.ts +++ b/src/horizon/types/effects.ts @@ -1,4 +1,4 @@ -import { Horizon } 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 @@ -67,7 +67,7 @@ export enum EffectType { contract_credited = 96, contract_debited = 97 } -export interface BaseEffectRecord extends Horizon.BaseResponse { +export interface BaseEffectRecord extends HorizonApi.BaseResponse { id: string; account: string; paging_token: string; @@ -263,35 +263,35 @@ export type SignerSponsorshipRemoved = Omit< "new_sponsor" | "sponsor" > & { type_i: EffectType.signer_sponsorship_removed }; -export interface ClaimableBalanceClawedBack extends Horizon.BaseResponse { +export interface ClaimableBalanceClawedBack extends HorizonApi.BaseResponse { balance_id: string } -export interface LiquidityPoolEffectRecord extends Horizon.BaseResponse { +export interface LiquidityPoolEffectRecord extends HorizonApi.BaseResponse { id: string; fee_bp: number; - type: Horizon.LiquidityPoolType; + type: HorizonApi.LiquidityPoolType; total_trustlines: string; total_shares: string; - reserves: Horizon.Reserve[]; + reserves: HorizonApi.Reserve[]; } export interface LiquidityPoolDeposited extends BaseEffectRecord { type_i: EffectType.liquidity_pool_deposited; liquidity_pool: LiquidityPoolEffectRecord; - reserves_deposited: Horizon.Reserve[]; + reserves_deposited: HorizonApi.Reserve[]; shares_received: string; } export interface LiquidityPoolWithdrew extends BaseEffectRecord { type_i: EffectType.liquidity_pool_withdrew; liquidity_pool: LiquidityPoolEffectRecord; - reserves_received: Horizon.Reserve[]; + reserves_received: HorizonApi.Reserve[]; shares_redeemed: string; } export interface LiquidityPoolTrade extends BaseEffectRecord { type_i: EffectType.liquidity_pool_trade; liquidity_pool: LiquidityPoolEffectRecord; - sold: Horizon.Reserve; - bought: Horizon.Reserve; + sold: HorizonApi.Reserve; + bought: HorizonApi.Reserve; } export interface LiquidityPoolCreated extends BaseEffectRecord { type_i: EffectType.liquidity_pool_created; diff --git a/src/types/offer.ts b/src/horizon/types/offer.ts similarity index 71% rename from src/types/offer.ts rename to src/horizon/types/offer.ts index 9319a33f5..6aaf4c5fd 100644 --- a/src/types/offer.ts +++ b/src/horizon/types/offer.ts @@ -1,5 +1,5 @@ import { AssetType } from "stellar-base"; -import { Horizon } from "./../horizon_api"; +import { HorizonApi } from "./../horizon_api"; export interface OfferAsset { asset_type: AssetType; @@ -7,14 +7,14 @@ export interface OfferAsset { asset_issuer?: string; } -export interface OfferRecord extends Horizon.BaseResponse { +export interface OfferRecord extends HorizonApi.BaseResponse { id: number | string; paging_token: string; seller: string; selling: OfferAsset; buying: OfferAsset; amount: string; - price_r: Horizon.PriceRShorthand; + price_r: HorizonApi.PriceRShorthand; price: string; last_modified_ledger: number; last_modified_time: string; diff --git a/src/types/trade.ts b/src/horizon/types/trade.ts similarity index 100% rename from src/types/trade.ts rename to src/horizon/types/trade.ts diff --git a/src/index.ts b/src/index.ts index 49b9c30c5..28750e1f7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,36 +1,26 @@ // tslint:disable-next-line: no-reference /// -/* tslint:disable:no-var-requires */ -const version = require("../package.json").version; - // Expose all types -export * from "./horizon_api"; -export * from "./server_api"; +export * from './errors'; +export { Config } from './config'; +export { Utils } from './utils'; -// stellar-sdk classes to expose -export * from "./account_response"; -export * from "./errors"; -export { Config } from "./config"; -export { Server } from "./server"; -export { - FederationServer, - FEDERATION_RESPONSE_MAX_SIZE -} from "./federation_server"; -export { - StellarTomlResolver, - STELLAR_TOML_MAX_SIZE -} from "./stellar_toml_resolver"; -export { - default as HorizonAxiosClient, - SERVER_TIME_MAP, - getCurrentServerTime -} from "./horizon_axios_client"; -export * from "./utils"; +// TOML (SEP-1), Federation (SEP-2), and WebAuth (SEP-10) helpers to expose +export * as StellarToml from './stellartoml'; +export * as Federation from './federation'; +export * as WebAuth from './webauth'; -// expose classes and functions from stellar-base -export * from "stellar-base"; +export * as Friendbot from './friendbot'; + +// Horizon-related classes to expose +export * as Horizon from './horizon'; -export { version }; +// Soroban RPC-related classes to expose +export * as SorobanRpc from './soroban'; +export { ContractSpec } from './contract_spec'; + +// expose classes and functions from stellar-base +export * from 'stellar-base'; export default module.exports; diff --git a/src/soroban/.eslintrc.js b/src/soroban/.eslintrc.js new file mode 100644 index 000000000..9d226df13 --- /dev/null +++ b/src/soroban/.eslintrc.js @@ -0,0 +1,42 @@ +module.exports = { + env: { + es6: true + }, + parser: '@babel/eslint-parser', + extends: ['airbnb-base', 'prettier'], + plugins: ['prettier', 'prefer-import'], + 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, + 'new-cap': 0, + 'no-param-reassign': 0, + 'no-underscore-dangle': 0, + 'no-use-before-define': 0, + 'prefer-destructuring': 0, + 'lines-between-class-members': 0, + + // WARN + 'prefer-import/prefer-import-over-require': [1], + 'no-console': ['warn', { allow: ['assert'] }], + 'no-debugger': 1, + 'no-unused-vars': 1, + 'arrow-body-style': 1, + 'valid-jsdoc': [ + 1, + { + requireReturnDescription: false + } + ], + 'prefer-const': 1, + 'object-shorthand': 1, + 'require-await': 1, + + // ERROR + 'no-unused-expressions': [2, { allowTaggedTemplates: true }] + } +}; diff --git a/src/soroban/axios.ts b/src/soroban/axios.ts new file mode 100644 index 000000000..bf19ff7d9 --- /dev/null +++ b/src/soroban/axios.ts @@ -0,0 +1,12 @@ +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', + 'X-Client-Version': version + } +}); + +export default AxiosClient; diff --git a/src/soroban/browser.ts b/src/soroban/browser.ts new file mode 100644 index 000000000..bbd6932ff --- /dev/null +++ b/src/soroban/browser.ts @@ -0,0 +1,9 @@ +/* tslint:disable:no-var-requires */ + +export * from './index'; +export * as StellarBase from 'stellar-base'; + +import axios from 'axios'; // idk why axios is weird +export { axios }; + +export default module.exports; diff --git a/src/soroban/index.ts b/src/soroban/index.ts new file mode 100644 index 000000000..a6084da00 --- /dev/null +++ b/src/soroban/index.ts @@ -0,0 +1,13 @@ +// tslint:disable-next-line: no-reference +/// + +// Expose all types +export * from './soroban_rpc'; + +// soroban-client classes to expose +export { Server, Durability } from './server'; +export { default as AxiosClient } from './axios'; +export { parseRawSimulation, parseRawEvents } from './parsers'; +export * from './transaction'; + +export default module.exports; diff --git a/src/soroban/jsonrpc.ts b/src/soroban/jsonrpc.ts new file mode 100644 index 000000000..18dcfd65c --- /dev/null +++ b/src/soroban/jsonrpc.ts @@ -0,0 +1,83 @@ +import axios from "./axios"; + +export type Id = string | number; + +export interface Request { + jsonrpc: "2.0"; + id: Id; + method: string; + params: T; +} + +export interface Notification { + jsonrpc: "2.0"; + method: string; + params?: T; +} + +export type Response = { + jsonrpc: "2.0"; + id: Id; +} & ({ error: Error } | { result: T }); + +export interface Error { + code: number; + message?: string; + data?: E; +} + +/** + * Sends the jsonrpc 'params' as an array. + */ +export async function post( + url: string, + method: string, + ...params: any +): Promise { + if (params && params.length < 1) { + params = null; + } + const response = await axios.post>(url, { + jsonrpc: "2.0", + // TODO: Generate a unique request id + id: 1, + method, + params, + }); + if (hasOwnProperty(response.data, "error")) { + throw response.data.error; + } else { + return response.data?.result; + } +} + +/** + * Sends the jsonrpc 'params' as the single 'param' obj, no array wrapper is applied. + */ +export async function postObject( + url: string, + method: string, + param: any, +): Promise { + const response = await axios.post>(url, { + jsonrpc: "2.0", + // TODO: Generate a unique request id + id: 1, + method, + params: param, + }); + if (hasOwnProperty(response.data, "error")) { + throw response.data.error; + } 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); +} diff --git a/src/soroban/parsers.ts b/src/soroban/parsers.ts new file mode 100644 index 000000000..429030903 --- /dev/null +++ b/src/soroban/parsers.ts @@ -0,0 +1,144 @@ +import { xdr, Contract, SorobanDataBuilder } from 'stellar-base'; +import { Api } from './soroban_rpc'; + +export function parseRawSendTransaction( + r: Api.RawSendTransactionResponse +): Api.SendTransactionResponse { + const errResult = r.errorResultXdr; + delete r.errorResultXdr; + + if (!!errResult) { + return { + ...r, + errorResult: xdr.TransactionResult.fromXDR(errResult, 'base64') + }; + } + + return { ...r } as Api.BaseSendTransactionResponse; +} + +export function parseRawEvents( + r: Api.RawGetEventsResponse +): Api.GetEventsResponse { + return { + latestLedger: r.latestLedger, + events: (r.events ?? []).map((evt) => { + return { + ...evt, + contractId: new Contract(evt.contractId), + topic: evt.topic.map((topic) => xdr.ScVal.fromXDR(topic, 'base64')), + value: xdr.DiagnosticEvent.fromXDR(evt.value.xdr, 'base64') + }; + }) + }; +} + +export function parseRawLedgerEntries( + raw: Api.RawGetLedgerEntriesResponse +): Api.GetLedgerEntriesResponse { + return { + latestLedger: raw.latestLedger, + entries: (raw.entries ?? []).map((rawEntry) => { + if (!rawEntry.key || !rawEntry.xdr) { + throw new TypeError( + `invalid ledger entry: ${JSON.stringify(rawEntry)}` + ); + } + + return { + lastModifiedLedgerSeq: rawEntry.lastModifiedLedgerSeq, + key: xdr.LedgerKey.fromXDR(rawEntry.key, 'base64'), + val: xdr.LedgerEntryData.fromXDR(rawEntry.xdr, 'base64'), + expirationLedgerSeq: rawEntry.expirationLedgerSeq + }; + }) + }; +} + +/** + * Converts a raw response schema into one with parsed XDR fields and a + * simplified interface. + * + * @param raw the raw response schema (parsed ones are allowed, best-effort + * detected, and returned untouched) + * + * @returns the original parameter (if already parsed), parsed otherwise + * + * @warning This API is only exported for testing purposes and should not be + * relied on or considered "stable". + */ +export function parseRawSimulation( + sim: + | Api.SimulateTransactionResponse + | Api.RawSimulateTransactionResponse +): Api.SimulateTransactionResponse { + const looksRaw = Api.isSimulationRaw(sim); + if (!looksRaw) { + // Gordon Ramsey in shambles + return sim; + } + + // shared across all responses + let base: Api.BaseSimulateTransactionResponse = { + _parsed: true, + id: sim.id, + latestLedger: sim.latestLedger, + events: + sim.events?.map((evt) => xdr.DiagnosticEvent.fromXDR(evt, 'base64')) ?? [] + }; + + // error type: just has error string + if (typeof sim.error === 'string') { + return { + ...base, + error: sim.error + }; + } + + return parseSuccessful(sim, base); +} + +function parseSuccessful( + sim: Api.RawSimulateTransactionResponse, + partial: Api.BaseSimulateTransactionResponse +): + | Api.SimulateTransactionRestoreResponse + | Api.SimulateTransactionSuccessResponse { + // success type: might have a result (if invoking) and... + const success: Api.SimulateTransactionSuccessResponse = { + ...partial, + transactionData: new SorobanDataBuilder(sim.transactionData!), + minResourceFee: sim.minResourceFee!, + cost: sim.cost!, + ...// 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 { + auth: (row.auth ?? []).map((entry) => + xdr.SorobanAuthorizationEntry.fromXDR(entry, 'base64') + ), + // if return value is missing ("falsy") we coalesce to void + retval: !!row.xdr + ? xdr.ScVal.fromXDR(row.xdr, 'base64') + : xdr.ScVal.scvVoid() + }; + })[0] + }) + }; + + if (!sim.restorePreamble || sim.restorePreamble.transactionData === '') { + return success; + } + + // ...might have a restoration hint (if some state is expired) + return { + ...success, + restorePreamble: { + minResourceFee: sim.restorePreamble!.minResourceFee, + transactionData: new SorobanDataBuilder( + sim.restorePreamble!.transactionData + ) + } + }; +} diff --git a/src/soroban/server.ts b/src/soroban/server.ts new file mode 100644 index 000000000..924057667 --- /dev/null +++ b/src/soroban/server.ts @@ -0,0 +1,825 @@ +/* tslint:disable:variable-name no-namespace */ +import URI from 'urijs'; + +import { + Account, + Address, + Contract, + FeeBumpTransaction, + Keypair, + Transaction, + xdr, + hash +} from 'stellar-base'; + +import AxiosClient from './axios'; +import { Api as FriendbotApi } from '../friendbot'; +import * as jsonrpc from './jsonrpc'; +import { Api } from './soroban_rpc'; +import { assembleTransaction } from './transaction'; +import { + parseRawSendTransaction, + parseRawSimulation, + parseRawLedgerEntries, + parseRawEvents +} from './parsers'; + +export const SUBMIT_TRANSACTION_TIMEOUT = 60 * 1000; + +/** Specifies the durability namespace of contract-related ledger entries. */ +export enum Durability { + Temporary = 'temporary', + Persistent = 'persistent' +} + +export namespace Server { + /** Describes the complex filter combinations available for event queries. */ + export interface GetEventsRequest { + filters: Api.EventFilter[]; + startLedger?: number; // either this or cursor + cursor?: string; // either this or startLedger + limit?: number; + } + + export interface Options { + allowHttp?: boolean; + timeout?: number; + headers?: Record; + } +} + +/** + * Handles the network connection to a Soroban RPC instance, exposing an + * interface for requests to that instance. + * + * @constructor + * + * @param {string} serverURL Soroban-RPC Server URL (ex. + * `http://localhost:8000/soroban/rpc`). + * @param {object} [opts] Options object + * @param {boolean} [opts.allowHttp] allows connecting to insecure http servers + * (default: `false`). This must be set to false in production deployments! + * You can also use {@link Config} class to set this globally. + * @param {Record} [opts.headers] allows setting custom headers + * + * @see https://soroban.stellar.org/api/methods + */ +export class Server { + /** Soroban RPC Server URL (ex. `http://localhost:8000/soroban/rpc`). */ + public readonly serverURL: URI; + + constructor(serverURL: string, opts: Server.Options = {}) { + this.serverURL = URI(serverURL); + + if (opts.headers && Object.keys(opts.headers).length === 0) { + AxiosClient.interceptors.request.use((config: any) => { + // merge the custom headers into any existing headers + config.headers = Object.assign(config.headers, opts.headers); + return config; + }); + } + + if (this.serverURL.protocol() !== 'https' && !opts.allowHttp) { + throw new Error( + "Cannot connect to insecure Soroban RPC server if `allowHttp` isn't set" + ); + } + } + + /** + * Fetch a minimal set of current info about a Stellar account. + * + * Needed to get the current sequence number for the account so you can build + * a successful transaction with {@link TransactionBuilder}. + * + * @param {string} address - The public address of the account to load. + * + * @returns {Promise} a promise to the {@link Account} object with + * a populated sequence number + * + * @see https://soroban.stellar.org/api/methods/getLedgerEntries + * @example + * const accountId = "GBZC6Y2Y7Q3ZQ2Y4QZJ2XZ3Z5YXZ6Z7Z2Y4QZJ2XZ3Z5YXZ6Z7Z2Y4"; + * server.getAccount(accountId).then((account) => { + * console.log("sequence:", account.sequence); + * }); + */ + public async getAccount(address: string): Promise { + const ledgerKey = xdr.LedgerKey.account( + new xdr.LedgerKeyAccount({ + accountId: Keypair.fromPublicKey(address).xdrPublicKey() + }) + ); + + const resp = await this.getLedgerEntries(ledgerKey); + if (resp.entries.length === 0) { + return Promise.reject({ + code: 404, + message: `Account not found: ${address}` + }); + } + + const accountEntry = resp.entries[0].val.account(); + return new Account(address, accountEntry.seqNum().toString()); + } + + /** + * General node health check. + * + * @returns {Promise} a promise to the + * {@link Api.GetHealthResponse} object with the status of the + * server (e.g. "healthy"). + * + * @see https://soroban.stellar.org/api/methods/getHealth + * @example + * server.getHealth().then((health) => { + * console.log("status:", health.status); + * }); + */ + public async getHealth(): Promise { + return jsonrpc.post( + this.serverURL.toString(), + 'getHealth' + ); + } + + /** + * Reads the current value of contract data ledger entries directly. + * + * 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}. + * + * @param {string|Address|Contract} contract the contract ID containing the + * 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 + * keyspace" that this ledger key belongs to, which is either 'temporary' + * or 'persistent' (the default), see {@link Durability}. + * + * @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"; + * const key = xdr.ScVal.scvSymbol("counter"); + * server.getContractData(contractId, key, Durability.Temporary).then(data => { + * console.log("value:", data.val); + * console.log("expirationLedgerSeq:", data.expirationLedgerSeq); + * console.log("lastModified:", data.lastModifiedLedgerSeq); + * console.log("latestLedger:", data.latestLedger); + * }); + */ + public async getContractData( + contract: string | Address | Contract, + key: xdr.ScVal, + durability: Durability = Durability.Persistent + ): Promise { + // coalesce `contract` param variants to an ScAddress + let scAddress: xdr.ScAddress; + if (typeof contract === 'string') { + scAddress = new Contract(contract).address().toScAddress(); + } else if (contract instanceof Address) { + scAddress = contract.toScAddress(); + } else if (contract instanceof Contract) { + scAddress = contract.address().toScAddress(); + } else { + throw new TypeError(`unknown contract type: ${contract}`); + } + + let xdrDurability: xdr.ContractDataDurability; + switch (durability) { + case Durability.Temporary: + xdrDurability = xdr.ContractDataDurability.temporary(); + break; + + case Durability.Persistent: + xdrDurability = xdr.ContractDataDurability.persistent(); + break; + + default: + throw new TypeError(`invalid durability: ${durability}`); + } + + let contractKey = xdr.LedgerKey.contractData( + new xdr.LedgerKeyContractData({ + key, + contract: scAddress, + durability: xdrDurability + }) + ); + + return this.getLedgerEntries(contractKey).then( + (r: Api.GetLedgerEntriesResponse) => { + if (r.entries.length === 0) { + return Promise.reject({ + code: 404, + message: `Contract data not found. Contract: ${Address.fromScAddress( + scAddress + ).toString()}, Key: ${key.toXDR( + 'base64' + )}, Durability: ${durability}` + }); + } + + return r.entries[0]; + } + ); + } + + /** + * Reads the current value of arbitrary ledger entries directly. + * + * Allows you to directly inspect the current state of contracts, contract's + * code, accounts, or any other ledger entries. + * + * To fetch a contract's WASM byte-code, built the appropriate + * {@link xdr.LedgerKeyContractCode} ledger entry key (or see + * {@link Contract.getFootprint}). + * + * @param {xdr.ScVal[]} keys one or more ledger entry keys to load + * + * @returns {Promise} the current + * on-chain values for the given ledger keys + * + * @see Server._getLedgerEntries + * @see https://soroban.stellar.org/api/methods/getLedgerEntries + * @example + * const contractId = "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"; + * const key = xdr.LedgerKey.contractData(new xdr.LedgerKeyContractData({ + * contractId: StrKey.decodeContract(contractId), + * key: xdr.ScVal.scvSymbol("counter"), + * })); + * + * server.getLedgerEntries([key]).then(response => { + * const ledgerData = response.entries[0]; + * console.log("key:", ledgerData.key); + * console.log("value:", ledgerData.val); + * console.log("expirationLedgerSeq:", ledgerData.expirationLedgerSeq); + * console.log("lastModified:", ledgerData.lastModifiedLedgerSeq); + * console.log("latestLedger:", response.latestLedger); + * }); + */ + public async getLedgerEntries( + ...keys: xdr.LedgerKey[] + ): Promise { + return this._getLedgerEntries(...keys).then(parseRawLedgerEntries); + } + + public async _getLedgerEntries(...keys: xdr.LedgerKey[]) { + return jsonrpc + .post( + this.serverURL.toString(), + 'getLedgerEntries', + expandRequestIncludeExpirationLedgers(keys).map((k) => + k.toXDR('base64') + ) + ) + .then((response) => mergeResponseExpirationLedgers(response, keys)); + } + + /** + * Fetch the details of a submitted transaction. + * + * After submitting a transaction, clients should poll this to tell when the + * transaction has completed. + * + * @param {string} hash hex-encoded hash of the transaction to check + * + * @returns {Promise} the status, + * result, and other details about the transaction + * + * @see https://soroban.stellar.org/api/methods/getTransaction + * @example + * const transactionHash = "c4515e3bdc0897f21cc5dbec8c82cf0a936d4741cb74a8e158eb51b9fb00411a"; + * server.getTransaction(transactionHash).then((tx) => { + * console.log("status:", tx.status); + * console.log("envelopeXdr:", tx.envelopeXdr); + * console.log("resultMetaXdr:", tx.resultMetaXdr); + * console.log("resultXdr:", tx.resultXdr); + * }); + */ + public async getTransaction( + hash: string + ): Promise { + return this._getTransaction(hash).then((raw) => { + let successInfo: Omit< + Api.GetSuccessfulTransactionResponse, + keyof Api.GetFailedTransactionResponse + > = {} as any; + + if (raw.status === Api.GetTransactionStatus.SUCCESS) { + const meta = xdr.TransactionMeta.fromXDR(raw.resultMetaXdr!, 'base64'); + successInfo = { + ledger: raw.ledger!, + createdAt: raw.createdAt!, + applicationOrder: raw.applicationOrder!, + feeBump: raw.feeBump!, + envelopeXdr: xdr.TransactionEnvelope.fromXDR( + raw.envelopeXdr!, + 'base64' + ), + resultXdr: xdr.TransactionResult.fromXDR(raw.resultXdr!, 'base64'), + resultMetaXdr: meta, + ...(meta.switch() === 3 && + meta.v3().sorobanMeta() !== null && { + returnValue: meta.v3().sorobanMeta()?.returnValue() + }) + }; + } + + const result: Api.GetTransactionResponse = { + status: raw.status, + latestLedger: raw.latestLedger, + latestLedgerCloseTime: raw.latestLedgerCloseTime, + oldestLedger: raw.oldestLedger, + oldestLedgerCloseTime: raw.oldestLedgerCloseTime, + ...successInfo + }; + + return result; + }); + } + + public async _getTransaction( + hash: string + ): Promise { + return jsonrpc.post(this.serverURL.toString(), 'getTransaction', hash); + } + + /** + * Fetch all events that match a given set of filters. + * + * The given filters (see {@link Api.EventFilter} for detailed fields) + * are combined only in a logical OR fashion, and all of the fields in each + * filter are optional. + * + * To page through events, use the `pagingToken` field on the relevant + * {@link Api.EventResponse} object to set the `cursor` parameter. + * + * @param {Server.GetEventsRequest} request event filters + * @returns {Promise} a paginatable set of the + * events matching the given event filters + * + * @see https://soroban.stellar.org/api/methods/getEvents + * @example + * server.getEvents({ + * startLedger: "1000", + * filters: [ + * { + * type: "contract", + * contractIds: [ "deadb33f..." ], + * topics: [[ "AAAABQAAAAh0cmFuc2Zlcg==", "AAAAAQB6Mcc=", "*" ]] + * }, { + * type: "system", + * contractIds: [ "...c4f3b4b3..." ], + * topics: [[ "*" ], [ "*", "AAAAAQB6Mcc=" ]] + * }, { + * contractIds: [ "...c4f3b4b3..." ], + * topics: [[ "AAAABQAAAAh0cmFuc2Zlcg==" ]] + * }, { + * type: "diagnostic", + * topics: [[ "AAAAAQB6Mcc=" ]] + * } + * ], + * limit: 10, + * }); + */ + public async getEvents( + request: Server.GetEventsRequest + ): Promise { + return this._getEvents(request).then(parseRawEvents); + } + + public async _getEvents( + request: Server.GetEventsRequest + ): Promise { + return jsonrpc.postObject(this.serverURL.toString(), 'getEvents', { + filters: request.filters ?? [], + pagination: { + ...(request.cursor && { cursor: request.cursor }), // add if defined + ...(request.limit && { limit: request.limit }) + }, + ...(request.startLedger && { + startLedger: request.startLedger.toString() + }) + }); + } + + /** + * Fetch metadata about the network this Soroban RPC server is connected to. + * + * @returns {Promise} metadata about the + * current network this RPC server is connected to + * + * @see https://soroban.stellar.org/api/methods/getNetwork + * @example + * server.getNetwork().then((network) => { + * console.log("friendbotUrl:", network.friendbotUrl); + * console.log("passphrase:", network.passphrase); + * console.log("protocolVersion:", network.protocolVersion); + * }); + */ + public async getNetwork(): Promise { + return await jsonrpc.post(this.serverURL.toString(), 'getNetwork'); + } + + /** + * Fetch the latest ledger meta info from network which this Soroban RPC + * server is connected to. + * + * @returns {Promise} metadata about the + * latest ledger on the network that this RPC server is connected to + * + * @see https://soroban.stellar.org/api/methods/getLatestLedger + * @example + * server.getLatestLedger().then((response) => { + * console.log("hash:", response.id); + * console.log("sequence:", response.sequence); + * console.log("protocolVersion:", response.protocolVersion); + * }); + */ + public async getLatestLedger(): Promise { + return jsonrpc.post(this.serverURL.toString(), 'getLatestLedger'); + } + + /** + * 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 + * simulate, which should include exactly one operation (one of + * {@link xdr.InvokeHostFunctionOp}, {@link xdr.BumpFootprintExpirationOp}, + * or {@link xdr.RestoreFootprintOp}). Any provided footprint or auth + * information will be ignored. + * + * @returns {Promise} an object with + * the cost, footprint, result/auth requirements (if applicable), and error + * of the transaction + * + * @see https://developers.stellar.org/docs/glossary/transactions/ + * @see https://soroban.stellar.org/api/methods/simulateTransaction + * @see Server.prepareTransaction + * @see assembleTransaction + * + * @example + * const contractId = 'CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE'; + * const contract = new StellarSdk.Contract(contractId); + * + * // Right now, this is just the default fee for this example. + * const fee = StellarSdk.BASE_FEE; + * const transaction = new StellarSdk.TransactionBuilder(account, { fee }) + * // Uncomment the following line to build transactions for the live network. Be + * // sure to also change the horizon hostname. + * //.setNetworkPassphrase(StellarSdk.Networks.PUBLIC) + * .setNetworkPassphrase(StellarSdk.Networks.FUTURENET) + * .setTimeout(30) // valid for the next 30s + * // Add an operation to call increment() on the contract + * .addOperation(contract.call("increment")) + * .build(); + * + * server.simulateTransaction(transaction).then((sim) => { + * console.log("cost:", sim.cost); + * console.log("result:", sim.result); + * console.log("error:", sim.error); + * console.log("latestLedger:", sim.latestLedger); + * }); + */ + public async simulateTransaction( + transaction: Transaction | FeeBumpTransaction + ): Promise { + return this._simulateTransaction(transaction).then(parseRawSimulation); + } + + public async _simulateTransaction( + transaction: Transaction | FeeBumpTransaction + ): Promise { + return jsonrpc.post( + this.serverURL.toString(), + 'simulateTransaction', + transaction.toXDR() + ); + } + + /** + * Submit a trial contract invocation, first run a simulation of the contract + * invocation as defined on the incoming transaction, and apply the results to + * a new copy of the transaction which is then returned. Setting the ledger + * footprint and authorization, so the resulting transaction is ready for + * signing & sending. + * + * The returned transaction will also have an updated fee that is the sum of + * fee set on incoming transaction with the contract resource fees estimated + * from simulation. It is adviseable to check the fee on returned transaction + * and validate or take appropriate measures for interaction with user to + * confirm it is acceptable. + * + * You can call the {@link Server.simulateTransaction} method directly first + * 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 + * prepare. It should include exactly one operation, which must be one of + * {@link xdr.InvokeHostFunctionOp}, {@link xdr.BumpFootprintExpirationOp}, + * or {@link xdr.RestoreFootprintOp}. + * + * Any provided footprint will be overwritten. However, if your operation + * has existing auth entries, they will be preferred over ALL auth entries + * from the simulation. In other words, if you include auth entries, you + * don't care about the auth returned from the simulation. Other fields + * (footprint, etc.) will be filled as normal. + * @param {string} [networkPassphrase] explicitly provide a network + * passphrase (default: requested from the server via + * {@link Server.getNetwork}). + * + * @returns {Promise} a copy of the + * transaction with the expected authorizations (in the case of + * invocation), resources, and ledger footprints added. The transaction fee + * will also automatically be padded with the contract's minimum resource + * fees discovered from the simulation. + * + * @see assembleTransaction + * @see https://soroban.stellar.org/api/methods/simulateTransaction + * @throws {jsonrpc.Error | Error} if simulation fails + * @example + * const contractId = 'CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE'; + * const contract = new StellarSdk.Contract(contractId); + * + * // Right now, this is just the default fee for this example. + * const fee = StellarSdk.BASE_FEE; + * const transaction = new StellarSdk.TransactionBuilder(account, { fee }) + * // Uncomment the following line to build transactions for the live network. Be + * // sure to also change the horizon hostname. + * //.setNetworkPassphrase(StellarSdk.Networks.PUBLIC) + * .setNetworkPassphrase(StellarSdk.Networks.FUTURENET) + * .setTimeout(30) // valid for the next 30s + * // Add an operation to call increment() on the contract + * .addOperation(contract.call("increment")) + * .build(); + * + * const preparedTransaction = await server.prepareTransaction(transaction); + * + * // Sign this transaction with the secret key + * // NOTE: signing is transaction is network specific. Test network transactions + * // won't work in the public network. To switch networks, use the Network object + * // as explained above (look for StellarSdk.Network). + * const sourceKeypair = StellarSdk.Keypair.fromSecret(sourceSecretKey); + * preparedTransaction.sign(sourceKeypair); + * + * server.sendTransaction(transaction).then(result => { + * console.log("hash:", result.hash); + * console.log("status:", result.status); + * console.log("errorResultXdr:", result.errorResultXdr); + * }); + */ + public async prepareTransaction( + transaction: Transaction | FeeBumpTransaction, + networkPassphrase?: string + ): Promise { + const [{ passphrase }, simResponse] = await Promise.all([ + networkPassphrase + ? Promise.resolve({ passphrase: networkPassphrase }) + : this.getNetwork(), + this.simulateTransaction(transaction) + ]); + if (Api.isSimulationError(simResponse)) { + throw simResponse.error; + } + if (!simResponse.result) { + throw new Error('transaction simulation failed'); + } + + return assembleTransaction(transaction, passphrase, simResponse).build(); + } + + /** + * Submit a real transaction to the Stellar network. + * + * Unlike Horizon, Soroban RPC does not wait for transaction completion. It + * simply validates the transaction and enqueues it. Clients should call + * {@link Server.getTransactionStatus} to learn about transaction + * success/failure. + * + * @param {Transaction | FeeBumpTransaction} transaction to submit + * @returns {Promise} the + * transaction id, status, and any error if available + * + * @see https://developers.stellar.org/docs/glossary/transactions/ + * @see https://soroban.stellar.org/api/methods/sendTransaction + * @example + * const contractId = 'CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE'; + * const contract = new StellarSdk.Contract(contractId); + * + * // Right now, this is just the default fee for this example. + * const fee = StellarSdk.BASE_FEE; + * const transaction = new StellarSdk.TransactionBuilder(account, { fee }) + * // Uncomment the following line to build transactions for the live network. Be + * // sure to also change the horizon hostname. + * //.setNetworkPassphrase(StellarSdk.Networks.PUBLIC) + * .setNetworkPassphrase(StellarSdk.Networks.FUTURENET) + * .setTimeout(30) // valid for the next 30s + * // Add an operation to call increment() on the contract + * .addOperation(contract.call("increment")) + * .build(); + * + * // Sign this transaction with the secret key + * // NOTE: signing is transaction is network specific. Test network transactions + * // won't work in the public network. To switch networks, use the Network object + * // as explained above (look for StellarSdk.Network). + * const sourceKeypair = StellarSdk.Keypair.fromSecret(sourceSecretKey); + * transaction.sign(sourceKeypair); + * + * server.sendTransaction(transaction).then((result) => { + * console.log("hash:", result.hash); + * console.log("status:", result.status); + * console.log("errorResultXdr:", result.errorResultXdr); + * }); + */ + public async sendTransaction( + transaction: Transaction | FeeBumpTransaction + ): Promise { + return this._sendTransaction(transaction).then(parseRawSendTransaction); + } + + public async _sendTransaction( + transaction: Transaction | FeeBumpTransaction + ): Promise { + return jsonrpc.post( + this.serverURL.toString(), + 'sendTransaction', + transaction.toXDR() + ); + } + + /** + * Fund a new account using the network's friendbot faucet, if any. + * + * @param {string | Account} address the address or account instance that we + * want to create and fund with friendbot + * @param {string} [friendbotUrl] optionally, an explicit address for + * friendbot (by default: this calls the Soroban RPC + * {@link Server.getNetwork} method to try to discover this network's + * Friendbot url). + * + * @returns {Promise} an {@link Account} object for the created + * account, or the existing account if it's already funded with the + * populated sequence number (note that the account will not be "topped + * off" if it already exists) + * + * @throws if Friendbot is not configured on this network or request failure + * + * @see + * https://developers.stellar.org/docs/fundamentals-and-concepts/testnet-and-pubnet#friendbot + * @see Friendbot.Response + * @example + * server + * .requestAirdrop("GBZC6Y2Y7Q3ZQ2Y4QZJ2XZ3Z5YXZ6Z7Z2Y4QZJ2XZ3Z5YXZ6Z7Z2Y4") + * .then((accountCreated) => { + * console.log("accountCreated:", accountCreated); + * }).catch((error) => { + * console.error("error:", error); + * }); + */ + public async requestAirdrop( + address: string | Pick, + friendbotUrl?: string + ): Promise { + const account = typeof address === 'string' ? address : address.accountId(); + friendbotUrl = friendbotUrl || (await this.getNetwork()).friendbotUrl; + if (!friendbotUrl) { + throw new Error('No friendbot URL configured for current network'); + } + + try { + const response = await AxiosClient.post( + `${friendbotUrl}?addr=${encodeURIComponent(account)}` + ); + + const meta = xdr.TransactionMeta.fromXDR( + response.data.result_meta_xdr, + 'base64' + ); + const sequence = findCreatedAccountSequenceInTransactionMeta(meta); + return new Account(account, sequence); + } catch (error: any) { + if (error.response?.status === 400) { + if (error.response.detail?.includes('createAccountAlreadyExist')) { + // Account already exists, load the sequence number + return this.getAccount(account); + } + } + throw error; + } + } +} + +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'); +} + +// TODO - remove once rpc updated to +// append expiration entry per data LK requested onto server-side response +// https://github.com/stellar/soroban-tools/issues/1010 +function mergeResponseExpirationLedgers( + ledgerEntriesResponse: Api.RawGetLedgerEntriesResponse, + requestedKeys: xdr.LedgerKey[] +): Api.RawGetLedgerEntriesResponse { + const requestedKeyXdrs = new Set( + requestedKeys.map((requestedKey) => requestedKey.toXDR('base64')) + ); + const expirationKeyToRawEntryResult = new Map< + String, + Api.RawLedgerEntryResult + >(); + (ledgerEntriesResponse.entries ?? []).forEach((rawEntryResult) => { + if (!rawEntryResult.key || !rawEntryResult.xdr) { + throw new TypeError( + `invalid ledger entry: ${JSON.stringify(rawEntryResult)}` + ); + } + const parsedKey = xdr.LedgerKey.fromXDR(rawEntryResult.key, 'base64'); + const isExpirationMeta = + parsedKey.switch().value === xdr.LedgerEntryType.expiration().value && + !requestedKeyXdrs.has(rawEntryResult.key); + const keyHash = isExpirationMeta + ? parsedKey.expiration().keyHash().toString() + : hash(parsedKey.toXDR()).toString(); + + const rawEntry = + expirationKeyToRawEntryResult.get(keyHash) ?? rawEntryResult; + + if (isExpirationMeta) { + const expirationLedgerSeq = xdr.LedgerEntryData.fromXDR( + rawEntryResult.xdr, + 'base64' + ) + .expiration() + .expirationLedgerSeq(); + expirationKeyToRawEntryResult.set(keyHash, { + ...rawEntry, + expirationLedgerSeq + }); + } else { + expirationKeyToRawEntryResult.set(keyHash, { + ...rawEntry, + ...rawEntryResult + }); + } + }); + + ledgerEntriesResponse.entries = [...expirationKeyToRawEntryResult.values()]; + return ledgerEntriesResponse; +} + +// TODO - remove once rpc updated to +// include expiration entry on responses for any data LK's requested +// https://github.com/stellar/soroban-tools/issues/1010 +function expandRequestIncludeExpirationLedgers( + keys: xdr.LedgerKey[] +): xdr.LedgerKey[] { + return keys.concat( + keys + .filter( + (key) => key.switch().value !== xdr.LedgerEntryType.expiration().value + ) + .map((key) => + xdr.LedgerKey.expiration( + new xdr.LedgerKeyExpiration({ keyHash: hash(key.toXDR()) }) + ) + ) + ); +} diff --git a/src/soroban/soroban_rpc.ts b/src/soroban/soroban_rpc.ts new file mode 100644 index 000000000..bb1bbd3e6 --- /dev/null +++ b/src/soroban/soroban_rpc.ts @@ -0,0 +1,340 @@ +import { AssetType, Contract, SorobanDataBuilder, xdr } from 'stellar-base'; + +// TODO: Better parsing for hashes + +/* tslint:disable-next-line:no-namespace */ +/** @namespace Api */ +export namespace Api { + export interface Balance { + asset_type: AssetType.credit4 | AssetType.credit12; + asset_code: string; + asset_issuer: string; + classic: string; + smart: string; + } + + export interface Cost { + cpuInsns: string; + memBytes: string; + } + + export interface GetHealthResponse { + status: 'healthy'; + } + + export interface LedgerEntryResult { + lastModifiedLedgerSeq?: number; + key: xdr.LedgerKey; + val: xdr.LedgerEntryData; + expirationLedgerSeq?: number; + } + + export interface RawLedgerEntryResult { + lastModifiedLedgerSeq?: number; + /** a base-64 encoded {@link xdr.LedgerKey} instance */ + key: string; + /** a base-64 encoded {@link xdr.LedgerEntryData} instance */ + xdr: string; + /** optional, a future ledger number upon which this entry will expire + * based on https://github.com/stellar/soroban-tools/issues/1010 + */ + expirationLedgerSeq?: number; + } + + /** An XDR-parsed version of {@link RawLedgerEntryResult} */ + export interface GetLedgerEntriesResponse { + entries: LedgerEntryResult[]; + latestLedger: number; + } + + /** @see https://soroban.stellar.org/api/methods/getLedgerEntries */ + export interface RawGetLedgerEntriesResponse { + entries?: RawLedgerEntryResult[]; + latestLedger: number; + } + + /* Response for jsonrpc method `getNetwork` + */ + export interface GetNetworkResponse { + friendbotUrl?: string; + passphrase: string; + protocolVersion: string; + } + + /* Response for jsonrpc method `getLatestLedger` + */ + export interface GetLatestLedgerResponse { + id: string; + sequence: number; + protocolVersion: string; + } + + export enum GetTransactionStatus { + SUCCESS = 'SUCCESS', + NOT_FOUND = 'NOT_FOUND', + FAILED = 'FAILED' + } + + export type GetTransactionResponse = + | GetSuccessfulTransactionResponse + | GetFailedTransactionResponse + | GetMissingTransactionResponse; + + interface GetAnyTransactionResponse { + status: GetTransactionStatus; + latestLedger: string; + latestLedgerCloseTime: number; + oldestLedger: string; + oldestLedgerCloseTime: number; + } + + export interface GetMissingTransactionResponse + extends GetAnyTransactionResponse { + status: GetTransactionStatus.NOT_FOUND; + } + + export interface GetFailedTransactionResponse + extends GetAnyTransactionResponse { + status: GetTransactionStatus.FAILED; + } + + export interface GetSuccessfulTransactionResponse + extends GetAnyTransactionResponse { + status: GetTransactionStatus.SUCCESS; + + ledger: number; + createdAt: number; + applicationOrder: number; + feeBump: boolean; + envelopeXdr: xdr.TransactionEnvelope; + resultXdr: xdr.TransactionResult; + resultMetaXdr: xdr.TransactionMeta; + + returnValue?: xdr.ScVal; // present iff resultMeta is a v3 + } + + export interface RawGetTransactionResponse { + status: GetTransactionStatus; + latestLedger: string; + latestLedgerCloseTime: number; + oldestLedger: string; + oldestLedgerCloseTime: number; + + // the fields below are set if status is SUCCESS + applicationOrder?: number; + feeBump?: boolean; + envelopeXdr?: string; + resultXdr?: string; + resultMetaXdr?: string; + ledger?: number; + createdAt?: number; + } + + export type EventType = 'contract' | 'system' | 'diagnostic'; + + export interface EventFilter { + type?: EventType; + contractIds?: string[]; + topics?: string[][]; + } + + export interface GetEventsResponse { + latestLedger: string; + events: EventResponse[]; + } + + interface EventResponse extends BaseEventResponse { + contractId: Contract; + topic: xdr.ScVal[]; + value: xdr.DiagnosticEvent; + } + + export interface RawGetEventsResponse { + latestLedger: string; + events: RawEventResponse[]; + } + + interface BaseEventResponse { + id: string; + type: EventType; + ledger: string; + ledgerClosedAt: string; + pagingToken: string; + inSuccessfulContractCall: boolean; + } + + interface RawEventResponse extends BaseEventResponse { + contractId: string; + topic: string[]; + value: { + xdr: string; + }; + } + + export interface RequestAirdropResponse { + transaction_id: string; + } + + export type SendTransactionStatus = + | 'PENDING' + | 'DUPLICATE' + | 'TRY_AGAIN_LATER' + | 'ERROR'; + + export interface SendTransactionResponse extends BaseSendTransactionResponse { + errorResult?: xdr.TransactionResult; + } + + export interface RawSendTransactionResponse + extends BaseSendTransactionResponse { + /** + * This is a base64-encoded instance of {@link xdr.TransactionResult}, set + * only when `status` is `"ERROR"`. + * + * It contains details on why the network rejected the transaction. + */ + errorResultXdr?: string; + } + + export interface BaseSendTransactionResponse { + status: SendTransactionStatus; + hash: string; + latestLedger: number; + latestLedgerCloseTime: number; + } + + export interface SimulateHostFunctionResult { + auth: xdr.SorobanAuthorizationEntry[]; + retval: xdr.ScVal; + } + + /** + * Simplifies {@link 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 + * "resultify") + * - if there was an expiration error, this includes error and restoration + * fields + * - for all other errors, this only includes error fields + * + * @see https://soroban.stellar.org/api/methods/simulateTransaction#returns + */ + export type SimulateTransactionResponse = + | SimulateTransactionSuccessResponse + | SimulateTransactionRestoreResponse + | SimulateTransactionErrorResponse; + + export interface BaseSimulateTransactionResponse { + /** always present: the JSON-RPC request ID */ + id: string; + + /** always present: the LCL known to the server when responding */ + latestLedger: string; + + /** + * 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[]; + + /** a private field to mark the schema as parsed */ + _parsed: boolean; + } + + /** Includes simplified fields only present on success. */ + export interface SimulateTransactionSuccessResponse + extends BaseSimulateTransactionResponse { + transactionData: SorobanDataBuilder; + minResourceFee: string; + cost: Cost; + + /** present only for invocation simulation */ + result?: SimulateHostFunctionResult; + } + + /** Includes details about why the simulation failed */ + export interface SimulateTransactionErrorResponse + extends BaseSimulateTransactionResponse { + error: string; + events: xdr.DiagnosticEvent[]; + } + + export interface SimulateTransactionRestoreResponse + extends SimulateTransactionSuccessResponse { + result: SimulateHostFunctionResult; // not optional now + + /** + * Indicates that a restoration is necessary prior to submission. + * + * In other words, seeing a restoration preamble means that your invocation + * was executed AS IF the required ledger entries were present, and this + * field includes information about what you need to restore for the + * simulation to succeed. + */ + restorePreamble: { + minResourceFee: string; + transactionData: SorobanDataBuilder; + }; + } + + export function isSimulationError( + sim: SimulateTransactionResponse + ): sim is SimulateTransactionErrorResponse { + return 'error' in sim; + } + + export function isSimulationSuccess( + sim: SimulateTransactionResponse + ): sim is SimulateTransactionSuccessResponse { + return 'transactionData' in sim; + } + + export function isSimulationRestore( + sim: SimulateTransactionResponse + ): sim is SimulateTransactionRestoreResponse { + return ( + isSimulationSuccess(sim) && + 'restorePreamble' in sim && + !!sim.restorePreamble.transactionData + ); + } + + export function isSimulationRaw( + sim: + | Api.SimulateTransactionResponse + | Api.RawSimulateTransactionResponse + ): sim is Api.RawSimulateTransactionResponse { + return !(sim as Api.SimulateTransactionResponse)._parsed; + } + + interface RawSimulateHostFunctionResult { + // each string is SorobanAuthorizationEntry XDR in base64 + auth?: string[]; + // invocation return value: the ScVal in base64 + xdr: string; + } + + /** @see https://soroban.stellar.org/api/methods/simulateTransaction#returns */ + export interface RawSimulateTransactionResponse { + id: string; + latestLedger: string; + error?: string; + // this is an xdr.SorobanTransactionData in base64 + transactionData?: string; + // these are xdr.DiagnosticEvents in base64 + events?: string[]; + minResourceFee?: string; + // This will only contain a single element if present, because only a single + // invokeHostFunctionOperation is supported per transaction. + results?: RawSimulateHostFunctionResult[]; + cost?: Cost; + // present if succeeded but has expired ledger entries + restorePreamble?: { + minResourceFee: string; + transactionData: string; + }; + } +} diff --git a/src/soroban/transaction.ts b/src/soroban/transaction.ts new file mode 100644 index 000000000..7ebef2743 --- /dev/null +++ b/src/soroban/transaction.ts @@ -0,0 +1,115 @@ +import { + FeeBumpTransaction, + Operation, + Transaction, + TransactionBuilder +} from 'stellar-base'; + +import { Api } from './soroban_rpc'; +import { parseRawSimulation } from './parsers'; + +/** + * Combines the given raw transaction alongside the simulation results. + * + * @param raw the initial transaction, w/o simulation applied + * @param networkPassphrase the network this simulation applies to (see + * {@link Networks} for options) + * @param simulation the Soroban RPC simulation result (see + * {@link Server.simulateTransaction}) + * + * @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} + */ +export function assembleTransaction( + raw: Transaction | FeeBumpTransaction, + networkPassphrase: string, + simulation: + | Api.SimulateTransactionResponse + | Api.RawSimulateTransactionResponse +): TransactionBuilder { + if ('innerTransaction' in raw) { + // TODO: Handle feebump transactions + return assembleTransaction( + raw.innerTransaction, + networkPassphrase, + simulation + ); + } + + if (!isSorobanTransaction(raw)) { + throw new TypeError( + 'unsupported transaction: must contain exactly one ' + + 'invokeHostFunction, bumpFootprintExpiration, or restoreFootprint ' + + 'operation' + ); + } + + let success = parseRawSimulation(simulation); + if (!Api.isSimulationSuccess(success)) { + throw new Error(`simulation incorrect: ${JSON.stringify(success)}`); + } + + const classicFeeNum = parseInt(raw.fee) || 0; + const minResourceFeeNum = parseInt(success.minResourceFee) || 0; + const txnBuilder = TransactionBuilder.cloneFrom(raw, { + // automatically update the tx fee that will be set on the resulting tx to + // the sum of 'classic' fee provided from incoming tx.fee and minResourceFee + // provided by simulation. + // + // 'classic' tx fees are measured as the product of tx.fee * 'number of + // operations', In soroban contract tx, there can only be single operation + // in the tx, so can make simplification of total classic fees for the + // soroban transaction will be equal to incoming tx.fee + minResourceFee. + fee: (classicFeeNum + minResourceFeeNum).toString(), + // apply the pre-built Soroban Tx Data from simulation onto the Tx + sorobanData: success.transactionData.build(), + 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(); + + 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; + } + + return txnBuilder; +} + +function isSorobanTransaction(tx: Transaction): boolean { + if (tx.operations.length !== 1) { + return false; + } + + switch (tx.operations[0].type) { + case 'invokeHostFunction': + case 'bumpFootprintExpiration': + case 'restoreFootprint': + return true; + + default: + return false; + } +} diff --git a/src/soroban/utils.ts b/src/soroban/utils.ts new file mode 100644 index 000000000..af4bb76a2 --- /dev/null +++ b/src/soroban/utils.ts @@ -0,0 +1,8 @@ +// Check if the given object X has a field Y, and make that available to +// typescript typing. +export function hasOwnProperty( + obj: X, + prop: Y, +): obj is X & Record { + return obj.hasOwnProperty(prop); +} diff --git a/src/stellar_toml_resolver.ts b/src/stellartoml/index.ts similarity index 93% rename from src/stellar_toml_resolver.ts rename to src/stellartoml/index.ts index 181734daf..651f01298 100644 --- a/src/stellar_toml_resolver.ts +++ b/src/stellartoml/index.ts @@ -1,23 +1,22 @@ import axios from "axios"; -import { Networks } from "stellar-base"; import toml from "toml"; -import { Config } from "./config"; +import { Networks } from "stellar-base"; + +import { Config } from "../config"; -// STELLAR_TOML_MAX_SIZE is the maximum size of stellar.toml file +/** the maximum size of stellar.toml file */ export const STELLAR_TOML_MAX_SIZE = 100 * 1024; // axios timeout doesn't catch missing urls, e.g. those with no response // so we use the axios cancel token to ensure the timeout const CancelToken = axios.CancelToken; -/** - * StellarTomlResolver allows resolving `stellar.toml` files. - */ -export class StellarTomlResolver { +/** Resolver allows resolving `stellar.toml` files. */ +export class Resolver { /** * Returns a parsed `stellar.toml` file for a given domain. * ```js - * StellarSdk.StellarTomlResolver.resolve('acme.com') + * StellarSdk.Resolver.resolve('acme.com') * .then(stellarToml => { * // stellarToml in an object representing domain stellar.toml file. * }) @@ -34,8 +33,8 @@ export class StellarTomlResolver { */ public static async resolve( domain: string, - opts: StellarTomlResolver.StellarTomlResolveOptions = {}, - ): Promise { + opts: Api.StellarTomlResolveOptions = {}, + ): Promise { const allowHttp = typeof opts.allowHttp === "undefined" ? Config.isAllowHttp() @@ -84,7 +83,7 @@ export class StellarTomlResolver { } /* tslint:disable-next-line: no-namespace */ -export namespace StellarTomlResolver { +export namespace Api { export interface StellarTomlResolveOptions { allowHttp?: boolean; timeout?: number; diff --git a/src/utils.ts b/src/utils.ts index 2a99b4a97..88edd34e7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,724 +1,15 @@ -import randomBytes from "randombytes"; -import { - Account, - BASE_FEE, - FeeBumpTransaction, - Keypair, - Memo, - MemoID, - MemoNone, - Operation, - TimeoutInfinite, - Transaction, - TransactionBuilder, -} from "stellar-base"; -import { InvalidSep10ChallengeError } from "./errors"; -import { ServerApi } from "./server_api"; - -/** - * @namespace Utils - */ -export namespace Utils { - /** - * Returns a valid [SEP0010](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md) - * challenge transaction which you can use for Stellar Web Authentication. - * - * @see [SEP0010: Stellar Web Authentication](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md). - * @function - * @memberof Utils - * @param {Keypair} serverKeypair Keypair for server's signing account. - * @param {string} clientAccountID The stellar account (G...) or muxed account (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 {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 issuing the challenge. - * @param {string} [memo] The memo to attach to the challenge transaction. The memo must be of type `id`. If the `clientaccountID` is a muxed account, memos cannot be used. - * @param {string} [clientDomain] The fully qualified domain of the client requesting the challenge. Only necessary when the the 'client_domain' parameter is passed. - * @param {string} [clientSigningKey] The public key assigned to the SIGNING_KEY attribute specified on the stellar.toml hosted on the client domain. Only necessary when the 'client_domain' parameter is passed. - * @example - * import { Utils, Keypair, Networks } from 'stellar-sdk' - * - * let serverKeyPair = Keypair.fromSecret("server-secret") - * let challenge = Utils.buildChallengeTx(serverKeyPair, "client-stellar-account-id", "stellar.org", 300, Networks.TESTNET) - * @returns {string} A base64 encoded string of the raw TransactionEnvelope xdr struct for the transaction. - */ - export function buildChallengeTx( - serverKeypair: Keypair, - clientAccountID: string, - homeDomain: string, - timeout: number = 300, - networkPassphrase: string, - webAuthDomain: string, - memo: string | null = null, - clientDomain: string | null = null, - clientSigningKey: string | null = null, - ): string { - if (clientAccountID.startsWith("M") && memo) { - throw Error("memo cannot be used if clientAccountID is a muxed account"); - } - - const account = new Account(serverKeypair.publicKey(), "-1"); - const now = Math.floor(Date.now() / 1000); - - // A Base64 digit represents 6 bits, to generate a random 64 bytes - // base64 string, we need 48 random bytes = (64 * 6)/8 - // - // Each Base64 digit is in ASCII and each ASCII characters when - // turned into binary represents 8 bits = 1 bytes. - const value = randomBytes(48).toString("base64"); - - const builder = new TransactionBuilder(account, { - fee: BASE_FEE, - networkPassphrase, - timebounds: { - minTime: now, - maxTime: now + timeout, - }, - }) - .addOperation( - Operation.manageData({ - name: `${homeDomain} auth`, - value, - source: clientAccountID, - }), - ) - .addOperation( - Operation.manageData({ - name: "web_auth_domain", - value: webAuthDomain, - source: account.accountId(), - }), - ); - - if (clientDomain) { - if (!clientSigningKey) { - throw Error("clientSigningKey is required if clientDomain is provided"); - } - builder.addOperation( - Operation.manageData({ - name: `client_domain`, - value: clientDomain, - source: clientSigningKey, - }), - ); - } - - if (memo) { - builder.addMemo(Memo.id(memo)); - } - - const transaction = builder.build(); - transaction.sign(serverKeypair); - - return transaction - .toEnvelope() - .toXDR("base64") - .toString(); - } - - /** - * readChallengeTx reads a SEP 10 challenge transaction and returns the decoded - * transaction and client account ID contained within. - * - * It also verifies that the transaction has been signed by the server. - * - * It does not verify that the transaction has been signed by the client or - * that any signatures other than the server's on the transaction are valid. Use - * one of the following functions to completely verify the transaction: - * - verifyChallengeTxThreshold - * - verifyChallengeTxSigners - * - * @see [SEP0010: Stellar Web Authentication](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md). - * @function - * @memberof Utils - * @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'. - * @param {string|string[]} [homeDomains] The home domain that is expected to be included in the first Manage Data operation's string key. If an array is provided, one of the domain names in the array must match. - * @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 no such operation is included, this parameter is not used. - * @returns {Transaction|string|string|string} The actual transaction and the stellar public key (master key) used to sign the Manage Data operation, the matched home domain, and the memo attached to the transaction, which will be null if not present. - */ - export function readChallengeTx( - challengeTx: string, - serverAccountID: string, - networkPassphrase: string, - homeDomains: string | string[], - webAuthDomain: string, - ): { - tx: Transaction; - clientAccountID: string; - matchedHomeDomain: string; - memo: string | null; - } { - if (serverAccountID.startsWith("M")) { - throw Error( - "Invalid serverAccountID: multiplexed accounts are not supported.", - ); - } - - let transaction; - try { - transaction = new Transaction(challengeTx, networkPassphrase); - } catch { - try { - transaction = new FeeBumpTransaction(challengeTx, networkPassphrase); - } catch { - throw new InvalidSep10ChallengeError( - "Invalid challenge: unable to deserialize challengeTx transaction string", - ); - } - throw new InvalidSep10ChallengeError( - "Invalid challenge: expected a Transaction but received a FeeBumpTransaction", - ); - } - - // verify sequence number - const sequence = Number.parseInt(transaction.sequence, 10); - - if (sequence !== 0) { - throw new InvalidSep10ChallengeError( - "The transaction sequence number should be zero", - ); - } - - // verify transaction source - if (transaction.source !== serverAccountID) { - throw new InvalidSep10ChallengeError( - "The transaction source account is not equal to the server's account", - ); - } - - // verify operation - if (transaction.operations.length < 1) { - throw new InvalidSep10ChallengeError( - "The transaction should contain at least one operation", - ); - } - - const [operation, ...subsequentOperations] = transaction.operations; - - if (!operation.source) { - throw new InvalidSep10ChallengeError( - "The transaction's operation should contain a source account", - ); - } - const clientAccountID: string = operation.source!; - - let memo: string | null = null; - if (transaction.memo.type !== MemoNone) { - if (clientAccountID.startsWith("M")) { - throw new InvalidSep10ChallengeError( - "The transaction has a memo but the client account ID is a muxed account", - ); - } - if (transaction.memo.type !== MemoID) { - throw new InvalidSep10ChallengeError( - "The transaction's memo must be of type `id`", - ); - } - memo = transaction.memo.value as string; - } - - if (operation.type !== "manageData") { - throw new InvalidSep10ChallengeError( - "The transaction's operation type should be 'manageData'", - ); - } - - // verify timebounds - if ( - transaction.timeBounds && - Number.parseInt(transaction.timeBounds?.maxTime, 10) === TimeoutInfinite - ) { - throw new InvalidSep10ChallengeError( - "The transaction requires non-infinite timebounds", - ); - } - - // give a small grace period for the transaction time to account for clock drift - if (!validateTimebounds(transaction, 60 * 5)) { - throw new InvalidSep10ChallengeError("The transaction has expired"); - } - - if (operation.value === undefined) { - throw new InvalidSep10ChallengeError( - "The transaction's operation values should not be null", - ); - } - - // verify base64 - if (!operation.value) { - throw new InvalidSep10ChallengeError( - "The transaction's operation value should not be null", - ); - } - - if (Buffer.from(operation.value.toString(), "base64").length !== 48) { - throw new InvalidSep10ChallengeError( - "The transaction's operation value should be a 64 bytes base64 random string", - ); - } - - // verify homeDomains - if (!homeDomains) { - throw new InvalidSep10ChallengeError( - "Invalid homeDomains: a home domain must be provided for verification", - ); - } - - let matchedHomeDomain; - - if (typeof homeDomains === "string") { - if (`${homeDomains} auth` === operation.name) { - matchedHomeDomain = homeDomains; - } - } else if (Array.isArray(homeDomains)) { - matchedHomeDomain = homeDomains.find( - (domain) => `${domain} auth` === operation.name, - ); - } else { - throw new InvalidSep10ChallengeError( - `Invalid homeDomains: homeDomains type is ${typeof homeDomains} but should be a string or an array`, - ); - } - - if (!matchedHomeDomain) { - throw new InvalidSep10ChallengeError( - "Invalid homeDomains: the transaction's operation key name does not match the expected home domain", - ); - } - - // verify any subsequent operations are manage data ops and source account is the server - for (const op of subsequentOperations) { - if (op.type !== "manageData") { - throw new InvalidSep10ChallengeError( - "The transaction has operations that are not of type 'manageData'", - ); - } - if (op.source !== serverAccountID && op.name !== "client_domain") { - throw new InvalidSep10ChallengeError( - "The transaction has operations that are unrecognized", - ); - } - if (op.name === "web_auth_domain") { - if (op.value === undefined) { - throw new InvalidSep10ChallengeError( - "'web_auth_domain' operation value should not be null", - ); - } - if (op.value.compare(Buffer.from(webAuthDomain))) { - throw new InvalidSep10ChallengeError( - `'web_auth_domain' operation value does not match ${webAuthDomain}`, - ); - } - } - } - - if (!verifyTxSignedBy(transaction, serverAccountID)) { - throw new InvalidSep10ChallengeError( - `Transaction not signed by server: '${serverAccountID}'`, - ); - } - - return { tx: transaction, clientAccountID, matchedHomeDomain, memo }; - } - - /** - * verifyChallengeTxThreshold 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 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. - * - * @see [SEP0010: Stellar Web Authentication](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md). - * @function - * @memberof Utils - * @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'. - * @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. - * @example - * - * import { Networks, TransactionBuilder, Utils } from 'stellar-sdk'; - * - * const serverKP = Keypair.random(); - * const clientKP1 = Keypair.random(); - * const clientKP2 = Keypair.random(); - * - * // Challenge, possibly built in the server side - * const challenge = Utils.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()] - * Utils.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 InvalidSep10ChallengeError( - `signers with weight ${weight} do not meet threshold ${threshold}"`, - ); - } - - return signersFound; - } - - /** - * verifyChallengeTxSigners 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 the server account, and all other signatures match a signer - * that has been provided as an argument (as the accountIDs list). Additional signers - * can be provided that do not have a signature, but all signatures must be matched - * to a signer (accountIDs) for verification to succeed. If verification succeeds, - * a list of signers that were found is returned, not including the server account ID. - * - * 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 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. - * - * @see [SEP0010: Stellar Web Authentication](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md). - * @function - * @memberof Utils - * @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'. - * @param {string[]} signers The signers public keys. This list should contain the public keys for all signers that have signed the transaction. - * @param {string|string[]} [homeDomains] The home domain(s) that should be included in the first Manage Data operation's string key. Required in 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 readChallengeTx(). - * @returns {string[]} The list of signers public keys that have signed the transaction, excluding the server account ID. - * @example - * - * import { Networks, TransactionBuilder, Utils } from 'stellar-sdk'; - * - * const serverKP = Keypair.random(); - * const clientKP1 = Keypair.random(); - * const clientKP2 = Keypair.random(); - * - * // Challenge, possibly built in the server side - * const challenge = Utils.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(); - * - * // The result below should be equal to [clientKP1.publicKey(), clientKP2.publicKey()] - * Utils.verifyChallengeTxSigners(signedChallenge, serverKP.publicKey(), Networks.TESTNET, threshold, [clientKP1.publicKey(), clientKP2.publicKey()]); - */ - export function verifyChallengeTxSigners( - challengeTx: string, - serverAccountID: string, - networkPassphrase: string, - signers: string[], - homeDomains: string | string[], - webAuthDomain: string, - ): string[] { - // Read the transaction which validates its structure. - const { tx } = readChallengeTx( - challengeTx, - serverAccountID, - networkPassphrase, - homeDomains, - webAuthDomain, - ); - - // Ensure the server account ID is an address and not a seed. - let serverKP: Keypair; - try { - 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, - ); - } - - // 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); - } - - // Don't continue if none of the signers provided are in the final list. - if (clientSigners.size === 0) { - throw new InvalidSep10ChallengeError( - "No verifiable client signers provided, at least one G... address must be provided", - ); - } - - let clientSigningKey; - for (const op of tx.operations) { - if (op.type === "manageData" && op.name === "client_domain") { - if (clientSigningKey) { - throw new InvalidSep10ChallengeError( - "Found more than one client_domain operation", - ); - } - 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 - // checked in the ReadChallengeTx to ensure that every signature and signer - // are consumed only once on the transaction. - const allSigners: string[] = [ - serverKP.publicKey(), - ...Array.from(clientSigners), - ]; - if (clientSigningKey) { - allSigners.push(clientSigningKey); - } - - const signersFound: string[] = gatherTxSigners(tx, allSigners); - - let serverSignatureFound = false; - let clientSigningKeySignatureFound = false; - 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 InvalidSep10ChallengeError( - "Transaction not signed by server: '" + serverKP.publicKey() + "'", - ); - } - - // Confirm we matched a signature to the client domain's signer - if (clientSigningKey && !clientSigningKeySignatureFound) { - throw new InvalidSep10ChallengeError( - "Transaction not signed by the source account of the 'client_domain' " + - "ManageData operation", - ); - } - - // Confirm we matched at least one given signer with the transaction signatures - if (signersFound.length === 1) { - throw new InvalidSep10ChallengeError( - "None of the given signers match the transaction signatures", - ); - } - - // Confirm all signatures, including the server signature, were consumed by a signer: - if (signersFound.length !== tx.signatures.length) { - throw new InvalidSep10ChallengeError( - "Transaction has unrecognized signatures", - ); - } - - // Remove the server public key before returning - signersFound.splice(signersFound.indexOf(serverKP.publicKey()), 1); - if (clientSigningKey) { - // Remove the client domain public key public key before returning - signersFound.splice(signersFound.indexOf(clientSigningKey), 1); - } - - return signersFound; - } - - /** - * Verifies if a transaction was signed by the given account id. - * - * @function - * @memberof Utils - * @param {Transaction} transaction - * @param {string} accountID - * @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) - * Utils.verifyTxSignedBy(transaction, keypair.publicKey()) - * @returns {boolean}. - */ - export function verifyTxSignedBy( - transaction: FeeBumpTransaction | Transaction, - accountID: string, - ): boolean { - return gatherTxSigners(transaction, [accountID]).length !== 0; - } - - /** - * - * gatherTxSigners 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 Utils - * @param {Transaction} transaction the signed transaction. - * @param {string[]} signers The signers public keys. - * @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) - * Utils.gatherTxSigners(transaction, [keypair1.publicKey(), keypair2.publicKey()]) - * @returns {string[]} a list of signers that were found to have signed the transaction. - */ - 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(); - - 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 InvalidSep10ChallengeError( - "Signer is not a valid address: " + err.message, - ); - } - - 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); - } +import { Transaction } from "stellar-base"; +export class Utils { /** * Verifies if the current date is within the transaction's timebonds * + * @static * @function - * @memberof Utils * @param {Transaction} transaction the transaction whose timebonds will be validated. * @returns {boolean} returns true if the current time is within the transaction's [minTime, maxTime] range. */ - function validateTimebounds( + static validateTimebounds( transaction: Transaction, gracePeriod: number = 0, ): boolean { diff --git a/src/webauth/errors.ts b/src/webauth/errors.ts new file mode 100644 index 000000000..5ba7bca43 --- /dev/null +++ b/src/webauth/errors.ts @@ -0,0 +1,12 @@ + +export class InvalidChallengeError extends Error { + public __proto__: InvalidChallengeError; + + constructor(message: string) { + const trueProto = new.target.prototype; + super(message); + this.__proto__ = trueProto; + this.constructor = InvalidChallengeError; + this.name = "InvalidChallengeError"; + } + } \ No newline at end of file diff --git a/src/webauth/index.ts b/src/webauth/index.ts new file mode 100644 index 000000000..b6a43f27f --- /dev/null +++ b/src/webauth/index.ts @@ -0,0 +1,2 @@ +export * from './utils'; +export { InvalidChallengeError } from './errors'; \ No newline at end of file diff --git a/src/webauth/utils.ts b/src/webauth/utils.ts new file mode 100644 index 000000000..cc34287ba --- /dev/null +++ b/src/webauth/utils.ts @@ -0,0 +1,709 @@ +import randomBytes from "randombytes"; +import { + Account, + BASE_FEE, + FeeBumpTransaction, + Keypair, + Memo, + MemoID, + MemoNone, + Operation, + TimeoutInfinite, + Transaction, + TransactionBuilder, +} from "stellar-base"; + +import { Utils } from "../utils"; +import { InvalidChallengeError } from "./errors"; +import { ServerApi } from "../horizon/server_api"; + +/** + * Returns a valid [SEP0010](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md) + * challenge transaction which you can use for Stellar Web Authentication. + * + * @see [SEP0010: Stellar Web Authentication](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md). + * @function + * @memberof Utils + * @param {Keypair} serverKeypair Keypair for server's signing account. + * @param {string} clientAccountID The stellar account (G...) or muxed account (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 {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 issuing the challenge. + * @param {string} [memo] The memo to attach to the challenge transaction. The memo must be of type `id`. If the `clientaccountID` is a muxed account, memos cannot be used. + * @param {string} [clientDomain] The fully qualified domain of the client requesting the challenge. Only necessary when the the 'client_domain' parameter is passed. + * @param {string} [clientSigningKey] The public key assigned to the SIGNING_KEY attribute specified on the stellar.toml hosted on the client domain. Only necessary when the 'client_domain' parameter is passed. + * @example + * import { Utils, Keypair, Networks } from 'stellar-sdk' + * + * let serverKeyPair = Keypair.fromSecret("server-secret") + * let challenge = Sep10.buildChallengeTx(serverKeyPair, "client-stellar-account-id", "stellar.org", 300, Networks.TESTNET) + * @returns {string} A base64 encoded string of the raw TransactionEnvelope xdr struct for the transaction. + */ +export function buildChallengeTx( + serverKeypair: Keypair, + clientAccountID: string, + homeDomain: string, + timeout: number = 300, + networkPassphrase: string, + webAuthDomain: string, + memo: string | null = null, + clientDomain: string | null = null, + clientSigningKey: string | null = null, +): string { + if (clientAccountID.startsWith("M") && memo) { + throw Error("memo cannot be used if clientAccountID is a muxed account"); + } + + const account = new Account(serverKeypair.publicKey(), "-1"); + const now = Math.floor(Date.now() / 1000); + + // A Base64 digit represents 6 bits, to generate a random 64 bytes + // base64 string, we need 48 random bytes = (64 * 6)/8 + // + // Each Base64 digit is in ASCII and each ASCII characters when + // turned into binary represents 8 bits = 1 bytes. + const value = randomBytes(48).toString("base64"); + + const builder = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase, + timebounds: { + minTime: now, + maxTime: now + timeout, + }, + }) + .addOperation( + Operation.manageData({ + name: `${homeDomain} auth`, + value, + source: clientAccountID, + }), + ) + .addOperation( + Operation.manageData({ + name: "web_auth_domain", + value: webAuthDomain, + source: account.accountId(), + }), + ); + + if (clientDomain) { + if (!clientSigningKey) { + throw Error("clientSigningKey is required if clientDomain is provided"); + } + builder.addOperation( + Operation.manageData({ + name: `client_domain`, + value: clientDomain, + source: clientSigningKey, + }), + ); + } + + if (memo) { + builder.addMemo(Memo.id(memo)); + } + + const transaction = builder.build(); + transaction.sign(serverKeypair); + + return transaction + .toEnvelope() + .toXDR("base64") + .toString(); +} + +/** + * readChallengeTx reads a SEP 10 challenge transaction and returns the decoded + * transaction and client account ID contained within. + * + * It also verifies that the transaction has been signed by the server. + * + * It does not verify that the transaction has been signed by the client or + * that any signatures other than the server's on the transaction are valid. Use + * one of the following functions to completely verify the transaction: + * - verifyChallengeTxThreshold + * - verifyChallengeTxSigners + * + * @see [SEP0010: Stellar Web Authentication](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md). + * @function + * @memberof Utils + * @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'. + * @param {string|string[]} [homeDomains] The home domain that is expected to be included in the first Manage Data operation's string key. If an array is provided, one of the domain names in the array must match. + * @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 no such operation is included, this parameter is not used. + * @returns {Transaction|string|string|string} The actual transaction and the stellar public key (master key) used to sign the Manage Data operation, the matched home domain, and the memo attached to the transaction, which will be null if not present. + */ +export function readChallengeTx( + challengeTx: string, + serverAccountID: string, + networkPassphrase: string, + homeDomains: string | string[], + webAuthDomain: string, +): { + tx: Transaction; + clientAccountID: string; + matchedHomeDomain: string; + memo: string | null; +} { + if (serverAccountID.startsWith("M")) { + throw Error( + "Invalid serverAccountID: multiplexed accounts are not supported.", + ); + } + + let transaction; + try { + transaction = new Transaction(challengeTx, networkPassphrase); + } catch { + try { + transaction = new FeeBumpTransaction(challengeTx, networkPassphrase); + } catch { + throw new InvalidChallengeError( + "Invalid challenge: unable to deserialize challengeTx transaction string", + ); + } + throw new InvalidChallengeError( + "Invalid challenge: expected a Transaction but received a FeeBumpTransaction", + ); + } + + // verify sequence number + const sequence = Number.parseInt(transaction.sequence, 10); + + if (sequence !== 0) { + throw new InvalidChallengeError( + "The transaction sequence number should be zero", + ); + } + + // verify transaction source + if (transaction.source !== serverAccountID) { + throw new InvalidChallengeError( + "The transaction source account is not equal to the server's account", + ); + } + + // verify operation + if (transaction.operations.length < 1) { + throw new InvalidChallengeError( + "The transaction should contain at least one operation", + ); + } + + const [operation, ...subsequentOperations] = transaction.operations; + + if (!operation.source) { + throw new InvalidChallengeError( + "The transaction's operation should contain a source account", + ); + } + const clientAccountID: string = operation.source!; + + let memo: string | null = null; + if (transaction.memo.type !== MemoNone) { + if (clientAccountID.startsWith("M")) { + throw new InvalidChallengeError( + "The transaction has a memo but the client account ID is a muxed account", + ); + } + if (transaction.memo.type !== MemoID) { + throw new InvalidChallengeError( + "The transaction's memo must be of type `id`", + ); + } + memo = transaction.memo.value as string; + } + + if (operation.type !== "manageData") { + throw new InvalidChallengeError( + "The transaction's operation type should be 'manageData'", + ); + } + + // verify timebounds + if ( + transaction.timeBounds && + Number.parseInt(transaction.timeBounds?.maxTime, 10) === TimeoutInfinite + ) { + throw new InvalidChallengeError( + "The transaction requires non-infinite timebounds", + ); + } + + // give a small grace period for the transaction time to account for clock drift + if (!Utils.validateTimebounds(transaction, 60 * 5)) { + throw new InvalidChallengeError("The transaction has expired"); + } + + if (operation.value === undefined) { + throw new InvalidChallengeError( + "The transaction's operation values should not be null", + ); + } + + // verify base64 + if (!operation.value) { + throw new InvalidChallengeError( + "The transaction's operation value should not be null", + ); + } + + if (Buffer.from(operation.value.toString(), "base64").length !== 48) { + throw new InvalidChallengeError( + "The transaction's operation value should be a 64 bytes base64 random string", + ); + } + + // verify homeDomains + if (!homeDomains) { + throw new InvalidChallengeError( + "Invalid homeDomains: a home domain must be provided for verification", + ); + } + + let matchedHomeDomain; + + if (typeof homeDomains === "string") { + if (`${homeDomains} auth` === operation.name) { + matchedHomeDomain = homeDomains; + } + } else if (Array.isArray(homeDomains)) { + matchedHomeDomain = homeDomains.find( + (domain) => `${domain} auth` === operation.name, + ); + } else { + throw new InvalidChallengeError( + `Invalid homeDomains: homeDomains type is ${typeof homeDomains} but should be a string or an array`, + ); + } + + if (!matchedHomeDomain) { + throw new InvalidChallengeError( + "Invalid homeDomains: the transaction's operation key name does not match the expected home domain", + ); + } + + // verify any subsequent operations are manage data ops and source account is the server + for (const op of subsequentOperations) { + if (op.type !== "manageData") { + throw new InvalidChallengeError( + "The transaction has operations that are not of type 'manageData'", + ); + } + if (op.source !== serverAccountID && op.name !== "client_domain") { + throw new InvalidChallengeError( + "The transaction has operations that are unrecognized", + ); + } + if (op.name === "web_auth_domain") { + if (op.value === undefined) { + throw new InvalidChallengeError( + "'web_auth_domain' operation value should not be null", + ); + } + if (op.value.compare(Buffer.from(webAuthDomain))) { + throw new InvalidChallengeError( + `'web_auth_domain' operation value does not match ${webAuthDomain}`, + ); + } + } + } + + if (!verifyTxSignedBy(transaction, serverAccountID)) { + throw new InvalidChallengeError( + `Transaction not signed by server: '${serverAccountID}'`, + ); + } + + return { tx: transaction, clientAccountID, matchedHomeDomain, memo }; +} + +/** + * verifyChallengeTxThreshold 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 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. + * + * @see [SEP0010: Stellar Web Authentication](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md). + * @function + * @memberof Utils + * @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'. + * @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. + * @example + * + * import { Networks, TransactionBuilder, Utils } from 'stellar-sdk'; + * + * const serverKP = Keypair.random(); + * const clientKP1 = Keypair.random(); + * const clientKP2 = Keypair.random(); + * + * // Challenge, possibly built in the server side + * const challenge = Sep10.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()] + * Sep10.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; +} + +/** + * verifyChallengeTxSigners 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 the server account, and all other signatures match a signer + * that has been provided as an argument (as the accountIDs list). Additional signers + * can be provided that do not have a signature, but all signatures must be matched + * to a signer (accountIDs) for verification to succeed. If verification succeeds, + * a list of signers that were found is returned, not including the server account ID. + * + * 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 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. + * + * @see [SEP0010: Stellar Web Authentication](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md). + * @function + * @memberof Utils + * @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'. + * @param {string[]} signers The signers public keys. This list should contain the public keys for all signers that have signed the transaction. + * @param {string|string[]} [homeDomains] The home domain(s) that should be included in the first Manage Data operation's string key. Required in 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 readChallengeTx(). + * @returns {string[]} The list of signers public keys that have signed the transaction, excluding the server account ID. + * @example + * + * import { Networks, TransactionBuilder, Utils } from 'stellar-sdk'; + * + * const serverKP = Keypair.random(); + * const clientKP1 = Keypair.random(); + * const clientKP2 = Keypair.random(); + * + * // Challenge, possibly built in the server side + * const challenge = Sep10.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(); + * + * // The result below should be equal to [clientKP1.publicKey(), clientKP2.publicKey()] + * Sep10.verifyChallengeTxSigners(signedChallenge, serverKP.publicKey(), Networks.TESTNET, threshold, [clientKP1.publicKey(), clientKP2.publicKey()]); + */ +export function verifyChallengeTxSigners( + challengeTx: string, + serverAccountID: string, + networkPassphrase: string, + signers: string[], + homeDomains: string | string[], + webAuthDomain: string, +): string[] { + // Read the transaction which validates its structure. + const { tx } = readChallengeTx( + challengeTx, + serverAccountID, + networkPassphrase, + homeDomains, + webAuthDomain, + ); + + // Ensure the server account ID is an address and not a seed. + let serverKP: Keypair; + try { + 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, + ); + } + + // 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); + } + + // Don't continue if none of the signers provided are in the final list. + if (clientSigners.size === 0) { + throw new InvalidChallengeError( + "No verifiable client signers provided, at least one G... address must be provided", + ); + } + + let clientSigningKey; + for (const op of tx.operations) { + if (op.type === "manageData" && op.name === "client_domain") { + if (clientSigningKey) { + throw new InvalidChallengeError( + "Found more than one client_domain operation", + ); + } + 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 + // checked in the ReadChallengeTx to ensure that every signature and signer + // are consumed only once on the transaction. + const allSigners: string[] = [ + serverKP.publicKey(), + ...Array.from(clientSigners), + ]; + if (clientSigningKey) { + allSigners.push(clientSigningKey); + } + + const signersFound: string[] = gatherTxSigners(tx, allSigners); + + let serverSignatureFound = false; + let clientSigningKeySignatureFound = false; + 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() + "'", + ); + } + + // Confirm we matched a signature to the client domain's signer + if (clientSigningKey && !clientSigningKeySignatureFound) { + throw new InvalidChallengeError( + "Transaction not signed by the source account of the 'client_domain' " + + "ManageData operation", + ); + } + + // Confirm we matched at least one given signer with the transaction signatures + if (signersFound.length === 1) { + throw new InvalidChallengeError( + "None of the given signers match the transaction signatures", + ); + } + + // Confirm all signatures, including the server signature, were consumed by a signer: + if (signersFound.length !== tx.signatures.length) { + throw new InvalidChallengeError( + "Transaction has unrecognized signatures", + ); + } + + // Remove the server public key before returning + signersFound.splice(signersFound.indexOf(serverKP.publicKey()), 1); + if (clientSigningKey) { + // Remove the client domain public key public key before returning + signersFound.splice(signersFound.indexOf(clientSigningKey), 1); + } + + return signersFound; +} + +/** + * Verifies if a transaction was signed by the given account id. + * + * @function + * @memberof Sep10 + * @param {Transaction} transaction + * @param {string} accountID + * @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) + * Sep10.verifyTxSignedBy(transaction, keypair.publicKey()) + * @returns {boolean}. + */ +export function verifyTxSignedBy( + transaction: FeeBumpTransaction | Transaction, + accountID: string, +): boolean { + return gatherTxSigners(transaction, [accountID]).length !== 0; +} + +/** + * + * gatherTxSigners 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 Sep10 + * @param {Transaction} transaction the signed transaction. + * @param {string[]} signers The signers public keys. + * @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) + * Sep10.gatherTxSigners(transaction, [keypair1.publicKey(), keypair2.publicKey()]) + * @returns {string[]} a list of signers that were found to have signed the transaction. + */ +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(); + + 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++) { + 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 diff --git a/test/.eslintrc.js b/test/.eslintrc.js index 5aa559806..693deb59d 100644 --- a/test/.eslintrc.js +++ b/test/.eslintrc.js @@ -8,7 +8,6 @@ module.exports = { chai: true, sinon: true, expect: true, - HorizonAxiosClient: true, }, rules: { "no-unused-vars": 0, diff --git a/test/integration/apiary.js b/test/integration/apiary.js index 7c95d40fc..ac63ce827 100644 --- a/test/integration/apiary.js +++ b/test/integration/apiary.js @@ -4,13 +4,14 @@ // All endpoints from here are tested: // https://docs.google.com/document/d/1pXL8kr1a2vfYSap9T67R-g72B_WWbaE1YsLMa04OgoU/edit const _ = require("lodash"); +const { Horizon } = StellarSdk; const MOCK_SERVER = "https://private-d133c-ammmock.apiary-mock.com"; describe("tests the /liquidity_pools endpoint", function () { const lpId = "0569b19c75d7ecadce50501fffad6fe8ba4652455df9e1cc96dc408141124dd5"; - const server = new StellarSdk.Server(MOCK_SERVER, { allowHttp: true }); + const server = new Horizon.Server(MOCK_SERVER, { allowHttp: true }); it("GET /", function (done) { chai @@ -93,7 +94,7 @@ describe("tests the /liquidity_pools endpoint", function () { }); describe("tests the /accounts endpoint", function () { - const server = new StellarSdk.Server(MOCK_SERVER, { allowHttp: true }); + const server = new Horizon.Server(MOCK_SERVER, { allowHttp: true }); it("GET /", function (done) { chai diff --git a/test/integration/client_headers_test.js b/test/integration/client_headers_test.js index b285ab605..46c856927 100644 --- a/test/integration/client_headers_test.js +++ b/test/integration/client_headers_test.js @@ -1,3 +1,5 @@ +const { Horizon } = StellarSdk; + const http = require("http"); const url = require("url"); const port = 3100; @@ -27,7 +29,7 @@ describe("integration tests: client headers", function (done) { return; } - new StellarSdk.Server(`http://localhost:${port}`, { allowHttp: true }) + new Horizon.Server(`http://localhost:${port}`, { allowHttp: true }) .operations() .call(); }); @@ -56,7 +58,7 @@ describe("integration tests: client headers", function (done) { return; } - closeStream = new StellarSdk.Server(`http://localhost:${port}`, { + closeStream = new Horizon.Server(`http://localhost:${port}`, { allowHttp: true, }) .operations() diff --git a/test/integration/streaming_test.js b/test/integration/streaming_test.js index 080fafc1c..1d695764b 100644 --- a/test/integration/streaming_test.js +++ b/test/integration/streaming_test.js @@ -1,5 +1,6 @@ +const { Horizon } = StellarSdk; + const http = require("http"); -const url = require("url"); const port = 3100; describe("integration tests: streaming", function (done) { @@ -26,7 +27,7 @@ describe("integration tests: streaming", function (done) { return; } - closeStream = new StellarSdk.Server(`http://localhost:${port}`, { + closeStream = new Horizon.Server(`http://localhost:${port}`, { allowHttp: true, }) .operations() @@ -72,7 +73,7 @@ describe("integration tests: streaming", function (done) { return; } - closeStream = new StellarSdk.Server(`http://localhost:${port}`, { + closeStream = new Horizon.Server(`http://localhost:${port}`, { allowHttp: true, }) .operations() @@ -99,7 +100,7 @@ describe("end-to-end tests: real streaming", function (done) { // ledger's transaction batch). it("streams in perpetuity", function (done) { const DURATION = 30; - const server = new StellarSdk.Server("https://horizon.stellar.org"); + const server = new Horizon.Server("https://horizon.stellar.org"); this.timeout((DURATION + 5) * 1000); // pad timeout let transactions = []; diff --git a/test/spec.json b/test/spec.json new file mode 100644 index 000000000..887412199 --- /dev/null +++ b/test/spec.json @@ -0,0 +1,34 @@ +["AAAAAQAAAC9UaGlzIGlzIGZyb20gdGhlIHJ1c3QgZG9jIGFib3ZlIHRoZSBzdHJ1Y3QgVGVzdAAAAAAAAAAABFRlc3QAAAADAAAAAAAAAAFhAAAAAAAABAAAAAAAAAABYgAAAAAAAAEAAAAAAAAAAWMAAAAAAAAR", +"AAAAAgAAAAAAAAAAAAAAClNpbXBsZUVudW0AAAAAAAMAAAAAAAAAAAAAAAVGaXJzdAAAAAAAAAAAAAAAAAAABlNlY29uZAAAAAAAAAAAAAAAAAAFVGhpcmQAAAA=", +"AAAAAwAAAAAAAAAAAAAACVJveWFsQ2FyZAAAAAAAAAMAAAAAAAAABEphY2sAAAALAAAAAAAAAAVRdWVlbgAAAAAAAAwAAAAAAAAABEtpbmcAAAAN", +"AAAAAQAAAAAAAAAAAAAAC1R1cGxlU3RydWN0AAAAAAIAAAAAAAAAATAAAAAAAAfQAAAABFRlc3QAAAAAAAAAATEAAAAAAAfQAAAAClNpbXBsZUVudW0AAA==", +"AAAAAgAAAAAAAAAAAAAAC0NvbXBsZXhFbnVtAAAAAAUAAAABAAAAAAAAAAZTdHJ1Y3QAAAAAAAEAAAfQAAAABFRlc3QAAAABAAAAAAAAAAVUdXBsZQAAAAAAAAEAAAfQAAAAC1R1cGxlU3RydWN0AAAAAAEAAAAAAAAABEVudW0AAAABAAAH0AAAAApTaW1wbGVFbnVtAAAAAAABAAAAAAAAAAVBc3NldAAAAAAAAAIAAAATAAAACwAAAAAAAAAAAAAABFZvaWQ=", +"AAAABAAAAAAAAAAAAAAABUVycm9yAAAAAAAAAQAAABxQbGVhc2UgcHJvdmlkZSBhbiBvZGQgbnVtYmVyAAAAD051bWJlck11c3RCZU9kZAAAAAAB", +"AAAAAAAAAAAAAAAFaGVsbG8AAAAAAAABAAAAAAAAAAVoZWxsbwAAAAAAABEAAAABAAAAEQ==", +"AAAAAAAAAAAAAAAEd29pZAAAAAAAAAAA", +"AAAAAAAAAAAAAAADdmFsAAAAAAAAAAABAAAAAA==", +"AAAAAAAAAAAAAAAQdTMyX2ZhaWxfb25fZXZlbgAAAAEAAAAAAAAABHUzMl8AAAAEAAAAAQAAA+kAAAAEAAAAAw==", +"AAAAAAAAAAAAAAAEdTMyXwAAAAEAAAAAAAAABHUzMl8AAAAEAAAAAQAAAAQ=", +"AAAAAAAAAAAAAAAEaTMyXwAAAAEAAAAAAAAABGkzMl8AAAAFAAAAAQAAAAU=", +"AAAAAAAAAAAAAAAEaTY0XwAAAAEAAAAAAAAABGk2NF8AAAAHAAAAAQAAAAc=", +"AAAAAAAAACxFeGFtcGxlIGNvbnRyYWN0IG1ldGhvZCB3aGljaCB0YWtlcyBhIHN0cnVjdAAAAApzdHJ1a3RfaGVsAAAAAAABAAAAAAAAAAZzdHJ1a3QAAAAAB9AAAAAEVGVzdAAAAAEAAAPqAAAAEQ==", +"AAAAAAAAAAAAAAAGc3RydWt0AAAAAAABAAAAAAAAAAZzdHJ1a3QAAAAAB9AAAAAEVGVzdAAAAAEAAAfQAAAABFRlc3Q=", +"AAAAAAAAAAAAAAAGc2ltcGxlAAAAAAABAAAAAAAAAAZzaW1wbGUAAAAAB9AAAAAKU2ltcGxlRW51bQAAAAAAAQAAB9AAAAAKU2ltcGxlRW51bQAA", +"AAAAAAAAAAAAAAAHY29tcGxleAAAAAABAAAAAAAAAAdjb21wbGV4AAAAB9AAAAALQ29tcGxleEVudW0AAAAAAQAAB9AAAAALQ29tcGxleEVudW0A", +"AAAAAAAAAAAAAAAIYWRkcmVzc2UAAAABAAAAAAAAAAhhZGRyZXNzZQAAABMAAAABAAAAEw==", +"AAAAAAAAAAAAAAAFYnl0ZXMAAAAAAAABAAAAAAAAAAVieXRlcwAAAAAAAA4AAAABAAAADg==", +"AAAAAAAAAAAAAAAHYnl0ZXNfbgAAAAABAAAAAAAAAAdieXRlc19uAAAAA+4AAAAJAAAAAQAAA+4AAAAJ", +"AAAAAAAAAAAAAAAEY2FyZAAAAAEAAAAAAAAABGNhcmQAAAfQAAAACVJveWFsQ2FyZAAAAAAAAAEAAAfQAAAACVJveWFsQ2FyZAAAAA==", +"AAAAAAAAAAAAAAAHYm9vbGVhbgAAAAABAAAAAAAAAAdib29sZWFuAAAAAAEAAAABAAAAAQ==", +"AAAAAAAAABdOZWdhdGVzIGEgYm9vbGVhbiB2YWx1ZQAAAAADbm90AAAAAAEAAAAAAAAAB2Jvb2xlYW4AAAAAAQAAAAEAAAAB", +"AAAAAAAAAAAAAAAEaTEyOAAAAAEAAAAAAAAABGkxMjgAAAALAAAAAQAAAAs=", +"AAAAAAAAAAAAAAAEdTEyOAAAAAEAAAAAAAAABHUxMjgAAAAKAAAAAQAAAAo=", +"AAAAAAAAAAAAAAAKbXVsdGlfYXJncwAAAAAAAgAAAAAAAAABYQAAAAAAAAQAAAAAAAAAAWIAAAAAAAABAAAAAQAAAAQ=", +"AAAAAAAAAAAAAAADbWFwAAAAAAEAAAAAAAAAA21hcAAAAAPsAAAABAAAAAEAAAABAAAD7AAAAAQAAAAB", +"AAAAAAAAAAAAAAADdmVjAAAAAAEAAAAAAAAAA3ZlYwAAAAPqAAAABAAAAAEAAAPqAAAABA==", +"AAAAAAAAAAAAAAAFdHVwbGUAAAAAAAABAAAAAAAAAAV0dXBsZQAAAAAAA+0AAAACAAAAEQAAAAQAAAABAAAD7QAAAAIAAAARAAAABA==", +"AAAAAAAAAB9FeGFtcGxlIG9mIGFuIG9wdGlvbmFsIGFyZ3VtZW50AAAAAAZvcHRpb24AAAAAAAEAAAAAAAAABm9wdGlvbgAAAAAD6AAAAAQAAAABAAAD6AAAAAQ=", +"AAAAAAAAAAAAAAAEdTI1NgAAAAEAAAAAAAAABHUyNTYAAAAMAAAAAQAAAAw=", +"AAAAAAAAAAAAAAAEaTI1NgAAAAEAAAAAAAAABGkyNTYAAAANAAAAAQAAAA0=", +"AAAAAAAAAAAAAAAGc3RyaW5nAAAAAAABAAAAAAAAAAZzdHJpbmcAAAAAABAAAAABAAAAEA==", +"AAAAAAAAAAAAAAAMdHVwbGVfc3RydWt0AAAAAQAAAAAAAAAMdHVwbGVfc3RydWt0AAAH0AAAAAtUdXBsZVN0cnVjdAAAAAABAAAH0AAAAAtUdXBsZVN0cnVjdAA="] \ No newline at end of file diff --git a/test/test-browser.js b/test/test-browser.js index 430811c72..52f962918 100644 --- a/test/test-browser.js +++ b/test/test-browser.js @@ -1,4 +1,6 @@ /* eslint-disable no-undef */ chai.use(require("chai-as-promised")); window.axios = StellarSdk.axios; -window.HorizonAxiosClient = StellarSdk.HorizonAxiosClient; +window.HorizonAxiosClient = StellarSdk.Horizon.AxiosClient; +window.SorobanAxiosClient = StellarSdk.Soroban.AxiosClient; +window.serverUrl = "https://horizon-live.stellar.org:1337/api/v1/jsonrpc"; diff --git a/test/test-nodejs.js b/test/test-nodejs.js index 50c87d04b..a318048e5 100644 --- a/test/test-nodejs.js +++ b/test/test-nodejs.js @@ -4,7 +4,7 @@ require("@babel/register"); global.StellarSdk = require("../lib/"); global.axios = require("axios"); -global.HorizonAxiosClient = StellarSdk.HorizonAxiosClient; +global.serverUrl = "https://horizon-live.stellar.org:1337/api/v1/jsonrpc"; var chaiAsPromised = require("chai-as-promised"); var chaiHttp = require("chai-http"); diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 000000000..841368ba1 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "@stellar/tsconfig", + "compilerOptions": { + "declaration": false, + "sourceMap": false, + "removeComments": false, + "lib": ["es2015"], + "moduleResolution": "node", + "rootDir": "..", + // "outDir": "./unit/build", + "target": "ES2020", + "strict": false, + }, + "include": ["**/*.ts" ] + } \ No newline at end of file diff --git a/test/unit/call_builders_test.js b/test/unit/call_builders_test.js index 80541b7ec..cfdcf838c 100644 --- a/test/unit/call_builders_test.js +++ b/test/unit/call_builders_test.js @@ -1,5 +1,6 @@ -const URI = require("urijs"); -const CallBuilder = require("../../lib/call_builder").CallBuilder; +import URI from "urijs"; +// not exported by the library +import { CallBuilder } from "../../lib/horizon/call_builder"; describe("CallBuilder functions", function () { it("doesn't mutate the constructor passed url argument (it clones it instead)", function () { diff --git a/test/unit/contract_spec.js b/test/unit/contract_spec.js new file mode 100644 index 000000000..b73e03b38 --- /dev/null +++ b/test/unit/contract_spec.js @@ -0,0 +1,244 @@ +import { xdr, Address, ContractSpec } from "../../lib"; +//@ts-ignore +import spec from "../spec.json"; +import { expect } from "chai"; +const publicKey = "GCBVOLOM32I7OD5TWZQCIXCXML3TK56MDY7ZMTAILIBQHHKPCVU42XYW"; +const addr = Address.fromString(publicKey); +let SPEC; +before(() => { + SPEC = new ContractSpec(spec); +}); +it("throws if no entries", () => { + expect(() => new ContractSpec([])).to.throw(/Contract spec must have at least one entry/i); +}); +describe("Can round trip custom types", function () { + function getResultType(funcName) { + let fn = SPEC.findEntry(funcName).value(); + if (!(fn instanceof xdr.ScSpecFunctionV0)) { + throw new Error("Not a function"); + } + if (fn.outputs().length === 0) { + return xdr.ScSpecTypeDef.scSpecTypeVoid(); + } + return fn.outputs()[0]; + } + function roundtrip(funcName, input, typeName) { + let type = getResultType(funcName); + let ty = typeName ?? funcName; + let obj = {}; + obj[ty] = input; + let scVal = SPEC.funcArgsToScVals(funcName, obj)[0]; + let result = SPEC.scValToNative(scVal, type); + expect(result).deep.equal(input); + } + it("u32", () => { + roundtrip("u32_", 1); + }); + it("i32", () => { + roundtrip("i32_", -1); + }); + it("i64", () => { + roundtrip("i64_", 1n); + }); + it("strukt", () => { + roundtrip("strukt", { a: 0, b: true, c: "hello" }); + }); + describe("simple", () => { + it("first", () => { + const simple = { tag: "First", values: undefined }; + roundtrip("simple", simple); + }); + it("simple second", () => { + const simple = { tag: "Second", values: undefined }; + roundtrip("simple", simple); + }); + it("simple third", () => { + const simple = { tag: "Third", values: undefined }; + roundtrip("simple", simple); + }); + }); + describe("complex", () => { + it("struct", () => { + const complex = { + tag: "Struct", + values: [{ a: 0, b: true, c: "hello" }], + }; + roundtrip("complex", complex); + }); + it("tuple", () => { + const complex = { + tag: "Tuple", + values: [ + [ + { a: 0, b: true, c: "hello" }, + { tag: "First", values: undefined }, + ], + ], + }; + roundtrip("complex", complex); + }); + it("enum", () => { + const complex = { + tag: "Enum", + values: [{ tag: "First", values: undefined }], + }; + roundtrip("complex", complex); + }); + it("asset", () => { + const complex = { tag: "Asset", values: [addr, 1n] }; + roundtrip("complex", complex); + }); + it("void", () => { + const complex = { tag: "Void", values: undefined }; + roundtrip("complex", complex); + }); + }); + it("addresse", () => { + roundtrip("addresse", addr); + }); + it("bytes", () => { + const bytes = Buffer.from("hello"); + roundtrip("bytes", bytes); + }); + it("bytes_n", () => { + const bytes_n = Buffer.from("123456789"); // what's the correct way to construct bytes_n? + roundtrip("bytes_n", bytes_n); + }); + it("card", () => { + const card = 11; + roundtrip("card", card); + }); + it("boolean", () => { + roundtrip("boolean", true); + }); + it("not", () => { + roundtrip("boolean", false); + }); + it("i128", () => { + roundtrip("i128", -1n); + }); + it("u128", () => { + roundtrip("u128", 1n); + }); + it("map", () => { + const map = new Map(); + map.set(1, true); + map.set(2, false); + roundtrip("map", map); + map.set(3, "hahaha"); + expect(() => roundtrip("map", map)).to.throw(/invalid type scSpecTypeBool specified for string value/i); + }); + it("vec", () => { + const vec = [1, 2, 3]; + roundtrip("vec", vec); + }); + it("tuple", () => { + const tuple = ["hello", 1]; + roundtrip("tuple", tuple); + }); + it("option", () => { + roundtrip("option", 1); + roundtrip("option", undefined); + }); + it("u256", () => { + roundtrip("u256", 1n); + expect(() => roundtrip("u256", -1n)).to.throw(/expected a positive value, got: -1/i); + }); + it("i256", () => { + roundtrip("i256", -1n); + }); + it("string", () => { + roundtrip("string", "hello"); + }); + it("tuple_strukt", () => { + const arg = [ + { a: 0, b: true, c: "hello" }, + { tag: "First", values: undefined }, + ]; + roundtrip("tuple_strukt", arg); + }); +}); +describe("parsing and building ScVals", function () { + it("Can parse entries", function () { + let spec = new ContractSpec([GIGA_MAP, func]); + let fn = spec.findEntry("giga_map"); + let gigaMap = spec.findEntry("GigaMap"); + expect(gigaMap).deep.equal(GIGA_MAP); + expect(fn).deep.equal(func); + }); +}); +export const GIGA_MAP = xdr.ScSpecEntry.scSpecEntryUdtStructV0(new xdr.ScSpecUdtStructV0({ + doc: "This is a kitchen sink of all the types", + lib: "", + name: "GigaMap", + fields: [ + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "bool", + type: xdr.ScSpecTypeDef.scSpecTypeBool(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "i128", + type: xdr.ScSpecTypeDef.scSpecTypeI128(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "u128", + type: xdr.ScSpecTypeDef.scSpecTypeU128(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "i256", + type: xdr.ScSpecTypeDef.scSpecTypeI256(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "u256", + type: xdr.ScSpecTypeDef.scSpecTypeU256(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "i32", + type: xdr.ScSpecTypeDef.scSpecTypeI32(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "u32", + type: xdr.ScSpecTypeDef.scSpecTypeU32(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "i64", + type: xdr.ScSpecTypeDef.scSpecTypeI64(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "u64", + type: xdr.ScSpecTypeDef.scSpecTypeU64(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "symbol", + type: xdr.ScSpecTypeDef.scSpecTypeSymbol(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "string", + type: xdr.ScSpecTypeDef.scSpecTypeString(), + }), + ], +})); +const GIGA_MAP_TYPE = xdr.ScSpecTypeDef.scSpecTypeUdt(new xdr.ScSpecTypeUdt({ name: "GigaMap" })); +let func = xdr.ScSpecEntry.scSpecEntryFunctionV0(new xdr.ScSpecFunctionV0({ + doc: "Kitchen Sink", + name: "giga_map", + inputs: [ + new xdr.ScSpecFunctionInputV0({ + doc: "", + name: "giga_map", + type: GIGA_MAP_TYPE, + }), + ], + outputs: [GIGA_MAP_TYPE], +})); diff --git a/test/unit/contract_spec.ts b/test/unit/contract_spec.ts new file mode 100644 index 000000000..b9481d2a1 --- /dev/null +++ b/test/unit/contract_spec.ts @@ -0,0 +1,289 @@ +import { xdr, Address, ContractSpec } from "../../lib"; + +//@ts-ignore +import spec from "../spec.json"; +import { expect } from "chai"; + +const publicKey = "GCBVOLOM32I7OD5TWZQCIXCXML3TK56MDY7ZMTAILIBQHHKPCVU42XYW"; +const addr = Address.fromString(publicKey); +let SPEC: ContractSpec; + +before(() => { + SPEC = new ContractSpec(spec); +}) + +it("throws if no entries", () => { + expect(() => new ContractSpec([])).to.throw(/Contract spec must have at least one entry/i); +}); + +describe("Can round trip custom types", function () { + + function getResultType(funcName: string): xdr.ScSpecTypeDef { + let fn = SPEC.findEntry(funcName).value(); + if (!(fn instanceof xdr.ScSpecFunctionV0)) { + throw new Error("Not a function"); + } + if (fn.outputs().length === 0) { + return xdr.ScSpecTypeDef.scSpecTypeVoid(); + } + return fn.outputs()[0]; + } + + function roundtrip(funcName: string, input: any, typeName?: string) { + let type = getResultType(funcName); + let ty = typeName ?? funcName; + let obj: any = {}; + obj[ty] = input; + let scVal = SPEC.funcArgsToScVals(funcName, obj)[0]; + let result = SPEC.scValToNative(scVal, type); + expect(result).deep.equal(input); + } + + it("u32", () => { + roundtrip("u32_", 1); + }); + + it("i32", () => { + roundtrip("i32_", -1); + }); + + it("i64", () => { + roundtrip("i64_", 1n); + }); + + it("strukt", () => { + roundtrip("strukt", { a: 0, b: true, c: "hello" }); + }); + + describe("simple", () => { + it("first", () => { + const simple = { tag: "First", values: undefined } as const; + roundtrip("simple", simple); + }); + it("simple second", () => { + const simple = { tag: "Second", values: undefined } as const; + roundtrip("simple", simple); + }); + + it("simple third", () => { + const simple = { tag: "Third", values: undefined } as const; + roundtrip("simple", simple); + }); + }); + + describe("complex", () => { + it("struct", () => { + const complex = { + tag: "Struct", + values: [{ a: 0, b: true, c: "hello" }], + } as const; + roundtrip("complex", complex); + }); + + it("tuple", () => { + const complex = { + tag: "Tuple", + values: [ + [ + { a: 0, b: true, c: "hello" }, + { tag: "First", values: undefined }, + ], + ], + } as const; + roundtrip("complex", complex); + }); + + it("enum", () => { + const complex = { + tag: "Enum", + values: [{ tag: "First", values: undefined }], + } as const; + roundtrip("complex", complex); + }); + + it("asset", () => { + const complex = { tag: "Asset", values: [addr, 1n] } as const; + roundtrip("complex", complex); + }); + + it("void", () => { + const complex = { tag: "Void", values: undefined } as const; + roundtrip("complex", complex); + }); + }); + + it("addresse", () => { + roundtrip("addresse", addr); + }); + + it("bytes", () => { + const bytes = Buffer.from("hello"); + roundtrip("bytes", bytes); + }); + + it("bytes_n", () => { + const bytes_n = Buffer.from("123456789"); // what's the correct way to construct bytes_n? + roundtrip("bytes_n", bytes_n); + }); + + it("card", () => { + const card = 11; + roundtrip("card", card); + }); + + it("boolean", () => { + roundtrip("boolean", true); + }); + + it("not", () => { + roundtrip("boolean", false); + }); + + it("i128", () => { + roundtrip("i128", -1n); + }); + + it("u128", () => { + roundtrip("u128", 1n); + }); + + it("map", () => { + const map = new Map(); + map.set(1, true); + map.set(2, false); + roundtrip("map", map); + + map.set(3, "hahaha") + expect(() => roundtrip("map", map)).to.throw(/invalid type scSpecTypeBool specified for string value/i); + }); + + it("vec", () => { + const vec = [1, 2, 3]; + roundtrip("vec", vec); + }); + + it("tuple", () => { + const tuple = ["hello", 1] as const; + roundtrip("tuple", tuple); + }); + + it("option", () => { + roundtrip("option", 1); + roundtrip("option", undefined); + }); + + it("u256", () => { + roundtrip("u256", 1n); + expect(() =>roundtrip("u256", -1n)).to.throw(/expected a positive value, got: -1/i) + }); + + it("i256", () => { + roundtrip("i256", -1n); + }); + + it("string", () => { + roundtrip("string", "hello"); + }); + + it("tuple_strukt", () => { + const arg = [ + { a: 0, b: true, c: "hello" }, + { tag: "First", values: undefined }, + ] as const; + + roundtrip("tuple_strukt", arg); + }); +}); + +describe("parsing and building ScVals", function () { + it("Can parse entries", function () { + let spec = new ContractSpec([GIGA_MAP, func]); + let fn = spec.findEntry("giga_map"); + let gigaMap = spec.findEntry("GigaMap"); + expect(gigaMap).deep.equal(GIGA_MAP); + expect(fn).deep.equal(func); + }); +}); + +export const GIGA_MAP = xdr.ScSpecEntry.scSpecEntryUdtStructV0( + new xdr.ScSpecUdtStructV0({ + doc: "This is a kitchen sink of all the types", + lib: "", + name: "GigaMap", + fields: [ + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "bool", + type: xdr.ScSpecTypeDef.scSpecTypeBool(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "i128", + type: xdr.ScSpecTypeDef.scSpecTypeI128(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "u128", + type: xdr.ScSpecTypeDef.scSpecTypeU128(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "i256", + type: xdr.ScSpecTypeDef.scSpecTypeI256(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "u256", + type: xdr.ScSpecTypeDef.scSpecTypeU256(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "i32", + type: xdr.ScSpecTypeDef.scSpecTypeI32(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "u32", + type: xdr.ScSpecTypeDef.scSpecTypeU32(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "i64", + type: xdr.ScSpecTypeDef.scSpecTypeI64(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "u64", + type: xdr.ScSpecTypeDef.scSpecTypeU64(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "symbol", + type: xdr.ScSpecTypeDef.scSpecTypeSymbol(), + }), + new xdr.ScSpecUdtStructFieldV0({ + doc: "", + name: "string", + type: xdr.ScSpecTypeDef.scSpecTypeString(), + }), + ], + }) +); +const GIGA_MAP_TYPE = xdr.ScSpecTypeDef.scSpecTypeUdt( + new xdr.ScSpecTypeUdt({ name: "GigaMap" }) +); + +let func = xdr.ScSpecEntry.scSpecEntryFunctionV0( + new xdr.ScSpecFunctionV0({ + doc: "Kitchen Sink", + name: "giga_map", + inputs: [ + new xdr.ScSpecFunctionInputV0({ + doc: "", + name: "giga_map", + type: GIGA_MAP_TYPE, + }), + ], + outputs: [GIGA_MAP_TYPE], + }) +); diff --git a/test/unit/federation_server_test.js b/test/unit/federation_server_test.js index b8d97181f..743e5bb13 100644 --- a/test/unit/federation_server_test.js +++ b/test/unit/federation_server_test.js @@ -1,11 +1,10 @@ const http = require("http"); +const { Server, FEDERATION_RESPONSE_MAX_SIZE } = StellarSdk.Federation; + describe("federation-server.js tests", function () { beforeEach(function () { - this.server = new StellarSdk.FederationServer( - "https://acme.com:1337/federation", - "stellar.org", - ); + this.server = new Server("https://acme.com:1337/federation", "stellar.org"); this.axiosMock = sinon.mock(axios); StellarSdk.Config.setDefault(); @@ -18,22 +17,16 @@ describe("federation-server.js tests", function () { describe("FederationServer.constructor", function () { it("throws error for insecure server", function () { expect( - () => - new StellarSdk.FederationServer( - "http://acme.com:1337/federation", - "stellar.org", - ), + () => new Server("http://acme.com:1337/federation", "stellar.org"), ).to.throw(/Cannot connect to insecure federation server/); }); it("allow insecure server when opts.allowHttp flag is set", function () { expect( () => - new StellarSdk.FederationServer( - "http://acme.com:1337/federation", - "stellar.org", - { allowHttp: true }, - ), + new Server("http://acme.com:1337/federation", "stellar.org", { + allowHttp: true, + }), ).to.not.throw(); }); @@ -41,11 +34,9 @@ describe("federation-server.js tests", function () { StellarSdk.Config.setAllowHttp(true); expect( () => - new StellarSdk.FederationServer( - "http://acme.com:1337/federation", - "stellar.org", - { allowHttp: true }, - ), + new Server("http://acme.com:1337/federation", "stellar.org", { + allowHttp: true, + }), ).to.not.throw(); }); }); @@ -192,17 +183,13 @@ FEDERATION_SERVER="https://api.stellar.org/federation" }), ); - StellarSdk.FederationServer.createForDomain("acme.com").then( - (federationServer) => { - expect(federationServer.serverURL.protocol()).equals("https"); - expect(federationServer.serverURL.hostname()).equals( - "api.stellar.org", - ); - expect(federationServer.serverURL.path()).equals("/federation"); - expect(federationServer.domain).equals("acme.com"); - done(); - }, - ); + Server.createForDomain("acme.com").then((federationServer) => { + expect(federationServer.serverURL.protocol()).equals("https"); + expect(federationServer.serverURL.hostname()).equals("api.stellar.org"); + expect(federationServer.serverURL.path()).equals("/federation"); + expect(federationServer.domain).equals("acme.com"); + done(); + }); }); it("fails when stellar.toml does not contain federation server info", function (done) { @@ -215,7 +202,7 @@ FEDERATION_SERVER="https://api.stellar.org/federation" }), ); - StellarSdk.FederationServer.createForDomain("acme.com") + Server.createForDomain("acme.com") .should.be.rejectedWith( /stellar.toml does not contain FEDERATION_SERVER field/, ) @@ -225,9 +212,7 @@ FEDERATION_SERVER="https://api.stellar.org/federation" describe("FederationServer.resolve", function () { it("succeeds for a valid account ID", function (done) { - StellarSdk.FederationServer.resolve( - "GAFSZ3VPBC2H2DVKCEWLN3PQWZW6BVDMFROWJUDAJ3KWSOKQIJ4R5W4J", - ) + Server.resolve("GAFSZ3VPBC2H2DVKCEWLN3PQWZW6BVDMFROWJUDAJ3KWSOKQIJ4R5W4J") .should.eventually.deep.equal({ account_id: "GAFSZ3VPBC2H2DVKCEWLN3PQWZW6BVDMFROWJUDAJ3KWSOKQIJ4R5W4J", @@ -236,7 +221,7 @@ FEDERATION_SERVER="https://api.stellar.org/federation" }); it("fails for invalid account ID", function (done) { - StellarSdk.FederationServer.resolve("invalid") + Server.resolve("invalid") .should.be.rejectedWith(/Invalid Account ID/) .notify(done); }); @@ -274,7 +259,7 @@ FEDERATION_SERVER="https://api.stellar.org/federation" }), ); - StellarSdk.FederationServer.resolve("bob*stellar.org") + Server.resolve("bob*stellar.org") .should.eventually.deep.equal({ stellar_address: "bob*stellar.org", account_id: @@ -286,7 +271,7 @@ FEDERATION_SERVER="https://api.stellar.org/federation" }); it("fails for invalid Stellar address", function (done) { - StellarSdk.FederationServer.resolve("bob*stellar.org*test") + Server.resolve("bob*stellar.org*test") .should.be.rejectedWith(/Invalid Stellar address/) .notify(done); }); @@ -322,20 +307,16 @@ FEDERATION_SERVER="https://api.stellar.org/federation" if (typeof window != "undefined") { return done(); } - var response = Array(StellarSdk.FEDERATION_RESPONSE_MAX_SIZE + 10).join( - "a", - ); + var response = Array(FEDERATION_RESPONSE_MAX_SIZE + 10).join("a"); let tempServer = http .createServer((req, res) => { res.setHeader("Content-Type", "application/json; charset=UTF-8"); res.end(response); }) .listen(4444, () => { - new StellarSdk.FederationServer( - "http://localhost:4444/federation", - "stellar.org", - { allowHttp: true }, - ) + new Server("http://localhost:4444/federation", "stellar.org", { + allowHttp: true, + }) .resolveAddress("bob*stellar.org") .should.be.rejectedWith( /federation response exceeds allowed size of [0-9]+/, @@ -373,11 +354,7 @@ FEDERATION_SERVER="https://api.stellar.org/federation" setTimeout(() => {}, 10000); }) .listen(4444, () => { - new StellarSdk.FederationServer( - "http://localhost:4444/federation", - "stellar.org", - opts, - ) + new Server("http://localhost:4444/federation", "stellar.org", opts) .resolveAddress("bob*stellar.org") .should.be.rejectedWith(/timeout of 1000ms exceeded/) .notify(done) @@ -395,11 +372,7 @@ FEDERATION_SERVER="https://api.stellar.org/federation" setTimeout(() => {}, 10000); }) .listen(4444, () => { - new StellarSdk.FederationServer( - "http://localhost:4444/federation", - "stellar.org", - opts, - ) + new Server("http://localhost:4444/federation", "stellar.org", opts) .resolveAccountId( "GB5XVAABEQMY63WTHDQ5RXADGYF345VWMNPTN2GFUDZT57D57ZQTJ7PS", ) @@ -419,11 +392,7 @@ FEDERATION_SERVER="https://api.stellar.org/federation" setTimeout(() => {}, 10000); }) .listen(4444, () => { - new StellarSdk.FederationServer( - "http://localhost:4444/federation", - "stellar.org", - opts, - ) + new Server("http://localhost:4444/federation", "stellar.org", opts) .resolveTransactionId( "3389e9f0f1a65f19736cacf544c2e825313e8447f569233bb8db39aa607c8889", ) @@ -443,7 +412,7 @@ FEDERATION_SERVER="https://api.stellar.org/federation" setTimeout(() => {}, 10000); }) .listen(4444, () => { - StellarSdk.FederationServer.createForDomain("localhost:4444", opts) + Server.createForDomain("localhost:4444", opts) .should.be.rejectedWith(/timeout of 1000ms exceeded/) .notify(done) .then(() => tempServer.close()); @@ -461,7 +430,7 @@ FEDERATION_SERVER="https://api.stellar.org/federation" setTimeout(() => {}, 10000); }) .listen(4444, () => { - StellarSdk.FederationServer.resolve("bob*localhost:4444", opts) + Server.resolve("bob*localhost:4444", opts) .should.eventually.be.rejectedWith(/timeout of 1000ms exceeded/) .notify(done) .then(() => tempServer.close()); diff --git a/test/unit/horizon_axios_client_test.js b/test/unit/horizon_axios_client_test.js index a56e8622b..d1a684ab1 100644 --- a/test/unit/horizon_axios_client_test.js +++ b/test/unit/horizon_axios_client_test.js @@ -1,5 +1,4 @@ -const SERVER_TIME_MAP = StellarSdk.SERVER_TIME_MAP; -const getCurrentServerTime = StellarSdk.getCurrentServerTime; +const { SERVER_TIME_MAP, getCurrentServerTime } = StellarSdk.Horizon; describe("getCurrentServerTime", () => { let clock; diff --git a/test/unit/horizon_path_test.js b/test/unit/horizon_path_test.js index 2b62adfdc..f4aec0dfa 100644 --- a/test/unit/horizon_path_test.js +++ b/test/unit/horizon_path_test.js @@ -1,6 +1,8 @@ +const { Horizon } = StellarSdk; + describe("horizon path tests", function () { beforeEach(function () { - this.axiosMock = sinon.mock(HorizonAxiosClient); + this.axiosMock = sinon.mock(Horizon.AxiosClient); StellarSdk.Config.setDefault(); }); @@ -10,7 +12,7 @@ describe("horizon path tests", function () { }); function test_horizon_paths(serverUrl) { - let server = new StellarSdk.Server(serverUrl); + let server = new Horizon.Server(serverUrl); let randomResult = { data: { diff --git a/test/unit/liquidity_pool_endpoints_test.js b/test/unit/liquidity_pool_endpoints_test.js index 92ab3b196..c6f827b3d 100644 --- a/test/unit/liquidity_pool_endpoints_test.js +++ b/test/unit/liquidity_pool_endpoints_test.js @@ -1,15 +1,12 @@ -// Helper function to deep-copy JSON responses. -function copyJson(js) { - return JSON.parse(JSON.stringify(js)); -} +const { Horizon } = StellarSdk; const BASE_URL = "https://horizon-live.stellar.org:1337"; const LP_URL = BASE_URL + "/liquidity_pools"; describe("/liquidity_pools tests", function () { beforeEach(function () { - this.server = new StellarSdk.Server(BASE_URL); - this.axiosMock = sinon.mock(HorizonAxiosClient); + this.server = new Horizon.Server(BASE_URL); + this.axiosMock = sinon.mock(Horizon.AxiosClient); StellarSdk.Config.setDefault(); }); @@ -1376,3 +1373,8 @@ describe("/liquidity_pools tests", function () { }); }); }); + +// Helper function to deep-copy JSON responses. +function copyJson(js) { + return JSON.parse(JSON.stringify(js)); +} diff --git a/test/unit/server/claimable_balances.js b/test/unit/server/horizon/claimable_balances.js similarity index 96% rename from test/unit/server/claimable_balances.js rename to test/unit/server/horizon/claimable_balances.js index d25680b13..c94bf86a3 100644 --- a/test/unit/server/claimable_balances.js +++ b/test/unit/server/horizon/claimable_balances.js @@ -1,11 +1,9 @@ -const MockAdapter = require("axios-mock-adapter"); +const { Horizon } = StellarSdk; describe("ClaimableBalanceCallBuilder", function () { beforeEach(function () { - this.server = new StellarSdk.Server( - "https://horizon-live.stellar.org:1337", - ); - this.axiosMock = sinon.mock(HorizonAxiosClient); + this.server = new Horizon.Server("https://horizon-live.stellar.org:1337"); + this.axiosMock = sinon.mock(Horizon.AxiosClient); StellarSdk.Config.setDefault(); }); diff --git a/test/unit/server/join_test.js b/test/unit/server/horizon/join_test.js similarity index 98% rename from test/unit/server/join_test.js rename to test/unit/server/horizon/join_test.js index 39853a83d..523b73ca4 100644 --- a/test/unit/server/join_test.js +++ b/test/unit/server/horizon/join_test.js @@ -1,11 +1,9 @@ -const MockAdapter = require("axios-mock-adapter"); +const { Horizon } = StellarSdk; describe("Server - CallBuilder#join", function () { beforeEach(function () { - this.server = new StellarSdk.Server( - "https://horizon-live.stellar.org:1337", - ); - this.axiosMock = sinon.mock(HorizonAxiosClient); + this.server = new Horizon.Server("https://horizon-live.stellar.org:1337"); + this.axiosMock = sinon.mock(Horizon.AxiosClient); }); afterEach(function () { diff --git a/test/unit/server_test.js b/test/unit/server/horizon/server_test.js similarity index 99% rename from test/unit/server_test.js rename to test/unit/server/horizon/server_test.js index 0d1ff3b44..6d1e7f190 100644 --- a/test/unit/server_test.js +++ b/test/unit/server/horizon/server_test.js @@ -1,11 +1,10 @@ +const { Horizon } = StellarSdk; const MockAdapter = require("axios-mock-adapter"); describe("server.js non-transaction tests", function () { beforeEach(function () { - this.server = new StellarSdk.Server( - "https://horizon-live.stellar.org:1337", - ); - this.axiosMock = sinon.mock(HorizonAxiosClient); + this.server = new Horizon.Server("https://horizon-live.stellar.org:1337"); + this.axiosMock = sinon.mock(Horizon.AxiosClient); StellarSdk.Config.setDefault(); }); @@ -17,14 +16,14 @@ describe("server.js non-transaction tests", function () { describe("Server.constructor", function () { it("throws error for insecure server", function () { expect( - () => new StellarSdk.Server("http://horizon-live.stellar.org:1337"), + () => new Horizon.Server("http://horizon-live.stellar.org:1337"), ).to.throw(/Cannot connect to insecure horizon server/); }); it("allow insecure server when opts.allowHttp flag is set", function () { expect( () => - new StellarSdk.Server("http://horizon-live.stellar.org:1337", { + new Horizon.Server("http://horizon-live.stellar.org:1337", { allowHttp: true, }), ).to.not.throw(); @@ -33,7 +32,7 @@ describe("server.js non-transaction tests", function () { it("allow insecure server when global Config.allowHttp flag is set", function () { StellarSdk.Config.setAllowHttp(true); expect( - () => new StellarSdk.Server("http://horizon-live.stellar.org:1337"), + () => new Horizon.Server("http://horizon-live.stellar.org:1337"), ).to.not.throw(); }); }); @@ -47,7 +46,7 @@ describe("server.js non-transaction tests", function () { // use MockAdapter instead of this.axiosMock // because we don't want to replace the get function // we need to use axios's one so interceptors run!! - this.axiosMockAdapter = new MockAdapter(HorizonAxiosClient); + this.axiosMockAdapter = new MockAdapter(Horizon.AxiosClient); }); afterEach(function () { diff --git a/test/unit/server/soroban/constructor_test.js b/test/unit/server/soroban/constructor_test.js new file mode 100644 index 000000000..82c4465e1 --- /dev/null +++ b/test/unit/server/soroban/constructor_test.js @@ -0,0 +1,27 @@ +const { Server, AxiosClient } = StellarSdk.SorobanRpc; + +describe("Server.constructor", function () { + beforeEach(function () { + this.server = new Server(serverUrl); + this.axiosMock = sinon.mock(AxiosClient); + }); + + afterEach(function () { + this.axiosMock.verify(); + this.axiosMock.restore(); + }); + + let insecureServerUrl = serverUrl.replace("https://", "http://"); + + it("throws error for insecure server", function () { + expect(() => new Server(insecureServerUrl)).to.throw( + /Cannot connect to insecure Soroban RPC server/i, + ); + }); + + it("allow insecure server when opts.allowHttp flag is set", function () { + expect( + () => new Server(insecureServerUrl, { allowHttp: true }), + ).to.not.throw(); + }); +}); diff --git a/test/unit/server/soroban/get_account_test.js b/test/unit/server/soroban/get_account_test.js new file mode 100644 index 000000000..a84ca641d --- /dev/null +++ b/test/unit/server/soroban/get_account_test.js @@ -0,0 +1,97 @@ +const { Account, Keypair, StrKey, hash, xdr } = StellarSdk; +const { Server, AxiosClient } = StellarSdk.SorobanRpc; + +describe("Server#getAccount", function () { + beforeEach(function () { + this.server = new Server(serverUrl); + this.axiosMock = sinon.mock(AxiosClient); + }); + + afterEach(function () { + this.axiosMock.verify(); + this.axiosMock.restore(); + }); + + const address = "GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI"; + const accountId = Keypair.fromPublicKey(address).xdrPublicKey(); + const key = xdr.LedgerKey.account(new xdr.LedgerKeyAccount({ accountId })); + const accountEntry = + "AAAAAAAAAABzdv3ojkzWHMD7KUoXhrPx0GH18vHKV0ZfqpMiEblG1g3gtpoE608YAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAQAAAAAY9D8iA"; + const ledgerExpirationKey = xdr.LedgerKey.expiration( + new xdr.LedgerKeyExpiration({ keyHash: hash(key.toXDR()) }), + ); + + it("requests the correct method", function (done) { + this.axiosMock + .expects("post") + .withArgs(serverUrl, { + jsonrpc: "2.0", + id: 1, + method: "getLedgerEntries", + params: [[key.toXDR("base64"), ledgerExpirationKey.toXDR("base64")]], + }) + .returns( + Promise.resolve({ + data: { + result: { + latestLedger: 0, + entries: [ + { + key: key.toXDR("base64"), + xdr: accountEntry, + }, + ], + }, + }, + }), + ); + + const expected = new Account(address, "1"); + this.server + .getAccount(address) + .then(function (response) { + expect(response).to.be.deep.equal(expected); + done(); + }) + .catch(done); + }); + + it("throws a useful error when the account is not found", function (done) { + const address = "GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI"; + const accountId = xdr.PublicKey.publicKeyTypeEd25519( + StrKey.decodeEd25519PublicKey(address), + ); + + this.axiosMock + .expects("post") + .withArgs(serverUrl, { + jsonrpc: "2.0", + id: 1, + method: "getLedgerEntries", + params: [[key.toXDR("base64"), ledgerExpirationKey.toXDR("base64")]], + }) + .returns( + Promise.resolve({ + data: { + result: { + latestLedger: 0, + entries: null, + }, + }, + }), + ); + + this.server + .getAccount(address) + .then(function (_) { + done(new Error("Expected error to be thrown")); + }) + .catch(function (err) { + done( + err.message === `Account not found: ${address}` + ? null + : new Error(`Received unexpected error: ${err.message}`), + ); + }); + }); +}); diff --git a/test/unit/server/soroban/get_contract_data_test.js b/test/unit/server/soroban/get_contract_data_test.js new file mode 100644 index 000000000..ef16a089d --- /dev/null +++ b/test/unit/server/soroban/get_contract_data_test.js @@ -0,0 +1,194 @@ +const { xdr, nativeToScVal, hash } = StellarSdk; +const { Server, AxiosClient, Durability } = StellarSdk.SorobanRpc; + +describe("Server#getContractData", function () { + beforeEach(function () { + this.server = new Server(serverUrl); + this.axiosMock = sinon.mock(AxiosClient); + }); + + afterEach(function () { + this.axiosMock.verify(); + this.axiosMock.restore(); + }); + + const address = "CCJZ5DGASBWQXR5MPFCJXMBI333XE5U3FSJTNQU7RIKE3P5GN2K2WYD5"; + const key = nativeToScVal(["Admin"]); + + const ledgerEntry = xdr.LedgerEntryData.contractData( + new xdr.ContractDataEntry({ + ext: new xdr.ExtensionPoint(0), + contract: new StellarSdk.Address(address).toScAddress(), + durability: xdr.ContractDataDurability.persistent(), + key, + val: key, // lazy + }), + ); + + // the key is a subset of the val + const ledgerKey = xdr.LedgerKey.contractData( + new xdr.LedgerKeyContractData({ + contract: ledgerEntry.contractData().contract(), + durability: ledgerEntry.contractData().durability(), + key: ledgerEntry.contractData().key(), + }), + ); + + const ledgerExpirationKey = xdr.LedgerKey.expiration( + new xdr.LedgerKeyExpiration({ keyHash: hash(ledgerKey.toXDR()) }), + ); + + const ledgerExpirationEntry = xdr.LedgerEntryData.expiration( + new xdr.ExpirationEntry({ + keyHash: hash(ledgerKey.toXDR()), + expirationLedgerSeq: 1000, + }), + ); + + it("contract data key found", function (done) { + let result = { + lastModifiedLedgerSeq: 1, + key: ledgerKey, + val: ledgerEntry, + expirationLedgerSeq: 1000, + }; + + this.axiosMock + .expects("post") + .withArgs(serverUrl, { + jsonrpc: "2.0", + id: 1, + method: "getLedgerEntries", + params: [ + [ledgerKey.toXDR("base64"), ledgerExpirationKey.toXDR("base64")], + ], + }) + .returns( + Promise.resolve({ + data: { + result: { + latestLedger: 420, + entries: [ + { + lastModifiedLedgerSeq: result.lastModifiedLedgerSeq, + key: ledgerKey.toXDR("base64"), + xdr: ledgerEntry.toXDR("base64"), + }, + { + lastModifiedLedgerSeq: result.lastModifiedLedgerSeq, + key: ledgerExpirationKey.toXDR("base64"), + xdr: ledgerExpirationEntry.toXDR("base64"), + }, + ], + }, + }, + }), + ); + + this.server + .getContractData(address, key, Durability.Persistent) + .then(function (response) { + expect(response.key.toXDR("base64")).to.eql(result.key.toXDR("base64")); + expect(response.val.toXDR("base64")).to.eql(result.val.toXDR("base64")); + expect(response.expirationLedgerSeq).to.eql(1000); + done(); + }) + .catch((err) => done(err)); + }); + + it("expiration entry not present for contract data key in server response", function (done) { + let result = { + lastModifiedLedgerSeq: 1, + key: ledgerKey, + val: ledgerEntry, + }; + + this.axiosMock + .expects("post") + .withArgs(serverUrl, { + jsonrpc: "2.0", + id: 1, + method: "getLedgerEntries", + params: [ + [ledgerKey.toXDR("base64"), ledgerExpirationKey.toXDR("base64")], + ], + }) + .returns( + Promise.resolve({ + data: { + result: { + latestLedger: 420, + entries: [ + { + lastModifiedLedgerSeq: result.lastModifiedLedgerSeq, + key: result.key.toXDR("base64"), + xdr: result.val.toXDR("base64"), + }, + ], + }, + }, + }), + ); + + this.server + .getContractData(address, key, Durability.Persistent) + .then(function (response) { + expect(response.key.toXDR("base64")).to.eql(result.key.toXDR("base64")); + expect(response.val.toXDR("base64")).to.eql(result.val.toXDR("base64")); + expect(response.expirationLedgerSeq).to.be.undefined; + done(); + }) + .catch((err) => done(err)); + }); + + it("contract data key not found", function (done) { + // clone and change durability to test this case + const ledgerKeyDupe = xdr.LedgerKey.fromXDR(ledgerKey.toXDR()); + ledgerKeyDupe + .contractData() + .durability(xdr.ContractDataDurability.temporary()); + + const ledgerExpirationKeyDupe = xdr.LedgerKey.expiration( + new xdr.LedgerKeyExpiration({ keyHash: hash(ledgerKeyDupe.toXDR()) }), + ); + + this.axiosMock + .expects("post") + .withArgs(serverUrl, { + jsonrpc: "2.0", + id: 1, + method: "getLedgerEntries", + params: [ + [ + ledgerKeyDupe.toXDR("base64"), + ledgerExpirationKeyDupe.toXDR("base64"), + ], + ], + }) + .returns(Promise.resolve({ data: { result: { entries: [] } } })); + + this.server + .getContractData(address, key, Durability.Temporary) + .then(function (_response) { + done(new Error("Expected error")); + }) + .catch(function (err) { + done( + err.code == 404 + ? null + : new Error("Expected error code 404, got: " + err.code), + ); + }); + }); + + it("fails on hex address (was deprecated now unsupported)", function (done) { + let hexAddress = "0".repeat(63) + "1"; + this.server + .getContractData(hexAddress, key, Durability.Persistent) + .then((reply) => done(new Error(`should fail, got: ${reply}`))) + .catch((error) => { + expect(error).to.contain(/unsupported contract id/i); + done(); + }); + }); +}); diff --git a/test/unit/server/soroban/get_events_test.js b/test/unit/server/soroban/get_events_test.js new file mode 100644 index 000000000..57c6a5241 --- /dev/null +++ b/test/unit/server/soroban/get_events_test.js @@ -0,0 +1,285 @@ +const { SorobanRpc } = StellarSdk; +const { Server, AxiosClient } = StellarSdk.SorobanRpc; + +describe("Server#getEvents", function () { + beforeEach(function () { + this.server = new Server(serverUrl); + this.axiosMock = sinon.mock(AxiosClient); + }); + + afterEach(function () { + this.axiosMock.verify(); + this.axiosMock.restore(); + }); + + it("requests the correct endpoint", function (done) { + let result = { latestLedger: 0, events: [] }; + setupMock( + this.axiosMock, + { + filters: [], + pagination: {}, + startLedger: "1", + }, + result, + ); + + this.server + .getEvents({ startLedger: 1 }) + .then(function (response) { + expect(response).to.be.deep.equal(result); + done(); + }) + .catch(function (err) { + done(err); + }); + }); + + it("can build wildcard filters", function (done) { + let result = { + latestLedger: 1, + events: filterEvents(getEventsResponseFixture, "*/*"), + }; + + setupMock( + this.axiosMock, + { + startLedger: "1", + filters: [ + { + topics: [["*", "*"]], + }, + ], + pagination: {}, + }, + result, + ); + + this.server + .getEvents({ + startLedger: 1, + filters: [ + { + topics: [["*", "*"]], + }, + ], + }) + .then(function (response) { + expect(response).to.be.deep.equal(parseEvents(result)); + done(); + }) + .catch(done); + }); + + it("can build matching filters", function (done) { + let result = { + latestLedger: 1, + events: filterEvents( + getEventsResponseFixture, + "AAAABQAAAAh0cmFuc2Zlcg==/AAAAAQB6Mcc=", + ), + }; + + setupMock( + this.axiosMock, + { + startLedger: "1", + filters: [ + { + topics: [["AAAABQAAAAh0cmFuc2Zlcg==", "AAAAAQB6Mcc="]], + }, + ], + pagination: {}, + }, + result, + ); + + this.server + .getEvents({ + startLedger: 1, + filters: [ + { + topics: [["AAAABQAAAAh0cmFuc2Zlcg==", "AAAAAQB6Mcc="]], + }, + ], + }) + .then(function (response) { + expect(response).to.be.deep.equal(result); + done(); + }) + .catch(done); + }); + + it("can build mixed filters", function (done) { + let result = { + latestLedger: 1, + events: filterEventsByLedger( + filterEvents(getEventsResponseFixture, "AAAABQAAAAh0cmFuc2Zlcg==/*"), + 1, + ), + }; + + setupMock( + this.axiosMock, + { + startLedger: "1", + filters: [ + { + topics: [["AAAABQAAAAh0cmFuc2Zlcg==", "*"]], + }, + ], + pagination: {}, + }, + result, + ); + + this.server + .getEvents({ + startLedger: 1, + filters: [ + { + topics: [["AAAABQAAAAh0cmFuc2Zlcg==", "*"]], + }, + ], + }) + .then(function (response) { + expect(response).to.be.deep.equal(result); + done(); + }) + .catch(done); + }); + + it("can paginate", function (done) { + let result = { + latestLedger: 1, + events: filterEventsByLedger( + filterEvents(getEventsResponseFixture, "*/*"), + 1, + ), + }; + + setupMock( + this.axiosMock, + { + filters: [ + { + topics: [["*", "*"]], + }, + ], + pagination: { + limit: 10, + cursor: "0164090849041387521-0000000000", + }, + }, + result, + ); + + this.server + .getEvents({ + filters: [ + { + topics: [["*", "*"]], + }, + ], + cursor: "0164090849041387521-0000000000", + limit: 10, + }) + .then(function (response) { + expect(response).to.be.deep.equal(result); + done(); + }) + .catch(done); + }); +}); + +function filterEvents(events, filter) { + return events.filter( + (e, i) => + e.topic.length == filter.length && + e.topic.every((s, j) => s === filter[j] || s === "*"), + ); +} + +function filterEventsByLedger(events, start) { + return events.filter((e) => { + return e.ledger.parseInt() >= start; + }); +} + +function setupMock(axiosMock, params, result) { + axiosMock + .expects("post") + .withArgs(serverUrl, { + jsonrpc: "2.0", + id: 1, + method: "getEvents", + params: params, + }) + .returns(Promise.resolve({ data: { result } })); +} + +function parseEvents(result) { + return { + ...result, + events: result.events.map(SorobanRpc.parseRawEvents), + }; +} + +let getEventsResponseFixture = [ + { + type: "system", + ledger: "1", + ledgerClosedAt: "2022-11-16T16:10:41Z", + contractId: + "e3e82a76cc316f6289fd1ffbdf315da0f2c6be9582b84b9983a402f02ea0fff7", + id: "0164090849041387521-0000000003", + pagingToken: "164090849041387521-3", + topic: ["AAAABQAAAAh0cmFuc2Zlcg==", "AAAAAQB6Mcc="], + inSuccessfulContractCall: true, + value: { + xdr: "AAAABQAAAApHaWJNb255UGxzAAA=", + }, + }, + { + type: "contract", + ledger: "2", + ledgerClosedAt: "2022-11-16T16:10:41Z", + contractId: + "e3e82a76cc316f6289fd1ffbdf315da0f2c6be9582b84b9983a402f02ea0fff7", + id: "0164090849041387521-0000000003", + pagingToken: "164090849041387521-3", + topic: ["AAAAAQB6Mcc=", "AAAABQAAAAh0cmFuc2Zlcg=="], + inSuccessfulContractCall: true, + value: { + xdr: "AAAABQAAAApHaWJNb255UGxzAAA=", + }, + }, + { + type: "diagnostic", + ledger: "2", + ledgerClosedAt: "2022-11-16T16:10:41Z", + contractId: + "a3e82a76cc316f6289fd1ffbdf315da0f2c6be9582b84b9983a402f02ea0fff7", + id: "0164090849041387521-0000000003", + pagingToken: "164090849041387521-3", + inSuccessfulContractCall: true, + topic: ["AAAAAQB6Mcc="], + value: { + xdr: "AAAABQAAAApHaWJNb255UGxzAAA=", + }, + }, + { + type: "contract", + ledger: "3", + ledgerClosedAt: "2022-12-14T01:01:20Z", + contractId: + "6ebe0114ae15f72f187f05d06dcb66b22bd97218755c9b4646b034ab961fc1d5", + id: "0000000171798695936-0000000001", + pagingToken: "0000000171798695936-0000000001", + inSuccessfulContractCall: true, + topic: ["AAAABQAAAAdDT1VOVEVSAA==", "AAAABQAAAAlpbmNyZW1lbnQAAAA="], + value: { + xdr: "AAAAAQAAAAE=", + }, + }, +]; diff --git a/test/unit/server/soroban/get_health_test.js b/test/unit/server/soroban/get_health_test.js new file mode 100644 index 000000000..917a945f6 --- /dev/null +++ b/test/unit/server/soroban/get_health_test.js @@ -0,0 +1,39 @@ +const { Server, AxiosClient } = StellarSdk.SorobanRpc; + +describe("Server#getHealth", function () { + beforeEach(function () { + this.server = new Server(serverUrl); + this.axiosMock = sinon.mock(AxiosClient); + }); + + afterEach(function () { + this.axiosMock.verify(); + this.axiosMock.restore(); + }); + + it("requests the correct endpoint", function (done) { + let result = { + status: "healthy", + }; + + this.axiosMock + .expects("post") + .withArgs(serverUrl, { + jsonrpc: "2.0", + id: 1, + method: "getHealth", + params: null, + }) + .returns(Promise.resolve({ data: { result } })); + + this.server + .getHealth() + .then(function (response) { + expect(response).to.be.deep.equal(result); + done(); + }) + .catch(function (err) { + done(err); + }); + }); +}); diff --git a/test/unit/server/soroban/get_latest_ledger_test.js b/test/unit/server/soroban/get_latest_ledger_test.js new file mode 100644 index 000000000..6aa048033 --- /dev/null +++ b/test/unit/server/soroban/get_latest_ledger_test.js @@ -0,0 +1,40 @@ +const { Server, AxiosClient } = StellarSdk.SorobanRpc; + +describe("Server#getLatestLedger", function () { + beforeEach(function () { + this.server = new Server(serverUrl); + this.axiosMock = sinon.mock(AxiosClient); + }); + + afterEach(function () { + this.axiosMock.verify(); + this.axiosMock.restore(); + }); + + it("requests the correct method", function (done) { + const result = { + id: "hashed_id", + sequence: 123, + protocolVersion: 20, + }; + this.axiosMock + .expects("post") + .withArgs(serverUrl, { + jsonrpc: "2.0", + id: 1, + method: "getLatestLedger", + params: null, + }) + .returns(Promise.resolve({ data: { result } })); + + this.server + .getLatestLedger() + .then(function (response) { + expect(response).to.be.deep.equal(result); + done(); + }) + .catch(function (err) { + done(err); + }); + }); +}); diff --git a/test/unit/server/soroban/get_ledger_entries_test.js b/test/unit/server/soroban/get_ledger_entries_test.js new file mode 100644 index 000000000..093073efa --- /dev/null +++ b/test/unit/server/soroban/get_ledger_entries_test.js @@ -0,0 +1,213 @@ +const { xdr, nativeToScVal, Durability, hash } = StellarSdk; +const { Server, AxiosClient } = StellarSdk.SorobanRpc; + +describe("Server#getLedgerEntries", function () { + const address = "CCJZ5DGASBWQXR5MPFCJXMBI333XE5U3FSJTNQU7RIKE3P5GN2K2WYD5"; + const key = nativeToScVal(["test"]); + const ledgerEntry = xdr.LedgerEntryData.contractData( + new xdr.ContractDataEntry({ + ext: new xdr.ExtensionPoint(0), + contract: new StellarSdk.Address(address).toScAddress(), + durability: xdr.ContractDataDurability.persistent(), + key, + val: key, + }), + ); + const ledgerKey = xdr.LedgerKey.contractData( + new xdr.LedgerKeyContractData({ + contract: ledgerEntry.contractData().contract(), + durability: ledgerEntry.contractData().durability(), + key: ledgerEntry.contractData().key(), + }), + ); + const ledgerExpirationKey = xdr.LedgerKey.expiration( + new xdr.LedgerKeyExpiration({ keyHash: hash(ledgerKey.toXDR()) }), + ); + const ledgerExpirationEntry = new xdr.ExpirationEntry({ + keyHash: hash(ledgerKey.toXDR()), + expirationLedgerSeq: 1000, + }); + const ledgerExpirationEntryData = xdr.LedgerEntryData.expiration( + ledgerExpirationEntry, + ); + + const ledgerEntryXDR = ledgerEntry.toXDR("base64"); + const ledgerKeyXDR = ledgerKey.toXDR("base64"); + const ledgerExpirationKeyXDR = ledgerExpirationKey.toXDR("base64"); + const ledgerExpirationEntryXDR = ledgerExpirationEntry.toXDR("base64"); + const ledgerExpirationEntryDataXDR = + ledgerExpirationEntryData.toXDR("base64"); + + beforeEach(function () { + this.server = new Server(serverUrl); + this.axiosMock = sinon.mock(AxiosClient); + }); + + afterEach(function () { + this.axiosMock.verify(); + this.axiosMock.restore(); + }); + + function mockRPC(axiosMock, requests, entries) { + axiosMock + .expects("post") + .withArgs(serverUrl, { + jsonrpc: "2.0", + id: 1, + method: "getLedgerEntries", + params: [requests], + }) + .returns( + Promise.resolve({ + data: { + result: { + latestLedger: 420, + entries, + }, + }, + }), + ); + } + + it("ledger entry found, includes expiration meta", function (done) { + mockRPC( + this.axiosMock, + [ledgerKeyXDR, ledgerExpirationKeyXDR], + [ + { + lastModifiedLedgerSeq: 1, + key: ledgerKeyXDR, + xdr: ledgerEntryXDR, + }, + { + lastModifiedLedgerSeq: 2, + key: ledgerExpirationKeyXDR, + xdr: ledgerExpirationEntryDataXDR, + }, + ], + ); + + this.server + .getLedgerEntries(ledgerKey) + .then((response) => { + expect(response.entries).to.have.lengthOf(1); + let result = response.entries[0]; + expect(result.lastModifiedLedgerSeq).to.eql(1); + expect(result.key.toXDR("base64")).to.eql(ledgerKeyXDR); + expect(result.val.toXDR("base64")).to.eql(ledgerEntryXDR); + expect(result.expirationLedgerSeq).to.eql(1000); + done(); + }) + .catch((err) => done(err)); + }); + + it("ledger entry found, no expiration meta included in response", function (done) { + mockRPC( + this.axiosMock, + [ledgerKeyXDR, ledgerExpirationKeyXDR], + [ + { + lastModifiedLedgerSeq: 1, + key: ledgerKeyXDR, + xdr: ledgerEntryXDR, + }, + ], + ); + + this.server + .getLedgerEntries(ledgerKey) + .then((response) => { + expect(response.entries).to.have.lengthOf(1); + let result = response.entries[0]; + expect(result.lastModifiedLedgerSeq).to.eql(1); + expect(result.key.toXDR("base64")).to.eql(ledgerKeyXDR); + expect(result.val.toXDR("base64")).to.eql(ledgerEntryXDR); + expect(result.expirationLedgerSeq).to.be.undefined; + done(); + }) + .catch((err) => done(err)); + }); + + it("ledger entry found, includes expiration meta from any order in response", function (done) { + mockRPC( + this.axiosMock, + [ledgerKeyXDR, ledgerExpirationKeyXDR], + [ + { + lastModifiedLedgerSeq: 2, + key: ledgerExpirationKeyXDR, + xdr: ledgerExpirationEntryDataXDR, + }, + { + lastModifiedLedgerSeq: 1, + key: ledgerKeyXDR, + xdr: ledgerEntryXDR, + }, + ], + ); + + this.server + .getLedgerEntries(ledgerKey) + .then((response) => { + expect(response.entries).to.have.lengthOf(1); + let result = response.entries[0]; + expect(result.lastModifiedLedgerSeq).to.eql(1); + expect(result.key.toXDR("base64")).to.eql(ledgerKeyXDR); + expect(result.val.toXDR("base64")).to.eql(ledgerEntryXDR); + expect(result.expirationLedgerSeq).to.eql(1000); + done(); + }) + .catch((err) => done(err)); + }); + + it("ledger expiration key is requested by caller, no expiration meta needed on response", function (done) { + mockRPC( + this.axiosMock, + [ledgerExpirationKeyXDR], + [ + { + lastModifiedLedgerSeq: 2, + key: ledgerExpirationKeyXDR, + xdr: ledgerExpirationEntryDataXDR, + }, + ], + ); + + this.server + .getLedgerEntries(ledgerExpirationKey) + .then((response) => { + expect(response.entries).to.have.lengthOf(1); + let result = response.entries[0]; + expect(result.lastModifiedLedgerSeq).to.eql(2); + expect(result.key.toXDR("base64")).to.eql(ledgerExpirationKeyXDR); + expect(result.val.toXDR("base64")).to.eql(ledgerExpirationEntryDataXDR); + expect(result.expirationLedgerSeq).to.be.undefined; + done(); + }) + .catch((err) => done(err)); + }); + + it("throws when invalid rpc response", function (done) { + // these are simulating invalid json, missing `xdr` and `key` + mockRPC( + this.axiosMock, + [ledgerKeyXDR, ledgerExpirationKeyXDR], + [ + { + lastModifiedLedgerSeq: 2, + }, + { + lastModifiedLedgerSeq: 1, + }, + ], + ); + + this.server + .getLedgerEntries(ledgerKey) + .then((reply) => done(new Error(`should have failed, got: ${reply}`))) + .catch((error) => { + expect(error).to.contain(/invalid ledger entry/i); + done(); + }); + }); +}); diff --git a/test/unit/server/soroban/get_network_test.js b/test/unit/server/soroban/get_network_test.js new file mode 100644 index 000000000..a5b4e05a7 --- /dev/null +++ b/test/unit/server/soroban/get_network_test.js @@ -0,0 +1,40 @@ +const { Server, AxiosClient } = StellarSdk.SorobanRpc; + +describe("Server#getNetwork", function () { + beforeEach(function () { + this.server = new Server(serverUrl); + this.axiosMock = sinon.mock(AxiosClient); + }); + + afterEach(function () { + this.axiosMock.verify(); + this.axiosMock.restore(); + }); + + it("requests the correct method", function (done) { + const result = { + friendbotUrl: "https://friendbot.stellar.org", + passphrase: "Soroban Testnet ; December 2018", + protocolVersion: 20, + }; + this.axiosMock + .expects("post") + .withArgs(serverUrl, { + jsonrpc: "2.0", + id: 1, + method: "getNetwork", + params: null, + }) + .returns(Promise.resolve({ data: { result } })); + + this.server + .getNetwork() + .then(function (response) { + expect(response).to.be.deep.equal(result); + done(); + }) + .catch(function (err) { + done(err); + }); + }); +}); diff --git a/test/unit/server/soroban/get_transaction_test.js b/test/unit/server/soroban/get_transaction_test.js new file mode 100644 index 000000000..84beac445 --- /dev/null +++ b/test/unit/server/soroban/get_transaction_test.js @@ -0,0 +1,161 @@ +const { + xdr, + Keypair, + Account, + TransactionBuilder, + nativeToScVal, + XdrLargeInt, +} = StellarSdk; +const { Server, AxiosClient } = StellarSdk.SorobanRpc; + +describe("Server#getTransaction", function () { + let keypair = Keypair.random(); + let account = new StellarSdk.Account(keypair.publicKey(), "56199647068161"); + + beforeEach(function () { + this.server = new Server(serverUrl); + this.axiosMock = sinon.mock(AxiosClient); + let transaction = new TransactionBuilder(account, { + fee: 100, + networkPassphrase: StellarSdk.Networks.TESTNET, + v1: true, + }) + .addOperation( + StellarSdk.Operation.payment({ + destination: + "GASOCNHNNLYFNMDJYQ3XFMI7BYHIOCFW3GJEOWRPEGK2TDPGTG2E5EDW", + asset: StellarSdk.Asset.native(), + amount: "100.50", + }), + ) + .setTimeout(StellarSdk.TimeoutInfinite) + .build(); + transaction.sign(keypair); + + this.transaction = transaction; + this.hash = this.transaction.hash().toString("hex"); + this.blob = transaction.toEnvelope().toXDR().toString("base64"); + this.prepareAxios = (result) => { + this.axiosMock + .expects("post") + .withArgs(serverUrl, { + jsonrpc: "2.0", + id: 1, + method: "getTransaction", + params: [this.hash], + }) + .returns(Promise.resolve({ data: { id: 1, result } })); + }; + }); + + afterEach(function () { + this.axiosMock.verify(); + this.axiosMock.restore(); + }); + + it("transaction not found", function (done) { + const result = makeTxResult("NOT_FOUND"); + this.prepareAxios(result); + + this.server + .getTransaction(this.hash) + .then(function (response) { + expect(response).to.be.deep.equal(result); + done(); + }) + .catch((err) => done(err)); + }); + + it("transaction success", function (done) { + const result = makeTxResult("SUCCESS", true); + this.prepareAxios(result); + + let expected = JSON.parse(JSON.stringify(result)); + [ + ["envelopeXdr", xdr.TransactionEnvelope], + ["resultXdr", xdr.TransactionResult], + ["resultMetaXdr", xdr.TransactionMeta], + ].forEach(([field, struct]) => { + expected[field] = struct.fromXDR(result[field], "base64"); + }); + expected.returnValue = expected.resultMetaXdr + .v3() + .sorobanMeta() + .returnValue(); + + this.server + .getTransaction(this.hash) + .then((resp) => { + expect(Object.keys(resp)).to.eql(Object.keys(expected)); + expect(resp).to.eql(expected); + expect(resp.returnValue).to.eql(new XdrLargeInt("u64", 1234).toScVal()); + done(); + }) + .catch((err) => done(err)); + }); + + xit("non-Soroban transaction success", function (done) { + const result = makeTxResult("SUCCESS", false); + this.prepareAxios(result); + + this.server + .getTransaction(this.hash) + .then((resp) => { + expect(resp).to.be.deep.equal(result); + done(); + }) + .catch((err) => done(err)); + }); + + xit("transaction pending", function (done) {}); + xit("transaction error", function (done) {}); +}); + +function makeTxResult(status, addSoroban = true) { + const metaV3 = new xdr.TransactionMeta( + 3, + new xdr.TransactionMetaV3({ + ext: new xdr.ExtensionPoint(0), + txChangesBefore: [], + operations: [], + txChangesAfter: [], + sorobanMeta: new xdr.SorobanTransactionMeta({ + ext: new xdr.ExtensionPoint(0), + events: [], + diagnosticEvents: [], + returnValue: nativeToScVal(1234), + }), + }), + ); + + // only injected in the success case + // + // this data was picked from a random transaction in horizon: + // aa6a8e198abe53c7e852e4870413b29fe9ef04da1415a97a5de1a4ae489e11e2 + const successInfo = { + ledger: 1234, + createdAt: 123456789010, + applicationOrder: 2, + feeBump: false, + envelopeXdr: + "AAAAAgAAAAAT/LQZdYz0FcQ4Xwyg8IM17rkUx3pPCCWLu+SowQ/T+gBLB24poiQa9iwAngAAAAEAAAAAAAAAAAAAAABkwdeeAAAAAAAAAAEAAAABAAAAAC/9E8hDhnktyufVBS5tqA734Yz5XrLX2XNgBgH/YEkiAAAADQAAAAAAAAAAAAA1/gAAAAAv/RPIQ4Z5Lcrn1QUubagO9+GM+V6y19lzYAYB/2BJIgAAAAAAAAAAAAA1/gAAAAQAAAACU0lMVkVSAAAAAAAAAAAAAFDutWuu6S6UPJBrotNSgfmXa27M++63OT7TYn1qjgy+AAAAAVNHWAAAAAAAUO61a67pLpQ8kGui01KB+Zdrbsz77rc5PtNifWqODL4AAAACUEFMTEFESVVNAAAAAAAAAFDutWuu6S6UPJBrotNSgfmXa27M++63OT7TYn1qjgy+AAAAAlNJTFZFUgAAAAAAAAAAAABQ7rVrrukulDyQa6LTUoH5l2tuzPvutzk+02J9ao4MvgAAAAAAAAACwQ/T+gAAAEA+ztVEKWlqHXNnqy6FXJeHr7TltHzZE6YZm5yZfzPIfLaqpp+5cyKotVkj3d89uZCQNsKsZI48uoyERLne+VwL/2BJIgAAAEA7323gPSaezVSa7Vi0J4PqsnklDH1oHLqNBLwi5EWo5W7ohLGObRVQZ0K0+ufnm4hcm9J4Cuj64gEtpjq5j5cM", + resultXdr: + "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAANAAAAAAAAAAUAAAACZ4W6fmN63uhVqYRcHET+D2NEtJvhCIYflFh9GqtY+AwAAAACU0lMVkVSAAAAAAAAAAAAAFDutWuu6S6UPJBrotNSgfmXa27M++63OT7TYn1qjgy+AAAYW0toL2gAAAAAAAAAAAAANf4AAAACcgyAkXD5kObNTeRYciLh7R6ES/zzKp0n+cIK3Y6TjBkAAAABU0dYAAAAAABQ7rVrrukulDyQa6LTUoH5l2tuzPvutzk+02J9ao4MvgAAGlGnIJrXAAAAAlNJTFZFUgAAAAAAAAAAAABQ7rVrrukulDyQa6LTUoH5l2tuzPvutzk+02J9ao4MvgAAGFtLaC9oAAAAApmc7UgUBInrDvij8HMSridx2n1w3I8TVEn4sLr1LSpmAAAAAlBBTExBRElVTQAAAAAAAABQ7rVrrukulDyQa6LTUoH5l2tuzPvutzk+02J9ao4MvgAAIUz88EqYAAAAAVNHWAAAAAAAUO61a67pLpQ8kGui01KB+Zdrbsz77rc5PtNifWqODL4AABpRpyCa1wAAAAKYUsaaCZ233xB1p+lG7YksShJWfrjsmItbokiR3ifa0gAAAAJTSUxWRVIAAAAAAAAAAAAAUO61a67pLpQ8kGui01KB+Zdrbsz77rc5PtNifWqODL4AABv52PPa5wAAAAJQQUxMQURJVU0AAAAAAAAAUO61a67pLpQ8kGui01KB+Zdrbsz77rc5PtNifWqODL4AACFM/PBKmAAAAAJnhbp+Y3re6FWphFwcRP4PY0S0m+EIhh+UWH0aq1j4DAAAAAAAAAAAAAA9pAAAAAJTSUxWRVIAAAAAAAAAAAAAUO61a67pLpQ8kGui01KB+Zdrbsz77rc5PtNifWqODL4AABv52PPa5wAAAAAv/RPIQ4Z5Lcrn1QUubagO9+GM+V6y19lzYAYB/2BJIgAAAAAAAAAAAAA9pAAAAAA=", + resultMetaXdr: metaV3.toXDR("base64"), + }; + + if (!addSoroban) { + // replace the V3 Soroban meta with a "classic" V2 version + successInfo.resultMetaXdr = + "AAAAAgAAAAIAAAADAtL5awAAAAAAAAAAS0CFMhOtWUKJWerx66zxkxORaiH6/3RUq7L8zspD5RoAAAAAAcm9QAKVkpMAAHpMAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAwAAAAAC0vi5AAAAAGTB02oAAAAAAAAAAQLS+WsAAAAAAAAAAEtAhTITrVlCiVnq8eus8ZMTkWoh+v90VKuy/M7KQ+UaAAAAAAHJvUAClZKTAAB6TQAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAAtL5awAAAABkwdd1AAAAAAAAAAEAAAAGAAAAAwLS+VQAAAACAAAAAG4cwu71zHNXx3jHCzRGOIthcnfwRgfN2f/AoHFLLMclAAAAAEySDkgAAAAAAAAAAkJVU0lORVNTAAAAAAAAAAC3JfDeo9vreItKNPoe74EkFIqWybeUQNFvLvURhHtskAAAAAAeQtHTL5f6TAAAXH0AAAAAAAAAAAAAAAAAAAABAtL5awAAAAIAAAAAbhzC7vXMc1fHeMcLNEY4i2Fyd/BGB83Z/8CgcUssxyUAAAAATJIOSAAAAAAAAAACQlVTSU5FU1MAAAAAAAAAALcl8N6j2+t4i0o0+h7vgSQUipbJt5RA0W8u9RGEe2yQAAAAAB5C0dNHf4CAAACLCQAAAAAAAAAAAAAAAAAAAAMC0vlUAAAAAQAAAABuHMLu9cxzV8d4xws0RjiLYXJ38EYHzdn/wKBxSyzHJQAAAAJCVVNJTkVTUwAAAAAAAAAAtyXw3qPb63iLSjT6Hu+BJBSKlsm3lEDRby71EYR7bJAAAAAAAABAL3//////////AAAAAQAAAAEAE3H3TnhnuQAAAAAAAAAAAAAAAAAAAAAAAAABAtL5awAAAAEAAAAAbhzC7vXMc1fHeMcLNEY4i2Fyd/BGB83Z/8CgcUssxyUAAAACQlVTSU5FU1MAAAAAAAAAALcl8N6j2+t4i0o0+h7vgSQUipbJt5RA0W8u9RGEe2yQAAAAAAAAQC9//////////wAAAAEAAAABABNx9J6Z4RkAAAAAAAAAAAAAAAAAAAAAAAAAAwLS+WsAAAAAAAAAAG4cwu71zHNXx3jHCzRGOIthcnfwRgfN2f/AoHFLLMclAAAAH37+zXQCXdRTAAASZAAAApIAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAEAAABbBXKIigAAABhZWyiOAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAAtL0awAAAABkwbqrAAAAAAAAAAEC0vlrAAAAAAAAAABuHMLu9cxzV8d4xws0RjiLYXJ38EYHzdn/wKBxSyzHJQAAAB9+/s10Al3UUwAAEmQAAAKSAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAWwVyiIoAAAAYWVsojgAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAALS9GsAAAAAZMG6qwAAAAAAAAAA"; + } + + return { + status, + latestLedger: 100, + latestLedgerCloseTime: 12345, + oldestLedger: 50, + oldestLedgerCloseTime: 500, + ...(status === "SUCCESS" && successInfo), + }; +} diff --git a/test/unit/server/soroban/request_airdrop_test.js b/test/unit/server/soroban/request_airdrop_test.js new file mode 100644 index 000000000..ec9db6764 --- /dev/null +++ b/test/unit/server/soroban/request_airdrop_test.js @@ -0,0 +1,258 @@ +const { Account, Keypair, StrKey, Networks, xdr, hash } = StellarSdk; +const { Server, AxiosClient } = StellarSdk.SorobanRpc; + +describe("Server#requestAirdrop", function () { + function accountLedgerEntryData(accountId, sequence) { + return new xdr.LedgerEntryData.account( + new xdr.AccountEntry({ + accountId: xdr.AccountId.publicKeyTypeEd25519( + StrKey.decodeEd25519PublicKey(accountId), + ), + balance: xdr.Int64.fromString("1"), + seqNum: xdr.SequenceNumber.fromString(sequence), + numSubEntries: 0, + inflationDest: null, + flags: 0, + homeDomain: "", + // Taken from a real response. idk. + thresholds: Buffer.from("AQAAAA==", "base64"), + signers: [], + ext: new xdr.AccountEntryExt(0), + }), + ); + } + + // Create a mock transaction meta for the account we're going to request an airdrop for + function transactionMetaFor(accountId, sequence) { + return new xdr.TransactionMeta(0, [ + new xdr.OperationMeta({ + changes: [ + xdr.LedgerEntryChange.ledgerEntryCreated( + new xdr.LedgerEntry({ + lastModifiedLedgerSeq: 0, + data: accountLedgerEntryData(accountId, sequence), + ext: new xdr.LedgerEntryExt(0), + }), + ), + ], + }), + ]); + } + + beforeEach(function () { + this.server = new Server(serverUrl); + this.axiosMock = sinon.mock(AxiosClient); + }); + + afterEach(function () { + this.axiosMock.verify(); + this.axiosMock.restore(); + }); + + function mockGetNetwork(friendbotUrl) { + const result = { + friendbotUrl, + passphrase: Networks.FUTURENET, + protocolVersion: 20, + }; + this.axiosMock + .expects("post") + .withArgs(serverUrl, { + jsonrpc: "2.0", + id: 1, + method: "getNetwork", + params: null, + }) + .returns(Promise.resolve({ data: { result } })); + } + + it("returns true when the account is created", function (done) { + const friendbotUrl = "https://friendbot.stellar.org"; + const accountId = + "GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI"; + mockGetNetwork.call(this, friendbotUrl); + + const result_meta_xdr = transactionMetaFor(accountId, "1234").toXDR( + "base64", + ); + this.axiosMock + .expects("post") + .withArgs(`${friendbotUrl}?addr=${accountId}`) + .returns(Promise.resolve({ data: { result_meta_xdr } })); + + this.server + .requestAirdrop(accountId) + .then(function (response) { + expect(response).to.be.deep.equal(new Account(accountId, "1234")); + done(); + }) + .catch(function (err) { + done(err); + }); + }); + + it("returns false if the account already exists", function (done) { + const friendbotUrl = "https://friendbot.stellar.org"; + const accountId = + "GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI"; + mockGetNetwork.call(this, friendbotUrl); + + this.axiosMock + .expects("post") + .withArgs(`${friendbotUrl}?addr=${accountId}`) + .returns( + Promise.reject({ + response: { + status: 400, + detail: + "createAccountAlreadyExist (AAAAAAAAAGT/////AAAAAQAAAAAAAAAA/////AAAAAA=)", + }, + }), + ); + + const accountKey = xdr.LedgerKey.account( + new xdr.LedgerKeyAccount({ + accountId: Keypair.fromPublicKey(accountId).xdrPublicKey(), + }), + ); + + const ledgerExpirationKey = xdr.LedgerKey.expiration( + new xdr.LedgerKeyExpiration({ keyHash: hash(accountKey.toXDR()) }), + ); + + this.axiosMock + .expects("post") + .withArgs(serverUrl, { + jsonrpc: "2.0", + id: 1, + method: "getLedgerEntries", + params: [ + [accountKey.toXDR("base64"), ledgerExpirationKey.toXDR("base64")], + ], + }) + .returns( + Promise.resolve({ + data: { + result: { + entries: [ + { + key: accountKey.toXDR("base64"), + xdr: accountLedgerEntryData(accountId, "1234").toXDR( + "base64", + ), + }, + ], + }, + }, + }), + ); + + this.server + .requestAirdrop(accountId) + .then(function (response) { + expect(response).to.be.deep.equal(new Account(accountId, "1234")); + done(); + }) + .catch(function (err) { + done(err); + }); + }); + + it("uses custom friendbotUrl if passed", function (done) { + const friendbotUrl = "https://custom-friendbot.stellar.org"; + const accountId = + "GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI"; + + const result_meta_xdr = transactionMetaFor(accountId, "1234").toXDR( + "base64", + ); + this.axiosMock + .expects("post") + .withArgs(`${friendbotUrl}?addr=${accountId}`) + .returns(Promise.resolve({ data: { result_meta_xdr } })); + + this.server + .requestAirdrop(accountId, friendbotUrl) + .then(function (response) { + expect(response).to.be.deep.equal(new Account(accountId, "1234")); + done(); + }) + .catch(function (err) { + done(err); + }); + }); + + it("rejects invalid addresses", function (done) { + const friendbotUrl = "https://friendbot.stellar.org"; + const accountId = "addr&injected=1"; + mockGetNetwork.call(this, friendbotUrl); + + this.axiosMock + .expects("post") + .withArgs(`${friendbotUrl}?addr=addr%26injected%3D1`) + .returns( + Promise.reject({ + response: { + status: 400, + type: "https://stellar.org/horizon-errors/bad_request", + title: "Bad Request", + detail: "The request you sent was invalid in some way.", + extras: { + invalid_field: "addr", + reason: + "base32 decode failed: illegal base32 data at input byte 7", + }, + }, + }), + ); + + this.server + .requestAirdrop(accountId) + .then(function (_) { + done(new Error("Should have thrown")); + }) + .catch(function (err) { + expect(err.response.extras.reason).to.include("base32 decode failed"); + done(); + }); + }); + + it("throws if there is no friendbotUrl set", function (done) { + const accountId = "addr&injected=1"; + mockGetNetwork.call(this, undefined); + + this.server + .requestAirdrop(accountId) + .then(function (_) { + done(new Error("Should have thrown")); + }) + .catch(function (err) { + expect(err.message).to.be.equal( + "No friendbot URL configured for current network", + ); + done(); + }); + }); + + it("throws if the request fails", function (done) { + const friendbotUrl = "https://friendbot.stellar.org"; + const accountId = + "GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI"; + mockGetNetwork.call(this, friendbotUrl); + + this.axiosMock + .expects("post") + .withArgs(`${friendbotUrl}?addr=${accountId}`) + .returns(Promise.reject(new Error("Request failed"))); + + this.server + .requestAirdrop(accountId) + .then(function (_) { + done(new Error("Should have thrown")); + }) + .catch(function (err) { + expect(err.message).to.be.equal("Request failed"); + done(); + }); + }); +}); diff --git a/test/unit/server/soroban/send_transaction_test.js b/test/unit/server/soroban/send_transaction_test.js new file mode 100644 index 000000000..a7e390202 --- /dev/null +++ b/test/unit/server/soroban/send_transaction_test.js @@ -0,0 +1,110 @@ +const { xdr } = StellarSdk; +const { Server, AxiosClient } = StellarSdk.SorobanRpc; + +describe("Server#sendTransaction", function () { + let keypair = StellarSdk.Keypair.random(); + let account = new StellarSdk.Account(keypair.publicKey(), "56199647068161"); + + beforeEach(function () { + this.server = new Server(serverUrl); + this.axiosMock = sinon.mock(AxiosClient); + let transaction = new StellarSdk.TransactionBuilder(account, { + fee: 100, + networkPassphrase: StellarSdk.Networks.TESTNET, + v1: true, + }) + .addOperation( + StellarSdk.Operation.payment({ + destination: + "GASOCNHNNLYFNMDJYQ3XFMI7BYHIOCFW3GJEOWRPEGK2TDPGTG2E5EDW", + asset: StellarSdk.Asset.native(), + amount: "100.50", + }), + ) + .setTimeout(StellarSdk.TimeoutInfinite) + .build(); + transaction.sign(keypair); + + this.transaction = transaction; + this.hash = this.transaction.hash().toString("hex"); + this.blob = transaction.toEnvelope().toXDR().toString("base64"); + }); + + afterEach(function () { + this.axiosMock.verify(); + this.axiosMock.restore(); + }); + + it("sends a transaction", function (done) { + this.axiosMock + .expects("post") + .withArgs(serverUrl, { + jsonrpc: "2.0", + id: 1, + method: "sendTransaction", + params: [this.blob], + }) + .returns( + Promise.resolve({ + data: { id: 1, result: { id: this.hash, status: "PENDING" } }, + }), + ); + + this.server + .sendTransaction(this.transaction) + .then(function () { + done(); + }) + .catch(function (err) { + done(err); + }); + }); + + it("encodes the error result", function (done) { + const txResult = new xdr.TransactionResult({ + feeCharged: new xdr.Int64(1), + result: xdr.TransactionResultResult.txSorobanInvalid(), + ext: new xdr.TransactionResultExt(0), + }); + + this.axiosMock + .expects("post") + .withArgs(serverUrl, { + jsonrpc: "2.0", + id: 1, + method: "sendTransaction", + params: [this.blob], + }) + .returns( + Promise.resolve({ + data: { + id: 1, + result: { + id: this.hash, + status: "ERROR", + errorResultXdr: txResult.toXDR("base64"), + }, + }, + }), + ); + + this.server + .sendTransaction(this.transaction) + .then(function (r) { + expect(r.errorResult).to.be.instanceOf(xdr.TransactionResult); + expect(r.errorResult).to.eql(txResult); + expect(r.errorResultXdr).to.be.undefined; + + done(); + }) + .catch(done); + }); + + xit("adds metadata - tx was too small and was immediately deleted"); + xit("adds metadata, order immediately fills"); + xit("adds metadata, order is open"); + xit("adds metadata, partial fill"); + xit("doesnt add metadata to non-offers"); + xit("adds metadata about offers, even if some ops are not"); + xit("submits fee bump transactions"); +}); diff --git a/test/unit/server/soroban/simulate_transaction_test.js b/test/unit/server/soroban/simulate_transaction_test.js new file mode 100644 index 000000000..f2217f373 --- /dev/null +++ b/test/unit/server/soroban/simulate_transaction_test.js @@ -0,0 +1,289 @@ +const { + Account, + Keypair, + Networks, + SorobanRpc, + SorobanDataBuilder, + authorizeInvocation, + xdr, +} = StellarSdk; +const { Server, AxiosClient, parseRawSimulation } = StellarSdk.SorobanRpc; + +const randomSecret = Keypair.random().secret(); + +describe("Server#simulateTransaction", async function (done) { + let keypair = Keypair.random(); + let contractId = "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"; + let contract = new StellarSdk.Contract(contractId); + let address = contract.address().toScAddress(); + + const simulationResponse = await invokeSimulationResponse(address); + const parsedSimulationResponse = { + id: simulationResponse.id, + events: simulationResponse.events, + latestLedger: simulationResponse.latestLedger, + minResourceFee: simulationResponse.minResourceFee, + transactionData: new SorobanDataBuilder(simulationResponse.transactionData), + result: { + auth: simulationResponse.results[0].auth.map((entry) => + xdr.SorobanAuthorizationEntry.fromXDR(entry, "base64"), + ), + retval: xdr.ScVal.fromXDR(simulationResponse.results[0].xdr, "base64"), + }, + cost: simulationResponse.cost, + _parsed: true, + }; + + beforeEach(function () { + this.server = new Server(serverUrl); + this.axiosMock = sinon.mock(AxiosClient); + const source = new Account( + "GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI", + "1", + ); + function emptyContractTransaction() { + return new StellarSdk.TransactionBuilder(source, { fee: 100 }) + .setNetworkPassphrase("Test") + .setTimeout(StellarSdk.TimeoutInfinite) + .addOperation( + StellarSdk.Operation.invokeHostFunction({ + func: new xdr.HostFunction.hostFunctionTypeInvokeContract( + new xdr.InvokeContractArgs({ + contractAddress: address, + functionName: "hello", + args: [], + }), + ), + auth: [], + }), + ) + .build(); + } + + const transaction = emptyContractTransaction(); + transaction.sign(keypair); + + this.transaction = transaction; + this.hash = this.transaction.hash().toString("hex"); + this.blob = transaction.toEnvelope().toXDR().toString("base64"); + }); + + afterEach(function () { + this.axiosMock.verify(); + this.axiosMock.restore(); + }); + + it("simulates a transaction", function (done) { + this.axiosMock + .expects("post") + .withArgs(serverUrl, { + jsonrpc: "2.0", + id: 1, + method: "simulateTransaction", + params: [this.blob], + }) + .returns( + Promise.resolve({ data: { id: 1, result: simulationResponse } }), + ); + + this.server + .simulateTransaction(this.transaction) + .then(function (response) { + expect(response).to.be.deep.equal(parsedSimulationResponse); + done(); + }) + .catch(function (err) { + done(err); + }); + }); + + it("works when there are no results", function () { + const simResponse = baseSimulationResponse(); + const parsedCopy = cloneSimulation(parsedSimulationResponse); + delete parsedCopy.result; + + const parsed = parseRawSimulation(simResponse); + expect(parsed).to.deep.equal(parsedCopy); + expect(SorobanRpc.Api.assembleTransaction(parsed)).to.be.true; + }); + + it("works with no auth", async function () { + return invokeSimulationResponse(address).then((simResponse) => { + delete simResponse.results[0].auth; + + const parsedCopy = cloneSimulation(parsedSimulationResponse); + parsedCopy.result.auth = []; + const parsed = parseRawSimulation(simResponse); + + expect(parsed).to.be.deep.equal(parsedCopy); + expect(SorobanRpc.Api.assembleTransaction(parsed)).to.be.true; + }); + }); + + it("works with restoration", async function () { + return invokeSimulationResponseWithRestoration(address).then( + (simResponse) => { + const expected = cloneSimulation(parsedSimulationResponse); + expected.restorePreamble = { + minResourceFee: "51", + transactionData: new SorobanDataBuilder(), + }; + + const parsed = parseRawSimulation(simResponse); + expect(StellarSdk.Api.isSimulationRestore(parsed)).to.be.true; + expect(parsed).to.be.deep.equal(expected); + }, + ); + }); + + it("works with errors", function () { + let simResponse = simulationResponseError(); + + const expected = cloneSimulation(parsedSimulationResponse); + // drop fields that go away with errors + delete expected.result; + delete expected.cost; + delete expected.transactionData; + delete expected.minResourceFee; + expected.error = "This is an error"; + expected.events = []; + + const parsed = parseRawSimulation(simResponse); + expect(parsed).to.be.deep.equal(expected); + expect(StellarSdk.Api.isSimulationError(parsed)).to.be.true; + }); + + xit("simulates fee bump transactions"); + + done(); +}); + +function cloneSimulation(sim) { + return { + id: sim.id, + events: Array.from(sim.events), + latestLedger: sim.latestLedger, + minResourceFee: sim.minResourceFee, + transactionData: new SorobanDataBuilder(sim.transactionData.build()), + result: { + auth: sim.result.auth.map((entry) => + xdr.SorobanAuthorizationEntry.fromXDR(entry.toXDR()), + ), + retval: xdr.ScVal.fromXDR(sim.result.retval.toXDR()), + }, + cost: sim.cost, + _parsed: sim._parsed, + }; +} + +async function buildAuthEntry(address) { + if (!address) { + throw new Error("where address?"); + } + + // Basic fake invocation + const root = new xdr.SorobanAuthorizedInvocation({ + subInvocations: [], + function: + xdr.SorobanAuthorizedFunction.sorobanAuthorizedFunctionTypeContractFn( + new xdr.InvokeContractArgs({ + contractAddress: address, + functionName: "test", + args: [], + }), + ), + }); + + // do some voodoo to make this return a deterministic auth entry + const kp = Keypair.fromSecret(randomSecret); + let entry = authorizeInvocation(kp, 1, root); + entry.credentials().address().nonce(new xdr.Int64(0xdeadbeef)); + + return authorizeEntry(entry, kp, 1); // overwrites signature w/ above nonce +} + +async function invokeSimulationResponse(address) { + return baseSimulationResponse([ + { + auth: [await buildAuthEntry(address)].map((entry) => + entry.toXDR("base64"), + ), + xdr: xdr.ScVal.scvU32(0).toXDR("base64"), + }, + ]); +} + +function simulationResponseError(events) { + return { + id: 1, + ...(events !== undefined && { events }), + latestLedger: 3, + error: "This is an error", + }; +} + +function baseSimulationResponse(results) { + return { + id: 1, + events: [], + latestLedger: 3, + minResourceFee: "15", + transactionData: new SorobanDataBuilder().build().toXDR("base64"), + ...(results !== undefined && { results }), + cost: { + cpuInsns: "1", + memBytes: "2", + }, + }; +} + +async function invokeSimulationResponseWithRestoration(address) { + return { + ...(await invokeSimulationResponse(address)), + restorePreamble: { + minResourceFee: "51", + transactionData: new SorobanDataBuilder().build().toXDR("base64"), + }, + }; +} + +describe("works with real responses", function () { + const schema = { + transactionData: + "AAAAAAAAAAIAAAAGAAAAAa/6eoLeofDK5ksPljSZ7t/rAj/XR18e40fCB9LBugstAAAAFAAAAAEAAAAHqA0LEZLq3WL+N3rBQLTWuPqdV3Vv6XIAGeBJaz1wMdsAAAAAABg1gAAAAxwAAAAAAAAAAAAAAAk=", + minResourceFee: "27889", + events: [ + "AAAAAQAAAAAAAAAAAAAAAgAAAAAAAAADAAAADwAAAAdmbl9jYWxsAAAAAA0AAAAgr/p6gt6h8MrmSw+WNJnu3+sCP9dHXx7jR8IH0sG6Cy0AAAAPAAAABWhlbGxvAAAAAAAADwAAAAVBbG9oYQAAAA==", + "AAAAAQAAAAAAAAABr/p6gt6h8MrmSw+WNJnu3+sCP9dHXx7jR8IH0sG6Cy0AAAACAAAAAAAAAAIAAAAPAAAACWZuX3JldHVybgAAAAAAAA8AAAAFaGVsbG8AAAAAAAAQAAAAAQAAAAIAAAAPAAAABUhlbGxvAAAAAAAADwAAAAVBbG9oYQAAAA==", + ], + results: [ + { + auth: [], + xdr: "AAAAEAAAAAEAAAACAAAADwAAAAVIZWxsbwAAAAAAAA8AAAAFQWxvaGEAAAA=", + }, + ], + cost: { + cpuInsns: "1322134", + memBytes: "1207047", + }, + restorePreamble: { + transactionData: "", + minResourceFee: "0", + }, + latestLedger: "2634", + }; + + it("parses the schema", function () { + expect(SorobanRpc.Api.isSimulationRaw(schema)).to.be.true; + + const parsed = parseRawSimulation(schema); + + expect(parsed.results).to.be.undefined; + expect(parsed.result.auth).to.be.empty; + expect(parsed.result.retval).to.be.instanceOf(xdr.ScVal); + expect(parsed.transactionData).to.be.instanceOf(SorobanDataBuilder); + expect(parsed.events).to.be.lengthOf(2); + expect(parsed.events[0]).to.be.instanceOf(xdr.DiagnosticEvent); + expect(parsed.restorePreamble).to.be.undefined; + }); +}); diff --git a/test/unit/server_check_memo_required_test.js b/test/unit/server_check_memo_required_test.js index bde7f2bb6..f2a17af90 100644 --- a/test/unit/server_check_memo_required_test.js +++ b/test/unit/server_check_memo_required_test.js @@ -1,3 +1,5 @@ +const { Horizon } = StellarSdk; + function buildTransaction(destination, operations = [], builderOpts = {}) { let txBuilderOpts = { fee: 100, @@ -105,8 +107,8 @@ function mockAccountRequest(axiosMock, id, status, data = {}) { describe("server.js check-memo-required", function () { beforeEach(function () { - this.server = new StellarSdk.Server("https://horizon-testnet.stellar.org"); - this.axiosMock = sinon.mock(HorizonAxiosClient); + this.server = new Horizon.Server("https://horizon-testnet.stellar.org"); + this.axiosMock = sinon.mock(Horizon.AxiosClient); }); afterEach(function () { diff --git a/test/unit/server_transaction_test.js b/test/unit/server_transaction_test.js index 50ea7d49c..233ace6a5 100644 --- a/test/unit/server_transaction_test.js +++ b/test/unit/server_transaction_test.js @@ -1,12 +1,12 @@ +const { Horizon } = StellarSdk; + describe("server.js transaction tests", function () { let keypair = StellarSdk.Keypair.random(); let account = new StellarSdk.Account(keypair.publicKey(), "56199647068161"); beforeEach(function () { - this.server = new StellarSdk.Server( - "https://horizon-live.stellar.org:1337", - ); - this.axiosMock = sinon.mock(HorizonAxiosClient); + this.server = new Horizon.Server("https://horizon-live.stellar.org:1337"); + this.axiosMock = sinon.mock(Horizon.AxiosClient); let transaction = new StellarSdk.TransactionBuilder(account, { fee: 100, networkPassphrase: StellarSdk.Networks.TESTNET, diff --git a/test/unit/stellar_toml_resolver_test.js b/test/unit/stellar_toml_resolver_test.js index cd932490a..fae2640c0 100644 --- a/test/unit/stellar_toml_resolver_test.js +++ b/test/unit/stellar_toml_resolver_test.js @@ -1,5 +1,7 @@ const http = require("http"); +const { Resolver, STELLAR_TOML_MAX_SIZE } = StellarSdk.StellarToml; + describe("stellar_toml_resolver.js tests", function () { beforeEach(function () { this.axiosMock = sinon.mock(axios); @@ -11,7 +13,7 @@ describe("stellar_toml_resolver.js tests", function () { this.axiosMock.restore(); }); - describe("StellarTomlResolver.resolve", function () { + describe("Resolver.resolve", function () { afterEach(function () { StellarSdk.Config.setDefault(); }); @@ -30,7 +32,7 @@ FEDERATION_SERVER="https://api.stellar.org/federation" }), ); - StellarSdk.StellarTomlResolver.resolve("acme.com").then((stellarToml) => { + Resolver.resolve("acme.com").then((stellarToml) => { expect(stellarToml.FEDERATION_SERVER).equals( "https://api.stellar.org/federation", ); @@ -52,7 +54,7 @@ FEDERATION_SERVER="http://api.stellar.org/federation" }), ); - StellarSdk.StellarTomlResolver.resolve("acme.com", { + Resolver.resolve("acme.com", { allowHttp: true, }).then((stellarToml) => { expect(stellarToml.FEDERATION_SERVER).equals( @@ -78,7 +80,7 @@ FEDERATION_SERVER="http://api.stellar.org/federation" }), ); - StellarSdk.StellarTomlResolver.resolve("acme.com").then((stellarToml) => { + Resolver.resolve("acme.com").then((stellarToml) => { expect(stellarToml.FEDERATION_SERVER).equals( "http://api.stellar.org/federation", ); @@ -100,7 +102,7 @@ FEDERATION_SERVER="https://api.stellar.org/federation" }), ); - StellarSdk.StellarTomlResolver.resolve("acme.com") + Resolver.resolve("acme.com") .should.be.rejectedWith(/Parsing error on line/) .and.notify(done); }); @@ -111,9 +113,7 @@ FEDERATION_SERVER="https://api.stellar.org/federation" .withArgs(sinon.match("https://acme.com/.well-known/stellar.toml")) .returns(Promise.reject()); - StellarSdk.StellarTomlResolver.resolve( - "acme.com", - ).should.be.rejected.and.notify(done); + Resolver.resolve("acme.com").should.be.rejected.and.notify(done); }); it("fails when response exceeds the limit", function (done) { @@ -121,14 +121,14 @@ FEDERATION_SERVER="https://api.stellar.org/federation" if (typeof window != "undefined") { return done(); } - var response = Array(StellarSdk.STELLAR_TOML_MAX_SIZE + 10).join("a"); + var response = Array(STELLAR_TOML_MAX_SIZE + 10).join("a"); let tempServer = http .createServer((req, res) => { res.setHeader("Content-Type", "text/x-toml; charset=UTF-8"); res.end(response); }) .listen(4444, () => { - StellarSdk.StellarTomlResolver.resolve("localhost:4444", { + Resolver.resolve("localhost:4444", { allowHttp: true, }) .should.be.rejectedWith( @@ -152,7 +152,7 @@ FEDERATION_SERVER="https://api.stellar.org/federation" setTimeout(() => {}, 10000); }) .listen(4444, () => { - StellarSdk.StellarTomlResolver.resolve("localhost:4444", { + Resolver.resolve("localhost:4444", { allowHttp: true, }) .should.be.rejectedWith(/timeout of 1000ms exceeded/) @@ -164,7 +164,7 @@ FEDERATION_SERVER="https://api.stellar.org/federation" }); }); - it("rejects after given timeout when timeout specified in StellarTomlResolver opts param", function (done) { + it("rejects after given timeout when timeout specified in Resolver opts param", function (done) { // Unable to create temp server in a browser if (typeof window != "undefined") { return done(); @@ -175,7 +175,7 @@ FEDERATION_SERVER="https://api.stellar.org/federation" setTimeout(() => {}, 10000); }) .listen(4444, () => { - StellarSdk.StellarTomlResolver.resolve("localhost:4444", { + Resolver.resolve("localhost:4444", { allowHttp: true, timeout: 1000, }) diff --git a/test/unit/transaction_test.js b/test/unit/transaction_test.js new file mode 100644 index 000000000..b8eb127a9 --- /dev/null +++ b/test/unit/transaction_test.js @@ -0,0 +1,232 @@ +const { xdr, SorobanRpc } = StellarSdk; + +describe("assembleTransaction", () => { + xit("works with keybump transactions"); + + const scAddress = new StellarSdk.Address( + "GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI", + ).toScAddress(); + + const fnAuth = new xdr.SorobanAuthorizationEntry({ + // Include a credentials w/ a nonce to trigger this + credentials: new xdr.SorobanCredentials.sorobanCredentialsAddress( + new xdr.SorobanAddressCredentials({ + address: scAddress, + nonce: new xdr.Int64(0), + signatureExpirationLedger: 1, + signature: xdr.ScVal.scvVoid(), + }), + ), + // And a basic invocation + rootInvocation: new xdr.SorobanAuthorizedInvocation({ + function: + xdr.SorobanAuthorizedFunction.sorobanAuthorizedFunctionTypeContractFn( + new xdr.InvokeContractArgs({ + contractAddress: scAddress, + functionName: "fn", + args: [], + }), + ), + subInvocations: [], + }), + }).toXDR(); + + const sorobanTransactionData = new StellarSdk.SorobanDataBuilder() + .setResources(0, 5, 0, 0) + .build(); + + const simulationResponse = { + transactionData: sorobanTransactionData.toXDR("base64"), + events: [], + minResourceFee: "115", + results: [ + { + auth: [fnAuth], + xdr: xdr.ScVal.scvU32(0).toXDR("base64"), + }, + ], + latestLedger: 3, + cost: { + cpuInsns: "0", + memBytes: "0", + }, + }; + + describe("Transaction", () => { + const networkPassphrase = StellarSdk.Networks.TESTNET; + const source = new StellarSdk.Account( + "GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI", + "1", + ); + + function singleContractFnTransaction(auth) { + return new StellarSdk.TransactionBuilder(source, { fee: 100 }) + .setNetworkPassphrase("Test") + .setTimeout(StellarSdk.TimeoutInfinite) + .addOperation( + StellarSdk.Operation.invokeHostFunction({ + func: xdr.HostFunction.hostFunctionTypeInvokeContract( + new xdr.InvokeContractArgs({ + contractAddress: scAddress, + functionName: "hello", + args: [xdr.ScVal.scvString("hello")], + }), + ), + auth: auth ?? [], + }), + ) + .build(); + } + + it("simulate updates the tx data from simulation response", () => { + const txn = singleContractFnTransaction(); + const result = SorobanRpc.assembleTransaction( + txn, + networkPassphrase, + simulationResponse, + ).build(); + + // validate it auto updated the tx fees from sim response fees + // since it was greater than tx.fee + expect(result.toEnvelope().v1().tx().fee()).to.equal(215); + + // validate it udpated sorobantransactiondata block in the tx ext + expect(result.toEnvelope().v1().tx().ext().sorobanData()).to.deep.equal( + sorobanTransactionData, + ); + }); + + it("simulate adds the auth to the host function in tx operation", () => { + const txn = singleContractFnTransaction(); + const result = SorobanRpc.assembleTransaction( + txn, + networkPassphrase, + simulationResponse, + ).build(); + + expect( + result + .toEnvelope() + .v1() + .tx() + .operations()[0] + .body() + .invokeHostFunctionOp() + .auth()[0] + .rootInvocation() + .function() + .contractFn() + .functionName() + .toString(), + ).to.equal("fn"); + + expect( + StellarSdk.StrKey.encodeEd25519PublicKey( + result + .toEnvelope() + .v1() + .tx() + .operations()[0] + .body() + .invokeHostFunctionOp() + .auth()[0] + .credentials() + .address() + .address() + .accountId() + .ed25519(), + ), + ).to.equal("GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI"); + }); + + it("simulate ignores non auth from simulation", () => { + const txn = singleContractFnTransaction(); + let simulateResp = JSON.parse(JSON.stringify(simulationResponse)); + simulateResp.results[0].auth = null; + const result = SorobanRpc.assembleTransaction( + txn, + networkPassphrase, + simulateResp, + ).build(); + + expect( + result + .toEnvelope() + .v1() + .tx() + .operations()[0] + .body() + .invokeHostFunctionOp() + .auth(), + ).to.have.length(0); + }); + + it("throws for non-Soroban ops", () => { + const txn = new StellarSdk.TransactionBuilder(source, { + fee: 100, + networkPassphrase, + v1: true, + }) + .addOperation( + StellarSdk.Operation.changeTrust({ + asset: StellarSdk.Asset.native(), + }), + ) + .setTimeout(StellarSdk.TimeoutInfinite) + .build(); + + expect(() => { + SorobanRpc.assembleTransaction(txn, networkPassphrase, { + transactionData: {}, + events: [], + minResourceFee: "0", + results: [], + latestLedger: 3, + }).build(); + expect.fail(); + }).to.throw(/unsupported transaction/i); + }); + + it("works for all Soroban ops", function () { + [ + StellarSdk.Operation.invokeHostFunction({ + func: xdr.HostFunction.hostFunctionTypeInvokeContract(), + }), + StellarSdk.Operation.bumpFootprintExpiration({ + ledgersToExpire: 27, + }), + StellarSdk.Operation.restoreFootprint(), + ].forEach((op) => { + const txn = new StellarSdk.TransactionBuilder(source, { + fee: 100, + networkPassphrase, + v1: true, + }) + .setTimeout(StellarSdk.TimeoutInfinite) + .addOperation(op) + .build(); + + const tx = SorobanRpc.assembleTransaction( + txn, + networkPassphrase, + simulationResponse, + ).build(); + expect(tx.operations[0].type).to.equal(op.body().switch().name); + }); + }); + + it("doesn't overwrite auth if it's present", function () { + const txn = singleContractFnTransaction([fnAuth, fnAuth, fnAuth]); + const tx = SorobanRpc.assembleTransaction( + txn, + networkPassphrase, + simulationResponse, + ).build(); + + expect(tx.operations[0].auth.length).to.equal( + 3, + `auths aren't preserved after simulation: ${simulationResponse}, ${tx}`, + ); + }); + }); +}); diff --git a/test/unit/utils_test.js b/test/unit/utils_test.js index f712c9d7c..f638201bd 100644 --- a/test/unit/utils_test.js +++ b/test/unit/utils_test.js @@ -1,4 +1,5 @@ const randomBytes = require("randombytes"); +const { WebAuth } = StellarSdk; function newClientSigner(key, weight) { return { key, weight }; @@ -19,7 +20,7 @@ describe("Utils", function () { clock.restore(); }); - describe("Utils.buildChallengeTx", function () { + describe("WebAuth.buildChallengeTx", function () { it("allows non-muxed accounts", function () { let keypair = StellarSdk.Keypair.random(); let muxedAddress = @@ -27,7 +28,7 @@ describe("Utils", function () { let challenge; expect( () => - (challenge = StellarSdk.Utils.buildChallengeTx( + (challenge = WebAuth.buildChallengeTx( keypair, "MAAAAAAAAAAAAAB7BQ2L7E5NBWMXDUCMZSIPOBKRDSBYVLMXGSSKF6YNPIB7Y77ITLVL6", "testanchor.stellar.org", @@ -49,7 +50,7 @@ describe("Utils", function () { let challenge; expect( () => - (challenge = StellarSdk.Utils.buildChallengeTx( + (challenge = WebAuth.buildChallengeTx( keypair, StellarSdk.Keypair.random().publicKey(), "testanchor.stellar.org", @@ -71,7 +72,7 @@ describe("Utils", function () { let keypair = StellarSdk.Keypair.random(); expect( () => - (challenge = StellarSdk.Utils.buildChallengeTx( + (challenge = WebAuth.buildChallengeTx( keypair, StellarSdk.Keypair.random().publicKey(), "testanchor.stellar.org", @@ -89,7 +90,7 @@ describe("Utils", function () { "MAAAAAAAAAAAAAB7BQ2L7E5NBWMXDUCMZSIPOBKRDSBYVLMXGSSKF6YNPIB7Y77ITLVL6"; expect( () => - (challenge = StellarSdk.Utils.buildChallengeTx( + (challenge = WebAuth.buildChallengeTx( keypair, muxedAddress, "testanchor.stellar.org", @@ -105,7 +106,7 @@ describe("Utils", function () { let keypair = StellarSdk.Keypair.random(); let clientSigningKeypair = StellarSdk.Keypair.random(); - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( keypair, "GBDIT5GUJ7R5BXO3GJHFXJ6AZ5UQK6MNOIDMPQUSMXLIHTUNR2Q5CFNF", "testanchor.stellar.org", @@ -156,7 +157,7 @@ describe("Utils", function () { it("uses the passed-in timeout", function () { let keypair = StellarSdk.Keypair.random(); - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( keypair, "GBDIT5GUJ7R5BXO3GJHFXJ6AZ5UQK6MNOIDMPQUSMXLIHTUNR2Q5CFNF", "testanchor.stellar.org", @@ -183,7 +184,7 @@ describe("Utils", function () { const muxedAddress = "MCQQMHTBRF2NPCEJWO2JMDT2HBQ2FGDCYREY2YIBSHLTXDG54Y3KTWX3R7NBER62VBELC"; expect(() => - StellarSdk.Utils.buildChallengeTx( + WebAuth.buildChallengeTx( keypair, muxedAddress, "testanchor.stellar.org", @@ -197,7 +198,7 @@ describe("Utils", function () { it("throws an error if clientSigningKey is not passed", function () { expect(() => - StellarSdk.Utils.buildChallengeTx( + WebAuth.buildChallengeTx( StellarSdk.Keypair.random(), StellarSdk.Keypair.random().publicKey(), "testanchor.stellar.org", @@ -212,12 +213,12 @@ describe("Utils", function () { }); }); - describe("Utils.readChallengeTx", function () { + describe("WebAuth.readChallengeTx", function () { it("requires a envelopeTypeTxV0 or envelopeTypeTx", function () { let serverKP = StellarSdk.Keypair.random(); let clientKP = StellarSdk.Keypair.random(); - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( serverKP, clientKP.publicKey(), "SDF", @@ -254,7 +255,7 @@ describe("Utils", function () { ).toXDR(); expect(() => - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( feeBump, serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -262,34 +263,34 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /Invalid challenge: expected a Transaction but received a FeeBumpTransaction/, ); expect(() => - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( challenge, serverKP.publicKey(), StellarSdk.Networks.TESTNET, "SDF", "testanchor.stellar.org", ), - ).to.not.throw(StellarSdk.InvalidSep10ChallengeError); + ).to.not.throw(WebAuth.InvalidChallengeError); expect(() => - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( feeBump.toXDR().toString("base64"), serverKP.publicKey(), StellarSdk.Networks.TESTNET, "SDF", "testanchor.stellar.org", ), - ).to.not.throw(StellarSdk.InvalidSep10ChallengeError); + ).to.not.throw(WebAuth.InvalidChallengeError); }); it("returns the transaction and the clientAccountID (client's pubKey) if the challenge was created successfully", function () { let serverKP = StellarSdk.Keypair.random(); let clientKP = StellarSdk.Keypair.random(); - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( serverKP, clientKP.publicKey(), "SDF", @@ -306,7 +307,7 @@ describe("Utils", function () { ); expect( - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( challenge, serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -326,7 +327,7 @@ describe("Utils", function () { let clientKP = StellarSdk.Keypair.random(); let clientMemo = "7659725268483412096"; - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( serverKP, clientKP.publicKey(), "SDF", @@ -344,7 +345,7 @@ describe("Utils", function () { ); expect( - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( challenge, serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -364,7 +365,7 @@ describe("Utils", function () { let muxedAddress = "MCQQMHTBRF2NPCEJWO2JMDT2HBQ2FGDCYREY2YIBSHLTXDG54Y3KTWX3R7NBER62VBELC"; - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( serverKP, muxedAddress, "SDF", @@ -382,7 +383,7 @@ describe("Utils", function () { ); expect( - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( challenge, serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -428,7 +429,7 @@ describe("Utils", function () { ); expect(() => - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( challenge, serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -436,7 +437,7 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /The transaction has a memo but the client account ID is a muxed account/, ); }); @@ -462,7 +463,7 @@ describe("Utils", function () { const challenge = transaction.toEnvelope().toXDR("base64").toString(); expect(() => - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( challenge, serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -470,7 +471,7 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, "Transaction not signed by server: '" + serverKP.publicKey() + "'", ); }); @@ -489,7 +490,7 @@ describe("Utils", function () { let challenge = transaction.toEnvelope().toXDR("base64").toString(); expect(() => - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( challenge, keypair.publicKey(), StellarSdk.Networks.TESTNET, @@ -497,7 +498,7 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /The transaction sequence number should be zero/, ); }); @@ -505,7 +506,7 @@ describe("Utils", function () { it("throws an error if transaction source account is different to server account id", function () { let keypair = StellarSdk.Keypair.random(); - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( keypair, "GBDIT5GUJ7R5BXO3GJHFXJ6AZ5UQK6MNOIDMPQUSMXLIHTUNR2Q5CFNF", "SDF", @@ -517,7 +518,7 @@ describe("Utils", function () { let serverAccountId = StellarSdk.Keypair.random().publicKey(); expect(() => - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( challenge, serverAccountId, StellarSdk.Networks.TESTNET, @@ -525,7 +526,7 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /The transaction source account is not equal to the server's account/, ); }); @@ -544,7 +545,7 @@ describe("Utils", function () { const challenge = transaction.toEnvelope().toXDR("base64").toString(); expect(() => - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( challenge, keypair.publicKey(), StellarSdk.Networks.TESTNET, @@ -552,7 +553,7 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /The transaction should contain at least one operation/, ); }); @@ -577,7 +578,7 @@ describe("Utils", function () { const challenge = transaction.toEnvelope().toXDR("base64").toString(); expect(() => - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( challenge, keypair.publicKey(), StellarSdk.Networks.TESTNET, @@ -585,7 +586,7 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /The transaction\'s operation should contain a source account/, ); }); @@ -610,7 +611,7 @@ describe("Utils", function () { const challenge = transaction.toEnvelope().toXDR("base64").toString(); expect(() => - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( challenge, keypair.publicKey(), StellarSdk.Networks.TESTNET, @@ -618,7 +619,7 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /The transaction\'s operation type should be \'manageData\'/, ); }); @@ -667,7 +668,7 @@ describe("Utils", function () { .toString(); expect(() => - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( signedChallenge, serverKeypair.publicKey(), StellarSdk.Networks.TESTNET, @@ -675,7 +676,7 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /The transaction requires non-infinite timebounds/, ); }); @@ -701,7 +702,7 @@ describe("Utils", function () { const challenge = transaction.toEnvelope().toXDR("base64").toString(); expect(() => - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( challenge, keypair.publicKey(), StellarSdk.Networks.TESTNET, @@ -709,7 +710,7 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /The transaction\'s operation value should be a 64 bytes base64 random string/, ); }); @@ -735,13 +736,13 @@ describe("Utils", function () { const challenge = transaction.toEnvelope().toXDR("base64").toString(); expect(() => - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( challenge, keypair.publicKey(), StellarSdk.Networks.TESTNET, ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /The transaction\'s operation values should not be null/, ); }); @@ -750,7 +751,7 @@ describe("Utils", function () { let keypair = StellarSdk.Keypair.random(); let clientKeypair = StellarSdk.Keypair.random(); - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( keypair, clientKeypair.publicKey(), "SDF", @@ -774,17 +775,14 @@ describe("Utils", function () { .toString(); expect(() => - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( signedChallenge, keypair.publicKey(), StellarSdk.Networks.TESTNET, "SDF", "testanchor.stellar.org", ), - ).to.throw( - StellarSdk.InvalidSep10ChallengeError, - /The transaction has expired/, - ); + ).to.throw(WebAuth.InvalidChallengeError, /The transaction has expired/); }); it("does NOT throw errors when the user is slightly out of minTime", function () { @@ -796,7 +794,7 @@ describe("Utils", function () { "AAAAAgAAAADZJunw2QO9LzjqagEjh/mpWG8Us5nOb+gc6wOex8G+IwAAAGQAAAAAAAAAAAAAAAEAAAAAYPhZ6gAAAXrKHz2UAAAAAAAAAAEAAAABAAAAAJyknd/qYHdzX6iV3TkHlh/usJUr5/U8cRsfVNqaruBAAAAACgAAAB50ZXN0bmV0LXNlcC5zdGFibGV4LmNsb3VkIGF1dGgAAAAAAAEAAABAaEs3QUZieUFCZzBEekx0WnpTVXJkcEhWOXdkdExXUkwxUHFFOW5QRVIrZVlaZzQvdDJlc3drclpBc0ZnTnp5UQAAAAAAAAABx8G+IwAAAEA8I5qQ+/HHXoHrULlg1ODTiCEQ92GQrVBFaB40OKxJhTf1c597AuKLHhJ3c4TNdSp1rjLGbk7qUuhjauxUuH0N"; expect(() => - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( signedChallenge, "GDMSN2PQ3EB32LZY5JVACI4H7GUVQ3YUWOM4437IDTVQHHWHYG7CGA5Z", StellarSdk.Networks.TESTNET, @@ -804,7 +802,7 @@ describe("Utils", function () { "staging-transfer-server.zetl.network", ), ).not.to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /The transaction has expired/, ); }); @@ -836,7 +834,7 @@ describe("Utils", function () { ); expect( - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( challenge, serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -878,7 +876,7 @@ describe("Utils", function () { ); expect( - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( challenge, serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -915,14 +913,14 @@ describe("Utils", function () { const challenge = transaction.toEnvelope().toXDR("base64").toString(); expect(() => - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( challenge, serverKP.publicKey(), StellarSdk.Networks.TESTNET, // home domain not provided ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /Invalid homeDomains: a home domain must be provided for verification/, ); }); @@ -949,7 +947,7 @@ describe("Utils", function () { const challenge = transaction.toEnvelope().toXDR("base64").toString(); expect(() => - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( challenge, serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -957,7 +955,7 @@ describe("Utils", function () { 1, ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /Invalid homeDomains: homeDomains type is number but should be a string or an array/, ); }); @@ -984,7 +982,7 @@ describe("Utils", function () { const challenge = transaction.toEnvelope().toXDR("base64").toString(); expect(() => - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( challenge, serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -992,7 +990,7 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /Invalid homeDomains: the transaction\'s operation key name does not match the expected home domain/, ); }); @@ -1019,7 +1017,7 @@ describe("Utils", function () { const challenge = transaction.toEnvelope().toXDR("base64").toString(); expect(() => - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( challenge, serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -1027,7 +1025,7 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /Invalid homeDomains: the transaction\'s operation key name does not match the expected home domain/, ); }); @@ -1066,7 +1064,7 @@ describe("Utils", function () { ); expect( - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( challenge, serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -1115,7 +1113,7 @@ describe("Utils", function () { ); expect(() => - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( challenge, serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -1123,7 +1121,7 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /The transaction has operations that are unrecognized/, ); }); @@ -1161,7 +1159,7 @@ describe("Utils", function () { ); expect(() => - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( challenge, serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -1169,7 +1167,7 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /The transaction has operations that are not of type 'manageData'/, ); }); @@ -1208,7 +1206,7 @@ describe("Utils", function () { ); expect(() => - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( challenge, serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -1216,7 +1214,7 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /'web_auth_domain' operation value does not match testanchor.stellar.org/, ); }); @@ -1255,7 +1253,7 @@ describe("Utils", function () { ); expect(() => - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( challenge, serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -1263,7 +1261,7 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /The transaction has operations that are unrecognized/, ); }); @@ -1295,7 +1293,7 @@ describe("Utils", function () { ); expect( - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( challenge, serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -1344,7 +1342,7 @@ describe("Utils", function () { ); expect( - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( challenge, serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -1393,7 +1391,7 @@ describe("Utils", function () { ); expect( - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( challenge, serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -1438,7 +1436,7 @@ describe("Utils", function () { transaction.sign(serverKP); const challenge = transaction.toEnvelope().toXDR("base64").toString(); - StellarSdk.Utils.readChallengeTx( + WebAuth.readChallengeTx( challenge, serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -1448,7 +1446,7 @@ describe("Utils", function () { }); }); - describe("Utils.verifyChallengeTxThreshold", function () { + describe("WebAuth.verifyChallengeTxThreshold", function () { beforeEach(function () { this.serverKP = StellarSdk.Keypair.random(); this.clientKP1 = StellarSdk.Keypair.random(); @@ -1496,7 +1494,7 @@ describe("Utils", function () { const challenge = transaction.toEnvelope().toXDR("base64").toString(); expect(() => - StellarSdk.Utils.verifyChallengeTxThreshold( + WebAuth.verifyChallengeTxThreshold( challenge, this.serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -1506,13 +1504,13 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, "Transaction not signed by server: '" + this.serverKP.publicKey() + "'", ); }); it("successfully validates server and client key meeting threshold", function () { - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( this.serverKP, this.clientKP1.publicKey(), "SDF", @@ -1537,7 +1535,7 @@ describe("Utils", function () { const signerSummary = [newClientSigner(this.clientKP1.publicKey(), 1)]; expect( - StellarSdk.Utils.verifyChallengeTxThreshold( + WebAuth.verifyChallengeTxThreshold( signedChallenge, this.serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -1550,7 +1548,7 @@ describe("Utils", function () { }); it("successfully validates server and multiple client keys, meeting threshold", function () { - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( this.serverKP, this.clientKP1.publicKey(), "SDF", @@ -1578,7 +1576,7 @@ describe("Utils", function () { ]; expect( - StellarSdk.Utils.verifyChallengeTxThreshold( + WebAuth.verifyChallengeTxThreshold( signedChallenge, this.serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -1591,7 +1589,7 @@ describe("Utils", function () { }); it("successfully validates server and multiple client keys, meeting threshold with more keys than needed", function () { - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( this.serverKP, this.clientKP1.publicKey(), "SDF", @@ -1620,7 +1618,7 @@ describe("Utils", function () { ]; expect( - StellarSdk.Utils.verifyChallengeTxThreshold( + WebAuth.verifyChallengeTxThreshold( signedChallenge, this.serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -1639,7 +1637,7 @@ describe("Utils", function () { const unknownSignerType = "?ARPF6NZRR7EEVO7ESIWUDXHAOMM2QSKIQQBJK6I2FB7YKDZES5UCLWD"; - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( this.serverKP, this.clientKP1.publicKey(), "SDF", @@ -1671,7 +1669,7 @@ describe("Utils", function () { ]; expect( - StellarSdk.Utils.verifyChallengeTxThreshold( + WebAuth.verifyChallengeTxThreshold( signedChallenge, this.serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -1684,7 +1682,7 @@ describe("Utils", function () { }); it("throws an error if multiple client keys were not enough to meet the threshold", function () { - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( this.serverKP, this.clientKP1.publicKey(), "SDF", @@ -1713,7 +1711,7 @@ describe("Utils", function () { ]; expect(() => - StellarSdk.Utils.verifyChallengeTxThreshold( + WebAuth.verifyChallengeTxThreshold( signedChallenge, this.serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -1723,13 +1721,13 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, `signers with weight 3 do not meet threshold ${threshold}"`, ); }); it("throws an error if an unrecognized (not from the signerSummary) key has signed the transaction", function () { - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( this.serverKP, this.clientKP1.publicKey(), "SDF", @@ -1757,7 +1755,7 @@ describe("Utils", function () { ]; expect(() => - StellarSdk.Utils.verifyChallengeTxThreshold( + WebAuth.verifyChallengeTxThreshold( signedChallenge, this.serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -1767,13 +1765,13 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /Transaction has unrecognized signatures/, ); }); it("throws an error if the signerSummary is empty", function () { - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( this.serverKP, this.clientKP1.publicKey(), "SDF", @@ -1797,7 +1795,7 @@ describe("Utils", function () { const threshold = 10; expect(() => - StellarSdk.Utils.verifyChallengeTxThreshold( + WebAuth.verifyChallengeTxThreshold( signedChallenge, this.serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -1807,13 +1805,13 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /No verifiable client signers provided, at least one G... address must be provided/, ); }); }); - describe("Utils.verifyChallengeTxSigners", function () { + describe("WebAuth.verifyChallengeTxSigners", function () { beforeEach(function () { this.serverKP = StellarSdk.Keypair.random(); this.clientKP1 = StellarSdk.Keypair.random(); @@ -1844,7 +1842,7 @@ describe("Utils", function () { }); it("successfully validates server and client master key signatures in the transaction", function () { - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( this.serverKP, this.clientKP1.publicKey(), "SDF", @@ -1867,7 +1865,7 @@ describe("Utils", function () { .toString(); expect( - StellarSdk.Utils.verifyChallengeTxSigners( + WebAuth.verifyChallengeTxSigners( signedChallenge, this.serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -1895,7 +1893,7 @@ describe("Utils", function () { .toString(); expect(() => - StellarSdk.Utils.verifyChallengeTxSigners( + WebAuth.verifyChallengeTxSigners( invalidsServerSignedChallenge, this.serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -1904,13 +1902,13 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, "Transaction not signed by server: '" + this.serverKP.publicKey() + "'", ); }); it("throws an error if the list of signers is empty", function () { - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( this.serverKP, this.clientKP1.publicKey(), "SDF", @@ -1922,7 +1920,7 @@ describe("Utils", function () { clock.tick(200); expect(() => - StellarSdk.Utils.verifyChallengeTxSigners( + WebAuth.verifyChallengeTxSigners( challenge, this.serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -1931,13 +1929,13 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /No verifiable client signers provided, at least one G... address must be provided/, ); }); it("throws an error if none of the given signers have signed the transaction", function () { - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( this.serverKP, this.clientKP1.publicKey(), "SDF", @@ -1962,7 +1960,7 @@ describe("Utils", function () { .toString(); expect(() => - StellarSdk.Utils.verifyChallengeTxSigners( + WebAuth.verifyChallengeTxSigners( signedChallenge, this.serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -1971,13 +1969,13 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /None of the given signers match the transaction signatures/, ); }); it("successfully validates server and multiple client signers in the transaction", function () { - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( this.serverKP, this.clientKP1.publicKey(), "SDF", @@ -2002,7 +2000,7 @@ describe("Utils", function () { .toString(); expect( - StellarSdk.Utils.verifyChallengeTxSigners( + WebAuth.verifyChallengeTxSigners( signedChallenge, this.serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -2014,7 +2012,7 @@ describe("Utils", function () { }); it("successfully validates server and multiple client signers, in reverse order", function () { - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( this.serverKP, this.clientKP1.publicKey(), "SDF", @@ -2039,7 +2037,7 @@ describe("Utils", function () { .toString(); expect( - StellarSdk.Utils.verifyChallengeTxSigners( + WebAuth.verifyChallengeTxSigners( signedChallenge, this.serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -2051,7 +2049,7 @@ describe("Utils", function () { }); it("successfully validates server and non-masterkey client signer", function () { - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( this.serverKP, this.clientKP1.publicKey(), "SDF", @@ -2074,7 +2072,7 @@ describe("Utils", function () { .toString(); expect( - StellarSdk.Utils.verifyChallengeTxSigners( + WebAuth.verifyChallengeTxSigners( signedChallenge, this.serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -2086,7 +2084,7 @@ describe("Utils", function () { }); it("successfully validates server and non-master key client signer, ignoring extra signer", function () { - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( this.serverKP, this.clientKP1.publicKey(), "SDF", @@ -2109,7 +2107,7 @@ describe("Utils", function () { .toString(); expect( - StellarSdk.Utils.verifyChallengeTxSigners( + WebAuth.verifyChallengeTxSigners( signedChallenge, this.serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -2121,7 +2119,7 @@ describe("Utils", function () { }); it("throws an error if no client but instead the server has signed the transaction", function () { - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( this.serverKP, this.clientKP1.publicKey(), "SDF", @@ -2144,7 +2142,7 @@ describe("Utils", function () { .toString(); expect(() => - StellarSdk.Utils.verifyChallengeTxSigners( + WebAuth.verifyChallengeTxSigners( signedChallenge, this.serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -2153,13 +2151,13 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /None of the given signers match the transaction signatures/, ); }); it("successfully validates server and non-masterkey client signer, ignoring duplicated client signers", function () { - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( this.serverKP, this.clientKP1.publicKey(), "SDF", @@ -2182,7 +2180,7 @@ describe("Utils", function () { .toString(); expect( - StellarSdk.Utils.verifyChallengeTxSigners( + WebAuth.verifyChallengeTxSigners( signedChallenge, this.serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -2200,7 +2198,7 @@ describe("Utils", function () { const unknownSignerType = "?ARPF6NZRR7EEVO7ESIWUDXHAOMM2QSKIQQBJK6I2FB7YKDZES5UCLWD"; - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( this.serverKP, this.clientKP1.publicKey(), "SDF", @@ -2223,7 +2221,7 @@ describe("Utils", function () { .toString(); expect( - StellarSdk.Utils.verifyChallengeTxSigners( + WebAuth.verifyChallengeTxSigners( signedChallenge, this.serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -2235,7 +2233,7 @@ describe("Utils", function () { }); it("throws an error if duplicated signers have been provided and they haven't actually signed the transaction", function () { - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( this.serverKP, this.clientKP1.publicKey(), "SDF", @@ -2257,7 +2255,7 @@ describe("Utils", function () { .toString(); expect(() => - StellarSdk.Utils.verifyChallengeTxSigners( + WebAuth.verifyChallengeTxSigners( signedChallenge, this.serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -2266,13 +2264,13 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /None of the given signers match the transaction signatures/, ); }); it("throws an error if the same KP has signed the transaction more than once", function () { - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( this.serverKP, this.clientKP1.publicKey(), "SDF", @@ -2295,7 +2293,7 @@ describe("Utils", function () { .toString(); expect(() => - StellarSdk.Utils.verifyChallengeTxSigners( + WebAuth.verifyChallengeTxSigners( signedChallenge, this.serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -2304,13 +2302,13 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /Transaction has unrecognized signatures/, ); }); it("throws an error if the client attempts to verify the transaction with a Seed instead of the Public Key", function () { - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( this.serverKP, this.clientKP1.publicKey(), "SDF", @@ -2333,7 +2331,7 @@ describe("Utils", function () { .toString(); expect(() => - StellarSdk.Utils.verifyChallengeTxSigners( + WebAuth.verifyChallengeTxSigners( signedChallenge, this.serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -2342,7 +2340,7 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /No verifiable client signers provided, at least one G... address must be provided/, ); }); @@ -2365,7 +2363,7 @@ describe("Utils", function () { ]; expect(() => - StellarSdk.Utils.verifyChallengeTxSigners( + WebAuth.verifyChallengeTxSigners( challenge, this.serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -2374,13 +2372,13 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /None of the given signers match the transaction signatures/, ); }); it("throws an error if no public keys were provided to verify signatires", function () { - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( this.serverKP, this.clientKP1.publicKey(), "SDF", @@ -2403,7 +2401,7 @@ describe("Utils", function () { .toString(); expect(() => - StellarSdk.Utils.verifyChallengeTxSigners( + WebAuth.verifyChallengeTxSigners( signedChallenge, this.serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -2412,7 +2410,7 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /No verifiable client signers provided, at least one G... address must be provided/, ); }); @@ -2421,7 +2419,7 @@ describe("Utils", function () { const serverKP = StellarSdk.Keypair.random(); const clientKP = StellarSdk.Keypair.random(); const clientSigningKey = StellarSdk.Keypair.random(); - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( serverKP, clientKP.publicKey(), "SDF", @@ -2448,7 +2446,7 @@ describe("Utils", function () { .toXDR("base64") .toString(); - const signersFound = StellarSdk.Utils.verifyChallengeTxSigners( + const signersFound = WebAuth.verifyChallengeTxSigners( signedChallenge, serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -2464,7 +2462,7 @@ describe("Utils", function () { const serverKP = StellarSdk.Keypair.random(); const clientKP = StellarSdk.Keypair.random(); const clientSigningKeypair = StellarSdk.Keypair.random(); - const challenge = StellarSdk.Utils.buildChallengeTx( + const challenge = WebAuth.buildChallengeTx( serverKP, clientKP.publicKey(), "SDF", @@ -2491,7 +2489,7 @@ describe("Utils", function () { .toString(); expect(() => - StellarSdk.Utils.verifyChallengeTxSigners( + WebAuth.verifyChallengeTxSigners( signedChallenge, serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -2500,7 +2498,7 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /Transaction not signed by the source account of the 'client_domain' ManageData operation/, ); }); @@ -2552,7 +2550,7 @@ describe("Utils", function () { .toString(); expect(() => - StellarSdk.Utils.verifyChallengeTxSigners( + WebAuth.verifyChallengeTxSigners( signedChallenge, serverKP.publicKey(), StellarSdk.Networks.TESTNET, @@ -2561,13 +2559,13 @@ describe("Utils", function () { "testanchor.stellar.org", ), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /Found more than one client_domain operation/, ); }); }); - describe("Utils.verifyTxSignedBy", function () { + describe("WebAuth.verifyTxSignedBy", function () { beforeEach(function () { this.keypair = StellarSdk.Keypair.random(); this.account = new StellarSdk.Account(this.keypair.publicKey(), "-1"); @@ -2587,10 +2585,7 @@ describe("Utils", function () { this.transaction.sign(this.keypair); expect( - StellarSdk.Utils.verifyTxSignedBy( - this.transaction, - this.keypair.publicKey(), - ), + WebAuth.verifyTxSignedBy(this.transaction, this.keypair.publicKey()), ).to.eql(true); }); @@ -2600,7 +2595,7 @@ describe("Utils", function () { let differentKeypair = StellarSdk.Keypair.random(); expect( - StellarSdk.Utils.verifyTxSignedBy( + WebAuth.verifyTxSignedBy( this.transaction, differentKeypair.publicKey(), ), @@ -2609,15 +2604,12 @@ describe("Utils", function () { it("works with an unsigned transaction", function () { expect( - StellarSdk.Utils.verifyTxSignedBy( - this.transaction, - this.keypair.publicKey(), - ), + WebAuth.verifyTxSignedBy(this.transaction, this.keypair.publicKey()), ).to.eql(false); }); }); - describe("Utils.gatherTxSigners", function () { + describe("WebAuth.gatherTxSigners", function () { beforeEach(function () { this.keypair1 = StellarSdk.Keypair.random(); this.keypair2 = StellarSdk.Keypair.random(); @@ -2642,7 +2634,7 @@ describe("Utils", function () { this.keypair2.publicKey(), ]; expect( - StellarSdk.Utils.gatherTxSigners(this.transaction, expectedSignatures), + WebAuth.gatherTxSigners(this.transaction, expectedSignatures), ).to.eql(expectedSignatures); }); @@ -2661,7 +2653,7 @@ describe("Utils", function () { this.keypair2.publicKey(), ]; expect( - StellarSdk.Utils.gatherTxSigners(this.transaction, [ + WebAuth.gatherTxSigners(this.transaction, [ this.keypair1.publicKey(), this.keypair2.publicKey(), ]), @@ -2677,14 +2669,14 @@ describe("Utils", function () { StellarSdk.Keypair.random().publicKey(), ]; - expect( - StellarSdk.Utils.gatherTxSigners(this.transaction, wrongSignatures), - ).to.eql([]); + expect(WebAuth.gatherTxSigners(this.transaction, wrongSignatures)).to.eql( + [], + ); }); it("calling gatherTxSigners with an unsigned transaction will return an empty list", function () { expect( - StellarSdk.Utils.gatherTxSigners(this.transaction, [ + WebAuth.gatherTxSigners(this.transaction, [ this.keypair1.publicKey(), this.keypair2.publicKey(), ]), @@ -2696,12 +2688,12 @@ describe("Utils", function () { const preauthTxHash = "TAQCSRX2RIDJNHFIFHWD63X7D7D6TRT5Y2S6E3TEMXTG5W3OECHZ2OG4"; expect(() => - StellarSdk.Utils.gatherTxSigners(this.transaction, [ + WebAuth.gatherTxSigners(this.transaction, [ preauthTxHash, this.keypair1.publicKey(), ]), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /Signer is not a valid address/, ); }); @@ -2711,12 +2703,12 @@ describe("Utils", function () { const invalidGHash = "GBDIT5GUJ7R5BXO3GJHFXJ6AZ5UQK6MNOIDMPQUSMXLIHTUNR2Q5CAAA"; expect(() => - StellarSdk.Utils.gatherTxSigners(this.transaction, [ + WebAuth.gatherTxSigners(this.transaction, [ invalidGHash, this.keypair1.publicKey(), ]), ).to.throw( - StellarSdk.InvalidSep10ChallengeError, + WebAuth.InvalidChallengeError, /Signer is not a valid address/, ); }); diff --git a/yarn.lock b/yarn.lock index 22c4012ac..d84c763a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -39,25 +39,25 @@ "@babel/highlight" "^7.22.13" chalk "^2.4.2" -"@babel/compat-data@^7.22.20", "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.20.tgz#8df6e96661209623f1975d66c35ffca66f3306d0" - integrity sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw== +"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9", "@babel/compat-data@^7.23.2": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.2.tgz#6a12ced93455827037bfb5ed8492820d60fc32cc" + integrity sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ== "@babel/core@^7.12.3", "@babel/core@^7.23.0", "@babel/core@^7.7.5": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.0.tgz#f8259ae0e52a123eb40f552551e647b506a94d83" - integrity sha512-97z/ju/Jy1rZmDxybphrBuI+jtJjFVoz7Mr9yUQVVVi+DNZE333uFQeMOqcCIy1x3WYBIbWftUSLmbNXNT7qFQ== + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.2.tgz#ed10df0d580fff67c5f3ee70fd22e2e4c90a9f94" + integrity sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ== dependencies: "@ampproject/remapping" "^2.2.0" "@babel/code-frame" "^7.22.13" "@babel/generator" "^7.23.0" "@babel/helper-compilation-targets" "^7.22.15" "@babel/helper-module-transforms" "^7.23.0" - "@babel/helpers" "^7.23.0" + "@babel/helpers" "^7.23.2" "@babel/parser" "^7.23.0" "@babel/template" "^7.22.15" - "@babel/traverse" "^7.23.0" + "@babel/traverse" "^7.23.2" "@babel/types" "^7.23.0" convert-source-map "^2.0.0" debug "^4.1.0" @@ -140,10 +140,10 @@ regexpu-core "^5.3.1" semver "^6.3.1" -"@babel/helper-define-polyfill-provider@^0.4.2": - version "0.4.2" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz#82c825cadeeeee7aad237618ebbe8fa1710015d7" - integrity sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw== +"@babel/helper-define-polyfill-provider@^0.4.3": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz#a71c10f7146d809f4a256c373f462d9bba8cf6ba" + integrity sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug== dependencies: "@babel/helper-compilation-targets" "^7.22.6" "@babel/helper-plugin-utils" "^7.22.5" @@ -208,7 +208,7 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== -"@babel/helper-remap-async-to-generator@^7.22.5", "@babel/helper-remap-async-to-generator@^7.22.9": +"@babel/helper-remap-async-to-generator@^7.22.20", "@babel/helper-remap-async-to-generator@^7.22.5": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz#7b68e1cb4fa964d2996fd063723fb48eca8498e0" integrity sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw== @@ -271,13 +271,13 @@ "@babel/template" "^7.22.15" "@babel/types" "^7.22.19" -"@babel/helpers@^7.23.0": - version "7.23.1" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.1.tgz#44e981e8ce2b9e99f8f0b703f3326a4636c16d15" - integrity sha512-chNpneuK18yW5Oxsr+t553UZzzAs3aZnFm4bxhebsNTeshrC95yA7l5yl7GBAG+JG1rF0F7zzD2EixK9mWSDoA== +"@babel/helpers@^7.23.2": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.2.tgz#2832549a6e37d484286e15ba36a5330483cac767" + integrity sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ== dependencies: "@babel/template" "^7.22.15" - "@babel/traverse" "^7.23.0" + "@babel/traverse" "^7.23.2" "@babel/types" "^7.23.0" "@babel/highlight@^7.22.13": @@ -463,14 +463,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-async-generator-functions@^7.22.15": - version "7.22.15" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.15.tgz#3b153af4a6b779f340d5b80d3f634f55820aefa3" - integrity sha512-jBm1Es25Y+tVoTi5rfd5t1KLmL8ogLKpXszboWOTTtGFGz2RKnQe2yn7HbZ+kb/B8N0FVSGQo874NSlOU1T4+w== +"@babel/plugin-transform-async-generator-functions@^7.23.2": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.2.tgz#054afe290d64c6f576f371ccc321772c8ea87ebb" + integrity sha512-BBYVGxbDVHfoeXbOwcagAkOQAm9NxoTdMGfTqghu1GrvadSaw6iW3Je6IcL5PNOw8VwjxqBECXy50/iCQSY/lQ== dependencies: - "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.20" "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-remap-async-to-generator" "^7.22.9" + "@babel/helper-remap-async-to-generator" "^7.22.20" "@babel/plugin-syntax-async-generators" "^7.8.4" "@babel/plugin-transform-async-to-generator@^7.22.5": @@ -489,7 +489,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-block-scoping@^7.22.15": +"@babel/plugin-transform-block-scoping@^7.23.0": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.0.tgz#8744d02c6c264d82e1a4bc5d2d501fd8aff6f022" integrity sha512-cOsrbmIOXmf+5YbL99/S49Y3j46k/T16b9ml8bm9lP6N9US5iQ2yBK7gpui1pg0V/WMcXdkfKbTb7HXq9u+v4g== @@ -536,7 +536,7 @@ "@babel/helper-plugin-utils" "^7.22.5" "@babel/template" "^7.22.5" -"@babel/plugin-transform-destructuring@^7.22.15": +"@babel/plugin-transform-destructuring@^7.23.0": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.0.tgz#6447aa686be48b32eaf65a73e0e2c0bd010a266c" integrity sha512-vaMdgNXFkYrB+8lbgniSYWHsgqK5gjaMNcc84bMIOMRLH0L9AqYq3hwMdvnyqj1OPqea8UtjPEuS/DCenah1wg== @@ -628,7 +628,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-modules-amd@^7.22.5": +"@babel/plugin-transform-modules-amd@^7.23.0": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.0.tgz#05b2bc43373faa6d30ca89214731f76f966f3b88" integrity sha512-xWT5gefv2HGSm4QHtgc1sYPbseOyf+FFDo2JbpE25GWl5BqTGO9IMwTYJRoIdjsF85GE+VegHxSCUt5EvoYTAw== @@ -636,7 +636,7 @@ "@babel/helper-module-transforms" "^7.23.0" "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-transform-modules-commonjs@^7.22.15", "@babel/plugin-transform-modules-commonjs@^7.23.0": +"@babel/plugin-transform-modules-commonjs@^7.23.0": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.0.tgz#b3dba4757133b2762c00f4f94590cf6d52602481" integrity sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ== @@ -645,7 +645,7 @@ "@babel/helper-plugin-utils" "^7.22.5" "@babel/helper-simple-access" "^7.22.5" -"@babel/plugin-transform-modules-systemjs@^7.22.11": +"@babel/plugin-transform-modules-systemjs@^7.23.0": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.0.tgz#77591e126f3ff4132a40595a6cccd00a6b60d160" integrity sha512-qBej6ctXZD2f+DhlOC9yO47yEYgUh5CZNz/aBoH4j/3NOlRfJXJbY7xDQCqQVf9KbrqGzIWER1f23doHGrIHFg== @@ -721,7 +721,7 @@ "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" -"@babel/plugin-transform-optional-chaining@^7.22.15": +"@babel/plugin-transform-optional-chaining@^7.22.15", "@babel/plugin-transform-optional-chaining@^7.23.0": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.0.tgz#73ff5fc1cf98f542f09f29c0631647d8ad0be158" integrity sha512-sBBGXbLJjxTzLBF5rFWaikMnOGOk/BmK6vVByIdEggZ7Vn6CvWXZyRkkLFK6WE0IF8jSliyOkUN6SScFgzCM0g== @@ -855,11 +855,11 @@ "@babel/helper-plugin-utils" "^7.22.5" "@babel/preset-env@^7.22.20": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.22.20.tgz#de9e9b57e1127ce0a2f580831717f7fb677ceedb" - integrity sha512-11MY04gGC4kSzlPHRfvVkNAZhUxOvm7DCJ37hPDnUENwe06npjIRAfInEMTGSb4LZK5ZgDFkv5hw0lGebHeTyg== + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.23.2.tgz#1f22be0ff0e121113260337dbc3e58fafce8d059" + integrity sha512-BW3gsuDD+rvHL2VO2SjAUNTBe5YrjsTiDyqamPDWY723na3/yPQ65X5oQkFVJZ0o50/2d+svm1rkPoJeR1KxVQ== dependencies: - "@babel/compat-data" "^7.22.20" + "@babel/compat-data" "^7.23.2" "@babel/helper-compilation-targets" "^7.22.15" "@babel/helper-plugin-utils" "^7.22.5" "@babel/helper-validator-option" "^7.22.15" @@ -885,15 +885,15 @@ "@babel/plugin-syntax-top-level-await" "^7.14.5" "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" "@babel/plugin-transform-arrow-functions" "^7.22.5" - "@babel/plugin-transform-async-generator-functions" "^7.22.15" + "@babel/plugin-transform-async-generator-functions" "^7.23.2" "@babel/plugin-transform-async-to-generator" "^7.22.5" "@babel/plugin-transform-block-scoped-functions" "^7.22.5" - "@babel/plugin-transform-block-scoping" "^7.22.15" + "@babel/plugin-transform-block-scoping" "^7.23.0" "@babel/plugin-transform-class-properties" "^7.22.5" "@babel/plugin-transform-class-static-block" "^7.22.11" "@babel/plugin-transform-classes" "^7.22.15" "@babel/plugin-transform-computed-properties" "^7.22.5" - "@babel/plugin-transform-destructuring" "^7.22.15" + "@babel/plugin-transform-destructuring" "^7.23.0" "@babel/plugin-transform-dotall-regex" "^7.22.5" "@babel/plugin-transform-duplicate-keys" "^7.22.5" "@babel/plugin-transform-dynamic-import" "^7.22.11" @@ -905,9 +905,9 @@ "@babel/plugin-transform-literals" "^7.22.5" "@babel/plugin-transform-logical-assignment-operators" "^7.22.11" "@babel/plugin-transform-member-expression-literals" "^7.22.5" - "@babel/plugin-transform-modules-amd" "^7.22.5" - "@babel/plugin-transform-modules-commonjs" "^7.22.15" - "@babel/plugin-transform-modules-systemjs" "^7.22.11" + "@babel/plugin-transform-modules-amd" "^7.23.0" + "@babel/plugin-transform-modules-commonjs" "^7.23.0" + "@babel/plugin-transform-modules-systemjs" "^7.23.0" "@babel/plugin-transform-modules-umd" "^7.22.5" "@babel/plugin-transform-named-capturing-groups-regex" "^7.22.5" "@babel/plugin-transform-new-target" "^7.22.5" @@ -916,7 +916,7 @@ "@babel/plugin-transform-object-rest-spread" "^7.22.15" "@babel/plugin-transform-object-super" "^7.22.5" "@babel/plugin-transform-optional-catch-binding" "^7.22.11" - "@babel/plugin-transform-optional-chaining" "^7.22.15" + "@babel/plugin-transform-optional-chaining" "^7.23.0" "@babel/plugin-transform-parameters" "^7.22.15" "@babel/plugin-transform-private-methods" "^7.22.5" "@babel/plugin-transform-private-property-in-object" "^7.22.11" @@ -933,10 +933,10 @@ "@babel/plugin-transform-unicode-regex" "^7.22.5" "@babel/plugin-transform-unicode-sets-regex" "^7.22.5" "@babel/preset-modules" "0.1.6-no-external-plugins" - "@babel/types" "^7.22.19" - babel-plugin-polyfill-corejs2 "^0.4.5" - babel-plugin-polyfill-corejs3 "^0.8.3" - babel-plugin-polyfill-regenerator "^0.5.2" + "@babel/types" "^7.23.0" + babel-plugin-polyfill-corejs2 "^0.4.6" + babel-plugin-polyfill-corejs3 "^0.8.5" + babel-plugin-polyfill-regenerator "^0.5.3" core-js-compat "^3.31.0" semver "^6.3.1" @@ -950,9 +950,9 @@ esutils "^2.0.2" "@babel/preset-typescript@^7.23.0": - version "7.23.0" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.23.0.tgz#cc6602d13e7e5b2087c811912b87cf937a9129d9" - integrity sha512-6P6VVa/NM/VlAYj5s2Aq/gdVg8FSENCg3wlZ6Qau9AcPaoF5LbN1nyGlR9DTRIw9PpxI94e+ReydsJHcjwAweg== + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.23.2.tgz#c8de488130b7081f7e1482936ad3de5b018beef4" + integrity sha512-u4UJc1XsS1GhIGteM8rnGiIvf9rJpiVgMEeCnwlLA7WJPC+jcXWJAGxYmeqs5hOZD8BbAfnV5ezBOxQbb4OUxA== dependencies: "@babel/helper-plugin-utils" "^7.22.5" "@babel/helper-validator-option" "^7.22.15" @@ -977,9 +977,9 @@ integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== "@babel/runtime@^7.8.4": - version "7.23.1" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.1.tgz#72741dc4d413338a91dcb044a86f3c0bc402646d" - integrity sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g== + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885" + integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg== dependencies: regenerator-runtime "^0.14.0" @@ -992,7 +992,7 @@ "@babel/parser" "^7.22.15" "@babel/types" "^7.22.15" -"@babel/traverse@^7.23.0": +"@babel/traverse@^7.23.2": version "7.23.2" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== @@ -1122,17 +1122,17 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.50.0": - version "8.50.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.50.0.tgz#9e93b850f0f3fa35f5fa59adfd03adae8488e484" - integrity sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ== +"@eslint/js@8.52.0": + version "8.52.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.52.0.tgz#78fe5f117840f69dc4a353adf9b9cd926353378c" + integrity sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA== -"@humanwhocodes/config-array@^0.11.11": - version "0.11.11" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.11.tgz#88a04c570dbbc7dd943e4712429c3df09bc32844" - integrity sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA== +"@humanwhocodes/config-array@^0.11.13": + version "0.11.13" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz#075dc9684f40a531d9b26b0822153c1e832ee297" + integrity sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ== dependencies: - "@humanwhocodes/object-schema" "^1.2.1" + "@humanwhocodes/object-schema" "^2.0.1" debug "^4.1.1" minimatch "^3.0.5" @@ -1141,10 +1141,10 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/object-schema@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" - integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@humanwhocodes/object-schema@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz#e5211452df060fa8522b55c7b3c0c4d1981cb044" + integrity sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw== "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" @@ -1227,9 +1227,9 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.19" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811" - integrity sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw== + version "0.3.20" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f" + integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q== dependencies: "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" @@ -1375,10 +1375,10 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== -"@types/chai@4": - version "4.3.6" - resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.6.tgz#7b489e8baf393d5dd1266fb203ddd4ea941259e6" - integrity sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw== +"@types/chai@4", "@types/chai@^4.3.6": + version "4.3.9" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.9.tgz#144d762491967db8c6dea38e03d2206c2623feec" + integrity sha512-69TtiDzu0bcmKQv3yg1Zx409/Kd7r0b5F1PfpYJfSHzLGtB53547V4u+9iqKYsTu/O2ai6KTb0TInNpvuQ3qmg== "@types/cookie@^0.4.1": version "0.4.1" @@ -1386,71 +1386,71 @@ integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== "@types/cookiejar@*": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8" - integrity sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog== + version "2.1.3" + resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.3.tgz#c54976fb8f3a32ea8da844f59f0374dd39656e13" + integrity sha512-LZ8SD3LpNmLMDLkG2oCBjZg+ETnx6XdCjydUE0HwojDmnDfDUnhMKKbtth1TZh+hzcqb03azrYWoXLS8sMXdqg== "@types/cors@^2.8.12": - version "2.8.14" - resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.14.tgz#94eeb1c95eda6a8ab54870a3bf88854512f43a92" - integrity sha512-RXHUvNWYICtbP6s18PnOCaqToK8y14DnLd75c6HfyKf228dxy7pHNOQkxPtvXKp/hINFMDjbYzsj63nnpPMSRQ== + version "2.8.15" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.15.tgz#eb143aa2f8807ddd78e83cbff141bbedd91b60ee" + integrity sha512-n91JxbNLD8eQIuXDIChAN1tCKNWCEgpceU9b7ZMbFA+P+Q4yIeh80jizFLEvolRPc1ES0VdwFlGv+kJTSirogw== dependencies: "@types/node" "*" "@types/detect-node@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/detect-node/-/detect-node-2.0.0.tgz#696e024ddd105c72bbc6a2e3f97902a2886f2c3f" - integrity sha512-+BozjlbPTACYITf1PWf62HLtDV79HbmZosUN1mv1gGrnjDCRwBXkDKka1sf6YQJvspmfPXVcy+X6tFW62KteeQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/detect-node/-/detect-node-2.0.1.tgz#d01889b4a7bbff00ab21ab277884c3a29f7453f0" + integrity sha512-y7AlL0WTosxlajspWsDr2rxjgTTk+nvBFgr+9DF1WMa8laGfEKWKZBcK0gC1t5SVHz6dm7wcKyW9TGfBvPM6+g== "@types/eslint-scope@^3.7.3": - version "3.7.5" - resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.5.tgz#e28b09dbb1d9d35fdfa8a884225f00440dfc5a3e" - integrity sha512-JNvhIEyxVW6EoMIFIvj93ZOywYFatlpu9deeH6eSx6PE3WHYvHaQtmHmQeNw7aA81bYGBPPQqdtBm6b1SsQMmA== + version "3.7.6" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.6.tgz#585578b368ed170e67de8aae7b93f54a1b2fdc26" + integrity sha512-zfM4ipmxVKWdxtDaJ3MP3pBurDXOCoyjvlpE3u6Qzrmw4BPbfm4/ambIeTk/r/J0iq/+2/xp0Fmt+gFvXJY2PQ== dependencies: "@types/eslint" "*" "@types/estree" "*" "@types/eslint@*", "@types/eslint@^8.37.0": - version "8.44.3" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.44.3.tgz#96614fae4875ea6328f56de38666f582d911d962" - integrity sha512-iM/WfkwAhwmPff3wZuPLYiHX18HI24jU8k1ZSH7P8FHwxTjZ2P6CoX2wnF43oprR+YXJM6UUxATkNvyv/JHd+g== + version "8.44.6" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.44.6.tgz#60e564551966dd255f4c01c459f0b4fb87068603" + integrity sha512-P6bY56TVmX8y9J87jHNgQh43h6VVU+6H7oN7hgvivV81K2XY8qJZ5vqPy/HdUoVIelii2kChYVzQanlswPWVFw== dependencies: "@types/estree" "*" "@types/json-schema" "*" "@types/estree@*", "@types/estree@^1.0.0": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.2.tgz#ff02bc3dc8317cd668dfec247b750ba1f1d62453" - integrity sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA== + version "1.0.3" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.3.tgz#2be19e759a3dd18c79f9f436bd7363556c1a73dd" + integrity sha512-CS2rOaoQ/eAgAfcTfq6amKG7bsN+EMcgGY4FAFQdvSj2y1ixvOZTUA9mOtCai7E1SYu283XNw7urKK30nP3wkQ== "@types/eventsource@^1.1.12": - version "1.1.12" - resolved "https://registry.yarnpkg.com/@types/eventsource/-/eventsource-1.1.12.tgz#ceed409a2a19ef8ecb44545d9546f74dc448f426" - integrity sha512-KlVguyxdoO8VkAhOMwOemK+NhFAg0gOwJHgimrWJUgM6LrdVW2nLa+d47WVWQcs8feRn0eeP+5yUDmDfzLBjRA== + version "1.1.14" + resolved "https://registry.yarnpkg.com/@types/eventsource/-/eventsource-1.1.14.tgz#b5b115b19f3a392a6c29331486bc88dcb4e8a4e2" + integrity sha512-WiPIkZ5fuhTkeaVaPKbaP6vHuTX9FHnFNTrkSbm+Uf6g4TH3YNbdfw5/1oLzKIWsQRbrvSiByO2nPSxjr5/cgQ== "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" - integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#fdfdd69fa16d530047d9963635bd77c71a08c068" + integrity sha512-zONci81DZYCZjiLe0r6equvZut0b+dBRPBN5kBDjsONnutYNtJMoWQ9uR2RkL1gLG9NMTzvf+29e5RFfPbeKhQ== "@types/istanbul-lib-report@*": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#412e0725ef41cde73bfa03e0e833eaff41e0fd63" - integrity sha512-gPQuzaPR5h/djlAv2apEG1HVOyj1IUs7GpfMZixU0/0KXT3pm64ylHuMUI1/Akh+sq/iikxg6Z2j+fcMDXaaTQ== + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.2.tgz#394798d5f727402eb5ec99eb9618ffcd2b7645a1" + integrity sha512-8toY6FgdltSdONav1XtUHl4LN1yTmLza+EuDazb/fEmRNCwjyqNVIQWs2IfC74IqjHkREs/nQ2FWq5kZU9IC0w== dependencies: "@types/istanbul-lib-coverage" "*" "@types/istanbul-reports@^3.0.0": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.2.tgz#edc8e421991a3b4df875036d381fc0a5a982f549" - integrity sha512-kv43F9eb3Lhj+lr/Hn6OcLCs/sSM8bt+fIaP11rCYngfV6NVjzWXJ17owQtDQTL9tQ8WSLUrGsSJ6rJz0F1w1A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.3.tgz#0313e2608e6d6955d195f55361ddeebd4b74c6e7" + integrity sha512-1nESsePMBlf0RPRffLZi5ujYh7IH1BWL4y9pr+Bn3cJBdxz+RTP8bUFljLz9HvzhhOSWKdyBZ4DIivdL6rvgZg== dependencies: "@types/istanbul-lib-report" "*" "@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": - version "7.0.13" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.13.tgz#02c24f4363176d2d18fc8b70b9f3c54aba178a85" - integrity sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ== + version "7.0.14" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.14.tgz#74a97a5573980802f32c8e47b663530ab3b6b7d1" + integrity sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw== "@types/json5@^0.0.29": version "0.0.29" @@ -1458,14 +1458,14 @@ integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== "@types/linkify-it@*": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.3.tgz#15a0712296c5041733c79efe233ba17ae5a7587b" - integrity sha512-pTjcqY9E4nOI55Wgpz7eiI8+LzdYnw3qxXCfHyBDdPbYvbyLgWLJGh8EdPvqawwMK1Uo1794AUkkR38Fr0g+2g== + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.4.tgz#def6a9bb0ce78140860602f16ace37a9997f086a" + integrity sha512-hPpIeeHb/2UuCw06kSNAOVWgehBLXEo0/fUs0mw3W2qhqX89PI2yvok83MnuctYGCPrabGIoi0fFso4DQ+sNUQ== "@types/lodash@^4.14.199": - version "4.14.199" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.199.tgz#c3edb5650149d847a277a8961a7ad360c474e9bf" - integrity sha512-Vrjz5N5Ia4SEzWWgIVwnHNEnb1UE1XMkvY5DGXrAeOGE9imk0hgTHh5GyDjLDJi9OTCn9oo9dXH1uToK1VRfrg== + version "4.14.200" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.200.tgz#435b6035c7eba9cdf1e039af8212c9e9281e7149" + integrity sha512-YI/M/4HRImtNf3pJgbF+W6FrXovqj+T+/HpENLTooK9PnkacBsDpeP3IpHab40CClUfhNmdM2WTNP2sa2dni5Q== "@types/markdown-it@^12.2.3": version "12.2.3" @@ -1476,14 +1476,21 @@ "@types/mdurl" "*" "@types/mdurl@*": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.3.tgz#d0aefccdd1a96f4bec76047d6b314601f0b0f3de" - integrity sha512-T5k6kTXak79gwmIOaDF2UUQXFbnBE0zBUzF20pz7wDYu0RQMzWg+Ml/Pz50214NsFHBITkoi5VtdjFZnJ2ijjA== + version "1.0.4" + resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.4.tgz#574bfbec51eb41ab5f444116c8555bc4347feba5" + integrity sha512-ARVxjAEX5TARFRzpDRVC6cEk0hUIXCCwaMhz8y7S1/PxU6zZS1UMjyobz7q4w/D/R552r4++EhwmXK1N2rAy0A== + +"@types/mocha@^10.0.2": + version "10.0.3" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.3.tgz#4804fe9cd39da26eb62fa65c15ea77615a187812" + integrity sha512-RsOPImTriV/OE4A9qKjMtk2MnXiuLLbcO3nCXK+kvq4nr0iMfFgpjaX3MPLb6f7+EL1FGSelYvuJMV6REH+ZPQ== "@types/node@*", "@types/node@>=10.0.0", "@types/node@^20.8.2": - version "20.8.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.2.tgz#d76fb80d87d0d8abfe334fc6d292e83e5524efc4" - integrity sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w== + version "20.8.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.7.tgz#ad23827850843de973096edfc5abc9e922492a25" + integrity sha512-21TKHHh3eUHIi2MloeptJWALuCu5H7HQTdTrWIFReA8ad+aggoX+lRes3ex7/FtpC+sVUpFMQ+QTfYr74mruiQ== + dependencies: + undici-types "~5.25.1" "@types/node@^14.14.35": version "14.18.63" @@ -1491,21 +1498,33 @@ integrity sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ== "@types/parsimmon@^1.10.1": - version "1.10.7" - resolved "https://registry.yarnpkg.com/@types/parsimmon/-/parsimmon-1.10.7.tgz#6100f80a3ac6b5ea371e8c940e85c03fc080ec42" - integrity sha512-QnO7brOMB4XCVJzU0GZAYhpay7CZLiXowKBOyAmiRcJ4SIGlrh6/cfWdTod+yfSsyli9tx7aunwQij50yHX9Fg== + version "1.10.8" + resolved "https://registry.yarnpkg.com/@types/parsimmon/-/parsimmon-1.10.8.tgz#308ad54da883f4158ca8af960230542a5ab8c24d" + integrity sha512-i6oOxO9QKaqwMdMnnagvtVduc0ii9rl+BkNdAdSDhN27k9ryP8Fm6S9bcvNtuauK7PwTdoxCRorPcJvyGqwHgQ== "@types/randombytes@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@types/randombytes/-/randombytes-2.0.1.tgz#9cd250685fcc5897ff0c2370856604519418d338" - integrity sha512-kWMqPyxpTUTofwbGN47MWddBFiJnWJlfLBdDg2NvmZSKHOmKY9ujVA3PIfBgXcIHTCpsqoQqYudBwanFXzGD9A== + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/randombytes/-/randombytes-2.0.2.tgz#646831f06b295b086ea227d33089f42bc88e90d5" + integrity sha512-B7C5oKZppg1QzPbcb7uGAVge3Up+0HfSLgMDd4Hx2nCdf2JrjTzuIV5m12C11eQdpnPvNoTT1LK/0XCkHGABdw== dependencies: "@types/node" "*" "@types/semver@^7.3.12": - version "7.5.3" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.3.tgz#9a726e116beb26c24f1ccd6850201e1246122e04" - integrity sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw== + version "7.5.4" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.4.tgz#0a41252ad431c473158b22f9bfb9a63df7541cff" + integrity sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ== + +"@types/sinon@^10.0.19": + version "10.0.20" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.20.tgz#f1585debf4c0d99f9938f4111e5479fb74865146" + integrity sha512-2APKKruFNCAZgx3daAyACGzWuJ028VVCUDk6o2rw/Z4PXT0ogwdV4KUegW0MwVs0Zu59auPXbbuBJHF12Sx1Eg== + dependencies: + "@types/sinonjs__fake-timers" "*" + +"@types/sinonjs__fake-timers@*": + version "8.1.4" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.4.tgz#663bb44e01f6bae4eaae3719d8b037411217c992" + integrity sha512-GDV68H0mBSN449sa5HEj51E0wfpVQb8xNSMzxf/PrypMFcLTMwJMOM/cgXiv71Mq5drkOQmUGvL1okOZcu6RrQ== "@types/superagent@4.1.13": version "4.1.13" @@ -1516,19 +1535,19 @@ "@types/node" "*" "@types/urijs@^1.19.20": - version "1.19.20" - resolved "https://registry.yarnpkg.com/@types/urijs/-/urijs-1.19.20.tgz#7ea4254f4c2cdbd7d34e47d483e76fa1b81e19a4" - integrity sha512-77Mq/2BeHU894J364dUv9tSwxxyCLtcX228Pc8TwZpP5bvOoMns+gZoftp3LYl3FBH8vChpWbuagKGiMki2c1A== + version "1.19.22" + resolved "https://registry.yarnpkg.com/@types/urijs/-/urijs-1.19.22.tgz#188c573007001de3be3983af5437727bf3042dd6" + integrity sha512-qnYBwfN7O/+i6E1Kr8JaCKsrCLpRCiQ1XxkSxNIYuJ/5Aagt0+HlMX78DJMUrNzDULMz0eu2gcprlxJaDtACOw== "@types/yargs-parser@*": - version "21.0.1" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.1.tgz#07773d7160494d56aa882d7531aac7319ea67c3b" - integrity sha512-axdPBuLuEJt0c4yI5OZssC19K2Mq1uKdrfZBzuxLvaztgqUtFYZUNw7lETExPYJR9jdEoIg4mb7RQKRQzOkeGQ== + version "21.0.2" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.2.tgz#7bd04c5da378496ef1695a1008bf8f71847a8b8b" + integrity sha512-5qcvofLPbfjmBfKaLfj/+f+Sbd6pN4zl7w7VSVI5uz7m9QZTuB2aZAa2uo1wHFBNN2x6g/SoTkXmd8mQnQF2Cw== "@types/yargs@^17.0.8": - version "17.0.26" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.26.tgz#388e5002a8b284ad7b4599ba89920a6d74d8d79a" - integrity sha512-Y3vDy2X6zw/ZCumcwLpdhM5L7jmyGpmBCTYMHDLqT2IKVMYRRLdv6ZakA+wxhra6Z/3bwhNbNl9bDGXaFU+6rw== + version "17.0.29" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.29.tgz#06aabc72497b798c643c812a8b561537fea760cf" + integrity sha512-nacjqA3ee9zRF/++a3FUY1suHTFKZeHba2n8WeDw9cCVdmzmHpIxyzOJBcpHvvEmS8E9KqWlSnWHUkOrkhWcvA== dependencies: "@types/yargs-parser" "*" @@ -1559,14 +1578,14 @@ debug "^4.3.4" "@typescript-eslint/parser@^6.7.4": - version "6.7.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.7.4.tgz#23d1dd4fe5d295c7fa2ab651f5406cd9ad0bd435" - integrity sha512-I5zVZFY+cw4IMZUeNCU7Sh2PO5O57F7Lr0uyhgCJmhN/BuTlnc55KxPonR4+EM3GBdfiCyGZye6DgMjtubQkmA== - dependencies: - "@typescript-eslint/scope-manager" "6.7.4" - "@typescript-eslint/types" "6.7.4" - "@typescript-eslint/typescript-estree" "6.7.4" - "@typescript-eslint/visitor-keys" "6.7.4" + version "6.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.9.0.tgz#2b402cadeadd3f211c25820e5433413347b27391" + integrity sha512-GZmjMh4AJ/5gaH4XF2eXA8tMnHWP+Pm1mjQR2QN4Iz+j/zO04b9TOvJYOX2sCNIQHtRStKTxRY1FX7LhpJT4Gw== + dependencies: + "@typescript-eslint/scope-manager" "6.9.0" + "@typescript-eslint/types" "6.9.0" + "@typescript-eslint/typescript-estree" "6.9.0" + "@typescript-eslint/visitor-keys" "6.9.0" debug "^4.3.4" "@typescript-eslint/scope-manager@5.62.0": @@ -1577,13 +1596,13 @@ "@typescript-eslint/types" "5.62.0" "@typescript-eslint/visitor-keys" "5.62.0" -"@typescript-eslint/scope-manager@6.7.4": - version "6.7.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.7.4.tgz#a484a17aa219e96044db40813429eb7214d7b386" - integrity sha512-SdGqSLUPTXAXi7c3Ob7peAGVnmMoGzZ361VswK2Mqf8UOYcODiYvs8rs5ILqEdfvX1lE7wEZbLyELCW+Yrql1A== +"@typescript-eslint/scope-manager@6.9.0": + version "6.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.9.0.tgz#2626e9a7fe0e004c3e25f3b986c75f584431134e" + integrity sha512-1R8A9Mc39n4pCCz9o79qRO31HGNDvC7UhPhv26TovDsWPBDx+Sg3rOZdCELIA3ZmNoWAuxaMOT7aWtGRSYkQxw== dependencies: - "@typescript-eslint/types" "6.7.4" - "@typescript-eslint/visitor-keys" "6.7.4" + "@typescript-eslint/types" "6.9.0" + "@typescript-eslint/visitor-keys" "6.9.0" "@typescript-eslint/type-utils@5.62.0": version "5.62.0" @@ -1600,10 +1619,10 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== -"@typescript-eslint/types@6.7.4": - version "6.7.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.7.4.tgz#5d358484d2be986980c039de68e9f1eb62ea7897" - integrity sha512-o9XWK2FLW6eSS/0r/tgjAGsYasLAnOWg7hvZ/dGYSSNjCh+49k5ocPN8OmG5aZcSJ8pclSOyVKP2x03Sj+RrCA== +"@typescript-eslint/types@6.9.0": + version "6.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.9.0.tgz#86a0cbe7ac46c0761429f928467ff3d92f841098" + integrity sha512-+KB0lbkpxBkBSiVCuQvduqMJy+I1FyDbdwSpM3IoBS7APl4Bu15lStPjgBIdykdRqQNYqYNMa8Kuidax6phaEw== "@typescript-eslint/typescript-estree@5.62.0", "@typescript-eslint/typescript-estree@^5.55.0": version "5.62.0" @@ -1618,13 +1637,13 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@6.7.4": - version "6.7.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.4.tgz#f2baece09f7bb1df9296e32638b2e1130014ef1a" - integrity sha512-ty8b5qHKatlNYd9vmpHooQz3Vki3gG+3PchmtsA4TgrZBKWHNjWfkQid7K7xQogBqqc7/BhGazxMD5vr6Ha+iQ== +"@typescript-eslint/typescript-estree@6.9.0": + version "6.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.9.0.tgz#d0601b245be873d8fe49f3737f93f8662c8693d4" + integrity sha512-NJM2BnJFZBEAbCfBP00zONKXvMqihZCrmwCaik0UhLr0vAgb6oguXxLX1k00oQyD+vZZ+CJn3kocvv2yxm4awQ== dependencies: - "@typescript-eslint/types" "6.7.4" - "@typescript-eslint/visitor-keys" "6.7.4" + "@typescript-eslint/types" "6.9.0" + "@typescript-eslint/visitor-keys" "6.9.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" @@ -1653,14 +1672,19 @@ "@typescript-eslint/types" "5.62.0" eslint-visitor-keys "^3.3.0" -"@typescript-eslint/visitor-keys@6.7.4": - version "6.7.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.4.tgz#80dfecf820fc67574012375859085f91a4dff043" - integrity sha512-pOW37DUhlTZbvph50x5zZCkFn3xzwkGtNoJHzIM3svpiSkJzwOYr/kVBaXmf+RAQiUDs1AHEZVNPg6UJCJpwRA== +"@typescript-eslint/visitor-keys@6.9.0": + version "6.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.9.0.tgz#cc69421c10c4ac997ed34f453027245988164e80" + integrity sha512-dGtAfqjV6RFOtIP8I0B4ZTBRrlTT8NHHlZZSchQx3qReaoDeXhYM++M4So2AgFK9ZB0emRPA6JI1HkafzA2Ibg== dependencies: - "@typescript-eslint/types" "6.7.4" + "@typescript-eslint/types" "6.9.0" eslint-visitor-keys "^3.4.1" +"@ungap/structured-clone@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" + integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== + "@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" @@ -1998,7 +2022,7 @@ array-buffer-byte-length@^1.0.0: call-bind "^1.0.2" is-array-buffer "^3.0.1" -array-includes@^3.1.6: +array-includes@^3.1.7: version "3.1.7" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.7.tgz#8cd2e01b26f7a3086cbc87271593fe921c62abda" integrity sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ== @@ -2014,7 +2038,7 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -array.prototype.findlastindex@^1.2.2: +array.prototype.findlastindex@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz#b37598438f97b579166940814e2c0493a4f50207" integrity sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA== @@ -2025,7 +2049,7 @@ array.prototype.findlastindex@^1.2.2: es-shim-unscopables "^1.0.0" get-intrinsic "^1.2.1" -array.prototype.flat@^1.3.1: +array.prototype.flat@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== @@ -2035,7 +2059,7 @@ array.prototype.flat@^1.3.1: es-abstract "^1.22.1" es-shim-unscopables "^1.0.0" -array.prototype.flatmap@^1.3.1: +array.prototype.flatmap@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== @@ -2166,29 +2190,29 @@ babel-plugin-istanbul@^6.1.1: istanbul-lib-instrument "^5.0.4" test-exclude "^6.0.0" -babel-plugin-polyfill-corejs2@^0.4.5: - version "0.4.5" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz#8097b4cb4af5b64a1d11332b6fb72ef5e64a054c" - integrity sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg== +babel-plugin-polyfill-corejs2@^0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.6.tgz#b2df0251d8e99f229a8e60fc4efa9a68b41c8313" + integrity sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q== dependencies: "@babel/compat-data" "^7.22.6" - "@babel/helper-define-polyfill-provider" "^0.4.2" + "@babel/helper-define-polyfill-provider" "^0.4.3" semver "^6.3.1" -babel-plugin-polyfill-corejs3@^0.8.3: - version "0.8.4" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.4.tgz#1fac2b1dcef6274e72b3c72977ed8325cb330591" - integrity sha512-9l//BZZsPR+5XjyJMPtZSK4jv0BsTO1zDac2GC6ygx9WLGlcsnRd1Co0B2zT5fF5Ic6BZy+9m3HNZ3QcOeDKfg== +babel-plugin-polyfill-corejs3@^0.8.5: + version "0.8.6" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.6.tgz#25c2d20002da91fe328ff89095c85a391d6856cf" + integrity sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ== dependencies: - "@babel/helper-define-polyfill-provider" "^0.4.2" - core-js-compat "^3.32.2" + "@babel/helper-define-polyfill-provider" "^0.4.3" + core-js-compat "^3.33.1" -babel-plugin-polyfill-regenerator@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz#80d0f3e1098c080c8b5a65f41e9427af692dc326" - integrity sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA== +babel-plugin-polyfill-regenerator@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.3.tgz#d4c49e4b44614607c13fb769bcd85c72bb26a4a5" + integrity sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw== dependencies: - "@babel/helper-define-polyfill-provider" "^0.4.2" + "@babel/helper-define-polyfill-provider" "^0.4.3" balanced-match@^1.0.0: version "1.0.2" @@ -2447,13 +2471,14 @@ caching-transform@^4.0.0: package-hash "^4.0.0" write-file-atomic "^3.0.0" -call-bind@^1.0.0, call-bind@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== +call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.4, call-bind@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.5.tgz#6fa2b7845ce0ea49bf4d8b9ef64727a2c2e2e513" + integrity sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ== dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" + function-bind "^1.1.2" + get-intrinsic "^1.2.1" + set-function-length "^1.1.1" callsites@^3.0.0: version "3.1.0" @@ -2471,9 +2496,9 @@ camelcase@^6.0.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001541: - version "1.0.30001542" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001542.tgz#823ddb5aed0a70d5e2bfb49126478e84e9514b85" - integrity sha512-UrtAXVcj1mvPBFQ4sKd38daP8dEcXXr5sQe6QNNinaPd0iA/cxg9/l3VrSdL73jgw5sKyuQ6jNgiKO12W3SsVA== + version "1.0.30001553" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001553.tgz#e64e7dc8fd4885cd246bb476471420beb5e474b5" + integrity sha512-N0ttd6TrFfuqKNi+pMgWJTb9qrdJu4JSpgPFLe/lrD19ugC6fZgF0pUewRowDwzdDnb9V41mFcdlYgl/PyKf4A== caseless@~0.12.0: version "0.12.0" @@ -2599,9 +2624,9 @@ chrome-trace-event@^1.0.2: integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== ci-info@^3.2.0: - version "3.8.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" - integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== + version "3.9.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: version "1.0.4" @@ -2826,10 +2851,10 @@ cookiejar@^2.1.4: resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== -core-js-compat@^3.31.0, core-js-compat@^3.32.2: - version "3.33.0" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.33.0.tgz#24aa230b228406450b2277b7c8bfebae932df966" - integrity sha512-0w4LcLXsVEuNkIqwjjf9rjCoPhK8uqA4tMRh4Ge26vfLtUutshn+aRJU21I9LCJlh2QQHfisNToLjw1XEJLTWw== +core-js-compat@^3.31.0, core-js-compat@^3.33.1: + version "3.33.1" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.33.1.tgz#debe80464107d75419e00c2ee29f35982118ff84" + integrity sha512-6pYKNOgD/j/bkC5xS5IIg6bncid3rfrI42oBH1SQJbsmYPKF7rhzcFzYCcxYMmNQQ0rCEB8WqpW7QHndOggaeQ== dependencies: browserslist "^4.22.1" @@ -3005,10 +3030,10 @@ default-require-extensions@^3.0.0: dependencies: strip-bom "^4.0.0" -define-data-property@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.0.tgz#0db13540704e1d8d479a0656cf781267531b9451" - integrity sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g== +define-data-property@^1.0.1, define-data-property@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.1.tgz#c35f7cd0ab09883480d12ac5cb213715587800b3" + integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ== dependencies: get-intrinsic "^1.2.1" gopd "^1.0.1" @@ -3153,9 +3178,9 @@ ee-first@1.1.1: integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== electron-to-chromium@^1.4.535: - version "1.4.539" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.539.tgz#5ce6b161e252132cc84501bc35d084995a2a9840" - integrity sha512-wRmWJ8F7rgmINuI32S6r2SLrw/h/bJQsDSvBiq9GBfvc2Lh73qTOwn73r3Cf67mjVgFGJYcYtmERzySa5jIWlg== + version "1.4.564" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.564.tgz#9c6ada8ec7b43c65d8629300a0916a346ac5c0c2" + integrity sha512-bGAx9+teIzL5I4esQwCMtiXtb78Ysc8xOKTPOvmafbJZ4SQ40kDO1ym3yRcGSkfaBtV81fGgHOgPoe6DsmpmkA== elliptic@^6.5.3: version "6.5.4" @@ -3198,9 +3223,9 @@ engine.io-parser@~5.2.1: integrity sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ== engine.io@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.5.2.tgz#769348ced9d56bd47bd83d308ec1c3375e85937c" - integrity sha512-IXsMcGpw/xRfjra46sVZVHiSWo/nJ/3g1337q9KNXtS6YRzbW5yIzTCb9DjhrBe7r3GZQR0I4+nq+4ODk5g/cA== + version "6.5.3" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.5.3.tgz#80b0692912cef3a417e1b7433301d6397bf0374b" + integrity sha512-IML/R4eG/pUS5w7OfcDE0jKrljWS9nwnEfsxWCIJF5eO6AHo6+Hlv+lQbdlAYsiJPHzUthLm1RUjnBzWOs45cw== dependencies: "@types/cookie" "^0.4.1" "@types/cors" "^2.8.12" @@ -3237,25 +3262,25 @@ envinfo@^7.7.3: integrity sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw== es-abstract@^1.22.1: - version "1.22.2" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.2.tgz#90f7282d91d0ad577f505e423e52d4c1d93c1b8a" - integrity sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA== + version "1.22.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.3.tgz#48e79f5573198de6dee3589195727f4f74bc4f32" + integrity sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA== dependencies: array-buffer-byte-length "^1.0.0" arraybuffer.prototype.slice "^1.0.2" available-typed-arrays "^1.0.5" - call-bind "^1.0.2" + call-bind "^1.0.5" es-set-tostringtag "^2.0.1" es-to-primitive "^1.2.1" function.prototype.name "^1.1.6" - get-intrinsic "^1.2.1" + get-intrinsic "^1.2.2" get-symbol-description "^1.0.0" globalthis "^1.0.3" gopd "^1.0.1" - has "^1.0.3" has-property-descriptors "^1.0.0" has-proto "^1.0.1" has-symbols "^1.0.3" + hasown "^2.0.0" internal-slot "^1.0.5" is-array-buffer "^3.0.2" is-callable "^1.2.7" @@ -3265,7 +3290,7 @@ es-abstract@^1.22.1: is-string "^1.0.7" is-typed-array "^1.1.12" is-weakref "^1.0.2" - object-inspect "^1.12.3" + object-inspect "^1.13.1" object-keys "^1.1.1" object.assign "^4.1.4" regexp.prototype.flags "^1.5.1" @@ -3279,7 +3304,7 @@ es-abstract@^1.22.1: typed-array-byte-offset "^1.0.0" typed-array-length "^1.0.4" unbox-primitive "^1.0.2" - which-typed-array "^1.1.11" + which-typed-array "^1.1.13" es-module-lexer@^1.2.1: version "1.3.1" @@ -3287,20 +3312,20 @@ es-module-lexer@^1.2.1: integrity sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q== es-set-tostringtag@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" - integrity sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg== + version "2.0.2" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz#11f7cc9f63376930a5f20be4915834f4bc74f9c9" + integrity sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q== dependencies: - get-intrinsic "^1.1.3" - has "^1.0.3" + get-intrinsic "^1.2.2" has-tostringtag "^1.0.0" + hasown "^2.0.0" es-shim-unscopables@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" - integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w== + version "1.0.2" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" + integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== dependencies: - has "^1.0.3" + hasown "^2.0.0" es-to-primitive@^1.2.1: version "1.2.1" @@ -3356,7 +3381,7 @@ eslint-config-prettier@^9.0.0: resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz#eb25485946dd0c66cd216a46232dc05451518d1f" integrity sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw== -eslint-import-resolver-node@^0.3.7: +eslint-import-resolver-node@^0.3.9: version "0.3.9" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== @@ -3381,25 +3406,25 @@ eslint-plugin-es@^3.0.0: regexpp "^3.0.0" eslint-plugin-import@^2.28.1: - version "2.28.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.28.1.tgz#63b8b5b3c409bfc75ebaf8fb206b07ab435482c4" - integrity sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A== - dependencies: - array-includes "^3.1.6" - array.prototype.findlastindex "^1.2.2" - array.prototype.flat "^1.3.1" - array.prototype.flatmap "^1.3.1" + version "2.29.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz#8133232e4329ee344f2f612885ac3073b0b7e155" + integrity sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg== + dependencies: + array-includes "^3.1.7" + array.prototype.findlastindex "^1.2.3" + array.prototype.flat "^1.3.2" + array.prototype.flatmap "^1.3.2" debug "^3.2.7" doctrine "^2.1.0" - eslint-import-resolver-node "^0.3.7" + eslint-import-resolver-node "^0.3.9" eslint-module-utils "^2.8.0" - has "^1.0.3" - is-core-module "^2.13.0" + hasown "^2.0.0" + is-core-module "^2.13.1" is-glob "^4.0.3" minimatch "^3.1.2" - object.fromentries "^2.0.6" - object.groupby "^1.0.0" - object.values "^1.1.6" + object.fromentries "^2.0.7" + object.groupby "^1.0.1" + object.values "^1.1.7" semver "^6.3.1" tsconfig-paths "^3.14.2" @@ -3421,9 +3446,9 @@ eslint-plugin-prefer-import@^0.0.1: integrity sha512-2OKD3Bjgqkn0BvEGRwpEDhjXPSXvT3CXmWIrIavwafOkQE8FLTvFybEBT9dm7P0kHnjlNGv1AfNsL/i/GNDNHA== eslint-plugin-prettier@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.0.tgz#6887780ed95f7708340ec79acfdf60c35b9be57a" - integrity sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w== + version "5.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.1.tgz#a3b399f04378f79f066379f544e42d6b73f11515" + integrity sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg== dependencies: prettier-linter-helpers "^1.0.0" synckit "^0.8.5" @@ -3483,17 +3508,18 @@ eslint-webpack-plugin@^4.0.1: schema-utils "^4.0.0" eslint@^8.17.0, eslint@^8.50.0: - version "8.50.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.50.0.tgz#2ae6015fee0240fcd3f83e1e25df0287f487d6b2" - integrity sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg== + version "8.52.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.52.0.tgz#d0cd4a1fac06427a61ef9242b9353f36ea7062fc" + integrity sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.6.1" "@eslint/eslintrc" "^2.1.2" - "@eslint/js" "8.50.0" - "@humanwhocodes/config-array" "^0.11.11" + "@eslint/js" "8.52.0" + "@humanwhocodes/config-array" "^0.11.13" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" + "@ungap/structured-clone" "^1.2.0" ajv "^6.12.4" chalk "^4.0.0" cross-spawn "^7.0.2" @@ -3792,11 +3818,11 @@ findup@0.1.5: commander "~2.1.0" flat-cache@^3.0.4: - version "3.1.0" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.1.0.tgz#0e54ab4a1a60fe87e2946b6b00657f1c99e1af3f" - integrity sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew== + version "3.1.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.1.1.tgz#a02a15fdec25a8f844ff7cc658f03dd99eb4609b" + integrity sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q== dependencies: - flatted "^3.2.7" + flatted "^3.2.9" keyv "^4.5.3" rimraf "^3.0.2" @@ -3805,7 +3831,7 @@ flat@^5.0.2: resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== -flatted@^3.2.7: +flatted@^3.2.7, flatted@^3.2.9: version "3.2.9" resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf" integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== @@ -3923,10 +3949,10 @@ fstream@^1.0.12: mkdirp ">=0.5 0" rimraf "2" -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== function.prototype.name@^1.1.6: version "1.1.6" @@ -3967,20 +3993,20 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-func-name@^2.0.0, get-func-name@^2.0.2: +get-func-name@^2.0.1, get-func-name@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" - integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== +get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz#281b7622971123e1ef4b3c90fd7539306da93f3b" + integrity sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA== dependencies: - function-bind "^1.1.1" - has "^1.0.3" + function-bind "^1.1.2" has-proto "^1.0.1" has-symbols "^1.0.3" + hasown "^2.0.0" get-package-type@^0.1.0: version "0.1.0" @@ -4068,9 +4094,9 @@ globals@^11.1.0: integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== globals@^13.19.0: - version "13.22.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.22.0.tgz#0c9fcb9c48a2494fbb5edbfee644285543eba9d8" - integrity sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw== + version "13.23.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.23.0.tgz#ef31673c926a0976e1f61dab4dca57e0c0a8af02" + integrity sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA== dependencies: type-fest "^0.20.2" @@ -4151,11 +4177,11 @@ has-flag@^4.0.0: integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== has-property-descriptors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" - integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz#52ba30b6c5ec87fd89fa574bc1c39125c6f65340" + integrity sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg== dependencies: - get-intrinsic "^1.1.1" + get-intrinsic "^1.2.2" has-proto@^1.0.1: version "1.0.1" @@ -4179,13 +4205,6 @@ has-unicode@^2.0.0: resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - hash-base@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" @@ -4211,6 +4230,13 @@ hasha@^5.0.0: is-stream "^2.0.0" type-fest "^0.8.0" +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + he@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -4348,12 +4374,12 @@ inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, i integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== internal-slot@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986" - integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ== + version "1.0.6" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.6.tgz#37e756098c4911c5e912b8edbf71ed3aa116f930" + integrity sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg== dependencies: - get-intrinsic "^1.2.0" - has "^1.0.3" + get-intrinsic "^1.2.2" + hasown "^2.0.0" side-channel "^1.0.4" interpret@^3.1.1: @@ -4415,12 +4441,12 @@ is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-core-module@^2.13.0, is-core-module@^2.5.0: - version "2.13.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db" - integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ== +is-core-module@^2.13.0, is-core-module@^2.13.1, is-core-module@^2.5.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== dependencies: - has "^1.0.3" + hasown "^2.0.0" is-date-object@^1.0.1: version "1.0.5" @@ -4981,9 +5007,9 @@ karma@^6.4.1: yargs "^16.1.1" keyv@^4.5.3: - version "4.5.3" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.3.tgz#00873d2b046df737963157bd04f294ca818c9c25" - integrity sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug== + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== dependencies: json-buffer "3.0.1" @@ -5154,11 +5180,11 @@ log4js@^6.4.1: streamroller "^3.1.5" loupe@^2.3.6: - version "2.3.6" - resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.6.tgz#76e4af498103c532d1ecc9be102036a21f787b53" - integrity sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA== + version "2.3.7" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" + integrity sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA== dependencies: - get-func-name "^2.0.0" + get-func-name "^2.0.1" lru-cache@^5.1.1: version "5.1.1" @@ -5438,9 +5464,9 @@ neo-async@^2.6.2: integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== nise@^5.1.4: - version "5.1.4" - resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.4.tgz#491ce7e7307d4ec546f5a659b2efe94a18b4bbc0" - integrity sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg== + version "5.1.5" + resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.5.tgz#f2aef9536280b6c18940e32ba1fbdc770b8964ee" + integrity sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw== dependencies: "@sinonjs/commons" "^2.0.0" "@sinonjs/fake-timers" "^10.0.2" @@ -5592,10 +5618,10 @@ object-assign@^4, object-assign@^4.1.0: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-inspect@^1.12.3, object-inspect@^1.9.0: - version "1.12.3" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" - integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== +object-inspect@^1.13.1, object-inspect@^1.9.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" + integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== object-is@^1.1.5: version "1.1.5" @@ -5629,7 +5655,7 @@ object.entries@^1.1.5: define-properties "^1.2.0" es-abstract "^1.22.1" -object.fromentries@^2.0.6: +object.fromentries@^2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.7.tgz#71e95f441e9a0ea6baf682ecaaf37fa2a8d7e616" integrity sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA== @@ -5638,7 +5664,7 @@ object.fromentries@^2.0.6: define-properties "^1.2.0" es-abstract "^1.22.1" -object.groupby@^1.0.0: +object.groupby@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.1.tgz#d41d9f3c8d6c778d9cbac86b4ee9f5af103152ee" integrity sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ== @@ -5648,7 +5674,7 @@ object.groupby@^1.0.0: es-abstract "^1.22.1" get-intrinsic "^1.2.1" -object.values@^1.1.6: +object.values@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.7.tgz#617ed13272e7e1071b43973aa1655d9291b8442a" integrity sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng== @@ -6264,9 +6290,9 @@ resolve-from@^5.0.0: integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== resolve@^1.10.1, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22.4, resolve@^1.3.2: - version "1.22.6" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.6.tgz#dd209739eca3aef739c626fea1b4f3c506195362" - integrity sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw== + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== dependencies: is-core-module "^2.13.0" path-parse "^1.0.7" @@ -6420,6 +6446,16 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== +set-function-length@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.1.1.tgz#4bc39fafb0307224a33e106a7d35ca1218d659ed" + integrity sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ== + dependencies: + define-data-property "^1.1.1" + get-intrinsic "^1.2.1" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + set-function-name@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.1.tgz#12ce38b7954310b9f61faa12701620a0c882793a" @@ -6486,9 +6522,9 @@ sinon-chai@^3.7.0: integrity sha512-mf5NURdUaSdnatJx3uhoBOrY9dtL19fiOtAdT1Azxg3+lNJFiuN0uzaU3xX1LeAfL17kHQhTAJgpsfhbMJMY2g== sinon@^16.0.0: - version "16.0.0" - resolved "https://registry.yarnpkg.com/sinon/-/sinon-16.0.0.tgz#06da4e63624b946c9d7e67cce21c2f67f40f23a9" - integrity sha512-B8AaZZm9CT5pqe4l4uWJztfD/mOTa7dL8Qo0W4+s+t74xECOgSZDDQCBjNgIK3+n4kyxQrSTv2V5ul8K25qkiQ== + version "16.1.3" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-16.1.3.tgz#b760ddafe785356e2847502657b4a0da5501fba8" + integrity sha512-mjnWWeyxcAf9nC0bXcPmiDut+oE8HYridTNzBbF98AYVLmWwGRp2ISEpyhYflG1ifILT+eNn3BmKUJPxjXUPlA== dependencies: "@sinonjs/commons" "^3.0.0" "@sinonjs/fake-timers" "^10.3.0" @@ -6612,9 +6648,9 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.15" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.15.tgz#142460aabaca062bc7cd4cc87b7d50725ed6a4ba" - integrity sha512-lpT8hSQp9jAKp9mhtBU4Xjon8LPGBvLIuBiSVhMEtmLecTh2mO0tlqrAMp47tBXzMr13NJMQ2lf7RpQGLJ3HsQ== + version "3.0.16" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz#a14f64e0954f6e25cc6587bd4f392522db0d998f" + integrity sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw== sprintf-js@~1.0.2: version "1.0.3" @@ -6622,9 +6658,9 @@ sprintf-js@~1.0.2: integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== sshpk@^1.7.0: - version "1.17.0" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5" - integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== + version "1.18.0" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.18.0.tgz#1663e55cddf4d688b86a46b77f0d5fe363aba028" + integrity sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ== dependencies: asn1 "~0.2.3" assert-plus "^1.0.0" @@ -6653,10 +6689,10 @@ statuses@~1.5.0: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -stellar-base@10.0.0-beta.2: - version "10.0.0-beta.2" - resolved "https://registry.yarnpkg.com/stellar-base/-/stellar-base-10.0.0-beta.2.tgz#36452bb14a7a9e66175e6231feb64de3d2fe3627" - integrity sha512-fgVcNzlGuXTte4gEg3fDA/pJB5VbqfE8+gkbjsphVecDELR8t3K+QalbDhcafi3g54nRYUaHnN7LcGeoschuIA== +stellar-base@10.0.0-beta.3: + version "10.0.0-beta.3" + resolved "https://registry.yarnpkg.com/stellar-base/-/stellar-base-10.0.0-beta.3.tgz#c90e261945c58e2176b0a50b9a7b96003ecb0768" + integrity sha512-+B1fOdsDWJEnYSYkKSAVHVngzaqDtD8wdDMT/FC+11MrohP3uGY1OmrEeVn34jiBmUlpYZVudDnpDMSXD4RqDA== dependencies: base32.js "^0.1.0" bignumber.js "^9.1.2" @@ -6918,9 +6954,9 @@ terser-webpack-plugin@^5.3.7, terser-webpack-plugin@^5.3.9: terser "^5.16.8" terser@^5.16.8: - version "5.20.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.20.0.tgz#ea42aea62578703e33def47d5c5b93c49772423e" - integrity sha512-e56ETryaQDyebBwJIWYB2TT6f2EZ0fL0sW/JRXNMN26zZdKi2u/E/5my5lG6jNxym6qsrVXfFRmOdV42zlAgLQ== + version "5.22.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.22.0.tgz#4f18103f84c5c9437aafb7a14918273310a8a49d" + integrity sha512-hHZVLgRA2z4NWcN6aS5rQDc+7Dcy58HOf2zbYwmFcQ+ua3h6eEFf5lIDKTzbWwlazPyOZsFQO8V80/IjVNExEw== dependencies: "@jridgewell/source-map" "^0.3.3" acorn "^8.8.2" @@ -7210,6 +7246,11 @@ underscore@~1.13.2: resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.6.tgz#04786a1f589dc6c09f761fc5f45b89e935136441" integrity sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A== +undici-types@~5.25.1: + version "5.25.3" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.25.3.tgz#e044115914c85f0bcbb229f346ab739f064998c3" + integrity sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" @@ -7391,11 +7432,12 @@ webpack-merge@^4.1.5: lodash "^4.17.15" webpack-merge@^5.7.3: - version "5.9.0" - resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.9.0.tgz#dc160a1c4cf512ceca515cc231669e9ddb133826" - integrity sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg== + version "5.10.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.10.0.tgz#a3ad5d773241e9c682803abf628d4cd62b8a4177" + integrity sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA== dependencies: clone-deep "^4.0.1" + flat "^5.0.2" wildcard "^2.0.0" webpack-sources@^3.2.3: @@ -7404,9 +7446,9 @@ webpack-sources@^3.2.3: integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== webpack@^5.88.2: - version "5.88.2" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.88.2.tgz#f62b4b842f1c6ff580f3fcb2ed4f0b579f4c210e" - integrity sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ== + version "5.89.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.89.0.tgz#56b8bf9a34356e93a6625770006490bf3a7f32dc" + integrity sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw== dependencies: "@types/eslint-scope" "^3.7.3" "@types/estree" "^1.0.0" @@ -7449,13 +7491,13 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== -which-typed-array@^1.1.11, which-typed-array@^1.1.2: - version "1.1.11" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.11.tgz#99d691f23c72aab6768680805a271b69761ed61a" - integrity sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew== +which-typed-array@^1.1.11, which-typed-array@^1.1.13, which-typed-array@^1.1.2: + version "1.1.13" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.13.tgz#870cd5be06ddb616f504e7b039c4c24898184d36" + integrity sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow== dependencies: available-typed-arrays "^1.0.5" - call-bind "^1.0.2" + call-bind "^1.0.4" for-each "^0.3.3" gopd "^1.0.1" has-tostringtag "^1.0.0"