diff --git a/src/generated-sdk/capabilities/networking/http/v1alpha/client_sdk_gen.ts b/src/generated-sdk/capabilities/networking/http/v1alpha/client_sdk_gen.ts index d071503b..39d9a83f 100644 --- a/src/generated-sdk/capabilities/networking/http/v1alpha/client_sdk_gen.ts +++ b/src/generated-sdk/capabilities/networking/http/v1alpha/client_sdk_gen.ts @@ -15,7 +15,7 @@ import { /** * Client Capability - * + * * Capability ID: http-actions@1.0.0-alpha * Default Mode: Mode.NODE * Capability Name: http-actions @@ -24,17 +24,14 @@ import { export class ClientCapability { /** The capability ID for this service */ static readonly CAPABILITY_ID = "http-actions@1.0.0-alpha"; - + /** The default execution mode for this capability */ static readonly DEFAULT_MODE = Mode.NODE; static readonly CAPABILITY_NAME = "http-actions"; static readonly CAPABILITY_VERSION = "1.0.0-alpha"; - - constructor( - private readonly mode: Mode = ClientCapability.DEFAULT_MODE - ) {} + constructor(private readonly mode: Mode = ClientCapability.DEFAULT_MODE) {} async sendRequest(input: RequestJson): Promise { const payload = { @@ -42,7 +39,7 @@ export class ClientCapability { value: toBinary(RequestSchema, fromJson(RequestSchema, input)), }; const capabilityId = ClientCapability.CAPABILITY_ID; - + return callCapability({ capabilityId, method: "SendRequest", @@ -65,7 +62,10 @@ export class ClientCapability { }); } - return fromBinary(ResponseSchema, capabilityResponse.response.value.value); + return fromBinary( + ResponseSchema, + capabilityResponse.response.value.value + ); }); } } diff --git a/src/sdk/engine/execute.test.ts b/src/sdk/engine/execute.test.ts index dd15569a..7390420f 100644 --- a/src/sdk/engine/execute.test.ts +++ b/src/sdk/engine/execute.test.ts @@ -1,5 +1,4 @@ -import { describe, test, expect, mock, beforeEach } from "bun:test"; -import { handleExecuteRequest } from "@cre/sdk/engine/execute"; +import { describe, test, expect, mock } from "bun:test"; import { handler } from "@cre/sdk/workflow"; import { create, toBinary, fromBinary } from "@bufbuild/protobuf"; import { @@ -15,20 +14,40 @@ import { OutputsSchema as BasicTriggerOutputsSchema } from "@cre/generated/capab import { getTypeUrl } from "@cre/sdk/utils/typeurl"; import { emptyConfig, basicRuntime } from "@cre/sdk/testhelpers/mocks"; +// Mock the hostBindings module before importing handleExecuteRequest +const mockSendResponse = mock((_response: string) => 0); +const mockHostBindings = { + sendResponse: mockSendResponse, + switchModes: mock((_mode: 0 | 1 | 2) => {}), + log: mock((_message: string) => {}), + callCapability: mock((_request: string) => 1), + awaitCapabilities: mock((_awaitRequest: string, _maxResponseLen: number) => + btoa("mock_await_capabilities_response") + ), + getSecrets: mock((_request: string, _maxResponseLen: number) => 1), + awaitSecrets: mock((_awaitRequest: string, _maxResponseLen: number) => + btoa("mock_await_secrets_response") + ), + versionV2: mock(() => {}), + randomSeed: mock((_mode: 1 | 2) => Math.random()), + getWasiArgs: mock(() => '["mock.wasm", ""]'), +}; + +// Mock the module +mock.module("@cre/sdk/runtime/host-bindings", () => ({ + hostBindings: mockHostBindings, +})); + +import { handleExecuteRequest } from "@cre/sdk/engine/execute"; + const decodeExecutionResult = (b64: string) => fromBinary(ExecutionResultSchema, Buffer.from(b64, "base64")); describe("engine/execute", () => { - let originalSendResponse: typeof globalThis.sendResponse; - - beforeEach(() => { - originalSendResponse = globalThis.sendResponse; - }); - test("subscribe returns TriggerSubscriptionRequest wrapped in ExecutionResult", async () => { const subs: string[] = []; // mock sendResponse to capture base64 payload - globalThis.sendResponse = mock((resp: string) => { + mockSendResponse.mockImplementation((resp: string) => { const exec = decodeExecutionResult(resp); const ts = exec.result.case === "triggerSubscriptions" @@ -71,7 +90,7 @@ describe("engine/execute", () => { "basic-test-trigger@1.0.0:Trigger:type.googleapis.com/capabilities.internal.basictrigger.v1.Config" ); - globalThis.sendResponse = originalSendResponse; + mockSendResponse.mockRestore(); }); test("trigger routes by id and decodes payload for correct handler", async () => { diff --git a/src/sdk/engine/execute.ts b/src/sdk/engine/execute.ts index 674fab66..1118c71c 100644 --- a/src/sdk/engine/execute.ts +++ b/src/sdk/engine/execute.ts @@ -3,7 +3,7 @@ import type { CapabilityResponse, } from "@cre/generated/sdk/v1alpha/sdk_pb"; import type { Workflow } from "@cre/sdk/workflow"; -import type { Runtime } from "@cre/sdk/runtime"; +import type { Runtime } from "@cre/sdk/runtime/runtime"; import { handleSubscribePhase } from "./handleSubscribePhase"; import { handleExecutionPhase } from "./handleExecutionPhase"; diff --git a/src/sdk/engine/handleExecutionPhase.ts b/src/sdk/engine/handleExecutionPhase.ts index ec420f6d..c495500a 100644 --- a/src/sdk/engine/handleExecutionPhase.ts +++ b/src/sdk/engine/handleExecutionPhase.ts @@ -5,7 +5,7 @@ import type { import { fromBinary } from "@bufbuild/protobuf"; import type { Workflow } from "@cre/sdk/workflow"; import { getTypeUrl } from "@cre/sdk/utils/typeurl"; -import type { Runtime } from "@cre/sdk/runtime"; +import type { Runtime } from "@cre/sdk/runtime/runtime"; export const handleExecutionPhase = async ( req: ExecuteRequest, diff --git a/src/sdk/engine/handleSubscribePhase.ts b/src/sdk/engine/handleSubscribePhase.ts index 9f444b32..07dc279a 100644 --- a/src/sdk/engine/handleSubscribePhase.ts +++ b/src/sdk/engine/handleSubscribePhase.ts @@ -8,7 +8,7 @@ import { ExecutionResultSchema, } from "@cre/generated/sdk/v1alpha/sdk_pb"; import type { Workflow } from "@cre/sdk/workflow"; -import { host } from "@cre/sdk/utils/host"; +import { hostBindings } from "@cre/sdk/runtime/host-bindings"; export const handleSubscribePhase = ( req: ExecuteRequest, @@ -35,5 +35,5 @@ export const handleSubscribePhase = ( }); const encoded = toBinary(ExecutionResultSchema, execResult); - host.sendResponse(Buffer.from(encoded).toString("base64")); + hostBindings.sendResponse(Buffer.from(encoded).toString("base64")); }; diff --git a/src/sdk/runtime.ts b/src/sdk/runtime.ts deleted file mode 100644 index dd6b5d15..00000000 --- a/src/sdk/runtime.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Mode } from "@cre/generated/sdk/v1alpha/sdk_pb"; -import { logger, type Logger } from "@cre/sdk/logger"; - -export type Runtime = { - mode: Mode; - logger: Logger; -}; - -export const runtime: Runtime = { - mode: Mode.DON, - logger, -}; diff --git a/src/sdk/runtime/errors.ts b/src/sdk/runtime/errors.ts new file mode 100644 index 00000000..fa9184d5 --- /dev/null +++ b/src/sdk/runtime/errors.ts @@ -0,0 +1,51 @@ +import { Mode } from "@cre/generated/sdk/v1alpha/sdk_pb"; + +export class DonModeError extends Error { + public name: string; + public capabilityId?: string; + public method?: string; + public mode?: Mode; + + constructor( + message = "cannot use DON Runtime inside Node mode", + options?: { + capabilityId?: string; + method?: string; + mode?: Mode; + } + ) { + super(message); + this.name = "DonModeError"; + + if (options) { + this.capabilityId = options.capabilityId; + this.method = options.method; + this.mode = options.mode; + } + } +} + +export class NodeModeError extends Error { + public name: string; + public capabilityId?: string; + public method?: string; + public mode?: Mode; + + constructor( + message = "cannot use Node Runtime inside DON mode", + options?: { + capabilityId?: string; + method?: string; + mode?: Mode; + } + ) { + super(message); + this.name = "NodeModeError"; + + if (options) { + this.capabilityId = options.capabilityId; + this.method = options.method; + this.mode = options.mode; + } + } +} diff --git a/src/sdk/runtime/host-bindings.ts b/src/sdk/runtime/host-bindings.ts new file mode 100644 index 00000000..7e622c3b --- /dev/null +++ b/src/sdk/runtime/host-bindings.ts @@ -0,0 +1,48 @@ +import { Mode } from "@cre/generated/sdk/v1alpha/sdk_pb"; +import { z } from "zod"; + +// Zod schema for validating global host functions +const globalHostBindingsSchema = z.object({ + switchModes: z.function().args(z.nativeEnum(Mode)).returns(z.void()), + log: z.function().args(z.string()).returns(z.void()), + sendResponse: z.function().args(z.string()).returns(z.number()), + randomSeed: z + .function() + .args(z.union([z.literal(Mode.DON), z.literal(Mode.NODE)])) + .returns(z.number()), + versionV2: z.function().args().returns(z.void()), + callCapability: z.function().args(z.string()).returns(z.number()), + awaitCapabilities: z + .function() + .args(z.string(), z.number()) + .returns(z.string()), + getSecrets: z.function().args(z.string(), z.number()).returns(z.number()), + awaitSecrets: z.function().args(z.string(), z.number()).returns(z.string()), + getWasiArgs: z.function().args().returns(z.string()), +}); + +type GlobalHostBindingsMap = z.infer; + +// Validate global host functions at runtime +const validateGlobalHostBindings = (): GlobalHostBindingsMap => { + const globalFunctions = + globalThis as unknown as Partial; + + try { + return globalHostBindingsSchema.parse(globalFunctions); + } catch (error) { + const missingFunctions = Object.keys(globalHostBindingsSchema.shape).filter( + (key) => !(key in globalFunctions) + ); + + throw new Error( + `Missing required global host functions: ${missingFunctions.join( + ", " + )}. ` + + `This indicates the runtime environment is not properly configured.` + ); + } +}; + +// Initialize validated global functions +export const hostBindings = validateGlobalHostBindings(); diff --git a/src/sdk/runtime/run-in-node-mode.test.ts b/src/sdk/runtime/run-in-node-mode.test.ts index 1f1a7bf2..01ab5eed 100644 --- a/src/sdk/runtime/run-in-node-mode.test.ts +++ b/src/sdk/runtime/run-in-node-mode.test.ts @@ -1,12 +1,39 @@ import { describe, test, expect, mock } from "bun:test"; -import { runInNodeMode } from "@cre/sdk/runtime/run-in-node-mode"; import { create } from "@bufbuild/protobuf"; import { SimpleConsensusInputsSchema, type SimpleConsensusInputsJson, } from "@cre/generated/sdk/v1alpha/sdk_pb"; import { ConsensusCapability } from "@cre/generated-sdk/capabilities/internal/consensus/v1alpha/consensus_sdk_gen"; -import { host } from "@cre/sdk/utils/host"; +import { type NodeRuntime } from "@cre/sdk/runtime/runtime"; + +// Mock hostBindings before importing runInNodeMode +const calls: string[] = []; +const mockHostBindings = { + sendResponse: mock((_response: string) => 0), + switchModes: mock((mode: 0 | 1 | 2) => { + calls.push(mode === 2 ? "NODE" : mode === 1 ? "DON" : "UNSPECIFIED"); + }), + log: mock((_message: string) => {}), + callCapability: mock((_request: string) => 1), + awaitCapabilities: mock((_awaitRequest: string, _maxResponseLen: number) => + btoa("mock_await_capabilities_response") + ), + getSecrets: mock((_request: string, _maxResponseLen: number) => 1), + awaitSecrets: mock((_awaitRequest: string, _maxResponseLen: number) => + btoa("mock_await_secrets_response") + ), + versionV2: mock(() => {}), + randomSeed: mock((_mode: 1 | 2) => Math.random()), + getWasiArgs: mock(() => '["mock.wasm", ""]'), +}; + +// Mock the module +mock.module("@cre/sdk/runtime/host-bindings", () => ({ + hostBindings: mockHostBindings, +})); + +import { runInNodeMode } from "@cre/sdk/runtime/run-in-node-mode"; describe("runInNodeMode", () => { test("accepts message input and returns Value", async () => { @@ -39,11 +66,8 @@ describe("runInNodeMode", () => { }); test("restores DON mode before calling consensus", async () => { - const calls: string[] = []; - const origSwitch = (globalThis as any).switchModes; - (globalThis as any).switchModes = (mode: 0 | 1 | 2) => { - calls.push(mode === 2 ? "NODE" : mode === 1 ? "DON" : "UNSPECIFIED"); - }; + // Clear the calls array for this test + calls.length = 0; const origSimple = ConsensusCapability.prototype.simple; ConsensusCapability.prototype.simple = mock( @@ -59,11 +83,10 @@ describe("runInNodeMode", () => { // restore ConsensusCapability.prototype.simple = origSimple; - (globalThis as any).switchModes = origSwitch; }); test("guards DON calls while in node mode", async () => { - // Simulate host.switchModes by touching global function used by host + // Simulate switchModes by touching global function used by host const origSwitch = (globalThis as any).switchModes; (globalThis as any).switchModes = (_m: 0 | 1 | 2) => {}; @@ -77,9 +100,9 @@ describe("runInNodeMode", () => { let threw = false; try { - await runInNodeMode(async () => { + await runInNodeMode(async (nodeRuntime: NodeRuntime) => { // During builder, we are in NODE mode, performing a DON call should throw - expect(() => host.log("")); + expect(() => nodeRuntime.logger.log("")); return create(SimpleConsensusInputsSchema); }); } catch (_e) { diff --git a/src/sdk/runtime/run-in-node-mode.ts b/src/sdk/runtime/run-in-node-mode.ts index 543df796..a3dac4a3 100644 --- a/src/sdk/runtime/run-in-node-mode.ts +++ b/src/sdk/runtime/run-in-node-mode.ts @@ -6,8 +6,8 @@ import { type SimpleConsensusInputsJson, } from "@cre/generated/sdk/v1alpha/sdk_pb"; import { ConsensusCapability } from "@cre/generated-sdk/capabilities/internal/consensus/v1alpha/consensus_sdk_gen"; -import { host } from "@cre/sdk/utils/host"; import type { Value } from "@cre/generated/values/v1/values_pb"; +import { runtime, type NodeRuntime } from "@cre/sdk/runtime/runtime"; type Inputs = SimpleConsensusInputs | SimpleConsensusInputsJson; @@ -49,16 +49,16 @@ const toInputsJson = (input: Inputs): SimpleConsensusInputsJson => { * Ensures mode is switched back to DON even if errors occur. */ export const runInNodeMode = async ( - buildConsensusInputs: () => Promise | Inputs + buildConsensusInputs: (nodeRuntime: NodeRuntime) => Promise | Inputs ): Promise => { - host.switchModes(Mode.NODE); + const nodeRuntime: NodeRuntime = runtime.switchModes(Mode.NODE); let consensusInputJson: SimpleConsensusInputsJson; try { - const consensusInput = await buildConsensusInputs(); + const consensusInput = await buildConsensusInputs(nodeRuntime); consensusInputJson = toInputsJson(consensusInput); } finally { // Always restore DON mode before invoking consensus - host.switchModes(Mode.DON); + runtime.switchModes(Mode.DON); } const consensus = new ConsensusCapability(); diff --git a/src/sdk/runtime/runtime.test.ts b/src/sdk/runtime/runtime.test.ts new file mode 100644 index 00000000..6e46b422 --- /dev/null +++ b/src/sdk/runtime/runtime.test.ts @@ -0,0 +1,75 @@ +import { describe, test, expect, beforeEach } from "bun:test"; +import { Mode } from "@cre/generated/sdk/v1alpha/sdk_pb"; +import { + runtime, + runtimeGuards, + type NodeRuntime, + type Runtime, +} from "./runtime"; +import { DonModeError, NodeModeError } from "./errors"; + +describe("Handling runtime in TS SDK", () => { + beforeEach(() => { + runtimeGuards.setMode(Mode.DON); + }); + + test("runtime should be DON by default", () => { + expect(runtime.mode).toBe(Mode.DON); + }); + + test("switching modes should work", () => { + // Start from DON mode + expect(runtime.isNodeRuntime).toBe(false); + + // Switch to NODE mode and verify it works + const nodeRt = runtime.switchModes(Mode.NODE); + expect(nodeRt.isNodeRuntime).toBe(true); + expect(nodeRt.mode).toBe(Mode.NODE); + + // Switch back to DON mode and verify it works + const rt = nodeRt.switchModes(Mode.DON); + expect(rt.mode).toBe(Mode.DON); + expect(rt.isNodeRuntime).toBe(false); + }); + + test("switching modes should be noop when already in that mode", () => { + // By default we are in DON mode, but we call switchModes to confirm that's a noop + let rt = runtime.switchModes(Mode.DON); + expect(rt.mode).toBe(Mode.DON); + expect(rt.isNodeRuntime).toBe(false); + + // // Now actually switch to NODE mode + let nodeRuntime = rt.switchModes(Mode.NODE); + expect(nodeRuntime.mode).toBe(Mode.NODE); + expect(nodeRuntime.isNodeRuntime).toBe(true); + + // And now switch to Node mode again, make sure it's a noop and the runtime is still in NODE mode. + nodeRuntime = nodeRuntime.switchModes(Mode.NODE); + expect(nodeRuntime.mode).toBe(Mode.NODE); + expect(nodeRuntime.isNodeRuntime).toBe(true); + }); + + test("assertDonSafe should throw an error if called in NODE mode", () => { + // DON mode is default so asserting DON safe should not throw an error + runtime.assertDonSafe(); + + // Switch to NODE mode and assert DON safe should throw an error + const nodeRuntime: NodeRuntime = runtime.switchModes(Mode.NODE); + expect(() => nodeRuntime.assertDonSafe()).toThrow(DonModeError); + + // Asserting node safe should not throw however + nodeRuntime.assertNodeSafe(); + + const rt: Runtime = nodeRuntime.switchModes(Mode.DON); + expect(() => rt.assertNodeSafe()).toThrow(NodeModeError); + }); + + test("getSecret is only available in DON mode", () => { + // Casting to any because typescript itself is not letting us do this + const nodeRuntime: any = runtime.switchModes(Mode.NODE); + expect(nodeRuntime.getSecret).toBeUndefined(); + + const rt: Runtime = nodeRuntime.switchModes(Mode.DON); + expect(rt.getSecret).toBeDefined(); + }); +}); diff --git a/src/sdk/runtime/runtime.ts b/src/sdk/runtime/runtime.ts new file mode 100644 index 00000000..81c65b26 --- /dev/null +++ b/src/sdk/runtime/runtime.ts @@ -0,0 +1,110 @@ +import { Mode } from "@cre/generated/sdk/v1alpha/sdk_pb"; +import { logger, type Logger } from "@cre/sdk/logger"; +import { DonModeError, NodeModeError } from "@cre/sdk/runtime/errors"; +import { hostBindings } from "@cre/sdk/runtime/host-bindings"; +import { getSecret } from "@cre/sdk/utils/secrets/get-secret"; + +/** + * Runtime guards are not actually causing / throwing errors. + * They pre-set the errors, depending on the current mode and then + * assert methods are used to check if feature can be called in given mode. + * If not the prepared error will be thrown. + */ +export const runtimeGuards = (() => { + let currentMode: Mode = Mode.DON; + let donModeGuardError: DonModeError | null = null; + let nodeModeGuardError: NodeModeError | null = new NodeModeError(); + + const setMode = (mode: Mode) => { + currentMode = mode; + if (mode === Mode.NODE) { + // In node mode, forbid DON runtime calls + donModeGuardError = new DonModeError(); + nodeModeGuardError = null; + } else if (mode === Mode.DON) { + // Back in DON mode, forbid node runtime calls + nodeModeGuardError = new NodeModeError(); + donModeGuardError = null; + } else { + donModeGuardError = null; + nodeModeGuardError = null; + } + }; + + const assertDonSafe = () => { + if (donModeGuardError) { + throw donModeGuardError; + } + }; + + const assertNodeSafe = () => { + if (nodeModeGuardError) { + throw nodeModeGuardError; + } + }; + + const getMode = () => currentMode; + + return { setMode, assertDonSafe, assertNodeSafe, getMode }; +})(); + +export type BaseRuntime = { + logger: Logger; + mode: M; + assertDonSafe(): asserts this is Runtime; + assertNodeSafe(): asserts this is NodeRuntime; +}; + +export type Runtime = BaseRuntime & { + isNodeRuntime: false; + switchModes(mode: Mode.NODE): NodeRuntime; + switchModes(mode: Mode.DON): Runtime; + getSecret(id: string): Promise; +}; + +export type NodeRuntime = BaseRuntime & { + isNodeRuntime: true; + switchModes(mode: Mode.NODE): NodeRuntime; + switchModes(mode: Mode.DON): Runtime; +}; + +// Shared implementation for mode switching +function switchModes(mode: Mode.NODE): NodeRuntime; +function switchModes(mode: Mode.DON): Runtime; +function switchModes(mode: Mode): Runtime | NodeRuntime; +function switchModes(mode: Mode): Runtime | NodeRuntime { + // Changing to the same mode should be a noop, we make sure to actually call switching logic if it's different mode + if (mode !== runtimeGuards.getMode()) { + hostBindings.switchModes(mode); + runtimeGuards.setMode(mode); + } + + return mode === Mode.NODE ? nodeRuntime : runtime; +} + +export const runtime: Runtime = { + mode: Mode.DON, + isNodeRuntime: false, + logger, + switchModes, + assertDonSafe: function (): asserts this is Runtime { + runtimeGuards.assertDonSafe(); + }, + assertNodeSafe: function (): asserts this is NodeRuntime { + runtimeGuards.assertNodeSafe(); + }, + getSecret, +}; + +export const nodeRuntime: NodeRuntime = { + mode: Mode.NODE, + isNodeRuntime: true, + logger, + switchModes, + assertNodeSafe: function (): asserts this is NodeRuntime { + runtimeGuards.assertNodeSafe(); + }, + assertDonSafe: function (): asserts this is Runtime { + runtimeGuards.assertDonSafe(); + }, +}; diff --git a/src/sdk/testhelpers/mocks.ts b/src/sdk/testhelpers/mocks.ts index efe4888b..fac1b90e 100644 --- a/src/sdk/testhelpers/mocks.ts +++ b/src/sdk/testhelpers/mocks.ts @@ -1,10 +1,18 @@ import { Mode } from "@cre/generated/sdk/v1alpha/sdk_pb"; import { logger } from "@cre/sdk/logger"; -import type { Runtime } from "@cre/sdk/runtime"; +import type { Runtime } from "@cre/sdk/runtime/runtime"; +import { getSecret } from "../utils/secrets/get-secret"; export const emptyConfig = {}; export const basicRuntime: Runtime = { mode: Mode.DON, logger, + isNodeRuntime: false, + assertDonSafe: () => {}, + assertNodeSafe: () => {}, + switchModes: (() => { + return basicRuntime; + }) as any, + getSecret, }; diff --git a/src/sdk/utils/capabilities/call-capability.ts b/src/sdk/utils/capabilities/call-capability.ts index 8f3a7cb0..c0ee2349 100644 --- a/src/sdk/utils/capabilities/call-capability.ts +++ b/src/sdk/utils/capabilities/call-capability.ts @@ -5,7 +5,7 @@ import { type CapabilityResponse, } from "@cre/generated/sdk/v1alpha/sdk_pb"; import { LazyPromise } from "@cre/sdk/utils/lazy-promise"; -import { runtimeGuards } from "@cre/sdk/utils/host"; +import { runtimeGuards } from "@cre/sdk/runtime/runtime"; export type CallCapabilityParams = { capabilityId: string; diff --git a/src/sdk/utils/error-boundary.ts b/src/sdk/utils/error-boundary.ts index 7e61640f..d5b3d137 100644 --- a/src/sdk/utils/error-boundary.ts +++ b/src/sdk/utils/error-boundary.ts @@ -1,9 +1,29 @@ +import { DonModeError, NodeModeError } from "@cre/sdk/runtime/errors"; + export const errorBoundary = (e: any) => { - console.log("ErrorBoundary: TS error thrown."); - if (e instanceof Error) { + // TODO: links should be configurable + if (e instanceof DonModeError || e instanceof NodeModeError) { + console.log(` + + +[${e.constructor.name}]: ${e.message}. + +Learn more about mode switching here: https://documentation-preview-git-cre-priv-577744-chainlink-labs-devrel.vercel.app/cre/getting-started/part-2-fetching-data#step-2-understand-the-runinnodemode-pattern + + +`); + } else if (e instanceof Error) { console.log(e.message); console.log(e.stack); } else { console.log(e); } }; + +export const withErrorBoundary = async (fn: () => Promise) => { + try { + await fn(); + } catch (e) { + errorBoundary(e); + } +}; diff --git a/src/sdk/utils/host.ts b/src/sdk/utils/host.ts deleted file mode 100644 index 499e5c4d..00000000 --- a/src/sdk/utils/host.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Mode } from "@cre/generated/sdk/v1alpha/sdk_pb"; - -// TODO: zod validation can be setup before running the workflows -// Making sure the hosts functions are exposed and this code will be removed -type GlobalHostFunctionsMap = { - switchModes: (mode: Mode) => void; - log: (message: string) => void; - sendResponse: (response: string) => number; - randomSeed: (mode: Mode.DON | Mode.NODE) => number; - versionV2: () => void; - callCapability: (request: string) => number; - awaitCapabilities: (awaitRequest: string, maxResponseLen: number) => string; - getSecrets: (request: string, maxResponseLen: number) => number; - awaitSecrets: (awaitRequest: string, maxResponseLen: number) => string; - getWasiArgs: () => string; -}; - -const g = globalThis as unknown as Partial; - -export const host = { - switchModes: (mode: Mode): void => { - g.switchModes?.(mode); - runtimeGuards.setMode(mode); - }, - log: (message: string): void => g.log?.(String(message)), - sendResponse: (payloadBase64: string): number => - g.sendResponse ? g.sendResponse(payloadBase64) : -1, - randomSeed: (mode: Mode.DON | Mode.NODE = Mode.DON): number => - g.randomSeed ? g.randomSeed(mode) : 0, -}; - -// Simple runtime guard state -export const runtimeGuards = (() => { - let currentMode: Mode = Mode.DON; - let donModeGuardError: Error | null = null; - let nodeModeGuardError: Error | null = null; - - const setMode = (mode: Mode) => { - currentMode = mode; - if (mode === Mode.NODE) { - // In node mode, forbid DON runtime calls - donModeGuardError = new Error("cannot use Runtime inside RunInNodeMode"); - nodeModeGuardError = null; - } else if (mode === Mode.DON) { - // Back in DON mode, forbid node runtime calls - nodeModeGuardError = new Error( - "cannot use NodeRuntime outside RunInNodeMode" - ); - donModeGuardError = null; - } else { - donModeGuardError = null; - nodeModeGuardError = null; - } - }; - - const assertDonSafe = () => { - if (donModeGuardError) { - throw donModeGuardError; - } - }; - - const assertNodeSafe = () => { - if (nodeModeGuardError) { - throw nodeModeGuardError; - } - }; - - const getMode = () => currentMode; - - return { setMode, assertDonSafe, assertNodeSafe, getMode }; -})(); diff --git a/src/sdk/utils/secrets/await-async-secret.ts b/src/sdk/utils/secrets/await-async-secret.ts index 4a4f6b9d..880f4f60 100644 --- a/src/sdk/utils/secrets/await-async-secret.ts +++ b/src/sdk/utils/secrets/await-async-secret.ts @@ -24,31 +24,26 @@ export const awaitAsyncSecret = async (callbackId: number) => { const response = awaitSecrets(awaitSecretRequestString, 1024 * 1024); - try { - const bytes = Buffer.from(response, "base64"); + const bytes = Buffer.from(response, "base64"); - // Decode as AwaitSecretsResponse first - const awaitResponse = fromBinary(AwaitSecretsResponseSchema, bytes); + // Decode as AwaitSecretsResponse first + const awaitResponse = fromBinary(AwaitSecretsResponseSchema, bytes); - // Get the specific secretId response for our callback ID - const secretResponses = awaitResponse.responses[callbackId]; - if (!secretResponses || !secretResponses.responses.length) { - throw new Error(`No response found for callback ID ${callbackId}`); - } - - const secretResponse = secretResponses.responses[0]; + // Get the specific secretId response for our callback ID + const secretResponses = awaitResponse.responses[callbackId]; + if (!secretResponses || !secretResponses.responses.length) { + throw new Error(`No response found for callback ID ${callbackId}`); + } - if (secretResponse.response.case === "secret") { - return secretResponse.response.value.value; - } + const secretResponse = secretResponses.responses[0]; - if (secretResponse.response.case === "error") { - throw new SecretsError(secretResponse.response.value.error); - } + if (secretResponse.response.case === "secret") { + return secretResponse.response.value.value; + } - throw new Error(`No secret found for callback ID ${callbackId}`); - } catch (e) { - errorBoundary(e); - throw e; + if (secretResponse.response.case === "error") { + throw new SecretsError(secretResponse.response.value.error); } + + throw new Error(`No secret found for callback ID ${callbackId}`); }; diff --git a/src/sdk/utils/secrets/get-secret.ts b/src/sdk/utils/secrets/get-secret.ts index 072a6302..1bc966f9 100644 --- a/src/sdk/utils/secrets/get-secret.ts +++ b/src/sdk/utils/secrets/get-secret.ts @@ -3,6 +3,6 @@ import { awaitAsyncSecret } from "@cre/sdk/utils/secrets/await-async-secret"; import { LazyPromise } from "@cre/sdk/utils/lazy-promise"; export const getSecret = (id: string): Promise => { - const callbackId = doGetSecret("Foo"); + const callbackId = doGetSecret(id); return new LazyPromise(async () => awaitAsyncSecret(callbackId)); }; diff --git a/src/sdk/utils/values/consensus-hooks.ts b/src/sdk/utils/values/consensus-hooks.ts index 6c46b609..16021796 100644 --- a/src/sdk/utils/values/consensus-hooks.ts +++ b/src/sdk/utils/values/consensus-hooks.ts @@ -4,6 +4,7 @@ import { getAggregatedValue, } from "@cre/sdk/utils/values/consensus"; import { type SupportedValueTypes, val } from "@cre/sdk/utils/values/value"; +import { type NodeRuntime } from "@cre/sdk/runtime/runtime"; // ===== TYPE HELPERS FOR BETTER TYPE SAFETY ===== @@ -38,7 +39,7 @@ export const useConsensus = < aggregationType: ConsenusAggregator ) => { return async (...args: TArgs): Promise => { - return cre.runInNodeMode(async () => { + return cre.runInNodeMode(async (_nodeRuntime: NodeRuntime) => { const result = await fn(...args); return getAggregatedValue( (val as any)[valueType](result), diff --git a/src/sdk/workflow.ts b/src/sdk/workflow.ts index fb7c5317..a17d6d08 100644 --- a/src/sdk/workflow.ts +++ b/src/sdk/workflow.ts @@ -4,7 +4,7 @@ import type { Trigger } from "@cre/sdk/utils/triggers/trigger-interface"; import { handleExecuteRequest } from "@cre/sdk/engine/execute"; import { getRequest } from "@cre/sdk/utils/get-request"; import { configHandler, type ConfigHandlerParams } from "@cre/sdk/utils/config"; -import { runtime, type Runtime } from "@cre/sdk/runtime"; +import { runtime, type Runtime } from "@cre/sdk/runtime/runtime"; export type HandlerFn = ( config: TConfig, diff --git a/src/workflows/hello-world/hello-world.ts b/src/workflows/hello-world/hello-world.ts index 1fe54eb3..4de66607 100644 --- a/src/workflows/hello-world/hello-world.ts +++ b/src/workflows/hello-world/hello-world.ts @@ -1,5 +1,5 @@ import { cre } from "@cre/sdk/cre"; -import type { Runtime } from "@cre/sdk/runtime"; +import type { Runtime } from "@cre/sdk/runtime/runtime"; type Config = { schedule: string; diff --git a/src/workflows/http-fetch/http-fetch-hook.ts b/src/workflows/http-fetch/http-fetch-hook.ts index 50acf30c..abc7a387 100644 --- a/src/workflows/http-fetch/http-fetch-hook.ts +++ b/src/workflows/http-fetch/http-fetch-hook.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { cre } from "@cre/sdk/cre"; import { useMedianConsensus } from "@cre/sdk/utils/values/consensus-hooks"; +import { withErrorBoundary } from "@cre/sdk/utils/error-boundary"; const configSchema = z.object({ schedule: z.string(), @@ -40,4 +41,4 @@ export async function main() { await runner.run(initWorkflow); } -main(); +withErrorBoundary(main); diff --git a/src/workflows/http-fetch/http-fetch.ts b/src/workflows/http-fetch/http-fetch.ts index ed0562a3..e8dbf859 100644 --- a/src/workflows/http-fetch/http-fetch.ts +++ b/src/workflows/http-fetch/http-fetch.ts @@ -1,5 +1,7 @@ import { z } from "zod"; import { cre } from "@cre/sdk/cre"; +import { type NodeRuntime } from "@cre/sdk/runtime/runtime"; +import { withErrorBoundary } from "@cre/sdk/utils/error-boundary"; const configSchema = z.object({ schedule: z.string(), @@ -16,7 +18,7 @@ const fetchMathResult = async (config: Config) => { }; const fetchAggregatedResult = async (config: Config) => - cre.runInNodeMode(async () => { + cre.runInNodeMode(async (_nodeRuntime: NodeRuntime) => { const result = await fetchMathResult(config); return cre.utils.consensus.getAggregatedValue( cre.utils.val.float64(result), @@ -39,9 +41,9 @@ const initWorkflow = (config: Config) => { export async function main() { const runner = await cre.newRunner({ - configSchema: configSchema, + configSchema, }); await runner.run(initWorkflow); } -main(); +withErrorBoundary(main); diff --git a/src/workflows/on-chain-write/on-chain-write.ts b/src/workflows/on-chain-write/on-chain-write.ts index d8d39705..186ab337 100644 --- a/src/workflows/on-chain-write/on-chain-write.ts +++ b/src/workflows/on-chain-write/on-chain-write.ts @@ -11,7 +11,7 @@ import { } from "viem"; import { bytesToHex } from "@cre/sdk/utils/hex-utils"; import { CALCULATOR_CONSUMER_ABI, STORAGE_ABI } from "./abi"; -import type { Runtime } from "@cre/sdk/runtime"; +import type { Runtime } from "@cre/sdk/runtime/runtime"; import { useMedianConsensus } from "@cre/sdk/utils/values/consensus-hooks"; // Storage contract ABI - we only need the 'get' function diff --git a/src/workflows/on-chain/on-chain.ts b/src/workflows/on-chain/on-chain.ts index 3d351460..98b3638a 100644 --- a/src/workflows/on-chain/on-chain.ts +++ b/src/workflows/on-chain/on-chain.ts @@ -4,7 +4,7 @@ import { sendResponseValue } from "@cre/sdk/utils/send-response-value"; import { val } from "@cre/sdk/utils/values/value"; import { encodeFunctionData, decodeFunctionResult, type Hex } from "viem"; import { bytesToHex } from "@cre/sdk/utils/hex-utils"; -import type { Runtime } from "@cre/sdk/runtime"; +import type { Runtime } from "@cre/sdk/runtime/runtime"; import { useMedianConsensus } from "@cre/sdk/utils/values/consensus-hooks"; // Storage contract ABI - we only need the 'get' function diff --git a/src/workflows/standard_tests/mode_switch/successful_mode_switch/testts.ts b/src/workflows/standard_tests/mode_switch/successful_mode_switch/testts.ts index d59c1fd1..2408c99c 100644 --- a/src/workflows/standard_tests/mode_switch/successful_mode_switch/testts.ts +++ b/src/workflows/standard_tests/mode_switch/successful_mode_switch/testts.ts @@ -18,6 +18,7 @@ import { getRequest } from "@cre/sdk/utils/get-request"; import { BasicCapability as BasicTriggerCapability } from "@cre/generated-sdk/capabilities/internal/basictrigger/v1/basic_sdk_gen"; import { runInNodeMode } from "@cre/sdk/runtime/run-in-node-mode"; import { basicRuntime, emptyConfig } from "@cre/sdk/testhelpers/mocks"; +import { type NodeRuntime } from "@cre/sdk/runtime/runtime"; export async function main() { console.log( @@ -33,23 +34,25 @@ export async function main() { const basicActionCapability = new BasicActionCapability(); const donResponse = await basicActionCapability.performAction(donInput); - const consensusOutput = await runInNodeMode(async () => { - Date.now(); - const nodeActionCapability = new NodeActionCapability(); - const nodeResponse = await nodeActionCapability.performAction({ - inputThing: true, - }); - const consensusInput = create(SimpleConsensusInputsSchema, { - observation: observationValue( - val.mapValue({ OutputThing: val.int64(nodeResponse.outputThing) }) - ), - descriptors: consensusFieldsFrom({ - OutputThing: AggregationType.MEDIAN, - }), - default: val.mapValue({ OutputThing: val.int64(123) }), - }); - return consensusInput; - }); + const consensusOutput = await runInNodeMode( + async (_nodeRuntime: NodeRuntime) => { + Date.now(); + const nodeActionCapability = new NodeActionCapability(); + const nodeResponse = await nodeActionCapability.performAction({ + inputThing: true, + }); + const consensusInput = create(SimpleConsensusInputsSchema, { + observation: observationValue( + val.mapValue({ OutputThing: val.int64(nodeResponse.outputThing) }) + ), + descriptors: consensusFieldsFrom({ + OutputThing: AggregationType.MEDIAN, + }), + default: val.mapValue({ OutputThing: val.int64(123) }), + }); + return consensusInput; + } + ); Date.now(); const outputJson = toJson(ValueSchema, consensusOutput); diff --git a/src/workflows/standard_tests/random/testts.ts b/src/workflows/standard_tests/random/testts.ts index 42d022e6..a55af2ae 100644 --- a/src/workflows/standard_tests/random/testts.ts +++ b/src/workflows/standard_tests/random/testts.ts @@ -17,6 +17,7 @@ import { } from "@cre/sdk/utils/values/consensus"; import { runInNodeMode } from "@cre/sdk/runtime/run-in-node-mode"; import { emptyConfig, basicRuntime } from "@cre/sdk/testhelpers/mocks"; +import { type NodeRuntime } from "@cre/sdk/runtime/runtime"; export async function main() { console.log( @@ -29,7 +30,7 @@ export async function main() { const donRandomNumber = new Rand(donSeed); let total = donRandomNumber.Uint64(); - await runInNodeMode(async () => { + await runInNodeMode(async (_nodeRuntime: NodeRuntime) => { const nodeSeed = BigInt(randomSeed(Mode.NODE)); const nodeRandomNumber = new Rand(nodeSeed);