diff --git a/src/commands/account/create.ts b/src/commands/account/create.ts index a9000c4a..8972f8af 100644 --- a/src/commands/account/create.ts +++ b/src/commands/account/create.ts @@ -1,22 +1,37 @@ import { Flags } from "@oclif/core"; import chalk from "chalk"; -import { ChainAccount, encrypt } from "../../lib/index.js"; +import { ChainAccount, encrypt, getSwankyConfig, isLocalConfigCheck } from "../../lib/index.js"; import { AccountData } from "../../types/index.js"; import inquirer from "inquirer"; +import { SwankyCommand } from "../../lib/swankyCommand.js"; +import { FileError } from "../../lib/errors.js"; +import { ConfigBuilder } from "../../lib/config-builder.js"; import { SwankyAccountCommand } from "./swankyAccountCommands.js"; export class CreateAccount extends SwankyAccountCommand { static description = "Create a new dev account in config"; static flags = { - generate: Flags.boolean({ + global: Flags.boolean({ char: "g", + description: "Create account globally stored in Swanky system config.", + + }), + new: Flags.boolean({ + char: "n", + description: "Generate a brand new account.", }), dev: Flags.boolean({ char: "d", + description: "Make this account a dev account for local network usage.", }), }; + constructor(argv: string[], baseConfig: any) { + super(argv, baseConfig); + (this.constructor as typeof SwankyCommand).ENSURE_SWANKY_CONFIG = false; + } + async run(): Promise { const { flags } = await this.parse(CreateAccount); @@ -36,8 +51,8 @@ export class CreateAccount extends SwankyAccountCommand { ); } - let tmpMnemonic: string; - if (flags.generate) { + let tmpMnemonic = ""; + if (flags.new) { tmpMnemonic = ChainAccount.generate(); console.log( `${ @@ -76,9 +91,22 @@ export class CreateAccount extends SwankyAccountCommand { accountData.mnemonic = tmpMnemonic; } - this.swankyConfig.accounts.push(accountData); + const configType = flags.global ? "global" : isLocalConfigCheck() ? "local" : "global"; + const config = configType === "global" ? getSwankyConfig("global") : getSwankyConfig("local"); + + const configBuilder = new ConfigBuilder(config).addAccount(accountData); - await this.storeConfig(); + if (config.defaultAccount === null) { + configBuilder.setDefaultAccount(accountData.alias); + } + + try { + await this.storeConfig(configBuilder.build(), configType); + } catch (cause) { + throw new FileError(`Error storing created account in ${configType} config`, { + cause, + }); + } this.log( `${chalk.greenBright("✔")} Account with alias ${chalk.yellowBright( diff --git a/src/commands/account/default.ts b/src/commands/account/default.ts new file mode 100644 index 00000000..7ffdb422 --- /dev/null +++ b/src/commands/account/default.ts @@ -0,0 +1,82 @@ +import { Args, Flags } from "@oclif/core"; +import chalk from "chalk"; +import { SwankySystemConfig } from "../../types/index.js"; +import inquirer from "inquirer"; +import { SwankyCommand } from "../../lib/swankyCommand.js"; +import { ConfigError, FileError } from "../../lib/errors.js"; +import { getSwankyConfig, isLocalConfigCheck } from "../../lib/index.js"; +import { ConfigBuilder } from "../../lib/config-builder.js"; + +export class DefaultAccount extends SwankyCommand { + static description = "Set default account to use"; + + static flags = { + global: Flags.boolean({ + char: "g", + description: "Set default account globally in Swanky system config.", + }), + }; + + static args = { + accountAlias: Args.string({ + name: "accountAlias", + required: false, + description: "Alias of account to be used as default", + }), + }; + + constructor(argv: string[], baseConfig: any) { + super(argv, baseConfig); + (this.constructor as typeof SwankyCommand).ENSURE_SWANKY_CONFIG = false; + } + + async run(): Promise { + const { args, flags } = await this.parse(DefaultAccount); + + const configType = flags.global ? "global" : isLocalConfigCheck() ? "local" : "global"; + const config = configType === "global" ? getSwankyConfig("global") : getSwankyConfig("local"); + + const accountAlias = args.accountAlias ?? (await this.promptForAccountAlias(config)); + this.ensureAccountExists(config, accountAlias); + + const newConfig = new ConfigBuilder(config).setDefaultAccount(accountAlias).build(); + + try { + await this.storeConfig(newConfig, configType); + } catch (cause) { + throw new FileError(`Error storing default account in ${configType} config`, { + cause, + }); + } + + this.log( + `${chalk.greenBright("✔")} Account with alias ${chalk.yellowBright( + accountAlias + )} set as default in ${configType} config` + ); + } + + private async promptForAccountAlias(config: SwankySystemConfig): Promise { + const choices = config.accounts.map((account) => ({ + name: `${account.alias} (${account.address})`, + value: account.alias, + })); + + const answer = await inquirer.prompt([ + { + type: "list", + name: "defaultAccount", + message: "Select default account", + choices: choices, + }, + ]); + + return answer.defaultAccount; + } + + private ensureAccountExists(config: SwankySystemConfig, alias: string) { + const isSomeAccount = config.accounts.some((account) => account.alias === alias); + if (!isSomeAccount) + throw new ConfigError(`Provided account alias ${chalk.yellowBright(alias)} not found`); + } +} diff --git a/src/commands/account/list.ts b/src/commands/account/list.ts index a7c294bd..67f05ecd 100644 --- a/src/commands/account/list.ts +++ b/src/commands/account/list.ts @@ -5,11 +5,36 @@ export class ListAccounts extends SwankyCommand { static description = "List dev accounts stored in config"; static aliases = [`account:ls`]; + constructor(argv: string[], baseConfig: any) { + super(argv, baseConfig); + (this.constructor as typeof SwankyCommand).ENSURE_SWANKY_CONFIG = false; + } + async run(): Promise { - this.log(`${chalk.greenBright("✔")} Stored dev accounts:`); + const countOfDevAccounts = this.swankyConfig.accounts.filter((account) => account.isDev).length; + + if(countOfDevAccounts !== 0) { + this.log(`${chalk.greenBright("✔")} Stored dev accounts:`); + + for (const account of this.swankyConfig.accounts) { + if(account.isDev){ + this.log(`\t${chalk.yellowBright("Alias: ")} ${account.alias} \ +${chalk.yellowBright("Address: ")} ${account.address} ${this.swankyConfig.defaultAccount === account.alias ? chalk.greenBright("<- Default") : ""}`); + } + } + } + + const countOfProdAccounts = this.swankyConfig.accounts.length - countOfDevAccounts; + + if(countOfProdAccounts !== 0) { + this.log(`${chalk.greenBright("✔")} Stored prod accounts:`); - for (const account of this.swankyConfig.accounts) { - this.log(`\t${chalk.yellowBright("Alias: ")} ${account.alias}`); + for (const account of this.swankyConfig.accounts) { + if(!account.isDev){ + this.log(`\t${chalk.yellowBright("Alias: ")} ${account.alias} \ +${chalk.yellowBright("Address: ")} ${account.address} ${this.swankyConfig.defaultAccount === account.alias ? chalk.greenBright("<- Default") : ""}`); + } + } } } } diff --git a/src/commands/check/index.ts b/src/commands/check/index.ts index 1875a8eb..eaa169fe 100644 --- a/src/commands/check/index.ts +++ b/src/commands/check/index.ts @@ -1,7 +1,7 @@ import { Listr } from "listr2"; import { commandStdoutOrNull, extractCargoContractVersion } from "../../lib/index.js"; import { SwankyConfig } from "../../types/index.js"; -import { pathExistsSync, readJSON, writeJson } from "fs-extra/esm"; +import { pathExistsSync, writeJson } from "fs-extra/esm"; import { readFileSync } from "fs"; import path from "node:path"; import TOML from "@iarna/toml"; @@ -31,7 +31,7 @@ interface Ctx { contracts: Record>; swankyNode: string | null; }; - swankyConfig?: SwankyConfig; + swankyConfig: SwankyConfig; mismatchedVersions: Record; looseDefinitionDetected: boolean; } @@ -46,11 +46,16 @@ export default class Check extends SwankyCommand { }), }; + constructor(argv: string[], baseConfig: any) { + super(argv, baseConfig); + (this.constructor as typeof SwankyCommand).ENSURE_SWANKY_CONFIG = false; + } + public async run(): Promise { const { flags } = await this.parse(Check); const swankyNodeVersion = this.swankyConfig.node.version; const isSwankyNodeInstalled = !!swankyNodeVersion; - const anyContracts = Object.keys(this.swankyConfig?.contracts).length > 0; + const anyContracts = Object.keys(this.swankyConfig.contracts ?? {}).length > 0; const tasks = new Listr([ { title: "Check OS", @@ -136,11 +141,9 @@ export default class Check extends SwankyCommand { { title: "Read ink dependencies", enabled: anyContracts, + skip: (ctx) => Object.keys(ctx.swankyConfig.contracts).length == 0, task: async (ctx) => { - const swankyConfig = await readJSON("swanky.config.json"); - ctx.swankyConfig = swankyConfig; - - for (const contract in swankyConfig.contracts) { + for (const contract in ctx.swankyConfig.contracts) { const tomlPath = path.resolve(`contracts/${contract}/Cargo.toml`); const doesCargoTomlExist = pathExistsSync(tomlPath); if (!doesCargoTomlExist) { @@ -168,7 +171,7 @@ export default class Check extends SwankyCommand { skip: (ctx) => Object.keys(ctx.versions.contracts).length === 0, enabled: anyContracts && isSwankyNodeInstalled, task: async (ctx) => { - const supportedInk = ctx.swankyConfig!.node.supportedInk; + const supportedInk = ctx.swankyConfig.node.supportedInk; const mismatched: Record = {}; Object.entries(ctx.versions.contracts).forEach(([contract, inkDependencies]) => { Object.entries(inkDependencies).forEach(([depName, version]) => { @@ -256,6 +259,7 @@ export default class Check extends SwankyCommand { contracts: {}, swankyNode: swankyNodeVersion || null, }, + swankyConfig: this.swankyConfig, looseDefinitionDetected: false, mismatchedVersions: {} }); diff --git a/src/commands/contract/compile.ts b/src/commands/contract/compile.ts index 56d87283..93a00538 100644 --- a/src/commands/contract/compile.ts +++ b/src/commands/contract/compile.ts @@ -3,9 +3,10 @@ import path from "node:path"; import { spawn } from "node:child_process"; import { pathExists } from "fs-extra/esm"; import { SwankyCommand } from "../../lib/swankyCommand.js"; -import { ensureCargoContractVersionCompatibility, extractCargoContractVersion, Spinner, storeArtifacts } from "../../lib/index.js"; +import { ensureCargoContractVersionCompatibility, extractCargoContractVersion, Spinner, storeArtifacts, configName, getSwankyConfig } from "../../lib/index.js"; import { ConfigError, InputError, ProcessError } from "../../lib/errors.js"; -import { BuildMode } from "../../index.js"; +import { BuildMode, SwankyConfig } from "../../index.js"; +import { ConfigBuilder } from "../../lib/config-builder.js"; export class CompileContract extends SwankyCommand { static description = "Compile the smart contract(s) in your contracts directory"; @@ -41,6 +42,8 @@ export class CompileContract extends SwankyCommand { async run(): Promise { const { args, flags } = await this.parse(CompileContract); + const localConfig = getSwankyConfig("local") as SwankyConfig; + if (args.contractName === undefined && !flags.all) { throw new InputError("No contracts were selected to compile", { winston: { stack: true } }); } @@ -55,7 +58,7 @@ export class CompileContract extends SwankyCommand { const contractInfo = this.swankyConfig.contracts[contractName]; if (!contractInfo) { throw new ConfigError( - `Cannot find contract info for ${contractName} contract in swanky.config.json`, + `Cannot find contract info for ${contractName} contract in "${configName()}"` ); } const contractPath = path.resolve("contracts", contractInfo.name); @@ -130,14 +133,18 @@ export class CompileContract extends SwankyCommand { return storeArtifacts(artifactsPath, contractInfo.name, contractInfo.moduleName); }, "Moving artifacts"); - this.swankyConfig.contracts[contractName].build = { - timestamp: Date.now(), - artifactsPath, - buildMode, - isVerified: false, - }; - - await this.storeConfig(); + await this.spinner.runCommand(async () => { + const buildData = { + timestamp: Date.now(), + artifactsPath, + buildMode, + isVerified: false, + }; + const newLocalConfig = new ConfigBuilder(localConfig) + .addContractBuild(args.contractName, buildData) + .build(); + await this.storeConfig(newLocalConfig, "local"); + }, "Writing config"); } } } diff --git a/src/commands/contract/deploy.ts b/src/commands/contract/deploy.ts index ac3489b4..44d3cd8d 100644 --- a/src/commands/contract/deploy.ts +++ b/src/commands/contract/deploy.ts @@ -1,22 +1,20 @@ import { Args, Flags } from "@oclif/core"; -import path from "node:path"; -import { writeJSON } from "fs-extra/esm"; import { cryptoWaitReady } from "@polkadot/util-crypto/crypto"; -import { AbiType, ChainAccount, ChainApi, decrypt, resolveNetworkUrl } from "../../lib/index.js"; -import { BuildMode, Encrypted } from "../../types/index.js"; +import { AbiType, ChainAccount, ChainApi, decrypt, resolveNetworkUrl, ensureAccountIsSet, configName, getSwankyConfig } from "../../lib/index.js"; +import { BuildMode, Encrypted, SwankyConfig } from "../../types/index.js"; import inquirer from "inquirer"; import chalk from "chalk"; import { Contract } from "../../lib/contract.js"; import { SwankyCommand } from "../../lib/swankyCommand.js"; -import { ApiError, ConfigError, FileError, ProcessError } from "../../lib/errors.js"; +import { ApiError, ConfigError, FileError, InputError, ProcessError } from "../../lib/errors.js"; +import { ConfigBuilder } from "../../lib/config-builder.js"; export class DeployContract extends SwankyCommand { static description = "Deploy contract to a running node"; static flags = { account: Flags.string({ - required: true, - description: "Alias of account to be used", + description: "Account alias to deploy contract with", }), gas: Flags.integer({ char: "g", @@ -32,6 +30,7 @@ export class DeployContract extends SwankyCommand { }), network: Flags.string({ char: "n", + default: "local", description: "Network name to connect to", }), }; @@ -47,10 +46,11 @@ export class DeployContract extends SwankyCommand { async run(): Promise { const { args, flags } = await this.parse(DeployContract); - const contractRecord = this.swankyConfig.contracts[args.contractName]; + const localConfig = getSwankyConfig("local") as SwankyConfig; + const contractRecord = localConfig.contracts[args.contractName]; if (!contractRecord) { throw new ConfigError( - `Cannot find a contract named ${args.contractName} in swanky.config.json` + `Cannot find a contract named ${args.contractName} in "${configName()}"` ); } @@ -97,7 +97,22 @@ export class DeployContract extends SwankyCommand { }); } - const accountData = this.findAccountByAlias(flags.account); + ensureAccountIsSet(flags.account, this.swankyConfig); + + const accountAlias = flags.account ?? this.swankyConfig.defaultAccount; + + if (accountAlias === null) { + throw new InputError(`An account is required to deploy ${args.contractName}`); + } + + const accountData = this.findAccountByAlias(accountAlias); + + if (accountData.isDev && flags.network !== "local") { + throw new ConfigError( + `Account ${accountAlias} is a DEV account and can only be used with local network` + ); + } + const mnemonic = accountData.isDev ? (accountData.mnemonic as string) : decrypt( @@ -149,19 +164,16 @@ export class DeployContract extends SwankyCommand { }, "Deploying")) as string; await this.spinner.runCommand(async () => { - contractRecord.deployments = [ - ...contractRecord.deployments, - { - timestamp: Date.now(), - address: contractAddress, - networkUrl, - deployerAlias: flags.account, - }, - ]; - - await writeJSON(path.resolve("swanky.config.json"), this.swankyConfig, { - spaces: 2, - }); + const deploymentData = { + timestamp: Date.now(), + address: contractAddress, + networkUrl, + deployerAlias: accountAlias, + }; + const newLocalConfig = new ConfigBuilder(localConfig) + .addContractDeployment(args.contractName, deploymentData) + .build(); + await this.storeConfig(newLocalConfig, "local"); }, "Writing config"); this.log(`Contract deployed!`); diff --git a/src/commands/contract/explain.ts b/src/commands/contract/explain.ts index b171338d..3efba84e 100644 --- a/src/commands/contract/explain.ts +++ b/src/commands/contract/explain.ts @@ -2,6 +2,7 @@ import { SwankyCommand } from "../../lib/swankyCommand.js"; import { Args } from "@oclif/core"; import { Contract } from "../../lib/contract.js"; import { ConfigError, FileError } from "../../lib/errors.js"; +import { configName } from "../../lib/index.js"; export class ExplainContract extends SwankyCommand { static description = "Explain contract messages based on the contracts' metadata"; @@ -20,7 +21,7 @@ export class ExplainContract extends SwankyCommand { const contractRecord = this.swankyConfig.contracts[args.contractName]; if (!contractRecord) { throw new ConfigError( - `Cannot find a contract named ${args.contractName} in swanky.config.json` + `Cannot find a contract named ${args.contractName} in "${configName()}"` ); } diff --git a/src/commands/contract/new.ts b/src/commands/contract/new.ts index c671886f..0547f2eb 100644 --- a/src/commands/contract/new.ts +++ b/src/commands/contract/new.ts @@ -1,12 +1,13 @@ import { Args, Flags } from "@oclif/core"; import path from "node:path"; -import { ensureDir, pathExists, pathExistsSync, writeJSON } from "fs-extra/esm"; +import { ensureDir, pathExists, pathExistsSync } from "fs-extra/esm"; import { checkCliDependencies, copyContractTemplateFiles, processTemplates, getTemplates, prepareTestFiles, + getSwankyConfig, } from "../../lib/index.js"; import { email, name, pickTemplate } from "../../lib/prompts.js"; import { paramCase, pascalCase, snakeCase } from "change-case"; @@ -14,6 +15,7 @@ import { execaCommandSync } from "execa"; import inquirer from "inquirer"; import { SwankyCommand } from "../../lib/swankyCommand.js"; import { InputError } from "../../lib/errors.js"; +import { ConfigBuilder } from "../../lib/config-builder.js"; export class NewContract extends SwankyCommand { static description = "Generate a new smart contract template inside a project"; @@ -108,13 +110,10 @@ export class NewContract extends SwankyCommand { await ensureDir(path.resolve(projectPath, "artifacts", args.contractName)); await this.spinner.runCommand(async () => { - this.swankyConfig.contracts[args.contractName] = { - name: args.contractName, - moduleName: snakeCase(args.contractName), - deployments: [], - }; - - await writeJSON(path.resolve("swanky.config.json"), this.swankyConfig, { spaces: 2 }); + const newLocalConfig = new ConfigBuilder(getSwankyConfig("local")) + .addContract(args.contractName) + .build(); + await this.storeConfig(newLocalConfig, "local"); }, "Writing config"); this.log("😎 New contract successfully generated! 😎"); diff --git a/src/commands/contract/query.ts b/src/commands/contract/query.ts index b094103c..0442c1e5 100644 --- a/src/commands/contract/query.ts +++ b/src/commands/contract/query.ts @@ -6,13 +6,15 @@ export class Query extends ContractCall { static args = { ...ContractCall.callArgs }; + static flags = { ...ContractCall.callFlags }; + public async run(): Promise { const { flags, args } = await this.parse(Query); const contract = new ContractPromise( this.api.apiInst, this.metadata, - this.deploymentInfo.address + this.deploymentInfo.address, ); const storageDepositLimit = null; @@ -27,7 +29,7 @@ export class Query extends ContractCall { gasLimit, storageDepositLimit, }, - ...flags.params + ...flags.params, ); await this.api.apiInst.disconnect(); diff --git a/src/commands/contract/test.ts b/src/commands/contract/test.ts index 3ef72bfe..29bd6e6a 100644 --- a/src/commands/contract/test.ts +++ b/src/commands/contract/test.ts @@ -9,7 +9,7 @@ import { Contract } from "../../lib/contract.js"; import { SwankyCommand } from "../../lib/swankyCommand.js"; import { ConfigError, FileError, InputError, ProcessError, TestError } from "../../lib/errors.js"; import { spawn } from "node:child_process"; -import { Spinner } from "../../lib/index.js"; +import { configName, Spinner } from "../../lib/index.js"; declare global { var contractTypesPath: string; // eslint-disable-line no-var @@ -55,7 +55,7 @@ export class TestContract extends SwankyCommand { const contractRecord = this.swankyConfig.contracts[contractName]; if (!contractRecord) { throw new ConfigError( - `Cannot find a contract named ${args.contractName} in swanky.config.json` + `Cannot find a contract named ${args.contractName} in "${configName()}"` ); } diff --git a/src/commands/contract/tx.ts b/src/commands/contract/tx.ts index b7153789..dbe13ce0 100644 --- a/src/commands/contract/tx.ts +++ b/src/commands/contract/tx.ts @@ -12,11 +12,7 @@ export class Tx extends ContractCall { char: "d", description: "Do a dry run, without signing the transaction", }), - account: Flags.string({ - required: true, - char: "a", - description: "Account to sign the transaction with", - }), + ...ContractCall.callFlags, }; static args = { ...ContractCall.callArgs }; diff --git a/src/commands/contract/verify.ts b/src/commands/contract/verify.ts index e6ea3991..a0d4b85f 100644 --- a/src/commands/contract/verify.ts +++ b/src/commands/contract/verify.ts @@ -1,10 +1,12 @@ import { Args, Flags } from "@oclif/core"; import path from "node:path"; -import { ensureCargoContractVersionCompatibility, extractCargoContractVersion, Spinner } from "../../lib/index.js"; +import { ensureCargoContractVersionCompatibility, extractCargoContractVersion, getSwankyConfig, Spinner } from "../../lib/index.js"; import { pathExists } from "fs-extra/esm"; import { SwankyCommand } from "../../lib/swankyCommand.js"; import { ConfigError, InputError, ProcessError } from "../../lib/errors.js"; import { spawn } from "node:child_process"; +import { ConfigBuilder } from "../../lib/config-builder.js"; +import { BuildData, SwankyConfig } from "../../index.js"; export class VerifyContract extends SwankyCommand { static description = "Verify the smart contract(s) in your contracts directory"; @@ -29,6 +31,8 @@ export class VerifyContract extends SwankyCommand { async run(): Promise { const { args, flags } = await this.parse(VerifyContract); + const localConfig = getSwankyConfig("local") as SwankyConfig; + const cargoContractVersion = extractCargoContractVersion(); if (cargoContractVersion === null) throw new InputError( @@ -112,11 +116,20 @@ export class VerifyContract extends SwankyCommand { `Verifying ${contractName} contract`, `${contractName} Contract verified successfully` ); - contractInfo.build.isVerified = true; - this.swankyConfig.contracts[contractName] = contractInfo; + await this.spinner.runCommand(async () => { + const buildData = { + ...contractInfo.build, + isVerified: true + } as BuildData; + + const newLocalConfig = new ConfigBuilder(localConfig) + .addContractBuild(args.contractName, buildData) + .build(); + + await this.storeConfig(newLocalConfig, "local"); + }, "Writing config"); - await this.storeConfig(); } } } diff --git a/src/commands/generate/types.ts b/src/commands/generate/types.ts index 79cf6134..a6c0ed24 100644 --- a/src/commands/generate/types.ts +++ b/src/commands/generate/types.ts @@ -1,5 +1,5 @@ import { Args } from "@oclif/core"; -import { generateTypes } from "../../lib/index.js"; +import { configName, generateTypes } from "../../lib/index.js"; import { Contract } from "../../lib/contract.js"; import { SwankyCommand } from "../../lib/swankyCommand.js"; import { ConfigError, FileError } from "../../lib/errors.js"; @@ -21,7 +21,7 @@ export class GenerateTypes extends SwankyCommand { const contractRecord = this.swankyConfig.contracts[args.contractName]; if (!contractRecord) { throw new ConfigError( - `Cannot find a contract named ${args.contractName} in swanky.config.json` + `Cannot find a contract named ${args.contractName} in "${configName()}"`, ); } @@ -29,7 +29,7 @@ export class GenerateTypes extends SwankyCommand { if (!(await contract.pathExists())) { throw new FileError( - `Path to contract ${args.contractName} does not exist: ${contract.contractPath}` + `Path to contract ${args.contractName} does not exist: ${contract.contractPath}`, ); } @@ -37,7 +37,7 @@ export class GenerateTypes extends SwankyCommand { if (!artifactsCheck.result) { throw new FileError( - `No artifact file found at path: ${artifactsCheck.missingPaths.toString()}` + `No artifact file found at path: ${artifactsCheck.missingPaths.toString()}`, ); } diff --git a/src/commands/init/index.ts b/src/commands/init/index.ts index a8915be3..6a2ed7a7 100644 --- a/src/commands/init/index.ts +++ b/src/commands/init/index.ts @@ -8,7 +8,7 @@ import inquirer from "inquirer"; import TOML from "@iarna/toml"; import { choice, email, name, pickNodeVersion, pickTemplate } from "../../lib/prompts.js"; import { - ChainAccount, + buildSwankyConfig, checkCliDependencies, copyCommonTemplateFiles, copyContractTemplateFiles, @@ -19,20 +19,14 @@ import { processTemplates, swankyNodeVersions } from "../../lib/index.js"; -import { - ALICE_URI, BOB_URI, - DEFAULT_ASTAR_NETWORK_URL, - DEFAULT_NETWORK_URL, DEFAULT_NODE_INFO, - DEFAULT_SHIBUYA_NETWORK_URL, - DEFAULT_SHIDEN_NETWORK_URL, -} from "../../lib/consts.js"; import { SwankyCommand } from "../../lib/swankyCommand.js"; import { InputError, UnknownError } from "../../lib/errors.js"; import { globby, GlobEntry } from "globby"; import { merge } from "lodash-es"; import inquirerFuzzyPath from "inquirer-fuzzy-path"; -import { SwankyConfig } from "../../types/index.js"; import chalk from "chalk"; +import { ConfigBuilder } from "../../lib/config-builder.js"; +import { DEFAULT_NODE_INFO } from "../../lib/consts.js"; type TaskFunction = (...args: any[]) => any; @@ -96,26 +90,10 @@ export class Init extends SwankyCommand { projectPath = ""; - - configBuilder: Partial = { - node: { - localPath: "", - polkadotPalletVersions: "", - supportedInk: "", - version: "", - }, - accounts: [], - networks: { - local: { url: DEFAULT_NETWORK_URL }, - astar: { url: DEFAULT_ASTAR_NETWORK_URL }, - shiden: { url: DEFAULT_SHIDEN_NETWORK_URL }, - shibuya: { url: DEFAULT_SHIBUYA_NETWORK_URL }, - }, - contracts: {}, - }; - taskQueue: Task[] = []; + configBuilder = new ConfigBuilder(buildSwankyConfig()); + async run(): Promise { const { args, flags } = await this.parse(Init); @@ -180,44 +158,23 @@ export class Init extends SwankyCommand { task: downloadNode, args: [this.projectPath, nodeInfo, this.spinner], runningMessage: "Downloading Swanky node", - callback: (result) => { - this.configBuilder.node = { - supportedInk: nodeInfo.supportedInk, - polkadotPalletVersions: nodeInfo.polkadotPalletVersions, - version: nodeInfo.version, - localPath: result, - }; - } + callback: (localPath) => this.configBuilder.updateNodeSettings({ supportedInk: nodeInfo.supportedInk, + polkadotPalletVersions: nodeInfo.polkadotPalletVersions, + version: nodeInfo.version, localPath }), }); } } - this.configBuilder.accounts = [ - { - alias: "alice", - mnemonic: ALICE_URI, - isDev: true, - address: new ChainAccount(ALICE_URI).pair.address, - }, - { - alias: "bob", - mnemonic: BOB_URI, - isDev: true, - address: new ChainAccount(BOB_URI).pair.address, - }, - ]; - - Object.keys(this.configBuilder.contracts!).forEach(async (contractName) => { + Object.keys(this.swankyConfig.contracts).forEach(async (contractName) => { await ensureDir(path.resolve(this.projectPath, "artifacts", contractName)); }); this.taskQueue.push({ - task: () => - writeJSON(path.resolve(this.projectPath, "swanky.config.json"), this.configBuilder, { - spaces: 2, - }), + task: async () => + await this.storeConfig(this.configBuilder.build(), "local", this.projectPath), args: [], runningMessage: "Writing config", + shouldExitOnError: true, }); for (const { @@ -314,13 +271,13 @@ export class Init extends SwankyCommand { runningMessage: "Processing templates", }); - this.configBuilder.contracts = { + this.configBuilder.updateContracts( { [contractName as string]: { name: contractName, moduleName: snakeCase(contractName), deployments: [], }, - }; + }); } async convert(pathToExistingProject: string, projectName: string) { @@ -390,14 +347,8 @@ export class Init extends SwankyCommand { }, }); - if (!this.configBuilder.contracts) this.configBuilder.contracts = {}; - for (const contract of confirmedCopyList.contracts) { - this.configBuilder.contracts[contract.name] = { - name: contract.name, - moduleName: contract.moduleName!, - deployments: [], - }; + this.configBuilder.addContract(contract.name, contract.moduleName); } let rootToml = await readRootCargoToml(pathToExistingProject); diff --git a/src/commands/node/install.ts b/src/commands/node/install.ts index 79beab3e..9af510e1 100644 --- a/src/commands/node/install.ts +++ b/src/commands/node/install.ts @@ -1,9 +1,9 @@ import { SwankyCommand } from "../../lib/swankyCommand.js"; import { Flags } from "@oclif/core"; -import { downloadNode, swankyNodeVersions } from "../../lib/index.js"; +import { downloadNode, getSwankyConfig, swankyNodeVersions } from "../../lib/index.js"; import path from "node:path"; -import { writeJSON } from "fs-extra/esm"; import inquirer from "inquirer"; +import { ConfigBuilder } from "../../lib/config-builder.js"; import { DEFAULT_NODE_INFO } from "../../lib/consts.js"; import { choice, pickNodeVersion } from "../../lib/prompts.js"; import { InputError } from "../../lib/errors.js"; @@ -55,23 +55,19 @@ export class InstallNode extends SwankyCommand { () => downloadNode(projectPath, nodeInfo, this.spinner), "Downloading Swanky node" )) as string; - const nodePath = path.relative(projectPath, taskResult); + const nodePath = path.resolve(projectPath, taskResult); - - this.swankyConfig.node = { - localPath: nodePath, - polkadotPalletVersions: nodeInfo.polkadotPalletVersions, - supportedInk: nodeInfo.supportedInk, - version: nodeInfo.version, - }; - - await this.spinner.runCommand( - () => - writeJSON(path.resolve(projectPath, "swanky.config.json"), this.swankyConfig, { - spaces: 2, - }), - "Updating swanky config" - ); + await this.spinner.runCommand(async () => { + const newLocalConfig = new ConfigBuilder(getSwankyConfig("local")) + .updateNodeSettings({ + localPath: nodePath, + polkadotPalletVersions: nodeInfo.polkadotPalletVersions, + supportedInk: nodeInfo.supportedInk, + version: nodeInfo.version, + }) + .build(); + await this.storeConfig(newLocalConfig, "local"); + }, "Updating swanky config"); this.log("Swanky Node Installed successfully"); } diff --git a/src/commands/node/start.ts b/src/commands/node/start.ts index e92e65e6..53a85590 100644 --- a/src/commands/node/start.ts +++ b/src/commands/node/start.ts @@ -29,9 +29,8 @@ export class StartNode extends SwankyCommand { async run(): Promise { const { flags } = await this.parse(StartNode); - if (this.swankyConfig.node.version === "") { - this.log("Node is not installed"); - return; + if(this.swankyConfig.node.localPath === "") { + this.error("Swanky node is not installed. Please run `swanky node:install` first."); } // Run persistent mode by default. non-persistent mode in case flag is provided. // Non-Persistent mode (`--dev`) allows all CORS origin, without `--dev`, users need to specify origins by `--rpc-cors`. diff --git a/src/lib/command-utils.ts b/src/lib/command-utils.ts index 4478b6ee..270ea732 100644 --- a/src/lib/command-utils.ts +++ b/src/lib/command-utils.ts @@ -1,9 +1,22 @@ import { execaCommand, execaCommandSync } from "execa"; -import { copy, emptyDir, ensureDir, readJSON } from "fs-extra/esm"; +import { copy, emptyDir, ensureDir, readJSONSync } from "fs-extra/esm"; import path from "node:path"; -import { DEFAULT_NETWORK_URL, ARTIFACTS_PATH, TYPED_CONTRACTS_PATH } from "./consts.js"; -import { SwankyConfig } from "../types/index.js"; -import { ConfigError, FileError, InputError } from "./errors.js"; +import { + DEFAULT_NETWORK_URL, + ARTIFACTS_PATH, + TYPED_CONTRACTS_PATH, + DEFAULT_SHIBUYA_NETWORK_URL, + DEFAULT_SHIDEN_NETWORK_URL, + DEFAULT_ASTAR_NETWORK_URL, + DEFAULT_ACCOUNT, + DEFAULT_CONFIG_NAME, + DEFAULT_CONFIG_FOLDER_NAME, + DEFAULT_NODE_INFO, +} from "./consts.js"; +import { SwankyConfig, SwankySystemConfig } from "../types/index.js"; +import { ConfigError, FileError } from "./errors.js"; +import { userInfo } from "os"; +import { existsSync } from "fs"; export function commandStdoutOrNull(command: string): string | null { try { @@ -14,13 +27,24 @@ export function commandStdoutOrNull(command: string): string | null { } } -export async function getSwankyConfig(): Promise { - try { - const config = await readJSON("swanky.config.json"); - return config; - } catch (cause) { - throw new InputError("Error reading swanky.config.json in the current directory!", { cause }); +export function getSwankyConfig(configType: "local" | "global"): SwankyConfig | SwankySystemConfig { + let configPath: string; + + if (configType === "global") { + configPath = getSystemConfigDirectoryPath() + `/${DEFAULT_CONFIG_NAME}`; + } else { + configPath = isEnvConfigCheck() ? process.env.SWANKY_CONFIG! : DEFAULT_CONFIG_NAME; } + + const config = readJSONSync(configPath); + return config; +} + + +export function getSystemConfigDirectoryPath(): string { + const homeDir = userInfo().homedir; + const configPath = homeDir + `/${DEFAULT_CONFIG_FOLDER_NAME}`; + return configPath; } export function resolveNetworkUrl(config: SwankyConfig, networkName: string): string { @@ -120,3 +144,66 @@ export async function generateTypes(contractName: string) { `npx typechain-polkadot --in ${relativeInputPath} --out ${relativeOutputPath}` ); } +export function ensureAccountIsSet(account: string | undefined, config: SwankyConfig) { + if(!account && config.defaultAccount === null) { + throw new ConfigError("No default account set. Please set one or provide an account alias with --account"); + } +} + +export function buildSwankyConfig() { + return { + node: { + localPath: "", + polkadotPalletVersions: DEFAULT_NODE_INFO.polkadotPalletVersions, + supportedInk: DEFAULT_NODE_INFO.supportedInk, + version: DEFAULT_NODE_INFO.version, + }, + defaultAccount: DEFAULT_ACCOUNT, + accounts: [ + { + "alias": "alice", + "mnemonic": "//Alice", + "isDev": true, + "address": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" + }, + { + "alias": "bob", + "mnemonic": "//Bob", + "isDev": true, + "address": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" + }, + ], + networks: { + local: { url: DEFAULT_NETWORK_URL }, + astar: { url: DEFAULT_ASTAR_NETWORK_URL }, + shiden: { url: DEFAULT_SHIDEN_NETWORK_URL }, + shibuya: { url: DEFAULT_SHIBUYA_NETWORK_URL }, + }, + contracts: {}, + }; +} + +export function isEnvConfigCheck(): boolean { + if (process.env.SWANKY_CONFIG === undefined) { + return false; + } else if (existsSync(process.env.SWANKY_CONFIG)) { + return true; + } else { + throw new ConfigError(`Provided config path ${process.env.SWANKY_CONFIG} does not exist`); + } +} + +export function isLocalConfigCheck(): boolean { + const defaultLocalConfigPath = process.cwd() + `/${DEFAULT_CONFIG_NAME}`; + return process.env.SWANKY_CONFIG === undefined + ? existsSync(defaultLocalConfigPath) + : existsSync(process.env.SWANKY_CONFIG); +} + +export function configName(): string { + if (!isLocalConfigCheck()) { + return DEFAULT_CONFIG_NAME + " [system config]"; + } + + return process.env.SWANKY_CONFIG?.split("/").pop() ?? DEFAULT_CONFIG_NAME; +} \ No newline at end of file diff --git a/src/lib/config-builder.ts b/src/lib/config-builder.ts new file mode 100644 index 00000000..ace3fccd --- /dev/null +++ b/src/lib/config-builder.ts @@ -0,0 +1,70 @@ +import { AccountData, BuildData, DeploymentData, SwankyConfig, SwankySystemConfig } from "../index.js"; +import { snakeCase } from "change-case"; + +export class ConfigBuilder { + private config: T; + + constructor(existingConfig: T) { + this.config = { ...existingConfig }; + } + + setDefaultAccount(account: string): ConfigBuilder { + this.config.defaultAccount = account; + return this; + } + + addAccount(account: AccountData): ConfigBuilder { + this.config.accounts.push(account); + return this; + } + + updateNetwork(name: string, url: string): ConfigBuilder { + if (this.config.networks?.[name]) { + this.config.networks[name].url = url; + } + return this; + } + + updateNodeSettings(nodeSettings: Partial): ConfigBuilder { + if ("node" in this.config) { + this.config.node = { ...this.config.node, ...nodeSettings }; + } + return this; + } + + updateContracts(contracts: SwankyConfig["contracts"]): ConfigBuilder { + if ("contracts" in this.config) { + this.config.contracts = { ...contracts }; + } + return this; + } + + addContract(name: string, moduleName?: string): ConfigBuilder { + if ("contracts" in this.config) { + this.config.contracts[name] = { + name: name, + moduleName: moduleName ?? snakeCase(name), + deployments: [], + }; + } + return this; + } + + addContractDeployment(name: string, data: DeploymentData): ConfigBuilder { + if ("contracts" in this.config) { + this.config.contracts[name].deployments.push(data); + } + return this; + } + + addContractBuild(name: string, data: BuildData): ConfigBuilder { + if ("contracts" in this.config) { + this.config.contracts[name].build = data; + } + return this; + } + + build(): T { + return this.config; + } +} diff --git a/src/lib/consts.ts b/src/lib/consts.ts index b9391283..8883913b 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -7,6 +7,10 @@ export const DEFAULT_ASTAR_NETWORK_URL = "wss://rpc.astar.network"; export const DEFAULT_SHIDEN_NETWORK_URL = "wss://rpc.shiden.astar.network"; export const DEFAULT_SHIBUYA_NETWORK_URL = "wss://shibuya.public.blastapi.io"; +export const DEFAULT_ACCOUNT = "alice"; +export const DEFAULT_CONFIG_FOLDER_NAME = "swanky"; +export const DEFAULT_CONFIG_NAME = "swanky.config.json"; + export const ARTIFACTS_PATH = "artifacts"; export const TYPED_CONTRACTS_PATH = "typedContracts"; diff --git a/src/lib/contractCall.ts b/src/lib/contractCall.ts index 9c9e0b2f..a65f4894 100644 --- a/src/lib/contractCall.ts +++ b/src/lib/contractCall.ts @@ -1,4 +1,4 @@ -import { AbiType, ChainAccount, ChainApi, decrypt, resolveNetworkUrl } from "./index.js"; +import { AbiType, ChainAccount, ChainApi, configName, ensureAccountIsSet, decrypt, resolveNetworkUrl } from "./index.js"; import { ContractData, DeploymentData, Encrypted } from "../types/index.js"; import { Args, Command, Flags, Interfaces } from "@oclif/core"; import inquirer from "inquirer"; @@ -28,6 +28,14 @@ export abstract class ContractCall extends SwankyComma }), }; + static callFlags = { + network: Flags.string({ + char: "n", + default: "local", + description: "Name of network to connect to", + }), + } + protected flags!: JoinedFlagsType; protected args!: Record; protected contractInfo!: ContractData; @@ -40,11 +48,12 @@ export abstract class ContractCall extends SwankyComma await super.init(); const { flags, args } = await this.parse(this.ctor); this.args = args; + this.flags = flags as JoinedFlagsType; const contractRecord = this.swankyConfig.contracts[args.contractName]; if (!contractRecord) { throw new ConfigError( - `Cannot find a contract named ${args.contractName} in swanky.config.json` + `Cannot find a contract named ${args.contractName} in "${configName()}"`, ); } @@ -52,7 +61,7 @@ export abstract class ContractCall extends SwankyComma if (!(await contract.pathExists())) { throw new FileError( - `Path to contract ${args.contractName} does not exist: ${contract.contractPath}` + `Path to contract ${args.contractName} does not exist: ${contract.contractPath}`, ); } @@ -60,24 +69,32 @@ export abstract class ContractCall extends SwankyComma if (!artifactsCheck.result) { throw new FileError( - `No artifact file found at path: ${artifactsCheck.missingPaths.toString()}` + `No artifact file found at path: ${artifactsCheck.missingPaths.toString()}`, ); } const deploymentData = flags.address ? contract.deployments.find( - (deployment: DeploymentData) => deployment.address === flags.address - ) + (deployment: DeploymentData) => deployment.address === flags.address, + ) : contract.deployments[0]; if (!deploymentData?.address) throw new NetworkError( - `Cannot find a deployment with address: ${flags.address} in swanky.config.json` + `Cannot find a deployment with address: ${flags.address} in "${configName()}"`, ); this.deploymentInfo = deploymentData; + ensureAccountIsSet(flags.account, this.swankyConfig); + + const accountAlias = flags.account ?? this.swankyConfig.defaultAccount; const accountData = this.findAccountByAlias(flags.account || "alice"); + + if (accountData.isDev && (flags.network !== "local" || !flags.network)) { + throw new ConfigError(`Account ${chalk.redBright(accountAlias)} is a dev account and can only be used on the local network`); + } + const networkUrl = resolveNetworkUrl(this.swankyConfig, flags.network ?? ""); const api = await ChainApi.create(networkUrl); this.api = api; @@ -86,17 +103,17 @@ export abstract class ContractCall extends SwankyComma const mnemonic = accountData.isDev ? (accountData.mnemonic as string) : decrypt( - accountData.mnemonic as Encrypted, - ( - await inquirer.prompt([ - { - type: "password", - message: `Enter password for ${chalk.yellowBright(accountData.alias)}: `, - name: "password", - }, - ]) - ).password - ); + accountData.mnemonic as Encrypted, + ( + await inquirer.prompt([ + { + type: "password", + message: `Enter password for ${chalk.yellowBright(accountData.alias)}: `, + name: "password", + }, + ]) + ).password, + ); const account = (await this.spinner.runCommand(async () => { await cryptoWaitReady(); @@ -143,7 +160,7 @@ ContractCall.baseFlags = { }), account: Flags.string({ char: "a", - description: "Account to sign the transaction with", + description: "Account alias to sign the transaction with", }), address: Flags.string({ required: false, diff --git a/src/lib/swankyCommand.ts b/src/lib/swankyCommand.ts index 4f2b5a73..a184cb86 100644 --- a/src/lib/swankyCommand.ts +++ b/src/lib/swankyCommand.ts @@ -1,11 +1,18 @@ import { Command, Flags, Interfaces } from "@oclif/core"; import chalk from "chalk"; -import { getSwankyConfig, Spinner } from "./index.js"; -import { AccountData, SwankyConfig } from "../types/index.js"; +import { buildSwankyConfig, + configName, + getSwankyConfig, + getSystemConfigDirectoryPath, Spinner } from "./index.js"; +import { AccountData, SwankyConfig, SwankySystemConfig } from "../types/index.js"; import { writeJSON } from "fs-extra/esm"; +import { existsSync, mkdirSync } from "fs"; import { BaseError, ConfigError, UnknownError } from "./errors.js"; import { swankyLogger } from "./logger.js"; import { Logger } from "winston"; +import path from "node:path"; +import { DEFAULT_CONFIG_FOLDER_NAME, DEFAULT_CONFIG_NAME } from "./consts.js"; + export type Flags = Interfaces.InferredFlags< (typeof SwankyCommand)["baseFlags"] & T["flags"] >; @@ -31,20 +38,14 @@ export abstract class SwankyCommand extends Command { args: this.ctor.args, strict: this.ctor.strict, }); + this.flags = flags as Flags; this.args = args as Args; this.logger = swankyLogger; - try { - this.swankyConfig = await getSwankyConfig(); - } catch (error) { - if ( - error instanceof Error && - error.message.includes("swanky.config.json") && - (this.constructor as typeof SwankyCommand).ENSURE_SWANKY_CONFIG - ) - throw new ConfigError("Cannot find swanky.config.json", { cause: error }); - } + this.swankyConfig = buildSwankyConfig(); + + await this.loadAndMergeConfig(); this.logger.info(`Running command: ${this.ctor.name} Args: ${JSON.stringify(this.args)} @@ -52,6 +53,91 @@ export abstract class SwankyCommand extends Command { Full command: ${JSON.stringify(process.argv)}`); } + protected async loadAndMergeConfig(): Promise { + try { + const systemConfig = getSwankyConfig("global"); + this.swankyConfig = { ...this.swankyConfig, ...systemConfig }; + } catch (error) { + this.warn( + `No Swanky system config found; creating one in "/${DEFAULT_CONFIG_FOLDER_NAME}/${DEFAULT_CONFIG_NAME}}" at home directory` + ); + await this.storeConfig(this.swankyConfig, "global"); + } + + try { + const localConfig = getSwankyConfig("local") as SwankyConfig; + this.mergeAccountsWithExistingConfig(this.swankyConfig, localConfig); + const originalDefaultAccount = this.swankyConfig.defaultAccount; + this.swankyConfig = { ...this.swankyConfig, ...localConfig }; + this.swankyConfig.defaultAccount = localConfig.defaultAccount ?? originalDefaultAccount; + } catch (error) { + this.handleLocalConfigError(error); + } + } + + private handleLocalConfigError(error: unknown): void { + this.logger.warn("No local config found"); + if ( + error instanceof Error && + error.message.includes(configName()) && + (this.constructor as typeof SwankyCommand).ENSURE_SWANKY_CONFIG + ) { + throw new ConfigError(`Cannot find ${process.env.SWANKY_CONFIG ?? DEFAULT_CONFIG_NAME}`, { + cause: error, + }); + } + } + + protected async storeConfig( + newConfig: SwankyConfig | SwankySystemConfig, + configType: "local" | "global", + projectPath?: string + ) { + let configPath: string; + + if (configType === "local") { + configPath = + process.env.SWANKY_CONFIG ?? + path.resolve(projectPath ?? process.cwd(), DEFAULT_CONFIG_NAME); + } else { + // global + configPath = getSystemConfigDirectoryPath() + `/${DEFAULT_CONFIG_NAME}`; + if ("node" in newConfig) { + // If it's a SwankyConfig, extract only the system relevant parts for the global SwankySystemConfig config + newConfig = { + defaultAccount: newConfig.defaultAccount, + accounts: newConfig.accounts, + networks: newConfig.networks, + }; + } + if (existsSync(configPath)) { + const systemConfig = getSwankyConfig("global"); + this.mergeAccountsWithExistingConfig(systemConfig, newConfig); + } + } + + this.ensureDirectoryExists(configPath); + await writeJSON(configPath, newConfig, { spaces: 2 }); + } + + private ensureDirectoryExists(filePath: string) { + const directory = path.dirname(filePath); + if (!existsSync(directory)) { + mkdirSync(directory, { recursive: true }); + } + } + + private mergeAccountsWithExistingConfig( + existingConfig: SwankySystemConfig | SwankyConfig, + newConfig: SwankySystemConfig + ) { + const accountMap = new Map( + [...existingConfig.accounts, ...newConfig.accounts].map((account) => [account.alias, account]) + ); + + newConfig.accounts = Array.from(accountMap.values()); + } + protected findAccountByAlias(alias: string): AccountData { const accountData = this.swankyConfig.accounts.find( (account: AccountData) => account.alias === alias @@ -64,10 +150,6 @@ export abstract class SwankyCommand extends Command { return accountData; } - protected async storeConfig() { - await writeJSON("swanky.config.json", this.swankyConfig, { spaces: 2 }); - } - protected async catch(err: Error & { exitCode?: number }): Promise { // add any custom logic to handle errors from the command // or simply return the parent class error handling diff --git a/src/types/index.ts b/src/types/index.ts index 0dab9358..8e0af423 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -24,7 +24,7 @@ export interface ContractData { name: string; moduleName: string; build?: BuildData; - deployments: DeploymentData[] | []; + deployments: DeploymentData[]; } export interface BuildData { @@ -40,15 +40,19 @@ export interface DeploymentData { deployerAlias: string; address: string; } -export interface SwankyConfig { +export interface SwankyConfig extends SwankySystemConfig{ node: { polkadotPalletVersions: string; localPath: string; supportedInk: string; version: string; }; - accounts: AccountData[]; contracts: Record | Record; +} + +export interface SwankySystemConfig { + defaultAccount: string | null; + accounts: AccountData[]; networks: Record }