mobilestacks 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +107 -0
- package/dist/cli/init.d.ts +2 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +83 -0
- package/dist/config/config-loading.d.ts +3 -0
- package/dist/config/config-loading.d.ts.map +1 -0
- package/dist/config/config-loading.js +35 -0
- package/dist/core/dsl.d.ts +30 -0
- package/dist/core/dsl.d.ts.map +1 -0
- package/dist/core/dsl.js +68 -0
- package/dist/core/dsl.test.d.ts +2 -0
- package/dist/core/dsl.test.d.ts.map +1 -0
- package/dist/core/dsl.test.js +50 -0
- package/dist/core/env.d.ts +6 -0
- package/dist/core/env.d.ts.map +1 -0
- package/dist/core/env.js +13 -0
- package/dist/core/extender.d.ts +12 -0
- package/dist/core/extender.d.ts.map +1 -0
- package/dist/core/extender.js +25 -0
- package/dist/core/runtime-environment.d.ts +16 -0
- package/dist/core/runtime-environment.d.ts.map +1 -0
- package/dist/core/runtime-environment.js +72 -0
- package/dist/core/simnet.d.ts +17 -0
- package/dist/core/simnet.d.ts.map +1 -0
- package/dist/core/simnet.js +46 -0
- package/dist/core/tasks-definitions.d.ts +28 -0
- package/dist/core/tasks-definitions.d.ts.map +1 -0
- package/dist/core/tasks-definitions.js +24 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/src/cli/init.js +3 -3
- package/dist/src/tasks/deploy-contract.js +3 -1
- package/dist/tasks/call-contract-function.d.ts +2 -0
- package/dist/tasks/call-contract-function.d.ts.map +1 -0
- package/dist/tasks/call-contract-function.js +36 -0
- package/dist/tasks/deploy-contract.d.ts +2 -0
- package/dist/tasks/deploy-contract.d.ts.map +1 -0
- package/dist/tasks/deploy-contract.js +51 -0
- package/dist/tasks/example-task.d.ts +2 -0
- package/dist/tasks/example-task.d.ts.map +1 -0
- package/dist/tasks/example-task.js +8 -0
- package/dist/tasks/faucet-request.d.ts +2 -0
- package/dist/tasks/faucet-request.d.ts.map +1 -0
- package/dist/tasks/faucet-request.js +26 -0
- package/dist/tasks/get-balance.d.ts +2 -0
- package/dist/tasks/get-balance.d.ts.map +1 -0
- package/dist/tasks/get-balance.js +38 -0
- package/dist/tasks/get-contract-info.d.ts +2 -0
- package/dist/tasks/get-contract-info.d.ts.map +1 -0
- package/dist/tasks/get-contract-info.js +23 -0
- package/dist/tasks/get-tx-history.d.ts +2 -0
- package/dist/tasks/get-tx-history.d.ts.map +1 -0
- package/dist/tasks/get-tx-history.js +43 -0
- package/dist/tasks/list-accounts.d.ts +2 -0
- package/dist/tasks/list-accounts.d.ts.map +1 -0
- package/dist/tasks/list-accounts.js +26 -0
- package/dist/tasks/send-stx.d.ts +2 -0
- package/dist/tasks/send-stx.d.ts.map +1 -0
- package/dist/tasks/send-stx.js +38 -0
- package/dist/tasks/verify-contract.d.ts +2 -0
- package/dist/tasks/verify-contract.d.ts.map +1 -0
- package/dist/tasks/verify-contract.js +38 -0
- package/dist/types/config.d.ts +100 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +23 -0
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const commander_1 = require("commander");
|
|
9
|
+
const config_loading_1 = require("../config/config-loading");
|
|
10
|
+
const runtime_environment_1 = require("../core/runtime-environment");
|
|
11
|
+
const tasks_definitions_1 = require("../core/tasks-definitions");
|
|
12
|
+
const init_1 = require("./init");
|
|
13
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
14
|
+
// Import all user tasks
|
|
15
|
+
const fs_1 = __importDefault(require("fs"));
|
|
16
|
+
const path_1 = __importDefault(require("path"));
|
|
17
|
+
// Auto-load all tasks in src/tasks
|
|
18
|
+
const tasksDir = path_1.default.join(__dirname, '../tasks');
|
|
19
|
+
fs_1.default.readdirSync(tasksDir)
|
|
20
|
+
.filter(f => (f.endsWith('.ts') || f.endsWith('.js')) && !f.endsWith('.d.ts') && !f.includes('.test.'))
|
|
21
|
+
.forEach(f => {
|
|
22
|
+
require(path_1.default.join(tasksDir, f));
|
|
23
|
+
});
|
|
24
|
+
const program = new commander_1.Command();
|
|
25
|
+
program
|
|
26
|
+
.name('mobilestacks')
|
|
27
|
+
.description('Professional Task Runner for Stacks')
|
|
28
|
+
.version('0.1.0');
|
|
29
|
+
// Init command for project scaffolding
|
|
30
|
+
program
|
|
31
|
+
.command('init')
|
|
32
|
+
.description('Scaffold a new mobilestacks project and config')
|
|
33
|
+
.action(async () => {
|
|
34
|
+
await (0, init_1.runInit)();
|
|
35
|
+
process.exit(0);
|
|
36
|
+
});
|
|
37
|
+
// List all tasks if no command is given
|
|
38
|
+
program.action(() => {
|
|
39
|
+
console.log(chalk_1.default.bold.blue('\nMobilestacks - Professional Task Runner for Stacks\n'));
|
|
40
|
+
console.log(chalk_1.default.white('USAGE: ') + chalk_1.default.green('mobilestacks <task> [options]\n'));
|
|
41
|
+
console.log(chalk_1.default.bold('Available tasks:'));
|
|
42
|
+
tasks_definitions_1.TaskDefinitions.getInstance().getAllTasks().forEach(task => {
|
|
43
|
+
const params = task.params.map(p => chalk_1.default.yellow(`--${p.name}`)).join(' ');
|
|
44
|
+
console.log(' ' + chalk_1.default.cyan(task.name) + ' ' + params);
|
|
45
|
+
console.log(' ' + chalk_1.default.gray(task.description));
|
|
46
|
+
});
|
|
47
|
+
console.log('\n' + chalk_1.default.white('Use ') + chalk_1.default.green('mobilestacks <task> --help') + chalk_1.default.white(' for more info on a task.'));
|
|
48
|
+
console.log(chalk_1.default.white('\nExample:'));
|
|
49
|
+
console.log(' ' + chalk_1.default.green('mobilestacks deploy-contract --contractName my-contract --file ./contracts/my-contract.clar --network testnet'));
|
|
50
|
+
console.log(chalk_1.default.white('\nDocs: ') + chalk_1.default.underline('https://github.com/your-org/mobilestacks#readme'));
|
|
51
|
+
program.help({ error: false });
|
|
52
|
+
});
|
|
53
|
+
// Dynamically add all registered tasks
|
|
54
|
+
tasks_definitions_1.TaskDefinitions.getInstance().getAllTasks().forEach(task => {
|
|
55
|
+
const cmd = program.command(task.name)
|
|
56
|
+
.description(task.description);
|
|
57
|
+
task.params.forEach(param => {
|
|
58
|
+
const optStr = `--${param.name} <value>`;
|
|
59
|
+
if (param.required !== false) {
|
|
60
|
+
cmd.option(optStr, param.description);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
cmd.option(optStr, param.description, param.defaultValue);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
cmd.action(async (opts) => {
|
|
67
|
+
try {
|
|
68
|
+
// Prompt for missing required params
|
|
69
|
+
const missing = task.params.filter(p => p.required !== false && !opts[p.name]);
|
|
70
|
+
if (missing.length > 0) {
|
|
71
|
+
const answers = await inquirer_1.default.prompt(missing.map(p => ({
|
|
72
|
+
type: p.type === 'boolean' ? 'confirm' : 'input',
|
|
73
|
+
name: p.name,
|
|
74
|
+
message: p.description,
|
|
75
|
+
default: p.defaultValue
|
|
76
|
+
})));
|
|
77
|
+
Object.assign(opts, answers);
|
|
78
|
+
}
|
|
79
|
+
// Type conversion
|
|
80
|
+
task.params.forEach(p => {
|
|
81
|
+
if (opts[p.name] && p.type === 'number')
|
|
82
|
+
opts[p.name] = Number(opts[p.name]);
|
|
83
|
+
if (opts[p.name] && p.type === 'boolean')
|
|
84
|
+
opts[p.name] = Boolean(opts[p.name]);
|
|
85
|
+
});
|
|
86
|
+
const config = (0, config_loading_1.loadConfig)();
|
|
87
|
+
const env = new runtime_environment_1.RuntimeEnvironment(config);
|
|
88
|
+
const result = await task.action(opts, env);
|
|
89
|
+
if (typeof result === 'object') {
|
|
90
|
+
console.log(chalk_1.default.greenBright('Success!'));
|
|
91
|
+
console.dir(result, { depth: null, colors: true });
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
console.log(chalk_1.default.greenBright(result));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
const error = err;
|
|
99
|
+
console.error(chalk_1.default.redBright('Task failed:'), error.message || error);
|
|
100
|
+
if (error && typeof error === 'object' && 'stack' in error && error.stack) {
|
|
101
|
+
console.error(chalk_1.default.gray(error.stack));
|
|
102
|
+
}
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/cli/init.ts"],"names":[],"mappings":"AAIA,wBAAsB,OAAO,kBAoF5B"}
|
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runInit = runInit;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
10
|
+
async function runInit() {
|
|
11
|
+
console.log('Welcome to mobilestacks project initialization!');
|
|
12
|
+
const answers = await inquirer_1.default.prompt([
|
|
13
|
+
{
|
|
14
|
+
type: 'input',
|
|
15
|
+
name: 'projectName',
|
|
16
|
+
message: 'Project name:',
|
|
17
|
+
default: path_1.default.basename(process.cwd()),
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
type: 'input',
|
|
21
|
+
name: 'mainnetUrl',
|
|
22
|
+
message: 'Stacks mainnet node URL:',
|
|
23
|
+
default: 'https://api.mainnet.hiro.so',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
type: 'input',
|
|
27
|
+
name: 'testnetUrl',
|
|
28
|
+
message: 'Stacks testnet node URL:',
|
|
29
|
+
default: 'https://api.testnet.hiro.so',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
type: 'input',
|
|
33
|
+
name: 'derivationPath',
|
|
34
|
+
message: "Derivation path (default: m/44'/5757'/0'/0/0):",
|
|
35
|
+
default: "m/44'/5757'/0'/0/0",
|
|
36
|
+
},
|
|
37
|
+
]);
|
|
38
|
+
// ── Config file: references env vars, never embeds secrets ──
|
|
39
|
+
const config = `import 'dotenv/config';
|
|
40
|
+
|
|
41
|
+
export default {
|
|
42
|
+
networks: {
|
|
43
|
+
mainnet: { url: process.env.STACKS_MAINNET_URL || ${JSON.stringify(answers.mainnetUrl)}, name: 'mainnet' },
|
|
44
|
+
testnet: { url: process.env.STACKS_TESTNET_URL || ${JSON.stringify(answers.testnetUrl)}, name: 'testnet' },
|
|
45
|
+
},
|
|
46
|
+
defaultNetwork: 'testnet',
|
|
47
|
+
wallet: {
|
|
48
|
+
// Secrets are read from environment variables — never hard-code them here.
|
|
49
|
+
privateKey: process.env.MOBILESTACKS_PRIVATE_KEY || '',
|
|
50
|
+
seedPhrase: process.env.MOBILESTACKS_SEED_PHRASE || '',
|
|
51
|
+
derivationPath: ${JSON.stringify(answers.derivationPath)},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
`;
|
|
55
|
+
fs_1.default.writeFileSync(path_1.default.join(process.cwd(), 'mobilestacks.config.ts'), config);
|
|
56
|
+
// ── .env file (if it doesn't exist yet) ──
|
|
57
|
+
const envPath = path_1.default.join(process.cwd(), '.env');
|
|
58
|
+
if (!fs_1.default.existsSync(envPath)) {
|
|
59
|
+
const envContent = `# Mobilestacks secrets — NEVER commit this file!\nMOBILESTACKS_PRIVATE_KEY=\nMOBILESTACKS_SEED_PHRASE=\nSTACKS_MAINNET_URL=${answers.mainnetUrl}\nSTACKS_TESTNET_URL=${answers.testnetUrl}\n`;
|
|
60
|
+
fs_1.default.writeFileSync(envPath, envContent, { mode: 0o600 }); // owner-only permissions
|
|
61
|
+
}
|
|
62
|
+
// ── Scaffold example contract ──
|
|
63
|
+
const contractsDir = path_1.default.join(process.cwd(), 'contracts');
|
|
64
|
+
if (!fs_1.default.existsSync(contractsDir))
|
|
65
|
+
fs_1.default.mkdirSync(contractsDir);
|
|
66
|
+
fs_1.default.writeFileSync(path_1.default.join(contractsDir, 'sample-contract.clar'), '(define-public (hello-world)\n (ok "Hello, Stacks!"))\n');
|
|
67
|
+
// ── Scaffold example user task ──
|
|
68
|
+
const tasksDir = path_1.default.join(process.cwd(), 'src', 'tasks');
|
|
69
|
+
if (!fs_1.default.existsSync(tasksDir))
|
|
70
|
+
fs_1.default.mkdirSync(tasksDir, { recursive: true });
|
|
71
|
+
fs_1.default.writeFileSync(path_1.default.join(tasksDir, 'example-task.ts'), "import { task } from 'mobilestacks';\n\ntask('example', 'An example user task for onboarding')\n .addParam('name', 'Your name', { type: 'string', required: false, defaultValue: 'World' })\n .setAction(async (args: Record<string, string>) => {\n return `Hello, ${args.name}! Welcome to mobilestacks.`;\n });\n");
|
|
72
|
+
// ── Security notice ──
|
|
73
|
+
console.log('\n✅ Created:');
|
|
74
|
+
console.log(' • mobilestacks.config.ts (reads secrets from env vars)');
|
|
75
|
+
console.log(' • .env (store your secrets here)');
|
|
76
|
+
console.log(' • contracts/sample-contract.clar');
|
|
77
|
+
console.log(' • src/tasks/example-task.ts');
|
|
78
|
+
console.log('\n⚠️ SECURITY WARNING:');
|
|
79
|
+
console.log(' Your wallet secrets belong in the .env file, NOT in source code.');
|
|
80
|
+
console.log(' • .env is already listed in .gitignore — never remove that entry.');
|
|
81
|
+
console.log(' • Set MOBILESTACKS_PRIVATE_KEY or MOBILESTACKS_SEED_PHRASE in .env');
|
|
82
|
+
console.log(' • See .env.example for the full list of supported variables.\n');
|
|
83
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-loading.d.ts","sourceRoot":"","sources":["../../src/config/config-loading.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAK/E,wBAAgB,UAAU,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,YAAY,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,kBAAkB,CAyB9G"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loadConfig = loadConfig;
|
|
7
|
+
const config_1 = require("../types/config");
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const env_1 = require("../core/env");
|
|
11
|
+
function loadConfig(configPath, cliOverrides = {}) {
|
|
12
|
+
const configFile = configPath || path_1.default.resolve(process.cwd(), 'mobilestacks.config.ts');
|
|
13
|
+
if (!fs_1.default.existsSync(configFile)) {
|
|
14
|
+
throw new Error(`Config file not found: ${configFile}`);
|
|
15
|
+
}
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
17
|
+
require('ts-node').register();
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
19
|
+
const userConfig = require(configFile);
|
|
20
|
+
let config = userConfig.default || userConfig;
|
|
21
|
+
// .env override
|
|
22
|
+
if (env_1.env.privateKey)
|
|
23
|
+
config.wallet.privateKey = env_1.env.privateKey;
|
|
24
|
+
if (env_1.env.mainnetUrl)
|
|
25
|
+
config.networks.mainnet.url = env_1.env.mainnetUrl;
|
|
26
|
+
if (env_1.env.testnetUrl)
|
|
27
|
+
config.networks.testnet.url = env_1.env.testnetUrl;
|
|
28
|
+
// CLI overrides
|
|
29
|
+
config = { ...config, ...cliOverrides };
|
|
30
|
+
const parsed = config_1.MobilestacksConfigSchema.safeParse(config);
|
|
31
|
+
if (!parsed.success) {
|
|
32
|
+
throw new Error('Invalid mobilestacks.config.ts: ' + JSON.stringify(parsed.error.format(), null, 2));
|
|
33
|
+
}
|
|
34
|
+
return parsed.data;
|
|
35
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { TaskDefinition, TaskParamType } from './tasks-definitions';
|
|
2
|
+
import { ZodSchema } from 'zod';
|
|
3
|
+
import { RuntimeEnvironment } from './runtime-environment';
|
|
4
|
+
import { extendEnvironment } from './extender';
|
|
5
|
+
declare function task(name: string, description: string): {
|
|
6
|
+
addParam(paramName: string, paramDesc: string, options?: {
|
|
7
|
+
type?: TaskParamType;
|
|
8
|
+
required?: boolean;
|
|
9
|
+
defaultValue?: unknown;
|
|
10
|
+
schema?: ZodSchema;
|
|
11
|
+
}): /*elided*/ any;
|
|
12
|
+
setAction(action: (args: Record<string, unknown>, env: RuntimeEnvironment) => Promise<unknown>): TaskDefinition;
|
|
13
|
+
};
|
|
14
|
+
declare function subtask(name: string, description: string, parentTaskName?: string): {
|
|
15
|
+
addParam(paramName: string, paramDesc: string, options?: {
|
|
16
|
+
type?: TaskParamType;
|
|
17
|
+
required?: boolean;
|
|
18
|
+
defaultValue?: unknown;
|
|
19
|
+
schema?: ZodSchema;
|
|
20
|
+
}): /*elided*/ any;
|
|
21
|
+
setAction(action: (args: Record<string, unknown>, env: RuntimeEnvironment) => Promise<unknown>): TaskDefinition;
|
|
22
|
+
};
|
|
23
|
+
export type WorkflowStep = {
|
|
24
|
+
taskName: string;
|
|
25
|
+
args?: Record<string, unknown>;
|
|
26
|
+
};
|
|
27
|
+
export type Workflow = WorkflowStep[];
|
|
28
|
+
export declare function runWorkflow(workflow: Workflow, env: RuntimeEnvironment): Promise<void>;
|
|
29
|
+
export { task, subtask, extendEnvironment };
|
|
30
|
+
//# sourceMappingURL=dsl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dsl.d.ts","sourceRoot":"","sources":["../../src/core/dsl.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,cAAc,EAAa,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAChG,OAAO,EAAE,SAAS,EAAE,MAAM,KAAK,CAAC;AAChC,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE/C,iBAAS,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM;wBAI9B,MAAM,aACN,MAAM,YACP;QAAE,IAAI,CAAC,EAAE,aAAa,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,YAAY,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,SAAS,CAAA;KAAE;sBAYlF,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,kBAAkB,KAAK,OAAO,CAAC,OAAO,CAAC;EAuBjG;AAED,iBAAS,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM;wBAvC1D,MAAM,aACN,MAAM,YACP;QAAE,IAAI,CAAC,EAAE,aAAa,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,YAAY,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,SAAS,CAAA;KAAE;sBAYlF,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,kBAAkB,KAAK,OAAO,CAAC,OAAO,CAAC;EAkCjG;AAGD,MAAM,MAAM,YAAY,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC;AAChF,MAAM,MAAM,QAAQ,GAAG,YAAY,EAAE,CAAC;AAEtC,wBAAsB,WAAW,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,kBAAkB,iBAO5E;AAED,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC"}
|
package/dist/core/dsl.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extendEnvironment = void 0;
|
|
4
|
+
exports.runWorkflow = runWorkflow;
|
|
5
|
+
exports.task = task;
|
|
6
|
+
exports.subtask = subtask;
|
|
7
|
+
const tasks_definitions_1 = require("./tasks-definitions");
|
|
8
|
+
const extender_1 = require("./extender");
|
|
9
|
+
Object.defineProperty(exports, "extendEnvironment", { enumerable: true, get: function () { return extender_1.extendEnvironment; } });
|
|
10
|
+
function task(name, description) {
|
|
11
|
+
const params = [];
|
|
12
|
+
return {
|
|
13
|
+
addParam(paramName, paramDesc, options) {
|
|
14
|
+
params.push({
|
|
15
|
+
name: paramName,
|
|
16
|
+
description: paramDesc,
|
|
17
|
+
type: options?.type || 'string',
|
|
18
|
+
required: options?.required !== false, // default true
|
|
19
|
+
defaultValue: options?.defaultValue,
|
|
20
|
+
schema: options?.schema,
|
|
21
|
+
});
|
|
22
|
+
return this;
|
|
23
|
+
},
|
|
24
|
+
setAction(action) {
|
|
25
|
+
const def = {
|
|
26
|
+
name,
|
|
27
|
+
description,
|
|
28
|
+
params,
|
|
29
|
+
action: async (args, env) => {
|
|
30
|
+
// Validate args against schemas if present
|
|
31
|
+
for (const param of params) {
|
|
32
|
+
if (param.schema && args[param.name] !== undefined) {
|
|
33
|
+
try {
|
|
34
|
+
param.schema.parse(args[param.name]);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
throw new Error(`Invalid value for parameter '${param.name}': ${error}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return action(args, env);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
tasks_definitions_1.TaskDefinitions.getInstance().addTask(def);
|
|
45
|
+
return def;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function subtask(name, description, parentTaskName) {
|
|
50
|
+
// Register as a subtask with a parent if provided
|
|
51
|
+
const sub = task(name, description);
|
|
52
|
+
if (parentTaskName) {
|
|
53
|
+
// Attach parent info for future use (e.g., dependency graph, CLI grouping)
|
|
54
|
+
const def = tasks_definitions_1.TaskDefinitions.getInstance().getTask(name);
|
|
55
|
+
if (def)
|
|
56
|
+
def.parent = parentTaskName;
|
|
57
|
+
}
|
|
58
|
+
return sub;
|
|
59
|
+
}
|
|
60
|
+
async function runWorkflow(workflow, env) {
|
|
61
|
+
for (const step of workflow) {
|
|
62
|
+
const def = tasks_definitions_1.TaskDefinitions.getInstance().getTask(step.taskName);
|
|
63
|
+
if (!def)
|
|
64
|
+
throw new Error(`Task not found: ${step.taskName}`);
|
|
65
|
+
// eslint-disable-next-line no-await-in-loop
|
|
66
|
+
await def.action(step.args || {}, env);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dsl.test.d.ts","sourceRoot":"","sources":["../../src/core/dsl.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const dsl_1 = require("./dsl");
|
|
5
|
+
const tasks_definitions_1 = require("./tasks-definitions");
|
|
6
|
+
const zod_1 = require("zod");
|
|
7
|
+
const extender_1 = require("./extender");
|
|
8
|
+
(0, vitest_1.describe)('DSL', () => {
|
|
9
|
+
(0, vitest_1.beforeEach)(() => {
|
|
10
|
+
// Clear tasks before each test
|
|
11
|
+
tasks_definitions_1.TaskDefinitions.getInstance()._tasks = [];
|
|
12
|
+
});
|
|
13
|
+
(0, vitest_1.it)('should register a task with generic parameters', () => {
|
|
14
|
+
(0, dsl_1.task)('test-task', 'A test task')
|
|
15
|
+
.addParam('p1', 'param 1')
|
|
16
|
+
.setAction(async () => { return 'done'; });
|
|
17
|
+
const def = tasks_definitions_1.TaskDefinitions.getInstance().getTask('test-task');
|
|
18
|
+
(0, vitest_1.expect)(def).toBeDefined();
|
|
19
|
+
(0, vitest_1.expect)(def?.name).toBe('test-task');
|
|
20
|
+
(0, vitest_1.expect)(def?.params.length).toBe(1);
|
|
21
|
+
});
|
|
22
|
+
(0, vitest_1.it)('should validate Zod schema', async () => {
|
|
23
|
+
(0, dsl_1.task)('zod-task', 'Task with validation')
|
|
24
|
+
.addParam('age', 'Age param', { schema: zod_1.z.number().min(18) })
|
|
25
|
+
.setAction(async (args) => {
|
|
26
|
+
return args.age;
|
|
27
|
+
});
|
|
28
|
+
const def = tasks_definitions_1.TaskDefinitions.getInstance().getTask('zod-task');
|
|
29
|
+
(0, vitest_1.expect)(def).toBeDefined();
|
|
30
|
+
// Mock environment
|
|
31
|
+
const env = {};
|
|
32
|
+
// Valid call
|
|
33
|
+
const result = await def?.action({ age: 20 }, env);
|
|
34
|
+
(0, vitest_1.expect)(result).toBe(20);
|
|
35
|
+
// Invalid call
|
|
36
|
+
await (0, vitest_1.expect)(def?.action({ age: 10 }, env)).rejects.toThrow('Invalid value for parameter \'age\'');
|
|
37
|
+
});
|
|
38
|
+
(0, vitest_1.it)('should support environment extensions', () => {
|
|
39
|
+
// Clear extensions
|
|
40
|
+
extender_1.Extender.getInstance()._extensions = [];
|
|
41
|
+
(0, extender_1.extendEnvironment)((env) => {
|
|
42
|
+
env['foo'] = 'bar';
|
|
43
|
+
});
|
|
44
|
+
const extensions = extender_1.Extender.getInstance().getExtensions();
|
|
45
|
+
(0, vitest_1.expect)(extensions.length).toBe(1);
|
|
46
|
+
const mockEnv = {};
|
|
47
|
+
extensions[0](mockEnv);
|
|
48
|
+
(0, vitest_1.expect)(mockEnv['foo']).toBe('bar');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/core/env.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,GAAG;;;;CAIf,CAAC"}
|
package/dist/core/env.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.env = void 0;
|
|
7
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
8
|
+
dotenv_1.default.config();
|
|
9
|
+
exports.env = {
|
|
10
|
+
privateKey: process.env.STACKS_PRIVATE_KEY,
|
|
11
|
+
mainnetUrl: process.env.STACKS_MAINNET_URL,
|
|
12
|
+
testnetUrl: process.env.STACKS_TESTNET_URL,
|
|
13
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { RuntimeEnvironment } from './runtime-environment';
|
|
2
|
+
export type ExtensionFunc = (env: RuntimeEnvironment) => void;
|
|
3
|
+
export declare class Extender {
|
|
4
|
+
private static _instance;
|
|
5
|
+
private _extensions;
|
|
6
|
+
private constructor();
|
|
7
|
+
static getInstance(): Extender;
|
|
8
|
+
addExtension(func: ExtensionFunc): void;
|
|
9
|
+
getExtensions(): ExtensionFunc[];
|
|
10
|
+
}
|
|
11
|
+
export declare function extendEnvironment(func: ExtensionFunc): void;
|
|
12
|
+
//# sourceMappingURL=extender.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extender.d.ts","sourceRoot":"","sources":["../../src/core/extender.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE3D,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,kBAAkB,KAAK,IAAI,CAAC;AAE9D,qBAAa,QAAQ;IACnB,OAAO,CAAC,MAAM,CAAC,SAAS,CAAW;IACnC,OAAO,CAAC,WAAW,CAAuB;IAE1C,OAAO;WAEO,WAAW,IAAI,QAAQ;IAO9B,YAAY,CAAC,IAAI,EAAE,aAAa;IAIhC,aAAa,IAAI,aAAa,EAAE;CAGxC;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,aAAa,QAEpD"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Extender = void 0;
|
|
4
|
+
exports.extendEnvironment = extendEnvironment;
|
|
5
|
+
class Extender {
|
|
6
|
+
constructor() {
|
|
7
|
+
this._extensions = [];
|
|
8
|
+
}
|
|
9
|
+
static getInstance() {
|
|
10
|
+
if (!Extender._instance) {
|
|
11
|
+
Extender._instance = new Extender();
|
|
12
|
+
}
|
|
13
|
+
return Extender._instance;
|
|
14
|
+
}
|
|
15
|
+
addExtension(func) {
|
|
16
|
+
this._extensions.push(func);
|
|
17
|
+
}
|
|
18
|
+
getExtensions() {
|
|
19
|
+
return this._extensions;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
exports.Extender = Extender;
|
|
23
|
+
function extendEnvironment(func) {
|
|
24
|
+
Extender.getInstance().addExtension(func);
|
|
25
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { MobilestacksConfig } from '../types/config';
|
|
3
|
+
import { StacksNetwork } from '@stacks/network';
|
|
4
|
+
export declare class RuntimeEnvironment {
|
|
5
|
+
config: MobilestacksConfig;
|
|
6
|
+
network: StacksNetwork;
|
|
7
|
+
wallet: {
|
|
8
|
+
privateKey: string;
|
|
9
|
+
address: string;
|
|
10
|
+
accountIndex?: number;
|
|
11
|
+
};
|
|
12
|
+
stacks: Record<string, unknown>;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
constructor(config: MobilestacksConfig);
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=runtime-environment.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-environment.d.ts","sourceRoot":"","sources":["../../src/core/runtime-environment.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,CAAC;AACvB,OAAO,EAAE,kBAAkB,EAAiB,MAAM,iBAAiB,CAAC;AAGpE,OAAO,EAAE,aAAa,EAAiB,MAAM,iBAAiB,CAAC;AAG/D,qBAAa,kBAAkB;IACtB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,OAAO,EAAE,aAAa,CAAC;IACvB,MAAM,EAAG;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACxE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;gBAEX,MAAM,EAAE,kBAAkB;CAkEvC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RuntimeEnvironment = void 0;
|
|
4
|
+
require("dotenv/config");
|
|
5
|
+
const transactions_1 = require("@stacks/transactions");
|
|
6
|
+
const wallet_sdk_1 = require("@stacks/wallet-sdk");
|
|
7
|
+
const network_1 = require("@stacks/network");
|
|
8
|
+
const extender_1 = require("./extender");
|
|
9
|
+
class RuntimeEnvironment {
|
|
10
|
+
constructor(config) {
|
|
11
|
+
this.config = config;
|
|
12
|
+
const networkName = config.defaultNetwork;
|
|
13
|
+
const networkConfig = config.networks[networkName];
|
|
14
|
+
if (!networkConfig) {
|
|
15
|
+
throw new Error(`Network config not found for: ${networkName}`);
|
|
16
|
+
}
|
|
17
|
+
// Instantiate proper StacksNetwork
|
|
18
|
+
if (networkName === 'mainnet' || networkName === 'testnet') {
|
|
19
|
+
this.network = (0, network_1.createNetwork)({
|
|
20
|
+
network: networkName,
|
|
21
|
+
client: { baseUrl: networkConfig.url }
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
// Default to testnet structure for custom networks
|
|
26
|
+
this.network = (0, network_1.createNetwork)({
|
|
27
|
+
network: 'testnet',
|
|
28
|
+
client: { baseUrl: networkConfig.url }
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
// ── Resolve wallet secrets (env vars take priority over config) ──
|
|
32
|
+
const privateKey = process.env.MOBILESTACKS_PRIVATE_KEY ||
|
|
33
|
+
process.env.STACKS_PRIVATE_KEY ||
|
|
34
|
+
config.wallet.privateKey ||
|
|
35
|
+
'';
|
|
36
|
+
const seedPhrase = process.env.MOBILESTACKS_SEED_PHRASE ||
|
|
37
|
+
process.env.STACKS_SEED_PHRASE ||
|
|
38
|
+
config.wallet.seedPhrase ||
|
|
39
|
+
'';
|
|
40
|
+
if (privateKey) {
|
|
41
|
+
const address = (0, transactions_1.getAddressFromPrivateKey)(privateKey, networkName === 'mainnet' ? 'mainnet' : 'testnet');
|
|
42
|
+
this.wallet = { privateKey, address };
|
|
43
|
+
}
|
|
44
|
+
else if (seedPhrase) {
|
|
45
|
+
const derivPath = config.wallet.derivationPath || "m/44'/5757'/0'/0/0";
|
|
46
|
+
(0, wallet_sdk_1.generateWallet)({ secretKey: seedPhrase, password: '' }).then(wallet => {
|
|
47
|
+
const index = parseInt(derivPath.split('/').pop() || '0', 10);
|
|
48
|
+
const account = wallet.accounts[index] || wallet.accounts[0];
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
50
|
+
const { getStxAddress } = require('@stacks/wallet-sdk/dist/models/account');
|
|
51
|
+
const address = getStxAddress(account, this.network.transactionVersion);
|
|
52
|
+
this.wallet = {
|
|
53
|
+
privateKey: account.stxPrivateKey,
|
|
54
|
+
address,
|
|
55
|
+
accountIndex: account.index,
|
|
56
|
+
};
|
|
57
|
+
}).catch(() => {
|
|
58
|
+
throw new Error('Failed to generate wallet from seed phrase');
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
// No secrets anywhere — warn but don't crash (devnet may not need a wallet)
|
|
63
|
+
console.warn('⚠️ No wallet secret found. Set MOBILESTACKS_PRIVATE_KEY or MOBILESTACKS_SEED_PHRASE in your .env file.');
|
|
64
|
+
}
|
|
65
|
+
this.stacks = {};
|
|
66
|
+
// Apply extensions
|
|
67
|
+
for (const extension of extender_1.Extender.getInstance().getExtensions()) {
|
|
68
|
+
extension(this);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.RuntimeEnvironment = RuntimeEnvironment;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { initSimnet } from '@hirosystems/clarinet-sdk';
|
|
2
|
+
import { ClarityValue } from '@stacks/transactions';
|
|
3
|
+
type SimnetInstance = Awaited<ReturnType<typeof initSimnet>>;
|
|
4
|
+
export declare class Simnet {
|
|
5
|
+
private static _instance;
|
|
6
|
+
simnet: SimnetInstance;
|
|
7
|
+
accounts: Map<string, string>;
|
|
8
|
+
private constructor();
|
|
9
|
+
static init(): Promise<Simnet>;
|
|
10
|
+
getDeployer(): string;
|
|
11
|
+
getAccount(name: string): string;
|
|
12
|
+
callPublic(contract: string, func: string, args: ClarityValue[], sender: string): import("@hirosystems/clarinet-sdk").ParsedTransactionResult;
|
|
13
|
+
callReadOnly(contract: string, func: string, args: ClarityValue[], sender: string): import("@hirosystems/clarinet-sdk").ParsedTransactionResult;
|
|
14
|
+
mineBlock(txs: Parameters<SimnetInstance['mineBlock']>[0]): Promise<import("@hirosystems/clarinet-sdk").ParsedTransactionResult[]>;
|
|
15
|
+
}
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=simnet.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"simnet.d.ts","sourceRoot":"","sources":["../../src/core/simnet.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAKpD,KAAK,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC,CAAC;AAE7D,qBAAa,MAAM;IACjB,OAAO,CAAC,MAAM,CAAC,SAAS,CAAS;IAC1B,MAAM,EAAG,cAAc,CAAC;IACxB,QAAQ,EAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEtC,OAAO;WAEa,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;IAiBpC,WAAW,IAAI,MAAM;IAMrB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAMhC,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE,MAAM,EAAE,MAAM;IAI/E,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE,MAAM,EAAE,MAAM;IAI3E,SAAS,CAAC,GAAG,EAAE,UAAU,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;CAGvE"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Simnet = void 0;
|
|
4
|
+
const clarinet_sdk_1 = require("@hirosystems/clarinet-sdk");
|
|
5
|
+
class Simnet {
|
|
6
|
+
constructor() { }
|
|
7
|
+
static async init() {
|
|
8
|
+
if (!Simnet._instance) {
|
|
9
|
+
Simnet._instance = new Simnet();
|
|
10
|
+
// Initialize SDK - auto-detects Clarinet.toml from CWD
|
|
11
|
+
try {
|
|
12
|
+
console.log('Initializing Simnet...');
|
|
13
|
+
Simnet._instance.simnet = await (0, clarinet_sdk_1.initSimnet)();
|
|
14
|
+
// Get accounts
|
|
15
|
+
Simnet._instance.accounts = Simnet._instance.simnet.getAccounts();
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
console.error('Failed to initialize Simnet:', error);
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return Simnet._instance;
|
|
23
|
+
}
|
|
24
|
+
getDeployer() {
|
|
25
|
+
const deployer = this.accounts.get('deployer');
|
|
26
|
+
if (!deployer)
|
|
27
|
+
throw new Error('Deployer account not found');
|
|
28
|
+
return deployer;
|
|
29
|
+
}
|
|
30
|
+
getAccount(name) {
|
|
31
|
+
const acc = this.accounts.get(name);
|
|
32
|
+
if (!acc)
|
|
33
|
+
throw new Error(`Account ${name} not found`);
|
|
34
|
+
return acc;
|
|
35
|
+
}
|
|
36
|
+
callPublic(contract, func, args, sender) {
|
|
37
|
+
return this.simnet.callPublicFn(contract, func, args, sender);
|
|
38
|
+
}
|
|
39
|
+
callReadOnly(contract, func, args, sender) {
|
|
40
|
+
return this.simnet.callReadOnlyFn(contract, func, args, sender);
|
|
41
|
+
}
|
|
42
|
+
async mineBlock(txs) {
|
|
43
|
+
return this.simnet.mineBlock(txs);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.Simnet = Simnet;
|