From 3f8dc1f8e98e06c49046d8283f5b12948caa4335 Mon Sep 17 00:00:00 2001 From: Andreas Gassmann Date: Thu, 9 Apr 2026 13:30:34 +0200 Subject: [PATCH 1/4] feat: add execution time warning --- README.md | 11 +++++++---- acurast.json | 2 +- src/acurast/createJob.ts | 8 ++++++-- src/devtools/devtools-snippet.ts | 4 +--- src/devtools/injectDevtoolsSnippet.ts | 2 -- src/util/validateConfig.ts | 13 +++++++++++++ test/devtools.test.ts | 13 ++----------- 7 files changed, 30 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 31a8a95..601d8dc 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ The acurast.json file is generated by running acurast init. Here is an example c "fileUrl": "dist/bundle.js", "network": "mainnet", "onlyAttestedDevices": true, + "enableDevtools": false, "assignmentStrategy": { "type": "Single" }, @@ -120,6 +121,7 @@ ACURAST_MNEMONIC=abandon abandon about ... - `fileUrl`: The path to the bundled file, including all dependencies (e.g., `dist/bundle.js`). - `network`: The network on which the project will be deployed. (e.g. `mainnet`) - `onlyAttestedDevices`: A boolean to specify if only attested devices are allowed to run the app. +- `enableDevtools`: A boolean to enable [DevTools](#devtools) for the deployment. When enabled, console logs from processor executions are forwarded to the DevTools dashboard. Defaults to `false`. - `startAt`: The start time of the deployment. - `msFromNow`: The deployment will start the specified number of milliseconds from now. - `timestamp`: The deployment will start at the specified timestamp. @@ -134,6 +136,7 @@ ACURAST_MNEMONIC=abandon abandon about ... - `type`: 'interval'`: Multiple executions for the deployment. - `intervalInMs`: Interval in milliseconds between each execution start. - `numberOfExecutions`: The number of executions. + - `maxExecutionTimeInMs`: Maximum execution time for each execution in milliseconds. If not specified, the full duration of the interval will be used (minus a 10s buffer). It is recommended to set this to at least 10s less than `intervalInMs`. - `maxAllowedStartDelayInMs`: Specifies the maximum allowed start delay (relative to the starting time) of the deployment in milliseconds. - `usageLimit`: The usage limits for the deployment: - `maxMemory`: Maximum memory usage in bytes. @@ -277,10 +280,10 @@ Logs are only accessible with a valid view key. The key is scoped to the specifi ### Environment variables -| Variable | Default | Description | -|---|---|---| -| `ACURAST_DEVTOOLS_URL` | `https://devtools.acurast.com` | DevTools frontend URL | -| `ACURAST_DEVTOOLS_API_URL` | `https://api.devtools.acurast.com` | DevTools API URL | +| Variable | Default | Description | +| -------------------------- | ---------------------------------- | --------------------- | +| `ACURAST_DEVTOOLS_URL` | `https://devtools.acurast.com` | DevTools frontend URL | +| `ACURAST_DEVTOOLS_API_URL` | `https://api.devtools.acurast.com` | DevTools API URL | ## Deployment Management diff --git a/acurast.json b/acurast.json index 00a7f63..631b3a5 100644 --- a/acurast.json +++ b/acurast.json @@ -224,7 +224,7 @@ "maxNetworkRequests": 0, "maxStorage": 0 }, - "numberOfReplicas": 20, + "numberOfReplicas": 1, "requiredModules": [], "minProcessorReputation": 0, "maxCostPerExecution": 3000000000, diff --git a/src/acurast/createJob.ts b/src/acurast/createJob.ts index bdfcd34..5dd75ba 100644 --- a/src/acurast/createJob.ts +++ b/src/acurast/createJob.ts @@ -48,7 +48,12 @@ export const createJob = async ( if (config.fileUrl.startsWith('ipfs://')) { ipfsHash = config.fileUrl - filelogger.debug(`config.fileUrl is an IPFS hash, so we this: ${ipfsHash}`) + if (config.enableDevtools) { + filelogger.warn( + 'enableDevtools is ignored when fileUrl is an IPFS hash — the devtools snippet can only be injected into local bundles.' + ) + } + filelogger.debug(`config.fileUrl is an IPFS hash, so we use this: ${ipfsHash}`) } else { filelogger.debug( `config.fileUrl is not an IPFS hash, so we zip it: ${config.fileUrl}` @@ -84,7 +89,6 @@ export const createJob = async ( zipPath, config.entrypoint ?? basename(config.fileUrl), devtoolsApiUrl, - wallet.address, snippetDir ) filelogger.debug('Devtools snippet injected into bundle') diff --git a/src/devtools/devtools-snippet.ts b/src/devtools/devtools-snippet.ts index eca2a86..0b2c7cc 100644 --- a/src/devtools/devtools-snippet.ts +++ b/src/devtools/devtools-snippet.ts @@ -2,12 +2,10 @@ // of user scripts when enableDevtools is true. It overrides console methods to // forward logs to the Acurast DevTools API. // -// Placeholders __DEVTOOLS_API_URL__ and __DEVTOOLS_DEPLOYER__ are replaced at -// injection time by the CLI. +// Placeholder __DEVTOOLS_API_URL__ is replaced at injection time by the CLI. ;(() => { const DEVTOOLS_API_URL = '__DEVTOOLS_API_URL__' - const DEVTOOLS_DEPLOYER = '__DEVTOOLS_DEPLOYER__' const originalConsole = { log: console.log, diff --git a/src/devtools/injectDevtoolsSnippet.ts b/src/devtools/injectDevtoolsSnippet.ts index 9d1a170..211a33f 100644 --- a/src/devtools/injectDevtoolsSnippet.ts +++ b/src/devtools/injectDevtoolsSnippet.ts @@ -10,7 +10,6 @@ export async function injectDevtoolsSnippet( zipPath: string, entrypoint: string, devtoolsApiUrl: string, - deployerAddress: string, snippetDir: string ): Promise { const snippetPath = join(snippetDir, 'devtools-snippet.js') @@ -23,7 +22,6 @@ export async function injectDevtoolsSnippet( .trim() snippet = snippet.replace(/__DEVTOOLS_API_URL__/g, devtoolsApiUrl) - snippet = snippet.replace(/__DEVTOOLS_DEPLOYER__/g, deployerAddress) const zip = new AdmZip(zipPath) const entry = zip.getEntry(entrypoint) diff --git a/src/util/validateConfig.ts b/src/util/validateConfig.ts index bf11ccd..163ea3f 100644 --- a/src/util/validateConfig.ts +++ b/src/util/validateConfig.ts @@ -236,6 +236,19 @@ const acurastProjectConfigSchemaWithNotes = } } + if ( + data.execution.type === 'interval' && + data.execution.maxExecutionTimeInMs !== undefined && + data.execution.maxExecutionTimeInMs > data.execution.intervalInMs - 10_000 + ) { + context.addIssue({ + code: z.ZodIssueCode.custom, + message: + 'Warning: maxExecutionTimeInMs should be at least 10s shorter than intervalInMs to allow enough time between executions.', + path: ['execution', 'maxExecutionTimeInMs'], + }) + } + // TODO: Add check for competing strategy and intervals }) diff --git a/test/devtools.test.ts b/test/devtools.test.ts index a5995ff..ab3e228 100644 --- a/test/devtools.test.ts +++ b/test/devtools.test.ts @@ -44,7 +44,6 @@ describe('devtools snippet injection', () => { zipPath, 'index.js', 'https://api.devtools.acurast.com', - '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', SNIPPET_DIR ) @@ -68,7 +67,6 @@ describe('devtools snippet injection', () => { zipPath, 'index.js', apiUrl, - '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', SNIPPET_DIR ) @@ -79,23 +77,19 @@ describe('devtools snippet injection', () => { expect(content).not.toContain('__DEVTOOLS_API_URL__') }) - it('should replace the deployer placeholder', async () => { + it('should not contain any unreplaced placeholders', async () => { const zipPath = createTestZip('index.js', '// user code') - const deployer = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY' - await injectDevtoolsSnippet( zipPath, 'index.js', 'https://api.devtools.acurast.com', - deployer, SNIPPET_DIR ) const zip = new AdmZip(zipPath) const content = zip.getEntry('index.js')!.getData().toString('utf-8') - expect(content).toContain(deployer) - expect(content).not.toContain('__DEVTOOLS_DEPLOYER__') + expect(content).not.toContain('__DEVTOOLS_API_URL__') }) it('should strip TSC artifacts (export {}, sourcemap comment)', async () => { @@ -105,7 +99,6 @@ describe('devtools snippet injection', () => { zipPath, 'index.js', 'https://api.devtools.acurast.com', - '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', SNIPPET_DIR ) @@ -142,7 +135,6 @@ describe('devtools snippet injection', () => { zipPath, 'index.js', 'https://api.devtools.acurast.com', - '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', SNIPPET_DIR ) @@ -163,7 +155,6 @@ describe('devtools snippet injection', () => { zipPath, 'index.js', 'https://api.devtools.acurast.com', - '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', SNIPPET_DIR ) From 2c312dd644522e250b28179fad84346cac0ad4bf Mon Sep 17 00:00:00 2001 From: Andreas Gassmann Date: Thu, 9 Apr 2026 13:31:16 +0200 Subject: [PATCH 2/4] chore(release): v0.7.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e94808d..01ac63a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@acurast/cli", - "version": "0.7.0", + "version": "0.7.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@acurast/cli", - "version": "0.7.0", + "version": "0.7.1", "license": "UNLICENSED", "dependencies": { "@acurast/dapp": "^1.0.1", diff --git a/package.json b/package.json index 14a779e..79fdeed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@acurast/cli", - "version": "0.7.0", + "version": "0.7.1", "description": "A cli to interact with the Acurast Cloud.", "main": "dist/index.js", "bin": { From f527b359fd88931ff3e9f9c06f87cf25c09fd68a Mon Sep 17 00:00:00 2001 From: Andreas Gassmann Date: Thu, 9 Apr 2026 13:35:47 +0200 Subject: [PATCH 3/4] fix: test --- test/devtools.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/devtools.test.ts b/test/devtools.test.ts index ab3e228..62ba0ac 100644 --- a/test/devtools.test.ts +++ b/test/devtools.test.ts @@ -117,7 +117,6 @@ describe('devtools snippet injection', () => { zipPath, 'nonexistent.js', 'https://api.devtools.acurast.com', - '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', SNIPPET_DIR ) ).rejects.toThrow('Could not find entrypoint') From 64c0804aa563486b93f635ae309b0f5fa36d96e2 Mon Sep 17 00:00:00 2001 From: Andreas Gassmann Date: Fri, 10 Apr 2026 14:23:26 +0200 Subject: [PATCH 4/4] feat: add file upload --- examples/devtools-test.js | 44 +++++++++++++++++++++++ src/acurast/createJob.ts | 1 - src/devtools/acurast-processor.d.ts | 16 +++++++++ src/devtools/devtools-snippet.ts | 50 +++++++++++++++++++++++++-- src/devtools/injectDevtoolsSnippet.ts | 2 -- test/devtools.test.ts | 32 +---------------- 6 files changed, 108 insertions(+), 37 deletions(-) diff --git a/examples/devtools-test.js b/examples/devtools-test.js index 72c1269..700c762 100644 --- a/examples/devtools-test.js +++ b/examples/devtools-test.js @@ -3,6 +3,20 @@ console.log("Devtools test started", { jobId: _STD_.job.getId(), device: _STD_.device.getAddress() }) console.info("Processor info", { timestamp: Date.now() }) +// Probe runtime for HTTP/upload primitives +console.log("Runtime probe", { + fetch: typeof fetch, + FormData: typeof FormData, + Blob: typeof Blob, + XMLHttpRequest: typeof XMLHttpRequest, + Request: typeof Request, + Response: typeof Response, + Headers: typeof Headers, + Buffer: typeof Buffer, + btoa: typeof btoa, + atob: typeof atob, +}) + let tick = 0 const interval = setInterval(() => { tick++ @@ -20,6 +34,36 @@ const interval = setInterval(() => { } } + if (tick === 2) { + const filename = "test-upload.json" + const content = JSON.stringify({ + message: "hello from acurast processor", + jobId: _STD_.job.getId().id, + device: _STD_.device.getAddress(), + timestamp: Date.now(), + }) + + console.log("Uploading file: " + filename + " (" + content.length + " bytes)") + + _DEVTOOLS_.uploadFile( + filename, + content, + "application/json", + (fileInfo) => { + console.log("Upload succeeded", { + id: fileInfo.id, + filename: fileInfo.filename, + mimeType: fileInfo.mimeType, + fileSize: fileInfo.fileSize, + createdAt: fileInfo.createdAt, + }) + }, + (error) => { + console.error("Upload failed:", error) + } + ) + } + if (tick >= 12) { clearInterval(interval) console.log("Devtools test complete after " + tick + " ticks") diff --git a/src/acurast/createJob.ts b/src/acurast/createJob.ts index bdfcd34..2066ef6 100644 --- a/src/acurast/createJob.ts +++ b/src/acurast/createJob.ts @@ -84,7 +84,6 @@ export const createJob = async ( zipPath, config.entrypoint ?? basename(config.fileUrl), devtoolsApiUrl, - wallet.address, snippetDir ) filelogger.debug('Devtools snippet injected into bundle') diff --git a/src/devtools/acurast-processor.d.ts b/src/devtools/acurast-processor.d.ts index f110dc4..d236439 100644 --- a/src/devtools/acurast-processor.d.ts +++ b/src/devtools/acurast-processor.d.ts @@ -8,6 +8,22 @@ declare function httpPOST( onError: (error: string) => void ): void +declare const _DEVTOOLS_: { + uploadFile( + filename: string, + content: string, + mimeType: string, + onSuccess: (fileInfo: { + id: number + filename: string + mimeType: string + fileSize: number + createdAt: string + }) => void, + onError: (error: string) => void + ): void +} + declare const _STD_: { job: { getId(): { origin: { kind: string; source: string }; id: string } diff --git a/src/devtools/devtools-snippet.ts b/src/devtools/devtools-snippet.ts index eca2a86..49b3c2d 100644 --- a/src/devtools/devtools-snippet.ts +++ b/src/devtools/devtools-snippet.ts @@ -2,12 +2,10 @@ // of user scripts when enableDevtools is true. It overrides console methods to // forward logs to the Acurast DevTools API. // -// Placeholders __DEVTOOLS_API_URL__ and __DEVTOOLS_DEPLOYER__ are replaced at -// injection time by the CLI. +// Placeholder __DEVTOOLS_API_URL__ is replaced at injection time by the CLI. ;(() => { const DEVTOOLS_API_URL = '__DEVTOOLS_API_URL__' - const DEVTOOLS_DEPLOYER = '__DEVTOOLS_DEPLOYER__' const originalConsole = { log: console.log, @@ -133,6 +131,52 @@ } } + // --- File upload via /v1/files --- + const uploadFile = ( + filename: string, + content: string, + mimeType: string, + onSuccess: (fileInfo: { id: number; filename: string; mimeType: string; fileSize: number; createdAt: string }) => void, + onError: (error: string) => void + ) => { + if (!apiKey) { + onError('[devtools] file upload failed: not authenticated yet') + return + } + + // The processor's httpPOST JSON-parses the body regardless of Content-Type, + // so we cannot send multipart/form-data through it. Use fetch + FormData + // instead — fetch sets the Content-Type header (with boundary) automatically. + const formData = new FormData() + formData.append('file', new Blob([content], { type: mimeType }), filename) + + fetch(`${DEVTOOLS_API_URL}/v1/files`, { + method: 'POST', + headers: { Authorization: 'Bearer ' + apiKey }, + body: formData, + }) + .then((res: Response) => + res.text().then((text: string) => ({ ok: res.ok, status: res.status, text })) + ) + .then(({ ok, status, text }: { ok: boolean; status: number; text: string }) => { + if (!ok) { + onError('[devtools] file upload failed: HTTP ' + status + ' ' + text) + return + } + try { + onSuccess(JSON.parse(text)) + } catch (_e) { + onError('[devtools] failed to parse upload response: ' + text) + } + }) + .catch((err: any) => { + onError('[devtools] file upload failed: ' + (err?.message ?? String(err))) + }) + } + + // Expose _DEVTOOLS_ global + ;(globalThis as any)._DEVTOOLS_ = { uploadFile } + for (const level of Object.keys(originalConsole) as Array< keyof typeof originalConsole >) { diff --git a/src/devtools/injectDevtoolsSnippet.ts b/src/devtools/injectDevtoolsSnippet.ts index 9d1a170..211a33f 100644 --- a/src/devtools/injectDevtoolsSnippet.ts +++ b/src/devtools/injectDevtoolsSnippet.ts @@ -10,7 +10,6 @@ export async function injectDevtoolsSnippet( zipPath: string, entrypoint: string, devtoolsApiUrl: string, - deployerAddress: string, snippetDir: string ): Promise { const snippetPath = join(snippetDir, 'devtools-snippet.js') @@ -23,7 +22,6 @@ export async function injectDevtoolsSnippet( .trim() snippet = snippet.replace(/__DEVTOOLS_API_URL__/g, devtoolsApiUrl) - snippet = snippet.replace(/__DEVTOOLS_DEPLOYER__/g, deployerAddress) const zip = new AdmZip(zipPath) const entry = zip.getEntry(entrypoint) diff --git a/test/devtools.test.ts b/test/devtools.test.ts index a5995ff..69daacc 100644 --- a/test/devtools.test.ts +++ b/test/devtools.test.ts @@ -44,7 +44,6 @@ describe('devtools snippet injection', () => { zipPath, 'index.js', 'https://api.devtools.acurast.com', - '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', SNIPPET_DIR ) @@ -64,13 +63,7 @@ describe('devtools snippet injection', () => { const zipPath = createTestZip('index.js', '// user code') const apiUrl = 'https://custom-api.example.com' - await injectDevtoolsSnippet( - zipPath, - 'index.js', - apiUrl, - '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', - SNIPPET_DIR - ) + await injectDevtoolsSnippet(zipPath, 'index.js', apiUrl, SNIPPET_DIR) const zip = new AdmZip(zipPath) const content = zip.getEntry('index.js')!.getData().toString('utf-8') @@ -79,25 +72,6 @@ describe('devtools snippet injection', () => { expect(content).not.toContain('__DEVTOOLS_API_URL__') }) - it('should replace the deployer placeholder', async () => { - const zipPath = createTestZip('index.js', '// user code') - const deployer = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY' - - await injectDevtoolsSnippet( - zipPath, - 'index.js', - 'https://api.devtools.acurast.com', - deployer, - SNIPPET_DIR - ) - - const zip = new AdmZip(zipPath) - const content = zip.getEntry('index.js')!.getData().toString('utf-8') - - expect(content).toContain(deployer) - expect(content).not.toContain('__DEVTOOLS_DEPLOYER__') - }) - it('should strip TSC artifacts (export {}, sourcemap comment)', async () => { const zipPath = createTestZip('index.js', '// user code') @@ -105,7 +79,6 @@ describe('devtools snippet injection', () => { zipPath, 'index.js', 'https://api.devtools.acurast.com', - '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', SNIPPET_DIR ) @@ -124,7 +97,6 @@ describe('devtools snippet injection', () => { zipPath, 'nonexistent.js', 'https://api.devtools.acurast.com', - '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', SNIPPET_DIR ) ).rejects.toThrow('Could not find entrypoint') @@ -142,7 +114,6 @@ describe('devtools snippet injection', () => { zipPath, 'index.js', 'https://api.devtools.acurast.com', - '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', SNIPPET_DIR ) @@ -163,7 +134,6 @@ describe('devtools snippet injection', () => { zipPath, 'index.js', 'https://api.devtools.acurast.com', - '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', SNIPPET_DIR )