diff --git a/bun.lock b/bun.lock index 4bd29685..eddf2faf 100644 --- a/bun.lock +++ b/bun.lock @@ -12,7 +12,7 @@ }, "packages/cre-sdk": { "name": "@chainlink/cre-sdk", - "version": "0.0.4-alpha", + "version": "0.0.5-alpha", "bin": { "cre-compile": "bin/cre-compile.ts", }, @@ -37,13 +37,16 @@ }, "packages/cre-sdk-examples": { "name": "@chainlink/cre-sdk-examples", - "version": "0.0.4-alpha", + "version": "0.0.5-alpha", "dependencies": { "@bufbuild/protobuf": "2.6.3", "@chainlink/cre-sdk": "workspace:*", "viem": "2.34.0", "zod": "3.25.76", }, + "devDependencies": { + "@types/bun": "1.2.21", + }, }, "packages/cre-sdk-javy-plugin": { "name": "@chainlink/cre-sdk-javy-plugin", diff --git a/packages/cre-sdk-examples/README.md b/packages/cre-sdk-examples/README.md index b7e84857..07dfbb97 100644 --- a/packages/cre-sdk-examples/README.md +++ b/packages/cre-sdk-examples/README.md @@ -58,6 +58,12 @@ cre workflow simulate ./src/workflows/on-chain-write cre workflow simulate ./src/workflows/proof-of-reserve ``` +[Star Wars character example](https://github.com/smartcontractkit/cre-sdk-typescript/blob/main/packages/cre-sdk-examples/src/workflows/star-wars/index.ts): + +```zsh +cre workflow simulate ./src/workflows/star-wars +``` + ## Testing workflow compilation only If you want use the CRE SDK to compile your workflows to WASM, choose any workflow you want to compile and run the following command: diff --git a/packages/cre-sdk-examples/package.json b/packages/cre-sdk-examples/package.json index b8286f78..4f98ab70 100644 --- a/packages/cre-sdk-examples/package.json +++ b/packages/cre-sdk-examples/package.json @@ -1,7 +1,7 @@ { "name": "@chainlink/cre-sdk-examples", "private": true, - "version": "0.0.5-alpha", + "version": "0.0.6-alpha", "type": "module", "author": "Ernest Nowacki", "license": "BUSL-1.1", @@ -19,6 +19,9 @@ "viem": "2.34.0", "zod": "3.25.76" }, + "devDependencies": { + "@types/bun": "1.2.21" + }, "engines": { "bun": ">=1.2.21" } diff --git a/packages/cre-sdk-examples/src/workflows/star-wars/config.json b/packages/cre-sdk-examples/src/workflows/star-wars/config.json new file mode 100644 index 00000000..efdcebe3 --- /dev/null +++ b/packages/cre-sdk-examples/src/workflows/star-wars/config.json @@ -0,0 +1,3 @@ +{ + "url": "https://swapi.info/api/people/{characterId}/" +} diff --git a/packages/cre-sdk-examples/src/workflows/star-wars/http_trigger_payload.json b/packages/cre-sdk-examples/src/workflows/star-wars/http_trigger_payload.json new file mode 100644 index 00000000..ab2d2466 --- /dev/null +++ b/packages/cre-sdk-examples/src/workflows/star-wars/http_trigger_payload.json @@ -0,0 +1,3 @@ +{ + "characterId": 1 +} diff --git a/packages/cre-sdk-examples/src/workflows/star-wars/index.ts b/packages/cre-sdk-examples/src/workflows/star-wars/index.ts new file mode 100644 index 00000000..615715bc --- /dev/null +++ b/packages/cre-sdk-examples/src/workflows/star-wars/index.ts @@ -0,0 +1,88 @@ +import { + consensusIdenticalAggregation, + cre, + decodeJson, + type HTTPPayload, + type HTTPSendRequester, + json, + ok, + Runner, + type Runtime, +} from '@chainlink/cre-sdk' +import { z } from 'zod' + +const configSchema = z.object({ + url: z.string(), +}) + +type Config = z.infer + +const responseSchema = z.object({ + name: z.string(), + height: z.string(), + mass: z.string(), + hair_color: z.string(), + skin_color: z.string(), + eye_color: z.string(), + birth_year: z.string(), + gender: z.string(), + homeworld: z.string(), + films: z.array(z.string()), + species: z.array(z.string()), + vehicles: z.array(z.string()), + starships: z.array(z.string()), + created: z.string().datetime(), + edited: z.string().datetime(), + url: z.string(), +}) + +type StarWarsCharacter = z.infer + +const fetchStarWarsCharacter = ( + sendRequester: HTTPSendRequester, + config: Config, + payload: HTTPPayload, +): StarWarsCharacter => { + const input = decodeJson(payload.input) + const url = config.url.replace('{characterId}', input.characterId) + + const response = sendRequester.sendRequest({ url }).result() + + // Check if the response is successful using the helper function + if (!ok(response)) { + throw new Error(`HTTP request failed with status: ${response.statusCode}`) + } + + const character = responseSchema.parse(json(response)) + + return character +} + +const onHTTPTrigger = async (runtime: Runtime, payload: HTTPPayload) => { + const httpCapability = new cre.capabilities.HTTPClient() + + const result: StarWarsCharacter = httpCapability + .sendRequest( + runtime, + fetchStarWarsCharacter, + consensusIdenticalAggregation(), + )(runtime.config, payload) + .result() + + return result +} + +const initWorkflow = () => { + const httpTrigger = new cre.capabilities.HTTPCapability() + + return [cre.handler(httpTrigger.trigger({}), onHTTPTrigger)] +} + +export async function main() { + const runner = await Runner.newRunner({ + configSchema, + }) + await runner.run(initWorkflow) +} + +main() diff --git a/packages/cre-sdk-examples/src/workflows/star-wars/workflow.yaml b/packages/cre-sdk-examples/src/workflows/star-wars/workflow.yaml new file mode 100644 index 00000000..3fbacb06 --- /dev/null +++ b/packages/cre-sdk-examples/src/workflows/star-wars/workflow.yaml @@ -0,0 +1,38 @@ +# ========================================================================== +# CRE WORKFLOW SETTINGS FILE +# ========================================================================== +# This file defines environment-specific workflow settings used by the CRE CLI. +# +# Each top-level key is a target (e.g., `production`, `production-testnet`, etc.). +# You can also define your own custom targets, such as `my-target`, and +# point the CLI to it via an environment variable. +# +# Note: If any setting in this file conflicts with a setting in the CRE Project Settings File, +# the value defined here in the workflow settings file will take precedence. +# +# Below is an example `my-target`: +# +# my-target: +# user-workflow: +# # Optional: The address of the workflow owner (wallet or MSIG contract). +# # Used to establish ownership for encrypting the workflow's secrets. +# # If omitted, defaults to an empty string. +# workflow-owner-address: "0x1234567890abcdef1234567890abcdef12345678" +# +# # Required: The name of the workflow to register with the Workflow Registry contract. +# workflow-name: "MyExampleWorkflow" + +# ========================================================================== +local-simulation: + user-workflow: + workflow-owner-address: "(optional) Multi-signature contract address" + workflow-name: "star-wars" + workflow-artifacts: + workflow-path: "./index.ts" + config-path: "./config.json" + +# ========================================================================== +production-testnet: + user-workflow: + workflow-owner-address: "(optional) Multi-signature contract address" + workflow-name: "star-wars" diff --git a/packages/cre-sdk-examples/tsconfig.json b/packages/cre-sdk-examples/tsconfig.json index 7200368c..d8cb82fa 100644 --- a/packages/cre-sdk-examples/tsconfig.json +++ b/packages/cre-sdk-examples/tsconfig.json @@ -1,11 +1,10 @@ { "compilerOptions": { // Enable latest features - "lib": ["ESNext", "DOM", "DOM.Iterable"], + "lib": ["ESNext"], "target": "ESNext", "module": "ESNext", "moduleDetection": "force", - "jsx": "react-jsx", "allowJs": true, // Bundler mode diff --git a/packages/cre-sdk-javy-plugin/dist/javy_chainlink_sdk.wasm b/packages/cre-sdk-javy-plugin/dist/javy_chainlink_sdk.wasm index 9a65e623..2c457c29 100755 Binary files a/packages/cre-sdk-javy-plugin/dist/javy_chainlink_sdk.wasm and b/packages/cre-sdk-javy-plugin/dist/javy_chainlink_sdk.wasm differ diff --git a/packages/cre-sdk-javy-plugin/package.json b/packages/cre-sdk-javy-plugin/package.json index b8a31af0..490840c1 100644 --- a/packages/cre-sdk-javy-plugin/package.json +++ b/packages/cre-sdk-javy-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@chainlink/cre-sdk-javy-plugin", - "version": "0.0.4-alpha", + "version": "0.0.5-alpha", "type": "module", "bin": { "cre-setup": "bin/setup.ts", diff --git a/packages/cre-sdk-javy-plugin/tsconfig.json b/packages/cre-sdk-javy-plugin/tsconfig.json index 907cfcf0..e314cb3c 100644 --- a/packages/cre-sdk-javy-plugin/tsconfig.json +++ b/packages/cre-sdk-javy-plugin/tsconfig.json @@ -1,11 +1,10 @@ { "compilerOptions": { // Enable latest features - "lib": ["ESNext", "DOM", "DOM.Iterable"], + "lib": ["ESNext"], "target": "ESNext", "module": "ESNext", "moduleDetection": "force", - "jsx": "react-jsx", "allowJs": true, // Bundler mode diff --git a/packages/cre-sdk/package.json b/packages/cre-sdk/package.json index ce86ea55..852f6483 100644 --- a/packages/cre-sdk/package.json +++ b/packages/cre-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@chainlink/cre-sdk", - "version": "0.0.5-alpha", + "version": "0.0.6-alpha", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -8,6 +8,9 @@ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" + }, + "./restricted-apis": { + "types": "./dist/restricted-apis.d.ts" } }, "bin": { @@ -22,7 +25,8 @@ "LICENSE.md" ], "scripts": { - "build": "bun run clean && bun run compile:build && bun run fix-imports", + "build": "bun run clean && bun run compile:build && bun run build:types && bun run fix-imports", + "build:types": "bun run scripts/run.ts build-types", "check": "biome check --write ${BIOME_PATHS:-.}", "clean": "rm -rf dist", "compile:all-standard-tests": "bun scripts/run.ts compile-all-standard-tests", diff --git a/packages/cre-sdk/scripts/run.ts b/packages/cre-sdk/scripts/run.ts index e3574295..a4a1afd3 100644 --- a/packages/cre-sdk/scripts/run.ts +++ b/packages/cre-sdk/scripts/run.ts @@ -1,14 +1,14 @@ #!/usr/bin/env bun const availableScripts = [ + 'build-types', 'compile-to-js', 'compile-to-wasm', 'compile-workflow', // TS -> JS -> WASM compilation in single script 'compile-all-standard-tests', // Do the above but for all standard tests - + 'fix-imports', // Fix @cre/* imports to relative paths 'generate-chain-selectors', 'generate-sdks', - 'fix-imports', // Fix @cre/* imports to relative paths ] /** diff --git a/packages/cre-sdk/scripts/src/build-types.ts b/packages/cre-sdk/scripts/src/build-types.ts new file mode 100644 index 00000000..6df0bb78 --- /dev/null +++ b/packages/cre-sdk/scripts/src/build-types.ts @@ -0,0 +1,22 @@ +import { glob } from 'fast-glob' +import { copyFile, mkdir } from 'fs/promises' +import { dirname, join, relative } from 'path' + +const buildTypes = async () => { + console.log('🔧 Including restricted-apis type in built files...') + + // Define paths relative to the scripts directory + const packageRoot = join(import.meta.dir, '../..') + const sourceFile = join(packageRoot, 'src/sdk/types/restricted-apis.d.ts') + const destFile = join(packageRoot, 'dist/restricted-apis.d.ts') + + // Ensure the dist directory exists + await mkdir(dirname(destFile), { recursive: true }) + + // Copy the file + await copyFile(sourceFile, destFile) + + console.log('✅ Included restricted-apis type in the build.') +} + +export const main = buildTypes diff --git a/packages/cre-sdk/src/index.ts b/packages/cre-sdk/src/index.ts index fb75b971..7fa8ad8e 100644 --- a/packages/cre-sdk/src/index.ts +++ b/packages/cre-sdk/src/index.ts @@ -1,4 +1,5 @@ /// +/// export * from './sdk' export * from './sdk/runtime' diff --git a/packages/cre-sdk/src/sdk/index.ts b/packages/cre-sdk/src/sdk/index.ts index d07c6fc4..bb7ebbd9 100644 --- a/packages/cre-sdk/src/sdk/index.ts +++ b/packages/cre-sdk/src/sdk/index.ts @@ -1,5 +1,5 @@ -export * from './cre' -export * from './report' -export type * from './runtime' -export * from './runtime' -export * from './workflow' +export * from "./cre"; +export * from "./report"; +export type * from "./runtime"; +export * from "./runtime"; +export * from "./workflow"; diff --git a/packages/cre-sdk/src/sdk/types/restricted-apis.d.ts b/packages/cre-sdk/src/sdk/types/restricted-apis.d.ts new file mode 100644 index 00000000..91196ec7 --- /dev/null +++ b/packages/cre-sdk/src/sdk/types/restricted-apis.d.ts @@ -0,0 +1,27 @@ +declare global { + /** @deprecated fetch is not available in CRE WASM workflows. Use cre.capabilities.HTTPClient instead. */ + const fetch: never + + /** @deprecated window is not available in CRE WASM workflows. */ + const window: never + + /** @deprecated document is not available in CRE WASM workflows. */ + const document: never + + /** @deprecated XMLHttpRequest is not available in CRE WASM workflows. Use cre.capabilities.HTTPClient instead. */ + const XMLHttpRequest: never + + /** @deprecated localStorage is not available in CRE WASM workflows. */ + const localStorage: never + + /** @deprecated sessionStorage is not available in CRE WASM workflows. */ + const sessionStorage: never + + /** @deprecated setTimeout is not available in CRE WASM workflows. Use cre.capabilities.CronCapability for scheduling. */ + const setTimeout: never + + /** @deprecated setInterval is not available in CRE WASM workflows. Use cre.capabilities.CronCapability for scheduling. */ + const setInterval: never +} + +export {} diff --git a/packages/cre-sdk/src/sdk/utils/capabilities/http/http-helpers.ts b/packages/cre-sdk/src/sdk/utils/capabilities/http/http-helpers.ts index 9f34f410..8cd0f8c2 100644 --- a/packages/cre-sdk/src/sdk/utils/capabilities/http/http-helpers.ts +++ b/packages/cre-sdk/src/sdk/utils/capabilities/http/http-helpers.ts @@ -10,6 +10,7 @@ import type { } from '@cre/generated-sdk/capabilities/networking/http/v1alpha/client_sdk_gen' import type { NodeRuntime } from '@cre/sdk' import type { Report } from '@cre/sdk/report' +import { decodeJson } from '@cre/sdk/utils/decode-json' /** * HTTP Response Helper Functions @@ -105,11 +106,9 @@ export function json( return { result: () => json(responseOrFn().result), } - } else { - const decoder = new TextDecoder('utf-8') - const textBody = decoder.decode(responseOrFn.body) - return JSON.parse(textBody) } + + return decodeJson(responseOrFn.body) } /** diff --git a/packages/cre-sdk/src/sdk/utils/decode-json.test.ts b/packages/cre-sdk/src/sdk/utils/decode-json.test.ts new file mode 100644 index 00000000..6e71b7b2 --- /dev/null +++ b/packages/cre-sdk/src/sdk/utils/decode-json.test.ts @@ -0,0 +1,171 @@ +import { describe, expect, it } from 'bun:test' +import { decodeJson } from './decode-json' + +describe('decodeJson', () => { + describe('valid JSON inputs', () => { + it('should decode a simple object', () => { + const input = { name: 'test', value: 123 } + const encoded = new TextEncoder().encode(JSON.stringify(input)) + expect(decodeJson(encoded)).toEqual(input) + }) + + it('should decode an array', () => { + const input = [1, 2, 3, 'test', true] + const encoded = new TextEncoder().encode(JSON.stringify(input)) + expect(decodeJson(encoded)).toEqual(input) + }) + + it('should decode a nested object', () => { + const input = { + user: { + name: 'John Wick', + age: 30, + address: { + street: '123 Main St', + city: 'New York', + }, + }, + active: true, + } + const encoded = new TextEncoder().encode(JSON.stringify(input)) + expect(decodeJson(encoded)).toEqual(input) + }) + + it('should decode JSON with special characters', () => { + const input = { + message: 'Hello "World"!', + path: 'C:\\Users\\test', + newline: 'line1\nline2', + } + const encoded = new TextEncoder().encode(JSON.stringify(input)) + expect(decodeJson(encoded)).toEqual(input) + }) + + it('should decode JSON with unicode characters', () => { + const input = { + emoji: '🚀', + chinese: 'źdźbło chrabąszcza', + mixed: 'Hello 世界 🌍', + } + const encoded = new TextEncoder().encode(JSON.stringify(input)) + expect(decodeJson(encoded)).toEqual(input) + }) + + it('should decode JSON primitive null', () => { + const input = null + const encoded = new TextEncoder().encode(JSON.stringify(input)) + expect(decodeJson(encoded)).toBeNull() + }) + + it('should decode JSON primitive boolean', () => { + const input = true + const encoded = new TextEncoder().encode(JSON.stringify(input)) + expect(decodeJson(encoded)).toBe(true) + }) + + it('should decode JSON primitive number', () => { + const input = 42.5 + const encoded = new TextEncoder().encode(JSON.stringify(input)) + expect(decodeJson(encoded)).toBe(input) + }) + + it('should decode JSON primitive string', () => { + const input = 'hello world' + const encoded = new TextEncoder().encode(JSON.stringify(input)) + expect(decodeJson(encoded)).toBe(input) + }) + + it('should decode an empty object', () => { + const input = {} + const encoded = new TextEncoder().encode(JSON.stringify(input)) + expect(decodeJson(encoded)).toEqual(input) + }) + + it('should decode an empty array', () => { + const input: unknown[] = [] + const encoded = new TextEncoder().encode(JSON.stringify(input)) + expect(decodeJson(encoded)).toEqual(input) + }) + }) + + describe('invalid JSON inputs', () => { + it('should throw error for invalid JSON', () => { + const encoded = new TextEncoder().encode('invalid json') + expect(() => decodeJson(encoded)).toThrow() + }) + + it('should throw error for incomplete JSON object', () => { + const encoded = new TextEncoder().encode('{"name": "test"') + expect(() => decodeJson(encoded)).toThrow() + }) + + it('should throw error for incomplete JSON array', () => { + const encoded = new TextEncoder().encode('[1, 2, 3') + expect(() => decodeJson(encoded)).toThrow() + }) + + it('should throw error for empty byte array', () => { + const encoded = new Uint8Array() + expect(() => decodeJson(encoded)).toThrow() + }) + + it('should throw error for malformed JSON with trailing comma', () => { + const encoded = new TextEncoder().encode('{"name": "test",}') + expect(() => decodeJson(encoded)).toThrow() + }) + + it('should throw error for unquoted keys', () => { + const encoded = new TextEncoder().encode('{name: "test"}') + expect(() => decodeJson(encoded)).toThrow() + }) + + it('should throw error for single quotes instead of double quotes', () => { + const encoded = new TextEncoder().encode("{'name': 'test'}") + expect(() => decodeJson(encoded)).toThrow() + }) + }) + + describe('edge cases', () => { + it('should handle JSON with whitespace', () => { + const input = { name: 'test' } + const encoded = new TextEncoder().encode(' \n {"name": "test"} \n ') + expect(decodeJson(encoded)).toEqual(input) + }) + + it('should handle JSON with zero values', () => { + const input = { zero: 0, empty: '', falsy: false } + const encoded = new TextEncoder().encode(JSON.stringify(input)) + expect(decodeJson(encoded)).toEqual(input) + }) + + it('should handle deeply nested JSON', () => { + const input = { + level1: { + level2: { + level3: { + level4: { + level5: { + value: 'deep', + }, + }, + }, + }, + }, + } + const encoded = new TextEncoder().encode(JSON.stringify(input)) + expect(decodeJson(encoded)).toEqual(input) + }) + + it('should handle large arrays', () => { + const input = Array.from({ length: 1000 }, (_, i) => i) + const encoded = new TextEncoder().encode(JSON.stringify(input)) + expect(decodeJson(encoded)).toEqual(input) + }) + + it('should handle JSON with null values in objects', () => { + const input = { value: null, nested: { inner: null } } + const encoded = new TextEncoder().encode(JSON.stringify(input)) + expect(decodeJson(encoded)).toEqual(input) + }) + }) +}) diff --git a/packages/cre-sdk/src/sdk/utils/decode-json.ts b/packages/cre-sdk/src/sdk/utils/decode-json.ts new file mode 100644 index 00000000..0fc128dd --- /dev/null +++ b/packages/cre-sdk/src/sdk/utils/decode-json.ts @@ -0,0 +1,12 @@ +/** + * Decodes a Uint8Array into a JSON object. + * Function would throw if the input is not a valid JSON string encoded as bytes. + * + * @param input - The Uint8Array to decode. + * @returns The decoded JSON object. + */ +export const decodeJson = (input: Uint8Array) => { + const decoder = new TextDecoder('utf-8') + const textBody = decoder.decode(input) + return JSON.parse(textBody) +} diff --git a/packages/cre-sdk/src/sdk/utils/index.ts b/packages/cre-sdk/src/sdk/utils/index.ts index 05fa4900..6b4221da 100644 --- a/packages/cre-sdk/src/sdk/utils/index.ts +++ b/packages/cre-sdk/src/sdk/utils/index.ts @@ -1,6 +1,7 @@ export * from './capabilities/blockchain/blockchain-helpers' export * from './capabilities/http/http-helpers' export * from './chain-selectors' +export * from './decode-json' export * from './hex-utils' export * from './values/consensus_aggregators' export * from './values/serializer_types' diff --git a/packages/cre-sdk/tsconfig.build.json b/packages/cre-sdk/tsconfig.build.json index 120fa05e..9edef799 100644 --- a/packages/cre-sdk/tsconfig.build.json +++ b/packages/cre-sdk/tsconfig.build.json @@ -1,11 +1,10 @@ { "compilerOptions": { // Enable latest features - "lib": ["ESNext", "DOM"], + "lib": ["ESNext"], "target": "ESNext", "module": "ESNext", "moduleDetection": "force", - "jsx": "react-jsx", "allowJs": true, "outDir": "./dist", "rootDir": "./src", diff --git a/packages/cre-sdk/tsconfig.json b/packages/cre-sdk/tsconfig.json index 5299b17f..ff7b2b43 100644 --- a/packages/cre-sdk/tsconfig.json +++ b/packages/cre-sdk/tsconfig.json @@ -1,11 +1,10 @@ { "compilerOptions": { // Enable latest features - "lib": ["ESNext", "DOM", "DOM.Iterable"], + "lib": ["ESNext"], "target": "ESNext", "module": "ESNext", "moduleDetection": "force", - "jsx": "react-jsx", "allowJs": true, // Bundler mode