diff --git a/package-lock.json b/package-lock.json index f5626d39d..61f19dbb1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7144,6 +7144,16 @@ "ink": "*" } }, + "node_modules/@types/inquirer": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-9.0.9.tgz", + "integrity": "sha512-/mWx5136gts2Z2e5izdoRCo46lPp5TMs9R15GTSsgg/XnZyxDWVqoVU3R9lWnccKpqwsJLvRoxbCjoJtZB7DSw==", + "dev": true, + "dependencies": { + "@types/through": "*", + "rxjs": "^7.2.0" + } + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "license": "MIT" @@ -7351,6 +7361,15 @@ "@types/node": "*" } }, + "node_modules/@types/through": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.33.tgz", + "integrity": "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/unist": { "version": "3.0.3", "dev": true, @@ -8808,7 +8827,6 @@ }, "node_modules/base64-js": { "version": "1.5.1", - "dev": true, "funding": [ { "type": "github", @@ -8929,7 +8947,6 @@ }, "node_modules/bl": { "version": "4.1.0", - "dev": true, "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -9101,7 +9118,6 @@ }, "node_modules/buffer": { "version": "5.7.1", - "dev": true, "funding": [ { "type": "github", @@ -9507,7 +9523,6 @@ }, "node_modules/chardet": { "version": "0.7.0", - "dev": true, "license": "MIT" }, "node_modules/cheerio": { @@ -9665,7 +9680,6 @@ }, "node_modules/cli-spinners": { "version": "2.6.1", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -9756,7 +9770,6 @@ }, "node_modules/clone": { "version": "1.0.4", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8" @@ -10752,7 +10765,6 @@ }, "node_modules/defaults": { "version": "1.0.4", - "dev": true, "license": "MIT", "dependencies": { "clone": "^1.0.2" @@ -12467,7 +12479,6 @@ }, "node_modules/external-editor": { "version": "3.1.0", - "dev": true, "license": "MIT", "dependencies": { "chardet": "^0.7.0", @@ -14930,7 +14941,6 @@ }, "node_modules/ieee754": { "version": "1.2.1", - "dev": true, "funding": [ { "type": "github", @@ -16129,7 +16139,6 @@ }, "node_modules/is-interactive": { "version": "1.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -16400,7 +16409,6 @@ }, "node_modules/is-unicode-supported": { "version": "0.1.0", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -19555,6 +19563,11 @@ "license": "MIT", "optional": true }, + "node_modules/lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==" + }, "node_modules/lodash.throttle": { "version": "4.1.1", "license": "MIT" @@ -19572,7 +19585,6 @@ }, "node_modules/log-symbols": { "version": "4.1.0", - "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.1.0", @@ -19587,7 +19599,6 @@ }, "node_modules/log-symbols/node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -19601,7 +19612,6 @@ }, "node_modules/log-symbols/node_modules/chalk": { "version": "4.1.2", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -19616,7 +19626,6 @@ }, "node_modules/log-symbols/node_modules/color-convert": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -19627,12 +19636,10 @@ }, "node_modules/log-symbols/node_modules/color-name": { "version": "1.1.4", - "dev": true, "license": "MIT" }, "node_modules/log-symbols/node_modules/has-flag": { "version": "4.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -19640,7 +19647,6 @@ }, "node_modules/log-symbols/node_modules/supports-color": { "version": "7.2.0", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -22451,7 +22457,6 @@ }, "node_modules/ora": { "version": "5.4.1", - "dev": true, "license": "MIT", "dependencies": { "bl": "^4.1.0", @@ -22473,7 +22478,6 @@ }, "node_modules/ora/node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -22487,7 +22491,6 @@ }, "node_modules/ora/node_modules/chalk": { "version": "4.1.2", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -22502,7 +22505,6 @@ }, "node_modules/ora/node_modules/color-convert": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -22513,12 +22515,10 @@ }, "node_modules/ora/node_modules/color-name": { "version": "1.1.4", - "dev": true, "license": "MIT" }, "node_modules/ora/node_modules/has-flag": { "version": "4.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -22526,7 +22526,6 @@ }, "node_modules/ora/node_modules/supports-color": { "version": "7.2.0", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -24394,7 +24393,6 @@ }, "node_modules/readable-stream": { "version": "3.6.2", - "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -24742,7 +24740,6 @@ }, "node_modules/rxjs": { "version": "7.8.1", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" @@ -25762,7 +25759,6 @@ }, "node_modules/string_decoder": { "version": "1.3.0", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -26608,7 +26604,6 @@ }, "node_modules/tmp": { "version": "0.0.33", - "dev": true, "license": "MIT", "dependencies": { "os-tmpdir": "~1.0.2" @@ -27794,7 +27789,6 @@ }, "node_modules/wcwidth": { "version": "1.0.1", - "dev": true, "license": "MIT", "dependencies": { "defaults": "^1.0.3" @@ -28413,6 +28407,12 @@ "version": "0.1.0", "license": "ISC", "dependencies": { + "@inquirer/figures": "^2.0.3", + "dotenv": "^17.2.3", + "inquirer": "^9.3.0", + "kleur": "^4.1.5", + "mina-fungible-token": "^1.1.0", + "reflect-metadata": "^0.1.13", "spectaql": "3.0.5", "ts-node": "^10.9.1", "yargs": "17.7.2" @@ -28421,18 +28421,148 @@ "proto-kit": "bin/protokit-cli.js" }, "devDependencies": { + "@types/inquirer": "^9.0.9", "@types/node": "^20.19.24", "@types/yargs": "17.0.32" }, "peerDependencies": { "@proto-kit/api": "*", "@proto-kit/common": "*", + "@proto-kit/explorer": "*", "@proto-kit/library": "*", "@proto-kit/module": "*", "@proto-kit/protocol": "*", "@proto-kit/sdk": "*", "@proto-kit/sequencer": "*", - "o1js": "^2.10.0" + "@proto-kit/stack": "*", + "o1js": "^2.10.0", + "tsyringe": "^4.10.0" + } + }, + "packages/cli/node_modules/@inquirer/figures": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.3.tgz", + "integrity": "sha512-y09iGt3JKoOCBQ3w4YrSJdokcD8ciSlMIWsD+auPu+OZpfxLuyz+gICAQ6GCBOmJJt4KEQGHuZSVff2jiNOy7g==", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "packages/cli/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "packages/cli/node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "engines": { + "node": ">= 12" + } + }, + "packages/cli/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "packages/cli/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "packages/cli/node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "packages/cli/node_modules/inquirer": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.3.0.tgz", + "integrity": "sha512-zdopqPUKWmnOcaBJYMMtjqWCB2HHXrteAou9tCYgkTJu01QheLfYOrkzigDfidPBtCizmkdpSU0fp2DKaMdFPA==", + "dependencies": { + "@inquirer/figures": "^1.0.3", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "external-editor": "^3.1.0", + "lodash.get": "^4.4.2", + "lodash.set": "^4.3.2", + "mute-stream": "1.0.0", + "ora": "^5.4.1", + "picocolors": "^1.0.1", + "run-async": "^3.0.0", + "rxjs": "^7.8.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=18" + } + }, + "packages/cli/node_modules/inquirer/node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "engines": { + "node": ">=18" + } + }, + "packages/cli/node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "engines": { + "node": ">=6" + } + }, + "packages/cli/node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "packages/cli/node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "engines": { + "node": ">=0.12.0" + } + }, + "packages/cli/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" } }, "packages/common": { @@ -28777,7 +28907,10 @@ "version": "0.1.1-develop.833+397881ed", "license": "MIT", "dependencies": { - "reflect-metadata": "^0.1.13" + "@prisma/client": "^5.19.1", + "mina-fungible-token": "^1.1.0", + "reflect-metadata": "^0.1.13", + "type-graphql": "2.0.0-rc.2" }, "devDependencies": { "@jest/globals": "^29.5.0" @@ -28786,9 +28919,11 @@ "@proto-kit/api": "*", "@proto-kit/common": "*", "@proto-kit/deployment": "*", + "@proto-kit/indexer": "*", "@proto-kit/library": "*", "@proto-kit/module": "*", "@proto-kit/persistance": "*", + "@proto-kit/processor": "*", "@proto-kit/protocol": "*", "@proto-kit/sdk": "*", "@proto-kit/sequencer": "*", diff --git a/packages/cli/package.json b/packages/cli/package.json index 7fbd14143..a37868ff3 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -21,21 +21,31 @@ "author": "", "license": "ISC", "dependencies": { - "yargs": "17.7.2", + "@inquirer/figures": "^2.0.3", + "dotenv": "^17.2.3", + "inquirer": "^9.3.0", + "kleur": "^4.1.5", + "mina-fungible-token": "^1.1.0", + "reflect-metadata": "^0.1.13", + "spectaql": "3.0.5", "ts-node": "^10.9.1", - "spectaql": "3.0.5" + "yargs": "17.7.2" }, "peerDependencies": { "@proto-kit/api": "*", "@proto-kit/common": "*", + "@proto-kit/explorer": "*", "@proto-kit/library": "*", "@proto-kit/module": "*", "@proto-kit/protocol": "*", "@proto-kit/sdk": "*", "@proto-kit/sequencer": "*", - "o1js": "^2.10.0" + "@proto-kit/stack": "*", + "o1js": "^2.10.0", + "tsyringe": "^4.10.0" }, "devDependencies": { + "@types/inquirer": "^9.0.9", "@types/node": "^20.19.24", "@types/yargs": "17.0.32" } diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index fbd97cd2c..d8f53fad7 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,16 +1,21 @@ #!/usr/bin/env node + +/* eslint-disable no-console */ import yargs from "yargs"; import { hideBin } from "yargs/helpers"; -import { generateGqlDocsCommand } from "./commands/generateGqlDocs"; +import { parseEnvArgs } from "./utils/loadEnv"; process.removeAllListeners("warning"); process.env.NODE_NO_WARNINGS = "1"; await yargs(hideBin(process.argv)) + .scriptName("proto-kit") + .usage("$0 [options]") + .strict() .command( "generate-gql-docs", - "generate GraphQL docs", + "Generate GraphQL docs", (yarg) => yarg .option("port", { @@ -33,6 +38,9 @@ await yargs(hideBin(process.argv)) }), async (args) => { try { + const { generateGqlDocsCommand } = await import( + "./scripts/graphqlDocs/generateGqlDocs" + ); await generateGqlDocsCommand(args); process.exit(0); } catch (error) { @@ -41,7 +49,349 @@ await yargs(hideBin(process.argv)) } } ) - .demandCommand() - .help() + .command( + "generate-keys [count]", + "Generate private/public key pairs for development", + (yarg) => + yarg.positional("count", { + type: "number", + default: 1, + describe: "number of keys to generate", + }), + async (args) => { + try { + const { generateKeysCommand } = await import("./scripts/generateKeys"); + await generateKeysCommand({ count: args.count }); + process.exit(0); + } catch (error) { + console.error("Failed to generate keys:", error); + process.exit(1); + } + } + ) + .command( + "lightnet:wait-for-network", + "Wait for lightnet network to be ready\n\nRequires: MINA_NODE_GRAPHQL_HOST, MINA_NODE_GRAPHQL_PORT", + (yarg) => + yarg + .option("env-path", { + type: "string", + describe: "path to .env file", + }) + .option("env", { + type: "string", + array: true, + describe: "environment variables as KEY=value", + }), + async (args) => { + try { + const { default: lightnetWaitForNetworkScript } = await import( + "./scripts/lightnet/wait-for-network" + ); + await lightnetWaitForNetworkScript({ + envPath: args["env-path"], + envVars: parseEnvArgs(args.env ?? []), + }); + process.exit(0); + } catch (error) { + console.error("Failed to wait for network:", error); + process.exit(1); + } + } + ) + .command( + "lightnet:faucet ", + "Send MINA to an account from the lightnet faucet", + (yarg) => + yarg + .positional("publicKey", { + type: "string", + describe: "public key to send MINA to", + demandOption: true, + }) + .option("env-path", { + type: "string", + describe: "path to .env file", + }) + .option("env", { + type: "string", + array: true, + describe: "environment variables as KEY=value", + }), + async (args) => { + try { + const { default: lightnetFaucetScript } = await import( + "./scripts/lightnet/faucet" + ); + await lightnetFaucetScript(args.publicKey); + process.exit(0); + } catch (error) { + console.error("Failed to send funds from faucet:", error); + process.exit(1); + } + } + ) + .command( + "settlement:deploy", + "Deploy settlement contracts\n\nRequires: PROTOKIT_SETTLEMENT_CONTRACT_PRIVATE_KEY, PROTOKIT_DISPATCHER_CONTRACT_PRIVATE_KEY, PROTOKIT_MINA_BRIDGE_CONTRACT_PRIVATE_KEY", + (yarg) => + yarg + .option("env-path", { type: "string", describe: "path to .env file" }) + .option("env", { + type: "string", + array: true, + describe: "environment variables as KEY=value", + }), + async (args) => { + try { + const { default: settlementDeployScript } = await import( + "./scripts/settlement/deploy" + ); + await settlementDeployScript({ + envPath: args["env-path"], + envVars: parseEnvArgs(args.env ?? []), + }); + process.exit(0); + } catch (error) { + console.error("Failed to deploy settlement:", error); + process.exit(1); + } + } + ) + .command( + "settlement:token:deploy [mintAmount]", + "Deploy custom fungible token for settlement\n\nRequires: PROTOKIT_SETTLEMENT_CONTRACT_PRIVATE_KEY, PROTOKIT_DISPATCHER_CONTRACT_PRIVATE_KEY, PROTOKIT_CUSTOM_TOKEN_PRIVATE_KEY, PROTOKIT_CUSTOM_TOKEN_ADMIN_PRIVATE_KEY, PROTOKIT_CUSTOM_TOKEN_BRIDGE_PRIVATE_KEY", + (yarg) => + yarg + .positional("tokenSymbol", { type: "string", demandOption: true }) + .positional("feepayerKey", { type: "string", demandOption: true }) + .positional("receiverPublicKey", { + type: "string", + demandOption: true, + }) + .positional("mintAmount", { type: "number", default: 0 }) + .option("env-path", { type: "string", describe: "path to .env file" }) + .option("env", { + type: "string", + array: true, + describe: "environment variables as KEY=value", + }), + async (args) => { + try { + const { default: settlementTokenDeployScript } = await import( + "./scripts/settlement/deploy-token" + ); + await settlementTokenDeployScript( + { + envPath: args["env-path"], + envVars: parseEnvArgs(args.env ?? []), + }, + { + tokenSymbol: args.tokenSymbol, + feepayerKey: args.feepayerKey, + receiverPublicKey: args.receiverPublicKey, + mintAmount: args.mintAmount, + } + ); + process.exit(0); + } catch (error) { + console.error("Failed to deploy settlement token:", error); + process.exit(1); + } + } + ) + .command( + "lightnet:initialize", + "Initialize lightnet: wait for network, fund accounts, and deploy settlement\n\nRequires: MINA_NODE_GRAPHQL_HOST, MINA_NODE_GRAPHQL_PORT, MINA_ARCHIVE_GRAPHQL_HOST, MINA_ARCHIVE_GRAPHQL_PORT, MINA_ACCOUNT_MANAGER_HOST, MINA_ACCOUNT_MANAGER_PORT, PROTOKIT_SETTLEMENT_CONTRACT_PRIVATE_KEY, PROTOKIT_DISPATCHER_CONTRACT_PRIVATE_KEY, PROTOKIT_MINA_BRIDGE_CONTRACT_PRIVATE_KEY", + (yarg) => + yarg + .option("env-path", { type: "string", describe: "path to .env file" }) + .option("env", { + type: "string", + array: true, + describe: "environment variables as KEY=value", + }), + async (args) => { + try { + const { lightnetInitializeCommand } = await import( + "./scripts/lightnetInitialize" + ); + await lightnetInitializeCommand({ + envPath: args["env-path"], + envVars: parseEnvArgs(args.env ?? []), + }); + process.exit(0); + } catch (error) { + console.error("Failed to initialize lightnet:", error); + process.exit(1); + } + } + ) + .command( + "bridge:deposit ", + "Deposit tokens to the bridge\n\nRequires: PROTOKIT_CUSTOM_TOKEN_PRIVATE_KEY (for custom tokens), PROTOKIT_CUSTOM_TOKEN_BRIDGE_PRIVATE_KEY, PROTOKIT_MINA_BRIDGE_CONTRACT_PRIVATE_KEY", + (yarg) => + yarg + .positional("tokenId", { type: "string", demandOption: true }) + .positional("fromKey", { type: "string", demandOption: true }) + .positional("toKey", { type: "string", demandOption: true }) + .positional("amount", { type: "number", demandOption: true }) + .option("env-path", { type: "string", describe: "path to .env file" }) + .option("env", { + type: "string", + array: true, + describe: "environment variables as KEY=value", + }), + async (args) => { + try { + const { default: bridgeDepositScript } = await import( + "./scripts/bridge/deposit" + ); + await bridgeDepositScript( + { + envPath: args["env-path"], + envVars: parseEnvArgs(args.env ?? []), + }, + { + tokenId: args.tokenId, + fromKey: args.fromKey, + toKey: args.toKey, + amount: args.amount, + } + ); + process.exit(0); + } catch (error) { + console.error("Failed to deposit to bridge:", error); + process.exit(1); + } + } + ) + .command( + "bridge:redeem ", + "Redeem tokens from the bridge\n\nRequires: PROTOKIT_CUSTOM_TOKEN_PRIVATE_KEY", + (yarg) => + yarg + .positional("tokenId", { type: "string", demandOption: true }) + .positional("toKey", { type: "string", demandOption: true }) + .positional("amount", { type: "number", demandOption: true }) + .option("env-path", { type: "string", describe: "path to .env file" }) + .option("env", { + type: "string", + array: true, + describe: "environment variables as KEY=value", + }), + async (args) => { + try { + const { default: bridgeRedeemScript } = await import( + "./scripts/bridge/redeem" + ); + await bridgeRedeemScript( + { + envPath: args["env-path"], + envVars: parseEnvArgs(args.env ?? []), + }, + { + tokenId: args.tokenId, + toKey: args.toKey, + amount: args.amount, + } + ); + process.exit(0); + } catch (error) { + console.error("Failed to redeem from bridge:", error); + process.exit(1); + } + } + ) + .command( + "bridge:withdraw ", + "Withdraw tokens\n\nRequires: NEXT_PUBLIC_PROTOKIT_GRAPHQL_URL", + (yarg) => + yarg + .positional("tokenId", { type: "string", demandOption: true }) + .positional("senderKey", { type: "string", demandOption: true }) + .positional("amount", { type: "number", demandOption: true }) + .option("env-path", { type: "string", describe: "path to .env file" }) + .option("env", { + type: "string", + array: true, + describe: "environment variables as KEY=value", + }), + async (args) => { + try { + const { default: bridgeWithdrawScript } = await import( + "./scripts/bridge/withdraw" + ); + await bridgeWithdrawScript( + { + envPath: args["env-path"], + envVars: parseEnvArgs(args.env ?? []), + }, + { + tokenId: args.tokenId, + senderKey: args.senderKey, + amount: args.amount, + } + ); + process.exit(0); + } catch (error) { + console.error("Failed to withdraw from bridge:", error); + process.exit(1); + } + } + ) + .command( + "env:create", + "Create a new environment configuration with guided wizard", + () => ({}), + async () => { + try { + const { default: createEnvironmentScript } = await import( + "./scripts/env/create-environment" + ); + await createEnvironmentScript(); + process.exit(0); + } catch (error) { + console.error("Failed to create environment:", error); + process.exit(1); + } + } + ) + .command( + "explorer:start", + "Start the explorer UI", + (yarg) => + yarg + .option("port", { + alias: "p", + type: "number", + default: 5003, + describe: "port to run the explorer on", + }) + .option("indexer-url", { + type: "string", + describe: "GraphQL endpoint URL for the indexer", + }), + async (args) => { + try { + const { default: explorerStartScript } = await import( + "./scripts/explorer/start" + ); + await explorerStartScript(args.port, args["indexer-url"]); + process.exit(0); + } catch (error) { + console.error("Failed to start explorer:", error); + process.exit(1); + } + } + ) + .demandCommand( + 1, + "You must specify a command. Use --help to see available commands." + ) + .help("help") + .alias("help", "h") + .option("help", { describe: "Show help" }) .strict() .parse(); +/* eslint-enable no-console */ diff --git a/packages/cli/src/scripts/bridge/deposit.ts b/packages/cli/src/scripts/bridge/deposit.ts new file mode 100644 index 000000000..cfb4ad492 --- /dev/null +++ b/packages/cli/src/scripts/bridge/deposit.ts @@ -0,0 +1,185 @@ +/* eslint-disable no-console */ +/* eslint-disable func-names */ + +import { + BridgingModule, + MinaTransactionSender, + Sequencer, + SettlementModule, + AppChain, +} from "@proto-kit/sequencer"; +import { Runtime } from "@proto-kit/module"; +import { DispatchSmartContract, Protocol } from "@proto-kit/protocol"; +import { DefaultConfigs, DefaultModules } from "@proto-kit/stack"; +import { + AccountUpdate, + fetchAccount, + Field, + Mina, + PrivateKey, + Provable, + PublicKey, + UInt64, +} from "o1js"; +import { FungibleToken } from "mina-fungible-token"; + +import { + loadEnvironmentVariables, + getRequiredEnv, + LoadEnvOptions, +} from "../../utils/loadEnv"; +import { loadUserModules } from "../../utils/loadUserModules"; + +export interface BridgeDepositArgs { + tokenId: string; + fromKey: string; + toKey: string; + amount: number; +} + +export default async function ( + options?: LoadEnvOptions, + bridgeArgs?: BridgeDepositArgs +) { + if (!bridgeArgs) { + throw new Error( + "Bridge deposit arguments required: tokenId, fromKey, toKey, amount" + ); + } + + loadEnvironmentVariables(options); + const { runtime, protocol } = await loadUserModules(); + const tokenId = Field(bridgeArgs.tokenId); + const fromPrivateKey = PrivateKey.fromBase58( + process.env[bridgeArgs.fromKey] ?? bridgeArgs.fromKey + ); + const toPublicKey = PublicKey.fromBase58( + process.env[bridgeArgs.toKey] ?? bridgeArgs.toKey + ); + const amount = bridgeArgs.amount * 1e9; + const fee = 0.1 * 1e9; + + const isCustomToken = tokenId.toBigInt() !== 1n; + const tokenOwnerPrivateKey = isCustomToken + ? PrivateKey.fromBase58(getRequiredEnv("PROTOKIT_CUSTOM_TOKEN_PRIVATE_KEY")) + : PrivateKey.random(); + const bridgeContractKey = isCustomToken + ? PrivateKey.fromBase58( + getRequiredEnv("PROTOKIT_CUSTOM_TOKEN_BRIDGE_PRIVATE_KEY") + ) + : PrivateKey.fromBase58( + getRequiredEnv("PROTOKIT_MINA_BRIDGE_CONTRACT_PRIVATE_KEY") + ); + + Provable.log("Preparing to deposit", { + tokenId, + fromPrivateKey, + toPublicKey, + amount, + fee, + }); + + const appChain = AppChain.from({ + Runtime: Runtime.from(runtime.modules), + Protocol: Protocol.from({ + ...protocol.modules, + ...protocol.settlementModules, + }), + Sequencer: Sequencer.from({ + ...DefaultModules.inMemoryDatabase(), + ...DefaultModules.settlementScript(), + }), + }); + + appChain.configure({ + Runtime: runtime.config, + Protocol: { + ...protocol.config, + ...protocol.settlementModulesConfig, + }, + Sequencer: { + ...DefaultConfigs.inMemoryDatabase(), + ...DefaultConfigs.settlementScript({ + preset: "development", + }), + }, + }); + + const proofsEnabled = process.env.PROTOKIT_PROOFS_ENABLED === "true"; + await appChain.start(proofsEnabled); + + const settlementModule = appChain.sequencer.resolveOrFail( + "SettlementModule", + SettlementModule + ); + + const bridgingModule = appChain.sequencer.resolveOrFail( + "BridgingModule", + BridgingModule + ); + + const settlement = settlementModule.getSettlementContract(); + const dispatch = + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + bridgingModule.getDispatchContract() as DispatchSmartContract; + + await fetchAccount({ publicKey: fromPrivateKey.toPublicKey() }); + await fetchAccount({ publicKey: settlement.address }); + await fetchAccount({ publicKey: dispatch.address }); + const bridgeAddress = await bridgingModule.getBridgeAddress(tokenId); + await fetchAccount({ publicKey: bridgeAddress!, tokenId: tokenId }); + await fetchAccount({ publicKey: bridgeAddress!, tokenId: tokenId }); + + const attestation = + await bridgingModule.getDepositContractAttestation(tokenId); + + console.log("Forging transaction..."); + const tx = await Mina.transaction( + { + memo: "User deposit", + sender: fromPrivateKey.toPublicKey(), + fee, + }, + async () => { + const au = AccountUpdate.createSigned( + fromPrivateKey.toPublicKey(), + tokenId + ); + au.balance.subInPlace(UInt64.from(amount)); + + await dispatch.deposit( + UInt64.from(amount), + tokenId, + bridgeContractKey.toPublicKey(), + attestation, + toPublicKey + ); + + if (isCustomToken) { + await new FungibleToken( + tokenOwnerPrivateKey.toPublicKey() + )!.approveAccountUpdates([au, dispatch.self]); + } + } + ); + console.log(tx.toPretty()); + + settlementModule.utils.signTransaction(tx, { + signingPublicKeys: [fromPrivateKey.toPublicKey()], + preventNoncePreconditionFor: [dispatch.address], + signingWithSignatureCheck: [tokenOwnerPrivateKey.toPublicKey()], + }); + + console.log("Sending..."); + console.log(tx.toPretty()); + + const { hash } = await appChain.sequencer + .resolveOrFail("TransactionSender", MinaTransactionSender) + .proveAndSendTransaction(tx, "included"); + + console.log(`Deposit transaction included in a block: ${hash}`); + + await appChain.close(); +} +/* eslint-enable no-console */ +/* eslint-enable func-names */ diff --git a/packages/cli/src/scripts/bridge/redeem.ts b/packages/cli/src/scripts/bridge/redeem.ts new file mode 100644 index 000000000..7a3b2c039 --- /dev/null +++ b/packages/cli/src/scripts/bridge/redeem.ts @@ -0,0 +1,154 @@ +/* eslint-disable no-console */ +/* eslint-disable func-names */ +import { + BridgingModule, + MinaTransactionSender, + Sequencer, + SettlementModule, + AppChain, +} from "@proto-kit/sequencer"; +import { Runtime } from "@proto-kit/module"; +import { Protocol } from "@proto-kit/protocol"; +import { + AccountUpdate, + fetchAccount, + Field, + Mina, + PrivateKey, + Provable, + UInt64, +} from "o1js"; +import { FungibleToken } from "mina-fungible-token"; +import { DefaultConfigs, DefaultModules } from "@proto-kit/stack"; + +import { + loadEnvironmentVariables, + getRequiredEnv, + LoadEnvOptions, +} from "../../utils/loadEnv"; +import { loadUserModules } from "../../utils/loadUserModules"; + +export interface BridgeRedeemArgs { + tokenId: string; + toKey: string; + amount: number; +} + +export default async function ( + options?: LoadEnvOptions, + bridgeArgs?: BridgeRedeemArgs +) { + if (!bridgeArgs) { + throw new Error("Bridge redeem arguments required: tokenId, toKey, amount"); + } + + loadEnvironmentVariables(options); + const { runtime, protocol } = await loadUserModules(); + const tokenId = Field(bridgeArgs.tokenId); + const toPrivateKey = PrivateKey.fromBase58( + process.env[bridgeArgs.toKey] ?? bridgeArgs.toKey + ); + const amount = bridgeArgs.amount * 1e9; + const fee = 0.1 * 1e9; + + const isCustomToken = tokenId.toBigInt() !== 1n; + const tokenOwnerPrivateKey = isCustomToken + ? PrivateKey.fromBase58(getRequiredEnv("PROTOKIT_CUSTOM_TOKEN_PRIVATE_KEY")) + : PrivateKey.random(); + + Provable.log("Preparing to redeem", { + tokenId, + to: toPrivateKey.toPublicKey(), + amount, + fee, + }); + + const appChain = AppChain.from({ + Runtime: Runtime.from(runtime.modules), + Protocol: Protocol.from({ + ...protocol.modules, + ...protocol.settlementModules, + }), + Sequencer: Sequencer.from({ + ...DefaultModules.inMemoryDatabase(), + ...DefaultModules.settlementScript(), + }), + }); + + appChain.configure({ + Runtime: runtime.config, + Protocol: { + ...protocol.config, + ...protocol.settlementModulesConfig, + }, + Sequencer: { + ...DefaultConfigs.inMemoryDatabase(), + ...DefaultConfigs.settlementScript({ + preset: "development", + }), + }, + }); + + const proofsEnabled = process.env.PROTOKIT_PROOFS_ENABLED === "true"; + await appChain.start(proofsEnabled); + + const bridgingModule = appChain.sequencer.resolveOrFail( + "BridgingModule", + BridgingModule + ); + + const bridgeContract = await bridgingModule.getBridgeContract(tokenId); + + const customAcc = await fetchAccount({ + publicKey: toPrivateKey.toPublicKey(), + tokenId: bridgeContract.deriveTokenId(), + }); + + Provable.log("Custom account", customAcc.account?.balance); + + console.log("Forging transaction..."); + const tx = await Mina.transaction( + { + sender: toPrivateKey.toPublicKey(), + fee, + }, + async () => { + const au = AccountUpdate.createSigned( + toPrivateKey.toPublicKey(), + tokenId + ); + au.balance.addInPlace(UInt64.from(amount)); + + await bridgeContract.redeem(au); + + if (isCustomToken) { + await new FungibleToken( + tokenOwnerPrivateKey.toPublicKey() + )!.approveAccountUpdate(bridgeContract.self); + } + } + ); + + const settlementModule = appChain.sequencer.resolveOrFail( + "SettlementModule", + SettlementModule + ); + + settlementModule.utils.signTransaction(tx, { + signingPublicKeys: [toPrivateKey.toPublicKey()], + signingWithSignatureCheck: [tokenOwnerPrivateKey.toPublicKey()], + }); + + console.log("Sending..."); + + const { hash } = await appChain.sequencer + .resolveOrFail("TransactionSender", MinaTransactionSender) + .proveAndSendTransaction(tx, "included"); + + console.log(`Redeem transaction included in a block: ${hash}`); + console.log(tx.toPretty()); + + await appChain.close(); +} +/* eslint-enable no-console */ +/* eslint-enable func-names */ diff --git a/packages/cli/src/scripts/bridge/withdraw.ts b/packages/cli/src/scripts/bridge/withdraw.ts new file mode 100644 index 000000000..346576db2 --- /dev/null +++ b/packages/cli/src/scripts/bridge/withdraw.ts @@ -0,0 +1,78 @@ +/* eslint-disable no-console */ +/* eslint-disable func-names */ +import { ClientAppChain, InMemorySigner } from "@proto-kit/sdk"; +import { Field, PrivateKey, Provable } from "o1js"; +import { UInt64 } from "@proto-kit/library"; +import { Runtime } from "@proto-kit/module"; +import { Protocol } from "@proto-kit/protocol"; + +import { loadEnvironmentVariables, LoadEnvOptions } from "../../utils/loadEnv"; +import { loadUserModules } from "../../utils/loadUserModules"; + +export interface BridgeWithdrawArgs { + tokenId: string; + senderKey: string; + amount: number; +} + +export default async function ( + options?: LoadEnvOptions, + bridgeArgs?: BridgeWithdrawArgs +) { + if (!bridgeArgs) { + throw new Error( + "Bridge withdraw arguments required: tokenId, senderKey, amount" + ); + } + + loadEnvironmentVariables(options); + const { runtime, protocol } = await loadUserModules(); + const tokenId = Field(bridgeArgs.tokenId); + const amount = UInt64.from(bridgeArgs.amount * 1e9); + const appChain = ClientAppChain.fromRemoteEndpoint( + Runtime.from(runtime.modules), + Protocol.from({ ...protocol.modules, ...protocol.settlementModules }), + InMemorySigner + ); + + appChain.configurePartial({ + Runtime: runtime.config, + Protocol: { + ...protocol.config, + ...protocol.settlementModulesConfig, + }, + GraphqlClient: { + url: process.env.NEXT_PUBLIC_PROTOKIT_GRAPHQL_URL, + }, + }); + + await appChain.start(); + + const senderPrivateKey = PrivateKey.fromBase58( + process.env[bridgeArgs.senderKey] ?? bridgeArgs.senderKey + ); + const senderPublicKey = senderPrivateKey.toPublicKey(); + const signer = appChain.resolve("Signer"); + signer.config.signer = senderPrivateKey; + + Provable.log("debug", { + senderPrivateKey, + senderPublicKey, + amount, + tokenId, + }); + + const withdrawals = appChain.runtime.resolve("Withdrawals"); + const tx = await appChain.transaction(senderPublicKey, async () => { + await withdrawals.withdraw(senderPublicKey, amount, tokenId); + }); + + await tx.sign(); + await tx.send(); + + console.log("withdrawal tx sent"); + + await appChain.close(); +} +/* eslint-enable no-console */ +/* eslint-enable func-names */ diff --git a/packages/cli/src/scripts/env/create-environment.ts b/packages/cli/src/scripts/env/create-environment.ts new file mode 100644 index 000000000..ea3c3d576 --- /dev/null +++ b/packages/cli/src/scripts/env/create-environment.ts @@ -0,0 +1,102 @@ +/* eslint-disable no-console */ +/* eslint-disable func-names */ +import * as fs from "fs"; +import * as path from "path"; + +import { cyan, green, red, bold } from "kleur/colors"; + +import { + copyAndUpdateEnvFile, + generateChainConfig, + generateIndexerConfig, + generateProcessorConfig, + icons, + promptUser, +} from "../../utils/create-environment"; + +export default async function () { + try { + const answers = await promptUser(); + + const cwd = process.cwd(); + const envDir = path.join( + cwd, + "src/core/environments", + answers.environmentName + ); + + if (!fs.existsSync(envDir)) { + fs.mkdirSync(envDir, { recursive: true }); + } + + const chainConfigPath = path.join(envDir, "chain.config.ts"); + const indexerConfigPath = path.join(envDir, "indexer.config.ts"); + const processorConfigPath = path.join(envDir, "processor.config.ts"); + + if (fs.existsSync(chainConfigPath)) { + console.log(`\nEnvironment already exists at ${envDir}`); + return; + } + + copyAndUpdateEnvFile(answers, cwd, envDir); + const chainConfig = generateChainConfig(answers); + fs.writeFileSync(chainConfigPath, chainConfig); + + if (answers.includeIndexer) { + const indexerConfig = generateIndexerConfig(answers); + if (indexerConfig) { + fs.writeFileSync(indexerConfigPath, indexerConfig); + } + } + + if (answers.includeProcessor && answers.includeIndexer) { + const processorConfig = generateProcessorConfig(answers); + if (processorConfig) { + fs.writeFileSync(processorConfigPath, processorConfig); + } + } + + console.log( + `\n${bold(green(" ╔════════════════════════════════════════╗"))}` + ); + console.log( + `${bold(green(" ║ ✓ Environment Created Successfully ║"))}` + ); + console.log( + `${bold(green(" ╚════════════════════════════════════════╝"))}` + ); + console.log(""); + + console.log(`${bold("Location:")}`); + console.log(` ${cyan(envDir)}\n`); + + console.log(`${bold("Generated Files:")}`); + console.log(` ${green(icons.checkmark)} .env`); + console.log(` ${green(icons.checkmark)} chain.config.ts`); + if (answers.includeIndexer) { + console.log(` ${green(icons.checkmark)} indexer.config.ts`); + } + if (answers.includeProcessor && answers.includeIndexer) { + console.log(` ${green(icons.checkmark)} processor.config.ts`); + } + + console.log(`\n${bold("Next Steps:")}`); + const cdCommand = `cd ${path.relative(process.cwd(), envDir)}`; + console.log(` 1. ${cyan(cdCommand)}`); + console.log(` 2. Update environment variables in ${cyan(".env")}`); + console.log( + ` 3. Add the following script to your root ${cyan("package.json")}:` + ); + const scriptCommand = `"env:${answers.environmentName}": "dotenv -e ./packages/chain/src/core/environments/${answers.environmentName}/.env -- pnpm"`; + console.log(`${cyan(scriptCommand)}`); + console.log(" 4. Start your application\n"); + } catch (error) { + console.log(`\n${bold(red("✗ Error"))}`); + console.log(`${red("-".repeat(50))}`); + console.error(` ${error}`); + console.log(`${red("-".repeat(50))}\n`); + process.exit(1); + } +} +/* eslint-enable no-console */ +/* eslint-enable func-names */ diff --git a/packages/cli/src/scripts/explorer/start.ts b/packages/cli/src/scripts/explorer/start.ts new file mode 100644 index 000000000..4ce12db6f --- /dev/null +++ b/packages/cli/src/scripts/explorer/start.ts @@ -0,0 +1,57 @@ +/* eslint-disable no-console */ +/* eslint-disable func-names */ +import { spawn } from "child_process"; +import path from "path"; +import { fileURLToPath } from "url"; + +export default async function (port?: number, indexerUrl?: string) { + let explorerDir: string; + + try { + const pkgUrl = await import.meta.resolve( + "@proto-kit/explorer/package.json" + ); + const pkgPath = fileURLToPath(pkgUrl); + explorerDir = path.dirname(pkgPath); + } catch (error) { + console.error("Failed to find @proto-kit/explorer package."); + throw error; + } + + return await new Promise((resolve, reject) => { + const child = spawn("npm", ["run", "dev", "--", "-p", String(port)], { + cwd: explorerDir, + stdio: "inherit", + env: { + ...process.env, + NODE_OPTIONS: "", + NEXT_PUBLIC_INDEXER_URL: indexerUrl, + }, + }); + + child.on("error", (error) => { + console.error("Failed to start explorer:", error); + reject(error); + }); + + child.on("exit", (code) => { + if (code !== null && code !== 0 && code !== 143) { + reject(new Error(`Explorer process exited with code ${code}`)); + } else { + resolve(); + } + }); + + process.on("SIGINT", () => { + child.kill(); + resolve(); + }); + + process.on("SIGTERM", () => { + child.kill(); + resolve(); + }); + }); +} +/* eslint-enable no-console */ +/* eslint-enable func-names */ diff --git a/packages/cli/src/scripts/generateKeys.ts b/packages/cli/src/scripts/generateKeys.ts new file mode 100644 index 000000000..d4d5fd663 --- /dev/null +++ b/packages/cli/src/scripts/generateKeys.ts @@ -0,0 +1,21 @@ +/* eslint-disable no-console */ + +import { PrivateKey } from "o1js"; + +type GenerateKeysArgs = { + count?: number; +}; + +export async function generateKeysCommand(args: GenerateKeysArgs) { + const count = args.count ?? 1; + console.log(`Generated ${count} keys for development purposes:`); + console.log("-".repeat(70)); + for (let i = 0; i < count; i++) { + const privateKey = PrivateKey.random(); + const publicKey = privateKey.toPublicKey(); + console.log("Private key:", privateKey.toBase58()); + console.log("Public key:", publicKey.toBase58()); + console.log("-".repeat(70)); + } +} +/* eslint-enable no-console */ diff --git a/packages/cli/src/commands/generateGqlDocs.ts b/packages/cli/src/scripts/graphqlDocs/generateGqlDocs.ts similarity index 94% rename from packages/cli/src/commands/generateGqlDocs.ts rename to packages/cli/src/scripts/graphqlDocs/generateGqlDocs.ts index f9fcedc17..2ab3c680d 100644 --- a/packages/cli/src/commands/generateGqlDocs.ts +++ b/packages/cli/src/scripts/graphqlDocs/generateGqlDocs.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import { BlockStorageNetworkStateModule, InMemoryTransactionSender, @@ -21,7 +22,7 @@ import { } from "@proto-kit/api"; import { Runtime } from "@proto-kit/module"; -import { generateGqlDocs } from "../utils"; +import { generateGqlDocs } from "../../utils/graphqlDocs"; export async function generateGqlDocsCommand(args: { empty: boolean; @@ -76,3 +77,4 @@ export async function generateGqlDocsCommand(args: { await generateGqlDocs(args.url); } } +/* eslint-enable no-console */ diff --git a/packages/cli/src/scripts/lightnet/faucet.ts b/packages/cli/src/scripts/lightnet/faucet.ts new file mode 100644 index 000000000..b3af98c63 --- /dev/null +++ b/packages/cli/src/scripts/lightnet/faucet.ts @@ -0,0 +1,79 @@ +/* eslint-disable func-names */ +import { + AccountUpdate, + fetchAccount, + Lightnet, + Mina, + Provable, + PublicKey, +} from "o1js"; + +import "reflect-metadata"; +import { getRequiredEnv } from "../../utils/loadEnv"; + +export default async function (publicKey: string) { + // configuration + const fee = 0.1 * 1e9; + const fundingAmount = 1000 * 1e9; + + const net = Mina.Network({ + mina: `${getRequiredEnv("MINA_NODE_GRAPHQL_HOST")}:${getRequiredEnv("MINA_NODE_GRAPHQL_PORT")}/graphql`, + archive: `${getRequiredEnv("MINA_ARCHIVE_GRAPHQL_HOST")}:${getRequiredEnv("MINA_ARCHIVE_GRAPHQL_PORT")}/graphql`, + lightnetAccountManager: `${getRequiredEnv("MINA_ACCOUNT_MANAGER_HOST")}:${getRequiredEnv("MINA_ACCOUNT_MANAGER_PORT")}`, + }); + + Mina.setActiveInstance(net); + + // get the source account from the account manager + const pair = await Lightnet.acquireKeyPair({ + isRegularAccount: true, + }); + + // which account to drip to + const keyArg = process.env[publicKey] ?? publicKey; + + if (keyArg?.length === 0) { + throw new Error("No key provided"); + } + + const key = PublicKey.fromBase58(keyArg); + + await fetchAccount({ publicKey: pair.publicKey }); + + Provable.log( + `Dripping ${fundingAmount / 1e9} MINA from ${pair.publicKey.toBase58()} to ${key.toBase58()}` + ); + + const tx = await Mina.transaction( + { + sender: pair.publicKey, + fee, + }, + async () => { + const account = await fetchAccount({ publicKey: key }); + // if the destination account does not exist yet, pay the creation fee for it + if (account.error) { + AccountUpdate.fundNewAccount(pair.publicKey); + } + + AccountUpdate.createSigned(pair.publicKey).balance.subInPlace( + fundingAmount + ); + AccountUpdate.create(key).balance.addInPlace(fundingAmount); + } + ); + + tx.sign([pair.privateKey]); + + const sentTx = await tx.send(); + await sentTx.wait(); + + Provable.log( + `Funded account ${key.toBase58()} with ${fundingAmount / 1e9} MINA` + ); + + await Lightnet.releaseKeyPair({ + publicKey: pair.publicKey.toBase58(), + }); +} +/* eslint-enable func-names */ diff --git a/packages/cli/src/scripts/lightnet/wait-for-network.ts b/packages/cli/src/scripts/lightnet/wait-for-network.ts new file mode 100644 index 000000000..9f7ccbae6 --- /dev/null +++ b/packages/cli/src/scripts/lightnet/wait-for-network.ts @@ -0,0 +1,41 @@ +/* eslint-disable no-console */ +/* eslint-disable func-names */ +import { sleep } from "@proto-kit/common"; +import { fetchLastBlock, Provable } from "o1js"; + +import { + loadEnvironmentVariables, + getRequiredEnv, + LoadEnvOptions, +} from "../../utils/loadEnv"; + +const maxAttempts = 24; +const delay = 5000; + +export default async function (options?: LoadEnvOptions) { + loadEnvironmentVariables(options); + const graphqlEndpoint = `${getRequiredEnv("MINA_NODE_GRAPHQL_HOST")}:${getRequiredEnv("MINA_NODE_GRAPHQL_PORT")}/graphql`; + let lastBlock; + let attempt = 0; + console.log("Waiting for network to be ready..."); + while (!lastBlock) { + attempt += 1; + if (attempt > maxAttempts) { + throw new Error( + `Network was still not ready after ${(delay / 1000) * (attempt - 1)}s` + ); + } + try { + // eslint-disable-next-line no-await-in-loop + lastBlock = await fetchLastBlock(graphqlEndpoint); + } catch (e) { + // continue + } + // eslint-disable-next-line no-await-in-loop + await sleep(delay); + } + + Provable.log("Network is ready", lastBlock); +} +/* eslint-enable no-console */ +/* eslint-enable func-names */ diff --git a/packages/cli/src/scripts/lightnetInitialize.ts b/packages/cli/src/scripts/lightnetInitialize.ts new file mode 100644 index 000000000..ab1f97d2f --- /dev/null +++ b/packages/cli/src/scripts/lightnetInitialize.ts @@ -0,0 +1,32 @@ +/* eslint-disable no-console */ + +import { + LoadEnvOptions, + getRequiredEnv, + loadEnvironmentVariables, +} from "../utils/loadEnv"; + +import lightnetWaitForNetworkScript from "./lightnet/wait-for-network"; +import lightnetFaucetScript from "./lightnet/faucet"; +import settlementDeployScript from "./settlement/deploy"; + +export async function lightnetInitializeCommand(options?: LoadEnvOptions) { + loadEnvironmentVariables(options); + + console.log("Step 1: Waiting for network to be ready..."); + await lightnetWaitForNetworkScript(options); + + console.log("Step 2: Funding PROTOKIT_SEQUENCER_PUBLIC_KEY from faucet..."); + await lightnetFaucetScript(getRequiredEnv("PROTOKIT_SEQUENCER_PUBLIC_KEY")); + + console.log("Step 3: Funding TEST_ACCOUNT_1_PUBLIC_KEY from faucet..."); + await lightnetFaucetScript(getRequiredEnv("TEST_ACCOUNT_1_PUBLIC_KEY")); + + console.log("Step 4: Deploying settlement contracts..."); + await settlementDeployScript(options); + + console.log( + "Lightnet initialization complete! Settlement contracts are deployed." + ); +} +/* eslint-enable no-console */ diff --git a/packages/cli/src/scripts/settlement/deploy-token.ts b/packages/cli/src/scripts/settlement/deploy-token.ts new file mode 100644 index 000000000..96d06a92d --- /dev/null +++ b/packages/cli/src/scripts/settlement/deploy-token.ts @@ -0,0 +1,263 @@ +/* eslint-disable no-console */ +/* eslint-disable func-names */ +import { Runtime } from "@proto-kit/module"; +import { DispatchSmartContract, Protocol } from "@proto-kit/protocol"; +import { + ArchiveNode, + MinaTransactionSender, + ProvenSettlementPermissions, + Sequencer, + SettlementModule, + SignedSettlementPermissions, + AppChain, + BridgingModule, +} from "@proto-kit/sequencer"; +import { + AccountUpdate, + Bool, + fetchAccount, + Mina, + PrivateKey, + Provable, + PublicKey, + UInt64, + UInt8, +} from "o1js"; +import "reflect-metadata"; +import { container } from "tsyringe"; +import { FungibleToken, FungibleTokenAdmin } from "mina-fungible-token"; +import { DefaultConfigs, DefaultModules } from "@proto-kit/stack"; + +import { loadEnvironmentVariables, LoadEnvOptions } from "../../utils/loadEnv"; +import { loadUserModules } from "../../utils/loadUserModules"; + +export interface TokenDeployArgs { + tokenSymbol: string; + feepayerKey: string; + receiverPublicKey: string; + mintAmount: number; +} + +export default async function ( + options?: LoadEnvOptions, + tokenArgs?: TokenDeployArgs +) { + if (!tokenArgs) { + throw new Error( + "Token deployment arguments required: tokenSymbol, feepayerKey, receiverPublicKey, [mintAmount]" + ); + } + + loadEnvironmentVariables(options); + const { runtime, protocol } = await loadUserModules(); + const appChain = AppChain.from({ + Runtime: Runtime.from(runtime.modules), + Protocol: Protocol.from({ + ...protocol.modules, + ...protocol.settlementModules, + }), + Sequencer: Sequencer.from({ + ...DefaultModules.PrismaRedisDatabase(), + ...DefaultModules.settlementScript(), + }), + }); + + appChain.configure({ + Runtime: runtime.config, + Protocol: { + ...protocol.config, + ...protocol.settlementModulesConfig, + }, + Sequencer: { + ...DefaultConfigs.prismaRedisDatabase({ + preset: "development", + overrides: { + pruneOnStartup: false, + }, + }), + ...DefaultConfigs.settlementScript({ + preset: "development", + }), + }, + }); + + const chainContainer = container.createChildContainer(); + const proofsEnabled = process.env.PROTOKIT_PROOFS_ENABLED === "true"; + await appChain.start(proofsEnabled, chainContainer); + const { tokenSymbol } = tokenArgs; + const feepayerPrivateKey = PrivateKey.fromBase58( + process.env[tokenArgs.feepayerKey] ?? tokenArgs.feepayerKey + ); + const receiverPublicKey = PublicKey.fromBase58( + process.env[tokenArgs.receiverPublicKey] ?? tokenArgs.receiverPublicKey + ); + const mintAmount = tokenArgs.mintAmount * 1e9; + const fee = 0.1 * 1e9; + + const settlementModule = appChain.sequencer.resolveOrFail( + "SettlementModule", + SettlementModule + ); + + const bridgingModule = appChain.sequencer.resolveOrFail( + "BridgingModule", + BridgingModule + ); + + const isSignedSettlement = settlementModule.utils.isSignedSettlement(); + + const tokenOwnerKey = PrivateKey.fromBase58( + process.env.PROTOKIT_CUSTOM_TOKEN_PRIVATE_KEY ?? + PrivateKey.random().toBase58() + ); + const tokenAdminKey = PrivateKey.fromBase58( + process.env.PROTOKIT_CUSTOM_TOKEN_ADMIN_PRIVATE_KEY ?? + PrivateKey.random().toBase58() + ); + const tokenBridgeKey = PrivateKey.fromBase58( + process.env.PROTOKIT_CUSTOM_TOKEN_BRIDGE_PRIVATE_KEY ?? + PrivateKey.random().toBase58() + ); + + await ArchiveNode.waitOnSync(appChain.sequencer.resolve("BaseLayer").config); + + async function deployTokenContracts() { + const permissions = isSignedSettlement + ? new SignedSettlementPermissions() + : new ProvenSettlementPermissions(); + + const tx = await Mina.transaction( + { + sender: feepayerPrivateKey.toPublicKey(), + memo: "Deploy custom token", + fee, + }, + async () => { + AccountUpdate.fundNewAccount(feepayerPrivateKey.toPublicKey(), 3); + + const admin = new FungibleTokenAdmin(tokenAdminKey.toPublicKey()); + await admin.deploy({ + adminPublicKey: feepayerPrivateKey.toPublicKey(), + }); + admin.self.account.permissions.set(permissions.bridgeContractToken()); + + const fungibleToken = new FungibleToken(tokenOwnerKey.toPublicKey()); + await fungibleToken.deploy({ + src: "", + symbol: tokenSymbol, + allowUpdates: false, + }); + fungibleToken!.self.account.permissions.set( + permissions.bridgeContractToken() + ); + + await fungibleToken.initialize( + tokenAdminKey.toPublicKey(), + UInt8.from(9), + Bool(false) + ); + } + ); + console.log("Sending deploy transaction..."); + console.log(tx.toPretty()); + + settlementModule.utils.signTransaction(tx, { + signingWithSignatureCheck: [ + tokenOwnerKey.toPublicKey(), + tokenAdminKey.toPublicKey(), + ], + signingPublicKeys: [feepayerPrivateKey.toPublicKey()], + }); + + await appChain.sequencer + .resolveOrFail("TransactionSender", MinaTransactionSender) + .proveAndSendTransaction(tx, "included"); + + console.log("Deploy transaction included"); + } + + async function mint() { + const tokenOwner = new FungibleToken(tokenOwnerKey.toPublicKey()); + await settlementModule.utils.fetchContractAccounts( + { + address: tokenOwner!.address, + tokenId: tokenOwner!.tokenId, + }, + { + address: tokenOwner!.address, + tokenId: tokenOwner!.deriveTokenId(), + } + ); + + const tx = await Mina.transaction( + { + sender: feepayerPrivateKey.toPublicKey(), + memo: "Mint custom token", + fee, + }, + async () => { + AccountUpdate.fundNewAccount(feepayerPrivateKey.toPublicKey(), 1); + + await tokenOwner!.mint(receiverPublicKey, UInt64.from(mintAmount)); + } + ); + + settlementModule.utils.signTransaction(tx, { + signingPublicKeys: [feepayerPrivateKey.toPublicKey()], + signingWithSignatureCheck: [ + tokenOwnerKey.toPublicKey(), + tokenAdminKey.toPublicKey(), + ], + }); + + await appChain.sequencer + .resolveOrFail("TransactionSender", MinaTransactionSender) + .proveAndSendTransaction(tx, "included"); + } + + async function deployBridge() { + const settlement = settlementModule.getSettlementContract(); + + const dispatch = + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + bridgingModule.getDispatchContract() as DispatchSmartContract; + + await fetchAccount({ + publicKey: settlementModule.utils.getSigner(), + }); + await fetchAccount({ publicKey: settlement.address }); + await fetchAccount({ publicKey: dispatch.address }); + + const tokenOwner = new FungibleToken(tokenOwnerKey.toPublicKey()); + // SetAdminEvent. + await bridgingModule.deployTokenBridge( + tokenOwner, + tokenBridgeKey.toPublicKey(), + {} + ); + console.log( + `Token bridge address: ${tokenBridgeKey.toPublicKey().toBase58()} @ ${tokenOwner.deriveTokenId().toString()}` + ); + } + + await deployTokenContracts(); + await mint(); + await deployBridge(); + + console.log( + `Deployed custom token with id ${new FungibleToken(tokenOwnerKey.toPublicKey())!.deriveTokenId()}` + ); + + Provable.log("Deployed and initialized settlement contracts", { + settlement: PrivateKey.fromBase58( + process.env.PROTOKIT_SETTLEMENT_CONTRACT_PRIVATE_KEY! + ).toPublicKey(), + dispatcher: PrivateKey.fromBase58( + process.env.PROTOKIT_DISPATCHER_CONTRACT_PRIVATE_KEY! + ).toPublicKey(), + }); + + await appChain.close(); +} +/* eslint-enable no-console */ +/* eslint-enable func-names */ diff --git a/packages/cli/src/scripts/settlement/deploy.ts b/packages/cli/src/scripts/settlement/deploy.ts new file mode 100644 index 000000000..c36f836fe --- /dev/null +++ b/packages/cli/src/scripts/settlement/deploy.ts @@ -0,0 +1,83 @@ +/* eslint-disable no-console */ +/* eslint-disable func-names */ + +import { Runtime } from "@proto-kit/module"; +import { Protocol } from "@proto-kit/protocol"; +import { + InMemoryDatabase, + Sequencer, + SettlementModule, + AppChain, +} from "@proto-kit/sequencer"; +import { Provable, PublicKey } from "o1js"; +import "reflect-metadata"; +import { container } from "tsyringe"; +import { DefaultConfigs, DefaultModules } from "@proto-kit/stack"; + +import { + loadEnvironmentVariables, + getRequiredEnv, + LoadEnvOptions, +} from "../../utils/loadEnv"; +import { loadUserModules } from "../../utils/loadUserModules"; + +export default async function (options?: LoadEnvOptions) { + loadEnvironmentVariables(options); + const { runtime, protocol } = await loadUserModules(); + const appChain = AppChain.from({ + Runtime: Runtime.from(runtime.modules), + Protocol: Protocol.from({ + ...protocol.modules, + ...protocol.settlementModules, + }), + Sequencer: Sequencer.from({ + Database: InMemoryDatabase, + ...DefaultModules.settlementScript(), + }), + }); + + appChain.configure({ + Runtime: runtime.config, + Protocol: { + ...protocol.config, + ...protocol.settlementModulesConfig, + }, + Sequencer: { + ...DefaultConfigs.inMemoryDatabase(), + ...DefaultConfigs.settlementScript({ preset: "development" }), + }, + }); + + const chainContainer = container.createChildContainer(); + const proofsEnabled = process.env.PROTOKIT_PROOFS_ENABLED === "true"; + await appChain.start(proofsEnabled, chainContainer); + + const settlementModule = appChain.sequencer.resolveOrFail( + "SettlementModule", + SettlementModule + ); + + console.log("Deploying settlement contracts..."); + + await settlementModule.deploy({ + settlementContract: PublicKey.fromBase58( + getRequiredEnv("PROTOKIT_SETTLEMENT_CONTRACT_PUBLIC_KEY") + ), + dispatchContract: PublicKey.fromBase58( + getRequiredEnv("PROTOKIT_DISPATCHER_CONTRACT_PUBLIC_KEY") + ), + }); + + Provable.log("Deployed and initialized settlement contracts", { + settlement: PublicKey.fromBase58( + getRequiredEnv("PROTOKIT_SETTLEMENT_CONTRACT_PUBLIC_KEY") + ), + dispatcher: PublicKey.fromBase58( + getRequiredEnv("PROTOKIT_DISPATCHER_CONTRACT_PUBLIC_KEY") + ), + }); + + await appChain.close(); +} +/* eslint-enable no-console */ +/* eslint-enable func-names */ diff --git a/packages/cli/src/utils/create-environment.ts b/packages/cli/src/utils/create-environment.ts new file mode 100644 index 000000000..6f4b5baad --- /dev/null +++ b/packages/cli/src/utils/create-environment.ts @@ -0,0 +1,372 @@ +import * as fs from "fs"; +import * as path from "path"; + +import inquirer from "inquirer"; +import figuresLib from "@inquirer/figures"; +import { cyan, green, blue, gray, bold } from "kleur/colors"; + +/* eslint-disable no-console */ + +export const icons = { + checkmark: figuresLib.tick, + cross: figuresLib.cross, + arrow: figuresLib.pointerSmall, + circle: figuresLib.bullet, + square: figuresLib.square, +}; + +export type PresetType = "inmemory" | "development" | "sovereign"; + +export interface WizardAnswers { + environmentName: string; + preset: PresetType; + includeIndexer: boolean; + includeProcessor: boolean; + includeMetrics: boolean; + settlementEnabled: boolean; +} + +export const PRESET_ENV_NAMES: Record = { + inmemory: "inmemory", + development: "development", + sovereign: "sovereign", +}; + +export const PRESET_DESCRIPTIONS: Record = { + inmemory: "Fast testing and development environment", + development: "Local development environment", + sovereign: "Production-ready environment", +}; + +export const PRESET_LABELS: Record = { + inmemory: "In-Memory", + development: "Development", + sovereign: "Sovereign", +}; + +export function printHeader(): void { + console.log(bold(cyan(" ╔════════════════════════════════════════╗"))); + console.log(bold(cyan(" ║ 🚀 Proto-Kit Environment Wizard ║"))); + console.log(bold(cyan(" ╚════════════════════════════════════════╝"))); + console.log(""); +} + +export function printSection(title: string): void { + const section = `${icons.square} ${title}`; + console.log(`\n${bold(blue(section))}`); + console.log(gray("-".repeat(50))); + console.log(""); +} + +export async function selectPreset(): Promise { + const presetTypes: PresetType[] = ["inmemory", "development", "sovereign"]; + const answer = await inquirer.prompt<{ preset: PresetType }>([ + { + type: "list", + name: "preset", + message: "Select Environment Preset", + choices: presetTypes.map((type) => { + const label = PRESET_LABELS[type]; + const description = PRESET_DESCRIPTIONS[type]; + return { + name: `${label} - ${description}`, + value: type, + }; + }), + }, + ]); + + return answer.preset; +} + +export async function selectModules(preset: PresetType): Promise<{ + includeIndexer: boolean; + includeProcessor: boolean; + includeMetrics: boolean; + settlementEnabled: boolean; +}> { + const isInMemory = preset === "inmemory"; + + const answers = await inquirer.prompt<{ + includeIndexer: boolean; + includeProcessor: boolean; + includeMetrics: boolean; + settlementEnabled: boolean; + }>([ + { + type: "confirm", + name: "includeIndexer", + message: "Include Indexer Module?", + default: false, + when: !isInMemory, + }, + { + type: "confirm", + name: "includeProcessor", + message: "Include Processor Module? (requires Indexer)", + default: false, + when: (ans: WizardAnswers) => ans.includeIndexer === true, + }, + { + type: "confirm", + name: "includeMetrics", + message: "Include OpenTelemetry Metrics?", + default: false, + }, + { + type: "confirm", + name: "settlementEnabled", + message: "Enable Settlement Module?", + default: false, + }, + ]); + if (isInMemory && answers.includeIndexer === false) { + answers.includeIndexer = false; + } + if (answers.includeProcessor === false) { + answers.includeProcessor = false; + } + + return answers; +} + +export async function promptUser(): Promise { + printHeader(); + + printSection("Environment Configuration"); + + const answers = await inquirer.prompt<{ environmentName: string }>([ + { + type: "input", + name: "environmentName", + message: "Environment name (e.g 'production')", + validate: (input: string) => { + if (!input.trim()) { + return "Environment name is required"; + } + return true; + }, + }, + ]); + + const environmentName = (answers.environmentName ?? "").trim(); + const confirmationMessage = `${icons.checkmark} Environment: ${environmentName}`; + console.log(`${green(confirmationMessage)}\n`); + + const preset = await selectPreset(); + + printSection("Configure Modules"); + const modules = await selectModules(preset); + + return { + environmentName, + preset, + ...modules, + }; +} + +export function generateChainConfig(answers: WizardAnswers): string { + const presetEnv = PRESET_ENV_NAMES[answers.preset]; + const isInMemory = answers.preset === "inmemory"; + + const moduleParts: string[] = []; + + if (answers.includeMetrics) { + moduleParts.push(" ...DefaultModules.metrics(),"); + } + if (isInMemory) { + moduleParts.push(" ...DefaultModules.inMemoryDatabase(),"); + } else { + moduleParts.push(" ...DefaultModules.PrismaRedisDatabase(),"); + } + moduleParts.push( + ` ...DefaultModules.core({ settlementEnabled: ${answers.settlementEnabled} }),` + ); + if (isInMemory) { + moduleParts.push(" ...DefaultModules.localTaskQueue(),"); + } else { + moduleParts.push(" ...DefaultModules.RedisTaskQueue(),"); + } + if (answers.includeIndexer) { + moduleParts.push(" ...DefaultModules.sequencerIndexer(),"); + } + if (answers.settlementEnabled) { + moduleParts.push(" ...DefaultModules.settlement(),"); + } + const modulesString = moduleParts.join("\n"); + const configParts: string[] = []; + const coreConfig = ` ...DefaultConfigs.core({ settlementEnabled: ${answers.settlementEnabled}, preset: "${presetEnv}" }),`; + configParts.push(coreConfig); + if (answers.includeIndexer) { + configParts.push(" ...DefaultConfigs.sequencerIndexer(),"); + } + if (answers.includeMetrics) { + configParts.push( + ` ...DefaultConfigs.metrics({ preset: "${presetEnv}" }),` + ); + } + if (isInMemory) { + configParts.push(" ...DefaultConfigs.localTaskQueue(),"); + configParts.push(" ...DefaultConfigs.inMemoryDatabase(),"); + } else { + configParts.push( + ` ...DefaultConfigs.redisTaskQueue({ + preset: "${presetEnv}", + }),` + ); + configParts.push( + ` ...DefaultConfigs.prismaRedisDatabase({ + preset: "${presetEnv}", + }),` + ); + } + if (answers.settlementEnabled) { + configParts.push( + ` ...DefaultConfigs.settlement({ preset: "${presetEnv}" }),` + ); + } + const configString = configParts.join("\n"); + return `import { Runtime } from "@proto-kit/module"; +import { Protocol } from "@proto-kit/protocol"; +import { AppChain, Sequencer } from "@proto-kit/sequencer"; +import runtime from "../../../runtime"; +import * as protocol from "../../../protocol"; + +import { Arguments } from "../../../start"; +import { Startable } from "@proto-kit/common"; +import { DefaultConfigs, DefaultModules } from "@proto-kit/stack"; + +const settlementEnabled = process.env.PROTOKIT_SETTLEMENT_ENABLED! === "true"; + +const appChain = AppChain.from({ + Runtime: Runtime.from(runtime.modules), + Protocol: Protocol.from({ + ...protocol.modules, + ...(settlementEnabled ? protocol.settlementModules : {}), + }), + Sequencer: Sequencer.from({ + // ordering of the modules matters due to dependency resolution +${modulesString} + }), + ...DefaultModules.appChainBase(), +}); + +export default async (args: Arguments): Promise => { + appChain.configurePartial({ + Runtime: runtime.config, + Protocol: { + ...protocol.config, + ...(settlementEnabled ? protocol.settlementModulesConfig : {}), + }, + Sequencer: { +${configString} + }, + ...DefaultConfigs.appChainBase(), + }); + + return appChain; +};`; +} + +export function generateIndexerConfig(answers: WizardAnswers): string { + if (!answers.includeIndexer) { + return ""; + } + + const presetEnv = PRESET_ENV_NAMES[answers.preset]; + + return `import { Indexer } from "@proto-kit/indexer"; +import { DefaultConfigs, DefaultModules } from "@proto-kit/stack"; + +const indexer = Indexer.from({ + ...DefaultModules.indexer(), +}); + +export default async (): Promise => { + indexer.configurePartial({ + ...DefaultConfigs.indexer({ + preset: "${presetEnv}", + }), + }); + + return indexer; +};`; +} + +export function generateProcessorConfig(answers: WizardAnswers): string { + if (!answers.includeProcessor || !answers.includeIndexer) { + return ""; + } + + const presetEnv = PRESET_ENV_NAMES[answers.preset]; + + return `import { Processor } from "@proto-kit/processor"; +import { DefaultConfigs, DefaultModules } from "@proto-kit/stack"; + +import { handlers } from "../../processor/handlers"; +import { resolvers } from "../../processor/api/resolvers"; + +const processor = Processor.from({ + ...DefaultModules.processor(resolvers, handlers), +}); + +export default async (): Promise => { + processor.configurePartial({ + ...DefaultConfigs.processor({ + preset: "${presetEnv}", + }), + }); + + return processor; +};`; +} + +export function copyAndUpdateEnvFile( + answers: WizardAnswers, + cwd: string, + envDir: string +): boolean { + const presetEnvPath = path.join( + cwd, + "src/core/environments", + answers.preset, + ".env" + ); + + if (!fs.existsSync(presetEnvPath)) { + console.warn(`Could not find .env file at ${presetEnvPath}`); + return false; + } + + try { + let envContent = fs.readFileSync(presetEnvPath, "utf-8"); + + if (envContent.includes("PROTOKIT_ENV_FOLDER=")) { + envContent = envContent.replace( + /PROTOKIT_ENV_FOLDER=.*/g, + `PROTOKIT_ENV_FOLDER=${answers.environmentName}` + ); + } else { + const envFolder = `PROTOKIT_ENV_FOLDER=${answers.environmentName}`; + envContent = `${envFolder}\n${envContent}`; + } + + if (envContent.includes("PROTOKIT_SETTLEMENT_ENABLED=")) { + envContent = envContent.replace( + /PROTOKIT_SETTLEMENT_ENABLED=.*/g, + `PROTOKIT_SETTLEMENT_ENABLED=${answers.settlementEnabled}` + ); + } else { + envContent += `\nPROTOKIT_SETTLEMENT_ENABLED=${answers.settlementEnabled}\n`; + } + + const envFilePath = path.join(envDir, ".env"); + fs.writeFileSync(envFilePath, envContent); + + return true; + } catch (error) { + console.error(`Error copying .env file: ${error}}`); + return false; + } +} +/* eslint-enable no-console */ diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils/graphqlDocs.ts similarity index 98% rename from packages/cli/src/utils.ts rename to packages/cli/src/utils/graphqlDocs.ts index fca7a9956..cd896cd13 100644 --- a/packages/cli/src/utils.ts +++ b/packages/cli/src/utils/graphqlDocs.ts @@ -1,3 +1,5 @@ +/* eslint-disable no-console */ + import { spawn } from "child_process"; import fs from "fs"; import path from "path"; @@ -110,3 +112,4 @@ export async function generateGqlDocs(gqlUrl: string) { console.log("Docs generated successfully!"); cleanUp(generatedPath); } +/* eslint-enable no-console */ diff --git a/packages/cli/src/utils/loadEnv.ts b/packages/cli/src/utils/loadEnv.ts new file mode 100644 index 000000000..3601c6cf7 --- /dev/null +++ b/packages/cli/src/utils/loadEnv.ts @@ -0,0 +1,65 @@ +/* eslint-disable no-console */ + +import path from "path"; +import fs from "fs"; + +import dotenv from "dotenv"; + +export type LoadEnvOptions = { + envPath?: string; + envVars?: Record; +}; + +export function loadEnvironmentVariables(options?: LoadEnvOptions) { + const cwd = process.cwd(); + + if (options?.envPath !== undefined) { + if (fs.existsSync(options.envPath)) { + dotenv.config({ path: options.envPath }); + console.log(`Loaded environment from ${options.envPath}`); + } else { + throw new Error(`Environment file not found at ${options.envPath}`); + } + } else { + const envPath = path.join(cwd, "./src/core/environments/development/.env"); + + if (fs.existsSync(envPath)) { + dotenv.config({ path: envPath }); + console.log(`Loaded environment from ${envPath}`); + } else { + console.warn(`.env file not found at ${envPath}`); + } + } + if (options?.envVars !== undefined) { + Object.entries(options.envVars).forEach(([key, value]) => { + process.env[key] = value; + }); + console.log( + `Loaded ${Object.keys(options.envVars).length} environment variables from arguments` + ); + } +} + +export function getRequiredEnv(key: string): string { + const value = process.env[key]; + if (value === undefined) { + throw new Error( + `Required environment variable "${key}" is not defined. Please check your .env file or pass it as an argument.` + ); + } + return value; +} + +export function parseEnvArgs(args: string[]): Record { + const envVars: Record = {}; + + for (const arg of args) { + if (arg.includes("=")) { + const [key, value] = arg.split("=", 2); + envVars[key.trim()] = value.trim(); + } + } + + return envVars; +} +/* eslint-enable no-console */ diff --git a/packages/cli/src/utils/loadUserModules.ts b/packages/cli/src/utils/loadUserModules.ts new file mode 100644 index 000000000..3ac32d5f1 --- /dev/null +++ b/packages/cli/src/utils/loadUserModules.ts @@ -0,0 +1,60 @@ +import path from "path"; + +import { + MandatoryProtocolModulesRecord, + ProtocolModulesRecord, +} from "@proto-kit/protocol"; +import { RuntimeModulesRecord } from "@proto-kit/module"; +import { ModulesConfig } from "@proto-kit/common"; +import { Withdrawals } from "@proto-kit/library"; + +/* eslint-disable no-console */ + +type AppRuntimeModules = RuntimeModulesRecord & { + Withdrawals: typeof Withdrawals; +}; + +interface RuntimeModule { + modules: AppRuntimeModules; + config: ModulesConfig; +} + +interface ProtocolModule { + modules: ProtocolModulesRecord & MandatoryProtocolModulesRecord; + + config: ModulesConfig; + + settlementModules?: ProtocolModulesRecord; + + settlementModulesConfig?: ModulesConfig; +} + +interface LoadedModules { + runtime: RuntimeModule; + protocol: ProtocolModule; +} + +export async function loadUserModules(): Promise { + const cwd = process.cwd(); + + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const runtimeImport: { default: RuntimeModule } = await import( + path.join(cwd, "src/runtime") + ); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const protocolImport: { default: ProtocolModule } = await import( + path.join(cwd, "src/protocol") + ); + + return { + runtime: runtimeImport.default, + protocol: protocolImport.default, + }; + } catch (error) { + console.error("Failed to load runtime or protocol modules."); + throw error; + } +} + +/* eslint-enable no-console */ diff --git a/packages/explorer/src/config.ts b/packages/explorer/src/config.ts index 97529e1ab..47c6a2c6f 100644 --- a/packages/explorer/src/config.ts +++ b/packages/explorer/src/config.ts @@ -1,5 +1,6 @@ const config = { - INDEXER_URL: "http://localhost:8081/graphql", + INDEXER_URL: + process.env.NEXT_PUBLIC_INDEXER_URL ?? "http://localhost:8081/graphql", }; export default config; diff --git a/packages/processor/src/index.ts b/packages/processor/src/index.ts index b6bc05ff7..918e46e03 100644 --- a/packages/processor/src/index.ts +++ b/packages/processor/src/index.ts @@ -1,6 +1,7 @@ export * from "./Processor"; export * from "./ProcessorModule"; export * from "./handlers/HandlersExecutor"; +export * from "./handlers/BasePrismaClient"; export * from "./storage/Database"; export * from "./triggers/TimedProcessorTrigger"; export * from "./indexer/BlockFetching"; diff --git a/packages/sequencer/src/settlement/utils/SettlementUtils.ts b/packages/sequencer/src/settlement/utils/SettlementUtils.ts index 9639a2fe5..9c9f2bba8 100644 --- a/packages/sequencer/src/settlement/utils/SettlementUtils.ts +++ b/packages/sequencer/src/settlement/utils/SettlementUtils.ts @@ -109,6 +109,14 @@ export class SettlementUtils { return this.signer.registerKey(privateKey); } + public getSigner(): PublicKey { + return this.signer.getFeepayerKey(); + } + + public isSignedSettlement(): boolean { + return this.baseLayer.isSignedSettlement(); + } + /** * Fetch a set of accounts (and there update internally) with respect to what network is set */ diff --git a/packages/stack/package.json b/packages/stack/package.json index 2851234e1..9258a54cc 100644 --- a/packages/stack/package.json +++ b/packages/stack/package.json @@ -28,6 +28,8 @@ "@proto-kit/protocol": "*", "@proto-kit/sdk": "*", "@proto-kit/sequencer": "*", + "@proto-kit/indexer": "*", + "@proto-kit/processor": "*", "o1js": "^2.10.0", "tsyringe": "^4.10.0" }, @@ -35,7 +37,10 @@ "@jest/globals": "^29.5.0" }, "dependencies": { - "reflect-metadata": "^0.1.13" + "reflect-metadata": "^0.1.13", + "@prisma/client": "^5.19.1", + "mina-fungible-token": "^1.1.0", + "type-graphql": "2.0.0-rc.2" }, "gitHead": "397881ed5d8f98f5005bcd7be7f5a12b3bc6f956" } diff --git a/packages/stack/src/index.ts b/packages/stack/src/index.ts index 35d49ee83..fb31453cc 100644 --- a/packages/stack/src/index.ts +++ b/packages/stack/src/index.ts @@ -1 +1,5 @@ export * from "./scripts/graphql/server"; +export * from "./presets/config"; +export * from "./presets/modules/types"; +export * from "./presets/modules/utils"; +export * from "./presets/modules"; diff --git a/packages/stack/src/presets/config.ts b/packages/stack/src/presets/config.ts new file mode 100644 index 000000000..040b14f42 --- /dev/null +++ b/packages/stack/src/presets/config.ts @@ -0,0 +1,184 @@ +export const inmemoryConfig = { + blockInterval: 5000, + graphqlHost: "localhost", + graphqlPort: 8080, + graphiqlEnabled: true, +}; +export const developmentConfig = { + proofsEnabled: false, + + shouldAttemptDbMigration: true, + shouldAttemptIndexerDbMigration: true, + shouldAttemptProcessorDbMigration: true, + + pruneOnStartup: false, + + blockInterval: 30000, + settlementInterval: 60000, + settlementEnabled: true, + + redisHost: "localhost", + redisPort: 6379, + redisPassword: "password", + + databaseUrl: + "postgresql://admin:password@localhost:5432/protokit?schema=public", + + indexerDatabaseUrl: + "postgresql://admin:password@localhost:5433/protokit-indexer?schema=public", + + processorDatabaseUrl: + "postgresql://admin:password@localhost:5434/protokit-processor?schema=public", + + graphqlHost: "0.0.0.0", + graphqlPort: 8080, + graphiqlEnabled: true, + + indexerGraphqlHost: "0.0.0.0", + indexerGraphqlPort: 8081, + indexerGraphqlEnabled: true, + + processorGraphqlHost: "0.0.0.0", + processorGraphqlPort: 8082, + processorGraphqlEnabled: true, + + processorIndexerGraphqlHost: "0.0.0.0", + + minaNetwork: "lightnet", + minaNodeGraphqlHost: "http://localhost", + minaNodeGraphqlPort: 8083, + + minaArchiveGraphqlHost: "http://localhost", + minaArchiveGraphqlPort: 8085, + + minaAccountManagerHost: "http://localhost", + minaAccountManagerPort: 8084, + minaExplorerPort: 3001, + + transactionFeeRecipientPrivateKey: + "EKEssvj33MMBCg2tcybTzL32nTKbbwFHm6yUxd3JassdhL3J5aT8", + transactionFeeRecipientPublicKey: + "B62qk4sNnzZqqjHp8YQXZUV3dBpnjiNieJVnsuh7mD2bMJ9PdbskH5H", + + sequencerPrivateKey: "EKEdKhgUHMuDvwWJEg2TdCMCeiTSd9hh2HrEr6uYJfPVuwur1s43", + sequencerPublicKey: "B62qizW6aroTxQorJz4ywVNZom4jA6W4QPPCK3wLeyhnJHtVStUNniL", + + settlementContractPrivateKey: + "EKErS9gYHZNawqKuwfMiwYYJtNptCrvca491QEvB3tz8sFsS5w66", + settlementContractPublicKey: + "B62qjKhzrvDgTPXCp34ozmpFSx4sC9owZe6eDzhdGPdoiUbGPmBkHTt", + + dispatcherContractPrivateKey: + "EKF9Ei5G9PeB5ULMh9R6P5LfWX2gs15XxPNsect1pbcbMY9vs6v7", + dispatcherContractPublicKey: + "B62qmAzUJ1jqcsEf2V3K1k2Ec4MLsEKnodEvvJ5uweTFSLYEUALe1zs", + + minaBridgeContractPrivateKey: + "EKFKTGqWU2egLKhMgoxX8mQ21zXSE1RZYkY82mmK9F3BxdSA7E5M", + minaBridgeContractPublicKey: + "B62qn8XRkWcaBvv6F7kvarKs4cViaKRMbTUHT8FrDXLnvxuV6n7CHsN", + + customTokenPrivateKey: "EKFZHQSo5YdrcU7neDaNZruYHvCiNncvdZyKXuS6MDCW1fyCFKDP", + + customTokenAdminPrivateKey: + "EKENQ2QRc4gAJkZjQXU86ZS9MDm1e7HFiNN6LgRJnniHJt1WXDn1", + + customTokenBridgePrivateKey: + "EKENQ2QRc4gAJkZjQXU86ZS9MDm1e7HFiNN6LgRJnniHJt1WXDn1", + + testAccount1PrivateKey: + "EKF5p3wQTFd4tRBiGicRf93yXK82bcRryokC1qoazRM6wq6gMzWJ", + testAccount1PublicKey: + "B62qkVfEwyfkm5yucHEqrRjxbyx98pgdWz82pHv7LYq9Qigs812iWZ8", + + openTelemetryTracingEnabled: true, + openTelemetryTracingUrl: "http://localhost:4318", + + openTelemetryMetricsEnabled: true, + openTelemetryMetricsHost: "0.0.0.0", + openTelemetryMetricsPort: 4320, + openTelemetryMetricsScrapingFrequency: 10, +}; +export const sovereignConfig = { + blockInterval: 10000, + settlementInterval: 30000, + settlementEnabled: true, + + shouldAttemptDbMigration: true, + shouldAttemptIndexerDbMigration: true, + shouldAttemptProcessorDbMigration: true, + + pruneOnStartup: false, + + redisHost: "redis", + redisPort: 6379, + redisPassword: "password", + + databaseUrl: + "postgresql://admin:password@postgres:5432/protokit?schema=public", + + indexerDatabaseUrl: + "postgresql://admin:password@indexer-postgres:5432/protokit-indexer?schema=public", + + processorDatabaseUrl: + "postgresql://admin:password@processor-postgres:5432/protokit-processor?schema=public", + + graphqlHost: "0.0.0.0", + graphqlPort: 8080, + graphiqlEnabled: true, + + indexerGraphqlHost: "0.0.0.0", + indexerGraphqlPort: 8081, + indexerGraphqlEnabled: true, + + processorGraphqlHost: "0.0.0.0", + processorGraphqlPort: 8082, + processorGraphqlEnabled: true, + processorIndexerGraphqlHost: "indexer", + + minaNetwork: "lightnet", + minaNodeGraphqlHost: "http://lightnet", + minaNodeGraphqlPort: 8080, + + minaArchiveGraphqlHost: "http://lightnet", + minaArchiveGraphqlPort: 8282, + + minaAccountManagerHost: "http://lightnet", + minaAccountManagerPort: 8084, + minaExplorerPort: 3001, + transactionFeeRecipientPrivateKey: + "EKEssvj33MMBCg2tcybTzL32nTKbbwFHm6yUxd3JassdhL3J5aT8", + transactionFeeRecipientPublicKey: + "B62qk4sNnzZqqjHp8YQXZUV3dBpnjiNieJVnsuh7mD2bMJ9PdbskH5H", + + sequencerPrivateKey: "EKEdKhgUHMuDvwWJEg2TdCMCeiTSd9hh2HrEr6uYJfPVuwur1s43", + sequencerPublicKey: "B62qizW6aroTxQorJz4ywVNZom4jA6W4QPPCK3wLeyhnJHtVStUNniL", + + settlementContractPrivateKey: + "EKErS9gYHZNawqKuwfMiwYYJtNptCrvca491QEvB3tz8sFsS5w66", + settlementContractPublicKey: + "B62qjKhzrvDgTPXCp34ozmpFSx4sC9owZe6eDzhdGPdoiUbGPmBkHTt", + + dispatcherContractPrivateKey: + "EKF9Ei5G9PeB5ULMh9R6P5LfWX2gs15XxPNsect1pbcbMY9vs6v7", + dispatcherContractPublicKey: + "B62qmAzUJ1jqcsEf2V3K1k2Ec4MLsEKnodEvvJ5uweTFSLYEUALe1zs", + + minaBridgeContractPrivateKey: + "EKFKTGqWU2egLKhMgoxX8mQ21zXSE1RZYkY82mmK9F3BxdSA7E5M", + minaBridgeContractPublicKey: + "B62qn8XRkWcaBvv6F7kvarKs4cViaKRMbTUHT8FrDXLnvxuV6n7CHsN", + + testAccount1PrivateKey: + "EKF5p3wQTFd4tRBiGicRf93yXK82bcRryokC1qoazRM6wq6gMzWJ", + testAccount1PublicKey: + "B62qkVfEwyfkm5yucHEqrRjxbyx98pgdWz82pHv7LYq9Qigs812iWZ8", + + openTelemetryTracingEnabled: true, + openTelemetryTracingUrl: "http://otel-collector:4317", + + openTelemetryMetricsEnabled: true, + openTelemetryMetricsHost: "0.0.0.0", + openTelemetryMetricsPort: 4320, + openTelemetryMetricsScrapingFrequency: 10, +}; diff --git a/packages/stack/src/presets/modules/index.ts b/packages/stack/src/presets/modules/index.ts new file mode 100644 index 000000000..90347a2f1 --- /dev/null +++ b/packages/stack/src/presets/modules/index.ts @@ -0,0 +1,532 @@ +import { + VanillaGraphqlModules, + GraphqlSequencerModule, + GraphqlServer, + OpenTelemetryServer, +} from "@proto-kit/api"; +import { + PrivateMempool, + SequencerModulesRecord, + TimedBlockTrigger, + BlockProducerModule, + SequencerStartupModule, + LocalTaskWorkerModule, + VanillaTaskWorkerModules, + MinaBaseLayer, + ConstantFeeStrategy, + BatchProducerModule, + SettlementModule, + DatabasePruneModule, + InMemoryDatabase, + LocalTaskQueue, + AppChainModulesRecord, + InMemoryMinaSigner, +} from "@proto-kit/sequencer"; +import { + IndexerNotifier, + GeneratedResolverFactoryGraphqlModule, + IndexBlockTask, + IndexBatchTask, + IndexPendingTxTask, + IndexSettlementTask, +} from "@proto-kit/indexer"; +import { PrismaRedisDatabase } from "@proto-kit/persistance"; +import { BullQueue } from "@proto-kit/deployment"; +import { + TimedProcessorTrigger, + BlockFetching, + HandlersExecutor, + ResolverFactoryGraphqlModule, + HandlersRecord, + BasePrismaClient, +} from "@proto-kit/processor"; +import { + BlockStorageNetworkStateModule, + InMemoryTransactionSender, + StateServiceQueryModule, +} from "@proto-kit/sdk"; +import { PrivateKey } from "o1js"; +import { NonEmptyArray } from "type-graphql"; + +import { + buildCustomTokenConfig, + buildSettlementTokenConfig, + resolveEnv, +} from "./utils"; +import { + Environment, + CoreEnv, + MetricsEnv, + IndexerEnv, + ProcessorEnv, + SettlementEnv, + RedisEnv, + DatabaseEnv, + RedisTaskQueueEnv, + GraphqlServerEnv, +} from "./types"; + +export class DefaultModules { + static api() { + return { + GraphqlServer, + Graphql: GraphqlSequencerModule.from(VanillaGraphqlModules.with({})), + } satisfies SequencerModulesRecord; + } + + static core(options?: { settlementEnabled?: boolean }) { + const settlementEnabled = options?.settlementEnabled ?? false; + return { + ...(settlementEnabled ? DefaultModules.settlement() : {}), + ...DefaultModules.api(), + Mempool: PrivateMempool, + BlockProducerModule, + BlockTrigger: TimedBlockTrigger, + SequencerStartupModule, + LocalTaskWorkerModule: LocalTaskWorkerModule.from( + VanillaTaskWorkerModules.withoutSettlement() + ), + } satisfies SequencerModulesRecord; + } + + static metrics() { + return { + OpenTelemetryServer, + } satisfies SequencerModulesRecord; + } + + static settlement() { + return { + BaseLayer: MinaBaseLayer, + FeeStrategy: ConstantFeeStrategy, + BatchProducerModule, + SettlementModule, + SettlementSigner: InMemoryMinaSigner, + LocalTaskWorkerModule: LocalTaskWorkerModule.from( + VanillaTaskWorkerModules.allTasks() + ), + } satisfies SequencerModulesRecord; + } + + static sequencerIndexer() { + return { + IndexerNotifier, + } satisfies SequencerModulesRecord; + } + + static indexer() { + return { + Database: PrismaRedisDatabase, + TaskQueue: BullQueue, + TaskWorker: LocalTaskWorkerModule.from({ + IndexBlockTask, + IndexPendingTxTask, + IndexBatchTask, + IndexSettlementTask, + }), + GraphqlServer, + Graphql: GraphqlSequencerModule.from({ + GeneratedResolverFactory: GeneratedResolverFactoryGraphqlModule, + }), + } satisfies SequencerModulesRecord; + } + + static processor( + resolvers: NonEmptyArray, + handlers: HandlersRecord + ) { + return { + GraphqlServer, + GraphqlSequencerModule: GraphqlSequencerModule.from({ + ResolverFactory: ResolverFactoryGraphqlModule.from(resolvers), + }), + HandlersExecutor: HandlersExecutor.from(handlers), + BlockFetching, + Trigger: TimedProcessorTrigger, + } satisfies SequencerModulesRecord; + } + + static inMemoryDatabase() { + return { + Database: InMemoryDatabase, + } satisfies SequencerModulesRecord; + } + + static prismaRedisDatabase() { + return { + Database: PrismaRedisDatabase, + DatabasePruneModule, + } satisfies SequencerModulesRecord; + } + + static localTaskQueue() { + return { + TaskQueue: LocalTaskQueue, + } satisfies SequencerModulesRecord; + } + + static redisTaskQueue() { + return { + TaskQueue: BullQueue, + } satisfies SequencerModulesRecord; + } + + static worker() { + return { + TaskQueue: BullQueue, + LocalTaskWorkerModule: LocalTaskWorkerModule.from( + VanillaTaskWorkerModules.allTasks() + ), + } satisfies SequencerModulesRecord; + } + + static appChainBase() { + return { + TransactionSender: InMemoryTransactionSender, + QueryTransportModule: StateServiceQueryModule, + NetworkStateTransportModule: BlockStorageNetworkStateModule, + } satisfies AppChainModulesRecord; + } + + static settlementScript() { + return { + ...DefaultModules.settlement(), + Mempool: PrivateMempool, + TaskQueue: LocalTaskQueue, + SequencerStartupModule, + } satisfies SequencerModulesRecord; + } +} +export class DefaultConfigs { + static api(options?: { + preset?: Environment; + overrides?: Partial; + }) { + return { + Graphql: VanillaGraphqlModules.defaultConfig(), + ...DefaultConfigs.graphqlServer({ + preset: options?.preset, + overrides: options?.overrides, + }), + }; + } + + static core(options?: { + preset?: Environment; + overrides?: Partial & + Partial & + Partial; + settlementEnabled?: boolean; + }) { + const settlementEnabled = options?.settlementEnabled ?? false; + const config = resolveEnv(options?.preset, options?.overrides); + const apiConfig = DefaultConfigs.api({ + preset: options?.preset, + overrides: options?.overrides, + }); + const settlementConfig = settlementEnabled + ? DefaultConfigs.settlement({ + preset: options?.preset, + overrides: options?.overrides, + }) + : {}; + const blockTriggerConfig = { + blockInterval: config.blockInterval, + produceEmptyBlocks: true, + ...(settlementEnabled + ? { + settlementInterval: config.settlementInterval, + settlementTokenConfig: buildSettlementTokenConfig( + config.minaBridgeContractPrivateKey!, + buildCustomTokenConfig( + config.customTokenPrivateKey, + config.customTokenBridgePrivateKey + ) + ), + } + : { settlementTokenConfig: {} }), + }; + + return { + ...apiConfig, + Mempool: {}, + BlockProducerModule: {}, + BlockTrigger: blockTriggerConfig, + SequencerStartupModule: {}, + LocalTaskWorkerModule: VanillaTaskWorkerModules.defaultConfig(), + ...settlementConfig, + }; + } + + static metrics(options?: { + preset?: Environment; + overrides?: Partial; + }) { + const config = resolveEnv(options?.preset, options?.overrides); + return { + OpenTelemetryServer: { + metrics: { + enabled: config.metricsEnabled, + prometheus: { + host: config.metricsHost, + port: config.metricsPort, + appendTimestamp: true, + }, + nodeScrapeInterval: config.metricsScrapingFrequency, + }, + tracing: { + enabled: config.tracingEnabled, + otlp: { + url: config.tracingUrl, + }, + }, + }, + }; + } + + static sequencerIndexer() { + return { IndexerNotifier: {} }; + } + + static indexer(options?: { + preset?: Environment; + overrides?: Partial; + }) { + const config = resolveEnv(options?.preset, options?.overrides); + const taskQueueConfig = DefaultConfigs.redisTaskQueue({ + preset: options?.preset, + overrides: options?.overrides, + }); + const databaseConfig = DefaultConfigs.prismaRedisDatabase({ + preset: options?.preset, + overrides: { + databaseUrl: config.indexerDatabaseUrl, + ...options?.overrides, + }, + }); + const graphqlServerConfig = DefaultConfigs.graphqlServer({ + preset: options?.preset, + overrides: { + graphqlHost: config.indexerGraphqlHost, + graphqlPort: config.indexerGraphqlPort, + graphiqlEnabled: config.indexerGraphqlEnabled, + ...options?.overrides, + }, + }); + + return { + ...databaseConfig, + ...taskQueueConfig, + TaskWorker: { + IndexBlockTask: {}, + IndexBatchTask: {}, + IndexPendingTxTask: {}, + IndexSettlementTask: {}, + }, + ...graphqlServerConfig, + Graphql: { + GeneratedResolverFactory: {}, + }, + }; + } + + static processor(options?: { + preset?: Environment; + overrides?: Partial; + }) { + const config = resolveEnv( + options?.preset, + options?.overrides + ); + const graphqlServerConfig = DefaultConfigs.graphqlServer({ + preset: options?.preset, + overrides: { + graphqlHost: config.processorGraphqlHost, + graphqlPort: config.processorGraphqlPort, + graphiqlEnabled: config.processorGraphqlEnabled, + ...options?.overrides, + }, + }); + return { + HandlersExecutor: {}, + BlockFetching: { + url: `http://${config.processorIndexerGraphqlHost}:${config.indexerGraphqlPort}`, + }, + Trigger: { + interval: Number(config.blockInterval) / 5, + }, + ...graphqlServerConfig, + GraphqlSequencerModule: { + ResolverFactory: {}, + }, + }; + } + + static settlement(options?: { + preset?: Environment; + overrides?: Partial; + }) { + const config = resolveEnv( + options?.preset, + options?.overrides + ); + + return { + BaseLayer: { + network: { + type: "lightnet" as const, + graphql: config.minaNodeGraphqlHost, + archive: config.minaArchiveGraphqlHost, + accountManager: config.minaAccountManagerHost, + }, + }, + SettlementModule: { + addresses: { + SettlementContract: PrivateKey.fromBase58( + config.settlementContractPrivateKey + ).toPublicKey(), + }, + }, + SettlementSigner: { + feepayer: PrivateKey.fromBase58(config.sequencerPrivateKey), + contractKeys: [ + PrivateKey.fromBase58(config.settlementContractPrivateKey), + PrivateKey.fromBase58(config.dispatcherContractPrivateKey), + PrivateKey.fromBase58(config.minaBridgeContractPrivateKey), + ], + }, + FeeStrategy: {}, + BatchProducerModule: {}, + LocalTaskWorkerModule: VanillaTaskWorkerModules.defaultConfig(), + }; + } + + static inMemoryDatabase() { + return { Database: {} }; + } + + static prismaRedisDatabase(options?: { + preset?: Environment; + overrides?: Partial; + }) { + const preset = options?.preset ?? "development"; + const config = resolveEnv(preset, options?.overrides); + const redisConfig = DefaultConfigs.redis({ + preset, + overrides: options?.overrides, + }); + return { + Database: { + ...redisConfig, + prisma: { + connection: config.databaseUrl, + }, + }, + DatabasePruneModule: { + pruneOnStartup: config.pruneOnStartup, + }, + }; + } + + static localTaskQueue() { + return { + TaskQueue: {}, + }; + } + + static redisTaskQueue(options?: { + preset?: Environment; + overrides?: Partial; + }) { + const config = resolveEnv( + options?.preset, + options?.overrides + ); + + return { + TaskQueue: { + redis: { + host: config.redisHost, + port: config.redisPort, + password: config.redisPassword, + db: config.redisDb, + }, + retryAttempts: config.retryAttempts, + }, + }; + } + + static graphqlServer(options?: { + preset?: Environment; + overrides?: Partial; + }) { + const config = resolveEnv( + options?.preset, + options?.overrides + ); + + return { + GraphqlServer: { + port: config.graphqlPort, + host: config.graphqlHost, + graphiql: config.graphiqlEnabled, + }, + }; + } + + static redis(options?: { + preset?: Environment; + overrides?: Partial; + }) { + const config = resolveEnv(options?.preset, options?.overrides); + + return { + redis: { + host: config.redisHost, + port: config.redisPort, + password: config.redisPassword, + }, + }; + } + + static appChainBase() { + return { + QueryTransportModule: {}, + NetworkStateTransportModule: {}, + TransactionSender: {}, + }; + } + + static worker(options?: { + preset?: Environment; + overrides?: Partial; + }) { + const taskQueueConfig = DefaultConfigs.redisTaskQueue({ + preset: options?.preset, + overrides: options?.overrides, + }); + + return { + ...taskQueueConfig, + LocalTaskWorkerModule: VanillaTaskWorkerModules.defaultConfig(), + }; + } + + static settlementScript(options?: { + preset?: Environment; + overrides?: Partial; + }) { + const settlementConfig = DefaultConfigs.settlement({ + preset: options?.preset, + overrides: options?.overrides, + }); + return { + ...settlementConfig, + SequencerStartupModule: {}, + TaskQueue: { + simulatedDuration: 0, + }, + Mempool: {}, + }; + } +} diff --git a/packages/stack/src/presets/modules/types.ts b/packages/stack/src/presets/modules/types.ts new file mode 100644 index 000000000..65efe1603 --- /dev/null +++ b/packages/stack/src/presets/modules/types.ts @@ -0,0 +1,63 @@ +export type Environment = "inmemory" | "development" | "sovereign"; + +export type GraphqlServerEnv = { + graphqlPort: number; + graphqlHost: string; + graphiqlEnabled: boolean; +}; +export type CoreEnv = { + blockInterval: number; + settlementInterval?: number; + minaBridgeContractPrivateKey?: string; + customTokenPrivateKey?: string; + customTokenBridgePrivateKey?: string; +}; +export type MetricsEnv = { + metricsEnabled: boolean; + metricsHost: string; + metricsPort: number; + metricsScrapingFrequency: number; + tracingEnabled: boolean; + tracingUrl: string; +}; +export type SettlementEnv = { + minaNetwork: string; + minaNodeGraphqlHost: string; + minaNodeGraphqlPort: number; + minaArchiveGraphqlHost: string; + minaArchiveGraphqlPort: number; + minaAccountManagerHost: string; + minaAccountManagerPort: number; + sequencerPrivateKey: string; + settlementContractPrivateKey: string; + dispatcherContractPrivateKey: string; + minaBridgeContractPrivateKey: string; +}; +export type IndexerEnv = RedisTaskQueueEnv & { + indexerDatabaseUrl: string; + indexerGraphqlHost: string; + indexerGraphqlPort: number; + indexerGraphqlEnabled: boolean; + pruneOnStartup?: boolean; +}; +export type ProcessorEnv = { + processorIndexerGraphqlHost: string; + indexerGraphqlPort: number; + blockInterval: number; + processorGraphqlHost: string; + processorGraphqlPort: number; + processorGraphqlEnabled: boolean; +}; +export type DatabaseEnv = RedisEnv & { + databaseUrl: string; + pruneOnStartup?: boolean; +}; +export type RedisEnv = { + redisHost: string; + redisPort: number; + redisPassword: string; +}; +export type RedisTaskQueueEnv = RedisEnv & { + redisDb?: number; + retryAttempts?: number; +}; diff --git a/packages/stack/src/presets/modules/utils.ts b/packages/stack/src/presets/modules/utils.ts new file mode 100644 index 000000000..ebe16616f --- /dev/null +++ b/packages/stack/src/presets/modules/utils.ts @@ -0,0 +1,62 @@ +import { PrivateKey, TokenId } from "o1js"; +import { FungibleToken } from "mina-fungible-token"; + +import { developmentConfig, inmemoryConfig, sovereignConfig } from "../config"; + +import { Environment } from "./types"; + +export function getConfigs(preset: Environment) { + switch (preset) { + case "development": + return developmentConfig; + case "sovereign": + return sovereignConfig; + case "inmemory": + default: + return inmemoryConfig; + } +} +export function resolveEnv( + preset: Environment = "inmemory", + envs?: Partial | undefined +): T { + const config = getConfigs(preset); + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + if (!envs) return config as T; + const resolved = { ...config, ...envs } satisfies Partial; + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return resolved as T; +} +export function buildCustomTokenConfig( + customTokenPrivateKey?: string, + customTokenBridgePrivateKey?: string +) { + if ( + customTokenPrivateKey === undefined || + customTokenBridgePrivateKey === undefined + ) { + return {}; + } + const pk = PrivateKey.fromBase58(customTokenPrivateKey); + const tokenId = TokenId.derive(pk.toPublicKey()).toString(); + return { + [tokenId]: { + bridgingContractPrivateKey: PrivateKey.fromBase58( + customTokenBridgePrivateKey + ), + tokenOwner: FungibleToken, + tokenOwnerPrivateKey: customTokenPrivateKey, + }, + }; +} +export function buildSettlementTokenConfig( + bridgePrivateKey: string, + customTokens: Record = {} +) { + return { + "1": { + bridgingContractPrivateKey: PrivateKey.fromBase58(bridgePrivateKey), + }, + ...customTokens, + }; +}