diff --git a/package-lock.json b/package-lock.json index f5626d39d..1e9edcf8e 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,26 +28407,162 @@ "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" }, "bin": { - "proto-kit": "bin/protokit-cli.js" + "protokit": "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..e1e8f0b5c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -4,7 +4,7 @@ "type": "module", "main": "./bin/protokit-cli.js", "bin": { - "proto-kit": "./bin/protokit-cli.js" + "protokit": "./bin/protokit-cli.js" }, "publishConfig": { "access": "public" @@ -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/commands/bridge/bridge.ts b/packages/cli/src/commands/bridge/bridge.ts new file mode 100644 index 000000000..5650ca266 --- /dev/null +++ b/packages/cli/src/commands/bridge/bridge.ts @@ -0,0 +1,23 @@ +import { CommandModule } from "yargs"; + +export const bridgeCommand: CommandModule = { + command: "bridge ", + describe: "Bridge operations", + builder: async (yargs) => { + const { depositCommand } = await import("./deposit"); + const { redeemCommand } = await import("./redeem"); + const { withdrawCommand } = await import("./withdraw"); + + return yargs + .command(depositCommand) + .command(redeemCommand) + .command(withdrawCommand) + .demandCommand( + 1, + "You must specify a subcommand. Use --help to see available options." + ); + }, + handler: () => { + console.log("Use a subcommand. See --help for available options."); + }, +}; diff --git a/packages/cli/src/commands/bridge/deposit.ts b/packages/cli/src/commands/bridge/deposit.ts new file mode 100644 index 000000000..690dea9eb --- /dev/null +++ b/packages/cli/src/commands/bridge/deposit.ts @@ -0,0 +1,50 @@ +import { CommandModule } from "yargs"; + +import { addEnvironmentOptions } from "../../utils/environmentOptions"; + +interface DepositArgs { + tokenId: string; + fromKey: string; + toKey: string; + amount: number; + "env-path"?: string; + env?: string; + set?: string[]; +} + +export const depositCommand: CommandModule<{}, DepositArgs> = { + command: "deposit ", + describe: + "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", + builder: (yarg) => + addEnvironmentOptions( + 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 }) + ), + handler: async (args) => { + try { + const { default: deposit } = await import("../../scripts/bridge/deposit"); + const { parseEnvArgs } = await import("../../utils/loadEnv"); + await deposit( + { + envPath: args["env-path"], + env: args.env!, + envVars: parseEnvArgs(args.set ?? []), + }, + { + 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); + } + }, +}; diff --git a/packages/cli/src/commands/bridge/redeem.ts b/packages/cli/src/commands/bridge/redeem.ts new file mode 100644 index 000000000..cc9133f8b --- /dev/null +++ b/packages/cli/src/commands/bridge/redeem.ts @@ -0,0 +1,47 @@ +import { CommandModule } from "yargs"; + +import { addEnvironmentOptions } from "../../utils/environmentOptions"; + +interface RedeemArgs { + tokenId: string; + toKey: string; + amount: number; + "env-path"?: string; + env?: string; + set?: string[]; +} + +export const redeemCommand: CommandModule<{}, RedeemArgs> = { + command: "redeem ", + describe: + "Redeem tokens from the bridge\n\nRequires: PROTOKIT_CUSTOM_TOKEN_PRIVATE_KEY", + builder: (yarg) => + addEnvironmentOptions( + yarg + .positional("tokenId", { type: "string", demandOption: true }) + .positional("toKey", { type: "string", demandOption: true }) + .positional("amount", { type: "number", demandOption: true }) + ), + handler: async (args) => { + try { + const { default: redeem } = await import("../../scripts/bridge/redeem"); + const { parseEnvArgs } = await import("../../utils/loadEnv"); + await redeem( + { + envPath: args["env-path"], + env: args.env!, + envVars: parseEnvArgs(args.set ?? []), + }, + { + 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); + } + }, +}; diff --git a/packages/cli/src/commands/bridge/withdraw.ts b/packages/cli/src/commands/bridge/withdraw.ts new file mode 100644 index 000000000..2ec6ec8e7 --- /dev/null +++ b/packages/cli/src/commands/bridge/withdraw.ts @@ -0,0 +1,48 @@ +import { CommandModule } from "yargs"; + +import { addEnvironmentOptions } from "../../utils/environmentOptions"; + +interface WithdrawArgs { + tokenId: string; + senderKey: string; + amount: number; + "env-path"?: string; + env?: string; + set?: string[]; +} + +export const withdrawCommand: CommandModule<{}, WithdrawArgs> = { + command: "withdraw ", + describe: "Withdraw tokens\n\nRequires: NEXT_PUBLIC_PROTOKIT_GRAPHQL_URL", + builder: (yarg) => + addEnvironmentOptions( + yarg + .positional("tokenId", { type: "string", demandOption: true }) + .positional("senderKey", { type: "string", demandOption: true }) + .positional("amount", { type: "number", demandOption: true }) + ), + handler: async (args) => { + try { + const { default: withdraw } = await import( + "../../scripts/bridge/withdraw" + ); + const { parseEnvArgs } = await import("../../utils/loadEnv"); + await withdraw( + { + envPath: args["env-path"], + env: args.env!, + envVars: parseEnvArgs(args.set ?? []), + }, + { + 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); + } + }, +}; diff --git a/packages/cli/src/commands/explorer/explorer.ts b/packages/cli/src/commands/explorer/explorer.ts new file mode 100644 index 000000000..037163de4 --- /dev/null +++ b/packages/cli/src/commands/explorer/explorer.ts @@ -0,0 +1,19 @@ +import { CommandModule } from "yargs"; + +export const explorerCommand: CommandModule = { + command: "explorer ", + describe: "Explorer commands", + builder: async (yargs) => { + const { explorerStartCommand } = await import("./explorerStart"); + + return yargs + .command(explorerStartCommand) + .demandCommand( + 1, + "You must specify a subcommand. Use --help to see available options." + ); + }, + handler: () => { + console.log("Use a subcommand. See --help for available options."); + }, +}; diff --git a/packages/cli/src/commands/explorer/explorerStart.ts b/packages/cli/src/commands/explorer/explorerStart.ts new file mode 100644 index 000000000..c3d7028d6 --- /dev/null +++ b/packages/cli/src/commands/explorer/explorerStart.ts @@ -0,0 +1,52 @@ +import { CommandModule } from "yargs"; + +interface ExplorerStartArgs { + port: number; + "indexer-url"?: string; + "dashboard-title"?: string; + "dashboard-slogan"?: string; +} + +export const explorerStartCommand: CommandModule<{}, ExplorerStartArgs> = { + command: "start", + describe: "Start the explorer UI", + builder: (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", + }) + .option("dashboard-title", { + type: "string", + default: "Protokit Explorer", + describe: "Title for the explorer dashboard", + }) + .option("dashboard-slogan", { + type: "string", + default: "Explore your Protokit AppChain", + describe: "Slogan for the explorer dashboard", + }), + handler: async (args) => { + try { + const { default: explorerStart } = await import( + "../../scripts/explorer/start" + ); + await explorerStart({ + port: args.port, + indexerUrl: args["indexer-url"], + dashboardTitle: args["dashboard-title"], + dashboardSlogan: args["dashboard-slogan"], + }); + process.exit(0); + } catch (error) { + console.error("Failed to start explorer:", error); + process.exit(1); + } + }, +}; diff --git a/packages/cli/src/commands/generateGqlDocs.ts b/packages/cli/src/commands/generateGqlDocs.ts index f9fcedc17..92d96d98d 100644 --- a/packages/cli/src/commands/generateGqlDocs.ts +++ b/packages/cli/src/commands/generateGqlDocs.ts @@ -1,78 +1,44 @@ -import { - BlockStorageNetworkStateModule, - InMemoryTransactionSender, - StateServiceQueryModule, -} from "@proto-kit/sdk"; -import { Protocol } from "@proto-kit/protocol"; -import { - AppChain, - Sequencer, - VanillaTaskWorkerModules, -} from "@proto-kit/sequencer"; -import { - InMemorySequencerModules, - VanillaProtocolModules, - VanillaRuntimeModules, -} from "@proto-kit/library"; -import { - GraphqlSequencerModule, - GraphqlServer, - VanillaGraphqlModules, -} from "@proto-kit/api"; -import { Runtime } from "@proto-kit/module"; +import { CommandModule } from "yargs"; -import { generateGqlDocs } from "../utils"; - -export async function generateGqlDocsCommand(args: { - empty: boolean; +interface GenerateGqlDocsArgs { port: number; url: string; -}) { - if (args.empty) { - const { port } = args; - console.log(`Starting AppChain on port ${port}...`); - - const appChain = AppChain.from({ - Runtime: Runtime.from(VanillaRuntimeModules.with({})), - Protocol: Protocol.from(VanillaProtocolModules.with({})), - Sequencer: Sequencer.from( - InMemorySequencerModules.with({ - GraphqlServer: GraphqlServer, - Graphql: GraphqlSequencerModule.from(VanillaGraphqlModules.with({})), - }) - ), - TransactionSender: InMemoryTransactionSender, - QueryTransportModule: StateServiceQueryModule, - NetworkStateTransportModule: BlockStorageNetworkStateModule, - }); - - appChain.configurePartial({ - Runtime: VanillaRuntimeModules.defaultConfig(), - Protocol: VanillaProtocolModules.defaultConfig(), - Sequencer: { - Database: {}, - TaskQueue: {}, - LocalTaskWorkerModule: VanillaTaskWorkerModules.defaultConfig(), - Mempool: {}, - BlockProducerModule: {}, - SequencerStartupModule: {}, - BlockTrigger: { blockInterval: 5000, produceEmptyBlocks: true }, - FeeStrategy: {}, - BaseLayer: {}, - BatchProducerModule: {}, - Graphql: VanillaGraphqlModules.defaultConfig(), - GraphqlServer: { port, host: "localhost", graphiql: true }, - }, - }); - - await appChain.start(); - console.log("AppChain started successfully!"); - - const gqlUrl = `http://localhost:${port}/graphql`; - await generateGqlDocs(gqlUrl); - await appChain.close(); - } else { - console.log(`Using existing GraphQL endpoint: ${args.url}`); - await generateGqlDocs(args.url); - } + empty: boolean; } + +export const generateGqlDocsCommand: CommandModule<{}, GenerateGqlDocsArgs> = { + command: "generate-gql-docs", + describe: "Generate GraphQL docs", + builder: (yarg) => + yarg + .option("port", { + alias: "p", + type: "number", + default: 8080, + describe: "Port for the GraphQL server if creating an AppChain", + }) + .option("url", { + alias: "u", + type: "string", + default: "http://localhost:8080/graphql", + describe: "GraphQL endpoint to use if not starting AppChain", + }) + .option("empty", { + alias: "e", + type: "boolean", + default: false, + describe: "Start a new AppChain instead of using existing URL", + }), + handler: async (args) => { + try { + const { default: generateGqlDocs } = await import( + "../scripts/graphqlDocs/generateGqlDocs" + ); + await generateGqlDocs(args); + process.exit(0); + } catch (error) { + console.error("Failed to start AppChain or generate docs:", error); + process.exit(1); + } + }, +}; diff --git a/packages/cli/src/commands/lightnet/faucet.ts b/packages/cli/src/commands/lightnet/faucet.ts new file mode 100644 index 000000000..4e1da6829 --- /dev/null +++ b/packages/cli/src/commands/lightnet/faucet.ts @@ -0,0 +1,41 @@ +import { CommandModule } from "yargs"; + +import { addEnvironmentOptions } from "../../utils/environmentOptions"; + +interface FaucetArgs { + publicKey: string; + "env-path"?: string; + env?: string; + set?: string[]; +} + +export const faucetCommand: CommandModule<{}, FaucetArgs> = { + command: "faucet ", + describe: "Send MINA to an account from the lightnet faucet", + builder: (yarg) => + addEnvironmentOptions( + yarg.positional("publicKey", { + type: "string", + describe: "public key to send MINA to", + demandOption: true, + }) + ), + handler: async (args) => { + try { + const { default: faucet } = await import("../../scripts/lightnet/faucet"); + const { loadEnvironmentVariables, parseEnvArgs } = await import( + "../../utils/loadEnv" + ); + loadEnvironmentVariables({ + envPath: args["env-path"], + env: args.env!, + envVars: parseEnvArgs(args.set ?? []), + }); + await faucet(args.publicKey); + process.exit(0); + } catch (error) { + console.error("Failed to send funds from faucet:", error); + process.exit(1); + } + }, +}; diff --git a/packages/cli/src/commands/lightnet/initialize.ts b/packages/cli/src/commands/lightnet/initialize.ts new file mode 100644 index 000000000..8bbefd1e5 --- /dev/null +++ b/packages/cli/src/commands/lightnet/initialize.ts @@ -0,0 +1,33 @@ +import { CommandModule } from "yargs"; + +import { addEnvironmentOptions } from "../../utils/environmentOptions"; + +interface InitializeArgs { + "env-path"?: string; + env?: string; + set?: string[]; +} + +export const initializeCommand: CommandModule<{}, InitializeArgs> = { + command: "initialize", + describe: + "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", + builder: (yarg) => addEnvironmentOptions(yarg), + handler: async (args) => { + try { + const { default: lightnetInitialize } = await import( + "../../scripts/lightnet/lightnetInitialize" + ); + const { parseEnvArgs } = await import("../../utils/loadEnv"); + await lightnetInitialize({ + envPath: args["env-path"], + env: args.env!, + envVars: parseEnvArgs(args.set ?? []), + }); + process.exit(0); + } catch (error) { + console.error("Failed to initialize lightnet:", error); + process.exit(1); + } + }, +}; diff --git a/packages/cli/src/commands/lightnet/lightnet.ts b/packages/cli/src/commands/lightnet/lightnet.ts new file mode 100644 index 000000000..12a363c45 --- /dev/null +++ b/packages/cli/src/commands/lightnet/lightnet.ts @@ -0,0 +1,23 @@ +import { CommandModule } from "yargs"; + +export const lightnetCommand: CommandModule = { + command: "lightnet ", + describe: "Lightnet operations", + builder: async (yargs) => { + const { faucetCommand } = await import("./faucet"); + const { initializeCommand } = await import("./initialize"); + const { waitForNetworkCommand } = await import("./waitForNetwork"); + + return yargs + .command(faucetCommand) + .command(initializeCommand) + .command(waitForNetworkCommand) + .demandCommand( + 1, + "You must specify a subcommand. Use --help to see available options." + ); + }, + handler: () => { + console.log("Use a subcommand. See --help for available options."); + }, +}; diff --git a/packages/cli/src/commands/lightnet/waitForNetwork.ts b/packages/cli/src/commands/lightnet/waitForNetwork.ts new file mode 100644 index 000000000..27aba9fea --- /dev/null +++ b/packages/cli/src/commands/lightnet/waitForNetwork.ts @@ -0,0 +1,33 @@ +import { CommandModule } from "yargs"; + +import { addEnvironmentOptions } from "../../utils/environmentOptions"; + +interface WaitForNetworkArgs { + "env-path"?: string; + env?: string; + set?: string[]; +} + +export const waitForNetworkCommand: CommandModule<{}, WaitForNetworkArgs> = { + command: "wait", + describe: + "Wait for network to be ready\n\nRequires: MINA_NODE_GRAPHQL_HOST, MINA_NODE_GRAPHQL_PORT", + builder: (yarg) => addEnvironmentOptions(yarg), + handler: async (args) => { + try { + const { default: waitForNetwork } = await import( + "../../scripts/lightnet/wait-for-network" + ); + const { parseEnvArgs } = await import("../../utils/loadEnv"); + await waitForNetwork({ + envPath: args["env-path"], + env: args.env!, + envVars: parseEnvArgs(args.set ?? []), + }); + process.exit(0); + } catch (error) { + console.error("Failed to wait for network:", error); + process.exit(1); + } + }, +}; diff --git a/packages/cli/src/commands/run/generateKeys.ts b/packages/cli/src/commands/run/generateKeys.ts new file mode 100644 index 000000000..f0f7e7718 --- /dev/null +++ b/packages/cli/src/commands/run/generateKeys.ts @@ -0,0 +1,26 @@ +import { CommandModule } from "yargs"; + +import type { GenerateKeysArgs } from "../../scripts/generateKeys"; + +export const generateKeysCommand: CommandModule<{}, GenerateKeysArgs> = { + command: "generate-keys [count]", + describe: "Generate private/public key pairs for development", + builder: (yarg) => + yarg.positional("count", { + type: "number", + default: 1, + describe: "number of keys to generate", + }), + handler: async (args) => { + try { + const { default: generateKeys } = await import( + "../../scripts/generateKeys" + ); + await generateKeys({ count: args.count }); + process.exit(0); + } catch (error) { + console.error("Failed to generate keys:", error); + process.exit(1); + } + }, +}; diff --git a/packages/cli/src/commands/run/run.ts b/packages/cli/src/commands/run/run.ts new file mode 100644 index 000000000..5eb2623f8 --- /dev/null +++ b/packages/cli/src/commands/run/run.ts @@ -0,0 +1,19 @@ +import { CommandModule } from "yargs"; + +export const runCommand: CommandModule = { + command: "run ", + describe: "Run various operations", + builder: async (yargs) => { + const { generateKeysCommand } = await import("./generateKeys"); + + return yargs + .command(generateKeysCommand) + .demandCommand( + 1, + "You must specify a subcommand. Use --help to see available options." + ); + }, + handler: () => { + console.log("Use a subcommand. See --help for available options."); + }, +}; diff --git a/packages/cli/src/commands/settlement/deploy.ts b/packages/cli/src/commands/settlement/deploy.ts new file mode 100644 index 000000000..568152f4a --- /dev/null +++ b/packages/cli/src/commands/settlement/deploy.ts @@ -0,0 +1,33 @@ +import { CommandModule } from "yargs"; + +import { addEnvironmentOptions } from "../../utils/environmentOptions"; + +interface DeployArgs { + "env-path"?: string; + env?: string; + set?: string[]; +} + +export const deployCommand: CommandModule<{}, DeployArgs> = { + command: "deploy", + describe: + "Deploy settlement contracts\n\nRequires: PROTOKIT_SETTLEMENT_CONTRACT_PRIVATE_KEY, PROTOKIT_DISPATCHER_CONTRACT_PRIVATE_KEY, PROTOKIT_MINA_BRIDGE_CONTRACT_PRIVATE_KEY", + builder: (yarg) => addEnvironmentOptions(yarg), + handler: async (args) => { + try { + const { default: deploy } = await import( + "../../scripts/settlement/deploy" + ); + const { parseEnvArgs } = await import("../../utils/loadEnv"); + await deploy({ + envPath: args["env-path"], + env: args.env!, + envVars: parseEnvArgs(args.set ?? []), + }); + process.exit(0); + } catch (error) { + console.error("Failed to deploy settlement:", error); + process.exit(1); + } + }, +}; diff --git a/packages/cli/src/commands/settlement/settlement.ts b/packages/cli/src/commands/settlement/settlement.ts new file mode 100644 index 000000000..1300549bf --- /dev/null +++ b/packages/cli/src/commands/settlement/settlement.ts @@ -0,0 +1,21 @@ +import { CommandModule } from "yargs"; + +export const settlementCommand: CommandModule = { + command: "settlement ", + describe: "Settlement operations", + builder: async (yargs) => { + const { deployCommand } = await import("./deploy"); + const { tokenDeployCommand } = await import("./tokenDeploy"); + + return yargs + .command(deployCommand) + .command(tokenDeployCommand) + .demandCommand( + 1, + "You must specify a subcommand. Use --help to see available options." + ); + }, + handler: () => { + console.log("Use a subcommand. See --help for available options."); + }, +}; diff --git a/packages/cli/src/commands/settlement/tokenDeploy.ts b/packages/cli/src/commands/settlement/tokenDeploy.ts new file mode 100644 index 000000000..0f3894385 --- /dev/null +++ b/packages/cli/src/commands/settlement/tokenDeploy.ts @@ -0,0 +1,56 @@ +import { CommandModule } from "yargs"; + +import { addEnvironmentOptions } from "../../utils/environmentOptions"; + +interface TokenDeployArgs { + tokenSymbol: string; + feepayerKey: string; + receiverPublicKey: string; + mintAmount: number; + "env-path"?: string; + env?: string; + set?: string[]; +} + +export const tokenDeployCommand: CommandModule<{}, TokenDeployArgs> = { + command: + "token-deploy [mintAmount]", + describe: + "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", + builder: (yarg) => + addEnvironmentOptions( + 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 }) + ), + handler: async (args) => { + try { + const { default: tokenDeploy } = await import( + "../../scripts/settlement/deploy-token" + ); + const { parseEnvArgs } = await import("../../utils/loadEnv"); + await tokenDeploy( + { + envPath: args["env-path"], + env: args.env!, + envVars: parseEnvArgs(args.set ?? []), + }, + { + tokenSymbol: args.tokenSymbol, + feepayerKey: args.feepayerKey, + receiverPublicKey: args.receiverPublicKey, + mintAmount: args.mintAmount, + } + ); + process.exit(0); + } catch (error) { + console.error("Failed to deploy token:", error); + process.exit(1); + } + }, +}; diff --git a/packages/cli/src/commands/wizard.ts b/packages/cli/src/commands/wizard.ts new file mode 100644 index 000000000..f24ba18a2 --- /dev/null +++ b/packages/cli/src/commands/wizard.ts @@ -0,0 +1,19 @@ +import { CommandModule } from "yargs"; + +export const wizardCommand: CommandModule<{}> = { + command: "wizard", + describe: "Create a new environment configuration with guided wizard", + builder: (yarg) => yarg, + handler: async () => { + try { + const { default: createEnvironment } = await import( + "../scripts/env/create-environment" + ); + await createEnvironment(); + process.exit(0); + } catch (error) { + console.error("Failed to create environment:", error); + process.exit(1); + } + }, +}; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index fbd97cd2c..7f60b14b9 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,47 +1,36 @@ #!/usr/bin/env node + import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import { generateGqlDocsCommand } from "./commands/generateGqlDocs"; +import { runCommand } from "./commands/run/run"; +import { explorerCommand } from "./commands/explorer/explorer"; +import { wizardCommand } from "./commands/wizard"; +import { settlementCommand } from "./commands/settlement/settlement"; +import { lightnetCommand } from "./commands/lightnet/lightnet"; +import { bridgeCommand } from "./commands/bridge/bridge"; process.removeAllListeners("warning"); process.env.NODE_NO_WARNINGS = "1"; await yargs(hideBin(process.argv)) - .command( - "generate-gql-docs", - "generate GraphQL docs", - (yarg) => - yarg - .option("port", { - alias: "p", - type: "number", - default: 8080, - describe: "Port for the GraphQL server if creating an AppChain", - }) - .option("url", { - alias: "u", - type: "string", - default: "http://localhost:8080/graphql", - describe: "GraphQL endpoint to use if not starting AppChain", - }) - .option("empty", { - alias: "e", - type: "boolean", - default: false, - describe: "Start a new AppChain instead of using existing URL", - }), - async (args) => { - try { - await generateGqlDocsCommand(args); - process.exit(0); - } catch (error) { - console.error("Failed to start AppChain or generate docs:", error); - process.exit(1); - } - } + .scriptName("protokit") + .usage("$0 [options]") + .strict() + .command(generateGqlDocsCommand) + .command(wizardCommand) + .command(runCommand) + .command(explorerCommand) + .command(settlementCommand) + .command(lightnetCommand) + .command(bridgeCommand) + .demandCommand( + 1, + "You must specify a command. Use --help to see available commands." ) - .demandCommand() - .help() + .help("help") + .alias("help", "h") + .option("help", { describe: "Show help" }) .strict() .parse(); diff --git a/packages/cli/src/scripts/bridge/deposit.ts b/packages/cli/src/scripts/bridge/deposit.ts new file mode 100644 index 000000000..b85d2044b --- /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..99a94f5ac --- /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..9c78471e4 --- /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..212c819e4 --- /dev/null +++ b/packages/cli/src/scripts/env/create-environment.ts @@ -0,0 +1,116 @@ +/* eslint-disable no-console */ +/* eslint-disable func-names */ +/* eslint-disable sonarjs/cognitive-complexity */ +import * as fs from "fs"; +import * as path from "path"; + +import { cyan, green, red, bold } from "kleur/colors"; + +import { + copyAndUpdateEnvFile, + generateChainConfig, + generateIndexerConfig, + generateProcessorConfig, + generateWorkerConfig, + 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"); + const workerConfigPath = path.join(envDir, "worker.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); + } + } + + if (answers.preset !== "inmemory") { + const workerConfig = generateWorkerConfig(answers); + if (workerConfig) { + fs.writeFileSync(workerConfigPath, workerConfig); + } + } + + 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`); + } + if (answers.preset !== "inmemory") { + console.log(` ${green(icons.checkmark)} worker.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 */ +/* eslint-enable sonarjs/cognitive-complexity */ diff --git a/packages/cli/src/scripts/explorer/start.ts b/packages/cli/src/scripts/explorer/start.ts new file mode 100644 index 000000000..67abeed10 --- /dev/null +++ b/packages/cli/src/scripts/explorer/start.ts @@ -0,0 +1,64 @@ +/* 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 (args: { + port?: number; + indexerUrl?: string; + dashboardTitle?: string; + dashboardSlogan?: string; +}): Promise { + 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(args.port)], { + cwd: explorerDir, + stdio: "inherit", + env: { + ...process.env, + NODE_OPTIONS: "", + NEXT_PUBLIC_INDEXER_URL: args.indexerUrl, + NEXT_PUBLIC_DASHBOARD_TITLE: args.dashboardTitle, + NEXT_PUBLIC_DASHBOARD_SLOGAN: args.dashboardSlogan, + }, + }); + + 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..6a10e4d65 --- /dev/null +++ b/packages/cli/src/scripts/generateKeys.ts @@ -0,0 +1,21 @@ +/* eslint-disable no-console */ + +import { PrivateKey } from "o1js"; + +export type GenerateKeysArgs = { + count?: number; +}; + +export default async function (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/scripts/graphqlDocs/generateGqlDocs.ts b/packages/cli/src/scripts/graphqlDocs/generateGqlDocs.ts new file mode 100644 index 000000000..b24b59401 --- /dev/null +++ b/packages/cli/src/scripts/graphqlDocs/generateGqlDocs.ts @@ -0,0 +1,80 @@ +/* eslint-disable no-console */ +import { + BlockStorageNetworkStateModule, + InMemoryTransactionSender, + StateServiceQueryModule, +} from "@proto-kit/sdk"; +import { Protocol } from "@proto-kit/protocol"; +import { + AppChain, + Sequencer, + VanillaTaskWorkerModules, +} from "@proto-kit/sequencer"; +import { + InMemorySequencerModules, + VanillaProtocolModules, + VanillaRuntimeModules, +} from "@proto-kit/library"; +import { + GraphqlSequencerModule, + GraphqlServer, + VanillaGraphqlModules, +} from "@proto-kit/api"; +import { Runtime } from "@proto-kit/module"; + +import { generateGqlDocs } from "../../utils/graphqlDocs"; + +export default async function (args: { + empty: boolean; + port: number; + url: string; +}) { + if (args.empty) { + const { port } = args; + console.log(`Starting AppChain on port ${port}...`); + + const appChain = AppChain.from({ + Runtime: Runtime.from(VanillaRuntimeModules.with({})), + Protocol: Protocol.from(VanillaProtocolModules.with({})), + Sequencer: Sequencer.from( + InMemorySequencerModules.with({ + GraphqlServer: GraphqlServer, + Graphql: GraphqlSequencerModule.from(VanillaGraphqlModules.with({})), + }) + ), + TransactionSender: InMemoryTransactionSender, + QueryTransportModule: StateServiceQueryModule, + NetworkStateTransportModule: BlockStorageNetworkStateModule, + }); + + appChain.configurePartial({ + Runtime: VanillaRuntimeModules.defaultConfig(), + Protocol: VanillaProtocolModules.defaultConfig(), + Sequencer: { + Database: {}, + TaskQueue: {}, + LocalTaskWorkerModule: VanillaTaskWorkerModules.defaultConfig(), + Mempool: {}, + BlockProducerModule: {}, + SequencerStartupModule: {}, + BlockTrigger: { blockInterval: 5000, produceEmptyBlocks: true }, + FeeStrategy: {}, + BaseLayer: {}, + BatchProducerModule: {}, + Graphql: VanillaGraphqlModules.defaultConfig(), + GraphqlServer: { port, host: "localhost", graphiql: true }, + }, + }); + + await appChain.start(); + console.log("AppChain started successfully!"); + + const gqlUrl = `http://localhost:${port}/graphql`; + await generateGqlDocs(gqlUrl); + await appChain.close(); + } else { + console.log(`Using existing GraphQL endpoint: ${args.url}`); + 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/lightnetInitialize.ts b/packages/cli/src/scripts/lightnet/lightnetInitialize.ts new file mode 100644 index 000000000..87fb6e68a --- /dev/null +++ b/packages/cli/src/scripts/lightnet/lightnetInitialize.ts @@ -0,0 +1,32 @@ +/* eslint-disable no-console */ + +import { + LoadEnvOptions, + getRequiredEnv, + loadEnvironmentVariables, +} from "../../utils/loadEnv"; +import settlementDeployScript from "../settlement/deploy"; + +import lightnetWaitForNetworkScript from "./wait-for-network"; +import lightnetFaucetScript from "./faucet"; + +export default async function (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/lightnet/wait-for-network.ts b/packages/cli/src/scripts/lightnet/wait-for-network.ts new file mode 100644 index 000000000..2612ce338 --- /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/settlement/deploy-token.ts b/packages/cli/src/scripts/settlement/deploy-token.ts new file mode 100644 index 000000000..a71fc1ffc --- /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..e939a4403 --- /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..001782c40 --- /dev/null +++ b/packages/cli/src/utils/create-environment.ts @@ -0,0 +1,442 @@ +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(),"); + } + 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}", + overrides: { + redisDb: 1, + }, + }),` + ); + configParts.push( + ` ...DefaultConfigs.prismaRedisDatabase({ + 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 { Arguments } from "../../../start"; +import { Startable } from "@proto-kit/common"; +import { DefaultConfigs, DefaultModules } from "@proto-kit/stack"; + +const indexer = Indexer.from({ + ...DefaultModules.indexer(), +}); + +export default async (args: Arguments): Promise => { + indexer.configurePartial({ + ...DefaultConfigs.indexer({ + preset: "${presetEnv}", + overrides: { + pruneOnStartup: args.pruneOnStartup, + redisDb: 1, + }, + }), + }); + + return indexer; +};`; +} + +export function generateProcessorConfig(answers: WizardAnswers): string { + if (!answers.includeProcessor || !answers.includeIndexer) { + return ""; + } + + const presetEnv = PRESET_ENV_NAMES[answers.preset]; + + return `import { DatabasePruneModule, Processor } from "@proto-kit/processor"; +import { databaseModule } from "../../processor"; +import { Arguments } from "../../../start"; +import { Startable } from "@proto-kit/common"; +import { DefaultConfigs, DefaultModules } from "@proto-kit/stack"; + +import { handlers } from "../../processor/handlers"; +import { resolvers } from "../../processor/api/resolvers"; + +const processor = Processor.from({ + Database: databaseModule, + DatabasePruneModule: DatabasePruneModule, + ...DefaultModules.processor(resolvers, handlers), +}); + +export default async (args: Arguments): Promise => { + processor.configurePartial({ + ...DefaultConfigs.processor({ + preset: "${presetEnv}", + }), + Database: {}, + DatabasePruneModule: { + pruneOnStartup: args.pruneOnStartup, + }, + }); + + return processor; +};`; +} + +export function generateWorkerConfig(answers: WizardAnswers): string { + if (answers.preset === "inmemory") { + return ""; + } + + const presetEnv = PRESET_ENV_NAMES[answers.preset]; + const taskWorkerImports = answers.settlementEnabled + ? "" + : " LocalTaskWorkerModule, VanillaTaskWorkerModules"; + const withoutSettlementTask = answers.settlementEnabled + ? "" + : `LocalTaskWorkerModule: LocalTaskWorkerModule.from( + VanillaTaskWorkerModules.withoutSettlement() + ), + `; + return `import { Runtime } from "@proto-kit/module"; +import { Protocol } from "@proto-kit/protocol"; +import { Sequencer, AppChain, ${taskWorkerImports} } from "@proto-kit/sequencer"; +import runtime from "../../../runtime"; +import * as protocol from "../../../protocol"; +import { Arguments } from "../../../start"; + +import { log, 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({ + ...DefaultModules.worker(), + ${withoutSettlementTask} + }), +}); + +export default async (args: Arguments): Promise => { + appChain.configurePartial({ + Runtime: runtime.config, + Protocol: { + ...protocol.config, + ...(settlementEnabled ? protocol.settlementModulesConfig : {}), + }, + Sequencer: DefaultConfigs.worker({ + preset: "${presetEnv}", + overrides: { + redisDb: 1, + }, + }), + }); + + log.setLevel("DEBUG"); + + return appChain; +};`; +} + +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/environmentOptions.ts b/packages/cli/src/utils/environmentOptions.ts new file mode 100644 index 000000000..e0aa38149 --- /dev/null +++ b/packages/cli/src/utils/environmentOptions.ts @@ -0,0 +1,20 @@ +import { Argv } from "yargs"; + +export function addEnvironmentOptions(yargs: Argv): Argv { + return yargs + .option("env-path", { + type: "string", + describe: "path to .env file", + }) + .option("env", { + type: "string", + describe: + "environment name to load from src/core/environments/{environment}/.env", + default: "development", + }) + .option("set", { + type: "string", + array: true, + describe: "environment variables as KEY=value", + }); +} 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..a4e69e39f --- /dev/null +++ b/packages/cli/src/utils/loadEnv.ts @@ -0,0 +1,57 @@ +/* eslint-disable no-console */ + +import path from "path"; +import fs from "fs"; + +import dotenv from "dotenv"; + +export type LoadEnvOptions = { + envPath?: string; + envVars?: Record; + env: string; +}; + +export function loadEnvironmentVariables(options: LoadEnvOptions) { + const cwd = process.cwd(); + const env = options.envPath ?? `./src/core/environments/${options.env}/.env`; + const envPath = path.isAbsolute(env) ? env : path.join(cwd, 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/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..4edf31e9c --- /dev/null +++ b/packages/stack/src/presets/modules/utils.ts @@ -0,0 +1,73 @@ +import { PrivateKey, TokenId } from "o1js"; +import { FungibleToken } from "mina-fungible-token"; +import { SettlementTokenConfig } from "@proto-kit/sequencer"; + +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 +): SettlementTokenConfig { + if ( + customTokenPrivateKey === undefined || + customTokenBridgePrivateKey === undefined + ) { + return {}; + } + const pk = PrivateKey.fromBase58(customTokenPrivateKey); + const tokenId = TokenId.derive(pk.toPublicKey()).toString(); + + const tokenOwner = new FungibleToken( + PrivateKey.fromBase58(customTokenPrivateKey).toPublicKey() + ); + + return { + [tokenId]: { + tokenOwner: tokenOwner, + bridgingContractPublicKey: PrivateKey.fromBase58( + customTokenBridgePrivateKey + ).toPublicKey(), + tokenOwnerPublicKey: pk.toPublicKey(), + }, + }; +} +export function buildSettlementTokenConfig( + bridgePrivateKey?: string, + customTokens: SettlementTokenConfig = {} +): SettlementTokenConfig { + return { + ...(bridgePrivateKey !== undefined + ? { + "1": { + bridgingContractPublicKey: + PrivateKey.fromBase58(bridgePrivateKey).toPublicKey(), + }, + } + : {}), + ...customTokens, + }; +}