diff --git a/README.md b/README.md index bd875a8..2957f0a 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ The CLI is available via the `ecloud` command after building: ```bash # Deploy an application -pnpm ecloud app deploy \ +npx ecloud app deploy \ --private-key \ --environment sepolia \ --image @@ -74,14 +74,12 @@ pnpm ecloud app deploy \ - `--private-key`: Your Ethereum private key (or set `ECLOUD_PRIVATE_KEY` env var) - `--environment`: Target environment (`sepolia` or `mainnet-alpha`) - `--rpc-url`: Custom RPC URL (optional, or set `ECLOUD_RPC_URL` env var) -- `--api-base-url`: API base URL (optional, or set `ECLOUD_API_BASE_URL` env var) **Example:** ```bash -pnpm ecloud app deploy \ +npx ecloud app deploy \ --private-key 0x... \ - --environment sepolia \ - --image myapp:latest + --environment sepolia ``` ### SDK Usage diff --git a/packages/cli/package.json b/packages/cli/package.json index d6bcee8..6fda724 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -5,16 +5,19 @@ "ecloud": "./bin/run.js" }, "dependencies": { + "@inquirer/prompts": "^7.10.1", "@layr-labs/ecloud-sdk": "workspace:*", "@oclif/core": "^4.8.0", - "viem": "^2.38.6", "dockerode": "^4.0.9", "handlebars": "^4.7.8", + "js-yaml": "^4.1.1", "node-forge": "^1.3.1", - "undici": "^7.16.0" + "undici": "^7.16.0", + "viem": "^2.38.6" }, "scripts": { - "build": "tsup", + "build": "tsup && npm run build:copy-templates", + "build:copy-templates": "node scripts/copy-templates.js", "lint": "eslint .", "format": "prettier --check .", "format:fix": "prettier --write .", diff --git a/packages/cli/scripts/copy-templates.js b/packages/cli/scripts/copy-templates.js new file mode 100644 index 0000000..430e2e4 --- /dev/null +++ b/packages/cli/scripts/copy-templates.js @@ -0,0 +1,69 @@ +#!/usr/bin/env node + +import * as fs from "fs"; +import * as path from "path"; +import { fileURLToPath } from "url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const cliRoot = path.resolve(__dirname, ".."); +const sdkRoot = path.resolve(cliRoot, "../sdk"); + +// Template files to copy from SDK to CLI dist +const templates = [ + "src/client/common/templates/Dockerfile.layered.tmpl", + "src/client/common/templates/compute-source-env.sh.tmpl", +]; + +// Create templates directory in CLI dist +const distTemplatesDir = path.join(cliRoot, "dist", "templates"); +if (!fs.existsSync(distTemplatesDir)) { + fs.mkdirSync(distTemplatesDir, { recursive: true }); +} + +// Copy each template file +for (const template of templates) { + const srcPath = path.join(sdkRoot, template); + const filename = path.basename(template); + const destPath = path.join(distTemplatesDir, filename); + + if (!fs.existsSync(srcPath)) { + console.warn(`Warning: Template file not found: ${srcPath}`); + continue; + } + + fs.copyFileSync(srcPath, destPath); + console.log(`Copied ${filename} to dist/templates/`); +} + +console.log("Template files copied successfully"); + +// Copy keys directory structure from SDK to CLI dist +const keysSrcDir = path.join(sdkRoot, "keys"); +const keysDistDir = path.join(cliRoot, "dist", "keys"); + +if (fs.existsSync(keysSrcDir)) { + // Copy entire keys directory structure recursively + function copyDirRecursive(src, dest) { + if (!fs.existsSync(dest)) { + fs.mkdirSync(dest, { recursive: true }); + } + + const entries = fs.readdirSync(src, { withFileTypes: true }); + for (const entry of entries) { + const srcPath = path.join(src, entry.name); + const destPath = path.join(dest, entry.name); + + if (entry.isDirectory()) { + copyDirRecursive(srcPath, destPath); + } else { + fs.copyFileSync(srcPath, destPath); + } + } + } + + copyDirRecursive(keysSrcDir, keysDistDir); + + console.log("Keys directory copied successfully"); +} else { + console.warn("Warning: Keys directory not found at", keysSrcDir); +} diff --git a/packages/cli/src/client.ts b/packages/cli/src/client.ts index 06d2d04..e599a74 100644 --- a/packages/cli/src/client.ts +++ b/packages/cli/src/client.ts @@ -1,15 +1,15 @@ import { createECloudClient } from "@ecloud/sdk"; export function loadClient(flags: { - privateKey: string; + verbose: boolean; environment: string; - rpcUrl?: string; - apiBaseUrl?: string; + "private-key": string; + "rpc-url"?: string; }) { return createECloudClient({ - privateKey: flags.privateKey as `0x${string}`, + verbose: flags.verbose, environment: flags.environment, - rpcUrl: flags.rpcUrl, - apiBaseUrl: flags.apiBaseUrl, + privateKey: flags["private-key"] as `0x${string}`, + rpcUrl: flags["rpc-url"], }); } diff --git a/packages/cli/src/commands/app/create.ts b/packages/cli/src/commands/app/create.ts new file mode 100644 index 0000000..8fae8d1 --- /dev/null +++ b/packages/cli/src/commands/app/create.ts @@ -0,0 +1,28 @@ +import { Command, Flags } from "@oclif/core"; +import { createApp } from "@ecloud/sdk"; + +export default class AppCreate extends Command { + static description = "Create a new app"; + + // CreateApp flags + static flags = { + name: Flags.string(), + language: Flags.string(), + template: Flags.string(), + templateVersion: Flags.string(), + verbose: Flags.boolean(), + }; + + async run() { + const { flags } = await this.parse(AppCreate); + + // Skip creating client and call createApp directly + return createApp(flags, { + info: (msg: string, ...args: any[]) => console.log(msg, ...args), + warn: (msg: string, ...args: any[]) => console.warn(msg, ...args), + error: (msg: string, ...args: any[]) => console.error(msg, ...args), + debug: (msg: string, ...args: any[]) => + flags.verbose && console.debug(msg, ...args), + }); + } +} diff --git a/packages/cli/src/commands/app/deploy.ts b/packages/cli/src/commands/app/deploy.ts index f2af054..f47d2a3 100644 --- a/packages/cli/src/commands/app/deploy.ts +++ b/packages/cli/src/commands/app/deploy.ts @@ -1,4 +1,5 @@ import { Command, Flags } from "@oclif/core"; +import { logVisibility } from "@ecloud/sdk"; import { loadClient } from "../../client"; import { commonFlags } from "../../flags"; @@ -7,11 +8,41 @@ export default class AppDeploy extends Command { static flags = { ...commonFlags, - image: Flags.string({ required: true }), - owner: Flags.string(), - cpu: Flags.integer(), - memory: Flags.integer(), - salt: Flags.string(), + name: Flags.string({ + required: false, + description: "Friendly name for the app", + env: "ECLOUD_NAME", + }), + dockerfile: Flags.string({ + required: false, + description: "Path to Dockerfile", + env: "ECLOUD_DOCKERFILE_PATH", + }), + "image-ref": Flags.string({ + required: false, + description: "Image reference pointing to registry", + env: "ECLOUD_IMAGE_REF", + }), + "env-file": Flags.string({ + required: false, + description: 'Environment file to use (default: ".env")', + default: ".env", + env: "ECLOUD_ENVFILE_PATH", + }), + "log-visibility": Flags.string({ + required: false, + description: "Log visibility setting: public, private, or off", + options: ["public", "private", "off"], + env: "ECLOUD_LOG_VISIBILITY", + }), + "instance-type": Flags.string({ + required: false, + description: + "Machine instance type to use e.g. g1-standard-4t, g1-standard-8t", + default: "g1-standard-4t", + options: ["g1-standard-4t", "g1-standard-8t"], + env: "ECLOUD_INSTANCE_TYPE", + }), }; async run() { @@ -19,10 +50,12 @@ export default class AppDeploy extends Command { const client = loadClient(flags); const res = await client.app.deploy({ - image: flags.image, - owner: flags.owner as `0x${string}` | undefined, - resources: { cpu: flags.cpu, memoryMiB: flags.memory }, - salt: flags.salt as `0x${string}` | undefined, + name: flags.name, + dockerfile: flags.dockerfile, + envFile: flags["env-file"], + imageRef: flags["image-ref"], + logVisibility: flags["log-visibility"] as logVisibility, + instanceType: flags["instance-type"], }); this.log(JSON.stringify(res, null, 2)); diff --git a/packages/cli/src/flags.ts b/packages/cli/src/flags.ts index a8d9b41..a7a37ea 100644 --- a/packages/cli/src/flags.ts +++ b/packages/cli/src/flags.ts @@ -1,12 +1,25 @@ import { Flags } from "@oclif/core"; export const commonFlags = { - privateKey: Flags.string({ required: true, env: "ECLOUD_PRIVATE_KEY" }), environment: Flags.string({ required: true, + description: "Deployment environment to use", options: ["sepolia", "mainnet-alpha"], env: "ECLOUD_ENV", }), - rpcUrl: Flags.string({ required: false, env: "ECLOUD_RPC_URL" }), - apiBaseUrl: Flags.string({ required: false, env: "ECLOUD_API_BASE_URL" }), + "private-key": Flags.string({ + required: true, + description: "Private key for signing transactions", + env: "ECLOUD_PRIVATE_KEY", + }), + "rpc-url": Flags.string({ + required: false, + description: "RPC URL to connect to blockchain", + env: "ECLOUD_RPC_URL", + }), + verbose: Flags.boolean({ + required: false, + description: "Enable verbose logging (default: false)", + default: false, + }), }; diff --git a/packages/sdk/keys/mainnet-alpha/prod/kms-encryption-public-key.pem b/packages/sdk/keys/mainnet-alpha/prod/kms-encryption-public-key.pem new file mode 100644 index 0000000..35bf310 --- /dev/null +++ b/packages/sdk/keys/mainnet-alpha/prod/kms-encryption-public-key.pem @@ -0,0 +1,14 @@ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0kHU86k17ofCIGcJKDcf +AFurFhSLeWmOL0bwWLCeVnTPG0MMHtJOq+woE0XXSWw6lzm+jzavBBTwKde1dgal +Ap91vULAZFMUpiUdd2dNUVtvU89qW0Pgf1Eu5FDj7BkY/SnyECbWJM4ga0BmpiGy +nQwLNN9mMGhjVoVLn2zwEGZ7JzS9Nz11EZKO/k/9DcO6LaoIFmKuvVf3jl6lvZg8 +aeA0LoZXjkycHlRUt/kfKwZnhakUaYHP1ksV7ZNmolS5GYDTSKGB2KPPNR1s4/Xu +u8zeEFC8HuGRU8XuuBeaAunitnGhbNVREUNJGff6HZOGB6CIFNXjbQETeZ3p5uro +0v+hd1QqQYBv7+DEaMCmGnJNGAyIMr2mn4vr7wGsIj0HonlSHmQ8rmdUhL2ocNTc +LhKgZiZmBuDpSbFW/r53R2G7CHcqaqGeUBnT54QCH4zsYKw0/4dOtwFxQpTyBf9/ ++k+KaWEJYKkx9d9OzKGyAvzrTDVOFoajddiJ6LPvRlMdOUQr3hl4IAC0/nh9lhHq +D0R+i5WAU96TkdAe7B7iTGH2D22k0KUPR6Q9W3aF353SLxQAMPNrgG4QQufAdRJn +AF+8ntun5TkTqjTWRSwAsUJZ1z4wb96DympWJbDi0OciJRZ3Fz3j9+amC43yCHGg +aaEMjdt35ewbztUSc04F10MCAwEAAQ== +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/packages/sdk/keys/mainnet-alpha/prod/kms-signing-public-key.pem b/packages/sdk/keys/mainnet-alpha/prod/kms-signing-public-key.pem new file mode 100644 index 0000000..063a425 --- /dev/null +++ b/packages/sdk/keys/mainnet-alpha/prod/kms-signing-public-key.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfxbhXJjH4D0DH/iW5/rK1HzWS+f9 +EyooZTrCYjCfezuOEmRuOWNaZLvwXN8SdzrvjWA7gSvOS85hLzp4grANRQ== +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/packages/sdk/keys/sepolia/dev/kms-encryption-public-key.pem b/packages/sdk/keys/sepolia/dev/kms-encryption-public-key.pem new file mode 100644 index 0000000..3420ab8 --- /dev/null +++ b/packages/sdk/keys/sepolia/dev/kms-encryption-public-key.pem @@ -0,0 +1,14 @@ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr/vqttU6aXX35HtsXavU +5teysunDzZB3HyaFM4qcuRnqj+70KxqLOwZsERN5SwZ/56Jm8T2ds1CcXsQCMUMw ++MPlsF6KMGfzghLtYHONwvKLnn+U9y886aAay6W8a0A7O7YCZehNYD3kQnCXjOIc +Mj6v8AEvMw+w/lNabjRXnwSBMKVIGp/cSL0hGwt8fGoC3TsxQN9opzvU1Z4rAw9K +a119l6dlPnqezDva378TCaXDjqKe/jSZOI1CcYpaSK2SJ+95Wbvte5j3lXbg1oT2 +0rXeJUHEJ68QxMtJplfw0Sg+Ek4CUJ2c/kbdg0u7sIIO5wcB4WHL/Lfbw2XPmcBI +t0r0EC575D3iHF/aI01Ms2IRA0GDeHnNcr5FJLWJljTjNLEt4tFITrXwBe1Ealm3 +NCxamApl5bBSwQ72Gb5fiQFwB8Fl2/XG3wfGTFInFEvWE4c/H8dtu1wHTsyEFZcG +B47IkD5GBSZq90Hd9xuZva55dxGpqUVrEJO88SqHGP9Oa+HLTYdEe5AR5Hitw4Mu +dk1cCH+X5OqY9dfpdoCNbKAM0N2SJvNAnDTU2JKGYheXrnDslXR6atBmU5gDkH+W +QVryDYl9xbwWIACMQsAQjrrtKw5xqJ4V89+06FN/wyEVF7KWAcJ4AhKiVnCvLqzb +BbISc+gOkRsefhCDJVPEKDkCAwEAAQ== +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/packages/sdk/keys/sepolia/dev/kms-signing-public-key.pem b/packages/sdk/keys/sepolia/dev/kms-signing-public-key.pem new file mode 100644 index 0000000..d46727c --- /dev/null +++ b/packages/sdk/keys/sepolia/dev/kms-signing-public-key.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEb2Q88/cxdic2xi4jS2V0dtYHjLwq +4wVFBFmaY8TTXoMXNggKEdU6PuE8EovocVKMpw3SIlaM27z9uxksNVL2xw== +-----END PUBLIC KEY----- diff --git a/packages/sdk/keys/sepolia/prod/kms-encryption-public-key.pem b/packages/sdk/keys/sepolia/prod/kms-encryption-public-key.pem new file mode 100644 index 0000000..d29f732 --- /dev/null +++ b/packages/sdk/keys/sepolia/prod/kms-encryption-public-key.pem @@ -0,0 +1,14 @@ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEApDvk8pAivkgtiC5li5MP +xMTJDduTeorBl18ynrooTxp2BwwgPwXfXbJaCA0qRubvc0aO2uh2VDrPM27CqMLH +o2S9YLtpLii4A1Nl7SE/MdWKWdG6v94xNGpc2YyPP7yWtHfqOkgDWp8sokl3Uq/9 +MS0pjUaI7RyS5boCTy8Qw90BxGMpucjOmqm+luw4EdPWZCrgriUR2bbGRRgAmrT1 +K4ou4IgPp799r120hwHbCWxnOvLdQdpiv2507b900xS/3yZahhnHCAn66146LU/f +BrRpQKSM0qSpktXrrc9MH/ru2VLR5cGLp89ZcZMQA9cRGglWM5XWVY3Ti2TPJ6Kd +An1d7qNkGJaSdVa3x3HkOf6c6HeTyqis5/L/6L+PFhUsTRbmKg1FtwD+3xxdyf7h +abFxryE9rv+WatHL6r6z5ztV0znJ/Fpfs5A45FWA6pfb28fA59RGpi/DQ8RxgdCH +nZRNvdz8dTgRaXSPgkfGXBcCFqb/QhFmad7XbWDthGzfhbPOxNPtiaGRQ1Dr/Pgq +n0ugdLbRQLmDOAFgaQcnr0U4y1TUlWJnvoZMETkVN7gmITtXA4F324ALT7Rd+Lgk +HikW5vG+NjAEwXfPsK0YzT+VbHd7o1lbru9UxiDlN03XVEkz/oRQi47CvSTo3FSr +5dB4lz8kov3UUcNJfQFZolMCAwEAAQ== +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/packages/sdk/keys/sepolia/prod/kms-signing-public-key.pem b/packages/sdk/keys/sepolia/prod/kms-signing-public-key.pem new file mode 100644 index 0000000..7594746 --- /dev/null +++ b/packages/sdk/keys/sepolia/prod/kms-signing-public-key.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEsk6ZdmmvBqFfKHs+1cYjIemRGN7h +1NatIEitFRyx+3q8wmTJ9LknTE1FwWBLcCNTseJDti8Rh+SaVxfGOyJuuA== +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 78873d9..6594cc4 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -13,20 +13,24 @@ } }, "scripts": { - "build": "tsup", + "build": "tsup && npm run build:copy-templates", + "build:copy-templates": "node scripts/copy-templates.js", "lint": "eslint .", "format": "prettier --check .", "format:fix": "prettier --write ." }, "dependencies": { + "@inquirer/prompts": "^7.10.1", "dockerode": "^4.0.9", "handlebars": "^4.7.8", + "js-yaml": "^4.1.1", "node-forge": "^1.3.1", "undici": "^7.16.0", "viem": "^2.38.6" }, "devDependencies": { "@types/dockerode": "^3.3.45", + "@types/js-yaml": "^4.0.9", "@types/node-forge": "^1.3.14" } } diff --git a/packages/sdk/scripts/copy-templates.js b/packages/sdk/scripts/copy-templates.js new file mode 100644 index 0000000..613a8ba --- /dev/null +++ b/packages/sdk/scripts/copy-templates.js @@ -0,0 +1,68 @@ +#!/usr/bin/env node + +import * as fs from "fs"; +import * as path from "path"; +import { fileURLToPath } from "url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const sdkRoot = path.resolve(__dirname, ".."); + +// Template files to copy from SDK src to SDK dist +const templates = [ + "src/client/common/templates/Dockerfile.layered.tmpl", + "src/client/common/templates/compute-source-env.sh.tmpl", +]; + +// Create templates directory in SDK dist +const distTemplatesDir = path.join(sdkRoot, "dist", "templates"); +if (!fs.existsSync(distTemplatesDir)) { + fs.mkdirSync(distTemplatesDir, { recursive: true }); +} + +// Copy each template file +for (const template of templates) { + const srcPath = path.join(sdkRoot, template); + const filename = path.basename(template); + const destPath = path.join(distTemplatesDir, filename); + + if (!fs.existsSync(srcPath)) { + console.warn(`Warning: Template file not found: ${srcPath}`); + continue; + } + + fs.copyFileSync(srcPath, destPath); + console.log(`Copied ${filename} to dist/templates/`); +} + +console.log("Template files copied successfully"); + +// Copy keys directory structure +const keysSrcDir = path.join(sdkRoot, "keys"); +const keysDistDir = path.join(sdkRoot, "dist", "keys"); + +if (fs.existsSync(keysSrcDir)) { + // Copy entire keys directory structure recursively + function copyDirRecursive(src, dest) { + if (!fs.existsSync(dest)) { + fs.mkdirSync(dest, { recursive: true }); + } + + const entries = fs.readdirSync(src, { withFileTypes: true }); + for (const entry of entries) { + const srcPath = path.join(src, entry.name); + const destPath = path.join(dest, entry.name); + + if (entry.isDirectory()) { + copyDirRecursive(srcPath, destPath); + } else { + fs.copyFileSync(srcPath, destPath); + } + } + } + + copyDirRecursive(keysSrcDir, keysDistDir); + + console.log("Keys directory copied successfully"); +} else { + console.warn("Warning: Keys directory not found at", keysSrcDir); +} diff --git a/packages/sdk/src/client/common/abis/AppController.json b/packages/sdk/src/client/common/abis/AppController.json new file mode 100644 index 0000000..0138840 --- /dev/null +++ b/packages/sdk/src/client/common/abis/AppController.json @@ -0,0 +1,1046 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "_version", + "type": "string", + "internalType": "string" + }, + { + "name": "_permissionController", + "type": "address", + "internalType": "contractIPermissionController" + }, + { + "name": "_releaseManager", + "type": "address", + "internalType": "contractIReleaseManager" + }, + { + "name": "_computeAVSRegistrar", + "type": "address", + "internalType": "contractIComputeAVSRegistrar" + }, + { + "name": "_computeOperator", + "type": "address", + "internalType": "contractIComputeOperator" + }, + { + "name": "_appBeacon", + "type": "address", + "internalType": "contractIBeacon" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "API_PERMISSION_TYPEHASH", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "appBeacon", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contractIBeacon" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "calculateApiPermissionDigestHash", + "inputs": [ + { + "name": "permission", + "type": "bytes4", + "internalType": "bytes4" + }, + { + "name": "expiry", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "calculateAppId", + "inputs": [ + { + "name": "deployer", + "type": "address", + "internalType": "address" + }, + { + "name": "salt", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contractIApp" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "computeAVSRegistrar", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contractIComputeAVSRegistrar" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "computeOperator", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contractIComputeOperator" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "createApp", + "inputs": [ + { + "name": "salt", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "release", + "type": "tuple", + "internalType": "structIAppController.Release", + "components": [ + { + "name": "rmsRelease", + "type": "tuple", + "internalType": "structIReleaseManagerTypes.Release", + "components": [ + { + "name": "artifacts", + "type": "tuple[]", + "internalType": "structIReleaseManagerTypes.Artifact[]", + "components": [ + { + "name": "digest", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "registry", + "type": "string", + "internalType": "string" + } + ] + }, + { + "name": "upgradeByTime", + "type": "uint32", + "internalType": "uint32" + } + ] + }, + { + "name": "publicEnv", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "encryptedEnv", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "outputs": [ + { + "name": "app", + "type": "address", + "internalType": "contractIApp" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "domainSeparator", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getActiveAppCount", + "inputs": [ + { + "name": "user", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint32", + "internalType": "uint32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getAppCreator", + "inputs": [ + { + "name": "app", + "type": "address", + "internalType": "contractIApp" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getAppLatestReleaseBlockNumber", + "inputs": [ + { + "name": "app", + "type": "address", + "internalType": "contractIApp" + } + ], + "outputs": [ + { + "name": "", + "type": "uint32", + "internalType": "uint32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getAppOperatorSetId", + "inputs": [ + { + "name": "app", + "type": "address", + "internalType": "contractIApp" + } + ], + "outputs": [ + { + "name": "", + "type": "uint32", + "internalType": "uint32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getAppStatus", + "inputs": [ + { + "name": "app", + "type": "address", + "internalType": "contractIApp" + } + ], + "outputs": [ + { + "name": "", + "type": "uint8", + "internalType": "enumIAppController.AppStatus" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getApps", + "inputs": [ + { + "name": "offset", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "limit", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "apps", + "type": "address[]", + "internalType": "contractIApp[]" + }, + { + "name": "appConfigsMem", + "type": "tuple[]", + "internalType": "structIAppController.AppConfig[]", + "components": [ + { + "name": "creator", + "type": "address", + "internalType": "address" + }, + { + "name": "operatorSetId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "latestReleaseBlockNumber", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "status", + "type": "uint8", + "internalType": "enumIAppController.AppStatus" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getAppsByCreator", + "inputs": [ + { + "name": "creator", + "type": "address", + "internalType": "address" + }, + { + "name": "offset", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "limit", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "apps", + "type": "address[]", + "internalType": "contractIApp[]" + }, + { + "name": "appConfigsMem", + "type": "tuple[]", + "internalType": "structIAppController.AppConfig[]", + "components": [ + { + "name": "creator", + "type": "address", + "internalType": "address" + }, + { + "name": "operatorSetId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "latestReleaseBlockNumber", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "status", + "type": "uint8", + "internalType": "enumIAppController.AppStatus" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getAppsByDeveloper", + "inputs": [ + { + "name": "developer", + "type": "address", + "internalType": "address" + }, + { + "name": "offset", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "limit", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "apps", + "type": "address[]", + "internalType": "contractIApp[]" + }, + { + "name": "appConfigsMem", + "type": "tuple[]", + "internalType": "structIAppController.AppConfig[]", + "components": [ + { + "name": "creator", + "type": "address", + "internalType": "address" + }, + { + "name": "operatorSetId", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "latestReleaseBlockNumber", + "type": "uint32", + "internalType": "uint32" + }, + { + "name": "status", + "type": "uint8", + "internalType": "enumIAppController.AppStatus" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getMaxActiveAppsPerUser", + "inputs": [ + { + "name": "user", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "uint32", + "internalType": "uint32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "globalActiveAppCount", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint32", + "internalType": "uint32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "initialize", + "inputs": [ + { + "name": "admin", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "maxGlobalActiveApps", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint32", + "internalType": "uint32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "permissionController", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contractIPermissionController" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "releaseManager", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contractIReleaseManager" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "setMaxActiveAppsPerUser", + "inputs": [ + { + "name": "user", + "type": "address", + "internalType": "address" + }, + { + "name": "limit", + "type": "uint32", + "internalType": "uint32" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setMaxGlobalActiveApps", + "inputs": [ + { + "name": "limit", + "type": "uint32", + "internalType": "uint32" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "startApp", + "inputs": [ + { + "name": "app", + "type": "address", + "internalType": "contractIApp" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "stopApp", + "inputs": [ + { + "name": "app", + "type": "address", + "internalType": "contractIApp" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "suspend", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + }, + { + "name": "apps", + "type": "address[]", + "internalType": "contractIApp[]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "terminateApp", + "inputs": [ + { + "name": "app", + "type": "address", + "internalType": "contractIApp" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "terminateAppByAdmin", + "inputs": [ + { + "name": "app", + "type": "address", + "internalType": "contractIApp" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "updateAppMetadataURI", + "inputs": [ + { + "name": "app", + "type": "address", + "internalType": "contractIApp" + }, + { + "name": "metadataURI", + "type": "string", + "internalType": "string" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "upgradeApp", + "inputs": [ + { + "name": "app", + "type": "address", + "internalType": "contractIApp" + }, + { + "name": "release", + "type": "tuple", + "internalType": "structIAppController.Release", + "components": [ + { + "name": "rmsRelease", + "type": "tuple", + "internalType": "structIReleaseManagerTypes.Release", + "components": [ + { + "name": "artifacts", + "type": "tuple[]", + "internalType": "structIReleaseManagerTypes.Artifact[]", + "components": [ + { + "name": "digest", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "registry", + "type": "string", + "internalType": "string" + } + ] + }, + { + "name": "upgradeByTime", + "type": "uint32", + "internalType": "uint32" + } + ] + }, + { + "name": "publicEnv", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "encryptedEnv", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "version", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "event", + "name": "AppCreated", + "inputs": [ + { + "name": "creator", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "app", + "type": "address", + "indexed": true, + "internalType": "contractIApp" + }, + { + "name": "operatorSetId", + "type": "uint32", + "indexed": false, + "internalType": "uint32" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "AppMetadataURIUpdated", + "inputs": [ + { + "name": "app", + "type": "address", + "indexed": true, + "internalType": "contractIApp" + }, + { + "name": "metadataURI", + "type": "string", + "indexed": false, + "internalType": "string" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "AppStarted", + "inputs": [ + { + "name": "app", + "type": "address", + "indexed": true, + "internalType": "contractIApp" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "AppStopped", + "inputs": [ + { + "name": "app", + "type": "address", + "indexed": true, + "internalType": "contractIApp" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "AppSuspended", + "inputs": [ + { + "name": "app", + "type": "address", + "indexed": true, + "internalType": "contractIApp" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "AppTerminated", + "inputs": [ + { + "name": "app", + "type": "address", + "indexed": true, + "internalType": "contractIApp" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "AppTerminatedByAdmin", + "inputs": [ + { + "name": "app", + "type": "address", + "indexed": true, + "internalType": "contractIApp" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "AppUpgraded", + "inputs": [ + { + "name": "app", + "type": "address", + "indexed": true, + "internalType": "contractIApp" + }, + { + "name": "rmsReleaseId", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "release", + "type": "tuple", + "indexed": false, + "internalType": "structIAppController.Release", + "components": [ + { + "name": "rmsRelease", + "type": "tuple", + "internalType": "structIReleaseManagerTypes.Release", + "components": [ + { + "name": "artifacts", + "type": "tuple[]", + "internalType": "structIReleaseManagerTypes.Artifact[]", + "components": [ + { + "name": "digest", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "registry", + "type": "string", + "internalType": "string" + } + ] + }, + { + "name": "upgradeByTime", + "type": "uint32", + "internalType": "uint32" + } + ] + }, + { + "name": "publicEnv", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "encryptedEnv", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "GlobalMaxActiveAppsSet", + "inputs": [ + { + "name": "limit", + "type": "uint32", + "indexed": false, + "internalType": "uint32" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "Initialized", + "inputs": [ + { + "name": "version", + "type": "uint8", + "indexed": false, + "internalType": "uint8" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "MaxActiveAppsSet", + "inputs": [ + { + "name": "user", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "limit", + "type": "uint32", + "indexed": false, + "internalType": "uint32" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "AccountHasActiveApps", + "inputs": [] + }, + { + "type": "error", + "name": "AppAlreadyExists", + "inputs": [] + }, + { + "type": "error", + "name": "AppDoesNotExist", + "inputs": [] + }, + { + "type": "error", + "name": "GlobalMaxActiveAppsExceeded", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidAppStatus", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidPermissions", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidReleaseMetadataURI", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidShortString", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidSignature", + "inputs": [] + }, + { + "type": "error", + "name": "MaxActiveAppsExceeded", + "inputs": [] + }, + { + "type": "error", + "name": "MoreThanOneArtifact", + "inputs": [] + }, + { + "type": "error", + "name": "SignatureExpired", + "inputs": [] + }, + { + "type": "error", + "name": "StringTooLong", + "inputs": [ + { + "name": "str", + "type": "string", + "internalType": "string" + } + ] + } +] diff --git a/packages/sdk/src/client/common/abis/ERC7702Delegator.json b/packages/sdk/src/client/common/abis/ERC7702Delegator.json new file mode 100644 index 0000000..fad630c --- /dev/null +++ b/packages/sdk/src/client/common/abis/ERC7702Delegator.json @@ -0,0 +1,1027 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "_delegationManager", + "type": "address", + "internalType": "contractIDelegationManager" + }, + { + "name": "_entryPoint", + "type": "address", + "internalType": "contractIEntryPoint" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "receive", + "stateMutability": "payable" + }, + { + "type": "function", + "name": "DOMAIN_VERSION", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "NAME", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "PACKED_USER_OP_TYPEHASH", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "VERSION", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "addDeposit", + "inputs": [], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "delegationManager", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contractIDelegationManager" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "disableDelegation", + "inputs": [ + { + "name": "_delegation", + "type": "tuple", + "internalType": "structDelegation", + "components": [ + { + "name": "delegate", + "type": "address", + "internalType": "address" + }, + { + "name": "delegator", + "type": "address", + "internalType": "address" + }, + { + "name": "authority", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "caveats", + "type": "tuple[]", + "internalType": "structCaveat[]", + "components": [ + { + "name": "enforcer", + "type": "address", + "internalType": "address" + }, + { + "name": "terms", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "args", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "name": "salt", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "signature", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "eip712Domain", + "inputs": [], + "outputs": [ + { + "name": "fields", + "type": "bytes1", + "internalType": "bytes1" + }, + { + "name": "name", + "type": "string", + "internalType": "string" + }, + { + "name": "version", + "type": "string", + "internalType": "string" + }, + { + "name": "chainId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "verifyingContract", + "type": "address", + "internalType": "address" + }, + { + "name": "salt", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "extensions", + "type": "uint256[]", + "internalType": "uint256[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "enableDelegation", + "inputs": [ + { + "name": "_delegation", + "type": "tuple", + "internalType": "structDelegation", + "components": [ + { + "name": "delegate", + "type": "address", + "internalType": "address" + }, + { + "name": "delegator", + "type": "address", + "internalType": "address" + }, + { + "name": "authority", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "caveats", + "type": "tuple[]", + "internalType": "structCaveat[]", + "components": [ + { + "name": "enforcer", + "type": "address", + "internalType": "address" + }, + { + "name": "terms", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "args", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "name": "salt", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "signature", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "entryPoint", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contractIEntryPoint" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "execute", + "inputs": [ + { + "name": "_execution", + "type": "tuple", + "internalType": "structExecution", + "components": [ + { + "name": "target", + "type": "address", + "internalType": "address" + }, + { + "name": "value", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "callData", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "execute", + "inputs": [ + { + "name": "_mode", + "type": "bytes32", + "internalType": "ModeCode" + }, + { + "name": "_executionCalldata", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "executeFromExecutor", + "inputs": [ + { + "name": "_mode", + "type": "bytes32", + "internalType": "ModeCode" + }, + { + "name": "_executionCalldata", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "returnData_", + "type": "bytes[]", + "internalType": "bytes[]" + } + ], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "getDeposit", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getDomainHash", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNonce", + "inputs": [ + { + "name": "_key", + "type": "uint192", + "internalType": "uint192" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getNonce", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getPackedUserOperationHash", + "inputs": [ + { + "name": "_userOp", + "type": "tuple", + "internalType": "structPackedUserOperation", + "components": [ + { + "name": "sender", + "type": "address", + "internalType": "address" + }, + { + "name": "nonce", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "initCode", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "callData", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "accountGasLimits", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "preVerificationGas", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "gasFees", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "paymasterAndData", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "signature", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getPackedUserOperationTypedDataHash", + "inputs": [ + { + "name": "_userOp", + "type": "tuple", + "internalType": "structPackedUserOperation", + "components": [ + { + "name": "sender", + "type": "address", + "internalType": "address" + }, + { + "name": "nonce", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "initCode", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "callData", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "accountGasLimits", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "preVerificationGas", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "gasFees", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "paymasterAndData", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "signature", + "type": "bytes", + "internalType": "bytes" + } + ] + } + ], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isDelegationDisabled", + "inputs": [ + { + "name": "_delegationHash", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isValidSignature", + "inputs": [ + { + "name": "_hash", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "_signature", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "magicValue_", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "onERC1155BatchReceived", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "onERC1155Received", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "onERC721Received", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "redeemDelegations", + "inputs": [ + { + "name": "_permissionContexts", + "type": "bytes[]", + "internalType": "bytes[]" + }, + { + "name": "_modes", + "type": "bytes32[]", + "internalType": "ModeCode[]" + }, + { + "name": "_executionCallDatas", + "type": "bytes[]", + "internalType": "bytes[]" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "supportsExecutionMode", + "inputs": [ + { + "name": "_mode", + "type": "bytes32", + "internalType": "ModeCode" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "supportsInterface", + "inputs": [ + { + "name": "_interfaceId", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "validateUserOp", + "inputs": [ + { + "name": "_userOp", + "type": "tuple", + "internalType": "structPackedUserOperation", + "components": [ + { + "name": "sender", + "type": "address", + "internalType": "address" + }, + { + "name": "nonce", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "initCode", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "callData", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "accountGasLimits", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "preVerificationGas", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "gasFees", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "paymasterAndData", + "type": "bytes", + "internalType": "bytes" + }, + { + "name": "signature", + "type": "bytes", + "internalType": "bytes" + } + ] + }, + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + }, + { + "name": "_missingAccountFunds", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "validationData_", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "withdrawDeposit", + "inputs": [ + { + "name": "_withdrawAddress", + "type": "address", + "internalType": "addresspayable" + }, + { + "name": "_withdrawAmount", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "EIP712DomainChanged", + "inputs": [], + "anonymous": false + }, + { + "type": "event", + "name": "SentPrefund", + "inputs": [ + { + "name": "sender", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "amount", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "success", + "type": "bool", + "indexed": false, + "internalType": "bool" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "SetDelegationManager", + "inputs": [ + { + "name": "newDelegationManager", + "type": "address", + "indexed": true, + "internalType": "contractIDelegationManager" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "SetEntryPoint", + "inputs": [ + { + "name": "entryPoint", + "type": "address", + "indexed": true, + "internalType": "contractIEntryPoint" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "TryExecuteUnsuccessful", + "inputs": [ + { + "name": "batchExecutionindex", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "result", + "type": "bytes", + "indexed": false, + "internalType": "bytes" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "ECDSAInvalidSignature", + "inputs": [] + }, + { + "type": "error", + "name": "ECDSAInvalidSignatureLength", + "inputs": [ + { + "name": "length", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "type": "error", + "name": "ECDSAInvalidSignatureS", + "inputs": [ + { + "name": "s", + "type": "bytes32", + "internalType": "bytes32" + } + ] + }, + { + "type": "error", + "name": "ExecutionFailed", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidEIP712NameLength", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidEIP712VersionLength", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidShortString", + "inputs": [] + }, + { + "type": "error", + "name": "NotDelegationManager", + "inputs": [] + }, + { + "type": "error", + "name": "NotEntryPoint", + "inputs": [] + }, + { + "type": "error", + "name": "NotEntryPointOrSelf", + "inputs": [] + }, + { + "type": "error", + "name": "NotSelf", + "inputs": [] + }, + { + "type": "error", + "name": "StringTooLong", + "inputs": [ + { + "name": "str", + "type": "string", + "internalType": "string" + } + ] + }, + { + "type": "error", + "name": "UnauthorizedCallContext", + "inputs": [] + }, + { + "type": "error", + "name": "UnsupportedCallType", + "inputs": [ + { + "name": "callType", + "type": "bytes1", + "internalType": "CallType" + } + ] + }, + { + "type": "error", + "name": "UnsupportedExecType", + "inputs": [ + { + "name": "execType", + "type": "bytes1", + "internalType": "ExecType" + } + ] + } +] diff --git a/packages/sdk/src/client/common/abis/PermissionController.json b/packages/sdk/src/client/common/abis/PermissionController.json new file mode 100644 index 0000000..7fde463 --- /dev/null +++ b/packages/sdk/src/client/common/abis/PermissionController.json @@ -0,0 +1,494 @@ +[ + { + "type": "function", + "name": "acceptAdmin", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "addPendingAdmin", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + }, + { + "name": "admin", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "canCall", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + }, + { + "name": "caller", + "type": "address", + "internalType": "address" + }, + { + "name": "target", + "type": "address", + "internalType": "address" + }, + { + "name": "selector", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "getAdmins", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "address[]", + "internalType": "address[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getAppointeePermissions", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + }, + { + "name": "appointee", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "", + "type": "bytes4[]", + "internalType": "bytes4[]" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "getAppointees", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + }, + { + "name": "target", + "type": "address", + "internalType": "address" + }, + { + "name": "selector", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "outputs": [ + { + "name": "", + "type": "address[]", + "internalType": "address[]" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "getPendingAdmins", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "address[]", + "internalType": "address[]" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isAdmin", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + }, + { + "name": "caller", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "isPendingAdmin", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + }, + { + "name": "pendingAdmin", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "removeAdmin", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + }, + { + "name": "admin", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "removeAppointee", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + }, + { + "name": "appointee", + "type": "address", + "internalType": "address" + }, + { + "name": "target", + "type": "address", + "internalType": "address" + }, + { + "name": "selector", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "removePendingAdmin", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + }, + { + "name": "admin", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "setAppointee", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + }, + { + "name": "appointee", + "type": "address", + "internalType": "address" + }, + { + "name": "target", + "type": "address", + "internalType": "address" + }, + { + "name": "selector", + "type": "bytes4", + "internalType": "bytes4" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "version", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "event", + "name": "AdminRemoved", + "inputs": [ + { + "name": "account", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "admin", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "AdminSet", + "inputs": [ + { + "name": "account", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "admin", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "AppointeeRemoved", + "inputs": [ + { + "name": "account", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "appointee", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "target", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "selector", + "type": "bytes4", + "indexed": false, + "internalType": "bytes4" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "AppointeeSet", + "inputs": [ + { + "name": "account", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "appointee", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "target", + "type": "address", + "indexed": false, + "internalType": "address" + }, + { + "name": "selector", + "type": "bytes4", + "indexed": false, + "internalType": "bytes4" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "PendingAdminAdded", + "inputs": [ + { + "name": "account", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "admin", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "PendingAdminRemoved", + "inputs": [ + { + "name": "account", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "admin", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "AdminAlreadyPending", + "inputs": [] + }, + { + "type": "error", + "name": "AdminAlreadySet", + "inputs": [] + }, + { + "type": "error", + "name": "AdminNotPending", + "inputs": [] + }, + { + "type": "error", + "name": "AdminNotSet", + "inputs": [] + }, + { + "type": "error", + "name": "AppointeeAlreadySet", + "inputs": [] + }, + { + "type": "error", + "name": "AppointeeNotSet", + "inputs": [] + }, + { + "type": "error", + "name": "CannotHaveZeroAdmins", + "inputs": [] + }, + { + "type": "error", + "name": "NotAdmin", + "inputs": [] + } +] diff --git a/packages/sdk/src/client/common/config/environment.ts b/packages/sdk/src/client/common/config/environment.ts new file mode 100644 index 0000000..9b19213 --- /dev/null +++ b/packages/sdk/src/client/common/config/environment.ts @@ -0,0 +1,95 @@ +/** + * Environment configuration for different networks + */ + +import { EnvironmentConfig } from "../types"; + +// Chain IDs +export const SEPOLIA_CHAIN_ID = 11155111; +export const MAINNET_CHAIN_ID = 1; + +// Common addresses across all chains +export const CommonAddresses: Record = { + ERC7702Delegator: "0x63c0c19a282a1b52b07dd5a65b58948a07dae32b", +}; + +// Addresses specific to each chain +export const ChainAddresses: Record> = { + [MAINNET_CHAIN_ID]: { + PermissionController: "0x25E5F8B1E7aDf44518d35D5B2271f114e081f0E5", + }, + [SEPOLIA_CHAIN_ID]: { + PermissionController: "0x44632dfBdCb6D3E21EF613B0ca8A6A0c618F5a37", + }, +}; + +// Environment configurations +const ENVIRONMENTS: Record> = { + sepolia: { + name: "sepolia", + appControllerAddress: "0x0dd810a6ffba6a9820a10d97b659f07d8d23d4E2", + permissionControllerAddress: + ChainAddresses[SEPOLIA_CHAIN_ID].PermissionController, + erc7702DelegatorAddress: CommonAddresses.ERC7702Delegator, + kmsServerURL: "http://10.128.15.203:8080", + userApiServerURL: "https://userapi-compute-sepolia-prod.eigencloud.xyz", + defaultRPCURL: "https://ethereum-sepolia-rpc.publicnode.com", + }, + "mainnet-alpha": { + name: "mainnet-alpha", + appControllerAddress: "0xc38d35Fc995e75342A21CBd6D770305b142Fbe67", + permissionControllerAddress: + ChainAddresses[MAINNET_CHAIN_ID].PermissionController, + erc7702DelegatorAddress: CommonAddresses.ERC7702Delegator, + kmsServerURL: "http://10.128.0.2:8080", + userApiServerURL: "https://userapi-compute.eigencloud.xyz", + defaultRPCURL: "https://ethereum-rpc.publicnode.com", + }, +}; + +const CHAIN_ID_TO_ENVIRONMENT: Record = { + [SEPOLIA_CHAIN_ID.toString()]: "sepolia", + [MAINNET_CHAIN_ID.toString()]: "mainnet-alpha", +}; + +/** + * Get environment configuration + */ +export function getEnvironmentConfig( + environment: string, + chainID?: bigint, +): EnvironmentConfig { + const env = ENVIRONMENTS[environment]; + if (!env) { + throw new Error(`Unknown environment: ${environment}`); + } + + // If chainID provided, validate it matches + if (chainID) { + const expectedEnv = CHAIN_ID_TO_ENVIRONMENT[chainID.toString()]; + if (expectedEnv && expectedEnv !== environment) { + throw new Error( + `Environment ${environment} does not match chain ID ${chainID}`, + ); + } + } + + // Determine chain ID from environment if not provided + const resolvedChainID = + chainID || + (environment === "sepolia" ? SEPOLIA_CHAIN_ID : MAINNET_CHAIN_ID); + + return { + ...env, + chainID: BigInt(resolvedChainID), + }; +} + +/** + * Detect environment from chain ID + */ +export function detectEnvironmentFromChainID( + chainID: bigint, +): string | undefined { + return CHAIN_ID_TO_ENVIRONMENT[chainID.toString()]; +} diff --git a/packages/sdk/src/client/modules/app/deploy/env/parser.ts b/packages/sdk/src/client/common/env/parser.ts similarity index 66% rename from packages/sdk/src/client/modules/app/deploy/env/parser.ts rename to packages/sdk/src/client/common/env/parser.ts index 8e03e5d..1ec969d 100644 --- a/packages/sdk/src/client/modules/app/deploy/env/parser.ts +++ b/packages/sdk/src/client/common/env/parser.ts @@ -2,34 +2,34 @@ * Environment file parsing and validation */ -import * as fs from 'fs'; -import { ParsedEnvironment } from '../types'; +import * as fs from "fs"; +import { ParsedEnvironment } from "../types"; -const MNEMONIC_ENV_VAR = 'MNEMONIC'; +const MNEMONIC_ENV_VAR = "MNEMONIC"; /** * Parse environment file and split into public/private variables */ export function parseAndValidateEnvFile( - envFilePath: string + envFilePath: string, ): ParsedEnvironment { if (!fs.existsSync(envFilePath)) { throw new Error(`Environment file not found: ${envFilePath}`); } - const content = fs.readFileSync(envFilePath, 'utf-8'); + const content = fs.readFileSync(envFilePath, "utf-8"); const env: Record = {}; let mnemonicFiltered = false; // Parse .env file (simple parser - can be enhanced) - const lines = content.split('\n'); + const lines = content.split("\n"); for (const line of lines) { const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith('#')) { + if (!trimmed || trimmed.startsWith("#")) { continue; } - const equalIndex = trimmed.indexOf('='); + const equalIndex = trimmed.indexOf("="); if (equalIndex === -1) { continue; } @@ -59,7 +59,7 @@ export function parseAndValidateEnvFile( const privateEnv: Record = {}; for (const [key, value] of Object.entries(env)) { - if (key.endsWith('_PUBLIC')) { + if (key.endsWith("_PUBLIC")) { publicEnv[key] = value; } else { privateEnv[key] = value; @@ -78,40 +78,45 @@ export function parseAndValidateEnvFile( * Display environment variables for user confirmation */ export function displayEnvironmentVariables( - parsed: ParsedEnvironment & { _mnemonicFiltered?: boolean } + parsed: ParsedEnvironment & { _mnemonicFiltered?: boolean }, ): void { - console.log('\nYour container will deploy with the following environment variables:\n'); + console.log( + "\nYour container will deploy with the following environment variables:\n", + ); if (parsed._mnemonicFiltered) { console.log( - '\x1b[3;36mMnemonic environment variable removed to be overridden by protocol provided mnemonic\x1b[0m\n' + "\x1b[3;36mMnemonic environment variable removed to be overridden by protocol provided mnemonic\x1b[0m\n", ); } // Print public variables if (Object.keys(parsed.public).length > 0) { - console.log('PUBLIC VARIABLE\tVALUE'); - console.log('---------------\t-----'); + console.log("PUBLIC VARIABLE\tVALUE"); + console.log("---------------\t-----"); for (const [key, value] of Object.entries(parsed.public)) { console.log(`${key}\t${value}`); } } else { - console.log('No public variables found'); + console.log("No public variables found"); } - console.log('\n-----------------------------------------\n'); + console.log("\n-----------------------------------------\n"); // Print private variables if (Object.keys(parsed.private).length > 0) { - console.log('PRIVATE VARIABLE\tVALUE'); - console.log('----------------\t-----\n'); + console.log("PRIVATE VARIABLE\tVALUE"); + console.log("----------------\t-----\n"); for (const [key, value] of Object.entries(parsed.private)) { // Mask private values for display - const masked = value.length > 8 ? `${value.substring(0, 4)}...${value.substring(value.length - 4)}` : '***'; + const masked = + value.length > 8 + ? `${value.substring(0, 4)}...${value.substring(value.length - 4)}` + : "***"; console.log(`${key}\t${masked}`); } } else { - console.log('No private variables found'); + console.log("No private variables found"); } console.log(); diff --git a/packages/sdk/src/client/common/templates/Dockerfile.layered.tmpl b/packages/sdk/src/client/common/templates/Dockerfile.layered.tmpl new file mode 100644 index 0000000..8ee9f8e --- /dev/null +++ b/packages/sdk/src/client/common/templates/Dockerfile.layered.tmpl @@ -0,0 +1,59 @@ +{{#if includeTLS}} +# Get Caddy from official image +FROM caddy:2.10.2-alpine AS caddy +{{/if}} + +FROM {{baseImage}} + +{{#if originalUser}} +# Switch to root to perform setup (base image has non-root USER: {{originalUser}}) +USER root +{{/if}} + +# Copy core TEE components +COPY compute-source-env.sh /usr/local/bin/ +COPY kms-client /usr/local/bin/ +COPY kms-encryption-public-key.pem /usr/local/bin/ +COPY kms-signing-public-key.pem /usr/local/bin/ + +{{#if includeTLS}} +# Copy Caddy from official image +COPY --from=caddy /usr/bin/caddy /usr/local/bin/caddy + +# Copy TLS components +COPY tls-keygen /usr/local/bin/ +COPY Caddyfile /etc/caddy/ +{{/if}} + +{{#if originalUser}} +# Make binaries executable (755 for executables, 644 for keys) +RUN chmod 755 /usr/local/bin/compute-source-env.sh \ + && chmod 755 /usr/local/bin/kms-client{{#if includeTLS}} \ + && chmod 755 /usr/local/bin/tls-keygen \ + && chmod 755 /usr/local/bin/caddy{{/if}} \ + && chmod 644 /usr/local/bin/kms-encryption-public-key.pem \ + && chmod 644 /usr/local/bin/kms-signing-public-key.pem + +# Switch back to the original user from base image +USER {{originalUser}} +{{else}} +# Make binaries executable (preserve existing permissions, just add execute) +RUN chmod +x /usr/local/bin/compute-source-env.sh \ + && chmod +x /usr/local/bin/kms-client{{#if includeTLS}} \ + && chmod +x /usr/local/bin/tls-keygen{{/if}} +{{/if}} + +{{#if logRedirect}} + +LABEL tee.launch_policy.log_redirect={{logRedirect}} +{{/if}} + +LABEL eigenx_cli_version={{ecloudCLIVersion}} + +{{#if includeTLS}} +# Expose both HTTP and HTTPS ports for Caddy +EXPOSE 80 443 +{{/if}} + +ENTRYPOINT ["/usr/local/bin/compute-source-env.sh"] +CMD {{{originalCmd}}} diff --git a/packages/sdk/src/client/common/templates/compute-source-env.sh.tmpl b/packages/sdk/src/client/common/templates/compute-source-env.sh.tmpl new file mode 100644 index 0000000..dcf903b --- /dev/null +++ b/packages/sdk/src/client/common/templates/compute-source-env.sh.tmpl @@ -0,0 +1,111 @@ +#!/bin/sh +echo "compute-source-env.sh: Running setup script..." + +# Fetch and source environment variables from KMS +echo "Fetching secrets from KMS..." +if /usr/local/bin/kms-client \ + --kms-server-url "{{kmsServerURL}}" \ + --jwt-file "{{jwtFile}}" \ + --kms-encryption-key-file /usr/local/bin/kms-encryption-public-key.pem \ + --kms-signing-key-file /usr/local/bin/kms-signing-public-key.pem \ + --output /tmp/.env; then + echo "compute-source-env.sh: Successfully fetched environment variables from KMS" + set -a && . /tmp/.env && set +a + rm -f /tmp/.env +else + echo "compute-source-env.sh: ERROR - Failed to fetch environment variables from KMS" + echo "compute-source-env.sh: Exiting - cannot start user workload without KMS secrets" + exit 1 +fi + +# Setup TLS if tls-keygen is present (which means TLS was configured at build time) +setup_tls() { + # If tls-keygen isn't present, TLS wasn't configured during build + if [ ! -x /usr/local/bin/tls-keygen ]; then + echo "compute-source-env.sh: TLS not configured (no tls-keygen binary)" + return 0 + fi + + local domain="${DOMAIN:-}" + local mnemonic="${MNEMONIC:-}" + + # Since tls-keygen is present, TLS is expected - validate requirements + if [ -z "$domain" ] || [ "$domain" = "localhost" ]; then + echo "compute-source-env.sh: ERROR - TLS binary present but DOMAIN not configured or is localhost" + echo "compute-source-env.sh: Set DOMAIN environment variable to a valid domain" + exit 1 + fi + + if [ -z "$mnemonic" ]; then + echo "compute-source-env.sh: ERROR - TLS binary present but MNEMONIC not available" + echo "compute-source-env.sh: Cannot obtain TLS certificate without mnemonic" + exit 1 + fi + + if [ ! -x /usr/local/bin/caddy ]; then + echo "compute-source-env.sh: ERROR - TLS binary present but Caddy not found" + exit 1 + fi + + echo "compute-source-env.sh: Setting up TLS for domain: $domain" + + # Obtain TLS certificate using ACME + # Default to http-01, but allow override via ACME_CHALLENGE env var + local challenge="${ACME_CHALLENGE:-http-01}" + + # Check if we should use staging (for testing) + local staging_flag="" + if [ "${ACME_STAGING:-false}" = "true" ]; then + staging_flag="-staging" + echo "compute-source-env.sh: Using Let's Encrypt STAGING environment (certificates won't be trusted)" + fi + + echo "compute-source-env.sh: Obtaining TLS certificate using $challenge challenge..." + # Pass the API URL for certificate persistence + if ! MNEMONIC="$mnemonic" DOMAIN="$domain" API_URL="{{userAPIURL}}" /usr/local/bin/tls-keygen \ + -challenge "$challenge" \ + $staging_flag; then + echo "compute-source-env.sh: ERROR - Failed to obtain TLS certificate" + echo "compute-source-env.sh: Certificate issuance failed for $domain" + exit 1 + fi + + echo "compute-source-env.sh: TLS certificate obtained successfully" + + # Validate Caddyfile before starting + if ! /usr/local/bin/caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile 2>/dev/null; then + echo "compute-source-env.sh: ERROR - Invalid Caddyfile" + echo "compute-source-env.sh: TLS was requested (DOMAIN=$domain) but setup failed" + exit 1 + fi + + # Start Caddy in background + echo "compute-source-env.sh: Starting Caddy reverse proxy..." + + # Check if Caddy logs should be enabled + if [ "${ENABLE_CADDY_LOGS:-false}" = "true" ]; then + if ! /usr/local/bin/caddy start --config /etc/caddy/Caddyfile --adapter caddyfile 2>&1; then + echo "compute-source-env.sh: ERROR - Failed to start Caddy" + echo "compute-source-env.sh: TLS was requested (DOMAIN=$domain) but setup failed" + exit 1 + fi + else + # Redirect Caddy output to /dev/null to silence logs + if ! /usr/local/bin/caddy start --config /etc/caddy/Caddyfile --adapter caddyfile >/dev/null 2>&1; then + echo "compute-source-env.sh: ERROR - Failed to start Caddy" + echo "compute-source-env.sh: TLS was requested (DOMAIN=$domain) but setup failed" + exit 1 + fi + fi + + # Give Caddy a moment to fully initialize + sleep 2 + echo "compute-source-env.sh: Caddy started successfully" + return 0 +} + +# Run TLS setup +setup_tls + +echo "compute-source-env.sh: Environment sourced." +exec "$@" diff --git a/packages/sdk/src/client/modules/app/deploy/types/index.ts b/packages/sdk/src/client/common/types/index.ts similarity index 50% rename from packages/sdk/src/client/modules/app/deploy/types/index.ts rename to packages/sdk/src/client/common/types/index.ts index faba85d..f03b936 100644 --- a/packages/sdk/src/client/modules/app/deploy/types/index.ts +++ b/packages/sdk/src/client/common/types/index.ts @@ -1,28 +1,59 @@ /** - * Core types for ecloud SDK + * Core types for ECloud SDK */ +import { Address } from "viem"; + +export type AppId = Address & { readonly __brand: unique symbol }; + +export type logVisibility = "public" | "private" | "off"; + +export interface DeployAppOpts { + name?: string; + dockerfile?: string; + envFile?: string; + imageRef?: string; + instanceType?: string; + logVisibility?: logVisibility; +} + +export interface UpgradeAppOpts { + image: string; + gas?: { maxFeePerGas?: bigint; maxPriorityFeePerGas?: bigint }; +} + +export interface LifecycleOpts { + gas?: { maxFeePerGas?: bigint; maxPriorityFeePerGas?: bigint }; +} + +export interface AppRecord { + id: AppId; + owner: `0x${string}`; + image: string; + status: "starting" | "running" | "stopped" | "terminated"; + createdAt: number; // epoch ms + lastUpdatedAt: number; // epoch ms +} + export interface DeployOptions { - /** Private key for signing transactions (hex string with or without 0x prefix) */ - privateKey: string; - /** RPC URL for blockchain connection */ - rpcUrl: string; - /** Environment name (e.g., 'sepolia', 'mainnet-alpha') */ - environment: string; + /** Private key for signing transactions (hex string with or without 0x prefix) - optional, will prompt if not provided */ + privateKey?: string; + /** RPC URL for blockchain connection - optional, uses environment default if not provided */ + rpcUrl?: string; + /** Environment name (e.g., 'sepolia', 'mainnet-alpha') - optional, defaults to 'sepolia' */ + environment?: string; /** Path to Dockerfile (if building from Dockerfile) */ dockerfilePath?: string; - /** Image reference (registry/path:tag) */ - imageRef: string; - /** Path to .env file */ + /** Image reference (registry/path:tag) - optional, will prompt if not provided */ + imageRef?: string; + /** Path to .env file - optional, will use .env if exists or prompt */ envFilePath?: string; - /** App name (optional, will be prompted if not provided) */ + /** App name - optional, will prompt if not provided */ appName?: string; - /** Instance type */ - instanceType: string; - /** Log redirect setting */ - logRedirect: string; - /** Whether logs should be public */ - publicLogs: boolean; + /** Instance type - optional, will prompt if not provided */ + instanceType?: string; + /** Log visibility setting - optional, will prompt if not provided */ + logVisibility?: logVisibility; } export interface DeployResult { @@ -46,6 +77,7 @@ export interface EnvironmentConfig { erc7702DelegatorAddress: string; kmsServerURL: string; userApiServerURL: string; + defaultRPCURL: string; } export interface Release { @@ -84,4 +116,3 @@ export interface Logger { warn(message: string, ...args: any[]): void; error(message: string, ...args: any[]): void; } - diff --git a/packages/sdk/src/client/common/utils/dirname.ts b/packages/sdk/src/client/common/utils/dirname.ts new file mode 100644 index 0000000..a15f658 --- /dev/null +++ b/packages/sdk/src/client/common/utils/dirname.ts @@ -0,0 +1,29 @@ +/** + * ESM and CJS compatible getDirname utility + */ + +import * as path from "path"; +import { fileURLToPath } from "url"; + +/** + * Get __dirname equivalent that works in both ESM and CJS + * In CJS builds, __dirname is available and will be used + * In ESM builds, import.meta.url is used + */ +export function getDirname(): string { + // Check for CJS __dirname first (available in CommonJS) + if (typeof __dirname !== "undefined") { + return __dirname; + } + + // For ESM, we need to use import.meta.url + // This will be evaluated at build time by tsup for ESM builds + // For CJS builds, the above check will catch it, so this won't execute + try { + const metaUrl = import.meta.url; + return path.dirname(fileURLToPath(metaUrl)); + } catch { + // Fallback (shouldn't reach here in normal usage) + return process.cwd(); + } +} diff --git a/packages/sdk/src/client/common/utils/index.ts b/packages/sdk/src/client/common/utils/index.ts new file mode 100644 index 0000000..6b93482 --- /dev/null +++ b/packages/sdk/src/client/common/utils/index.ts @@ -0,0 +1,7 @@ +/** + * Utility exports + */ + +export * from "./logger"; +export * from "./userapi"; +export * from "./dirname"; diff --git a/packages/sdk/src/client/common/utils/logger.ts b/packages/sdk/src/client/common/utils/logger.ts new file mode 100644 index 0000000..80cce53 --- /dev/null +++ b/packages/sdk/src/client/common/utils/logger.ts @@ -0,0 +1,21 @@ +/** + * Default logger + */ + +import { Logger } from "../types"; + +export const defaultLogger: Logger = { + info: (...args) => console.info(...args), + warn: (...args) => console.warn(...args), + error: (...args) => console.error(...args), + debug: (...args) => console.debug(...args), +}; + +export const getLogger: (verbose?: boolean) => Logger = ( + verbose?: boolean, +) => ({ + info: (...args) => console.info(...args), + warn: (...args) => console.warn(...args), + error: (...args) => console.error(...args), + debug: (...args) => verbose && console.debug(...args), +}); diff --git a/packages/sdk/src/client/common/utils/userapi.ts b/packages/sdk/src/client/common/utils/userapi.ts new file mode 100644 index 0000000..47f1759 --- /dev/null +++ b/packages/sdk/src/client/common/utils/userapi.ts @@ -0,0 +1,246 @@ +/** + * UserAPI Client to manage interactions with the coordinator + */ + +import { request, Agent as UndiciAgent } from "undici"; +import { Address, Hex } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { EnvironmentConfig } from "../types"; +// import { getKMSKeysForEnvironment } from '../../modules/app/deploy/utils/keys'; + +import { defaultLogger } from "./logger"; + +export interface AppInfo { + address: Address; + status: string; + ip: string; + machineType: string; +} + +export interface AppInfoResponse { + apps: Array<{ + addresses: { + data: { + evmAddresses: Address[]; + solanaAddresses: string[]; + }; + signature: string; + }; + app_status: string; + ip: string; + machine_type: string; + }>; +} + +const MAX_ADDRESS_COUNT = 5; + +// Permission constants (matching Go version) +export const CanViewAppLogsPermission = "0x2fd3f2fe" as Hex; +export const CanViewSensitiveAppInfoPermission = "0x0e67b22f" as Hex; + +export class UserApiClient { + private readonly account?: ReturnType; + + constructor( + private readonly config: EnvironmentConfig, + privateKey?: string | Hex, + ) { + if (privateKey) { + const privateKeyHex = + typeof privateKey === "string" + ? ((privateKey.startsWith("0x") + ? privateKey + : `0x${privateKey}`) as Hex) + : privateKey; + this.account = privateKeyToAccount(privateKeyHex); + } + } + + async getInfos( + appIDs: Address[], + addressCount = 1, + logger = defaultLogger, + ): Promise { + const count = Math.min(addressCount, MAX_ADDRESS_COUNT); + + const endpoint = `${this.config.userApiServerURL}/info`; + const url = `${endpoint}?${new URLSearchParams({ apps: appIDs.join(",") })}`; + + const res = await this.makeAuthenticatedRequest( + url, + CanViewSensitiveAppInfoPermission, + ); + const result: AppInfoResponse = await res.json(); + + // Print to debug logs + logger.debug(JSON.stringify(result, undefined, 2)); + + // optional: verify signatures with KMS key + // const { signingKey } = getKMSKeysForEnvironment(this.config.name); + + // Truncate without mutating the original object + return result.apps.map((app, i) => { + // TODO: Implement signature verification + // const valid = await verifyKMSSignature(appInfo.addresses, signingKey); + // if (!valid) { + // throw new Error(`Invalid signature for app ${appIDs[i]}`); + // } + const evm = app.addresses.data.evmAddresses.slice(0, count); + // const sol = app.addresses.data.solanaAddresses.slice(0, count); + // If the API ties each `apps[i]` to `appIDs[i]`, use i. Otherwise derive from `evm[0]` + const inferredAddress = evm[0] ?? appIDs[i] ?? appIDs[0]; + + return { + address: inferredAddress as Address, + status: app.app_status, + ip: app.ip, + machineType: app.machine_type, + }; + }); + } + + /** + * Get available SKUs (instance types) from UserAPI + */ + async getSKUs(): Promise<{ + skus: Array<{ sku: string; Description: string }>; + }> { + const endpoint = `${this.config.userApiServerURL}/skus`; + const response = await this.makeAuthenticatedRequest(endpoint); + + const result = await response.json(); + + // Transform response to match expected format + return { + skus: result.skus || result.SKUs || [], + }; + } + + private async makeAuthenticatedRequest( + url: string, + permission?: string, + ): Promise { + const headers: Record = {}; + // Add auth headers if permission is specified + if (permission && this.account) { + const expiry = BigInt(Math.floor(Date.now() / 1000) + 5 * 60); // 5 minutes + const authHeaders = await this.generateAuthHeaders(permission, expiry); + Object.assign(headers, authHeaders); + } + + try { + // Use undici directly with TLS config that skips certificate verification + // This matches the Go implementation which uses InsecureSkipVerify: true + const insecureAgent = new UndiciAgent({ + connect: { + rejectUnauthorized: false, // Skip TLS certificate verification + }, + }); + + // Use undici's request directly instead of fetch + const response = await request(url, { + method: "GET", + headers, + dispatcher: insecureAgent, + headersTimeout: 30000, // 30 second timeout (matches Go version) + }); + + // Convert undici response to fetch-like Response object + const status = response.statusCode; + const statusText = status >= 200 && status < 300 ? "OK" : "Error"; + + if (status < 200 || status >= 300) { + const body = await response.body.text(); + throw new Error( + `UserAPI request failed: ${status} ${statusText} - ${body}`, + ); + } + + // Create a Response-like object that works with our code + return { + ok: true, + status, + statusText, + json: async () => { + const text = await response.body.text(); + return JSON.parse(text); + }, + text: async () => { + return await response.body.text(); + }, + } as Response; + } catch (error: any) { + // Handle network errors (fetch failed, connection refused, etc.) + if ( + error.message?.includes("fetch failed") || + error.message?.includes("ECONNREFUSED") || + error.message?.includes("ENOTFOUND") || + error.cause + ) { + const cause = error.cause?.message || error.cause || error.message; + throw new Error( + `Failed to connect to UserAPI at ${url}: ${cause}\n` + + `Please check:\n` + + `1. Your internet connection\n` + + `2. The API server is accessible: ${this.config.userApiServerURL}\n` + + `3. Firewall/proxy settings`, + ); + } + // Re-throw other errors as-is + throw error; + } + } + + /** + * Generate authentication headers for UserAPI requests + * Signs a message containing permission and expiry timestamp + */ + private async generateAuthHeaders( + permission: string, + expiry: bigint, + ): Promise> { + if (!this.account) { + throw new Error("Private key required for authenticated requests"); + } + + // Create message to sign: permission + expiry + // Format matches what the backend expects + const message = `${permission}${expiry.toString(16).padStart(64, "0")}`; + + // Sign the message directly using the account's signMessage method + // This works for local accounts without needing a wallet client + const signature = await this.account.signMessage({ + message, + }); + + if ( + !signature || + typeof signature !== "string" || + !signature.startsWith("0x") + ) { + throw new Error(`Invalid signature format: ${signature}`); + } + + // Extract r, s, v from signature (65 bytes: 32 bytes r + 32 bytes s + 1 byte v) + const sigBytes = Buffer.from(signature.slice(2), "hex"); + if (sigBytes.length !== 65) { + throw new Error( + `Invalid signature length: expected 65 bytes, got ${sigBytes.length}`, + ); + } + + const r = `0x${sigBytes.slice(0, 32).toString("hex")}`; + const s = `0x${sigBytes.slice(32, 64).toString("hex")}`; + const v = sigBytes[64]; + + // Return auth headers (format may need adjustment based on backend expectations) + return { + "X-Auth-Address": this.account.address, + "X-Auth-Permission": permission, + "X-Auth-Expiry": expiry.toString(), + "X-Auth-Signature-R": r, + "X-Auth-Signature-S": s, + "X-Auth-Signature-V": `0x${v.toString(16).padStart(2, "0")}`, + }; + } +} diff --git a/packages/sdk/src/client/index.ts b/packages/sdk/src/client/index.ts index 0966a6b..f06caf7 100644 --- a/packages/sdk/src/client/index.ts +++ b/packages/sdk/src/client/index.ts @@ -1,26 +1,37 @@ +/** + * Main SDK Client entry point + */ + import { createPublicClient, createWalletClient, http } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { sepolia, mainnet, type Chain } from "viem/chains"; import { createAppModule, type AppModule } from "./modules/app"; import type { WalletClient, Transport, Account } from "viem"; +import { getEnvironmentConfig } from "./common/config/environment"; + +// Export all types +export * from "./common/types"; + +// special case on createApp - we don't need the client to run it +export { createApp, CreateAppOpts } from "./modules/app/create/create"; export type Environment = "sepolia" | "mainnet-alpha"; const CHAINS: Record = { sepolia, "mainnet-alpha": mainnet }; export interface CreateClientConfig { + verbose: boolean; privateKey: `0x${string}`; environment: Environment | string; rpcUrl?: string; - apiBaseUrl?: string; } export interface CoreContext { + verbose: boolean; chain: Chain; account: ReturnType; wallet: ReturnType; publicClient: ReturnType; - apiBaseUrl?: string; privateKey: `0x${string}`; rpcUrl: string; environment: string; @@ -33,9 +44,20 @@ export interface ecloudClient { export function createECloudClient(cfg: CreateClientConfig): ecloudClient { const chain = CHAINS[cfg.environment]; - const rpc = cfg.rpcUrl ?? chain.rpcUrls.default.http[0]; const account = privateKeyToAccount(cfg.privateKey); + const environmentConfig = getEnvironmentConfig(cfg.environment || "sepolia"); + + let rpc = cfg.rpcUrl; + if (!rpc) { + rpc = process.env.RPC_URL ?? environmentConfig.defaultRPCURL; + } + if (!rpc) { + throw new Error( + `RPC URL is required. Provide via options.rpcUrl, RPC_URL env var, or ensure environment has default RPC URL`, + ); + } + const wallet = createWalletClient({ account, chain, @@ -49,7 +71,7 @@ export function createECloudClient(cfg: CreateClientConfig): ecloudClient { account, wallet, publicClient, - apiBaseUrl: cfg.apiBaseUrl, + verbose: cfg.verbose, privateKey: cfg.privateKey, rpcUrl: rpc, environment: cfg.environment, diff --git a/packages/sdk/src/client/modules/app/create/create.ts b/packages/sdk/src/client/modules/app/create/create.ts new file mode 100644 index 0000000..fa57cfa --- /dev/null +++ b/packages/sdk/src/client/modules/app/create/create.ts @@ -0,0 +1,250 @@ +/** + * Create command + * + * Creates a new app project from a template + */ + +import * as fs from "fs"; +import * as path from "path"; +import { Logger } from "../../../common/types"; +import { defaultLogger } from "../../../common/utils"; +import { loadTemplateConfig, getTemplateURLs } from "./template/config"; +import { fetchTemplate, fetchTemplateSubdirectory } from "./template/git"; +import { postProcessTemplate } from "./template/postprocess"; +import { input, select } from "@inquirer/prompts"; + +export interface CreateAppOpts { + name?: string; + language?: string; + template?: string; + templateVersion?: string; + verbose?: boolean; +} + +// Language configuration +export const PRIMARY_LANGUAGES = ["typescript", "golang", "rust", "python"]; + +export const SHORT_NAMES: Record = { + ts: "typescript", + go: "golang", + rs: "rust", + py: "python", +}; + +export const LANGUAGE_FILES: Record = { + typescript: ["package.json"], + rust: ["Cargo.toml", "Dockerfile"], + golang: ["go.mod"], +}; + +/** + * Create a new app project from template + */ +export async function createApp( + options: CreateAppOpts, + logger: Logger = defaultLogger, +): Promise { + // 1. Get project name + let name = options.name; + if (!name) { + name = await promptProjectName(); + } + + // Validate project name + validateProjectName(name); + + // Check if directory exists + if (fs.existsSync(name)) { + throw new Error(`Directory ${name} already exists`); + } + + // 2. Get language - only needed for built-in templates + let language: string | undefined; + if (!options.template) { + language = options.language; + if (!language) { + language = await promptLanguage(); + } else { + // Resolve short names to full names + if (SHORT_NAMES[language]) { + language = SHORT_NAMES[language]; + } + + // Validate language is supported + if (!PRIMARY_LANGUAGES.includes(language)) { + throw new Error(`Unsupported language: ${language}`); + } + } + } + + // 3. Resolve template source + const { repoURL, ref, subPath } = await resolveTemplateSource( + options.template, + options.templateVersion, + language, + ); + + // 4. Create project directory + fs.mkdirSync(name, { mode: 0o755 }); + + try { + // 5. Check if we should use local templates (for development) + const useLocalTemplates = process.env.EIGENX_USE_LOCAL_TEMPLATES === "true"; + if (useLocalTemplates) { + await useLocalTemplate(name, language!, logger); + } else { + // 6. Fetch template from Git + if (subPath) { + // Fetch only the subdirectory + await fetchTemplateSubdirectory(repoURL, ref, subPath, name, logger); + } else { + // Fetch the full repository + await fetchTemplate( + repoURL, + ref, + name, + { verbose: options.verbose || false }, + logger, + ); + } + } + + // 7. Post-process only internal templates + if (subPath && language) { + await postProcessTemplate(name, language, logger); + } + + logger.info(`Successfully created ${language || "project"}: ${name}`); + } catch (error: any) { + // Cleanup on failure + fs.rmSync(name, { recursive: true, force: true }); + throw error; + } +} + +/** + * Validate project name + */ +function validateProjectName(name: string): void { + if (!name) { + throw new Error("Project name cannot be empty"); + } + if (name.includes(" ")) { + throw new Error("Project name cannot contain spaces"); + } +} + +/** + * Resolve template source (URL, ref, subdirectory path) + */ +async function resolveTemplateSource( + templateFlag?: string, + templateVersionFlag?: string, + language?: string, +): Promise<{ repoURL: string; ref: string; subPath: string }> { + if (templateFlag) { + // Custom template URL provided + const ref = templateVersionFlag || "main"; + return { repoURL: templateFlag, ref, subPath: "" }; + } + + // Use template configuration system for defaults + const config = await loadTemplateConfig(); + if (!language) { + throw new Error("Language is required for default templates"); + } + + // Get template URL and version from config for "tee" framework + const { templateURL, version } = getTemplateURLs(config, "tee", language); + + // Override version if templateVersionFlag provided + const ref = templateVersionFlag || version; + + // For templates from config, assume they follow our subdirectory structure + const subPath = `templates/minimal/${language}`; + + return { repoURL: templateURL, ref, subPath }; +} + +/** + * Use local template (for development) + */ +async function useLocalTemplate( + projectDir: string, + language: string, + logger: Logger, +): Promise { + // First try EIGENX_TEMPLATES_PATH env var, then look for the eigenx-templates directory as a sibling directory + let eigenxTemplatesPath = process.env.EIGENX_TEMPLATES_PATH; + + if (!eigenxTemplatesPath) { + // Look for eigenx-templates as a sibling directory + const possiblePaths = ["eigenx-templates", "../eigenx-templates"]; + for (const possiblePath of possiblePaths) { + const testPath = path.join(possiblePath, "templates/minimal"); + if (fs.existsSync(testPath)) { + eigenxTemplatesPath = possiblePath; + break; + } + } + + if (!eigenxTemplatesPath) { + throw new Error( + "Cannot find eigenx-templates directory. Set EIGENX_TEMPLATES_PATH or ensure eigenx-templates is a sibling directory", + ); + } + } + + // Use local templates from the eigenx-templates repository + const localTemplatePath = path.join( + eigenxTemplatesPath, + "templates/minimal", + language, + ); + + if (!fs.existsSync(localTemplatePath)) { + throw new Error(`Local template not found at ${localTemplatePath}`); + } + + // Copy local template to project directory + await copyDirectory(localTemplatePath, projectDir); + logger.info(`Using local template from ${localTemplatePath}`); +} + +/** + * Copy directory recursively + */ +async function copyDirectory(src: string, dst: string): Promise { + const entries = fs.readdirSync(src, { withFileTypes: true }); + + for (const entry of entries) { + const srcPath = path.join(src, entry.name); + const dstPath = path.join(dst, entry.name); + + if (entry.isDirectory()) { + fs.mkdirSync(dstPath, { recursive: true }); + await copyDirectory(srcPath, dstPath); + } else { + const stat = fs.statSync(srcPath); + fs.copyFileSync(srcPath, dstPath); + fs.chmodSync(dstPath, stat.mode); + } + } +} + +/** + * Prompt for project name + */ +async function promptProjectName(): Promise { + return input({ message: "Enter project name:" }); +} + +/** + * Prompt for language selection + */ +async function promptLanguage(): Promise { + return select({ + message: "Select a language", + choices: PRIMARY_LANGUAGES, + }); +} diff --git a/packages/sdk/src/client/modules/app/create/template/config.ts b/packages/sdk/src/client/modules/app/create/template/config.ts new file mode 100644 index 0000000..c84b969 --- /dev/null +++ b/packages/sdk/src/client/modules/app/create/template/config.ts @@ -0,0 +1,78 @@ +/** + * Template configuration + * + * Loads and manages template configuration + */ + +import * as fs from "fs"; +import * as path from "path"; +import { load as loadYaml } from "js-yaml"; +import { getDirname } from "../../../../common/utils/dirname"; + +const __dirname = getDirname(); + +export interface TemplateConfig { + framework: Record; +} + +export interface FrameworkSpec { + template: string; + version: string; + languages: string[]; +} + +/** + * Load template configuration + */ +export async function loadTemplateConfig(): Promise { + // Try to load from config directory + const configPath = path.join(__dirname, "../../config/templates.yaml"); + + if (fs.existsSync(configPath)) { + const content = fs.readFileSync(configPath, "utf-8"); + return loadYaml(content) as TemplateConfig; + } + + // Fallback to default config (matches config/templates.yaml) + return { + framework: { + tee: { + template: "https://github.com/Layr-Labs/eigenx-templates", + version: "main", + languages: ["typescript", "golang", "rust", "python"], + }, + }, + }; +} + +/** + * Get template URLs for framework and language + */ +export function getTemplateURLs( + config: TemplateConfig, + framework: string, + language: string, +): { templateURL: string; version: string } { + const fw = config.framework[framework]; + if (!fw) { + throw new Error(`Unknown framework: ${framework}`); + } + + if (!fw.template) { + throw new Error(`Template URL missing for framework: ${framework}`); + } + + // Language gate - only enforce if Languages array is populated + if (fw.languages && fw.languages.length > 0) { + if (!fw.languages.includes(language)) { + throw new Error( + `Language ${language} not available for framework ${framework}`, + ); + } + } + + return { + templateURL: fw.template, + version: fw.version, + }; +} diff --git a/packages/sdk/src/client/modules/app/create/template/git.ts b/packages/sdk/src/client/modules/app/create/template/git.ts new file mode 100644 index 0000000..f2dbad8 --- /dev/null +++ b/packages/sdk/src/client/modules/app/create/template/git.ts @@ -0,0 +1,164 @@ +/** + * Git template fetching + * + * Fetches templates from Git repositories + */ + +import * as fs from "fs"; +import * as path from "path"; +import * as os from "os"; +import { exec } from "child_process"; +import { promisify } from "util"; +import { Logger } from "../../../../common/types"; + +const execAsync = promisify(exec); + +export interface GitFetcherConfig { + verbose: boolean; +} + +/** + * Fetch full template repository + */ +export async function fetchTemplate( + repoURL: string, + ref: string, + targetDir: string, + config: GitFetcherConfig, + logger: Logger, +): Promise { + if (!repoURL) { + throw new Error("repoURL is required"); + } + + logger.info(`\nCloning repo: ${repoURL} → ${targetDir}\n`); + + try { + // Clone with no checkout + await execAsync( + `git clone --no-checkout --progress ${repoURL} ${targetDir}`, + { + maxBuffer: 10 * 1024 * 1024, + }, + ); + + // Checkout the desired ref + await execAsync(`git -C ${targetDir} checkout --quiet ${ref}`); + + // Update submodules + await execAsync( + `git -C ${targetDir} submodule update --init --recursive --progress`, + ); + + logger.info(`Clone repo complete: ${repoURL}\n`); + } catch (error: any) { + throw new Error(`Failed to clone repository: ${error.message}`); + } +} + +/** + * Fetch subdirectory from template repository using sparse checkout + */ +export async function fetchTemplateSubdirectory( + repoURL: string, + ref: string, + subPath: string, + targetDir: string, + logger: Logger, +): Promise { + if (!repoURL) { + throw new Error("repoURL is required"); + } + if (!subPath) { + throw new Error("subPath is required"); + } + + // Create temporary directory for sparse clone + let tempDir: string; + try { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "eigenx-template-")); + } catch { + // If that fails, try ~/.eigenx/tmp + const homeDir = os.homedir(); + const fallbackBase = path.join(homeDir, ".eigenx", "tmp"); + fs.mkdirSync(fallbackBase, { recursive: true }); + tempDir = fs.mkdtempSync(path.join(fallbackBase, "eigenx-template-")); + } + + try { + logger.info(`\nCloning template: ${repoURL} → extracting ${subPath}\n`); + + // Clone with sparse checkout + await cloneSparse(repoURL, ref, subPath, tempDir); + + // Verify subdirectory exists + const srcPath = path.join(tempDir, subPath); + if (!fs.existsSync(srcPath)) { + throw new Error( + `Template subdirectory ${subPath} not found in ${repoURL}`, + ); + } + + // Copy subdirectory contents to target + await copyDirectory(srcPath, targetDir); + + logger.info(`Template extraction complete: ${subPath}\n`); + } finally { + // Cleanup temp directory + fs.rmSync(tempDir, { recursive: true, force: true }); + } +} + +/** + * Clone repository with sparse checkout + */ +async function cloneSparse( + repoURL: string, + ref: string, + subPath: string, + tempDir: string, + // config: GitFetcherConfig +): Promise { + try { + // Initialize git repository + await execAsync(`git init ${tempDir}`); + + // Add remote + await execAsync(`git -C ${tempDir} remote add origin ${repoURL}`); + + // Enable sparse checkout + await execAsync(`git -C ${tempDir} config core.sparseCheckout true`); + + // Set sparse checkout path + const sparseCheckoutPath = path.join(tempDir, ".git/info/sparse-checkout"); + fs.writeFileSync(sparseCheckoutPath, `${subPath}\n`); + + // Fetch and checkout + await execAsync(`git -C ${tempDir} fetch origin ${ref}`); + + await execAsync(`git -C ${tempDir} checkout ${ref}`); + } catch (error: any) { + throw new Error(`Failed to clone sparse repository: ${error.message}`); + } +} + +/** + * Copy directory recursively + */ +async function copyDirectory(src: string, dst: string): Promise { + const entries = fs.readdirSync(src, { withFileTypes: true }); + + for (const entry of entries) { + const srcPath = path.join(src, entry.name); + const dstPath = path.join(dst, entry.name); + + if (entry.isDirectory()) { + fs.mkdirSync(dstPath, { recursive: true }); + await copyDirectory(srcPath, dstPath); + } else { + const stat = fs.statSync(srcPath); + fs.copyFileSync(srcPath, dstPath); + fs.chmodSync(dstPath, stat.mode); + } + } +} diff --git a/packages/sdk/src/client/modules/app/create/template/postprocess.ts b/packages/sdk/src/client/modules/app/create/template/postprocess.ts new file mode 100644 index 0000000..79ce447 --- /dev/null +++ b/packages/sdk/src/client/modules/app/create/template/postprocess.ts @@ -0,0 +1,212 @@ +/** + * Template post-processing + * + * Updates template files with project-specific values + */ + +import * as fs from "fs"; +import * as path from "path"; +import { Logger } from "../../../../common/types"; +import { LANGUAGE_FILES } from "../create"; +import { getDirname } from "../../../../common/utils/dirname"; + +// Config file paths +const __dirname = getDirname(); +const CONFIG_DIR = path.join(__dirname, "../../config"); + +/** + * Post-process template files + */ +export async function postProcessTemplate( + projectDir: string, + language: string, + logger: Logger, +): Promise { + const projectName = path.basename(projectDir); + const templateName = `eigenx-tee-${language}-app`; + + // 1. Copy .gitignore + await copyGitignore(projectDir); + + // 2. Copy shared template files (.env.example) + await copySharedTemplateFiles(projectDir); + + // 3. Update README.md title for all languages + await updateProjectFile( + projectDir, + "README.md", + templateName, + projectName, + logger, + ); + + // 4. Update language-specific project files + const languageFiles = LANGUAGE_FILES[language]; + if (languageFiles) { + for (const filename of languageFiles) { + await updateProjectFile( + projectDir, + filename, + templateName, + projectName, + logger, + ); + } + } +} + +/** + * Copy .gitignore from config + */ +async function copyGitignore(projectDir: string): Promise { + const destPath = path.join(projectDir, ".gitignore"); + + // Check if .gitignore already exists + if (fs.existsSync(destPath)) { + return; // File already exists, skip copying + } + + // Load from config directory + const gitignorePath = path.join(CONFIG_DIR, ".gitignore"); + if (fs.existsSync(gitignorePath)) { + fs.copyFileSync(gitignorePath, destPath); + } else { + // Fallback to default gitignore content + const defaultGitignore = `# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Build outputs +/bin/ +/out/ + +# OS-specific files +.DS_Store +Thumbs.db + +# Editor and IDE files +.vscode/ +.idea/ +*.swp + +# Environment +.env + +# Language-specific build outputs +node_modules/ +dist/ +build/ +target/ +__pycache__/ +*.pyc +`; + fs.writeFileSync(destPath, defaultGitignore, { mode: 0o644 }); + } +} + +/** + * Copy shared template files + */ +async function copySharedTemplateFiles(projectDir: string): Promise { + // Write .env.example + const envPath = path.join(projectDir, ".env.example"); + const envExamplePath = path.join(CONFIG_DIR, ".env.example"); + if (fs.existsSync(envExamplePath)) { + fs.copyFileSync(envExamplePath, envPath); + } else { + // Fallback to default .env.example + const defaultEnvExample = `# Environment variables +# Variables ending with _PUBLIC will be visible on-chain +# All other variables will be encrypted + +# Example public variable +# API_URL_PUBLIC=https://api.example.com + +# Example private variable +# SECRET_KEY=your-secret-key-here +`; + fs.writeFileSync(envPath, defaultEnvExample, { mode: 0o644 }); + } + + // Write or append README.md + const readmePath = path.join(projectDir, "README.md"); + const readmeConfigPath = path.join(CONFIG_DIR, "README.md"); + + if (fs.existsSync(readmePath)) { + // README.md exists, append the content + const readmeContent = fs.existsSync(readmeConfigPath) + ? fs.readFileSync(readmeConfigPath, "utf-8") + : getDefaultReadme(); + fs.appendFileSync(readmePath, "\n" + readmeContent); + } else { + // README.md doesn't exist, create it + const readmeContent = fs.existsSync(readmeConfigPath) + ? fs.readFileSync(readmeConfigPath, "utf-8") + : getDefaultReadme(); + fs.writeFileSync(readmePath, readmeContent, { mode: 0o644 }); + } +} + +/** + * Get default README content + */ +function getDefaultReadme(): string { + return `## Prerequisites + +Before deploying, you'll need: + +- **Docker** - To package and publish your application image +- **ETH** - To pay for deployment transactions + +## Deployment + +\`\`\`bash +eigenx app deploy username/image-name +\`\`\` + +The CLI will automatically detect the \`Dockerfile\` and build your app before deploying. + +## Management & Monitoring + +\`\`\`bash +npx ecloud app list # List all apps +npx ecloud app info [app-name] # Get app details +npx ecloud app logs [app-name] # View logs +npx ecloud app start [app-name] # Start stopped app +npx ecloud app stop [app-name] # Stop running app +npx ecloud app terminate [app-name] # Terminate app +npx ecloud app upgrade [app-name] [image] # Update deployment +\`\`\` +`; +} + +/** + * Update project file by replacing template name with project name + */ +async function updateProjectFile( + projectDir: string, + filename: string, + oldString: string, + newString: string, + logger: Logger, +): Promise { + const filePath = path.join(projectDir, filename); + + // Check if file exists + if (!fs.existsSync(filePath)) { + logger.debug(`File ${filename} not found, skipping update`); + return; + } + + // Read current file + const content = fs.readFileSync(filePath, "utf-8"); + + // Replace the specified string + const newContent = content.replaceAll(oldString, newString); + + // Write back to file + fs.writeFileSync(filePath, newContent, { mode: 0o644 }); +} diff --git a/packages/sdk/src/client/modules/app/deploy/config/environment.ts b/packages/sdk/src/client/modules/app/deploy/config/environment.ts deleted file mode 100644 index 7d4a36b..0000000 --- a/packages/sdk/src/client/modules/app/deploy/config/environment.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Environment configuration for different networks - */ - -import { EnvironmentConfig } from '../types'; - -// Chain IDs -export const SEPOLIA_CHAIN_ID = 11155111n; -export const MAINNET_CHAIN_ID = 1n; - -// Environment configurations -const ENVIRONMENTS: Record> = { - sepolia: { - name: 'sepolia', - appControllerAddress: '0x...', // TODO: Add actual addresses - permissionControllerAddress: '0x...', - erc7702DelegatorAddress: '0x...', - kmsServerURL: 'https://kms.sepolia.eigencloud.xyz', - userApiServerURL: 'https://api.sepolia.eigencloud.xyz', - }, - 'mainnet-alpha': { - name: 'mainnet-alpha', - appControllerAddress: '0x...', // TODO: Add actual addresses - permissionControllerAddress: '0x...', - erc7702DelegatorAddress: '0x...', - kmsServerURL: 'https://kms.eigencloud.xyz', - userApiServerURL: 'https://api.eigencloud.xyz', - }, -}; - -const CHAIN_ID_TO_ENVIRONMENT: Record = { - [SEPOLIA_CHAIN_ID.toString()]: 'sepolia', - [MAINNET_CHAIN_ID.toString()]: 'mainnet-alpha', -}; - -/** - * Get environment configuration - */ -export function getEnvironmentConfig( - environment: string, - chainID?: bigint -): EnvironmentConfig { - const env = ENVIRONMENTS[environment]; - if (!env) { - throw new Error(`Unknown environment: ${environment}`); - } - - // If chainID provided, validate it matches - if (chainID) { - const expectedEnv = CHAIN_ID_TO_ENVIRONMENT[chainID.toString()]; - if (expectedEnv && expectedEnv !== environment) { - throw new Error( - `Environment ${environment} does not match chain ID ${chainID}` - ); - } - } - - // Determine chain ID from environment if not provided - const resolvedChainID = - chainID || - (environment === 'sepolia' ? SEPOLIA_CHAIN_ID : MAINNET_CHAIN_ID); - - return { - ...env, - chainID: resolvedChainID, - }; -} - -/** - * Detect environment from chain ID - */ -export function detectEnvironmentFromChainID( - chainID: bigint -): string | undefined { - return CHAIN_ID_TO_ENVIRONMENT[chainID.toString()]; -} - diff --git a/packages/sdk/src/client/modules/app/deploy/constants.ts b/packages/sdk/src/client/modules/app/deploy/constants.ts index 7b7c4f9..87ba9b3 100644 --- a/packages/sdk/src/client/modules/app/deploy/constants.ts +++ b/packages/sdk/src/client/modules/app/deploy/constants.ts @@ -2,21 +2,21 @@ * Constants used throughout the SDK */ -export const DOCKER_PLATFORM = 'linux/amd64'; +export const DOCKER_PLATFORM = "linux/amd64"; export const REGISTRY_PROPAGATION_WAIT_SECONDS = 3; -export const LAYERED_DOCKERFILE_NAME = 'Dockerfile.eigencompute'; -export const ENV_SOURCE_SCRIPT_NAME = 'compute-source-env.sh'; -export const KMS_CLIENT_BINARY_NAME = 'kms-client'; -export const KMS_ENCRYPTION_KEY_NAME = 'kms-encryption-public-key.pem'; -export const KMS_SIGNING_KEY_NAME = 'kms-signing-public-key.pem'; -export const TLS_KEYGEN_BINARY_NAME = 'tls-keygen'; -export const CADDYFILE_NAME = 'Caddyfile'; -export const TEMP_IMAGE_PREFIX = 'ecloud-temp-'; -export const LAYERED_BUILD_DIR_PREFIX = 'ecloud-layered-build'; -export const SHA256_PREFIX = 'sha256:'; -export const JWT_FILE_PATH = '/run/container_launcher/attestation_verifier_claims_token'; +export const LAYERED_DOCKERFILE_NAME = "Dockerfile.eigencompute"; +export const ENV_SOURCE_SCRIPT_NAME = "compute-source-env.sh"; +export const KMS_CLIENT_BINARY_NAME = "kms-client"; +export const KMS_ENCRYPTION_KEY_NAME = "kms-encryption-public-key.pem"; +export const KMS_SIGNING_KEY_NAME = "kms-signing-public-key.pem"; +export const TLS_KEYGEN_BINARY_NAME = "tls-keygen"; +export const CADDYFILE_NAME = "Caddyfile"; +export const TEMP_IMAGE_PREFIX = "ecloud-temp-"; +export const LAYERED_BUILD_DIR_PREFIX = "ecloud-layered-build"; +export const SHA256_PREFIX = "sha256:"; +export const JWT_FILE_PATH = + "/run/container_launcher/attestation_verifier_claims_token"; // Template paths (relative to templates directory) -export const LAYERED_DOCKERFILE_TEMPLATE_PATH = 'Dockerfile.layered.tmpl'; -export const ENV_SOURCE_SCRIPT_TEMPLATE_PATH = 'compute-source-env.sh.tmpl'; - +export const LAYERED_DOCKERFILE_TEMPLATE_PATH = "Dockerfile.layered.tmpl"; +export const ENV_SOURCE_SCRIPT_TEMPLATE_PATH = "compute-source-env.sh.tmpl"; diff --git a/packages/sdk/src/client/modules/app/deploy/contract/abis/AppController.json b/packages/sdk/src/client/modules/app/deploy/contract/abis/AppController.json deleted file mode 100644 index 79cd065..0000000 --- a/packages/sdk/src/client/modules/app/deploy/contract/abis/AppController.json +++ /dev/null @@ -1,1046 +0,0 @@ -[ - { - "type": "constructor", - "inputs": [ - { - "name": "_version", - "type": "string", - "internalType": "string" - }, - { - "name": "_permissionController", - "type": "address", - "internalType": "contractIPermissionController" - }, - { - "name": "_releaseManager", - "type": "address", - "internalType": "contractIReleaseManager" - }, - { - "name": "_computeAVSRegistrar", - "type": "address", - "internalType": "contractIComputeAVSRegistrar" - }, - { - "name": "_computeOperator", - "type": "address", - "internalType": "contractIComputeOperator" - }, - { - "name": "_appBeacon", - "type": "address", - "internalType": "contractIBeacon" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "API_PERMISSION_TYPEHASH", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "bytes32", - "internalType": "bytes32" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "appBeacon", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "contractIBeacon" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "calculateApiPermissionDigestHash", - "inputs": [ - { - "name": "permission", - "type": "bytes4", - "internalType": "bytes4" - }, - { - "name": "expiry", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "bytes32", - "internalType": "bytes32" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "calculateAppId", - "inputs": [ - { - "name": "deployer", - "type": "address", - "internalType": "address" - }, - { - "name": "salt", - "type": "bytes32", - "internalType": "bytes32" - } - ], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "contractIApp" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "computeAVSRegistrar", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "contractIComputeAVSRegistrar" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "computeOperator", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "contractIComputeOperator" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "createApp", - "inputs": [ - { - "name": "salt", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "release", - "type": "tuple", - "internalType": "structIAppController.Release", - "components": [ - { - "name": "rmsRelease", - "type": "tuple", - "internalType": "structIReleaseManagerTypes.Release", - "components": [ - { - "name": "artifacts", - "type": "tuple[]", - "internalType": "structIReleaseManagerTypes.Artifact[]", - "components": [ - { - "name": "digest", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "registry", - "type": "string", - "internalType": "string" - } - ] - }, - { - "name": "upgradeByTime", - "type": "uint32", - "internalType": "uint32" - } - ] - }, - { - "name": "publicEnv", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "encryptedEnv", - "type": "bytes", - "internalType": "bytes" - } - ] - } - ], - "outputs": [ - { - "name": "app", - "type": "address", - "internalType": "contractIApp" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "domainSeparator", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "bytes32", - "internalType": "bytes32" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getActiveAppCount", - "inputs": [ - { - "name": "user", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "uint32", - "internalType": "uint32" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getAppCreator", - "inputs": [ - { - "name": "app", - "type": "address", - "internalType": "contractIApp" - } - ], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getAppLatestReleaseBlockNumber", - "inputs": [ - { - "name": "app", - "type": "address", - "internalType": "contractIApp" - } - ], - "outputs": [ - { - "name": "", - "type": "uint32", - "internalType": "uint32" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getAppOperatorSetId", - "inputs": [ - { - "name": "app", - "type": "address", - "internalType": "contractIApp" - } - ], - "outputs": [ - { - "name": "", - "type": "uint32", - "internalType": "uint32" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getAppStatus", - "inputs": [ - { - "name": "app", - "type": "address", - "internalType": "contractIApp" - } - ], - "outputs": [ - { - "name": "", - "type": "uint8", - "internalType": "enumIAppController.AppStatus" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getApps", - "inputs": [ - { - "name": "offset", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "limit", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "apps", - "type": "address[]", - "internalType": "contractIApp[]" - }, - { - "name": "appConfigsMem", - "type": "tuple[]", - "internalType": "structIAppController.AppConfig[]", - "components": [ - { - "name": "creator", - "type": "address", - "internalType": "address" - }, - { - "name": "operatorSetId", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "latestReleaseBlockNumber", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "status", - "type": "uint8", - "internalType": "enumIAppController.AppStatus" - } - ] - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getAppsByCreator", - "inputs": [ - { - "name": "creator", - "type": "address", - "internalType": "address" - }, - { - "name": "offset", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "limit", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "apps", - "type": "address[]", - "internalType": "contractIApp[]" - }, - { - "name": "appConfigsMem", - "type": "tuple[]", - "internalType": "structIAppController.AppConfig[]", - "components": [ - { - "name": "creator", - "type": "address", - "internalType": "address" - }, - { - "name": "operatorSetId", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "latestReleaseBlockNumber", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "status", - "type": "uint8", - "internalType": "enumIAppController.AppStatus" - } - ] - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getAppsByDeveloper", - "inputs": [ - { - "name": "developer", - "type": "address", - "internalType": "address" - }, - { - "name": "offset", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "limit", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "apps", - "type": "address[]", - "internalType": "contractIApp[]" - }, - { - "name": "appConfigsMem", - "type": "tuple[]", - "internalType": "structIAppController.AppConfig[]", - "components": [ - { - "name": "creator", - "type": "address", - "internalType": "address" - }, - { - "name": "operatorSetId", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "latestReleaseBlockNumber", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "status", - "type": "uint8", - "internalType": "enumIAppController.AppStatus" - } - ] - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getMaxActiveAppsPerUser", - "inputs": [ - { - "name": "user", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "uint32", - "internalType": "uint32" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "globalActiveAppCount", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint32", - "internalType": "uint32" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "initialize", - "inputs": [ - { - "name": "admin", - "type": "address", - "internalType": "address" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "maxGlobalActiveApps", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint32", - "internalType": "uint32" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "permissionController", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "contractIPermissionController" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "releaseManager", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "contractIReleaseManager" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "setMaxActiveAppsPerUser", - "inputs": [ - { - "name": "user", - "type": "address", - "internalType": "address" - }, - { - "name": "limit", - "type": "uint32", - "internalType": "uint32" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "setMaxGlobalActiveApps", - "inputs": [ - { - "name": "limit", - "type": "uint32", - "internalType": "uint32" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "startApp", - "inputs": [ - { - "name": "app", - "type": "address", - "internalType": "contractIApp" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "stopApp", - "inputs": [ - { - "name": "app", - "type": "address", - "internalType": "contractIApp" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "suspend", - "inputs": [ - { - "name": "account", - "type": "address", - "internalType": "address" - }, - { - "name": "apps", - "type": "address[]", - "internalType": "contractIApp[]" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "terminateApp", - "inputs": [ - { - "name": "app", - "type": "address", - "internalType": "contractIApp" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "terminateAppByAdmin", - "inputs": [ - { - "name": "app", - "type": "address", - "internalType": "contractIApp" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "updateAppMetadataURI", - "inputs": [ - { - "name": "app", - "type": "address", - "internalType": "contractIApp" - }, - { - "name": "metadataURI", - "type": "string", - "internalType": "string" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "upgradeApp", - "inputs": [ - { - "name": "app", - "type": "address", - "internalType": "contractIApp" - }, - { - "name": "release", - "type": "tuple", - "internalType": "structIAppController.Release", - "components": [ - { - "name": "rmsRelease", - "type": "tuple", - "internalType": "structIReleaseManagerTypes.Release", - "components": [ - { - "name": "artifacts", - "type": "tuple[]", - "internalType": "structIReleaseManagerTypes.Artifact[]", - "components": [ - { - "name": "digest", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "registry", - "type": "string", - "internalType": "string" - } - ] - }, - { - "name": "upgradeByTime", - "type": "uint32", - "internalType": "uint32" - } - ] - }, - { - "name": "publicEnv", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "encryptedEnv", - "type": "bytes", - "internalType": "bytes" - } - ] - } - ], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "version", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "string", - "internalType": "string" - } - ], - "stateMutability": "view" - }, - { - "type": "event", - "name": "AppCreated", - "inputs": [ - { - "name": "creator", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "app", - "type": "address", - "indexed": true, - "internalType": "contractIApp" - }, - { - "name": "operatorSetId", - "type": "uint32", - "indexed": false, - "internalType": "uint32" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "AppMetadataURIUpdated", - "inputs": [ - { - "name": "app", - "type": "address", - "indexed": true, - "internalType": "contractIApp" - }, - { - "name": "metadataURI", - "type": "string", - "indexed": false, - "internalType": "string" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "AppStarted", - "inputs": [ - { - "name": "app", - "type": "address", - "indexed": true, - "internalType": "contractIApp" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "AppStopped", - "inputs": [ - { - "name": "app", - "type": "address", - "indexed": true, - "internalType": "contractIApp" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "AppSuspended", - "inputs": [ - { - "name": "app", - "type": "address", - "indexed": true, - "internalType": "contractIApp" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "AppTerminated", - "inputs": [ - { - "name": "app", - "type": "address", - "indexed": true, - "internalType": "contractIApp" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "AppTerminatedByAdmin", - "inputs": [ - { - "name": "app", - "type": "address", - "indexed": true, - "internalType": "contractIApp" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "AppUpgraded", - "inputs": [ - { - "name": "app", - "type": "address", - "indexed": true, - "internalType": "contractIApp" - }, - { - "name": "rmsReleaseId", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - }, - { - "name": "release", - "type": "tuple", - "indexed": false, - "internalType": "structIAppController.Release", - "components": [ - { - "name": "rmsRelease", - "type": "tuple", - "internalType": "structIReleaseManagerTypes.Release", - "components": [ - { - "name": "artifacts", - "type": "tuple[]", - "internalType": "structIReleaseManagerTypes.Artifact[]", - "components": [ - { - "name": "digest", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "registry", - "type": "string", - "internalType": "string" - } - ] - }, - { - "name": "upgradeByTime", - "type": "uint32", - "internalType": "uint32" - } - ] - }, - { - "name": "publicEnv", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "encryptedEnv", - "type": "bytes", - "internalType": "bytes" - } - ] - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "GlobalMaxActiveAppsSet", - "inputs": [ - { - "name": "limit", - "type": "uint32", - "indexed": false, - "internalType": "uint32" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "Initialized", - "inputs": [ - { - "name": "version", - "type": "uint8", - "indexed": false, - "internalType": "uint8" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "MaxActiveAppsSet", - "inputs": [ - { - "name": "user", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "limit", - "type": "uint32", - "indexed": false, - "internalType": "uint32" - } - ], - "anonymous": false - }, - { - "type": "error", - "name": "AccountHasActiveApps", - "inputs": [] - }, - { - "type": "error", - "name": "AppAlreadyExists", - "inputs": [] - }, - { - "type": "error", - "name": "AppDoesNotExist", - "inputs": [] - }, - { - "type": "error", - "name": "GlobalMaxActiveAppsExceeded", - "inputs": [] - }, - { - "type": "error", - "name": "InvalidAppStatus", - "inputs": [] - }, - { - "type": "error", - "name": "InvalidPermissions", - "inputs": [] - }, - { - "type": "error", - "name": "InvalidReleaseMetadataURI", - "inputs": [] - }, - { - "type": "error", - "name": "InvalidShortString", - "inputs": [] - }, - { - "type": "error", - "name": "InvalidSignature", - "inputs": [] - }, - { - "type": "error", - "name": "MaxActiveAppsExceeded", - "inputs": [] - }, - { - "type": "error", - "name": "MoreThanOneArtifact", - "inputs": [] - }, - { - "type": "error", - "name": "SignatureExpired", - "inputs": [] - }, - { - "type": "error", - "name": "StringTooLong", - "inputs": [ - { - "name": "str", - "type": "string", - "internalType": "string" - } - ] - } -] diff --git a/packages/sdk/src/client/modules/app/deploy/contract/abis/ERC7702Delegator.json b/packages/sdk/src/client/modules/app/deploy/contract/abis/ERC7702Delegator.json deleted file mode 100644 index c8a3298..0000000 --- a/packages/sdk/src/client/modules/app/deploy/contract/abis/ERC7702Delegator.json +++ /dev/null @@ -1,1027 +0,0 @@ -[ - { - "type": "constructor", - "inputs": [ - { - "name": "_delegationManager", - "type": "address", - "internalType": "contractIDelegationManager" - }, - { - "name": "_entryPoint", - "type": "address", - "internalType": "contractIEntryPoint" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "receive", - "stateMutability": "payable" - }, - { - "type": "function", - "name": "DOMAIN_VERSION", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "string", - "internalType": "string" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "NAME", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "string", - "internalType": "string" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "PACKED_USER_OP_TYPEHASH", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "bytes32", - "internalType": "bytes32" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "VERSION", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "string", - "internalType": "string" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "addDeposit", - "inputs": [], - "outputs": [], - "stateMutability": "payable" - }, - { - "type": "function", - "name": "delegationManager", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "contractIDelegationManager" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "disableDelegation", - "inputs": [ - { - "name": "_delegation", - "type": "tuple", - "internalType": "structDelegation", - "components": [ - { - "name": "delegate", - "type": "address", - "internalType": "address" - }, - { - "name": "delegator", - "type": "address", - "internalType": "address" - }, - { - "name": "authority", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "caveats", - "type": "tuple[]", - "internalType": "structCaveat[]", - "components": [ - { - "name": "enforcer", - "type": "address", - "internalType": "address" - }, - { - "name": "terms", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "args", - "type": "bytes", - "internalType": "bytes" - } - ] - }, - { - "name": "salt", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "signature", - "type": "bytes", - "internalType": "bytes" - } - ] - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "eip712Domain", - "inputs": [], - "outputs": [ - { - "name": "fields", - "type": "bytes1", - "internalType": "bytes1" - }, - { - "name": "name", - "type": "string", - "internalType": "string" - }, - { - "name": "version", - "type": "string", - "internalType": "string" - }, - { - "name": "chainId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "verifyingContract", - "type": "address", - "internalType": "address" - }, - { - "name": "salt", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "extensions", - "type": "uint256[]", - "internalType": "uint256[]" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "enableDelegation", - "inputs": [ - { - "name": "_delegation", - "type": "tuple", - "internalType": "structDelegation", - "components": [ - { - "name": "delegate", - "type": "address", - "internalType": "address" - }, - { - "name": "delegator", - "type": "address", - "internalType": "address" - }, - { - "name": "authority", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "caveats", - "type": "tuple[]", - "internalType": "structCaveat[]", - "components": [ - { - "name": "enforcer", - "type": "address", - "internalType": "address" - }, - { - "name": "terms", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "args", - "type": "bytes", - "internalType": "bytes" - } - ] - }, - { - "name": "salt", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "signature", - "type": "bytes", - "internalType": "bytes" - } - ] - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "entryPoint", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "contractIEntryPoint" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "execute", - "inputs": [ - { - "name": "_execution", - "type": "tuple", - "internalType": "structExecution", - "components": [ - { - "name": "target", - "type": "address", - "internalType": "address" - }, - { - "name": "value", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "callData", - "type": "bytes", - "internalType": "bytes" - } - ] - } - ], - "outputs": [], - "stateMutability": "payable" - }, - { - "type": "function", - "name": "execute", - "inputs": [ - { - "name": "_mode", - "type": "bytes32", - "internalType": "ModeCode" - }, - { - "name": "_executionCalldata", - "type": "bytes", - "internalType": "bytes" - } - ], - "outputs": [], - "stateMutability": "payable" - }, - { - "type": "function", - "name": "executeFromExecutor", - "inputs": [ - { - "name": "_mode", - "type": "bytes32", - "internalType": "ModeCode" - }, - { - "name": "_executionCalldata", - "type": "bytes", - "internalType": "bytes" - } - ], - "outputs": [ - { - "name": "returnData_", - "type": "bytes[]", - "internalType": "bytes[]" - } - ], - "stateMutability": "payable" - }, - { - "type": "function", - "name": "getDeposit", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getDomainHash", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "bytes32", - "internalType": "bytes32" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getNonce", - "inputs": [ - { - "name": "_key", - "type": "uint192", - "internalType": "uint192" - } - ], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getNonce", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getPackedUserOperationHash", - "inputs": [ - { - "name": "_userOp", - "type": "tuple", - "internalType": "structPackedUserOperation", - "components": [ - { - "name": "sender", - "type": "address", - "internalType": "address" - }, - { - "name": "nonce", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "initCode", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "callData", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "accountGasLimits", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "preVerificationGas", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "gasFees", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "paymasterAndData", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "signature", - "type": "bytes", - "internalType": "bytes" - } - ] - } - ], - "outputs": [ - { - "name": "", - "type": "bytes32", - "internalType": "bytes32" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getPackedUserOperationTypedDataHash", - "inputs": [ - { - "name": "_userOp", - "type": "tuple", - "internalType": "structPackedUserOperation", - "components": [ - { - "name": "sender", - "type": "address", - "internalType": "address" - }, - { - "name": "nonce", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "initCode", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "callData", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "accountGasLimits", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "preVerificationGas", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "gasFees", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "paymasterAndData", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "signature", - "type": "bytes", - "internalType": "bytes" - } - ] - } - ], - "outputs": [ - { - "name": "", - "type": "bytes32", - "internalType": "bytes32" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "isDelegationDisabled", - "inputs": [ - { - "name": "_delegationHash", - "type": "bytes32", - "internalType": "bytes32" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "isValidSignature", - "inputs": [ - { - "name": "_hash", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "_signature", - "type": "bytes", - "internalType": "bytes" - } - ], - "outputs": [ - { - "name": "magicValue_", - "type": "bytes4", - "internalType": "bytes4" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "onERC1155BatchReceived", - "inputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - }, - { - "name": "", - "type": "address", - "internalType": "address" - }, - { - "name": "", - "type": "uint256[]", - "internalType": "uint256[]" - }, - { - "name": "", - "type": "uint256[]", - "internalType": "uint256[]" - }, - { - "name": "", - "type": "bytes", - "internalType": "bytes" - } - ], - "outputs": [ - { - "name": "", - "type": "bytes4", - "internalType": "bytes4" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "onERC1155Received", - "inputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - }, - { - "name": "", - "type": "address", - "internalType": "address" - }, - { - "name": "", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "", - "type": "bytes", - "internalType": "bytes" - } - ], - "outputs": [ - { - "name": "", - "type": "bytes4", - "internalType": "bytes4" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "onERC721Received", - "inputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - }, - { - "name": "", - "type": "address", - "internalType": "address" - }, - { - "name": "", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "", - "type": "bytes", - "internalType": "bytes" - } - ], - "outputs": [ - { - "name": "", - "type": "bytes4", - "internalType": "bytes4" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "redeemDelegations", - "inputs": [ - { - "name": "_permissionContexts", - "type": "bytes[]", - "internalType": "bytes[]" - }, - { - "name": "_modes", - "type": "bytes32[]", - "internalType": "ModeCode[]" - }, - { - "name": "_executionCallDatas", - "type": "bytes[]", - "internalType": "bytes[]" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "supportsExecutionMode", - "inputs": [ - { - "name": "_mode", - "type": "bytes32", - "internalType": "ModeCode" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "supportsInterface", - "inputs": [ - { - "name": "_interfaceId", - "type": "bytes4", - "internalType": "bytes4" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "validateUserOp", - "inputs": [ - { - "name": "_userOp", - "type": "tuple", - "internalType": "structPackedUserOperation", - "components": [ - { - "name": "sender", - "type": "address", - "internalType": "address" - }, - { - "name": "nonce", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "initCode", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "callData", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "accountGasLimits", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "preVerificationGas", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "gasFees", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "paymasterAndData", - "type": "bytes", - "internalType": "bytes" - }, - { - "name": "signature", - "type": "bytes", - "internalType": "bytes" - } - ] - }, - { - "name": "", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "_missingAccountFunds", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "validationData_", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "withdrawDeposit", - "inputs": [ - { - "name": "_withdrawAddress", - "type": "address", - "internalType": "addresspayable" - }, - { - "name": "_withdrawAmount", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "event", - "name": "EIP712DomainChanged", - "inputs": [], - "anonymous": false - }, - { - "type": "event", - "name": "SentPrefund", - "inputs": [ - { - "name": "sender", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - }, - { - "name": "success", - "type": "bool", - "indexed": false, - "internalType": "bool" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "SetDelegationManager", - "inputs": [ - { - "name": "newDelegationManager", - "type": "address", - "indexed": true, - "internalType": "contractIDelegationManager" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "SetEntryPoint", - "inputs": [ - { - "name": "entryPoint", - "type": "address", - "indexed": true, - "internalType": "contractIEntryPoint" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "TryExecuteUnsuccessful", - "inputs": [ - { - "name": "batchExecutionindex", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - }, - { - "name": "result", - "type": "bytes", - "indexed": false, - "internalType": "bytes" - } - ], - "anonymous": false - }, - { - "type": "error", - "name": "ECDSAInvalidSignature", - "inputs": [] - }, - { - "type": "error", - "name": "ECDSAInvalidSignatureLength", - "inputs": [ - { - "name": "length", - "type": "uint256", - "internalType": "uint256" - } - ] - }, - { - "type": "error", - "name": "ECDSAInvalidSignatureS", - "inputs": [ - { - "name": "s", - "type": "bytes32", - "internalType": "bytes32" - } - ] - }, - { - "type": "error", - "name": "ExecutionFailed", - "inputs": [] - }, - { - "type": "error", - "name": "InvalidEIP712NameLength", - "inputs": [] - }, - { - "type": "error", - "name": "InvalidEIP712VersionLength", - "inputs": [] - }, - { - "type": "error", - "name": "InvalidShortString", - "inputs": [] - }, - { - "type": "error", - "name": "NotDelegationManager", - "inputs": [] - }, - { - "type": "error", - "name": "NotEntryPoint", - "inputs": [] - }, - { - "type": "error", - "name": "NotEntryPointOrSelf", - "inputs": [] - }, - { - "type": "error", - "name": "NotSelf", - "inputs": [] - }, - { - "type": "error", - "name": "StringTooLong", - "inputs": [ - { - "name": "str", - "type": "string", - "internalType": "string" - } - ] - }, - { - "type": "error", - "name": "UnauthorizedCallContext", - "inputs": [] - }, - { - "type": "error", - "name": "UnsupportedCallType", - "inputs": [ - { - "name": "callType", - "type": "bytes1", - "internalType": "CallType" - } - ] - }, - { - "type": "error", - "name": "UnsupportedExecType", - "inputs": [ - { - "name": "execType", - "type": "bytes1", - "internalType": "ExecType" - } - ] - } -] diff --git a/packages/sdk/src/client/modules/app/deploy/contract/abis/PermissionController.json b/packages/sdk/src/client/modules/app/deploy/contract/abis/PermissionController.json deleted file mode 100644 index 1e35d37..0000000 --- a/packages/sdk/src/client/modules/app/deploy/contract/abis/PermissionController.json +++ /dev/null @@ -1,494 +0,0 @@ -[ - { - "type": "function", - "name": "acceptAdmin", - "inputs": [ - { - "name": "account", - "type": "address", - "internalType": "address" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "addPendingAdmin", - "inputs": [ - { - "name": "account", - "type": "address", - "internalType": "address" - }, - { - "name": "admin", - "type": "address", - "internalType": "address" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "canCall", - "inputs": [ - { - "name": "account", - "type": "address", - "internalType": "address" - }, - { - "name": "caller", - "type": "address", - "internalType": "address" - }, - { - "name": "target", - "type": "address", - "internalType": "address" - }, - { - "name": "selector", - "type": "bytes4", - "internalType": "bytes4" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "getAdmins", - "inputs": [ - { - "name": "account", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "address[]", - "internalType": "address[]" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getAppointeePermissions", - "inputs": [ - { - "name": "account", - "type": "address", - "internalType": "address" - }, - { - "name": "appointee", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "address[]", - "internalType": "address[]" - }, - { - "name": "", - "type": "bytes4[]", - "internalType": "bytes4[]" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "getAppointees", - "inputs": [ - { - "name": "account", - "type": "address", - "internalType": "address" - }, - { - "name": "target", - "type": "address", - "internalType": "address" - }, - { - "name": "selector", - "type": "bytes4", - "internalType": "bytes4" - } - ], - "outputs": [ - { - "name": "", - "type": "address[]", - "internalType": "address[]" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "getPendingAdmins", - "inputs": [ - { - "name": "account", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "address[]", - "internalType": "address[]" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "isAdmin", - "inputs": [ - { - "name": "account", - "type": "address", - "internalType": "address" - }, - { - "name": "caller", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "isPendingAdmin", - "inputs": [ - { - "name": "account", - "type": "address", - "internalType": "address" - }, - { - "name": "pendingAdmin", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "removeAdmin", - "inputs": [ - { - "name": "account", - "type": "address", - "internalType": "address" - }, - { - "name": "admin", - "type": "address", - "internalType": "address" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "removeAppointee", - "inputs": [ - { - "name": "account", - "type": "address", - "internalType": "address" - }, - { - "name": "appointee", - "type": "address", - "internalType": "address" - }, - { - "name": "target", - "type": "address", - "internalType": "address" - }, - { - "name": "selector", - "type": "bytes4", - "internalType": "bytes4" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "removePendingAdmin", - "inputs": [ - { - "name": "account", - "type": "address", - "internalType": "address" - }, - { - "name": "admin", - "type": "address", - "internalType": "address" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "setAppointee", - "inputs": [ - { - "name": "account", - "type": "address", - "internalType": "address" - }, - { - "name": "appointee", - "type": "address", - "internalType": "address" - }, - { - "name": "target", - "type": "address", - "internalType": "address" - }, - { - "name": "selector", - "type": "bytes4", - "internalType": "bytes4" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "version", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "string", - "internalType": "string" - } - ], - "stateMutability": "view" - }, - { - "type": "event", - "name": "AdminRemoved", - "inputs": [ - { - "name": "account", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "admin", - "type": "address", - "indexed": false, - "internalType": "address" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "AdminSet", - "inputs": [ - { - "name": "account", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "admin", - "type": "address", - "indexed": false, - "internalType": "address" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "AppointeeRemoved", - "inputs": [ - { - "name": "account", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "appointee", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "target", - "type": "address", - "indexed": false, - "internalType": "address" - }, - { - "name": "selector", - "type": "bytes4", - "indexed": false, - "internalType": "bytes4" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "AppointeeSet", - "inputs": [ - { - "name": "account", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "appointee", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "target", - "type": "address", - "indexed": false, - "internalType": "address" - }, - { - "name": "selector", - "type": "bytes4", - "indexed": false, - "internalType": "bytes4" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "PendingAdminAdded", - "inputs": [ - { - "name": "account", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "admin", - "type": "address", - "indexed": false, - "internalType": "address" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "PendingAdminRemoved", - "inputs": [ - { - "name": "account", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "admin", - "type": "address", - "indexed": false, - "internalType": "address" - } - ], - "anonymous": false - }, - { - "type": "error", - "name": "AdminAlreadyPending", - "inputs": [] - }, - { - "type": "error", - "name": "AdminAlreadySet", - "inputs": [] - }, - { - "type": "error", - "name": "AdminNotPending", - "inputs": [] - }, - { - "type": "error", - "name": "AdminNotSet", - "inputs": [] - }, - { - "type": "error", - "name": "AppointeeAlreadySet", - "inputs": [] - }, - { - "type": "error", - "name": "AppointeeNotSet", - "inputs": [] - }, - { - "type": "error", - "name": "CannotHaveZeroAdmins", - "inputs": [] - }, - { - "type": "error", - "name": "NotAdmin", - "inputs": [] - } -] diff --git a/packages/sdk/src/client/modules/app/deploy/contract/caller.ts b/packages/sdk/src/client/modules/app/deploy/contract/caller.ts index 9a40f10..d8339d9 100644 --- a/packages/sdk/src/client/modules/app/deploy/contract/caller.ts +++ b/packages/sdk/src/client/modules/app/deploy/contract/caller.ts @@ -1,17 +1,26 @@ /** * Contract interactions - * + * * This module handles on-chain contract interactions using viem */ -import { createWalletClient, createPublicClient, http, Address, Hex, encodeFunctionData, decodeFunctionResult } from 'viem'; -import { privateKeyToAccount } from 'viem/accounts'; -import { EnvironmentConfig, Logger } from '../types'; -import { Release } from '../types'; -import { executeBatch } from './eip7702'; +import { sepolia, mainnet } from "viem/chains"; +import { privateKeyToAccount } from "viem/accounts"; +import { executeBatch } from "./eip7702"; +import { + createWalletClient, + createPublicClient, + http, + Address, + Hex, + encodeFunctionData, +} from "viem"; -import AppControllerABI from './abis/AppController.json'; -import PermissionControllerABI from './abis/PermissionController.json'; +import { EnvironmentConfig, Logger } from "../../../../common/types"; +import { Release } from "../../../../common/types"; + +import AppControllerABI from "../../../../common/abis/AppController.json"; +import PermissionControllerABI from "../../../../common/abis/PermissionController.json"; export interface DeployAppOptions { privateKey: string; // Will be converted to Hex @@ -30,21 +39,44 @@ export async function calculateAppID( privateKey: string | Hex, rpcUrl: string, environmentConfig: EnvironmentConfig, - salt: Uint8Array + salt: Uint8Array, ): Promise
{ - const privateKeyHex = typeof privateKey === 'string' - ? (privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`) as Hex - : privateKey; + const privateKeyHex = + typeof privateKey === "string" + ? ((privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`) as Hex) + : privateKey; const account = privateKeyToAccount(privateKeyHex); + + // Map chainID to viem Chain + const chain = + environmentConfig.chainID === 11155111n + ? sepolia + : environmentConfig.chainID === 1n + ? mainnet + : sepolia; // Default to sepolia if unknown + const publicClient = createPublicClient({ + chain, transport: http(rpcUrl), }); + // Ensure salt is properly formatted as hex string (32 bytes = 64 hex chars) + const saltHexString = Buffer.from(salt).toString("hex"); + // Pad to 64 characters if needed (shouldn't be needed for 32 bytes, but just in case) + const paddedSaltHex = saltHexString.padStart(64, "0"); + const saltHex = `0x${paddedSaltHex}` as Hex; + + // Ensure address is a string (viem might return Hex type) + const accountAddress = + typeof account.address === "string" + ? account.address + : (account.address as Buffer).toString(); + const appID = await publicClient.readContract({ address: environmentConfig.appControllerAddress as Address, abi: AppControllerABI, - functionName: 'calculateAppId', - args: [account.address, `0x${Buffer.from(salt).toString('hex')}`], + functionName: "calculateAppId", + args: [accountAddress as Address, saltHex], }); return appID as Address; @@ -55,7 +87,7 @@ export async function calculateAppID( */ export async function deployApp( options: DeployAppOptions, - logger: Logger + logger: Logger, ): Promise<{ appAddress: Address; txHash: Hex }> { const { privateKey, @@ -67,43 +99,87 @@ export async function deployApp( imageRef, } = options; - const privateKeyHex = typeof privateKey === 'string' - ? (privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`) as Hex - : privateKey; + const privateKeyHex = + typeof privateKey === "string" + ? ((privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`) as Hex) + : privateKey; const account = privateKeyToAccount(privateKeyHex); + + // Map chainID to viem Chain + const chain = + environmentConfig.chainID === 11155111n + ? sepolia + : environmentConfig.chainID === 1n + ? mainnet + : sepolia; // Default to sepolia if unknown + const publicClient = createPublicClient({ + chain, transport: http(rpcUrl), }); const walletClient = createWalletClient({ account, + chain, transport: http(rpcUrl), }); // 1. Calculate app ID - logger.info('Calculating app ID...'); + logger.info("Calculating app ID..."); const appAddress = await calculateAppID( privateKeyHex, rpcUrl, environmentConfig, - salt + salt, ); logger.info(`App ID: ${appAddress}`); + // Verify the app address calculation matches what createApp will deploy + // This ensures we're calling acceptAdmin on the correct app address + logger.debug(`App address calculated: ${appAddress}`); + logger.debug(`This address will be used for acceptAdmin call`); + // 2. Pack create app call + // Ensure salt is properly formatted as hex string (32 bytes = 64 hex chars) + const saltHexString = Buffer.from(salt).toString("hex"); + // Pad to 64 characters if needed (shouldn't be needed for 32 bytes, but just in case) + const paddedSaltHex = saltHexString.padStart(64, "0"); + const saltHex = `0x${paddedSaltHex}` as Hex; + + // Convert Release Uint8Array values to hex strings for viem + // Viem expects hex strings for bytes and bytes32 types + const releaseForViem = { + rmsRelease: { + artifacts: release.rmsRelease.artifacts.map((artifact) => ({ + digest: + `0x${Buffer.from(artifact.digest).toString("hex").padStart(64, "0")}` as Hex, + registry: artifact.registry, + })), + upgradeByTime: release.rmsRelease.upgradeByTime, + }, + publicEnv: `0x${Buffer.from(release.publicEnv).toString("hex")}` as Hex, + encryptedEnv: + `0x${Buffer.from(release.encryptedEnv).toString("hex")}` as Hex, + }; + const createData = encodeFunctionData({ abi: AppControllerABI, - functionName: 'createApp', - args: [`0x${Buffer.from(salt).toString('hex')}`, release], + functionName: "createApp", + args: [saltHex, releaseForViem], }); // 3. Pack accept admin call + // NOTE: createApp calls initialize(admin) which adds the EOA (msg.sender) as a pending admin + // for the app account. So acceptAdmin should work when called from the EOA. + // The execution order in the batch ensures createApp completes before acceptAdmin runs. const acceptAdminData = encodeFunctionData({ abi: PermissionControllerABI, - functionName: 'acceptAdmin', + functionName: "acceptAdmin", args: [appAddress], }); // 4. Assemble executions + // CRITICAL: Order matters! createApp must complete first to call initialize(admin) + // which adds the EOA as a pending admin. Then acceptAdmin can be called. const executions: Array<{ target: Address; value: bigint; @@ -125,12 +201,12 @@ export async function deployApp( if (publicLogs) { const anyoneCanViewLogsData = encodeFunctionData({ abi: PermissionControllerABI, - functionName: 'setAppointee', + functionName: "setAppointee", args: [ appAddress, - '0x493219d9949348178af1f58740655951a8cd110c' as Address, // AnyoneCanCallAddress - '0x57ee1fb74c1087e26446abc4fb87fd8f07c43d8d' as Address, // ApiPermissionsTarget - '0x2fd3f2fe' as Hex, // CanViewAppLogsPermission + "0x493219d9949348178af1f58740655951a8cd110c" as Address, // AnyoneCanCallAddress + "0x57ee1fb74c1087e26446abc4fb87fd8f07c43d8d" as Address, // ApiPermissionsTarget + "0x2fd3f2fe" as Hex, // CanViewAppLogsPermission ], }); executions.push({ @@ -142,7 +218,7 @@ export async function deployApp( // 6. Execute batch via EIP-7702 delegator const confirmationPrompt = `Deploy new app with image: ${imageRef}`; - const pendingMessage = 'Deploying new app...'; + const pendingMessage = "Deploying new app..."; const txHash = await executeBatch( { @@ -153,8 +229,9 @@ export async function deployApp( needsConfirmation: environmentConfig.chainID === 1n, // Mainnet needs confirmation confirmationPrompt, pendingMessage, + privateKey: privateKeyHex, // Pass private key for manual transaction signing }, - logger + logger, ); return { appAddress, txHash }; diff --git a/packages/sdk/src/client/modules/app/deploy/contract/eip7702.ts b/packages/sdk/src/client/modules/app/deploy/contract/eip7702.ts index da64e50..c0ce2b8 100644 --- a/packages/sdk/src/client/modules/app/deploy/contract/eip7702.ts +++ b/packages/sdk/src/client/modules/app/deploy/contract/eip7702.ts @@ -1,22 +1,26 @@ /** * EIP-7702 transaction handling - * + * * This module handles EIP-7702 delegation and batch execution */ import { - createWalletClient, - createPublicClient, - http, Address, Hex, encodeFunctionData, - type WalletClient, - type PublicClient, -} from 'viem'; -import { EnvironmentConfig, Logger } from '../types'; + encodeAbiParameters, + decodeErrorResult, + keccak256, + toBytes, + concat, +} from "viem"; +import { hashAuthorization } from "viem/utils"; +import { sign } from "viem/accounts"; -import ERC7702DelegatorABI from './abis/ERC7702Delegator.json'; +import type { WalletClient, PublicClient } from "viem"; +import { EnvironmentConfig, Logger } from "../../../../common/types"; + +import ERC7702DelegatorABI from "../../../../common/abis/ERC7702Delegator.json"; export interface ExecuteBatchOptions { walletClient: WalletClient; @@ -30,6 +34,7 @@ export interface ExecuteBatchOptions { needsConfirmation: boolean; confirmationPrompt: string; pendingMessage: string; + privateKey?: Hex; // Private key for signing raw hash (required for authorization signing) } /** @@ -38,7 +43,7 @@ export interface ExecuteBatchOptions { export async function checkERC7702Delegation( publicClient: PublicClient, account: Address, - delegatorAddress: Address + delegatorAddress: Address, ): Promise { const code = await publicClient.getBytecode({ address: account }); if (!code) { @@ -50,57 +55,18 @@ export async function checkERC7702Delegation( return code.toLowerCase() === expectedCode.toLowerCase(); } -/** - * Create authorization signature for EIP-7702 - */ -export async function createAuthorization( - walletClient: WalletClient, - publicClient: PublicClient, - delegatorAddress: Address -): Promise<{ - chainId: number; - address: Address; - nonce: bigint; - signature: Hex; -}> { - const account = walletClient.account; - if (!account) { - throw new Error('Wallet client must have an account'); - } - - // Get current nonce - const nonce = await publicClient.getTransactionCount({ - address: account.address, - }); - - // Increment nonce for authorization - const authorizationNonce = BigInt(nonce) + 1n; - - // Get chain ID - const chainId = await publicClient.getChainId(); - - // Create authorization tuple for ERC-7702 delegation - const authorization = { - chainId, - address: delegatorAddress, - nonce: authorizationNonce, - }; - - // Sign the authorization - // Note: viem handles EIP-7702 authorization signing automatically - // when using sendTransaction with authorizationList - return { - ...authorization, - signature: '0x' as Hex, // Placeholder - viem handles this internally - }; -} - /** * Execute batch of operations via EIP-7702 delegator + * + * This function uses viem's built-in EIP-7702 support to handle transaction + * construction, signing, and sending. We focus on: + * 1. Encoding the executions correctly + * 2. Creating authorization if needed + * 3. Passing the right parameters to viem */ export async function executeBatch( options: ExecuteBatchOptions, - logger: Logger + logger: Logger, ): Promise { const { walletClient, @@ -110,115 +76,188 @@ export async function executeBatch( needsConfirmation, confirmationPrompt, pendingMessage, + privateKey, } = options; const account = walletClient.account; if (!account) { - throw new Error('Wallet client must have an account'); + throw new Error("Wallet client must have an account"); } - const chain = walletClient.chain + const chain = walletClient.chain; if (!chain) { - throw new Error('Wallet client must have an chain'); + throw new Error("Wallet client must have a chain"); } - // 1. Encode executions - const encodedExecutions = encodeFunctionData({ - abi: ERC7702DelegatorABI, - functionName: 'encodeExecutions', - args: [executions], - }); + // 1. Encode executions array + // The Execution struct is: { target: address, value: uint256, callData: bytes } + // Go's EncodeExecutions uses abi.Arguments.Pack which produces standard ABI encoding + const encodedExecutions = encodeAbiParameters( + [ + { + type: "tuple[]", + components: [ + { name: "target", type: "address" }, + { name: "value", type: "uint256" }, + { name: "callData", type: "bytes" }, + ], + }, + ], + [executions], + ); // 2. Pack ExecuteBatch call - const executeBatchData = encodeFunctionData({ - abi: ERC7702DelegatorABI, - functionName: 'execute', - args: [ - '0x01' as Hex, // executeBatchMode - encodedExecutions, - ], - }); + // Mode 0x01 is executeBatchMode (32 bytes, padded) (big endian) + const executeBatchMode = + "0x0100000000000000000000000000000000000000000000000000000000000000" as Hex; + + // Encode the execute function call + // Function signature: execute(bytes32 _mode, bytes _executionCalldata) + // Function selector: 0xe9ae5c53 + let executeBatchData: Hex; + try { + executeBatchData = encodeFunctionData({ + abi: ERC7702DelegatorABI, + functionName: "execute", + args: [executeBatchMode, encodedExecutions], + }); + } catch { + // Fallback: Manually construct if viem selects wrong overload + const functionSignature = "execute(bytes32,bytes)"; + const selector = keccak256(toBytes(functionSignature)).slice(0, 10) as Hex; + const encodedParams = encodeAbiParameters( + [{ type: "bytes32" }, { type: "bytes" }], + [executeBatchMode, encodedExecutions], + ); + executeBatchData = concat([selector as Hex, encodedParams]) as Hex; + } // 3. Check if account is delegated const isDelegated = await checkERC7702Delegation( publicClient, account.address, - environmentConfig.erc7702DelegatorAddress as Address + environmentConfig.erc7702DelegatorAddress as Address, ); - // 4. Create authorization if not delegated + // 4. Create authorization if needed let authorizationList: Array<{ chainId: number; address: Address; nonce: number; + r: Hex; + s: Hex; + yParity: number; }> = []; if (!isDelegated) { - logger.debug('Account not delegated, creating authorization...'); - const authorization = await createAuthorization( - walletClient, - publicClient, - environmentConfig.erc7702DelegatorAddress as Address - ); + if (!privateKey) { + throw new Error("Private key required for signing authorization"); + } + + const transactionNonce = await publicClient.getTransactionCount({ + address: account.address, + blockTag: "pending", + }); + + const chainId = await publicClient.getChainId(); + const authorizationNonce = BigInt(transactionNonce) + 1n; + + const authorization = { + chainId: Number(chainId), + address: environmentConfig.erc7702DelegatorAddress as Address, + nonce: authorizationNonce, + }; + + const sighash = hashAuthorization({ + chainId: authorization.chainId, + contractAddress: authorization.address, + nonce: Number(authorization.nonce), + }); + + const sig = await sign({ + hash: sighash, + privateKey, + }); + + const v = Number(sig.v); + const yParity = v === 27 ? 0 : 1; + authorizationList = [ { - chainId: +(authorization.chainId.toString()), + chainId: authorization.chainId, address: authorization.address, - nonce: +(authorization.nonce.toString()), + nonce: Number(authorization.nonce), + r: sig.r as Hex, + s: sig.s as Hex, + yParity, }, ]; } - // 5. Handle confirmation if needed + // 5. Send transaction using viem if (needsConfirmation) { - // Estimate gas to calculate cost try { - const gasEstimate = await publicClient.estimateGas({ - account: account.address, - to: account.address, // EIP-7702 txs send to themselves - data: executeBatchData, - authorizationList, - }); - - const gasPrice = await publicClient.getGasPrice(); - const maxCostWei = gasEstimate * gasPrice; + const fees = await publicClient.estimateFeesPerGas(); + const estimatedGas = 2000000n; + const maxCostWei = estimatedGas * fees.maxFeePerGas; const costEth = formatETH(maxCostWei); - - logger.info(`${confirmationPrompt} on ${environmentConfig.name} (max cost: ${costEth} ETH)`); - // const confirmed = await promptConfirmation('Continue?'); - // if (!confirmed) { - // throw new Error('Operation cancelled'); - // } - } catch (error: any) { - logger.warn(`Failed to estimate gas: ${error.message}`); + logger.info( + `${confirmationPrompt} on ${environmentConfig.name} (estimated max cost: ${costEth} ETH)`, + ); + // TODO: Add confirmation prompt + } catch (error) { + logger.warn(`Could not estimate cost for confirmation: ${error}`); } } - // 6. Show pending message if (pendingMessage) { logger.info(pendingMessage); } - // 7. Send transaction - try { - const hash = await walletClient.sendTransaction({ - account: account.address, - chain, - to: account.address, // EIP-7702 txs send to themselves - data: executeBatchData, - authorizationList, - }); + const txRequest: any = { + account: walletClient.account!, + chain, + to: account.address, + data: executeBatchData, + value: 0n, + }; - // 8. Wait for transaction receipt - const receipt = await publicClient.waitForTransactionReceipt({ hash }); - if (receipt.status === 'reverted') { - throw new Error(`Transaction reverted: ${hash}`); - } + if (authorizationList.length > 0) { + txRequest.authorizationList = authorizationList; + } + + const hash = await walletClient.sendTransaction(txRequest); + logger.info(`Transaction sent: ${hash}`); + + const receipt = await publicClient.waitForTransactionReceipt({ hash }); - return hash; - } catch (error: any) { - throw new Error(`Failed to execute batch: ${error.message}`); + if (receipt.status === "reverted") { + let revertReason = "Unknown reason"; + try { + await publicClient.call({ + to: account.address, + data: executeBatchData, + account: account.address, + }); + } catch (callError: any) { + if (callError.data) { + try { + const decoded = decodeErrorResult({ + abi: ERC7702DelegatorABI, + data: callError.data, + }); + revertReason = `${decoded.errorName}: ${JSON.stringify(decoded.args)}`; + } catch { + revertReason = callError.message || "Unknown reason"; + } + } else { + revertReason = callError.message || "Unknown reason"; + } + } + throw new Error(`Transaction reverted: ${hash}. Reason: ${revertReason}`); } + + return hash; } /** @@ -228,4 +267,3 @@ function formatETH(wei: bigint): string { const eth = Number(wei) / 1e18; return eth.toFixed(6); } - diff --git a/packages/sdk/src/client/modules/app/deploy/contract/userapi.ts b/packages/sdk/src/client/modules/app/deploy/contract/userapi.ts deleted file mode 100644 index 7e94cc9..0000000 --- a/packages/sdk/src/client/modules/app/deploy/contract/userapi.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { Agent as UndiciAgent } from 'undici'; -import { Address } from 'viem'; -import { EnvironmentConfig } from '../types'; -import { getKMSKeysForEnvironment } from '../utils/keys'; - -export interface AppInfo { - address: Address; - status: string; - ip: string; - machineType: string; -} - -export interface AppInfoResponse { - apps: Array<{ - addresses: { - data: { - evmAddresses: Address[]; - solanaAddresses: string[]; - }; - signature: string; - }; - app_status: string; - ip: string; - machine_type: string; - }>; -} - -const MAX_ADDRESS_COUNT = 5; - -export class UserApiClient { - constructor(private readonly config: EnvironmentConfig) {} - - async getInfos(appIDs: Address[], addressCount = 1): Promise { - const count = Math.min(addressCount, MAX_ADDRESS_COUNT); - - const endpoint = `${this.config.userApiServerURL}/info`; - const url = `${endpoint}?${new URLSearchParams({ apps: appIDs.join(',') })}`; - - const res = await this.makeAuthenticatedRequest(url, '0x0e67b22f'); - const result: AppInfoResponse = await res.json(); - - // optional: verify signatures with KMS key - const { signingKey } = getKMSKeysForEnvironment(this.config.name); - - // Truncate without mutating the original object - return result.apps.map((app, i) => { - // TODO: Implement signature verification - // const valid = await verifyKMSSignature(appInfo.addresses, signingKey); - // if (!valid) { - // throw new Error(`Invalid signature for app ${appIDs[i]}`); - // } - const evm = app.addresses.data.evmAddresses.slice(0, count); - const sol = app.addresses.data.solanaAddresses.slice(0, count); - // If the API ties each `apps[i]` to `appIDs[i]`, use i. Otherwise derive from `evm[0]` - const inferredAddress = evm[0] ?? appIDs[i] ?? appIDs[0]; - - return { - address: inferredAddress as Address, - status: app.app_status, - ip: app.ip, - machineType: app.machine_type, - }; - }); - } - - private async makeAuthenticatedRequest(url: string, permission?: string): Promise { - const headers: Record = {}; - // Add auth headers if permission is specified - if (permission) { - // TODO: Implement auth header generation - // const expiry = BigInt(Math.floor(Date.now() / 1000) + 5 * 60); // 5 minutes - // const authHeaders = await generateAuthHeaders(permission, expiry); - // Object.assign(headers, authHeaders); - } - - // Node fetch uses Undici under the hood - use dispatcher, not https.Agent - const insecureDispatcher = new UndiciAgent({ - connect: { rejectUnauthorized: false }, - }); - - const response = await fetch(url, { - method: 'GET', - headers, - // @ts-expect-error - lib.dom types don’t include Undici extension - dispatcher: insecureDispatcher, - }); - - if (!response.ok) { - const text = await response.text(); - throw new Error(`UserAPI request failed: ${response.status} ${text}`); - } - return response; - } -} diff --git a/packages/sdk/src/client/modules/app/deploy/contract/watcher.ts b/packages/sdk/src/client/modules/app/deploy/contract/watcher.ts index 83fb18e..1b580f2 100644 --- a/packages/sdk/src/client/modules/app/deploy/contract/watcher.ts +++ b/packages/sdk/src/client/modules/app/deploy/contract/watcher.ts @@ -1,12 +1,12 @@ /** * Contract watcher - * + * * Watches app status until it reaches Running state using UserAPI */ -import { Address } from 'viem'; -import { EnvironmentConfig, Logger } from '../types'; -import { UserApiClient } from './userapi'; +import { Address } from "viem"; +import { EnvironmentConfig, Logger } from "../../../../common/types"; +import { UserApiClient } from "../../../../common/utils/userapi"; export interface WatchUntilRunningOptions { privateKey: string; @@ -16,21 +16,21 @@ export interface WatchUntilRunningOptions { } const WATCH_POLL_INTERVAL_SECONDS = 5; -const APP_STATUS_RUNNING = 'Running'; -const APP_STATUS_FAILED = 'Failed'; -const APP_STATUS_DEPLOYING = 'Deploying'; +const APP_STATUS_RUNNING = "Running"; +const APP_STATUS_FAILED = "Failed"; +// const APP_STATUS_DEPLOYING = 'Deploying'; /** * Watch app until it reaches Running status with IP address */ export async function watchUntilRunning( options: WatchUntilRunningOptions, - logger: Logger + logger: Logger, ): Promise { - const { environmentConfig, appID } = options; + const { environmentConfig, appID, privateKey } = options; // Create UserAPI client - const userApiClient = new UserApiClient(environmentConfig); + const userApiClient = new UserApiClient(environmentConfig, privateKey); // Track initial status and whether we've seen a change let initialStatus: string | undefined; @@ -56,10 +56,10 @@ export async function watchUntilRunning( if (status === APP_STATUS_RUNNING && ip) { if (hasChanged || initialStatus !== APP_STATUS_RUNNING) { // Only log IP if we didn't have one initially - if (!initialIP || initialIP === 'No IP assigned') { + if (!initialIP || initialIP === "No IP assigned") { logger.info(`App is now running with IP: ${ip}`); } else { - logger.info('App is now running'); + logger.info("App is now running"); } return true; } @@ -77,7 +77,7 @@ export async function watchUntilRunning( while (true) { try { // Fetch app info - const info = await userApiClient.getInfos([appID], 1); + const info = await userApiClient.getInfos([appID], 1, logger); if (info.length === 0) { await sleep(WATCH_POLL_INTERVAL_SECONDS * 1000); continue; @@ -85,7 +85,7 @@ export async function watchUntilRunning( const appInfo = info[0]; const currentStatus = appInfo.status; - const currentIP = appInfo.ip || ''; + const currentIP = appInfo.ip || ""; // Check stop condition if (stopCondition(currentStatus, currentIP)) { diff --git a/packages/sdk/src/client/modules/app/deploy/deploy.ts b/packages/sdk/src/client/modules/app/deploy/deploy.ts index 2889eee..ff2baaf 100644 --- a/packages/sdk/src/client/modules/app/deploy/deploy.ts +++ b/packages/sdk/src/client/modules/app/deploy/deploy.ts @@ -1,16 +1,26 @@ /** * Main deploy function - * + * * This is the main entry point for deploying applications to ecloud TEE. * It orchestrates all the steps: build, push, encrypt, and deploy on-chain. */ -import { DeployOptions, DeployResult, Logger } from './types'; -import { getEnvironmentConfig } from './config/environment'; -import { ensureDockerIsRunning } from './docker/build'; -import { prepareRelease } from './release/prepare'; -import { deployApp } from './contract/caller'; -import { watchUntilRunning } from './contract/watcher'; +import { DeployOptions, DeployResult, Logger } from "../../../common/types"; +import { ensureDockerIsRunning } from "./docker/build"; +import { prepareRelease } from "./release/prepare"; +import { deployApp, calculateAppID } from "./contract/caller"; +import { watchUntilRunning } from "./contract/watcher"; +import { + getDockerfileInteractive, + getImageReferenceInteractive, + getOrPromptAppName, + getEnvFileInteractive, + getInstanceTypeInteractive, + getLogSettingsInteractive, +} from "./utils/prompts"; +import { doPreflightChecks, PreflightContext } from "./utils/preflight"; +import { UserApiClient } from "../../../common/utils/userapi"; +import { setAppName } from "./registry/appNames"; /** * Default logger (console-based) @@ -23,83 +33,144 @@ const defaultLogger: Logger = { }; /** - * Deploy an application to ecloud TEE + * Deploy an application to ECloud TEE + * + * This function follows the exact same flow as the Go CLI deploy command: + * 1. Preflight checks (auth, network, etc.) + * 2. Ensure Docker is running + * 3. Check for Dockerfile before asking for image reference + * 4. Get image reference (context-aware based on Dockerfile decision) + * 5. Get app name upfront (before any expensive operations) + * 6. Get environment file configuration + * 7. Get instance type selection + * 8. Get log settings from flags or interactive prompt + * 9. Generate random salt + * 10. Get app ID (calculate from salt and address) + * 11. Prepare the release (includes build/push if needed) + * 12. Deploy the app + * 13. Save the app name mapping + * 14. Watch until app is running */ export async function deploy( - options: DeployOptions, - logger: Logger = defaultLogger + options: Partial, + logger: Logger = defaultLogger, ): Promise { - logger.info('Starting deployment...'); - - // 1. Preflight checks - logger.debug('Performing preflight checks...'); - const environmentConfig = getEnvironmentConfig(options.environment); - - // 2. Ensure Docker is running - logger.debug('Checking Docker...'); + // 1. Do preflight checks (auth, network, etc.) first + logger.debug("Performing preflight checks..."); + const preflightCtx = await doPreflightChecks(options, logger); + + // 2. Check if docker is running, else try to start it + logger.debug("Checking Docker..."); await ensureDockerIsRunning(); - // 3. Generate random salt for app ID + // 3. Check for Dockerfile before asking for image reference + const dockerfilePath = await getDockerfileInteractive(options.dockerfilePath); + const buildFromDockerfile = dockerfilePath !== ""; + + // 4. Get image reference (context-aware based on Dockerfile decision) + const imageRef = await getImageReferenceInteractive( + options.imageRef, + buildFromDockerfile, + ); + + // 5. Get app name upfront (before any expensive operations) + const environment = preflightCtx.environmentConfig.name; + const appName = await getOrPromptAppName( + options.appName, + environment, + imageRef, + ); + + // 6. Get environment file configuration + const envFilePath = await getEnvFileInteractive(options.envFilePath); + + // 7. Get instance type selection (uses first from backend as default for new apps) + const availableTypes = await fetchAvailableInstanceTypes( + preflightCtx, + logger, + ); + const instanceType = await getInstanceTypeInteractive( + options.instanceType, + "", // defaultSKU - empty for new deployments + availableTypes, + ); + + // 8. Get log settings from flags or interactive prompt + const logSettings = await getLogSettingsInteractive( + options.logVisibility as "public" | "private" | "off" | undefined, + ); + const { logRedirect, publicLogs } = logSettings; + + // 9. Generate random salt const salt = generateRandomSalt(); - logger.debug(`Generated salt: ${Buffer.from(salt).toString('hex')}`); - - // 4. Calculate app ID (requires contract interaction) - const appID = await calculateAppID( - options.privateKey, - options.rpcUrl, - environmentConfig, - salt + logger.debug(`Generated salt: ${Buffer.from(salt).toString("hex")}`); + + // 10. Get app ID (calculate from salt and address) + logger.debug("Calculating app ID..."); + const appIDToBeDeployed = await calculateAppID( + preflightCtx.privateKey, + options.rpcUrl || preflightCtx.rpcUrl, + preflightCtx.environmentConfig, + salt, ); - logger.info(`App ID: ${appID}`); + logger.info(`App ID: ${appIDToBeDeployed}`); - // 5. Prepare release (build, push, encrypt) - logger.info('Preparing release...'); + // 11. Prepare the release (includes build/push if needed, with automatic retry on permission errors) + logger.info("Preparing release..."); const { release, finalImageRef } = await prepareRelease( { - dockerfilePath: options.dockerfilePath, - imageRef: options.imageRef, - envFilePath: options.envFilePath, - logRedirect: options.logRedirect, - instanceType: options.instanceType, - environmentConfig, - appID, + dockerfilePath, + imageRef, + envFilePath, + logRedirect, + instanceType, + environmentConfig: preflightCtx.environmentConfig, + appID: appIDToBeDeployed, }, - logger + logger, ); - // 6. Deploy on-chain - logger.info('Deploying on-chain...'); - const { appAddress: deployedAppID, txHash } = await deployApp( + // 12. Deploy the app + logger.info("Deploying on-chain..."); + const deployedAppID = await deployApp( { - privateKey: options.privateKey, - rpcUrl: options.rpcUrl, - environmentConfig, + privateKey: preflightCtx.privateKey, + rpcUrl: options.rpcUrl || preflightCtx.rpcUrl, + environmentConfig: preflightCtx.environmentConfig, salt, release, - publicLogs: options.publicLogs, + publicLogs, imageRef: finalImageRef, }, - logger + logger, ); - // 7. Watch until running - logger.info('Waiting for app to start...'); + // 13. Save the app name mapping + try { + await setAppName(environment, deployedAppID.appAddress, appName); + logger.info(`App saved with name: ${appName}`); + } catch (err: any) { + logger.warn(`Failed to save app name: ${err.message}`); + } + + // 14. Watch until app is running + logger.info("Waiting for app to start..."); const ipAddress = await watchUntilRunning( { - privateKey: options.privateKey, - rpcUrl: options.rpcUrl, - environmentConfig, - appID: deployedAppID, + privateKey: preflightCtx.privateKey, + rpcUrl: options.rpcUrl || preflightCtx.rpcUrl, + environmentConfig: preflightCtx.environmentConfig, + appID: deployedAppID.appAddress, }, - logger + logger, ); return { - appID: deployedAppID, - appName: options.appName || '', + appID: deployedAppID.appAddress, + txHash: deployedAppID.txHash, + appName, imageRef: finalImageRef, ipAddress, - txHash, }; } @@ -113,21 +184,27 @@ function generateRandomSalt(): Uint8Array { } /** - * Calculate app ID from owner address and salt + * Fetch available instance types from backend */ -async function calculateAppID( - privateKey: string, - rpcUrl: string, - environmentConfig: any, - salt: Uint8Array -): Promise { - // Import calculateAppID from contract caller - const { calculateAppID } = await import('./contract/caller'); - return calculateAppID( - privateKey as `0x${string}`, - rpcUrl, - environmentConfig, - salt - ); -} +async function fetchAvailableInstanceTypes( + preflightCtx: PreflightContext, + logger: Logger, +): Promise> { + try { + const userApiClient = new UserApiClient( + preflightCtx.environmentConfig, + preflightCtx.privateKey, + ); + const skuList = await userApiClient.getSKUs(); + if (skuList.skus.length === 0) { + throw new Error("No instance types available from server"); + } + + return skuList.skus; + } catch (err: any) { + logger.warn(`Failed to fetch instance types: ${err.message}`); + // Return a default fallback + return [{ sku: "standard", Description: "Standard instance type" }]; + } +} diff --git a/packages/sdk/src/client/modules/app/deploy/docker/build.ts b/packages/sdk/src/client/modules/app/deploy/docker/build.ts index 0e243fe..a24c81c 100644 --- a/packages/sdk/src/client/modules/app/deploy/docker/build.ts +++ b/packages/sdk/src/client/modules/app/deploy/docker/build.ts @@ -2,54 +2,84 @@ * Docker build operations */ -import * as child_process from 'child_process'; -import { promisify } from 'util'; -import { DOCKER_PLATFORM } from '../constants'; -import { Logger } from '../types'; +import * as child_process from "child_process"; +import { promisify } from "util"; +import { DOCKER_PLATFORM } from "../constants"; +import { Logger } from "../../../../common/types"; const exec = promisify(child_process.exec); /** * Build Docker image using docker buildx + * Streams output in real-time to logger */ export async function buildDockerImage( buildContext: string, dockerfilePath: string, tag: string, - logger?: Logger + logger?: Logger, ): Promise { - const command = [ - 'docker', - 'buildx', - 'build', - '--platform', + const args = [ + "buildx", + "build", + "--platform", DOCKER_PLATFORM, - '-t', + "-t", tag, - '-f', + "-f", dockerfilePath, - '--progress=plain', + "--progress=plain", buildContext, - ].join(' '); + ]; logger?.info(`Building Docker image: ${tag}`); - try { - const { stdout, stderr } = await exec(command, { + return new Promise((resolve, reject) => { + const process = child_process.spawn("docker", args, { cwd: buildContext, - maxBuffer: 10 * 1024 * 1024, // 10MB buffer + stdio: ["ignore", "pipe", "pipe"], }); - if (stdout) { - logger?.debug(stdout); - } - if (stderr) { - logger?.warn(stderr); - } - } catch (error: any) { - const errorMessage = error.stderr || error.message || 'Unknown error'; - throw new Error(`Docker build failed: ${errorMessage}`); - } + let stdout = ""; + let stderr = ""; + + // Stream stdout to logger + process.stdout?.on("data", (data: Buffer) => { + const output = data.toString(); + stdout += output; + // Log each line to info (Docker build output is important) + output.split("\n").forEach((line) => { + if (line.trim()) { + logger?.info(line); + } + }); + }); + + // Stream stderr to logger + process.stderr?.on("data", (data: Buffer) => { + const output = data.toString(); + stderr += output; + // Log each line to info (Docker build output is important) + output.split("\n").forEach((line) => { + if (line.trim()) { + logger?.info(line); + } + }); + }); + + process.on("close", (code) => { + if (code !== 0) { + const errorMessage = stderr || stdout || "Unknown error"; + reject(new Error(`Docker build failed: ${errorMessage}`)); + } else { + resolve(); + } + }); + + process.on("error", (error) => { + reject(new Error(`Failed to start Docker build: ${error.message}`)); + }); + }); } /** @@ -57,7 +87,7 @@ export async function buildDockerImage( */ export async function isDockerRunning(): Promise { try { - await exec('docker info'); + await exec("docker info"); return true; } catch { return false; @@ -71,8 +101,7 @@ export async function ensureDockerIsRunning(): Promise { const running = await isDockerRunning(); if (!running) { throw new Error( - 'Docker is not running. Please start Docker and try again.' + "Docker is not running. Please start Docker and try again.", ); } } - diff --git a/packages/sdk/src/client/modules/app/deploy/docker/inspect.ts b/packages/sdk/src/client/modules/app/deploy/docker/inspect.ts index 8fde800..30ed833 100644 --- a/packages/sdk/src/client/modules/app/deploy/docker/inspect.ts +++ b/packages/sdk/src/client/modules/app/deploy/docker/inspect.ts @@ -2,15 +2,16 @@ * Docker image inspection */ -import Docker from 'dockerode'; -import { DockerImageConfig } from '../types'; +import Docker from "dockerode"; + +import { DockerImageConfig } from "../../../../common/types"; /** * Extract image configuration (CMD, ENTRYPOINT, USER) */ export async function extractImageConfig( docker: Docker, - imageTag: string + imageTag: string, ): Promise { try { const image = docker.getImage(imageTag); @@ -19,7 +20,7 @@ export async function extractImageConfig( const config = inspect.Config || {}; const cmd = config.Cmd || []; const entrypoint = config.Entrypoint || []; - const user = config.User || ''; + const user = config.User || ""; const labels = config.Labels || {}; // Use CMD if available, otherwise use ENTRYPOINT @@ -41,11 +42,11 @@ export async function extractImageConfig( */ export async function checkIfImageAlreadyLayeredForecloud( docker: Docker, - imageTag: string + imageTag: string, ): Promise { try { const config = await extractImageConfig(docker, imageTag); - return 'ECLOUD_cli_version' in config.labels; + return "ECLOUD_cli_version" in config.labels; } catch { return false; } @@ -57,8 +58,11 @@ export async function checkIfImageAlreadyLayeredForecloud( export async function pullDockerImage( docker: Docker, imageTag: string, - platform: string = 'linux/amd64' + platform: string = "linux/amd64", + logger?: { debug?: (msg: string) => void; info?: (msg: string) => void }, ): Promise { + logger?.info?.(`Pulling image ${imageTag}...`); + return new Promise((resolve, reject) => { docker.pull(imageTag, { platform }, (err, stream) => { if (err) { @@ -67,16 +71,27 @@ export async function pullDockerImage( } // Must consume the stream to ensure pull completes - docker.modem.followProgress(stream!, (err) => { - if (err) { - reject( - new Error(`Failed to complete image pull for ${imageTag}: ${err.message}`) - ); - } else { - resolve(); - } - }); + docker.modem.followProgress( + stream!, + (err) => { + if (err) { + reject( + new Error( + `Failed to complete image pull for ${imageTag}: ${err.message}`, + ), + ); + } else { + logger?.info?.(`Image pull completed: ${imageTag}`); + resolve(); + } + }, + (event: any) => { + // Log progress events + if (event && event.status) { + logger?.info?.(event.status); + } + }, + ); }); }); } - diff --git a/packages/sdk/src/client/modules/app/deploy/docker/layer.ts b/packages/sdk/src/client/modules/app/deploy/docker/layer.ts index 784415b..587af7e 100644 --- a/packages/sdk/src/client/modules/app/deploy/docker/layer.ts +++ b/packages/sdk/src/client/modules/app/deploy/docker/layer.ts @@ -1,20 +1,26 @@ /** * Docker image layering - * + * * This module handles adding ecloud components to Docker images */ -import Docker from 'dockerode'; -import * as fs from 'fs'; -import * as os from 'os'; -import * as path from 'path'; -import { EnvironmentConfig, Logger } from '../types'; -import { extractImageConfig, checkIfImageAlreadyLayeredForecloud, pullDockerImage } from './inspect'; -import { buildDockerImage } from './build'; -import { pushDockerImage } from './push'; -import { processDockerfileTemplate } from '../templates/dockerfileTemplate'; -import { processScriptTemplate } from '../templates/scriptTemplate'; -import { getKMSKeysForEnvironment } from '../utils/keys'; +import Docker from "dockerode"; + +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; + +import { + extractImageConfig, + checkIfImageAlreadyLayeredForecloud, + pullDockerImage, +} from "./inspect"; +import { buildDockerImage } from "./build"; +import { pushDockerImage } from "./push"; +import { processDockerfileTemplate } from "../templates/dockerfileTemplate"; +import { processScriptTemplate } from "../templates/scriptTemplate"; +import { getKMSKeysForEnvironment } from "../utils/keys"; + import { LAYERED_DOCKERFILE_NAME, ENV_SOURCE_SCRIPT_NAME, @@ -25,7 +31,69 @@ import { CADDYFILE_NAME, LAYERED_BUILD_DIR_PREFIX, DOCKER_PLATFORM, -} from '../constants'; +} from "../constants"; + +import { getDirname } from "../../../../common/utils/dirname"; + +import { EnvironmentConfig, Logger } from "../../../../common/types"; + +/** + * Find binary file in tools directory + * Supports both CLI (bundled) and standalone SDK usage + */ +function findBinary(binaryName: string): string { + const __dirname = getDirname(); + + // Try to find SDK root by looking for tools directory + // Start from current directory and walk up + let currentDir = __dirname; + const maxDepth = 10; + let depth = 0; + + while (depth < maxDepth) { + const toolsPath = path.join(currentDir, "tools", binaryName); + if (fs.existsSync(toolsPath)) { + return toolsPath; + } + + // Also check if we're in a monorepo structure + const sdkToolsPath = path.join( + currentDir, + "packages", + "sdk", + "tools", + binaryName, + ); + if (fs.existsSync(sdkToolsPath)) { + return sdkToolsPath; + } + + const parentDir = path.dirname(currentDir); + if (parentDir === currentDir) { + break; // Reached filesystem root + } + currentDir = parentDir; + depth++; + } + + // Try relative paths as fallback + const possiblePaths = [ + path.join(__dirname, "../../../tools", binaryName), // Standalone SDK from dist + path.join(__dirname, "../../../../tools", binaryName), // CLI bundled + path.join(__dirname, "../../../../../tools", binaryName), // Alternative CLI path + path.resolve(__dirname, "../../../../tools", binaryName), // From source + path.resolve(__dirname, "../../../../../tools", binaryName), // From source alternative + ]; + + for (const possiblePath of possiblePaths) { + if (fs.existsSync(possiblePath)) { + return possiblePath; + } + } + + // Return the most likely path for error messages + return path.resolve(__dirname, "../../../../tools", binaryName); +} export interface BuildAndPushLayeredImageOptions { dockerfilePath: string; @@ -47,15 +115,21 @@ export interface LayerRemoteImageIfNeededOptions { */ export async function buildAndPushLayeredImage( options: BuildAndPushLayeredImageOptions, - logger: Logger + logger: Logger, ): Promise { - const { dockerfilePath, targetImageRef, logRedirect, envFilePath, environmentConfig } = options; + const { + dockerfilePath, + targetImageRef, + logRedirect, + envFilePath, + environmentConfig, + } = options; // 1. Build base image from user's Dockerfile const baseImageTag = `ecloud-temp-${path.basename(dockerfilePath).toLowerCase()}`; logger.info(`Building base image from ${dockerfilePath}...`); - await buildDockerImage('.', dockerfilePath, baseImageTag, logger); + await buildDockerImage(".", dockerfilePath, baseImageTag, logger); // 2. Layer the base image const docker = new Docker(); @@ -68,7 +142,7 @@ export async function buildAndPushLayeredImage( envFilePath, environmentConfig, }, - logger + logger, ); } @@ -77,28 +151,33 @@ export async function buildAndPushLayeredImage( */ export async function layerRemoteImageIfNeeded( options: LayerRemoteImageIfNeededOptions, - logger: Logger + logger: Logger, ): Promise { const { imageRef, logRedirect, envFilePath, environmentConfig } = options; const docker = new Docker(); // Check if image already has ecloud layering - const alreadyLayered = await checkIfImageAlreadyLayeredForecloud(docker, imageRef); + const alreadyLayered = await checkIfImageAlreadyLayeredForecloud( + docker, + imageRef, + ); if (alreadyLayered) { - logger.info('Image already has ecloud layering'); + logger.info("Image already has ecloud layering"); return imageRef; } // Pull image to ensure we have it locally logger.info(`Pulling image ${imageRef}...`); - await pullDockerImage(docker, imageRef, DOCKER_PLATFORM); + await pullDockerImage(docker, imageRef, DOCKER_PLATFORM, logger); // Prompt for target image (to avoid overwriting source) // TODO: Make this configurable via options const targetImageRef = `${imageRef}-layered`; - logger.info(`Adding ecloud components to create ${targetImageRef} from ${imageRef}...`); + logger.info( + `Adding ecloud components to create ${targetImageRef} from ${imageRef}...`, + ); const layeredImageRef = await layerLocalImage( { docker, @@ -108,7 +187,7 @@ export async function layerRemoteImageIfNeeded( envFilePath, environmentConfig, }, - logger + logger, ); return layeredImageRef; @@ -126,23 +205,33 @@ async function layerLocalImage( envFilePath?: string; environmentConfig: EnvironmentConfig; }, - logger: Logger + logger: Logger, ): Promise { - const { docker, sourceImageRef, targetImageRef, logRedirect, envFilePath, environmentConfig } = options; + const { + docker, + sourceImageRef, + targetImageRef, + logRedirect, + envFilePath, + environmentConfig, + } = options; // 1. Extract original command and user from source image const imageConfig = await extractImageConfig(docker, sourceImageRef); - const originalCmd = imageConfig.cmd.length > 0 ? imageConfig.cmd : imageConfig.entrypoint; + const originalCmd = + imageConfig.cmd.length > 0 ? imageConfig.cmd : imageConfig.entrypoint; const originalUser = imageConfig.user; // 2. Check if TLS is needed (check for DOMAIN in env file) let includeTLS = false; if (envFilePath && fs.existsSync(envFilePath)) { - const envContent = fs.readFileSync(envFilePath, 'utf-8'); + const envContent = fs.readFileSync(envFilePath, "utf-8"); const domainMatch = envContent.match(/^DOMAIN=(.+)$/m); - if (domainMatch && domainMatch[1] && domainMatch[1] !== 'localhost') { + if (domainMatch && domainMatch[1] && domainMatch[1] !== "localhost") { includeTLS = true; - logger.debug(`Found DOMAIN=${domainMatch[1]} in ${envFilePath}, including TLS components`); + logger.debug( + `Found DOMAIN=${domainMatch[1]} in ${envFilePath}, including TLS components`, + ); } } @@ -153,13 +242,13 @@ async function layerLocalImage( originalUser: originalUser, logRedirect: logRedirect, includeTLS: includeTLS, - ecloudCLIVersion: '0.1.0', // TODO: Get from package.json + ecloudCLIVersion: "0.1.0", // TODO: Get from package.json }); const scriptContent = processScriptTemplate({ - KMSServerURL: environmentConfig.kmsServerURL, - JWTFile: '/run/container_launcher/attestation_verifier_claims_token', - UserAPIURL: environmentConfig.userApiServerURL, + kmsServerURL: environmentConfig.kmsServerURL, + jwtFile: "/run/container_launcher/attestation_verifier_claims_token", + userAPIURL: environmentConfig.userApiServerURL, }); // 4. Setup build directory @@ -168,18 +257,25 @@ async function layerLocalImage( layeredDockerfileContent, scriptContent, includeTLS, - logger + // logger ); try { // 5. Build layered image - logger.info(`Building updated image with ecloud components for ${sourceImageRef}...`); + logger.info( + `Building updated image with ecloud components for ${sourceImageRef}...`, + ); const layeredDockerfilePath = path.join(tempDir, LAYERED_DOCKERFILE_NAME); - await buildDockerImage(tempDir, layeredDockerfilePath, targetImageRef, logger); + await buildDockerImage( + tempDir, + layeredDockerfilePath, + targetImageRef, + logger, + ); // 6. Push to registry logger.info(`Publishing updated image to ${targetImageRef}...`); - await pushDockerImage(docker, targetImageRef); + await pushDockerImage(docker, targetImageRef, logger); logger.info(`Successfully published updated image: ${targetImageRef}`); return targetImageRef; @@ -197,21 +293,27 @@ async function setupLayeredBuildDirectory( layeredDockerfileContent: string, scriptContent: string, includeTLS: boolean, - logger?: Logger + // logger?: Logger ): Promise { - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), LAYERED_BUILD_DIR_PREFIX)); + const tempDir = fs.mkdtempSync( + path.join(os.tmpdir(), LAYERED_BUILD_DIR_PREFIX), + ); try { // Write layered Dockerfile const layeredDockerfilePath = path.join(tempDir, LAYERED_DOCKERFILE_NAME); - fs.writeFileSync(layeredDockerfilePath, layeredDockerfileContent, { mode: 0o644 }); + fs.writeFileSync(layeredDockerfilePath, layeredDockerfileContent, { + mode: 0o644, + }); // Write wrapper script const scriptPath = path.join(tempDir, ENV_SOURCE_SCRIPT_NAME); fs.writeFileSync(scriptPath, scriptContent, { mode: 0o755 }); // Copy KMS keys - const { encryptionKey, signingKey } = getKMSKeysForEnvironment(environmentConfig.name); + const { encryptionKey, signingKey } = getKMSKeysForEnvironment( + environmentConfig.name, + ); const encryptionKeyPath = path.join(tempDir, KMS_ENCRYPTION_KEY_NAME); fs.writeFileSync(encryptionKeyPath, encryptionKey, { mode: 0o644 }); @@ -220,18 +322,30 @@ async function setupLayeredBuildDirectory( fs.writeFileSync(signingKeyPath, signingKey, { mode: 0o644 }); // Copy kms-client binary - // TODO: Embed kms-client binary or load from path const kmsClientPath = path.join(tempDir, KMS_CLIENT_BINARY_NAME); - // fs.writeFileSync(kmsClientPath, kmsClientBinary, { mode: 0o755 }); - // Note: kms-client binary needs to be embedded or provided + const kmsClientSource = findBinary("kms-client-linux-amd64"); + if (!fs.existsSync(kmsClientSource)) { + throw new Error( + `kms-client binary not found. Expected at: ${kmsClientSource}. ` + + "Make sure binaries are in packages/sdk/tools/ directory.", + ); + } + fs.copyFileSync(kmsClientSource, kmsClientPath); + fs.chmodSync(kmsClientPath, 0o755); // Include TLS components if requested if (includeTLS) { // Copy tls-keygen binary - // TODO: Embed tls-keygen binary or load from path const tlsKeygenPath = path.join(tempDir, TLS_KEYGEN_BINARY_NAME); - // fs.writeFileSync(tlsKeygenPath, tlsKeygenBinary, { mode: 0o755 }); - // Note: tls-keygen binary needs to be embedded or provided + const tlsKeygenSource = findBinary("tls-keygen-linux-amd64"); + if (!fs.existsSync(tlsKeygenSource)) { + throw new Error( + `tls-keygen binary not found. Expected at: ${tlsKeygenSource}. ` + + "Make sure binaries are in packages/sdk/tools/ directory.", + ); + } + fs.copyFileSync(tlsKeygenSource, tlsKeygenPath); + fs.chmodSync(tlsKeygenPath, 0o755); // Handle Caddyfile const caddyfilePath = path.join(process.cwd(), CADDYFILE_NAME); @@ -241,8 +355,8 @@ async function setupLayeredBuildDirectory( fs.writeFileSync(destCaddyfilePath, caddyfileContent, { mode: 0o644 }); } else { throw new Error( - 'TLS is enabled (DOMAIN is set) but Caddyfile not found. ' + - 'Run configure TLS to set up TLS configuration' + "TLS is enabled (DOMAIN is set) but Caddyfile not found. " + + "Run configure TLS to set up TLS configuration", ); } } @@ -254,4 +368,3 @@ async function setupLayeredBuildDirectory( throw error; } } - diff --git a/packages/sdk/src/client/modules/app/deploy/docker/push.ts b/packages/sdk/src/client/modules/app/deploy/docker/push.ts index b7801c0..193431c 100644 --- a/packages/sdk/src/client/modules/app/deploy/docker/push.ts +++ b/packages/sdk/src/client/modules/app/deploy/docker/push.ts @@ -2,47 +2,283 @@ * Docker push operations */ -import Docker from 'dockerode'; -import * as fs from 'fs'; -import * as os from 'os'; -import * as path from 'path'; +import Docker from "dockerode"; + +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import * as child_process from "child_process"; + +import { exec } from "child_process"; +import { promisify } from "util"; + +const execAsync = promisify(exec); + +/** + * Extract registry from image reference + */ +export function extractRegistry(imageRef: string): string { + // Handle different registry formats: + // - docker.io/library/image:tag + // - ghcr.io/owner/image:tag + // - gcr.io/project/image:tag + // - registry.example.com/image:tag + + const parts = imageRef.split("/"); + if (parts.length < 2) { + return "docker.io"; // Default to Docker Hub + } + + const firstPart = parts[0]; + + // Check if first part is a registry (contains . or is a known registry) + if ( + firstPart.includes(".") || + firstPart === "ghcr.io" || + firstPart.includes("gcr.io") + ) { + return firstPart; + } + + // Default to Docker Hub + return "docker.io"; +} + +/** + * Get auth config for a specific registry + * Returns an object with username/password or auth string + * Handles both direct auth in config.json and credential stores + */ +export async function getRegistryAuthConfig( + registry: string, +): Promise< + { username?: string; password?: string; auth?: string } | undefined +> { + const authConfig = getDockerAuthConfig(); + + // Helper to extract auth from config entry + const extractAuth = (auth: any) => { + if (!auth) return undefined; + // If auth string exists, use it + if (auth.auth) { + return { auth: auth.auth }; + } + // If username and password exist, use them + if (auth.username && auth.password) { + return { username: auth.username, password: auth.password }; + } + return undefined; + }; + + // Try exact match first + const exactMatch = extractAuth(authConfig[registry]); + if (exactMatch) return exactMatch; + + // Try with https:// prefix + const httpsRegistry = `https://${registry}`; + const httpsMatch = extractAuth(authConfig[httpsRegistry]); + if (httpsMatch) return httpsMatch; + + // For ghcr.io, also try common variants + if (registry === "ghcr.io") { + const ghcrVariants = ["ghcr.io", "https://ghcr.io", "https://ghcr.io/v1/"]; + for (const variant of ghcrVariants) { + const match = extractAuth(authConfig[variant]); + if (match) return match; + + // If entry exists but is empty (credential store), try to get from helper + if ( + authConfig[variant] && + Object.keys(authConfig[variant]).length === 0 + ) { + const creds = await getCredentialsFromHelper("ghcr.io"); + if (creds) { + return { username: creds.username, password: creds.password }; + } + } + } + + // Also try to get from helper even if no entry exists (for credential store only setups) + const creds = await getCredentialsFromHelper("ghcr.io"); + if (creds) { + return { username: creds.username, password: creds.password }; + } + } + + // For Docker Hub, try common variants + if (registry === "docker.io" || registry.includes("docker.io")) { + const dockerVariants = [ + "https://index.docker.io/v1/", + "https://index.docker.io/v1", + "index.docker.io", + "docker.io", + ]; + for (const variant of dockerVariants) { + const match = extractAuth(authConfig[variant]); + if (match) return match; + } + } + + return undefined; +} /** * Push Docker image to registry + * Uses Docker CLI directly for better credential helper support + * Streams output in real-time to logger */ export async function pushDockerImage( docker: Docker, - imageRef: string + imageRef: string, + logger?: { debug?: (msg: string) => void; info?: (msg: string) => void }, ): Promise { - const image = docker.getImage(imageRef); + // Use Docker CLI directly instead of dockerode for better credential helper support + // Docker CLI automatically handles credential helpers, which dockerode sometimes struggles with + logger?.info?.(`Pushing image ${imageRef}...`); - try { - // dockerode's types say ReadableStream (web), but runtime is a Node stream. - const stream = (await image.push({})) as unknown as NodeJS.ReadableStream; - - await new Promise((resolve, reject) => { - docker.modem.followProgress( - stream, - (err?: any) => { - if (err) { - const msg = String(err?.message ?? err); - if (isPermissionError(msg)) { - reject(new PushPermissionError(imageRef, new Error(msg))); - } else { - reject(new Error(`Failed to complete image push for ${imageRef}: ${msg}`)); - } - return; - } - resolve(); + return new Promise((resolve, reject) => { + const process = child_process.spawn("docker", ["push", imageRef], { + stdio: ["ignore", "pipe", "pipe"], + }); + + let stdout = ""; + let stderr = ""; + + // Stream stdout to logger + process.stdout?.on("data", (data: Buffer) => { + const output = data.toString(); + stdout += output; + // Log each line to info (Docker push output shows progress) + output.split("\n").forEach((line) => { + if (line.trim()) { + logger?.info?.(line); + } + }); + }); + + // Stream stderr to logger + process.stderr?.on("data", (data: Buffer) => { + const output = data.toString(); + stderr += output; + // Log each line to info (Docker push output shows progress) + output.split("\n").forEach((line) => { + if (line.trim()) { + logger?.info?.(line); + } + }); + }); + + process.on("close", async (code) => { + if (code !== 0) { + const errorMsg = stderr || stdout || "Unknown error"; + if (isPermissionError(errorMsg)) { + reject(new PushPermissionError(imageRef, new Error(errorMsg))); + } else { + reject(new Error(`Docker push failed: ${errorMsg}`)); } - ); + return; + } + + // Check for success indicators + const output = stdout + stderr; + if ( + !output.includes("digest:") && + !output.includes("pushed") && + !output.includes("Pushed") + ) { + logger?.debug?.( + "No clear success indicator in push output, verifying...", + ); + } + + logger?.info?.("Image push completed successfully"); + + // Verify the push by checking if image exists in registry + // Wait a bit longer for GHCR to process + try { + await verifyImageExists(imageRef, logger); + resolve(); + } catch (error: any) { + reject(error); + } + }); + + process.on("error", (error) => { + const msg = error.message || String(error); + if (msg.includes("command not found") || msg.includes("ENOENT")) { + reject( + new Error( + `Docker CLI not found. Please ensure Docker is installed and in your PATH.`, + ), + ); + } else { + reject(new Error(`Failed to start Docker push: ${msg}`)); + } }); - } catch (e: any) { - const msg = String(e?.message ?? e); - if (isPermissionError(msg)) { - throw new PushPermissionError(imageRef, new Error(msg)); + }); +} + +/** + * Verify that the image exists in the registry after push + */ +async function verifyImageExists( + imageRef: string, + logger?: { debug?: (msg: string) => void; info?: (msg: string) => void }, +): Promise { + // Wait longer for registry to process (GHCR can be slow) + logger?.debug?.("Waiting for registry to process image..."); + await new Promise((resolve) => setTimeout(resolve, 3000)); + + // Retry verification up to 5 times with increasing delays + let retries = 5; + + while (retries > 0) { + try { + await execAsync(`docker manifest inspect ${imageRef}`, { + maxBuffer: 10 * 1024 * 1024, + timeout: 10000, // 10 second timeout + }); + // If we get here, the image exists + logger?.debug?.("Image verified in registry"); + return; + } catch (error: any) { + const errorMsg = error.message || String(error); + + // If manifest inspect fails, wait and retry + if ( + errorMsg.includes("manifest unknown") || + errorMsg.includes("not found") + ) { + retries--; + if (retries > 0) { + const waitTime = (6 - retries) * 2000; // 2s, 4s, 6s, 8s, 10s + logger?.debug?.( + `Image not found yet, retrying in ${waitTime / 1000}s... (${retries} retries left)`, + ); + await new Promise((resolve) => setTimeout(resolve, waitTime)); + continue; + } + // All retries exhausted + throw new Error( + `Image push verification failed: Image ${imageRef} was not found in registry after multiple attempts.\n` + + `This usually means the push failed. Please check:\n` + + `1. Your authentication: docker login ghcr.io\n` + + `2. Your permissions: Ensure you have push access to the repository\n` + + `3. Try pushing manually: docker push ${imageRef}\n` + + `4. Check if the image exists: docker manifest inspect ${imageRef}`, + ); + } + // Other errors might be temporary (network issues, etc.) + // Retry once more + retries--; + if (retries > 0) { + await new Promise((resolve) => setTimeout(resolve, 2000)); + continue; + } + // Log a warning but don't fail for non-manifest-unknown errors + logger?.debug?.(`Warning: Could not verify image push: ${errorMsg}`); + return; } - throw new Error(`Failed to push image ${imageRef}: ${msg}`); } } @@ -52,14 +288,14 @@ export async function pushDockerImage( function isPermissionError(errMsg: string): boolean { const errLower = errMsg.toLowerCase(); const permissionKeywords = [ - 'denied', - 'unauthorized', - 'forbidden', - 'insufficient_scope', - 'authentication required', - 'access forbidden', - 'permission denied', - 'requested access to the resource is denied', + "denied", + "unauthorized", + "forbidden", + "insufficient_scope", + "authentication required", + "access forbidden", + "permission denied", + "requested access to the resource is denied", ]; return permissionKeywords.some((keyword) => errLower.includes(keyword)); @@ -71,32 +307,82 @@ function isPermissionError(errMsg: string): boolean { export class PushPermissionError extends Error { constructor( public imageRef: string, - public originalError: Error + public originalError: Error, ) { super(`Permission denied pushing to ${imageRef}: ${originalError.message}`); - this.name = 'PushPermissionError'; + this.name = "PushPermissionError"; } } /** * Get Docker auth config from system - * This reads from ~/.docker/config.json + * This reads from ~/.docker/config.json and handles credential stores */ export function getDockerAuthConfig(): Record { - const dockerConfigPath = path.join( - os.homedir(), - '.docker', - 'config.json' - ); + const dockerConfigPath = path.join(os.homedir(), ".docker", "config.json"); if (!fs.existsSync(dockerConfigPath)) { return {}; } try { - const config = JSON.parse(fs.readFileSync(dockerConfigPath, 'utf-8')); - return config.auths || {}; + const config = JSON.parse(fs.readFileSync(dockerConfigPath, "utf-8")); + const auths = config.auths || {}; + + // If credsStore is set, credentials are stored in credential helper (e.g., osxkeychain) + // In this case, dockerode should handle auth automatically, but we still return + // the auths structure (even if empty) to indicate the registry is configured + if (config.credsStore) { + // Return auths as-is (may be empty objects, but registry is configured) + return auths; + } + + return auths; } catch { return {}; } } + +/** + * Get credentials from Docker credential helper + */ +async function getCredentialsFromHelper( + registry: string, +): Promise<{ username: string; password: string } | undefined> { + const dockerConfigPath = path.join(os.homedir(), ".docker", "config.json"); + + if (!fs.existsSync(dockerConfigPath)) { + return undefined; + } + + try { + const config = JSON.parse(fs.readFileSync(dockerConfigPath, "utf-8")); + const credsStore = config.credsStore; + + if (!credsStore) { + return undefined; + } + + // Use Docker credential helper to get credentials + // Format: docker-credential- get + const { execSync } = await import("child_process"); + const helper = `docker-credential-${credsStore}`; + + try { + const output = execSync(`echo "${registry}" | ${helper} get`, { + encoding: "utf-8", + }); + const creds = JSON.parse(output); + if (creds.Username && creds.Secret) { + return { username: creds.Username, password: creds.Secret }; + } + } catch { + // Credential helper failed, return undefined + return undefined; + } + } catch { + return undefined; + } + + return undefined; +} diff --git a/packages/sdk/src/client/modules/app/deploy/encryption/kms.ts b/packages/sdk/src/client/modules/app/deploy/encryption/kms.ts index 21da687..7d03c77 100644 --- a/packages/sdk/src/client/modules/app/deploy/encryption/kms.ts +++ b/packages/sdk/src/client/modules/app/deploy/encryption/kms.ts @@ -3,8 +3,34 @@ * Implements RSA-OAEP + AES-256-GCM encryption */ -import * as forge from 'node-forge'; -import { Buffer } from 'buffer'; +import { Buffer } from "buffer"; +import { createRequire } from "module"; + +// Import node-forge at runtime to work in both ESM and CJS +// This function will be called when encryption is needed +function getForge(): any { + try { + // Try ESM context first - use createRequire with import.meta.url + if (typeof import.meta !== "undefined" && import.meta.url) { + const requireFn = createRequire(import.meta.url); + return requireFn("node-forge"); + } + } catch { + // Fall through to CJS require + } + + // CJS context - use regular require + return require("node-forge"); +} + +// Cache the forge module +let forgeCache: any = null; +function getForgeCached(): any { + if (!forgeCache) { + forgeCache = getForge(); + } + return forgeCache; +} /** * Get app protected headers for encryption @@ -23,14 +49,15 @@ export function getAppProtectedHeaders(appID: string): Record { export function encryptRSAOAEPAndAES256GCM( encryptionKeyPEM: string | Buffer, plaintext: Buffer, - protectedHeaders: Record + // protectedHeaders: Record ): string { const pemString = - typeof encryptionKeyPEM === 'string' + typeof encryptionKeyPEM === "string" ? encryptionKeyPEM - : encryptionKeyPEM.toString('utf-8'); + : encryptionKeyPEM.toString("utf-8"); // Parse RSA public key from PEM + const forge = getForgeCached(); const publicKey = forge.pki.publicKeyFromPem(pemString); // Generate random AES-256 key (32 bytes) @@ -38,16 +65,16 @@ export function encryptRSAOAEPAndAES256GCM( const iv = forge.random.getBytesSync(12); // 96-bit IV for GCM // Encrypt plaintext with AES-256-GCM - const cipher = forge.cipher.createCipher('AES-GCM', aesKey); + const cipher = forge.cipher.createCipher("AES-GCM", aesKey); cipher.start({ iv }); - cipher.update(forge.util.createBuffer(plaintext.toString('binary'))); + cipher.update(forge.util.createBuffer(plaintext.toString("binary"))); cipher.finish(); const encrypted = cipher.output.getBytes(); const tag = cipher.mode.tag.getBytes(); // Encrypt AES key with RSA-OAEP - const encryptedAESKey = publicKey.encrypt(aesKey, 'RSA-OAEP'); + const encryptedAESKey = publicKey.encrypt(aesKey, "RSA-OAEP"); // Combine: RSA-encrypted key + IV + ciphertext + tag // Format: [encrypted_key_length (4 bytes)] [encrypted_key] [iv_length (4 bytes)] [iv] [ciphertext] [tag] @@ -60,15 +87,15 @@ export function encryptRSAOAEPAndAES256GCM( const combined = Buffer.concat([ encryptedKeyLength, - Buffer.from(encryptedKeyBytes, 'binary'), + Buffer.from(encryptedKeyBytes, "binary"), ivLength, - Buffer.from(iv, 'binary'), - Buffer.from(encrypted, 'binary'), - Buffer.from(tag, 'binary'), + Buffer.from(iv, "binary"), + Buffer.from(encrypted, "binary"), + Buffer.from(tag, "binary"), ]); // Base64 encode the result - return combined.toString('base64'); + return combined.toString("base64"); } /** @@ -76,9 +103,9 @@ export function encryptRSAOAEPAndAES256GCM( */ export function decryptRSAOAEPAndAES256GCM( privateKeyPEM: string, - encryptedData: string + encryptedData: string, ): Buffer { - const combined = Buffer.from(encryptedData, 'base64'); + const combined = Buffer.from(encryptedData, "base64"); // Extract components let offset = 0; @@ -96,37 +123,36 @@ export function decryptRSAOAEPAndAES256GCM( // Ciphertext is everything except the last 16 bytes (tag) const tagLength = 16; - const ciphertext = combined.subarray( - offset, - combined.length - tagLength - ); + const ciphertext = combined.subarray(offset, combined.length - tagLength); const tag = combined.subarray(combined.length - tagLength); // Decrypt AES key with RSA-OAEP + const forge = getForgeCached(); const privateKey = forge.pki.privateKeyFromPem(privateKeyPEM); const encryptedKeyBase64 = forge.util.encode64( - encryptedKey.toString('binary') + encryptedKey.toString("binary"), ); - const aesKey = privateKey.decrypt(encryptedKeyBase64, 'RSA-OAEP'); + const aesKey = privateKey.decrypt(encryptedKeyBase64, "RSA-OAEP"); // Decrypt plaintext with AES-256-GCM - const decipher = forge.cipher.createDecipher('AES-GCM', aesKey); - decipher.start({ iv: iv.toString('binary'), tag: toByteStringBuffer(tag) }); - decipher.update(forge.util.createBuffer(ciphertext.toString('binary'))); + const decipher = forge.cipher.createDecipher("AES-GCM", aesKey); + decipher.start({ iv: iv.toString("binary"), tag: toByteStringBuffer(tag) }); + decipher.update(forge.util.createBuffer(ciphertext.toString("binary"))); const success = decipher.finish(); if (!success) { - throw new Error('Decryption failed: authentication tag mismatch'); + throw new Error("Decryption failed: authentication tag mismatch"); } - return Buffer.from(decipher.output.getBytes(), 'binary'); + return Buffer.from(decipher.output.getBytes(), "binary"); } -function toByteStringBuffer(buf: ArrayBuffer | Uint8Array): forge.util.ByteStringBuffer { +function toByteStringBuffer(buf: ArrayBuffer | Uint8Array): any { + const forge = getForgeCached(); const u8 = buf instanceof Uint8Array ? buf : new Uint8Array(buf); // Efficient binary encode const binary = forge.util.binary.raw.encode(u8); - return forge.util.createBuffer(binary, 'raw'); + return forge.util.createBuffer(binary, "raw"); } diff --git a/packages/sdk/src/client/modules/app/deploy/index.ts b/packages/sdk/src/client/modules/app/deploy/index.ts index 6f2cbd8..80ba530 100644 --- a/packages/sdk/src/client/modules/app/deploy/index.ts +++ b/packages/sdk/src/client/modules/app/deploy/index.ts @@ -2,17 +2,9 @@ * Main Deploy entry point */ -export * from './types'; -export * from './deploy'; -export * from './config/environment'; -export * from './docker/build'; -export * from './docker/push'; -export * from './docker/inspect'; -export * from './encryption/kms'; -export * from './env/parser'; -export * from './registry/digest'; -export * from './contract/caller'; -export * from './contract/watcher'; -export * from './contract/userapi'; -export * from './contract/eip7702'; - +export * from "./deploy"; +export * from "./docker/build"; +export * from "./docker/push"; +export * from "./docker/inspect"; +export * from "./encryption/kms"; +export * from "./registry/digest"; diff --git a/packages/sdk/src/client/modules/app/deploy/registry/appNames.ts b/packages/sdk/src/client/modules/app/deploy/registry/appNames.ts new file mode 100644 index 0000000..47b5d50 --- /dev/null +++ b/packages/sdk/src/client/modules/app/deploy/registry/appNames.ts @@ -0,0 +1,97 @@ +/** + * App name registry + * + * Stores and retrieves app names (friendly names for app IDs) + */ + +import * as fs from "fs"; +import * as path from "path"; +import * as os from "os"; + +const CONFIG_DIR = path.join(os.homedir(), ".eigenx"); +const APP_NAMES_FILE = "app_names.json"; + +interface AppNames { + [environment: string]: { + [appID: string]: string; // appID -> name + }; +} + +/** + * Set app name for an environment + */ +export async function setAppName( + environment: string, + appID: string, + name: string, +): Promise { + // Ensure config directory exists + if (!fs.existsSync(CONFIG_DIR)) { + fs.mkdirSync(CONFIG_DIR, { recursive: true }); + } + + const filePath = path.join(CONFIG_DIR, APP_NAMES_FILE); + let appNames: AppNames = {}; + + // Load existing names + if (fs.existsSync(filePath)) { + try { + const content = fs.readFileSync(filePath, "utf-8"); + appNames = JSON.parse(content); + } catch { + // If file is corrupted, start fresh + appNames = {}; + } + } + + // Set the name + if (!appNames[environment]) { + appNames[environment] = {}; + } + appNames[environment][appID.toLowerCase()] = name; + + // Save back to file + fs.writeFileSync(filePath, JSON.stringify(appNames, null, 2)); +} + +/** + * Get app name for an environment + */ +export function getAppName(environment: string, appID: string): string { + const filePath = path.join(CONFIG_DIR, APP_NAMES_FILE); + if (!fs.existsSync(filePath)) { + return ""; + } + + try { + const content = fs.readFileSync(filePath, "utf-8"); + const appNames: AppNames = JSON.parse(content); + return appNames[environment]?.[appID.toLowerCase()] || ""; + } catch { + return ""; + } +} + +/** + * List all apps for an environment + */ +export function listApps(environment: string): Record { + const filePath = path.join(CONFIG_DIR, APP_NAMES_FILE); + if (!fs.existsSync(filePath)) { + return {}; + } + + try { + const content = fs.readFileSync(filePath, "utf-8"); + const appNames: AppNames = JSON.parse(content); + // Invert the mapping: name -> appID + const result: Record = {}; + const envApps = appNames[environment] || {}; + for (const [appID, name] of Object.entries(envApps)) { + result[name] = appID; + } + return result; + } catch { + return {}; + } +} diff --git a/packages/sdk/src/client/modules/app/deploy/registry/digest.ts b/packages/sdk/src/client/modules/app/deploy/registry/digest.ts index f7366e4..b44d160 100644 --- a/packages/sdk/src/client/modules/app/deploy/registry/digest.ts +++ b/packages/sdk/src/client/modules/app/deploy/registry/digest.ts @@ -1,13 +1,13 @@ /** * Image registry operations - digest extraction - * + * * Uses Docker API to extract image digest and validate platform */ -import * as child_process from 'child_process'; -import { promisify } from 'util'; -import { ImageDigestResult } from '../types'; -import { DOCKER_PLATFORM, SHA256_PREFIX } from '../constants'; +import * as child_process from "child_process"; +import { promisify } from "util"; +import { ImageDigestResult } from "../../../../common/types"; +import { DOCKER_PLATFORM } from "../constants"; const exec = promisify(child_process.exec); @@ -34,7 +34,7 @@ interface Manifest { * Uses docker manifest inspect to get the manifest */ export async function getImageDigestAndName( - imageRef: string + imageRef: string, ): Promise { try { // Use docker manifest inspect to get the manifest @@ -53,7 +53,7 @@ export async function getImageDigestAndName( } } catch (error: any) { throw new Error( - `Failed to get image digest for ${imageRef}: ${error.message}` + `Failed to get image digest for ${imageRef}: ${error.message}`, ); } } @@ -63,7 +63,7 @@ export async function getImageDigestAndName( */ function extractDigestFromMultiPlatform( manifest: Manifest, - imageRef: string + imageRef: string, ): ImageDigestResult { if (!manifest.manifests) { throw new Error(`Invalid manifest for ${imageRef}: no manifests found`); @@ -98,7 +98,7 @@ function extractDigestFromMultiPlatform( */ async function extractDigestFromSinglePlatform( manifest: Manifest, - imageRef: string + imageRef: string, ): Promise { // For single-platform images, we need to get the config digest // and then inspect the image to get platform info @@ -115,7 +115,7 @@ async function extractDigestFromSinglePlatform( const config = inspectData[0].Architecture ? { - os: inspectData[0].Os || 'linux', + os: inspectData[0].Os || "linux", architecture: inspectData[0].Architecture, } : null; @@ -160,11 +160,11 @@ async function extractDigestFromSinglePlatform( // Platform mismatch throw createPlatformErrorMessage(imageRef, [platform]); } catch (error: any) { - if (error.message.includes('platform')) { + if (error.message.includes("platform")) { throw error; } throw new Error( - `Failed to extract digest from single-platform image ${imageRef}: ${error.message}` + `Failed to extract digest from single-platform image ${imageRef}: ${error.message}`, ); } } @@ -175,17 +175,15 @@ async function extractDigestFromSinglePlatform( function hexStringToBytes32(hexStr: string): Uint8Array { // Remove "sha256:" prefix if present let cleanHex = hexStr; - if (hexStr.includes(':')) { - cleanHex = hexStr.split(':')[1]; + if (hexStr.includes(":")) { + cleanHex = hexStr.split(":")[1]; } // Decode hex string - const bytes = Buffer.from(cleanHex, 'hex'); + const bytes = Buffer.from(cleanHex, "hex"); if (bytes.length !== 32) { - throw new Error( - `Digest must be exactly 32 bytes, got ${bytes.length}` - ); + throw new Error(`Digest must be exactly 32 bytes, got ${bytes.length}`); } return new Uint8Array(bytes); @@ -196,7 +194,7 @@ function hexStringToBytes32(hexStr: string): Uint8Array { * Format: "repo@sha256:xxxxx" -> returns 32-byte digest */ function extractDigestFromRepoDigest(repoDigest: string): Uint8Array { - const prefix = '@sha256:'; + const prefix = "@sha256:"; const idx = repoDigest.lastIndexOf(prefix); if (idx === -1) { throw new Error(`Invalid repo digest format: ${repoDigest}`); @@ -213,19 +211,19 @@ function extractDigestFromRepoDigest(repoDigest: string): Uint8Array { function extractRegistryName(imageRef: string): string { // Remove tag if present let name = imageRef; - const tagIndex = name.lastIndexOf(':'); - if (tagIndex !== -1 && !name.substring(tagIndex + 1).includes('/')) { + const tagIndex = name.lastIndexOf(":"); + if (tagIndex !== -1 && !name.substring(tagIndex + 1).includes("/")) { name = name.substring(0, tagIndex); } // Remove digest if present - const digestIndex = name.indexOf('@'); + const digestIndex = name.indexOf("@"); if (digestIndex !== -1) { name = name.substring(0, digestIndex); } // Extract registry (everything before last /) - const lastSlash = name.lastIndexOf('/'); + const lastSlash = name.lastIndexOf("/"); if (lastSlash === -1) { // No registry, just image name (e.g., "nginx") return name; @@ -233,7 +231,7 @@ function extractRegistryName(imageRef: string): string { // Check if it's a registry (contains . or : before the last /) const beforeLastSlash = name.substring(0, lastSlash); - if (beforeLastSlash.includes('.') || beforeLastSlash.includes(':')) { + if (beforeLastSlash.includes(".") || beforeLastSlash.includes(":")) { // This is a registry return beforeLastSlash; } @@ -247,12 +245,12 @@ function extractRegistryName(imageRef: string): string { */ function createPlatformErrorMessage( imageRef: string, - platforms: string[] + platforms: string[], ): Error { const errorMsg = `ecloud requires linux/amd64 images for TEE deployment. Image: ${imageRef} -Found platform(s): ${platforms.join(', ')} +Found platform(s): ${platforms.join(", ")} Required platform: ${DOCKER_PLATFORM} To fix this issue: diff --git a/packages/sdk/src/client/modules/app/deploy/release/prepare.ts b/packages/sdk/src/client/modules/app/deploy/release/prepare.ts index 278ae6c..cc98c59 100644 --- a/packages/sdk/src/client/modules/app/deploy/release/prepare.ts +++ b/packages/sdk/src/client/modules/app/deploy/release/prepare.ts @@ -1,18 +1,20 @@ /** * Release preparation - * + * * This module handles building/layering images, encrypting environment variables, * and creating the release struct. */ -import { Release, EnvironmentConfig, Logger } from '../types'; -import { buildAndPushLayeredImage } from '../docker/layer'; -import { layerRemoteImageIfNeeded } from '../docker/layer'; -import { getImageDigestAndName } from '../registry/digest'; -import { parseAndValidateEnvFile } from '../env/parser'; -import { encryptRSAOAEPAndAES256GCM, getAppProtectedHeaders } from '../encryption/kms'; -import { getKMSKeysForEnvironment } from '../utils/keys'; -import { REGISTRY_PROPAGATION_WAIT_SECONDS } from '../constants'; +import { buildAndPushLayeredImage } from "../docker/layer"; +import { layerRemoteImageIfNeeded } from "../docker/layer"; +import { getImageDigestAndName } from "../registry/digest"; +import { encryptRSAOAEPAndAES256GCM } from "../encryption/kms"; // getAppProtectedHeaders +import { getKMSKeysForEnvironment } from "../utils/keys"; +import { REGISTRY_PROPAGATION_WAIT_SECONDS } from "../constants"; + +import { parseAndValidateEnvFile } from "../../../../common/env/parser"; + +import { Release, EnvironmentConfig, Logger } from "../../../../common/types"; export interface PrepareReleaseOptions { dockerfilePath?: string; @@ -34,7 +36,7 @@ export interface PrepareReleaseResult { */ export async function prepareRelease( options: PrepareReleaseOptions, - logger: Logger + logger: Logger, ): Promise { const { dockerfilePath, @@ -43,7 +45,6 @@ export async function prepareRelease( logRedirect, instanceType, environmentConfig, - appID, } = options; let finalImageRef = imageRef; @@ -51,7 +52,7 @@ export async function prepareRelease( // 1. Build/layer image if needed if (dockerfilePath) { // Build from Dockerfile - logger.info('Building and pushing layered image...'); + logger.info("Building and pushing layered image..."); finalImageRef = await buildAndPushLayeredImage( { dockerfilePath, @@ -60,19 +61,19 @@ export async function prepareRelease( envFilePath, environmentConfig, }, - logger + logger, ); // Wait for registry propagation logger.info( - `Waiting ${REGISTRY_PROPAGATION_WAIT_SECONDS} seconds for registry propagation...` + `Waiting ${REGISTRY_PROPAGATION_WAIT_SECONDS} seconds for registry propagation...`, ); await new Promise((resolve) => - setTimeout(resolve, REGISTRY_PROPAGATION_WAIT_SECONDS * 1000) + setTimeout(resolve, REGISTRY_PROPAGATION_WAIT_SECONDS * 1000), ); } else { // Layer remote image if needed - logger.info('Checking if image needs layering...'); + logger.info("Checking if image needs layering..."); finalImageRef = await layerRemoteImageIfNeeded( { imageRef, @@ -80,52 +81,88 @@ export async function prepareRelease( envFilePath, environmentConfig, }, - logger + logger, ); // Wait for registry propagation if image was layered if (finalImageRef !== imageRef) { logger.info( - `Waiting ${REGISTRY_PROPAGATION_WAIT_SECONDS} seconds for registry propagation...` + `Waiting ${REGISTRY_PROPAGATION_WAIT_SECONDS} seconds for registry propagation...`, ); await new Promise((resolve) => - setTimeout(resolve, REGISTRY_PROPAGATION_WAIT_SECONDS * 1000) + setTimeout(resolve, REGISTRY_PROPAGATION_WAIT_SECONDS * 1000), ); } } - // 2. Get image digest and registry name - logger.info('Extracting image digest...'); - const { digest, registry } = await getImageDigestAndName(finalImageRef); - logger.info(`Image digest: ${Buffer.from(digest).toString('hex')}`); + // 2. Wait a moment for registry to process the push (especially for GHCR) + logger.info("Waiting for registry to process image..."); + await new Promise((resolve) => setTimeout(resolve, 2000)); + + // 3. Get image digest and registry name + logger.info("Extracting image digest..."); + let digest: Uint8Array | undefined; + let registry: string | undefined; + + // Retry getting digest in case registry needs more time + let retries = 3; + let lastError: Error | null = null; + + while (retries > 0) { + try { + const result = await getImageDigestAndName(finalImageRef); + digest = result.digest; + registry = result.registry; + break; + } catch (error: any) { + lastError = error; + retries--; + if (retries > 0) { + logger.info( + `Digest extraction failed, retrying in 2 seconds... (${retries} retries left)`, + ); + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + } + } + + if (!digest || !registry) { + throw new Error( + `Failed to get image digest after retries. This usually means the image wasn't pushed successfully.\n` + + `Original error: ${lastError?.message}\n` + + `Please verify the image exists: docker manifest inspect ${finalImageRef}`, + ); + } + + logger.info(`Image digest: ${Buffer.from(digest).toString("hex")}`); logger.info(`Registry: ${registry}`); - // 3. Parse and validate environment file + // 4. Parse and validate environment file let publicEnv: Record = {}; let privateEnv: Record = {}; if (envFilePath) { - logger.info('Parsing environment file...'); + logger.info("Parsing environment file..."); const parsed = parseAndValidateEnvFile(envFilePath); publicEnv = parsed.public; privateEnv = parsed.private; } else { - logger.info('Continuing without environment file'); + logger.info("Continuing without environment file"); } // 4. Add instance type to public env - publicEnv['EIGEN_MACHINE_TYPE'] = instanceType; + publicEnv["EIGEN_MACHINE_TYPE"] = instanceType; logger.info(`Instance type: ${instanceType}`); // 5. Encrypt private environment variables - logger.info('Encrypting environment variables...'); + logger.info("Encrypting environment variables..."); const { encryptionKey } = getKMSKeysForEnvironment(environmentConfig.name); - const protectedHeaders = getAppProtectedHeaders(appID); + // const protectedHeaders = getAppProtectedHeaders(appID); const privateEnvBytes = Buffer.from(JSON.stringify(privateEnv)); const encryptedEnvStr = encryptRSAOAEPAndAES256GCM( encryptionKey, privateEnvBytes, - protectedHeaders + // protectedHeaders ); // 6. Create release struct @@ -148,4 +185,3 @@ export async function prepareRelease( finalImageRef, }; } - diff --git a/packages/sdk/src/client/modules/app/deploy/templates/dockerfileTemplate.ts b/packages/sdk/src/client/modules/app/deploy/templates/dockerfileTemplate.ts index 675f24c..295ad64 100644 --- a/packages/sdk/src/client/modules/app/deploy/templates/dockerfileTemplate.ts +++ b/packages/sdk/src/client/modules/app/deploy/templates/dockerfileTemplate.ts @@ -1,8 +1,8 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import Handlebars from 'handlebars'; -import { LAYERED_DOCKERFILE_TEMPLATE_PATH } from '../constants'; -import { getDirname } from '../utils/dirname'; +import * as fs from "fs"; +import * as path from "path"; +import Handlebars from "handlebars"; +import { LAYERED_DOCKERFILE_TEMPLATE_PATH } from "../constants"; +import { getDirname } from "../../../../common/utils/dirname"; const __dirname = getDirname(); @@ -19,22 +19,34 @@ export interface DockerfileTemplateData { * Process Dockerfile template */ export function processDockerfileTemplate( - data: DockerfileTemplateData + data: DockerfileTemplateData, ): string { - // TODO: Load template from embedded files or file system - // For now, return a basic template - const templatePath = path.join( - __dirname, - '../../templates', - LAYERED_DOCKERFILE_TEMPLATE_PATH - ); + // Try multiple paths to support both CLI (bundled) and standalone SDK usage + const possiblePaths = [ + path.join(__dirname, "./templates", LAYERED_DOCKERFILE_TEMPLATE_PATH), // Standalone SDK + path.join(__dirname, "../../templates", LAYERED_DOCKERFILE_TEMPLATE_PATH), // CLI bundled + path.join( + __dirname, + "../../../templates", + LAYERED_DOCKERFILE_TEMPLATE_PATH, + ), // Alternative CLI path + ]; - if (!fs.existsSync(templatePath)) { - throw new Error(`Dockerfile template not found at ${templatePath}`); + let templatePath: string | null = null; + for (const possiblePath of possiblePaths) { + if (fs.existsSync(possiblePath)) { + templatePath = possiblePath; + break; + } } - const templateContent = fs.readFileSync(templatePath, 'utf-8'); + if (!templatePath) { + throw new Error( + `Dockerfile template not found. Tried: ${possiblePaths.join(", ")}`, + ); + } + + const templateContent = fs.readFileSync(templatePath, "utf-8"); const template = Handlebars.compile(templateContent); return template(data); } - diff --git a/packages/sdk/src/client/modules/app/deploy/templates/scriptTemplate.ts b/packages/sdk/src/client/modules/app/deploy/templates/scriptTemplate.ts index 9616ee5..ef1580e 100644 --- a/packages/sdk/src/client/modules/app/deploy/templates/scriptTemplate.ts +++ b/packages/sdk/src/client/modules/app/deploy/templates/scriptTemplate.ts @@ -1,38 +1,43 @@ -/** - * Script template processing - */ - -import * as fs from 'fs'; -import * as path from 'path'; -import Handlebars from 'handlebars'; -import { ENV_SOURCE_SCRIPT_TEMPLATE_PATH } from '../constants'; -import { getDirname } from '../utils/dirname'; +import * as fs from "fs"; +import * as path from "path"; +import Handlebars from "handlebars"; +import { ENV_SOURCE_SCRIPT_TEMPLATE_PATH } from "../constants"; +import { getDirname } from "../../../../common/utils/dirname"; const __dirname = getDirname(); export interface ScriptTemplateData { - KMSServerURL: string; - JWTFile: string; - UserAPIURL: string; + kmsServerURL: string; + jwtFile: string; + userAPIURL: string; } /** * Process script template */ export function processScriptTemplate(data: ScriptTemplateData): string { - // Load template from embedded files or file system - const templatePath = path.join( - __dirname, - '../../templates', - ENV_SOURCE_SCRIPT_TEMPLATE_PATH - ); + // Try multiple paths to support both CLI (bundled) and standalone SDK usage + const possiblePaths = [ + path.join(__dirname, "./templates", ENV_SOURCE_SCRIPT_TEMPLATE_PATH), // Standalone SDK + path.join(__dirname, "../../templates", ENV_SOURCE_SCRIPT_TEMPLATE_PATH), // CLI bundled + path.join(__dirname, "../../../templates", ENV_SOURCE_SCRIPT_TEMPLATE_PATH), // Alternative CLI path + ]; - if (!fs.existsSync(templatePath)) { - throw new Error(`Script template not found at ${templatePath}`); + let templatePath: string | null = null; + for (const possiblePath of possiblePaths) { + if (fs.existsSync(possiblePath)) { + templatePath = possiblePath; + break; + } } - const templateContent = fs.readFileSync(templatePath, 'utf-8'); + if (!templatePath) { + throw new Error( + `Script template not found. Tried: ${possiblePaths.join(", ")}`, + ); + } + + const templateContent = fs.readFileSync(templatePath, "utf-8"); const template = Handlebars.compile(templateContent); return template(data); } - diff --git a/packages/sdk/src/client/modules/app/deploy/utils/dirname.ts b/packages/sdk/src/client/modules/app/deploy/utils/dirname.ts deleted file mode 100644 index 06e677a..0000000 --- a/packages/sdk/src/client/modules/app/deploy/utils/dirname.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as path from 'path'; -import { fileURLToPath } from 'url'; - -/** - * Get __dirname equivalent for ES modules - * For CJS builds, tsup should handle the transformation - */ -export function getDirname(): string { - // Use import.meta.url for ESM (CLI uses ESM) - // @ts-expect-error - import.meta is only available in ESM, but we need it for CLI - return path.dirname(fileURLToPath(import.meta.url)); -} - diff --git a/packages/sdk/src/client/modules/app/deploy/utils/keys.ts b/packages/sdk/src/client/modules/app/deploy/utils/keys.ts index 686042c..bbe5725 100644 --- a/packages/sdk/src/client/modules/app/deploy/utils/keys.ts +++ b/packages/sdk/src/client/modules/app/deploy/utils/keys.ts @@ -2,42 +2,61 @@ * KMS key loading utilities */ -import * as fs from 'fs'; -import * as path from 'path'; -import { getDirname } from './dirname'; +import * as fs from "fs"; +import * as path from "path"; +import { getDirname } from "../../../../common/utils/dirname"; const __dirname = getDirname(); -const KEYS_BASE_PATH = path.join(__dirname, '../../keys'); + +// Try multiple paths to support both CLI (bundled) and standalone SDK usage +function findKeysBasePath(): string { + const possiblePaths = [ + path.join(__dirname, "./keys"), // Standalone SDK + path.join(__dirname, "../../keys"), // CLI bundled + path.join(__dirname, "../../../keys"), // Alternative CLI path + ]; + + for (const possiblePath of possiblePaths) { + if (fs.existsSync(possiblePath)) { + return possiblePath; + } + } + + // Return the most likely path for error messages + return path.join(__dirname, "../../keys"); +} + +const KEYS_BASE_PATH = findKeysBasePath(); /** * Get KMS keys for environment */ export function getKMSKeysForEnvironment( environment: string, - build: 'dev' | 'prod' = 'dev' + build: "dev" | "prod" = "dev", ): { encryptionKey: Buffer; signingKey: Buffer } { const encryptionPath = path.join( KEYS_BASE_PATH, environment, build, - 'kms-encryption-public-key.pem' + "kms-encryption-public-key.pem", ); const signingPath = path.join( KEYS_BASE_PATH, environment, build, - 'kms-signing-public-key.pem' + "kms-signing-public-key.pem", ); if (!fs.existsSync(encryptionPath)) { throw new Error( - `Encryption key not found at ${encryptionPath}. Keys must be embedded or provided.` + `Encryption key not found at ${encryptionPath}. Keys must be embedded or provided.`, ); } if (!fs.existsSync(signingPath)) { throw new Error( - `Signing key not found at ${signingPath}. Keys must be embedded or provided.` + `Signing key not found at ${signingPath}. Keys must be embedded or provided.`, ); } @@ -52,21 +71,20 @@ export function getKMSKeysForEnvironment( */ export function keysExistForEnvironment( environment: string, - build: 'dev' | 'prod' = 'dev' + build: "dev" | "prod" = "dev", ): boolean { const encryptionPath = path.join( KEYS_BASE_PATH, environment, build, - 'kms-encryption-public-key.pem' + "kms-encryption-public-key.pem", ); const signingPath = path.join( KEYS_BASE_PATH, environment, build, - 'kms-signing-public-key.pem' + "kms-signing-public-key.pem", ); return fs.existsSync(encryptionPath) && fs.existsSync(signingPath); } - diff --git a/packages/sdk/src/client/modules/app/deploy/utils/preflight.ts b/packages/sdk/src/client/modules/app/deploy/utils/preflight.ts new file mode 100644 index 0000000..cb2c4b7 --- /dev/null +++ b/packages/sdk/src/client/modules/app/deploy/utils/preflight.ts @@ -0,0 +1,122 @@ +/** + * Preflight checks + */ + +import { Address, createPublicClient, http, PrivateKeyAccount } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; + +import { getEnvironmentConfig } from "../../../../common/config/environment"; + +import { Logger, EnvironmentConfig } from "../../../../common/types"; + +export interface PreflightContext { + privateKey: string; + rpcUrl: string; + environmentConfig: EnvironmentConfig; + account: PrivateKeyAccount; + selfAddress: Address; +} + +/** + * Do preflight checks - performs early validation of authentication and network connectivity + */ +export async function doPreflightChecks( + options: Partial<{ + privateKey?: string; + rpcUrl?: string; + environment?: string; + }>, + logger: Logger, +): Promise { + // 1. Get and validate private key first (fail fast) + logger.debug("Checking authentication..."); + const privateKey = await getPrivateKeyOrFail(options.privateKey); + + // 2. Get environment configuration + logger.debug("Determining environment..."); + const environmentConfig = getEnvironmentConfig( + options.environment || "sepolia", + ); + + // 3. Get RPC URL (from option, env var, or environment default) + let rpcUrl = options.rpcUrl; + if (!rpcUrl) { + rpcUrl = process.env.RPC_URL ?? environmentConfig.defaultRPCURL; + } + if (!rpcUrl) { + throw new Error( + `RPC URL is required. Provide via options.rpcUrl, RPC_URL env var, or ensure environment has default RPC URL`, + ); + } + + // 4. Test network connectivity + logger.debug("Testing network connectivity..."); + const publicClient = createPublicClient({ + transport: http(rpcUrl), + }); + + try { + // 5. Get chain ID + const chainID = await publicClient.getChainId(); + if (BigInt(chainID) !== environmentConfig.chainID) { + throw new Error( + `Chain ID mismatch: expected ${environmentConfig.chainID}, got ${chainID}`, + ); + } + } catch (err: any) { + throw new Error( + `Cannot connect to ${environmentConfig.name} RPC at ${rpcUrl}: ${err.message}`, + ); + } + + // 6. Create account from private key + const privateKeyHex = privateKey.startsWith("0x") + ? (privateKey as `0x${string}`) + : (`0x${privateKey}` as `0x${string}`); + const account = privateKeyToAccount(privateKeyHex); + const selfAddress = account.address; + + return { + privateKey: privateKeyHex, + rpcUrl, + environmentConfig, + account, + selfAddress, + }; +} + +/** + * Get private key from options, environment variable, or keyring + */ +async function getPrivateKeyOrFail(privateKey?: string): Promise { + // Check option first + if (privateKey) { + validatePrivateKey(privateKey); + return privateKey; + } + + // Check environment variable + if (process.env.PRIVATE_KEY) { + validatePrivateKey(process.env.PRIVATE_KEY); + return process.env.PRIVATE_KEY; + } + + // TODO: Check keyring (OS keyring integration) + // For now, throw error with instructions + throw new Error( + `private key required. Please provide it via: + • Option: privateKey in deploy options + • Environment: export PRIVATE_KEY=YOUR_KEY + • Keyring: (not yet implemented)`, + ); +} + +/** + * Validate private key format + */ +function validatePrivateKey(key: string): void { + const cleaned = key.startsWith("0x") ? key.substring(2) : key; + if (!/^[0-9a-fA-F]{64}$/.test(cleaned)) { + throw new Error("Invalid private key format (must be 64 hex characters)"); + } +} diff --git a/packages/sdk/src/client/modules/app/deploy/utils/prompts.ts b/packages/sdk/src/client/modules/app/deploy/utils/prompts.ts new file mode 100644 index 0000000..2dfd27f --- /dev/null +++ b/packages/sdk/src/client/modules/app/deploy/utils/prompts.ts @@ -0,0 +1,592 @@ +/** + * Interactive prompts using @inquirer/prompts + */ + +import { input, select } from "@inquirer/prompts"; +import fs from "fs"; +import path from "path"; +import os from "os"; +import { listApps } from "../registry/appNames"; + +/** + * Prompt for Dockerfile selection + */ +export async function getDockerfileInteractive( + dockerfilePath?: string, +): Promise { + // Check if provided via option + if (dockerfilePath) { + return dockerfilePath; + } + + // Check if default Dockerfile exists + if (!fs.existsSync("Dockerfile")) { + // No Dockerfile found, return empty string (deploy existing image) + return ""; + } + + // Interactive prompt when Dockerfile exists + console.log("\nFound Dockerfile in current directory."); + + const choice = await select({ + message: "Choose deployment method:", + choices: [ + { name: "Build and deploy from Dockerfile", value: "build" }, + { name: "Deploy existing image from registry", value: "existing" }, + ], + }); + + switch (choice) { + case "build": + return "Dockerfile"; + case "existing": + return ""; + default: + throw new Error(`Unexpected choice: ${choice}`); + } +} + +/** + * Prompt for image reference + */ +export async function getImageReferenceInteractive( + imageRef?: string, + buildFromDockerfile: boolean = false, +): Promise { + // Check if provided + if (imageRef) { + return imageRef; + } + + // Get available registries + const registries = await getAvailableRegistries(); + const appName = getDefaultAppName(); + + // Interactive prompt + if (buildFromDockerfile) { + console.log("\n📦 Build & Push Configuration"); + console.log("Your Docker image will be built and pushed to a registry"); + console.log("so that EigenX can pull and run it in the TEE."); + console.log(); + + if (registries.length > 0) { + displayDetectedRegistries(registries, appName); + return selectRegistryInteractive(registries, appName, "latest"); + } + + // No registries detected + displayAuthenticationInstructions(); + } else { + console.log("\n🐳 Docker Image Selection"); + console.log( + "Specify an existing Docker image from a registry to run in the TEE.", + ); + console.log(); + } + + // Fallback to manual input + displayRegistryExamples(appName); + + const imageRefInput = await input({ + message: "Enter Docker image reference:", + default: "", + validate: validateImageReference, + }); + + return imageRefInput; +} + +/** + * Prompt for app name + */ +export async function getOrPromptAppName( + appName: string | undefined, + environment: string, + imageRef: string, +): Promise { + // Check if provided + if (appName) { + // Validate the provided name + validateAppName(appName); + // Check if it's available + if (isAppNameAvailable(environment, appName)) { + return appName; + } + console.log(`Warning: App name '${appName}' is already taken.`); + return getAvailableAppNameInteractive(environment, imageRef); + } + + // No name provided, get interactively + return getAvailableAppNameInteractive(environment, imageRef); +} + +/** + * Get available app name interactively + */ +async function getAvailableAppNameInteractive( + environment: string, + imageRef: string, +): Promise { + // Start with a suggestion from the image + const baseName = extractAppNameFromImage(imageRef); + const suggestedName = findAvailableName(environment, baseName); + + while (true) { + console.log("\nApp name selection:"); + const name = await input({ + message: "Enter app name:", + default: suggestedName, + validate: (value: string) => { + try { + validateAppName(value); + return true; + } catch (err: any) { + return err.message; + } + }, + }); + + // Check if the name is available + if (isAppNameAvailable(environment, name)) { + return name; + } + + // Name is taken, suggest alternatives and loop + console.log(`App name '${name}' is already taken.`); + const newSuggested = findAvailableName(environment, name); + console.log(`Suggested alternative: ${newSuggested}`); + } +} + +/** + * Prompt for environment file + */ +export async function getEnvFileInteractive( + envFilePath?: string, +): Promise { + // Check if provided via option and exists + if (envFilePath && fs.existsSync(envFilePath)) { + return envFilePath; + } + + // Check if default .env exists + if (fs.existsSync(".env")) { + return ".env"; + } + + // Interactive prompt when env file doesn't exist + console.log("\nEnvironment file not found."); + console.log("Environment files contain variables like RPC_URL, etc."); + + const choice = await select({ + message: "Choose an option:", + choices: [ + { name: "Enter path to existing env file", value: "enter" }, + { name: "Continue without env file", value: "continue" }, + ], + }); + + switch (choice) { + case "enter": + const envFile = await input({ + message: "Enter environment file path:", + default: "", + validate: validateFilePath, + }); + return envFile; + case "continue": + return ""; + default: + throw new Error(`Unexpected choice: ${choice}`); + } +} + +/** + * Prompt for instance type + */ +export async function getInstanceTypeInteractive( + instanceType: string | undefined, + defaultSKU: string, + availableTypes: Array<{ sku: string; Description: string }>, +): Promise { + // Check if provided and validate it + if (instanceType) { + return validateInstanceTypeSKU(instanceType, availableTypes); + } + + // Determine default SKU if not provided + const isCurrentType = defaultSKU !== ""; + if (defaultSKU === "" && availableTypes.length > 0) { + defaultSKU = availableTypes[0].sku; // Use first from backend as default + } + + // No option provided - show interactive prompt + return selectInstanceTypeInteractively( + availableTypes, + defaultSKU, + isCurrentType, + ); +} + +/** + * Prompt for log settings + */ +export async function getLogSettingsInteractive( + logVisibility?: "public" | "private" | "off", +): Promise<{ logRedirect: string; publicLogs: boolean }> { + // Check if flag is provided + if (logVisibility) { + switch (logVisibility) { + case "public": + return { logRedirect: "always", publicLogs: true }; + case "private": + return { logRedirect: "always", publicLogs: false }; + case "off": + return { logRedirect: "", publicLogs: false }; + default: + throw new Error( + `invalid log-visibility value: ${logVisibility} (must be public, private, or off)`, + ); + } + } + + // Interactive prompt with three options + const choice = await select({ + message: "Do you want to view your app's logs?", + choices: [ + { + name: "Yes, but only viewable by app and platform admins", + value: "private", + }, + { name: "Yes, publicly viewable by anyone", value: "public" }, + { name: "No, disable logs entirely", value: "off" }, + ], + }); + + switch (choice) { + case "private": + return { logRedirect: "always", publicLogs: false }; + case "public": + return { logRedirect: "always", publicLogs: true }; + case "off": + return { logRedirect: "", publicLogs: false }; + default: + throw new Error(`Unexpected choice: ${choice}`); + } +} + +// Helper functions + +interface RegistryInfo { + Type: string; + Username: string; + URL: string; +} + +async function getAvailableRegistries(): Promise { + const dockerConfigPath = path.join(os.homedir(), ".docker", "config.json"); + + if (!fs.existsSync(dockerConfigPath)) { + return []; + } + + try { + const configContent = fs.readFileSync(dockerConfigPath, "utf-8"); + const config = JSON.parse(configContent); + + // Docker config structure: + // { + // "auths": { + // "https://index.docker.io/v1/": { "username": "...", "auth": "..." }, + // "ghcr.io": { "username": "...", "auth": "..." }, + // ... + // } + // } + const auths = config.auths || {}; + const gcrProjects = new Map(); + const registries: RegistryInfo[] = []; + + for (const [registry, auth] of Object.entries(auths)) { + const authData = auth as { username?: string; auth?: string }; + if (!authData.username) { + continue; + } + + const info: RegistryInfo = { + URL: registry, + Username: authData.username, + Type: "other", + }; + + // Determine registry type + if ( + registry.includes("index.docker.io") || + registry.includes("docker.io") + ) { + // Skip access-token and refresh-token entries for Docker Hub + if ( + registry.includes("access-token") || + registry.includes("refresh-token") + ) { + continue; + } + info.Type = "dockerhub"; + info.URL = "https://index.docker.io/v1/"; // Normalize + } else if (registry.includes("ghcr.io")) { + info.Type = "ghcr"; + } else if (registry.includes("gcr.io") || registry.includes(".gcr.io")) { + info.Type = "gcr"; + // Deduplicate GCR registries - regional endpoints point to same storage + if (!gcrProjects.has(authData.username)) { + info.URL = "gcr.io"; // Normalize to canonical URL + gcrProjects.set(authData.username, info); + } + continue; // Skip adding now, add deduplicated later + } + + registries.push(info); + } + + // Add deduplicated GCR entries + for (const gcrInfo of gcrProjects.values()) { + registries.push(gcrInfo); + } + + // Sort registries with Docker Hub first + registries.sort((a, b) => { + if (a.Type === "dockerhub") return -1; + if (b.Type === "dockerhub") return 1; + return a.Type.localeCompare(b.Type); + }); + + return registries; + } catch { + // If config is invalid or can't be read, return empty array + return []; + } +} + +function displayDetectedRegistries( + registries: RegistryInfo[], + appName: string, +): void { + console.log("Detected authenticated registries:"); + for (const reg of registries) { + const suggestion = suggestImageReference(reg, appName, "latest"); + console.log(` ${reg.Type}: ${suggestion}`); + } + console.log(); +} + +function displayAuthenticationInstructions(): void { + console.log("No authenticated registries detected."); + console.log("To authenticate:"); + console.log(" docker login "); + console.log(); +} + +function displayRegistryExamples(appName: string): void { + console.log("Examples:"); + console.log(` docker.io/${appName.toLowerCase()}:latest`); + console.log(` ghcr.io/username/${appName.toLowerCase()}:latest`); + console.log(` gcr.io/project-id/${appName.toLowerCase()}:latest`); + console.log(); +} + +async function selectRegistryInteractive( + registries: RegistryInfo[], + imageName: string, + tag: string, +): Promise { + if (registries.length === 1) { + // Single registry - suggest it as default + const defaultRef = suggestImageReference(registries[0], imageName, tag); + return input({ + message: "Enter image reference:", + default: defaultRef, + validate: validateImageReference, + }); + } + + // Multiple registries - let user choose + const choices = registries.map((reg) => ({ + name: suggestImageReference(reg, imageName, tag), + value: suggestImageReference(reg, imageName, tag), + })); + choices.push({ name: "Enter custom image reference", value: "custom" }); + + const choice = await select({ + message: "Select image destination:", + choices, + }); + + if (choice === "custom") { + return input({ + message: "Enter image reference:", + default: "", + validate: validateImageReference, + }); + } + + return choice; +} + +function suggestImageReference( + registry: RegistryInfo, + imageName: string, + tag: string, +): string { + // Clean up image name for use in image reference + imageName = imageName.toLowerCase().replace(/_/g, "-"); + + // Default to latest if no tag provided + if (!tag) { + tag = "latest"; + } + + switch (registry.Type) { + case "dockerhub": + return `${registry.Username}/${imageName}:${tag}`; + case "ghcr": + return `ghcr.io/${registry.Username}/${imageName}:${tag}`; + case "gcr": + return `gcr.io/${registry.Username}/${imageName}:${tag}`; + default: + // For other registries, try to construct a reasonable default + let host = registry.URL; + if (host.startsWith("https://")) { + host = host.substring(8); + } else if (host.startsWith("http://")) { + host = host.substring(7); + } + host = host.replace(/\/$/, ""); + return `${host}/${registry.Username}/${imageName}:${tag}`; + } +} + +function getDefaultAppName(): string { + try { + return path.basename(process.cwd()); + } catch { + return "myapp"; + } +} + +function extractAppNameFromImage(imageRef: string): string { + // Remove registry prefix if present + const parts = imageRef.split("/"); + let imageName = parts.length > 1 ? parts[parts.length - 1] : imageRef; + + // Split image and tag + if (imageName.includes(":")) { + imageName = imageName.split(":")[0]; + } + + return imageName; +} + +function findAvailableName(environment: string, baseName: string): string { + const apps = listApps(environment); + + // Check if base name is available + if (!apps[baseName]) { + return baseName; + } + + // Try with incrementing numbers + for (let i = 2; i <= 100; i++) { + const candidate = `${baseName}-${i}`; + if (!apps[candidate]) { + return candidate; + } + } + + // Fallback to timestamp if somehow we have 100+ duplicates + return `${baseName}-${Date.now()}`; +} + +function isAppNameAvailable(environment: string, name: string): boolean { + const apps = listApps(environment); + return !apps[name]; +} + +function validateAppName(name: string): void { + if (!name) { + throw new Error("App name cannot be empty"); + } + if (name.includes(" ")) { + throw new Error("App name cannot contain spaces"); + } + if (name.length > 50) { + throw new Error("App name cannot be longer than 50 characters"); + } +} + +function validateImageReference(value: string): boolean | string { + if (!value) { + return "Image reference cannot be empty"; + } + // Basic validation - should contain at least one / and optionally : + if (!value.includes("/")) { + return "Image reference must contain at least one /"; + } + return true; +} + +function validateFilePath(value: string): boolean | string { + if (!value) { + return "File path cannot be empty"; + } + if (!fs.existsSync(value)) { + return "File does not exist"; + } + return true; +} + +function validateInstanceTypeSKU( + sku: string, + availableTypes: Array<{ sku: string }>, +): string { + // Check if SKU is valid + for (const it of availableTypes) { + if (it.sku === sku) { + return sku; + } + } + + // Build helpful error message with valid options + const validSKUs = availableTypes.map((it) => it.sku).join(", "); + throw new Error( + `invalid instance-type value: ${sku} (must be one of: ${validSKUs})`, + ); +} + +async function selectInstanceTypeInteractively( + availableTypes: Array<{ sku: string; Description: string }>, + defaultSKU: string, + isCurrentType: boolean, +): Promise { + // Show header based on context + if (isCurrentType && defaultSKU) { + console.log(`\nSelect instance type (current: ${defaultSKU}):`); + } else { + console.log("\nSelect instance type:"); + } + + // Build options + const choices = availableTypes.map((it) => { + let name = `${it.sku} - ${it.Description}`; + // Mark the default/current option + if (it.sku === defaultSKU) { + name += isCurrentType ? " (current)" : " (default)"; + } + return { name, value: it.sku }; + }); + + const choice = await select({ + message: "Choose instance:", + choices, + }); + + return choice; +} diff --git a/packages/sdk/src/client/modules/app/index.ts b/packages/sdk/src/client/modules/app/index.ts index b12f610..060d28c 100644 --- a/packages/sdk/src/client/modules/app/index.ts +++ b/packages/sdk/src/client/modules/app/index.ts @@ -1,18 +1,21 @@ +/** + * Main App namespace entry point + */ + +import { parseAbi } from "viem"; // decodeEventLog +import { deploy as deployApp } from "./deploy/deploy"; +import { createApp, CreateAppOpts } from "./create/create"; + +import { getEnvironmentConfig } from "../../common/config/environment"; + import type { CoreContext } from "../.."; import type { AppId, DeployAppOpts, LifecycleOpts, // UpgradeAppOpts, -} from "./types"; -import { parseAbi, type Address } from "viem"; // decodeEventLog -import { deploy as deployApp } from "./deploy/deploy"; - -// TODO: source addresses (using zeus?) -const ADDR: Record = { - 1: { factory: "0xFactoryMainnet", controller: "0xControllerMainnet" }, - 11155111: { factory: "0xFactorySepolia", controller: "0xControllerSepolia" }, -}; +} from "../../common/types"; +import { getLogger } from "../../common/utils"; // Minimal ABI const CONTROLLER_ABI = parseAbi([ @@ -23,6 +26,7 @@ const CONTROLLER_ABI = parseAbi([ ]); export interface AppModule { + create: (opts: CreateAppOpts) => Promise; deploy: (opts: DeployAppOpts) => Promise<{ appId: AppId; tx: `0x${string}` }>; start: (appId: AppId, opts?: LifecycleOpts) => Promise<{ tx: `0x${string}` }>; stop: (appId: AppId, opts?: LifecycleOpts) => Promise<{ tx: `0x${string}` }>; @@ -37,15 +41,15 @@ export interface AppModule { } export function createAppModule(ctx: CoreContext): AppModule { - const addresses = ADDR[ctx.chain.id]; - if (!addresses) - throw new Error(`No contract addresses for chain ${ctx.chain.id}`); - - const { wallet, publicClient } = ctx; + const { wallet } = ctx; const chain = wallet.chain!; const account = wallet.account!; + const environment = getEnvironmentConfig(ctx.environment); + + const logger = getLogger(ctx.verbose); + // Helper to merge user gas overrides const gas = (g?: { maxFeePerGas?: bigint; maxPriorityFeePerGas?: bigint }) => g @@ -56,6 +60,9 @@ export function createAppModule(ctx: CoreContext): AppModule { : {}; return { + async create(opts) { + return createApp(opts, logger); + }, // Write operations async deploy(opts) { // Map DeployAppOpts to DeployOptions and call the deploy function @@ -64,22 +71,14 @@ export function createAppModule(ctx: CoreContext): AppModule { privateKey: ctx.privateKey, rpcUrl: ctx.rpcUrl, environment: ctx.environment, - imageRef: opts.image, - instanceType: "standard", // Default instance type - logRedirect: "console", // Default log redirect - publicLogs: false, // Default to private logs - dockerfilePath: undefined, - envFilePath: undefined, - appName: undefined, + appName: opts.name, + instanceType: opts.instanceType, + dockerfilePath: opts.dockerfile, + envFilePath: opts.envFile, + imageRef: opts.imageRef, + logVisibility: opts.logVisibility, }, - { - debug: () => {}, // Silent logger for SDK usage - info: () => {}, - warn: () => {}, - error: (msg: string, ...args: any[]) => { - console.error(msg, ...args); - }, - } + logger, ); return { @@ -92,7 +91,7 @@ export function createAppModule(ctx: CoreContext): AppModule { const tx = await wallet.writeContract({ chain, account, - address: addresses.controller, + address: environment.appControllerAddress as `0x${string}`, abi: CONTROLLER_ABI, functionName: "startApp", args: [appId], @@ -105,7 +104,7 @@ export function createAppModule(ctx: CoreContext): AppModule { const tx = await wallet.writeContract({ chain, account, - address: addresses.controller, + address: environment.appControllerAddress as `0x${string}`, abi: CONTROLLER_ABI, functionName: "stopApp", args: [appId], @@ -118,7 +117,7 @@ export function createAppModule(ctx: CoreContext): AppModule { const tx = await wallet.writeContract({ chain, account, - address: addresses.controller, + address: environment.appControllerAddress as `0x${string}`, abi: CONTROLLER_ABI, functionName: "terminateApp", args: [appId], @@ -131,7 +130,7 @@ export function createAppModule(ctx: CoreContext): AppModule { // const tx = await wallet.writeContract({ // chain, // account, - // address: addresses.controller, + // address: environment.appControllerAddress as `0x${string}`, // abi: CONTROLLER_ABI, // functionName: "upgrade", // args: [appId, opts.image], diff --git a/packages/sdk/src/client/modules/app/types.ts b/packages/sdk/src/client/modules/app/types.ts deleted file mode 100644 index ac188ea..0000000 --- a/packages/sdk/src/client/modules/app/types.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Address } from "viem"; - -export type AppId = Address & { readonly __brand: unique symbol }; - -export interface DeployAppOpts { - image: string; // or content hash - owner?: `0x${string}`; - resources?: { cpu?: number; memoryMiB?: number }; - salt?: `0x${string}`; - gas?: { maxFeePerGas?: bigint; maxPriorityFeePerGas?: bigint }; -} - -export interface UpgradeAppOpts { - image: string; - gas?: { maxFeePerGas?: bigint; maxPriorityFeePerGas?: bigint }; -} - -export interface LifecycleOpts { - gas?: { maxFeePerGas?: bigint; maxPriorityFeePerGas?: bigint }; -} - -export interface AppRecord { - id: AppId; - owner: `0x${string}`; - image: string; - status: "starting" | "running" | "stopped" | "terminated"; - createdAt: number; // epoch ms - lastUpdatedAt: number; // epoch ms -} diff --git a/packages/sdk/src/client/types.ts b/packages/sdk/src/client/types.ts deleted file mode 100644 index 6219917..0000000 --- a/packages/sdk/src/client/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface CreateClientConfig { - privateKey: `0x${string}`; - environment: "sepolia" | "mainnet-alpha"; - rpcUrl?: string; -} diff --git a/packages/sdk/tools/kms-client-linux-amd64 b/packages/sdk/tools/kms-client-linux-amd64 new file mode 100755 index 0000000..96f6d41 Binary files /dev/null and b/packages/sdk/tools/kms-client-linux-amd64 differ diff --git a/packages/sdk/tools/tls-keygen-linux-amd64 b/packages/sdk/tools/tls-keygen-linux-amd64 new file mode 100755 index 0000000..c18f90e Binary files /dev/null and b/packages/sdk/tools/tls-keygen-linux-amd64 differ diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json index 9626462..ec5b05d 100644 --- a/packages/sdk/tsconfig.json +++ b/packages/sdk/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES2020", + "target": "ES2021", "module": "ESNext", "moduleResolution": "bundler", "strict": true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b72d075..5165e33 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: packages/cli: dependencies: + '@inquirer/prompts': + specifier: ^7.10.1 + version: 7.10.1(@types/node@18.19.130) '@layr-labs/ecloud-sdk': specifier: workspace:* version: link:../sdk @@ -53,6 +56,9 @@ importers: handlebars: specifier: ^4.7.8 version: 4.7.8 + js-yaml: + specifier: ^4.1.1 + version: 4.1.1 node-forge: specifier: ^1.3.1 version: 1.3.1 @@ -72,12 +78,18 @@ importers: packages/sdk: dependencies: + '@inquirer/prompts': + specifier: ^7.10.1 + version: 7.10.1(@types/node@24.10.0) dockerode: specifier: ^4.0.9 version: 4.0.9 handlebars: specifier: ^4.7.8 version: 4.7.8 + js-yaml: + specifier: ^4.1.1 + version: 4.1.1 node-forge: specifier: ^1.3.1 version: 1.3.1 @@ -91,6 +103,9 @@ importers: '@types/dockerode': specifier: ^3.3.45 version: 3.3.45 + '@types/js-yaml': + specifier: ^4.0.9 + version: 4.0.9 '@types/node-forge': specifier: ^1.3.14 version: 1.3.14 @@ -331,6 +346,140 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/checkbox@4.3.2': + resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/editor@4.2.23': + resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@4.0.23': + resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/input@4.3.1': + resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@3.0.23': + resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@4.0.23': + resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@7.10.1': + resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/rawlist@4.1.11': + resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@3.2.2': + resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@4.4.2': + resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -569,6 +718,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/js-yaml@4.0.9': + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -794,6 +946,9 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -809,6 +964,10 @@ packages: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -1075,6 +1234,10 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + iconv-lite@0.7.0: + resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -1146,8 +1309,8 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true json-buffer@3.0.1: @@ -1237,6 +1400,10 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -1735,6 +1902,10 @@ packages: wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -1778,6 +1949,10 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + snapshots: '@adraffy/ens-normalize@1.11.1': {} @@ -1897,7 +2072,7 @@ snapshots: globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.1.0 + js-yaml: 4.1.1 minimatch: 3.1.2 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -1942,6 +2117,252 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@inquirer/ansi@1.0.2': {} + + '@inquirer/checkbox@4.3.2(@types/node@18.19.130)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@18.19.130) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@18.19.130) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 18.19.130 + + '@inquirer/checkbox@4.3.2(@types/node@24.10.0)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@24.10.0) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.10.0) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.10.0 + + '@inquirer/confirm@5.1.21(@types/node@18.19.130)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@18.19.130) + '@inquirer/type': 3.0.10(@types/node@18.19.130) + optionalDependencies: + '@types/node': 18.19.130 + + '@inquirer/confirm@5.1.21(@types/node@24.10.0)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.10.0) + '@inquirer/type': 3.0.10(@types/node@24.10.0) + optionalDependencies: + '@types/node': 24.10.0 + + '@inquirer/core@10.3.2(@types/node@18.19.130)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@18.19.130) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 18.19.130 + + '@inquirer/core@10.3.2(@types/node@24.10.0)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.10.0) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.10.0 + + '@inquirer/editor@4.2.23(@types/node@18.19.130)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@18.19.130) + '@inquirer/external-editor': 1.0.3(@types/node@18.19.130) + '@inquirer/type': 3.0.10(@types/node@18.19.130) + optionalDependencies: + '@types/node': 18.19.130 + + '@inquirer/editor@4.2.23(@types/node@24.10.0)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.10.0) + '@inquirer/external-editor': 1.0.3(@types/node@24.10.0) + '@inquirer/type': 3.0.10(@types/node@24.10.0) + optionalDependencies: + '@types/node': 24.10.0 + + '@inquirer/expand@4.0.23(@types/node@18.19.130)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@18.19.130) + '@inquirer/type': 3.0.10(@types/node@18.19.130) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 18.19.130 + + '@inquirer/expand@4.0.23(@types/node@24.10.0)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.10.0) + '@inquirer/type': 3.0.10(@types/node@24.10.0) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.10.0 + + '@inquirer/external-editor@1.0.3(@types/node@18.19.130)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.0 + optionalDependencies: + '@types/node': 18.19.130 + + '@inquirer/external-editor@1.0.3(@types/node@24.10.0)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.0 + optionalDependencies: + '@types/node': 24.10.0 + + '@inquirer/figures@1.0.15': {} + + '@inquirer/input@4.3.1(@types/node@18.19.130)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@18.19.130) + '@inquirer/type': 3.0.10(@types/node@18.19.130) + optionalDependencies: + '@types/node': 18.19.130 + + '@inquirer/input@4.3.1(@types/node@24.10.0)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.10.0) + '@inquirer/type': 3.0.10(@types/node@24.10.0) + optionalDependencies: + '@types/node': 24.10.0 + + '@inquirer/number@3.0.23(@types/node@18.19.130)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@18.19.130) + '@inquirer/type': 3.0.10(@types/node@18.19.130) + optionalDependencies: + '@types/node': 18.19.130 + + '@inquirer/number@3.0.23(@types/node@24.10.0)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.10.0) + '@inquirer/type': 3.0.10(@types/node@24.10.0) + optionalDependencies: + '@types/node': 24.10.0 + + '@inquirer/password@4.0.23(@types/node@18.19.130)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@18.19.130) + '@inquirer/type': 3.0.10(@types/node@18.19.130) + optionalDependencies: + '@types/node': 18.19.130 + + '@inquirer/password@4.0.23(@types/node@24.10.0)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@24.10.0) + '@inquirer/type': 3.0.10(@types/node@24.10.0) + optionalDependencies: + '@types/node': 24.10.0 + + '@inquirer/prompts@7.10.1(@types/node@18.19.130)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@18.19.130) + '@inquirer/confirm': 5.1.21(@types/node@18.19.130) + '@inquirer/editor': 4.2.23(@types/node@18.19.130) + '@inquirer/expand': 4.0.23(@types/node@18.19.130) + '@inquirer/input': 4.3.1(@types/node@18.19.130) + '@inquirer/number': 3.0.23(@types/node@18.19.130) + '@inquirer/password': 4.0.23(@types/node@18.19.130) + '@inquirer/rawlist': 4.1.11(@types/node@18.19.130) + '@inquirer/search': 3.2.2(@types/node@18.19.130) + '@inquirer/select': 4.4.2(@types/node@18.19.130) + optionalDependencies: + '@types/node': 18.19.130 + + '@inquirer/prompts@7.10.1(@types/node@24.10.0)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@24.10.0) + '@inquirer/confirm': 5.1.21(@types/node@24.10.0) + '@inquirer/editor': 4.2.23(@types/node@24.10.0) + '@inquirer/expand': 4.0.23(@types/node@24.10.0) + '@inquirer/input': 4.3.1(@types/node@24.10.0) + '@inquirer/number': 3.0.23(@types/node@24.10.0) + '@inquirer/password': 4.0.23(@types/node@24.10.0) + '@inquirer/rawlist': 4.1.11(@types/node@24.10.0) + '@inquirer/search': 3.2.2(@types/node@24.10.0) + '@inquirer/select': 4.4.2(@types/node@24.10.0) + optionalDependencies: + '@types/node': 24.10.0 + + '@inquirer/rawlist@4.1.11(@types/node@18.19.130)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@18.19.130) + '@inquirer/type': 3.0.10(@types/node@18.19.130) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 18.19.130 + + '@inquirer/rawlist@4.1.11(@types/node@24.10.0)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.10.0) + '@inquirer/type': 3.0.10(@types/node@24.10.0) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.10.0 + + '@inquirer/search@3.2.2(@types/node@18.19.130)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@18.19.130) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@18.19.130) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 18.19.130 + + '@inquirer/search@3.2.2(@types/node@24.10.0)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.10.0) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.10.0) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.10.0 + + '@inquirer/select@4.4.2(@types/node@18.19.130)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@18.19.130) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@18.19.130) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 18.19.130 + + '@inquirer/select@4.4.2(@types/node@24.10.0)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@24.10.0) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.10.0) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.10.0 + + '@inquirer/type@3.0.10(@types/node@18.19.130)': + optionalDependencies: + '@types/node': 18.19.130 + + '@inquirer/type@3.0.10(@types/node@24.10.0)': + optionalDependencies: + '@types/node': 24.10.0 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -2150,6 +2571,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/js-yaml@4.0.9': {} + '@types/json-schema@7.0.15': {} '@types/node-forge@1.3.14': @@ -2402,6 +2825,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chardet@2.1.1: {} + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -2414,6 +2839,8 @@ snapshots: cli-spinners@2.9.2: {} + cli-width@4.1.0: {} + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -2715,6 +3142,10 @@ snapshots: has-flag@4.0.0: {} + iconv-lite@0.7.0: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -2768,7 +3199,7 @@ snapshots: joycon@3.1.1: {} - js-yaml@4.1.0: + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -2847,6 +3278,8 @@ snapshots: ms@2.1.3: {} + mute-stream@2.0.0: {} + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -3330,6 +3763,12 @@ snapshots: wordwrap@1.0.0: {} + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -3363,3 +3802,5 @@ snapshots: yn@3.1.1: {} yocto-queue@0.1.0: {} + + yoctocolors-cjs@2.1.3: {}