diff --git a/package.json b/package.json index 7fe804c52..172656ed3 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "@types/chai": "^4.3.6", "@types/detect-node": "^2.0.0", "@types/eventsource": "^1.1.12", + "@types/json-schema": "^7.0.15", "@types/lodash": "^4.14.199", "@types/mocha": "^10.0.2", "@types/node": "^20.8.10", @@ -145,6 +146,7 @@ "axios": "^1.6.0", "bignumber.js": "^9.1.2", "eventsource": "^2.0.2", + "json-schema-faker": "^0.5.4", "randombytes": "^2.1.0", "@stellar/stellar-base": "10.0.0", "toml": "^3.0.0", diff --git a/src/contract_spec.ts b/src/contract_spec.ts index b4e0b2289..6cc002609 100644 --- a/src/contract_spec.ts +++ b/src/contract_spec.ts @@ -1,22 +1,33 @@ +import { JSONSchema7 } from "json-schema"; import { - Address, - Contract, ScIntType, XdrLargeInt, + xdr, + Address, + Contract, scValToBigInt, - xdr -} from '@stellar/stellar-base'; +} from "."; export interface Union { tag: string; values?: T; } +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]; +} + /** * Provides a ContractSpec class which can contains the XDR types defined by the contract. * This allows the class to be used to convert between native and raw `xdr.ScVal`s. * * @example + * ```js * const specEntries = [...]; // XDR spec entries of a smart contract * const contractSpec = new ContractSpec(specEntries); * @@ -34,6 +45,7 @@ export interface Union { * const result = contractSpec.funcResToNative('funcName', resultScv); * * console.log(result); // {success: true} + * ``` */ export class ContractSpec { public entries: xdr.ScSpecEntry[] = []; @@ -47,17 +59,33 @@ export class ContractSpec { */ constructor(entries: xdr.ScSpecEntry[] | string[]) { if (entries.length == 0) { - throw new Error('Contract spec must have at least one entry'); + throw new Error("Contract spec must have at least one entry"); } let entry = entries[0]; - if (typeof entry === 'string') { + if (typeof entry === "string") { this.entries = (entries as string[]).map((s) => - xdr.ScSpecEntry.fromXDR(s, 'base64') + xdr.ScSpecEntry.fromXDR(s, "base64") ); } else { this.entries = entries as xdr.ScSpecEntry[]; } } + + /** + * Gets the XDR functions from the spec. + * + * @returns {xdr.ScSpecFunctionV0[]} all contract functions + * + */ + funcs(): xdr.ScSpecFunctionV0[] { + return this.entries + .filter( + (entry) => + entry.switch().value === + xdr.ScSpecEntryKind.scSpecEntryFunctionV0().value + ) + .map((entry) => entry.value() as xdr.ScSpecFunctionV0); + } /** * Gets the XDR function spec for the given function name. * @@ -118,8 +146,8 @@ export class ContractSpec { */ funcResToNative(name: string, val_or_base64: xdr.ScVal | string): any { let val = - typeof val_or_base64 === 'string' - ? xdr.ScVal.fromXDR(val_or_base64, 'base64') + typeof val_or_base64 === "string" + ? xdr.ScVal.fromXDR(val_or_base64, "base64") : val_or_base64; let func = this.getFunc(name); let outputs = func.outputs(); @@ -135,7 +163,10 @@ export class ContractSpec { } let output = outputs[0]; if (output.switch().value === xdr.ScSpecType.scSpecTypeResult().value) { - return this.scValToNative(val, output.result().okType()); + return this.scValToNative( + val, + (output.value() as xdr.ScSpecTypeResult).okType() + ); } return this.scValToNative(val, output); } @@ -181,9 +212,8 @@ export class ContractSpec { } return this.nativeToScVal(val, opt.valueType()); } - switch (typeof val) { - case 'object': { + case "object": { if (val === null) { switch (value) { case xdr.ScSpecType.scSpecTypeVoid().value: @@ -280,10 +310,11 @@ export class ContractSpec { return xdr.ScVal.scvMap(entries); } - if ((val.constructor?.name ?? '') !== 'Object') { + if ((val.constructor?.name ?? "") !== "Object") { throw new TypeError( - `cannot interpret ${val.constructor - ?.name} value as ScVal (${JSON.stringify(val)})` + `cannot interpret ${ + val.constructor?.name + } value as ScVal (${JSON.stringify(val)})` ); } @@ -292,8 +323,8 @@ export class ContractSpec { ); } - case 'number': - case 'bigint': { + case "number": + case "bigint": { switch (value) { case xdr.ScSpecType.scSpecTypeU32().value: return xdr.ScVal.scvU32(val as number); @@ -312,16 +343,16 @@ export class ContractSpec { throw new TypeError(`invalid type (${ty}) specified for integer`); } } - case 'string': + case "string": return stringToScVal(val, t); - case 'boolean': { + 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': { + case "undefined": { if (!ty) { return xdr.ScVal.scvVoid(); } @@ -336,7 +367,7 @@ export class ContractSpec { } } - case 'function': // FIXME: Is this too helpful? + case "function": // FIXME: Is this too helpful? return this.nativeToScVal(val(), ty); default: @@ -348,7 +379,7 @@ export class ContractSpec { let entry = this.findEntry(name); switch (entry.switch()) { case xdr.ScSpecEntryKind.scSpecEntryUdtEnumV0(): - if (typeof val !== 'number') { + if (typeof val !== "number") { throw new TypeError( `expected number for enum ${name}, but got ${typeof val}` ); @@ -417,7 +448,7 @@ export class ContractSpec { if (fields.some(isNumeric)) { if (!fields.every(isNumeric)) { throw new Error( - 'mixed numeric and non-numeric field names are not allowed' + "mixed numeric and non-numeric field names are not allowed" ); } return xdr.ScVal.scvVec( @@ -429,7 +460,7 @@ export class ContractSpec { let name = field.name().toString(); return new xdr.ScMapEntry({ key: this.nativeToScVal(name, xdr.ScSpecTypeDef.scSpecTypeSymbol()), - val: this.nativeToScVal(val[name], field.type()) + val: this.nativeToScVal(val[name], field.type()), }); }) ); @@ -452,7 +483,7 @@ export class ContractSpec { * @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); + return this.scValToNative(xdr.ScVal.fromXDR(scv, "base64"), typeDef); } /** @@ -506,7 +537,7 @@ export class ContractSpec { } case xdr.ScValType.scvAddress().value: - return Address.fromScVal(scv) as T; + return Address.fromScVal(scv).toString() as T; case xdr.ScValType.scvMap().value: { let map = scv.map() ?? []; @@ -517,7 +548,7 @@ export class ContractSpec { return new Map( map.map((entry) => [ this.scValToNative(entry.key(), keyType), - this.scValToNative(entry.val(), valueType) + this.scValToNative(entry.val(), valueType), ]) ) as T; } @@ -605,21 +636,15 @@ export class ContractSpec { `failed to find entry ${name} in union {udt.name().toString()}` ); } - let res: Union = { tag: name, values: undefined }; + let res: Union = { tag: name }; 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; + let values = ty.map((entry, i) => this.scValToNative(vec![i + 1], entry)); + res.values = values; } return res; } @@ -651,6 +676,48 @@ export class ContractSpec { } return num; } + + jsonSchema(funcName?: string): JSONSchema7 { + let definitions: any = {}; + let properties: any = {}; + for (let entry of this.entries) { + switch (entry.switch().value) { + case xdr.ScSpecEntryKind.scSpecEntryUdtEnumV0().value: { + let udt = entry.value() as xdr.ScSpecUdtEnumV0; + definitions[udt.name().toString()] = enumToJsonSchema(udt); + break; + } + case xdr.ScSpecEntryKind.scSpecEntryUdtStructV0().value: { + let udt = entry.value() as xdr.ScSpecUdtStructV0; + definitions[udt.name().toString()] = structToJsonSchema(udt); + break; + } + case xdr.ScSpecEntryKind.scSpecEntryUdtUnionV0().value: + let udt = entry.value() as xdr.ScSpecUdtUnionV0; + definitions[udt.name().toString()] = unionToJsonSchema(udt); + break; + case xdr.ScSpecEntryKind.scSpecEntryFunctionV0().value: { + let fn = entry.value() as xdr.ScSpecFunctionV0; + definitions[fn.name().toString()] = functionToJsonSchema(fn); + properties[fn.name().toString()] = { + $ref: `#/definitions/${fn.name().toString()}`, + }; + break; + } + case xdr.ScSpecEntryKind.scSpecEntryUdtErrorEnumV0().value: { + // throw new Error('Error enums not supported yet'); + } + } + } + let res: JSONSchema7 = { + $schema: "http://json-schema.org/draft-07/schema#", + definitions: { ...PRIMITIVE_DEFINITONS, ...definitions }, + }; + if (funcName) { + res["$ref"] = "#/definitions/" + funcName; + } + return res; + } } function stringToScVal(str: string, ty: xdr.ScSpecType): xdr.ScVal { @@ -663,6 +730,21 @@ function stringToScVal(str: string, ty: xdr.ScSpecType): xdr.ScVal { let addr = Address.fromString(str as string); return xdr.ScVal.scvAddress(addr.toScAddress()); } + case xdr.ScSpecType.scSpecTypeU64().value: + return new XdrLargeInt("u64", str).toScVal(); + case xdr.ScSpecType.scSpecTypeI64().value: + return new XdrLargeInt("i64", str).toScVal(); + case xdr.ScSpecType.scSpecTypeU128().value: + return new XdrLargeInt("u128", str).toScVal(); + case xdr.ScSpecType.scSpecTypeI128().value: + return new XdrLargeInt("i128", str).toScVal(); + case xdr.ScSpecType.scSpecTypeU256().value: + return new XdrLargeInt("u256", str).toScVal(); + case xdr.ScSpecType.scSpecTypeI256().value: + return new XdrLargeInt("i256", str).toScVal(); + case xdr.ScSpecType.scSpecTypeBytes().value: + case xdr.ScSpecType.scSpecTypeBytesN().value: + return xdr.ScVal.scvBytes(Buffer.from(str, "base64")); default: throw new TypeError(`invalid type ${ty.name} specified for string value`); @@ -690,11 +772,346 @@ function findCase(name: string) { }; } -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}`); +const PRIMITIVE_DEFINITONS = { + U64: { + type: "string", + pattern: "^[0-9]+$", + format: "bigint", + minLength: 1, + maxLength: 20, // 64-bit max value has 20 digits + }, + I64: { + type: "string", + pattern: "^-?^[0-9]+$", + format: "bigint", + minLength: 1, + maxLength: 21, // Includes additional digit for the potential '-' + }, + U32: { + type: "integer", + minimum: 0, + maximum: 4294967295, + }, + I32: { + type: "integer", + minimum: -2147483648, + maximum: 2147483647, + }, + U128: { + type: "string", + pattern: "^[0-9]+$", + format: "bigint", + minLength: 1, + maxLength: 39, // 128-bit max value has 39 digits + }, + I128: { + type: "string", + pattern: "^-?[0-9]+$", + format: "bigint", + minLength: 1, + maxLength: 40, // Includes additional digit for the potential '-' + }, + U256: { + type: "string", + pattern: "^[0-9]+$", + format: "bigint", + minLength: 1, + maxLength: 78, // 256-bit max value has 78 digits + }, + I256: { + type: "string", + pattern: "^-?[0-9]+$", + format: "bigint", + minLength: 1, + maxLength: 79, // Includes additional digit for the potential '-' + }, + Address: { + type: "string", + format: "address", + description: "Address can be a public key or contract id", + }, + ScString: { + type: "string", + description: "ScString is a string", + }, + ScSymbol: { + type: "string", + description: "ScString is a string", + }, + DataUrl: { + type: "string", + pattern: + "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=)?$", + }, +}; + +/** + * + * @param typeDef type to convert to json schema reference + * @returns + */ +function typeRef(typeDef: xdr.ScSpecTypeDef): object { + let t = typeDef.switch(); + let value = t.value; + let ref; + switch (value) { + case xdr.ScSpecType.scSpecTypeVal().value: { + ref = "Val"; + break; + } + case xdr.ScSpecType.scSpecTypeBool().value: { + return { type: "boolean" }; + } + case xdr.ScSpecType.scSpecTypeVoid().value: { + return { type: "null" }; + } + case xdr.ScSpecType.scSpecTypeError().value: { + ref = "Error"; + break; + } + case xdr.ScSpecType.scSpecTypeU32().value: { + ref = "U32"; + break; + } + case xdr.ScSpecType.scSpecTypeI32().value: { + ref = "I32"; + break; + } + case xdr.ScSpecType.scSpecTypeU64().value: { + ref = "U64"; + break; + } + case xdr.ScSpecType.scSpecTypeI64().value: { + ref = "I64"; + break; + } + case xdr.ScSpecType.scSpecTypeTimepoint().value: { + throw new Error("Timepoint type not supported"); + ref = "Timepoint"; + break; + } + case xdr.ScSpecType.scSpecTypeDuration().value: { + throw new Error("Duration not supported"); + ref = "Duration"; + break; + } + case xdr.ScSpecType.scSpecTypeU128().value: { + ref = "U128"; + break; + } + case xdr.ScSpecType.scSpecTypeI128().value: { + ref = "I128"; + break; + } + case xdr.ScSpecType.scSpecTypeU256().value: { + ref = "U256"; + break; + } + case xdr.ScSpecType.scSpecTypeI256().value: { + ref = "I256"; + break; + } + case xdr.ScSpecType.scSpecTypeBytes().value: { + ref = "DataUrl"; + break; + } + case xdr.ScSpecType.scSpecTypeString().value: { + ref = "ScString"; + break; + } + case xdr.ScSpecType.scSpecTypeSymbol().value: { + ref = "ScSymbol"; + break; + } + case xdr.ScSpecType.scSpecTypeAddress().value: { + ref = "Address"; + break; + } + case xdr.ScSpecType.scSpecTypeOption().value: { + let opt = typeDef.value() as xdr.ScSpecTypeOption; + return typeRef(opt.valueType()); + } + case xdr.ScSpecType.scSpecTypeResult().value: { + // throw new Error('Result type not supported'); + break; + } + case xdr.ScSpecType.scSpecTypeVec().value: { + let arr = typeDef.value() as xdr.ScSpecTypeVec; + let ref = typeRef(arr.elementType()); + return { + type: "array", + items: ref, + }; + } + case xdr.ScSpecType.scSpecTypeMap().value: { + let map = typeDef.value() as xdr.ScSpecTypeMap; + let ref = typeRef(map.valueType()); + return { + type: "object", + patternProperties: { + "^[a-zA-Z0-9]+$": ref, + }, + additionalProperties: false, + }; + } + case xdr.ScSpecType.scSpecTypeTuple().value: { + let tuple = typeDef.value() as xdr.ScSpecTypeTuple; + let minItems = tuple.valueTypes().length; + let maxItems = minItems; + let items = tuple.valueTypes().map(typeRef); + return { type: "array", items, minItems, maxItems }; + } + case xdr.ScSpecType.scSpecTypeBytesN().value: { + let arr = typeDef.value() as xdr.ScSpecTypeBytesN; + return { + $ref: "#/definitions/DataUrl", + maxLength: arr.n(), + }; + } + case xdr.ScSpecType.scSpecTypeUdt().value: { + let udt = typeDef.value() as xdr.ScSpecTypeUdt; + ref = udt.name().toString(); + break; + } } - return entry[1]; + return { $ref: `#/definitions/${ref}` }; +} + +function isRequired(typeDef: xdr.ScSpecTypeDef): boolean { + return typeDef.switch().value != xdr.ScSpecType.scSpecTypeOption().value; +} + +function structToJsonSchema(udt: xdr.ScSpecUdtStructV0): object { + let fields = udt.fields(); + if (fields.some(isNumeric)) { + if (!fields.every(isNumeric)) { + throw new Error( + "mixed numeric and non-numeric field names are not allowed" + ); + } + let items = fields.map((_, i) => typeRef(fields[i].type())); + return { + type: "array", + items, + minItems: fields.length, + maxItems: fields.length, + }; + } + let description = udt.doc().toString(); + let { properties, required }: any = args_and_required(fields); + properties["additionalProperties"] = false; + return { + description, + properties, + required, + type: "object", + }; +} + +function args_and_required( + input: { type: () => xdr.ScSpecTypeDef; name: () => string | Buffer }[] +): { properties: object; required?: string[] } { + let properties: any = {}; + let required: string[] = []; + for (let arg of input) { + let type_ = arg.type(); + let name = arg.name().toString(); + properties[name] = typeRef(type_); + if (isRequired(type_)) { + required.push(name); + } + } + let res: { properties: object; required?: string[] } = { properties }; + if (required.length > 0) { + res.required = required; + } + return res; +} + +function functionToJsonSchema(func: xdr.ScSpecFunctionV0): object { + let { properties, required }: any = args_and_required(func.inputs()); + let description = func.doc().toString(); + let args: any = { + additionalProperties: false, + properties, + type: "object", + }; + if (required?.length > 0) { + args.required = required; + } + let input: any = { + additionalProperties: false, + /// Previous way of determining if this type is a function + contractMethod: "view", + properties: { + args, + }, + }; + // let output: any = {}; + // let outputs = func.outputs(); + // if (outputs.length !== 0) { + // output[`${name}__Result`] = typeRef(func.outputs()[0]); + // } + if (description.length > 0) { + input.description = description; + } + + return { + ...input, + // ...output + }; +} + +function unionToJsonSchema(udt: xdr.ScSpecUdtUnionV0): any { + let description = udt.doc().toString(); + let cases = udt.cases(); + let oneOf: any[] = []; + for (let case_ of cases) { + switch (case_.switch().value) { + case xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseVoidV0().value: { + let c = case_.value() as xdr.ScSpecUdtUnionCaseVoidV0; + oneOf.push({ + tag: c.name().toString(), + }); + break; + } + case xdr.ScSpecUdtUnionCaseV0Kind.scSpecUdtUnionCaseTupleV0().value: { + let c = case_.value() as xdr.ScSpecUdtUnionCaseTupleV0; + oneOf.push({ + tag: c.name().toString(), + values: c.type().map(typeRef), + }); + } + } + } + + let res: any = { + oneOf, + }; + if (description.length > 0) { + res.description = description; + } + return res; +} + +function enumToJsonSchema(udt: xdr.ScSpecUdtEnumV0): any { + let description = udt.doc().toString(); + let cases = udt.cases(); + let oneOf: any[] = []; + for (let case_ of cases) { + let title = case_.name().toString(); + let description = case_.doc().toString(); + oneOf.push({ + description, + title, + enum: [case_.value()], + type: "number", + }); + } + + let res: any = { oneOf }; + if (description.length > 0) { + res.description = description; + } + return res; } diff --git a/test/unit/contract_spec.js b/test/unit/contract_spec.js index a16fbd30f..5dd3d1c9e 100644 --- a/test/unit/contract_spec.js +++ b/test/unit/contract_spec.js @@ -1,256 +1,287 @@ -import { xdr, Address, ContractSpec } from "../../lib"; -//@ts-ignore +import { xdr, Address, ContractSpec, Keypair } from "../.."; +import { JSONSchemaFaker } from "json-schema-faker"; import spec from "../spec.json"; import { expect } from "chai"; const publicKey = "GCBVOLOM32I7OD5TWZQCIXCXML3TK56MDY7ZMTAILIBQHHKPCVU42XYW"; const addr = Address.fromString(publicKey); let SPEC; +JSONSchemaFaker.format("address", () => { + let keypair = Keypair.random(); + return keypair.publicKey(); +}); +JSONSchemaFaker.format("bigint", (value) => { + let s = JSONSchemaFaker.generate(value); + return BigInt(s.toString()); +}); +const ints = ["i64", "u64", "i128", "u128", "i256", "u256"]; before(() => { - SPEC = new ContractSpec(spec); + SPEC = new ContractSpec(spec); }); it("throws if no entries", () => { - expect(() => new ContractSpec([])).to.throw( - /Contract spec must have at least one entry/i, - ); + 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"); + 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]; } - if (fn.outputs().length === 0) { - return xdr.ScSpecTypeDef.scSpecTypeVoid(); + async function jsonSchema_roundtrip(spec, funcName, num = 100) { + let funcSpec = spec.jsonSchema(funcName); + for (let i = 0; i < num; i++) { + let arg = await JSONSchemaFaker.resolve(funcSpec); + // @ts-ignore + let res = arg.args; + try { + let scVal = SPEC.funcArgsToScVals(funcName, res)[0]; + let result = SPEC.funcResToNative(funcName, scVal); + if (ints.some((i) => funcName.includes(i))) { + res[funcName] = BigInt(res[funcName]); + // result = result.toString(); + // if (result.startsWith("0") && result.length > 1) { + // result = result.slice(1); + // } + } + if (funcName.startsWith("bytes")) { + res[funcName] = Buffer.from(res[funcName], "base64"); + } + expect(res[funcName]).deep.equal(result); + } + catch (e) { + console.error(funcName, JSON.stringify(arg, null, 2), "\n", + //@ts-ignore + JSON.stringify(funcSpec.definitions[funcName]["properties"], null, 2)); + throw e; + } + } } - 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); + describe("Json Schema", () => { + SPEC = new ContractSpec(spec); + let names = SPEC.funcs().map((f) => f.name().toString()); + const banned = ["strukt_hel", "not", "woid", "val", "multi_args"]; + names + .filter((name) => !name.includes("fail")) + .filter((name) => !banned.includes(name)) + .forEach((name) => { + it(name, async () => { + await jsonSchema_roundtrip(SPEC, name); + }); + }); + }); + 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" }; + roundtrip("simple", simple); + }); + it("simple second", () => { + const simple = { tag: "Second" }; + roundtrip("simple", simple); + }); + it("simple third", () => { + const simple = { tag: "Third" }; + 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" }]], + }; + roundtrip("complex", complex); + }); + it("enum", () => { + const complex = { + tag: "Enum", + values: [{ tag: "First" }], + }; + roundtrip("complex", complex); + }); + it("asset", () => { + const complex = { tag: "Asset", values: [addr.toString(), 1n] }; + roundtrip("complex", complex); + }); + it("void", () => { + const complex = { tag: "Void" }; + roundtrip("complex", complex); + }); + }); + it("addresse", () => { + roundtrip("addresse", addr.toString()); }); - it("simple second", () => { - const simple = { tag: "Second", values: undefined }; - roundtrip("simple", simple); + it("bytes", () => { + const bytes = Buffer.from("hello"); + roundtrip("bytes", bytes); }); - it("simple third", () => { - const simple = { tag: "Third", values: undefined }; - roundtrip("simple", simple); + it("bytes_n", () => { + const bytes_n = Buffer.from("123456789"); // what's the correct way to construct bytes_n? + roundtrip("bytes_n", bytes_n); }); - }); - describe("complex", () => { - it("struct", () => { - const complex = { - tag: "Struct", - values: [{ a: 0, b: true, c: "hello" }], - }; - roundtrip("complex", complex); + 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 complex = { - tag: "Tuple", - values: [ - [ - { a: 0, b: true, c: "hello" }, - { tag: "First", values: undefined }, - ], - ], - }; - roundtrip("complex", complex); + const tuple = ["hello", 1]; + roundtrip("tuple", tuple); + }); + it("option", () => { + roundtrip("option", 1); + roundtrip("option", undefined); }); - it("enum", () => { - const complex = { - tag: "Enum", - values: [{ tag: "First", values: undefined }], - }; - roundtrip("complex", complex); + it("u256", () => { + roundtrip("u256", 1n); + expect(() => roundtrip("u256", -1n)).to.throw(/expected a positive value, got: -1/i); }); - it("asset", () => { - const complex = { tag: "Asset", values: [addr, 1n] }; - roundtrip("complex", complex); + it("i256", () => { + roundtrip("i256", -1n); }); - it("void", () => { - const complex = { tag: "Void", values: undefined }; - roundtrip("complex", complex); + it("string", () => { + roundtrip("string", "hello"); + }); + it("tuple_strukt", () => { + const arg = [{ a: 0, b: true, c: "hello" }, { tag: "First" }]; + roundtrip("tuple_strukt", arg); }); - }); - 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); - }); + 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({ +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(), - }), + 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({ +})); +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, - }), + 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 index b9481d2a1..2d8e0c881 100644 --- a/test/unit/contract_spec.ts +++ b/test/unit/contract_spec.ts @@ -1,23 +1,36 @@ -import { xdr, Address, ContractSpec } from "../../lib"; +import { xdr, Address, ContractSpec, Keypair } from "../.."; +import { JSONSchemaFaker } from "json-schema-faker"; -//@ts-ignore import spec from "../spec.json"; import { expect } from "chai"; const publicKey = "GCBVOLOM32I7OD5TWZQCIXCXML3TK56MDY7ZMTAILIBQHHKPCVU42XYW"; const addr = Address.fromString(publicKey); let SPEC: ContractSpec; +JSONSchemaFaker.format("address", () => { + let keypair = Keypair.random(); + return keypair.publicKey(); +}); + +JSONSchemaFaker.format("bigint", (value) => { + let s = JSONSchemaFaker.generate(value); + return BigInt(s!.toString()!); + +}); + +const ints = ["i64", "u64", "i128", "u128", "i256", "u256"]; before(() => { SPEC = new ContractSpec(spec); -}) +}); it("throws if no entries", () => { - expect(() => new ContractSpec([])).to.throw(/Contract spec must have at least one entry/i); + 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)) { @@ -29,6 +42,60 @@ describe("Can round trip custom types", function () { return fn.outputs()[0]; } + async function jsonSchema_roundtrip( + spec: ContractSpec, + funcName: string, + num: number = 100 + ) { + let funcSpec = spec.jsonSchema(funcName); + + for (let i = 0; i < num; i++) { + let arg = await JSONSchemaFaker.resolve(funcSpec)!; + // @ts-ignore + let res = arg.args; + try { + let scVal = SPEC.funcArgsToScVals(funcName, res)[0]; + let result = SPEC.funcResToNative(funcName, scVal); + if (ints.some((i) => funcName.includes(i))) { + res[funcName] = BigInt(res[funcName]); + // result = result.toString(); + // if (result.startsWith("0") && result.length > 1) { + // result = result.slice(1); + // } + } + if (funcName.startsWith("bytes")) { + res[funcName] = Buffer.from(res[funcName], "base64"); + } + expect(res[funcName]).deep.equal(result); + } catch (e) { + console.error( + funcName, + JSON.stringify(arg, null, 2), + + "\n", + //@ts-ignore + JSON.stringify(funcSpec.definitions![funcName]["properties"], null, 2) + + ); + throw e; + } + } + } + + describe("Json Schema", () => { + SPEC = new ContractSpec(spec); + let names = SPEC.funcs().map((f) => f.name().toString()); + const banned = ["strukt_hel", "not", "woid", "val", "multi_args"]; + names + .filter((name) => !name.includes("fail")) + .filter((name) => !banned.includes(name)) + .forEach((name) => { + it(name, async () => { + await jsonSchema_roundtrip(SPEC, name); + }); + }); + }); + function roundtrip(funcName: string, input: any, typeName?: string) { let type = getResultType(funcName); let ty = typeName ?? funcName; @@ -57,16 +124,16 @@ describe("Can round trip custom types", function () { describe("simple", () => { it("first", () => { - const simple = { tag: "First", values: undefined } as const; + const simple = { tag: "First" } as const; roundtrip("simple", simple); }); it("simple second", () => { - const simple = { tag: "Second", values: undefined } as const; + const simple = { tag: "Second" } as const; roundtrip("simple", simple); }); it("simple third", () => { - const simple = { tag: "Third", values: undefined } as const; + const simple = { tag: "Third" } as const; roundtrip("simple", simple); }); }); @@ -83,12 +150,7 @@ describe("Can round trip custom types", function () { it("tuple", () => { const complex = { tag: "Tuple", - values: [ - [ - { a: 0, b: true, c: "hello" }, - { tag: "First", values: undefined }, - ], - ], + values: [[{ a: 0, b: true, c: "hello" }, { tag: "First" }]], } as const; roundtrip("complex", complex); }); @@ -96,24 +158,24 @@ describe("Can round trip custom types", function () { it("enum", () => { const complex = { tag: "Enum", - values: [{ tag: "First", values: undefined }], + values: [{ tag: "First" }], } as const; roundtrip("complex", complex); }); it("asset", () => { - const complex = { tag: "Asset", values: [addr, 1n] } as const; + const complex = { tag: "Asset", values: [addr.toString(), 1n] } as const; roundtrip("complex", complex); }); it("void", () => { - const complex = { tag: "Void", values: undefined } as const; + const complex = { tag: "Void" } as const; roundtrip("complex", complex); }); }); it("addresse", () => { - roundtrip("addresse", addr); + roundtrip("addresse", addr.toString()); }); it("bytes", () => { @@ -153,8 +215,10 @@ describe("Can round trip custom types", function () { 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); + map.set(3, "hahaha"); + expect(() => roundtrip("map", map)).to.throw( + /invalid type scSpecTypeBool specified for string value/i + ); }); it("vec", () => { @@ -174,7 +238,9 @@ describe("Can round trip custom types", function () { it("u256", () => { roundtrip("u256", 1n); - expect(() =>roundtrip("u256", -1n)).to.throw(/expected a positive value, got: -1/i) + expect(() => roundtrip("u256", -1n)).to.throw( + /expected a positive value, got: -1/i + ); }); it("i256", () => { @@ -186,10 +252,7 @@ describe("Can round trip custom types", function () { }); it("tuple_strukt", () => { - const arg = [ - { a: 0, b: true, c: "hello" }, - { tag: "First", values: undefined }, - ] as const; + const arg = [{ a: 0, b: true, c: "hello" }, { tag: "First" }] as const; roundtrip("tuple_strukt", arg); }); diff --git a/yarn.lock b/yarn.lock index 8dfd16a87..067ae76b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1481,7 +1481,7 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== @@ -2453,6 +2453,11 @@ call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.4, call-bind@^1.0.5: get-intrinsic "^1.2.1" set-function-length "^1.1.1" +call-me-maybe@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.2.tgz#03f964f19522ba643b1b0693acb9152fe2074baa" + integrity sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ== + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -3864,6 +3869,11 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" +format-util@^1.0.3: + version "1.0.5" + resolved "https://registry.yarnpkg.com/format-util/-/format-util-1.0.5.tgz#1ffb450c8a03e7bccffe40643180918cc297d271" + integrity sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg== + formidable@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.1.2.tgz#fa973a2bec150e4ce7cac15589d7a25fc30ebd89" @@ -4774,7 +4784,7 @@ js-yaml@4.1.0, js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -js-yaml@^3.13.1, js-yaml@^3.7.0: +js-yaml@^3.12.1, js-yaml@^3.13.1, js-yaml@^3.7.0: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== @@ -4835,6 +4845,23 @@ json-parse-even-better-errors@^2.3.1: resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== +json-schema-faker@^0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/json-schema-faker/-/json-schema-faker-0.5.4.tgz#48395d17032972755e4703bf5b05fc1e494ec45d" + integrity sha512-DdRRnRNSxkQVXEsUUXzAtvBpsROZHvM59/LQcV6+3gQVMvaeMsqfNKN3ivRwaiahTW7pvxa+LJfOaPP+nhFo4g== + dependencies: + json-schema-ref-parser "^6.1.0" + jsonpath-plus "^7.2.0" + +json-schema-ref-parser@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/json-schema-ref-parser/-/json-schema-ref-parser-6.1.0.tgz#30af34aeab5bee0431da805dac0eb21b574bf63d" + integrity sha512-pXe9H1m6IgIpXmE5JSb8epilNTGsmTb2iPohAXpOdhqGFbQjNeHHsZxU+C8w6T81GZxSPFLeUoqDJmzxx5IGuw== + dependencies: + call-me-maybe "^1.0.1" + js-yaml "^3.12.1" + ono "^4.0.11" + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -4894,6 +4921,11 @@ jsonify@^0.0.1: resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978" integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg== +jsonpath-plus@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/jsonpath-plus/-/jsonpath-plus-7.2.0.tgz#7ad94e147b3ed42f7939c315d2b9ce490c5a3899" + integrity sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA== + jsprim@^1.2.2: version "1.4.2" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" @@ -5705,6 +5737,13 @@ onetime@^6.0.0: dependencies: mimic-fn "^4.0.0" +ono@^4.0.11: + version "4.0.11" + resolved "https://registry.yarnpkg.com/ono/-/ono-4.0.11.tgz#c7f4209b3e396e8a44ef43b9cedc7f5d791d221d" + integrity sha512-jQ31cORBFE6td25deYeD80wxKBMj+zBmHTrVxnc6CKhx8gho6ipmWM5zj/oeoqioZ99yqBls9Z/9Nss7J26G2g== + dependencies: + format-util "^1.0.3" + open@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/open/-/open-9.1.0.tgz#684934359c90ad25742f5a26151970ff8c6c80b6"