diff --git a/packages/js/client/jest.config.js b/packages/js/client/jest.config.js index 55919b7e0d..5a41a18e7d 100644 --- a/packages/js/client/jest.config.js +++ b/packages/js/client/jest.config.js @@ -3,7 +3,7 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', testMatch: ["**/?(*.)+(spec|test).[jt]s?(x)"], - modulePathIgnorePatterns: ["./src/__tests__/e2e/wasm-rs.spec.ts"], + modulePathIgnorePatterns: ["./src/__tests__/e2e/wasm-rs.spec.ts", ".polywrap"], globals: { 'ts-jest': { diagnostics: false diff --git a/packages/js/client/src/__tests__/core/error-structure.spec.ts b/packages/js/client/src/__tests__/core/error-structure.spec.ts new file mode 100644 index 0000000000..1dbf76bc3f --- /dev/null +++ b/packages/js/client/src/__tests__/core/error-structure.spec.ts @@ -0,0 +1,314 @@ +import { GetPathToTestWrappers } from "@polywrap/test-cases"; +import { Uri, PolywrapClient } from "../.."; +import { buildWrapper } from "@polywrap/test-env-js"; +import { WrapError, WrapErrorCode } from "@polywrap/core-js"; +import { mockPluginRegistration } from "../helpers/mockPluginRegistration"; + +jest.setTimeout(660000); + +// AS +const simpleWrapperPath = `${GetPathToTestWrappers()}/wasm-as/simple`; +const simpleWrapperUri = new Uri(`fs/${simpleWrapperPath}/build`); + +const subinvokeErrorWrapperPath = `${GetPathToTestWrappers()}/wasm-as/subinvoke-error/invoke`; +const subinvokeErrorWrapperUri = new Uri(`fs/${subinvokeErrorWrapperPath}/build`); + +const badMathWrapperPath = `${GetPathToTestWrappers()}/wasm-as/subinvoke-error/0-subinvoke`; +const badMathWrapperUri = new Uri(`fs/${badMathWrapperPath}/build`); + +const badUtilWrapperPath = `${GetPathToTestWrappers()}/wasm-as/subinvoke-error/1-subinvoke`; +const badUtilWrapperUri = new Uri(`fs/${badUtilWrapperPath}/build`); + +const incompatibleWrapperPath = `${GetPathToTestWrappers()}/wasm-as/simple-deprecated`; +const incompatibleWrapperUri = new Uri(`fs/${incompatibleWrapperPath}`); + +// RS +const invalidTypesWrapperPath = `${GetPathToTestWrappers()}/wasm-rs/invalid-types`; +const invalidTypesWrapperUri = new Uri(`fs/${invalidTypesWrapperPath}/build`); + +describe("Error structure", () => { + + let client: PolywrapClient; + + beforeAll(async () => { + await buildWrapper(simpleWrapperPath, undefined, true); + await buildWrapper(badUtilWrapperPath, undefined, true); + await buildWrapper(badMathWrapperPath, undefined, true); + await buildWrapper(subinvokeErrorWrapperPath, undefined, true); + await buildWrapper(invalidTypesWrapperPath, undefined, true); + + client = new PolywrapClient({ + packages: [mockPluginRegistration("plugin/mock")], + redirects: [ + { + from: "ens/bad-math.eth", + to: badMathWrapperUri, + }, + { + from: "ens/bad-util.eth", + to: badUtilWrapperUri, + } + ] + }) + }); + + describe("URI resolution", () => { + test("Invoke a wrapper that is not found", async () => { + const result = await client.invoke({ + uri: simpleWrapperUri.uri + "-not-found", + method: "simpleMethod", + args: { + arg: "test", + }, + }); + + expect(result.ok).toBeFalsy(); + if (result.ok) throw Error("should never happen"); + + expect(result.error?.name).toEqual("WrapError"); + expect(result.error?.code).toEqual(WrapErrorCode.URI_NOT_FOUND); + expect(result.error?.reason.startsWith("Unable to find URI ")).toBeTruthy(); + expect(result.error?.uri.endsWith("packages/test-cases/cases/wrappers/wasm-as/simple/build-not-found")).toBeTruthy(); + expect(result.error?.resolutionStack).toBeTruthy(); + }); + + test("Subinvoke a wrapper that is not found", async () => { + const result = await client.invoke({ + uri: subinvokeErrorWrapperUri.uri, + method: "subWrapperNotFound", + args: { + a: 1, + b: 1, + }, + }); + + expect(result.ok).toBeFalsy(); + if (result.ok) throw Error("should never happen"); + + expect(result.error?.name).toEqual("WrapError"); + expect(result.error?.code).toEqual(WrapErrorCode.WRAPPER_INVOKE_ABORTED); + expect(result.error?.reason.startsWith("SubInvocation exception encountered")).toBeTruthy(); + expect(result.error?.uri.endsWith("packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/invoke/build")).toBeTruthy(); + expect(result.error?.method).toEqual("subWrapperNotFound"); + expect(result.error?.args).toEqual("{\n \"a\": 1,\n \"b\": 1\n}"); + expect(result.error?.source).toEqual({ file: "~lib/@polywrap/wasm-as/containers/Result.ts", row: 171, col: 13 }); + + expect(result.error?.innerError instanceof WrapError).toBeTruthy(); + const prev = result.error?.innerError as WrapError; + expect(prev.name).toEqual("WrapError"); + expect(prev.code).toEqual(WrapErrorCode.URI_NOT_FOUND); + expect(prev.reason).toEqual("Unable to find URI wrap://ens/not-found.eth."); + expect(prev.uri).toEqual("wrap://ens/not-found.eth"); + expect(prev.resolutionStack).toBeTruthy(); + }); + }); + + describe("Wasm wrapper", () => { + test("Invoke a wrapper with malformed arguments - as", async () => { + const result = await client.invoke({ + uri: simpleWrapperUri.uri, + method: "simpleMethod", + args: { + arg: 3, + }, + }); + + expect(result.ok).toBeFalsy(); + if (result.ok) throw Error("should never happen"); + + expect(result.error?.name).toEqual("WrapError"); + expect(result.error?.code).toEqual(WrapErrorCode.WRAPPER_INVOKE_ABORTED); + expect(result.error?.reason.startsWith("__wrap_abort:")).toBeTruthy(); + expect(result.error?.uri.endsWith("packages/test-cases/cases/wrappers/wasm-as/simple/build")).toBeTruthy(); + expect(result.error?.method).toEqual("simpleMethod"); + expect(result.error?.args).toEqual("{\n \"arg\": 3\n}"); + expect(result.error?.source).toEqual({ file: "~lib/@polywrap/wasm-as/msgpack/ReadDecoder.ts", row: 167, col: 5 }); + }); + + test("Invoke a wrapper with malformed arguments - rs", async () => { + const result = await client.invoke({ + uri: invalidTypesWrapperUri.uri, + method: "boolMethod", + args: { + arg: 3, + }, + }); + + expect(result.ok).toBeFalsy(); + if (result.ok) throw Error("should never happen"); + + expect(result.error?.name).toEqual("WrapError"); + expect(result.error?.code).toEqual(WrapErrorCode.WRAPPER_INVOKE_ABORTED); + expect(result.error?.reason.startsWith("__wrap_abort:")).toBeTruthy(); + expect(result.error?.uri.endsWith("packages/test-cases/cases/wrappers/wasm-rs/invalid-types/build")).toBeTruthy(); + expect(result.error?.method).toEqual("boolMethod"); + expect(result.error?.args).toEqual("{\n \"arg\": 3\n}"); + expect(result.error?.source).toEqual({ file: "src/wrap/module/wrapped.rs", row: 38, col: 13 }); + }); + + test("Invoke a wrapper method that doesn't exist", async () => { + const result = await client.invoke({ + uri: simpleWrapperUri.uri, + method: "complexMethod", + args: { + arg: "test", + }, + }); + + expect(result.ok).toBeFalsy(); + if (result.ok) throw Error("should never happen"); + + expect(result.error?.name).toEqual("WrapError"); + expect(result.error?.code).toEqual(WrapErrorCode.WRAPPER_INVOKE_FAIL); + expect(result.error?.reason.startsWith("Could not find invoke function")).toBeTruthy(); + expect(result.error?.uri.endsWith("packages/test-cases/cases/wrappers/wasm-as/simple/build")).toBeTruthy(); + expect(result.error?.method).toEqual("complexMethod"); + expect(result.error?.args).toEqual("{\n \"arg\": \"test\"\n}"); + expect(result.error?.toString().split( + WrapErrorCode.WRAPPER_INVOKE_FAIL.valueOf().toString() + ).length).toEqual(2); + expect(result.error?.innerError).toBeUndefined(); + }); + + test("Subinvoke error two layers deep", async () => { + const result = await client.invoke({ + uri: subinvokeErrorWrapperUri.uri, + method: "throwsInTwoSubinvokeLayers", + args: { + a: 1, + b: 1, + }, + }); + + expect(result.ok).toBeFalsy(); + if (result.ok) throw Error("should never happen"); + + expect(result.error?.name).toEqual("WrapError"); + expect(result.error?.code).toEqual(WrapErrorCode.WRAPPER_INVOKE_ABORTED); + expect(result.error?.reason.startsWith("SubInvocation exception encountered")).toBeTruthy(); + expect(result.error?.uri.endsWith("packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/invoke/build")).toBeTruthy(); + expect(result.error?.method).toEqual("throwsInTwoSubinvokeLayers"); + expect(result.error?.args).toEqual("{\n \"a\": 1,\n \"b\": 1\n}"); + expect(result.error?.source).toEqual({ file: "~lib/@polywrap/wasm-as/containers/Result.ts", row: 171, col: 13 }); + + expect(result.error?.innerError instanceof WrapError).toBeTruthy(); + const prev = result.error?.innerError as WrapError; + expect(prev.name).toEqual("WrapError"); + expect(prev.code).toEqual(WrapErrorCode.WRAPPER_INVOKE_ABORTED); + expect(prev.reason.startsWith("SubInvocation exception encountered")).toBeTruthy(); + expect(prev.uri).toEqual("wrap://ens/bad-math.eth"); + expect(prev.method).toEqual("subInvokeWillThrow"); + expect(prev.args).toEqual("{\n \"0\": 130,\n \"1\": 161,\n \"2\": 97,\n \"3\": 1,\n \"4\": 161,\n \"5\": 98,\n \"6\": 1\n}"); + expect(prev.source).toEqual({ file: "~lib/@polywrap/wasm-as/containers/Result.ts", row: 171, col: 13 }); + + expect(prev.innerError instanceof WrapError).toBeTruthy(); + const prevOfPrev = prev.innerError as WrapError; + expect(prevOfPrev.name).toEqual("WrapError"); + expect(prevOfPrev.code).toEqual(WrapErrorCode.WRAPPER_INVOKE_ABORTED); + expect(prevOfPrev.reason).toEqual("__wrap_abort: I threw an error!"); + expect(prevOfPrev.uri.endsWith("wrap://ens/bad-util.eth")).toBeTruthy(); + expect(prevOfPrev.method).toEqual("iThrow"); + expect(prevOfPrev.args).toEqual("{\n \"0\": 129,\n \"1\": 161,\n \"2\": 97,\n \"3\": 0\n}"); + expect(prevOfPrev.source).toEqual({ file: "src/index.ts", row: 5, col: 5 }); + }); + + test("Invoke a wrapper of incompatible version", async () => { + const result = await client.invoke({ + uri: incompatibleWrapperUri.uri, + method: "simpleMethod", + args: { + arg: "test", + }, + }); + + expect(result.ok).toBeFalsy(); + if (result.ok) throw Error("should never happen"); + + expect(result.error?.name).toEqual("WrapError"); + expect(result.error?.code).toEqual(WrapErrorCode.URI_RESOLVER_ERROR); + expect(result.error?.uri.endsWith("packages/test-cases/cases/wrappers/wasm-as/simple-deprecated")).toBeTruthy(); + expect(result.error?.resolutionStack).toBeDefined(); + expect(`${result.error?.cause}`).toContain(`Unrecognized WrapManifest schema version "0.0.1"`); + }); + }); + + describe("Plugin wrapper", () => { + test("Invoke a plugin wrapper with malformed args", async () => { + const result = await client.invoke({ + uri: "wrap://ens/fs.polywrap.eth", + method: "readFile", + args: { + pathh: "packages/js/client/src/__tests__/core/index.ts", + }, + }); + + expect(result.ok).toBeFalsy(); + if (result.ok) throw Error("should never happen"); + + expect(result.error?.name).toEqual("WrapError"); + expect(result.error?.code).toEqual(WrapErrorCode.WRAPPER_INVOKE_ABORTED); + expect(result.error?.reason).toEqual("The \"path\" argument must be of type string or an instance of Buffer or URL. Received undefined"); + expect(result.error?.uri).toEqual("wrap://ens/fs.polywrap.eth"); + expect(result.error?.method).toEqual("readFile"); + expect(result.error?.args).toContain("{\n \"pathh\": \"packages/js/client/src/__tests__/core/index.ts\"\n}"); + expect(result.error?.source).toEqual({ file: "node:internal/fs/promises", row: 450, col: 10 }); + }); + + test("Invoke a plugin wrapper with a method that doesn't exist", async () => { + const result = await client.invoke({ + uri: "wrap://ens/fs.polywrap.eth", + method: "readFileNotFound", + args: { + path: __dirname + "/index.ts", + }, + }); + + expect(result.ok).toBeFalsy(); + if (result.ok) throw Error("should never happen"); + + expect(result.error?.name).toEqual("WrapError"); + expect(result.error?.code).toEqual(WrapErrorCode.WRAPPER_METHOD_NOT_FOUND); + expect(result.error?.reason.startsWith("Plugin missing method ")).toBeTruthy(); + expect(result.error?.uri).toEqual("wrap://ens/fs.polywrap.eth"); + expect(result.error?.method).toEqual("readFileNotFound"); + }); + + test("Invoke a plugin wrapper that throws explicitly", async () => { + const result = await client.invoke({ + uri: "wrap://plugin/mock", + method: "methodThatThrows", + }); + + expect(result.ok).toBeFalsy(); + if (result.ok) throw Error("should never happen"); + + expect(result.error?.name).toEqual("WrapError"); + expect(result.error?.code).toEqual(WrapErrorCode.WRAPPER_INVOKE_ABORTED); + expect(result.error?.reason).toEqual("I'm throwing!"); + expect(result.error?.uri).toEqual("wrap://plugin/mock"); + expect(result.error?.source?.file?.endsWith("packages/js/client/src/__tests__/helpers/mockPluginRegistration.ts")).toBeTruthy(); + expect(result.error?.source?.row).toEqual(13); + expect(result.error?.source?.col).toEqual(17); + }); + + test("Invoke a plugin wrapper that throws unexpectedly", async () => { + const result = await client.invoke({ + uri: "wrap://ens/fs.polywrap.eth", + method: "readFile", + args: { + path: "./this/path/does/not/exist.ts", + }, + }); + + expect(result.ok).toBeFalsy(); + if (result.ok) throw Error("should never happen"); + + expect(result.error?.name).toEqual("WrapError"); + expect(result.error?.code).toEqual(WrapErrorCode.WRAPPER_INVOKE_ABORTED); + expect(result.error?.reason.startsWith("ENOENT: no such file or directory")).toBeTruthy(); + expect(result.error?.uri).toEqual("wrap://ens/fs.polywrap.eth"); + expect(result.error?.method).toEqual("readFile"); + expect(result.error?.args).toEqual("{\n \"path\": \"./this/path/does/not/exist.ts\"\n}"); + }); + }); +}); diff --git a/packages/js/client/src/__tests__/core/sanity.spec.ts b/packages/js/client/src/__tests__/core/sanity.spec.ts index 88e9e9c956..71112210e8 100644 --- a/packages/js/client/src/__tests__/core/sanity.spec.ts +++ b/packages/js/client/src/__tests__/core/sanity.spec.ts @@ -102,7 +102,7 @@ describe("sanity", () => { resultError = (result as { error: Error }).error; expect(result.ok).toBeFalsy(); expect(resultError).toBeTruthy(); - expect(resultError.message).toContain("Error resolving URI"); + expect(resultError.message).toContain("Unable to find URI"); await buildWrapper(greetingPath, undefined, true); diff --git a/packages/js/client/src/__tests__/e2e/test-cases.ts b/packages/js/client/src/__tests__/e2e/test-cases.ts index 9a0c3176ad..af7f276571 100644 --- a/packages/js/client/src/__tests__/e2e/test-cases.ts +++ b/packages/js/client/src/__tests__/e2e/test-cases.ts @@ -374,7 +374,7 @@ export const runInvalidTypesTest = async (client: CoreClient, uri: string) => { arg: 10, }, }); - invalidBoolIntSent = invalidBoolIntSent as { ok: false; error: Error }; + invalidBoolIntSent = invalidBoolIntSent as ErrResult; expect(invalidBoolIntSent.error).toBeTruthy(); expect(invalidBoolIntSent.error?.message).toMatch( /Property must be of type 'bool'. Found 'int'./ diff --git a/packages/js/client/src/__tests__/helpers/mockPluginRegistration.ts b/packages/js/client/src/__tests__/helpers/mockPluginRegistration.ts index 9b1b72fa90..7dc2411b3d 100644 --- a/packages/js/client/src/__tests__/helpers/mockPluginRegistration.ts +++ b/packages/js/client/src/__tests__/helpers/mockPluginRegistration.ts @@ -8,6 +8,9 @@ export const mockPluginRegistration = (uri: string | Uri) => { () => ({ simpleMethod: (_: unknown): string => { return "plugin response"; + }, + methodThatThrows: (_: unknown): string => { + throw Error("I'm throwing!"); } }) ), diff --git a/packages/js/core-client/src/PolywrapCoreClient.ts b/packages/js/core-client/src/PolywrapCoreClient.ts index 1fcd2b8db7..27b58cf32f 100644 --- a/packages/js/core-client/src/PolywrapCoreClient.ts +++ b/packages/js/core-client/src/PolywrapCoreClient.ts @@ -1,4 +1,3 @@ -import { UriResolverError } from "./UriResolverError"; import { PolywrapCoreClientConfig } from "./PolywrapCoreClientConfig"; import { @@ -21,6 +20,8 @@ import { InvokeResult, ValidateOptions, buildCleanUriHistory, + WrapError, + WrapErrorCode, } from "@polywrap/core-js"; import { msgpackEncode, msgpackDecode } from "@polywrap/msgpack-js"; import { @@ -148,13 +149,12 @@ export class PolywrapCoreClient implements CoreClient { * returns a package's wrap manifest * * @param uri - a wrap URI - * @param options - { noValidate?: boolean } * @returns a Result containing the WrapManifest if the request was successful */ @Tracer.traceMethod("PolywrapClient: getManifest") public async getManifest( uri: TUri - ): Promise> { + ): Promise> { const load = await this.loadWrapper(Uri.from(uri), undefined); if (!load.ok) { return load; @@ -176,14 +176,22 @@ export class PolywrapCoreClient implements CoreClient { public async getFile( uri: TUri, options: GetFileOptions - ): Promise> { + ): Promise> { const load = await this.loadWrapper(Uri.from(uri), undefined); if (!load.ok) { return load; } const wrapper = load.value; - return await wrapper.getFile(options); + const result = await wrapper.getFile(options); + if (!result.ok) { + const error = new WrapError(result.error?.message, { + code: WrapErrorCode.CLIENT_GET_FILE_ERROR, + uri: uri.toString(), + }); + return ResultErr(error); + } + return ResultOk(result.value); } /** @@ -198,7 +206,7 @@ export class PolywrapCoreClient implements CoreClient { public async getImplementations( uri: TUri, options: GetImplementationsOptions = {} - ): Promise> { + ): Promise> { const isUriTypeString = typeof uri === "string"; const applyResolution = !!options.applyResolution; @@ -402,7 +410,7 @@ export class PolywrapCoreClient implements CoreClient { uri: Uri, resolutionContext?: IUriResolutionContext, options?: DeserializeManifestOptions - ): Promise> { + ): Promise> { Tracer.setAttribute("label", `Wrapper loaded: ${uri}`, TracingLevel.High); if (!resolutionContext) { @@ -415,35 +423,37 @@ export class PolywrapCoreClient implements CoreClient { }); if (!result.ok) { + const history = buildCleanUriHistory(resolutionContext.getHistory()); + + let error: WrapError; if (result.error) { - return ResultErr(new UriResolverError(result.error, resolutionContext)); + error = new WrapError("A URI Resolver returned an error.", { + code: WrapErrorCode.URI_RESOLVER_ERROR, + uri: uri.uri, + resolutionStack: history, + cause: result.error, + }); } else { - return ResultErr( - Error( - `Error resolving URI "${ - uri.uri - }"\nResolution Stack: ${JSON.stringify( - buildCleanUriHistory(resolutionContext.getHistory()), - null, - 2 - )}` - ) - ); + error = new WrapError("Error resolving URI", { + code: WrapErrorCode.URI_RESOLUTION_ERROR, + uri: uri.uri, + resolutionStack: history, + }); } + + return ResultErr(error); } const uriPackageOrWrapper = result.value; if (uriPackageOrWrapper.type === "uri") { - const error = Error( - `Error resolving URI "${uri.uri}"\nURI not found ${ - uriPackageOrWrapper.uri.uri - }\nResolution Stack: ${JSON.stringify( - buildCleanUriHistory(resolutionContext.getHistory()), - null, - 2 - )}` - ); + const message = `Unable to find URI ${uriPackageOrWrapper.uri.uri}.`; + const history = buildCleanUriHistory(resolutionContext.getHistory()); + const error = new WrapError(message, { + code: WrapErrorCode.URI_NOT_FOUND, + uri: uri.uri, + resolutionStack: history, + }); return ResultErr(error); } @@ -451,7 +461,12 @@ export class PolywrapCoreClient implements CoreClient { const result = await uriPackageOrWrapper.package.createWrapper(options); if (!result.ok) { - return result; + const error = new WrapError(result.error?.message, { + code: WrapErrorCode.CLIENT_LOAD_WRAPPER_ERROR, + uri: uri.uri, + cause: result.error, + }); + return ResultErr(error); } return ResultOk(result.value); @@ -460,34 +475,43 @@ export class PolywrapCoreClient implements CoreClient { } } - @Tracer.traceMethod("PolywrapClient: validateConfig") + @Tracer.traceMethod("PolywrapClient: validate") public async validate( uri: TUri, options: ValidateOptions - ): Promise> { + ): Promise> { const wrapper = await this.loadWrapper(Uri.from(uri)); if (!wrapper.ok) { - return ResultErr(new Error(wrapper.error?.message)); + return wrapper; } const { abi } = await wrapper.value.getManifest(); const importedModules: ImportedModuleDefinition[] = abi.importedModuleTypes || []; - const importUri = (importedModuleType: ImportedModuleDefinition) => { - return this.tryResolveUri({ uri: importedModuleType.uri }); + const importUri = async ( + importedModuleType: ImportedModuleDefinition + ): Promise<{ + uri: string; + result: Result; + }> => { + const uri = importedModuleType.uri; + return { + uri, + result: await this.tryResolveUri({ uri }), + }; }; const resolvedModules = await Promise.all(importedModules.map(importUri)); - const modulesNotFound = resolvedModules.filter(({ ok }) => !ok) as { - error: Error; - }[]; + const modulesNotFound = resolvedModules + .filter(({ result }) => !result.ok) + .map(({ uri }) => uri); if (modulesNotFound.length) { - const missingModules = modulesNotFound.map(({ error }) => { - const uriIndex = error?.message.indexOf("\n"); - return error?.message.substring(0, uriIndex); + const message = `The following URIs could not be resolved: ${modulesNotFound}`; + const error = new WrapError(message, { + code: WrapErrorCode.CLIENT_VALIDATE_RESOLUTION_FAIL, + uri: uri.toString(), }); - const error = new Error(JSON.stringify(missingModules)); return ResultErr(error); } @@ -506,7 +530,6 @@ export class PolywrapCoreClient implements CoreClient { ({ uri }) => importedModule.uri === uri ); - const errorMessage = `ABI from Uri: ${importedModule.uri} is not compatible with Uri: ${uri}`; for (const [i, _] of Object.keys(importedMethods).entries()) { const importedMethod = importedMethods[i]; @@ -514,9 +537,21 @@ export class PolywrapCoreClient implements CoreClient { const expectedMethod = expectedMethods?.methods[i]; const areEqual = compareSignature(importedMethod, expectedMethod); - if (!areEqual) return ResultErr(new Error(errorMessage)); + if (!areEqual) { + const message = `ABI from Uri: ${importedModule.uri} is not compatible with Uri: ${uri}`; + const error = new WrapError(message, { + code: WrapErrorCode.CLIENT_VALIDATE_ABI_FAIL, + uri: uri.toString(), + }); + return ResultErr(error); + } } else { - return ResultErr(new Error(errorMessage)); + const message = `ABI from Uri: ${importedModule.uri} is not compatible with Uri: ${uri}`; + const error = new WrapError(message, { + code: WrapErrorCode.CLIENT_VALIDATE_ABI_FAIL, + uri: uri.toString(), + }); + return ResultErr(error); } } } @@ -528,14 +563,18 @@ export class PolywrapCoreClient implements CoreClient { ); const resolverUris = await Promise.all(validateImportedModules); const invalidUris = resolverUris.filter(({ ok }) => !ok) as { - error: Error; + error: WrapError; }[]; + if (invalidUris.length) { - const missingUris = invalidUris.map(({ error }) => { - const uriIndex = error?.message.indexOf("\n"); - return error?.message.substring(0, uriIndex); + let message = "The following URIs failed validation:"; + for (const { error } of invalidUris) { + message += `\n${error.uri} -> ${error.reason}`; + } + const error = new WrapError(message, { + code: WrapErrorCode.CLIENT_VALIDATE_RECURSIVE_FAIL, + uri: uri.toString(), }); - const error = new Error(JSON.stringify(missingUris)); return ResultErr(error); } } diff --git a/packages/js/core-client/src/UriResolverError.ts b/packages/js/core-client/src/UriResolverError.ts deleted file mode 100644 index c58f024303..0000000000 --- a/packages/js/core-client/src/UriResolverError.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { IUriResolutionContext, buildCleanUriHistory } from "@polywrap/core-js"; - -export class UriResolverError< - TError extends unknown = undefined -> extends Error { - constructor( - public readonly resolverError: TError, - resolutionContext: IUriResolutionContext - ) { - super( - `An internal resolver error occurred while resolving a URI.\nResolution Stack: ${JSON.stringify( - buildCleanUriHistory(resolutionContext.getHistory()), - null, - 2 - )}` - ); - } -} diff --git a/packages/js/core/src/algorithms/GetImplementationsError.ts b/packages/js/core/src/algorithms/GetImplementationsError.ts deleted file mode 100644 index ef49d53cda..0000000000 --- a/packages/js/core/src/algorithms/GetImplementationsError.ts +++ /dev/null @@ -1,5 +0,0 @@ -export class GetImplementationsError extends Error { - constructor(public readonly internalError: TInternalError) { - super("Error occurred while getting implementations"); - } -} diff --git a/packages/js/core/src/algorithms/get-implementations.ts b/packages/js/core/src/algorithms/get-implementations.ts index 2865dffbe6..5083a9afc0 100644 --- a/packages/js/core/src/algorithms/get-implementations.ts +++ b/packages/js/core/src/algorithms/get-implementations.ts @@ -1,6 +1,11 @@ -import { Uri, InterfaceImplementations, CoreClient } from "../types"; +import { + Uri, + InterfaceImplementations, + CoreClient, + WrapError, + WrapErrorCode, +} from "../types"; import { IUriResolutionContext } from "../uri-resolution"; -import { GetImplementationsError } from "./GetImplementationsError"; import { applyResolution } from "./applyResolution"; import { Tracer } from "@polywrap/tracing-js"; @@ -13,7 +18,7 @@ export const getImplementations = Tracer.traceFunc( interfaces: readonly InterfaceImplementations[], client?: CoreClient, resolutionContext?: IUriResolutionContext - ): Promise> => { + ): Promise> => { const result: Uri[] = []; const addUniqueResult = (uri: Uri) => { @@ -26,7 +31,7 @@ export const getImplementations = Tracer.traceFunc( const addAllImplementationsFromImplementationsArray = async ( implementationsArray: readonly InterfaceImplementations[], wrapperInterfaceUri: Uri - ): Promise> => { + ): Promise> => { for (const interfaceImplementations of implementationsArray) { let fullyResolvedUri: Uri; if (client) { @@ -36,7 +41,12 @@ export const getImplementations = Tracer.traceFunc( resolutionContext ); if (!redirectsResult.ok) { - return redirectsResult; + const error = new WrapError("Failed to resolve redirects", { + uri: interfaceImplementations.interface.uri, + code: WrapErrorCode.CLIENT_GET_IMPLEMENTATIONS_ERROR, + cause: redirectsResult.error, + }); + return ResultErr(error); } fullyResolvedUri = redirectsResult.value; } else { @@ -61,7 +71,12 @@ export const getImplementations = Tracer.traceFunc( resolutionContext ); if (!redirectsResult.ok) { - return ResultErr(new GetImplementationsError(redirectsResult.error)); + const error = new WrapError("Failed to resolve redirects", { + uri: wrapperInterfaceUri.uri, + code: WrapErrorCode.CLIENT_GET_IMPLEMENTATIONS_ERROR, + cause: redirectsResult.error, + }); + return ResultErr(error); } finalUri = redirectsResult.value; } @@ -71,8 +86,6 @@ export const getImplementations = Tracer.traceFunc( finalUri ); - return addAllImp.ok - ? ResultOk(result) - : ResultErr(new GetImplementationsError(addAllImp.error)); + return addAllImp.ok ? ResultOk(result) : addAllImp; } ); diff --git a/packages/js/core/src/algorithms/index.ts b/packages/js/core/src/algorithms/index.ts index 7d9e2ed2f1..2983415b25 100644 --- a/packages/js/core/src/algorithms/index.ts +++ b/packages/js/core/src/algorithms/index.ts @@ -1,4 +1,3 @@ -export * from "./GetImplementationsError"; export * from "./applyResolution"; export * from "./get-implementations"; export * from "./clean-uri-history"; diff --git a/packages/js/core/src/interfaces/uri-resolver.ts b/packages/js/core/src/interfaces/uri-resolver.ts index 9b886a3da5..2be4064cbf 100644 --- a/packages/js/core/src/interfaces/uri-resolver.ts +++ b/packages/js/core/src/interfaces/uri-resolver.ts @@ -1,4 +1,4 @@ -import { Uri, Invoker } from "../"; +import { Uri, Invoker, WrapError } from "../"; import { Tracer } from "@polywrap/tracing-js"; import { Result } from "@polywrap/result"; @@ -15,7 +15,7 @@ export const module = { invoker: Invoker, wrapper: Uri, uri: Uri - ): Promise> => { + ): Promise> => { return invoker.invoke({ uri: wrapper.uri, method: `tryResolveUri`, @@ -32,7 +32,7 @@ export const module = { invoker: Invoker, wrapper: Uri, path: string - ): Promise> => { + ): Promise> => { return invoker.invoke({ uri: wrapper.uri, method: "getFile", diff --git a/packages/js/core/src/types/CoreClient.ts b/packages/js/core/src/types/CoreClient.ts index a81768fdc3..eb40e1364a 100644 --- a/packages/js/core/src/types/CoreClient.ts +++ b/packages/js/core/src/types/CoreClient.ts @@ -1,4 +1,4 @@ -import { Invoker, Uri, InterfaceImplementations, Env } from "."; +import { Invoker, Uri, InterfaceImplementations, Env, WrapError } from "."; import { IUriResolutionContext, IUriResolver } from "../uri-resolution"; import { UriResolverHandler } from "./UriResolver"; @@ -43,20 +43,20 @@ export interface CoreClient extends Invoker, UriResolverHandler { getManifest( uri: TUri - ): Promise>; + ): Promise>; getFile( uri: TUri, options: GetFileOptions - ): Promise>; + ): Promise>; getImplementations( uri: TUri, options: GetImplementationsOptions - ): Promise>; + ): Promise>; validate( uri: TUri, options?: ValidateOptions - ): Promise>; + ): Promise>; } diff --git a/packages/js/core/src/types/Invoke.ts b/packages/js/core/src/types/Invoke.ts index 3a0214f5fb..38bce7d87d 100644 --- a/packages/js/core/src/types/Invoke.ts +++ b/packages/js/core/src/types/Invoke.ts @@ -1,4 +1,4 @@ -import { Uri, Wrapper } from "."; +import { WrapError, Uri, Wrapper } from "."; import { IUriResolutionContext } from "../uri-resolution"; import { Result } from "@polywrap/result"; @@ -30,7 +30,7 @@ export interface InvokeOptions { * * @template TData Type of the invoke result data. */ -export type InvokeResult = Result; +export type InvokeResult = Result; export interface InvokerOptions extends InvokeOptions { diff --git a/packages/js/core/src/types/WrapError.ts b/packages/js/core/src/types/WrapError.ts new file mode 100644 index 0000000000..3fcee3c6cb --- /dev/null +++ b/packages/js/core/src/types/WrapError.ts @@ -0,0 +1,274 @@ +import { CleanResolutionStep } from "../algorithms"; + +export type ErrorSource = Readonly<{ + file?: string; + row?: number; + col?: number; +}>; + +/** +Wrap error codes provide additional context to WrapErrors. + +Error code naming convention (approximate): + type of handler + type of functionality + piece of functionality + ==> handler_typeFn_pieceFn + +Error code map: + 0 -> Invalid + 1-25 -> Client + 26-50 -> URI resolution + 51-75 -> Wrapper invocation & sub-invocation + 76-255 -> Unallocated + */ +export enum WrapErrorCode { + CLIENT_LOAD_WRAPPER_ERROR = 1, + CLIENT_GET_FILE_ERROR, + CLIENT_GET_IMPLEMENTATIONS_ERROR, + CLIENT_VALIDATE_RESOLUTION_FAIL, + CLIENT_VALIDATE_ABI_FAIL, + CLIENT_VALIDATE_RECURSIVE_FAIL, + URI_RESOLUTION_ERROR = 26, + URI_RESOLVER_ERROR, + URI_NOT_FOUND, + WRAPPER_INVOKE_ABORTED = 51, + WRAPPER_SUBINVOKE_ABORTED, + WRAPPER_INVOKE_FAIL, + WRAPPER_READ_FAIL, + WRAPPER_INTERNAL_ERROR, + WRAPPER_METHOD_NOT_FOUND, + WRAPPER_ARGS_MALFORMED, +} + +export interface WrapErrorOptions { + code: WrapErrorCode; + uri: string; + method?: string; + args?: string; + source?: ErrorSource; + resolutionStack?: CleanResolutionStep; + cause?: unknown; + innerError?: WrapError; +} + +type RegExpGroups = + | (RegExpExecArray & { + groups?: { [name in T]: string | undefined } | { [key: string]: string }; + }) + | null; + +export class WrapError extends Error { + readonly name: string = "WrapError"; + readonly code: WrapErrorCode; + readonly reason: string; + readonly uri: string; + readonly method?: string; + readonly args?: string; + readonly source?: ErrorSource; + readonly resolutionStack?: CleanResolutionStep; + readonly cause?: unknown; + readonly innerError?: WrapError; + + constructor(reason = "Encountered an exception.", options: WrapErrorOptions) { + super(WrapError.stringify(reason, options)); + + this.code = options.code; + this.reason = reason; + this.uri = options.uri; + this.method = options.method; + this.args = options.args; + this.source = options.source; + this.resolutionStack = options.resolutionStack; + this.cause = options.cause; + this.innerError = options.innerError; + + Object.setPrototypeOf(this, WrapError.prototype); + Error.captureStackTrace(this, this.constructor); + } + + private static re = new RegExp( + [ + /^(?:[A-Za-z_: ]*; )?WrapError: (?(?:.|\r|\n)*)/.source, + // there is some padding added to the number of words expected in an error code + /(?:\r\n|\r|\n)code: (?1?[0-9]{1,2}|2[0-4][0-9]|25[0-5]) (?:[A-Z]+ ?){1,5}/ + .source, + /(?:\r\n|\r|\n)uri: (?wrap:\/\/[A-Za-z0-9_-]+\/.+)/.source, + /(?:(?:\r\n|\r|\n)method: (?([A-Za-z_]{1}[A-Za-z0-9_]*)))?/ + .source, + /(?:(?:\r\n|\r|\n)args: (?\{(?:.|\r|\n)+} ))?/.source, + /(?:(?:\r\n|\r|\n)source: \{ file: "(?.+)", row: (?[0-9]+), col: (?[0-9]+) })?/ + .source, + /(?:(?:\r\n|\r|\n)uriResolutionStack: (?\[(?:.|\r|\n)+]))?/ + .source, + /(?:(?:\r\n|\r|\n){2}This exception was caused by the following exception:(?:\r\n|\r|\n)(?(?:.|\r|\n)+))?$/ + .source, + ].join("") + ); + + static parse(error: string): WrapError | undefined { + const delim = "\n\nAnother exception was encountered during execution:\n"; + const errorStrings = error.split(delim); + + // case: single WrapError or not a WrapError + if (errorStrings.length === 1) { + const args = WrapError._parse(error); + return args ? new WrapError(args.reason, args.options) : undefined; + } + + // case: stack of WrapErrors stringified + const errArgs = errorStrings.map(WrapError._parse); + + // iterate through args to assign `cause` and `prev` + let curr: WrapError | undefined = undefined; + for (let i = errArgs.length - 1; i >= 0; i--) { + const currArgs = errArgs[i]; + if (!currArgs) { + // should only happen if a user includes the delimiter in their error message + throw new Error("Failed to parse WrapError"); + } + curr = new WrapError(currArgs.reason, { + ...currArgs.options, + innerError: curr, + }); + } + return curr; + } + + toString(): string { + return `${this.name}: ${this.message}`; + } + + // parse a single WrapError, where the 'prev' property is undefined + private static _parse( + error: string + ): { reason: string; options: WrapErrorOptions } | undefined { + const result: RegExpGroups< + | "code" + | "reason" + | "uri" + | "method" + | "args" + | "file" + | "row" + | "col" + | "resolutionStack" + | "cause" + > = WrapError.re.exec(error); + if (!result) { + return undefined; + } + const { + code: codeStr, + reason, + uri, + method, + args, + file, + row, + col, + resolutionStack: resolutionStackStr, + cause, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + } = result.groups!; + + const code = parseInt(codeStr as string); + + const source: ErrorSource | undefined = file + ? { + file, + row: row ? parseInt(row) : undefined, + col: col ? parseInt(col) : undefined, + } + : undefined; + + const resolutionStack = resolutionStackStr + ? JSON.parse(resolutionStackStr) + : undefined; + + return { + reason: reason as string, + options: { + code, + uri: uri as string, + method, + args: args?.trim(), + source, + resolutionStack, + cause, + }, + }; + } + + private static stringify(reason: string, options: WrapErrorOptions) { + const { + code, + uri, + method, + args, + source, + resolutionStack, + cause, + innerError, + } = options; + const formattedCode = `${code} ${WrapErrorCode[code].replace(/_/g, " ")}`; + + // Some items are not always present + const maybeMethod = method ? `method: ${method}` : ""; + const maybeArgs = args ? `args: ${args} ` : ""; + // source is uses () instead of {} to facilitate regex + const maybeSource = source + ? `source: { file: "${source?.file}", row: ${source?.row}, col: ${source?.col} }` + : ""; + const maybeResolutionStack = resolutionStack + ? `uriResolutionStack: ${JSON.stringify(resolutionStack, null, 2)}` + : ""; + + const errorCause = WrapError.stringifyCause(cause); + const maybeCause = errorCause + ? `\nThis exception was caused by the following exception:\n${errorCause}` + : ""; + + const maybeDelim = innerError + ? `\nAnother exception was encountered during execution:\n${innerError}` + : ""; + + return [ + `${reason}`, + `code: ${formattedCode}`, + `uri: ${uri}`, + maybeMethod, + maybeArgs, + maybeSource, + maybeResolutionStack, + maybeCause, + maybeDelim, + ] + .filter((it) => !!it) + .join("\n"); + } + + private static stringifyCause(cause: unknown): string | undefined { + if (cause === undefined || cause === null) { + return undefined; + } else if (cause instanceof Error) { + return cause.toString(); + } else if (typeof cause === "object" && cause) { + if ( + cause.toString !== Object.prototype.toString && + typeof cause.toString === "function" + ) { + return cause.toString(); + } + return JSON.stringify(cause); + } else if ( + typeof cause === "function" && + cause.toString !== Object.prototype.toString && + typeof cause.toString === "function" + ) { + return cause.toString(); + } else { + return `${cause}`; + } + } +} diff --git a/packages/js/core/src/types/Wrapper.ts b/packages/js/core/src/types/Wrapper.ts index 5d5655b4b3..d2051d8b4f 100644 --- a/packages/js/core/src/types/Wrapper.ts +++ b/packages/js/core/src/types/Wrapper.ts @@ -33,7 +33,6 @@ export interface Wrapper extends Invocable { * Get a file from the Wrapper package. * * @param options Configuration options for file retrieval - * @param client The client instance requesting the file. */ getFile(options: GetFileOptions): Promise>; diff --git a/packages/js/core/src/types/index.ts b/packages/js/core/src/types/index.ts index b387559098..fa09582799 100644 --- a/packages/js/core/src/types/index.ts +++ b/packages/js/core/src/types/index.ts @@ -10,3 +10,4 @@ export * from "./IWrapPackage"; export * from "./IUriRedirect"; export * from "./IUriWrapper"; export * from "./IUriPackage"; +export * from "./WrapError"; diff --git a/packages/js/plugin/src/PluginModule.ts b/packages/js/plugin/src/PluginModule.ts index 739f0ff7f3..38c2f5730f 100644 --- a/packages/js/plugin/src/PluginModule.ts +++ b/packages/js/plugin/src/PluginModule.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { PluginMethod } from "./PluginMethod"; -import { CoreClient } from "@polywrap/core-js"; +import { CoreClient, WrapErrorCode } from "@polywrap/core-js"; import { Result, ResultErr, ResultOk } from "@polywrap/result"; export abstract class PluginModule< @@ -47,9 +47,13 @@ export abstract class PluginModule< ); } - const data = await fn(args, client); - - return ResultOk(data); + try { + const data = await fn(args, client); + return ResultOk(data); + } catch (e) { + e.code = WrapErrorCode.WRAPPER_INVOKE_ABORTED; + return ResultErr(e); + } } public getMethod< @@ -63,6 +67,6 @@ export abstract class PluginModule< PluginMethod >)[method]; - return fn.bind(this); + return fn?.bind(this); } } diff --git a/packages/js/plugin/src/PluginWrapper.ts b/packages/js/plugin/src/PluginWrapper.ts index b1540953e4..bdca73c016 100644 --- a/packages/js/plugin/src/PluginWrapper.ts +++ b/packages/js/plugin/src/PluginWrapper.ts @@ -1,4 +1,5 @@ import { PluginModule } from "./PluginModule"; +import { getErrorSource } from "./utils/getErrorSource"; import { Wrapper, @@ -8,6 +9,8 @@ import { Uri, GetFileOptions, isBuffer, + WrapError, + WrapErrorCode, } from "@polywrap/core-js"; import { WrapManifest } from "@polywrap/wrap-manifest-types-js"; import { msgpackDecode } from "@polywrap/msgpack-js"; @@ -53,7 +56,12 @@ export class PluginWrapper implements Wrapper { const args = options.args || {}; if (!this.module.getMethod(method)) { - return ResultErr(Error(`PluginWrapper: method "${method}" not found.`)); + const error = new WrapError(`Plugin missing method "${method}"`, { + code: WrapErrorCode.WRAPPER_METHOD_NOT_FOUND, + uri: options.uri.uri, + method, + }); + return ResultErr(error); } // Set the module's environment @@ -68,10 +76,16 @@ export class PluginWrapper implements Wrapper { Tracer.addEvent("msgpack-decoded", result); if (typeof result !== "object") { - const msgPackException = Error( - `PluginWrapper: decoded MsgPack args did not result in an object.\nResult: ${result}` + const error = new WrapError( + `Decoded MsgPack args did not result in an object.\nResult: ${result}`, + { + code: WrapErrorCode.WRAPPER_ARGS_MALFORMED, + uri: options.uri.uri, + method, + args: JSON.stringify(args), + } ); - return ResultErr(msgPackException); + return ResultErr(error); } jsArgs = result as Record; @@ -92,14 +106,19 @@ export class PluginWrapper implements Wrapper { encoded: false, }; } else { - const invocationException = Error( - `PluginWrapper: invocation exception encountered.\n` + - `uri: ${options.uri}\nmodule: ${module}\n` + - `method: ${method}\n` + - `args: ${JSON.stringify(jsArgs, null, 2)}\n` + - `exception: ${result.error?.message}` - ); - return ResultErr(invocationException); + const code = + (result.error as { code?: WrapErrorCode })?.code ?? + WrapErrorCode.WRAPPER_INVOKE_FAIL; + const reason = + result.error?.message ?? `Failed to invoke method "${method}"`; + const error = new WrapError(reason, { + code, + uri: options.uri.toString(), + method, + args: JSON.stringify(jsArgs, null, 2), + source: getErrorSource(result.error), + }); + return ResultErr(error); } } } diff --git a/packages/js/plugin/src/utils/PluginModuleWithMethods.ts b/packages/js/plugin/src/utils/PluginModuleWithMethods.ts index 665e264fff..6fa40f415b 100644 --- a/packages/js/plugin/src/utils/PluginModuleWithMethods.ts +++ b/packages/js/plugin/src/utils/PluginModuleWithMethods.ts @@ -4,8 +4,8 @@ import { PluginMethod } from "../PluginMethod"; import { PluginModule } from "../PluginModule"; import { GetPluginMethodsFunc } from "./GetPluginMethodsFunc"; -import { CoreClient } from "@polywrap/core-js"; -import { Result, ResultOk } from "@polywrap/result"; +import { CoreClient, WrapErrorCode } from "@polywrap/core-js"; +import { Result, ResultErr, ResultOk } from "@polywrap/result"; export class PluginModuleWithMethods< TEnv extends Record = Record @@ -25,16 +25,22 @@ export class PluginModuleWithMethods< const fn = this.getMethod(method); if (!fn) { - throw Error(`Plugin missing method "${method}"`); + return ResultErr(Error(`Plugin missing method "${method}"`)); } if (typeof fn !== "function") { - throw Error(`Plugin method "${method}" must be of type 'function'`); + return ResultErr( + Error(`Plugin method "${method}" must be of type 'function'`) + ); } - const data = await fn(args, client); - - return ResultOk(data); + try { + const data = await fn(args, client); + return ResultOk(data); + } catch (e) { + e.code = WrapErrorCode.WRAPPER_INVOKE_ABORTED; + return ResultErr(e); + } } getMethod< @@ -45,6 +51,6 @@ export class PluginModuleWithMethods< this )[method] as PluginMethod; - return fn.bind(this); + return fn?.bind(this); } } diff --git a/packages/js/plugin/src/utils/getErrorSource.ts b/packages/js/plugin/src/utils/getErrorSource.ts new file mode 100644 index 0000000000..8b737f432a --- /dev/null +++ b/packages/js/plugin/src/utils/getErrorSource.ts @@ -0,0 +1,31 @@ +import { ErrorSource } from "@polywrap/core-js"; + +type RegExpGroups = + | (RegExpExecArray & { + groups?: { [name in T]: string | undefined } | { [key: string]: string }; + }) + | null; + +const re = /\((?.*):(?\d+):(?\d+)\)$/; + +// retrieve the most recent line of source information for an error +export function getErrorSource(error?: Error): ErrorSource | undefined { + if (!error || !error.stack) return undefined; + + // find first source line in stack + const stack = error.stack?.split("\n"); + let i = 0; + for (i; i < stack.length && !stack[i].startsWith(` at`); i++) {} // eslint-disable-line no-empty + + const result: RegExpGroups<"file" | "row" | "col"> = re.exec(stack[i]); + if (!result) return undefined; + + const { file, row, col } = result.groups!; // eslint-disable-line @typescript-eslint/no-non-null-assertion + return file + ? { + file, + row: row ? parseInt(row) : undefined, + col: col ? parseInt(col) : undefined, + } + : undefined; +} diff --git a/packages/js/plugins/ethereum/src/__tests__/e2e.spec.ts b/packages/js/plugins/ethereum/src/__tests__/e2e.spec.ts index 9e6931b034..ec8b7a64d9 100644 --- a/packages/js/plugins/ethereum/src/__tests__/e2e.spec.ts +++ b/packages/js/plugins/ethereum/src/__tests__/e2e.spec.ts @@ -22,6 +22,7 @@ import { keccak256 } from "js-sha3"; import { Connections } from "../Connections"; import { Connection } from "../Connection"; import { getDefaultConfig } from "./helpers/getDefaultConfig"; +import { WrapError } from "@polywrap/core-js"; const { hash: namehash } = require("eth-ens-namehash"); const contracts = { @@ -935,7 +936,7 @@ describe("Ethereum Plugin", () => { uri, method: "requestAccounts", }); - result = result as { ok: false; error: Error | undefined }; + result = result as { ok: false; error: WrapError | undefined }; // eth_requestAccounts is not supported by Ganache // this RPC error indicates that the method call was attempted expect( diff --git a/packages/js/plugins/file-system/src/__tests__/e2e.spec.ts b/packages/js/plugins/file-system/src/__tests__/e2e.spec.ts index 4e9b2103dd..f0df413665 100644 --- a/packages/js/plugins/file-system/src/__tests__/e2e.spec.ts +++ b/packages/js/plugins/file-system/src/__tests__/e2e.spec.ts @@ -5,6 +5,7 @@ import { FileSystem_Module, FileSystem_EncodingEnum } from "../wrap"; import fs from "fs"; import path from "path"; import fileSystemEncodingToBufferEncoding from "../utils/fileSystemEncodingToBufferEncoding"; +import { WrapError } from "@polywrap/core-js"; jest.setTimeout(360000); @@ -61,7 +62,7 @@ describe("FileSystem plugin", () => { client ); - result = result as { ok: false; error: Error | undefined }; + result = result as { ok: false; error: WrapError | undefined }; expect(result.error).toBeTruthy(); expect(result.ok).toBeFalsy(); }); diff --git a/packages/js/plugins/http/src/__tests__/e2e/e2e.spec.ts b/packages/js/plugins/http/src/__tests__/e2e/e2e.spec.ts index 8c93d0c937..4b29dec15a 100644 --- a/packages/js/plugins/http/src/__tests__/e2e/e2e.spec.ts +++ b/packages/js/plugins/http/src/__tests__/e2e/e2e.spec.ts @@ -5,6 +5,7 @@ import { PolywrapClient } from "@polywrap/client-js"; import { UriResolver } from "@polywrap/uri-resolvers-js"; import nock from "nock"; +import { WrapError } from "@polywrap/core-js"; jest.setTimeout(360000); @@ -133,7 +134,7 @@ describe("e2e tests for HttpPlugin", () => { }, }); - response = response as { ok: false; error: Error | undefined }; + response = response as { ok: false; error: WrapError | undefined }; expect(response.error).toBeDefined(); expect(response.ok).toBeFalsy(); }); @@ -283,7 +284,7 @@ describe("e2e tests for HttpPlugin", () => { }, }); - response = response as { ok: false; error: Error | undefined }; + response = response as { ok: false; error: WrapError | undefined }; expect(response.error).toBeDefined(); expect(response.ok).toBeFalsy(); }); diff --git a/packages/js/plugins/ipfs/src/__tests__/e2e.spec.ts b/packages/js/plugins/ipfs/src/__tests__/e2e.spec.ts index b38febc227..ad27f00599 100644 --- a/packages/js/plugins/ipfs/src/__tests__/e2e.spec.ts +++ b/packages/js/plugins/ipfs/src/__tests__/e2e.spec.ts @@ -1,4 +1,4 @@ -import { Result } from "@polywrap/core-js"; +import { Result, WrapError } from "@polywrap/core-js"; import { initTestEnvironment, providers, @@ -138,10 +138,10 @@ describe("IPFS Plugin", () => { expect(result).toBeTruthy(); expect(result.ok).toBeFalsy(); - result = result as { ok: false; error: Error | undefined }; + result = result as { ok: false; error: WrapError | undefined }; expect(result.error).toBeTruthy(); - expect(result.error?.stack).toMatch("Timeout has been reached"); - expect(result.error?.stack).toMatch("Timeout: 1000"); + expect(result.error?.message).toMatch("Timeout has been reached"); + expect(result.error?.message).toMatch("Timeout: 1000"); const catPromiseWithTimeoutOverride = Ipfs_Module.cat( { @@ -164,8 +164,8 @@ describe("IPFS Plugin", () => { error: Error | undefined; }; expect(resultForOverride.error).toBeTruthy(); - expect(resultForOverride.error?.stack).toMatch("Timeout has been reached"); - expect(resultForOverride.error?.stack).toMatch("Timeout: 500"); + expect(resultForOverride.error?.message).toMatch("Timeout has been reached"); + expect(resultForOverride.error?.message).toMatch("Timeout: 500"); }); it("Should use provider from method options", async () => { diff --git a/packages/js/wasm/src/WasmPackage.ts b/packages/js/wasm/src/WasmPackage.ts index 3ab37e17a5..8e32997745 100644 --- a/packages/js/wasm/src/WasmPackage.ts +++ b/packages/js/wasm/src/WasmPackage.ts @@ -44,7 +44,12 @@ export class WasmPackage implements IWasmPackage { } const wrapManifest = result.value; - return ResultOk(await deserializeWrapManifest(wrapManifest, options)); + + try { + return ResultOk(await deserializeWrapManifest(wrapManifest, options)); + } catch (e) { + return ResultErr(e); + } } async getWasmModule(): Promise> { diff --git a/packages/js/wasm/src/WasmWrapper.ts b/packages/js/wasm/src/WasmWrapper.ts index 1cb83f16d0..344c7274e4 100644 --- a/packages/js/wasm/src/WasmWrapper.ts +++ b/packages/js/wasm/src/WasmWrapper.ts @@ -10,14 +10,17 @@ import { msgpackEncode } from "@polywrap/msgpack-js"; import { Tracer, TracingLevel } from "@polywrap/tracing-js"; import { AsyncWasmInstance } from "@polywrap/asyncify-js"; import { - Wrapper, - Uri, - InvokeOptions, CoreClient, - InvocableResult, - isBuffer, GetFileOptions, GetManifestOptions, + InvocableResult, + InvokeOptions, + isBuffer, + Uri, + Wrapper, + WrapError, + WrapErrorCode, + ErrorSource, } from "@polywrap/core-js"; import { Result, ResultErr, ResultOk } from "@polywrap/result"; @@ -151,7 +154,13 @@ export class WasmWrapper implements Wrapper { const args = options.args || {}; const wasmResult = await this._getWasmModule(); if (!wasmResult.ok) { - return wasmResult; + const error = new WrapError(wasmResult.error, { + code: WrapErrorCode.WRAPPER_READ_FAIL, + uri: options.uri.uri, + method, + args: JSON.stringify(args, null, 2), + }); + return ResultErr(error); } const wasm = wasmResult.value; @@ -172,12 +181,29 @@ export class WasmWrapper implements Wrapper { env: options.env ? msgpackEncode(options.env) : EMPTY_ENCODED_OBJECT, }; - const abort = (message: string) => { - throw Error( - `WasmWrapper: Wasm module aborted execution.\nURI: ${options.uri.uri}\n` + - `Method: ${method}\n` + - `Args: ${JSON.stringify(args, null, 2)}\nMessage: ${message}.\n` - ); + const abortWithInvokeAborted = ( + message: string, + source?: ErrorSource + ) => { + const prev = WrapError.parse(message); + const text = prev ? "SubInvocation exception encountered" : message; + throw new WrapError(text, { + code: WrapErrorCode.WRAPPER_INVOKE_ABORTED, + uri: options.uri.uri, + method, + args: JSON.stringify(args, null, 2), + source, + innerError: prev, + }); + }; + + const abortWithInternalError = (message: string) => { + throw new WrapError(message, { + code: WrapErrorCode.WRAPPER_INTERNAL_ERROR, + uri: options.uri.uri, + method, + args: JSON.stringify(args, null, 2), + }); }; const memory = AsyncWasmInstance.createMemory({ module: wasm }); @@ -187,7 +213,8 @@ export class WasmWrapper implements Wrapper { state, client, memory, - abort, + abortWithInvokeAborted, + abortWithInternalError, }), requiredExports: WasmWrapper.requiredExports, }); @@ -200,7 +227,7 @@ export class WasmWrapper implements Wrapper { state.env.byteLength ); - const invokeResult = this._processInvokeResult(state, result, abort); + const invokeResult = this._processInvokeResult(state, result); if (invokeResult.ok) { return { @@ -208,13 +235,12 @@ export class WasmWrapper implements Wrapper { encoded: true, }; } else { - const error = Error( - `WasmWrapper: invocation exception encountered.\n` + - `uri: ${options.uri.uri}\n` + - `method: ${method}\n` + - `args: ${JSON.stringify(args, null, 2)}\n` + - `exception: ${invokeResult.error?.message}` - ); + const error = new WrapError(invokeResult.error, { + code: WrapErrorCode.WRAPPER_INVOKE_FAIL, + uri: options.uri.uri, + method, + args: JSON.stringify(args, null, 2), + }); return ResultErr(error); } } catch (error) { @@ -225,31 +251,30 @@ export class WasmWrapper implements Wrapper { @Tracer.traceMethod("WasmWrapper: _processInvokeResult") private _processInvokeResult( state: State, - result: boolean, - abort: (message: string) => never - ): Result { + result: boolean + ): Result { if (result) { if (!state.invoke.result) { - abort("Invoke result is missing."); + return ResultErr("Invoke result is missing."); } return ResultOk(state.invoke.result); } else { if (!state.invoke.error) { - abort("Invoke error is missing."); + return ResultErr("Invoke error is missing."); } - return ResultErr(Error(state.invoke.error)); + return ResultErr(state.invoke.error); } } @Tracer.traceMethod("WasmWrapper: getWasmModule") - private async _getWasmModule(): Promise> { + private async _getWasmModule(): Promise> { if (this._wasmModule === undefined) { const result = await this._fileReader.readFile(WRAP_MODULE_PATH); if (!result.ok) { - return ResultErr(Error(`Wrapper does not contain a wasm module`)); + return ResultErr("Wrapper does not contain a wasm module"); } this._wasmModule = result.value; diff --git a/packages/js/wasm/src/imports.ts b/packages/js/wasm/src/imports.ts index 583e34a6ce..155a0ead77 100644 --- a/packages/js/wasm/src/imports.ts +++ b/packages/js/wasm/src/imports.ts @@ -5,15 +5,22 @@ import { readBytes, readString, writeBytes, writeString } from "./buffer"; import { State } from "./WasmWrapper"; import { msgpackEncode } from "@polywrap/msgpack-js"; -import { CoreClient } from "@polywrap/core-js"; +import { CoreClient, ErrorSource } from "@polywrap/core-js"; export const createImports = (config: { client: CoreClient; memory: WebAssembly.Memory; state: State; - abort: (message: string) => never; + abortWithInvokeAborted: (message: string, source: ErrorSource) => never; + abortWithInternalError: (message: string) => never; }): WrapImports => { - const { memory, state, client, abort } = config; + const { + memory, + state, + client, + abortWithInvokeAborted, + abortWithInternalError, + } = config; return { wrap: { @@ -51,7 +58,9 @@ export const createImports = (config: { // Give WASM the size of the result __wrap_subinvoke_result_len: (): u32 => { if (!state.subinvoke.result) { - abort("__wrap_subinvoke_result_len: subinvoke.result is not set"); + abortWithInternalError( + "__wrap_subinvoke_result_len: subinvoke.result is not set" + ); return 0; } return state.subinvoke.result.byteLength; @@ -59,7 +68,9 @@ export const createImports = (config: { // Copy the subinvoke result into WASM __wrap_subinvoke_result: (ptr: u32): void => { if (!state.subinvoke.result) { - abort("__wrap_subinvoke_result: subinvoke.result is not set"); + abortWithInternalError( + "__wrap_subinvoke_result: subinvoke.result is not set" + ); return; } writeBytes(state.subinvoke.result, memory.buffer, ptr); @@ -67,7 +78,9 @@ export const createImports = (config: { // Give WASM the size of the error __wrap_subinvoke_error_len: (): u32 => { if (!state.subinvoke.error) { - abort("__wrap_subinvoke_error_len: subinvoke.error is not set"); + abortWithInternalError( + "__wrap_subinvoke_error_len: subinvoke.error is not set" + ); return 0; } return state.subinvoke.error.length; @@ -75,7 +88,9 @@ export const createImports = (config: { // Copy the subinvoke error into WASM __wrap_subinvoke_error: (ptr: u32): void => { if (!state.subinvoke.error) { - abort("__wrap_subinvoke_error: subinvoke.error is not set"); + abortWithInternalError( + "__wrap_subinvoke_error: subinvoke.error is not set" + ); return; } writeString(state.subinvoke.error, memory.buffer, ptr); @@ -116,7 +131,7 @@ export const createImports = (config: { }, __wrap_subinvokeImplementation_result_len: (): u32 => { if (!state.subinvokeImplementation.result) { - abort( + abortWithInternalError( "__wrap_subinvokeImplementation_result_len: subinvokeImplementation.result is not set" ); return 0; @@ -125,7 +140,7 @@ export const createImports = (config: { }, __wrap_subinvokeImplementation_result: (ptr: u32): void => { if (!state.subinvokeImplementation.result) { - abort( + abortWithInternalError( "__wrap_subinvokeImplementation_result: subinvokeImplementation.result is not set" ); return; @@ -134,7 +149,7 @@ export const createImports = (config: { }, __wrap_subinvokeImplementation_error_len: (): u32 => { if (!state.subinvokeImplementation.error) { - abort( + abortWithInternalError( "__wrap_subinvokeImplementation_error_len: subinvokeImplementation.error is not set" ); return 0; @@ -143,7 +158,7 @@ export const createImports = (config: { }, __wrap_subinvokeImplementation_error: (ptr: u32): void => { if (!state.subinvokeImplementation.error) { - abort( + abortWithInternalError( "__wrap_subinvokeImplementation_error: subinvokeImplementation.error is not set" ); return; @@ -153,11 +168,11 @@ export const createImports = (config: { // Copy the invocation's method & args into WASM __wrap_invoke_args: (methodPtr: u32, argsPtr: u32): void => { if (!state.method) { - abort("__wrap_invoke_args: method is not set"); + abortWithInternalError("__wrap_invoke_args: method is not set"); return; } if (!state.args) { - abort("__wrap_invoke_args: args is not set"); + abortWithInternalError("__wrap_invoke_args: args is not set"); return; } writeString(state.method, memory.buffer, methodPtr); @@ -180,7 +195,7 @@ export const createImports = (config: { const uri = readString(memory.buffer, uriPtr, uriLen); const result = await client.getImplementations(uri, {}); if (!result.ok) { - abort(result.error?.message as string); + abortWithInternalError(result.error?.message as string); return false; } const implementations = result.value; @@ -189,14 +204,18 @@ export const createImports = (config: { }, __wrap_getImplementations_result_len: (): u32 => { if (!state.getImplementationsResult) { - abort("__wrap_getImplementations_result_len: result is not set"); + abortWithInternalError( + "__wrap_getImplementations_result_len: result is not set" + ); return 0; } return state.getImplementationsResult.byteLength; }, __wrap_getImplementations_result: (ptr: u32): void => { if (!state.getImplementationsResult) { - abort("__wrap_getImplementations_result: result is not set"); + abortWithInternalError( + "__wrap_getImplementations_result: result is not set" + ); return; } writeBytes(state.getImplementationsResult, memory.buffer, ptr); @@ -215,9 +234,11 @@ export const createImports = (config: { const msg = readString(memory.buffer, msgPtr, msgLen); const file = readString(memory.buffer, filePtr, fileLen); - abort( - `__wrap_abort: ${msg}\nFile: ${file}\nLocation: [${line},${column}]` - ); + abortWithInvokeAborted(`__wrap_abort: ${msg}`, { + file, + row: line, + col: column, + }); }, __wrap_debug_log: (ptr: u32, len: u32): void => { const msg = readString(memory.buffer, ptr, len); diff --git a/packages/test-cases/cases/wrappers/wasm-as/simple-deprecated/schema.graphql b/packages/test-cases/cases/wrappers/wasm-as/simple-deprecated/schema.graphql new file mode 100644 index 0000000000..8ca05a8d80 --- /dev/null +++ b/packages/test-cases/cases/wrappers/wasm-as/simple-deprecated/schema.graphql @@ -0,0 +1,56 @@ +### Polywrap Header START ### +scalar UInt +scalar UInt8 +scalar UInt16 +scalar UInt32 +scalar Int +scalar Int8 +scalar Int16 +scalar Int32 +scalar Bytes +scalar BigInt +scalar BigNumber +scalar JSON +scalar Map + +directive @imported( + uri: String! + namespace: String! + nativeType: String! +) on OBJECT | ENUM + +directive @imports( + types: [String!]! +) on OBJECT + +directive @capability( + type: String! + uri: String! + namespace: String! +) repeatable on OBJECT + +directive @enabled_interface on OBJECT + +directive @annotate(type: String!) on FIELD + +directive @env(required: Boolean!) on FIELD_DEFINITION + +### Polywrap Header END ### + +type Module { + simpleMethod( + arg: String! + ): String! +} + +### Imported Modules START ### + +### Imported Modules END ### + +### Imported Objects START ### + +### Imported Objects END ### + +### Imported Envs START ### + +### Imported Envs END ### diff --git a/packages/test-cases/cases/wrappers/wasm-as/simple-deprecated/wrap.info b/packages/test-cases/cases/wrappers/wasm-as/simple-deprecated/wrap.info new file mode 100644 index 0000000000..3ea79091d6 --- /dev/null +++ b/packages/test-cases/cases/wrappers/wasm-as/simple-deprecated/wrap.info @@ -0,0 +1 @@ +„£abiˆ«objectTypes©enumTypes®interfaceTypes³importedObjectTypes³importedModuleTypes±importedEnumTypes°importedEnvTypesªmoduleType‡¤type¦Module¤nameÀ¨requiredÀ¤kindÌ€§methods‘†¤type¦Method¤name¬simpleMethod¨requiredäkind@©arguments‘Š¤type¦String¤name£arg¨requiredäkind"¥arrayÀ£mapÀ¦scalar„¤type¦String¤name£arg¨requiredäkind¦objectÀ¤enumÀ¶unresolvedObjectOrEnumÀ¦returnŠ¤type¦String¤name¬simpleMethod¨requiredäkind"¥arrayÀ£mapÀ¦scalar„¤type¦String¤name¬simpleMethod¨requiredäkind¦objectÀ¤enumÀ¶unresolvedObjectOrEnumÀ§importsªinterfaces¤name°SimpleDeprecated¤type¤wasm§version¥0.0.1 \ No newline at end of file diff --git a/packages/test-cases/cases/wrappers/wasm-as/simple-deprecated/wrap.wasm b/packages/test-cases/cases/wrappers/wasm-as/simple-deprecated/wrap.wasm new file mode 100644 index 0000000000..6d97abc359 Binary files /dev/null and b/packages/test-cases/cases/wrappers/wasm-as/simple-deprecated/wrap.wasm differ diff --git a/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/0-subinvoke/package.json b/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/0-subinvoke/package.json new file mode 100644 index 0000000000..897ac01244 --- /dev/null +++ b/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/0-subinvoke/package.json @@ -0,0 +1,8 @@ +{ + "name": "test-case-simple-subinvoke", + "private": true, + "dependencies": { + "@polywrap/wasm-as": "0.9.1", + "assemblyscript": "0.19.1" + } +} diff --git a/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/0-subinvoke/polywrap.yaml b/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/0-subinvoke/polywrap.yaml new file mode 100644 index 0000000000..42cd93bbd9 --- /dev/null +++ b/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/0-subinvoke/polywrap.yaml @@ -0,0 +1,10 @@ +format: 0.2.0 +project: + name: Simple + type: wasm/assemblyscript +source: + schema: ./schema.graphql + module: ./src/index.ts + import_abis: + - uri: ens/bad-util.eth + abi: ../1-subinvoke/build/wrap.info diff --git a/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/0-subinvoke/schema.graphql b/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/0-subinvoke/schema.graphql new file mode 100644 index 0000000000..214779b400 --- /dev/null +++ b/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/0-subinvoke/schema.graphql @@ -0,0 +1,5 @@ +#import * into BadUtil from "ens/bad-util.eth" + +type Module { + subInvokeWillThrow(a: Int!, b: Int!): Int! +} diff --git a/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/0-subinvoke/src/index.ts b/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/0-subinvoke/src/index.ts new file mode 100644 index 0000000000..a73b226f89 --- /dev/null +++ b/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/0-subinvoke/src/index.ts @@ -0,0 +1,6 @@ +import { Args_subInvokeWillThrow, BadUtil_Module } from "./wrap"; + +export function subInvokeWillThrow(args: Args_subInvokeWillThrow): i32 { + const subInvokeResult = BadUtil_Module.iThrow({ a: 0 }).unwrap(); + return args.a + args.b + subInvokeResult; +} diff --git a/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/1-subinvoke/package.json b/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/1-subinvoke/package.json new file mode 100644 index 0000000000..897ac01244 --- /dev/null +++ b/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/1-subinvoke/package.json @@ -0,0 +1,8 @@ +{ + "name": "test-case-simple-subinvoke", + "private": true, + "dependencies": { + "@polywrap/wasm-as": "0.9.1", + "assemblyscript": "0.19.1" + } +} diff --git a/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/1-subinvoke/polywrap.yaml b/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/1-subinvoke/polywrap.yaml new file mode 100644 index 0000000000..2d21ce24e6 --- /dev/null +++ b/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/1-subinvoke/polywrap.yaml @@ -0,0 +1,7 @@ +format: 0.2.0 +project: + name: Simple + type: wasm/assemblyscript +source: + schema: ./schema.graphql + module: ./src/index.ts diff --git a/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/1-subinvoke/schema.graphql b/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/1-subinvoke/schema.graphql new file mode 100644 index 0000000000..af3a9c4bdc --- /dev/null +++ b/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/1-subinvoke/schema.graphql @@ -0,0 +1,3 @@ +type Module { + iThrow(a: Int!): Int! +} diff --git a/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/1-subinvoke/src/index.ts b/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/1-subinvoke/src/index.ts new file mode 100644 index 0000000000..af7b1e2633 --- /dev/null +++ b/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/1-subinvoke/src/index.ts @@ -0,0 +1,8 @@ +import { Args_iThrow } from "./wrap"; + +export function iThrow(args: Args_iThrow): i32 { + if (2 == 2) { + throw new Error("I threw an error!"); + } + return args.a + 1; +} diff --git a/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/invoke/package.json b/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/invoke/package.json new file mode 100644 index 0000000000..68ecff0a4e --- /dev/null +++ b/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/invoke/package.json @@ -0,0 +1,8 @@ +{ + "name": "test-case-simple-invoke", + "private": true, + "dependencies": { + "@polywrap/wasm-as": "0.9.1", + "assemblyscript": "0.19.1" + } +} diff --git a/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/invoke/polywrap.yaml b/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/invoke/polywrap.yaml new file mode 100644 index 0000000000..96144bcfce --- /dev/null +++ b/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/invoke/polywrap.yaml @@ -0,0 +1,12 @@ +format: 0.2.0 +project: + name: Simple + type: wasm/assemblyscript +source: + schema: ./schema.graphql + module: ./src/index.ts + import_abis: + - uri: ens/bad-math.eth + abi: ../0-subinvoke/build/wrap.info + - uri: ens/not-found.eth + abi: ../0-subinvoke/build/wrap.info diff --git a/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/invoke/schema.graphql b/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/invoke/schema.graphql new file mode 100644 index 0000000000..0b784f3ed9 --- /dev/null +++ b/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/invoke/schema.graphql @@ -0,0 +1,7 @@ +#import * into BadMath from "ens/bad-math.eth" +#import * into NotFound from "ens/not-found.eth" + +type Module { + throwsInTwoSubinvokeLayers(a: Int!, b: Int!): Int! + subWrapperNotFound(a: Int!, b: Int!): Int! +} diff --git a/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/invoke/src/index.ts b/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/invoke/src/index.ts new file mode 100644 index 0000000000..d4cbe5ecbb --- /dev/null +++ b/packages/test-cases/cases/wrappers/wasm-as/subinvoke-error/invoke/src/index.ts @@ -0,0 +1,19 @@ +import { Args_throwsInTwoSubinvokeLayers, Args_subWrapperNotFound, BadMath_Module, NotFound_Module } from "./wrap"; +import { Args_subInvokeWillThrow as BadMathArgs_subInvokeWillThrow } from "./wrap/imported/BadMath_Module/serialization"; +import { Args_subInvokeWillThrow as NotFoundArgs_subInvokeWillThrow } from "./wrap/imported/NotFound_Module/serialization"; + +export function throwsInTwoSubinvokeLayers(args: Args_throwsInTwoSubinvokeLayers): i32 { + let importedArgs: BadMathArgs_subInvokeWillThrow = { + a: args.a, + b: args.b + } + return BadMath_Module.subInvokeWillThrow(importedArgs).unwrap() +} + +export function subWrapperNotFound(args: Args_subWrapperNotFound): i32 { + let importedArgs: NotFoundArgs_subInvokeWillThrow = { + a: args.a, + b: args.b + } + return NotFound_Module.subInvokeWillThrow(importedArgs).unwrap() +} \ No newline at end of file