mobilestacks 0.1.0

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.
Files changed (96) hide show
  1. package/Clarinet.toml +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +188 -0
  4. package/contracts/sample-contract.clar +2 -0
  5. package/dist/mobilestacks.config.d.ts +20 -0
  6. package/dist/mobilestacks.config.d.ts.map +1 -0
  7. package/dist/mobilestacks.config.js +23 -0
  8. package/dist/src/cli/index.d.ts +3 -0
  9. package/dist/src/cli/index.d.ts.map +1 -0
  10. package/dist/src/cli/index.js +102 -0
  11. package/dist/src/cli/init.d.ts +2 -0
  12. package/dist/src/cli/init.d.ts.map +1 -0
  13. package/dist/src/cli/init.js +55 -0
  14. package/dist/src/config/config-loading.d.ts +3 -0
  15. package/dist/src/config/config-loading.d.ts.map +1 -0
  16. package/dist/src/config/config-loading.js +29 -0
  17. package/dist/src/core/dsl.d.ts +30 -0
  18. package/dist/src/core/dsl.d.ts.map +1 -0
  19. package/dist/src/core/dsl.js +62 -0
  20. package/dist/src/core/dsl.test.d.ts +2 -0
  21. package/dist/src/core/dsl.test.d.ts.map +1 -0
  22. package/dist/src/core/dsl.test.js +48 -0
  23. package/dist/src/core/env.d.ts +6 -0
  24. package/dist/src/core/env.d.ts.map +1 -0
  25. package/dist/src/core/env.js +7 -0
  26. package/dist/src/core/extender.d.ts +12 -0
  27. package/dist/src/core/extender.d.ts.map +1 -0
  28. package/dist/src/core/extender.js +20 -0
  29. package/dist/src/core/runtime-environment.d.ts +15 -0
  30. package/dist/src/core/runtime-environment.d.ts.map +1 -0
  31. package/dist/src/core/runtime-environment.js +65 -0
  32. package/dist/src/core/simnet.d.ts +17 -0
  33. package/dist/src/core/simnet.d.ts.map +1 -0
  34. package/dist/src/core/simnet.js +42 -0
  35. package/dist/src/core/tasks-definitions.d.ts +28 -0
  36. package/dist/src/core/tasks-definitions.d.ts.map +1 -0
  37. package/dist/src/core/tasks-definitions.js +21 -0
  38. package/dist/src/index.d.ts +5 -0
  39. package/dist/src/index.d.ts.map +1 -0
  40. package/dist/src/index.js +4 -0
  41. package/dist/src/tasks/call-contract-function.d.ts +2 -0
  42. package/dist/src/tasks/call-contract-function.d.ts.map +1 -0
  43. package/dist/src/tasks/call-contract-function.js +31 -0
  44. package/dist/src/tasks/deploy-contract.d.ts +2 -0
  45. package/dist/src/tasks/deploy-contract.d.ts.map +1 -0
  46. package/dist/src/tasks/deploy-contract.js +44 -0
  47. package/dist/src/tasks/example-task.d.ts +2 -0
  48. package/dist/src/tasks/example-task.d.ts.map +1 -0
  49. package/dist/src/tasks/example-task.js +6 -0
  50. package/dist/src/tasks/faucet-request.d.ts +2 -0
  51. package/dist/src/tasks/faucet-request.d.ts.map +1 -0
  52. package/dist/src/tasks/faucet-request.js +21 -0
  53. package/dist/src/tasks/get-balance.d.ts +2 -0
  54. package/dist/src/tasks/get-balance.d.ts.map +1 -0
  55. package/dist/src/tasks/get-balance.js +33 -0
  56. package/dist/src/tasks/get-contract-info.d.ts +2 -0
  57. package/dist/src/tasks/get-contract-info.d.ts.map +1 -0
  58. package/dist/src/tasks/get-contract-info.js +18 -0
  59. package/dist/src/tasks/get-tx-history.d.ts +2 -0
  60. package/dist/src/tasks/get-tx-history.d.ts.map +1 -0
  61. package/dist/src/tasks/get-tx-history.js +38 -0
  62. package/dist/src/tasks/list-accounts.d.ts +2 -0
  63. package/dist/src/tasks/list-accounts.d.ts.map +1 -0
  64. package/dist/src/tasks/list-accounts.js +24 -0
  65. package/dist/src/tasks/send-stx.d.ts +2 -0
  66. package/dist/src/tasks/send-stx.d.ts.map +1 -0
  67. package/dist/src/tasks/send-stx.js +36 -0
  68. package/dist/src/tasks/verify-contract.d.ts +2 -0
  69. package/dist/src/tasks/verify-contract.d.ts.map +1 -0
  70. package/dist/src/tasks/verify-contract.js +33 -0
  71. package/dist/src/types/config.d.ts +120 -0
  72. package/dist/src/types/config.d.ts.map +1 -0
  73. package/dist/src/types/config.js +19 -0
  74. package/package.json +70 -0
  75. package/src/cli/index.ts +105 -0
  76. package/src/cli/init.ts +57 -0
  77. package/src/config/config-loading.ts +31 -0
  78. package/src/core/dsl.test.ts +63 -0
  79. package/src/core/dsl.ts +73 -0
  80. package/src/core/env.ts +8 -0
  81. package/src/core/extender.ts +29 -0
  82. package/src/core/runtime-environment.ts +74 -0
  83. package/src/core/simnet.ts +56 -0
  84. package/src/core/tasks-definitions.ts +47 -0
  85. package/src/index.ts +4 -0
  86. package/src/tasks/call-contract-function.ts +31 -0
  87. package/src/tasks/deploy-contract.ts +49 -0
  88. package/src/tasks/example-task.ts +7 -0
  89. package/src/tasks/faucet-request.ts +21 -0
  90. package/src/tasks/get-balance.ts +34 -0
  91. package/src/tasks/get-contract-info.ts +18 -0
  92. package/src/tasks/get-tx-history.ts +39 -0
  93. package/src/tasks/list-accounts.ts +27 -0
  94. package/src/tasks/send-stx.ts +39 -0
  95. package/src/tasks/verify-contract.ts +33 -0
  96. package/src/types/config.ts +30 -0
@@ -0,0 +1,56 @@
1
+ import { initSimnet } from '@hirosystems/clarinet-sdk';
2
+ import { ClarityValue } from '@stacks/transactions';
3
+
4
+
5
+
6
+ // Import the SDK's Simnet type under an alias
7
+ type SimnetInstance = Awaited<ReturnType<typeof initSimnet>>;
8
+
9
+ export class Simnet {
10
+ private static _instance: Simnet;
11
+ public simnet!: SimnetInstance;
12
+ public accounts!: Map<string, string>;
13
+
14
+ private constructor() {}
15
+
16
+ public static async init(): Promise<Simnet> {
17
+ if (!Simnet._instance) {
18
+ Simnet._instance = new Simnet();
19
+ // Initialize SDK - auto-detects Clarinet.toml from CWD
20
+ try {
21
+ console.log('Initializing Simnet...');
22
+ Simnet._instance.simnet = await initSimnet();
23
+ // Get accounts
24
+ Simnet._instance.accounts = Simnet._instance.simnet.getAccounts();
25
+ } catch (error) {
26
+ console.error('Failed to initialize Simnet:', error);
27
+ throw error;
28
+ }
29
+ }
30
+ return Simnet._instance;
31
+ }
32
+
33
+ public getDeployer(): string {
34
+ const deployer = this.accounts.get('deployer');
35
+ if (!deployer) throw new Error('Deployer account not found');
36
+ return deployer;
37
+ }
38
+
39
+ public getAccount(name: string): string {
40
+ const acc = this.accounts.get(name);
41
+ if (!acc) throw new Error(`Account ${name} not found`);
42
+ return acc;
43
+ }
44
+
45
+ public callPublic(contract: string, func: string, args: ClarityValue[], sender: string) {
46
+ return this.simnet.callPublicFn(contract, func, args, sender);
47
+ }
48
+
49
+ public callReadOnly(contract: string, func: string, args: ClarityValue[], sender: string) {
50
+ return this.simnet.callReadOnlyFn(contract, func, args, sender);
51
+ }
52
+
53
+ public async mineBlock(txs: Parameters<SimnetInstance['mineBlock']>[0]) {
54
+ return this.simnet.mineBlock(txs);
55
+ }
56
+ }
@@ -0,0 +1,47 @@
1
+ import { ZodSchema } from 'zod';
2
+ import { RuntimeEnvironment } from './runtime-environment';
3
+
4
+ export type TaskParamType = 'string' | 'number' | 'boolean';
5
+ export interface TaskParam {
6
+ name: string;
7
+ description: string;
8
+ type?: TaskParamType;
9
+ required?: boolean;
10
+ defaultValue?: unknown;
11
+ schema?: ZodSchema;
12
+ }
13
+
14
+ export interface TaskDefinition {
15
+ name: string;
16
+ description: string;
17
+ params: TaskParam[];
18
+ action: (args: Record<string, unknown>, env: RuntimeEnvironment) => Promise<unknown>;
19
+ }
20
+
21
+ class TaskDefinitions {
22
+ private static _instance: TaskDefinitions;
23
+ private _tasks: TaskDefinition[] = [];
24
+
25
+ private constructor() {}
26
+
27
+ public static getInstance(): TaskDefinitions {
28
+ if (!TaskDefinitions._instance) {
29
+ TaskDefinitions._instance = new TaskDefinitions();
30
+ }
31
+ return TaskDefinitions._instance;
32
+ }
33
+
34
+ public addTask(task: TaskDefinition) {
35
+ this._tasks.push(task);
36
+ }
37
+
38
+ public getTask(name: string): TaskDefinition | undefined {
39
+ return this._tasks.find((t) => t.name === name);
40
+ }
41
+
42
+ public getAllTasks(): TaskDefinition[] {
43
+ return this._tasks;
44
+ }
45
+ }
46
+
47
+ export { TaskDefinitions };
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { task, subtask, extendEnvironment } from './core/dsl';
2
+ export { Simnet } from './core/simnet';
3
+ export { RuntimeEnvironment } from './core/runtime-environment';
4
+ export * from './core/tasks-definitions';
@@ -0,0 +1,31 @@
1
+ import { task } from '../core/dsl';
2
+ import fetch from 'node-fetch';
3
+
4
+ task('call-contract-function', 'Call a public function on a deployed Clarity contract')
5
+ .addParam('contractAddress', 'Deployed contract address (STX...)', { type: 'string', required: true })
6
+ .addParam('contractName', 'Contract name', { type: 'string', required: true })
7
+ .addParam('functionName', 'Function name', { type: 'string', required: true })
8
+ .addParam('args', 'Comma-separated function arguments', { type: 'string', required: false, defaultValue: '' })
9
+ .addParam('network', 'Network (mainnet|testnet)', { type: 'string', required: false, defaultValue: 'testnet' })
10
+ .setAction(async (args) => {
11
+ const contractAddress = args.contractAddress as string;
12
+ const contractName = args.contractName as string;
13
+ const functionName = args.functionName as string;
14
+ const fnArgs = args.args as string;
15
+ const network = args.network as string;
16
+ const apiUrl = network === 'mainnet'
17
+ ? 'https://stacks-node-api.mainnet.stacks.co'
18
+ : 'https://stacks-node-api.testnet.stacks.co';
19
+ const url = `${apiUrl}/v2/contracts/call-read/${contractAddress}/${contractName}/${functionName}`;
20
+ const body = fnArgs
21
+ ? { arguments: fnArgs.split(',').map((a: string) => a.trim()) }
22
+ : { arguments: [] };
23
+ const res = await fetch(url, {
24
+ method: 'POST',
25
+ headers: { 'Content-Type': 'application/json' },
26
+ body: JSON.stringify(body)
27
+ });
28
+ if (!res.ok) throw new Error(`Failed to call contract function: ${res.statusText}`);
29
+ const result = await res.json();
30
+ return result;
31
+ });
@@ -0,0 +1,49 @@
1
+ import { task } from '../core/dsl';
2
+ import {
3
+ makeContractDeploy,
4
+ broadcastTransaction,
5
+ } from '@stacks/transactions';
6
+ import { createNetwork } from '@stacks/network';
7
+ import fs from 'fs';
8
+
9
+ function maskAddress(address: string) {
10
+ return address ? address.slice(0, 6) + '...' + address.slice(-4) : '';
11
+ }
12
+
13
+ function containsSecret(obj: unknown): boolean {
14
+ const str = JSON.stringify(obj);
15
+ return /[A-Za-z0-9]{32,}/.test(str); // crude check for long secrets
16
+ }
17
+
18
+ // Deploy a Clarity contract to Stacks mainnet or testnet
19
+ task('deploy-contract', 'Deploy a Clarity smart contract to Stacks blockchain')
20
+ .addParam('contractName', 'Name of the contract', { type: 'string', required: true })
21
+ .addParam('file', 'Path to Clarity contract file', { type: 'string', required: true })
22
+ .addParam('network', 'Network to deploy to (mainnet|testnet)', { type: 'string', required: false, defaultValue: 'testnet' })
23
+ .setAction(async (args, env) => {
24
+ const contractName = args.contractName as string;
25
+ const file = args.file as string;
26
+ const network = args.network as string;
27
+ const codeBody = fs.readFileSync(file, 'utf8');
28
+ // Switch network if needed
29
+ if (network === 'mainnet') {
30
+ env.network = createNetwork({ network: 'mainnet', client: { baseUrl: env.config.networks.mainnet.url } });
31
+ } else {
32
+ env.network = createNetwork({ network: 'testnet', client: { baseUrl: env.config.networks.testnet.url } });
33
+ }
34
+ const tx = await makeContractDeploy({
35
+ contractName,
36
+ codeBody,
37
+ senderKey: env.wallet.privateKey,
38
+ network: env.network,
39
+ });
40
+ const result = await broadcastTransaction({ transaction: tx, network: env.network });
41
+ if (containsSecret(result)) {
42
+ console.warn('[mobilestacks] Warning: Output may contain sensitive data.');
43
+ }
44
+ // Mask any address fields if present
45
+ if (result && result.txid) {
46
+ result.txid = maskAddress(result.txid);
47
+ }
48
+ return result;
49
+ });
@@ -0,0 +1,7 @@
1
+ import { task } from '../core/dsl';
2
+
3
+ task('example', 'An example user task for onboarding')
4
+ .addParam('name', 'Your name', { type: 'string', required: false, defaultValue: 'World' })
5
+ .setAction(async (args) => {
6
+ return `Hello, ${args.name}! Welcome to mobilestacks.`;
7
+ });
@@ -0,0 +1,21 @@
1
+ import { task } from '../core/dsl';
2
+ import fetch from 'node-fetch';
3
+
4
+ task('faucet-request', 'Request STX from the testnet faucet')
5
+ .addParam('address', 'STX address to fund', { type: 'string', required: true })
6
+ .setAction(async (args, env) => {
7
+ if (env.network.client.baseUrl && env.network.client.baseUrl.includes('testnet')) {
8
+ const url = `https://stacks-node-api.testnet.stacks.co/extended/v1/faucet/stx`; // Default testnet faucet
9
+ const res = await fetch(url, {
10
+ method: 'POST',
11
+ headers: { 'Content-Type': 'application/json' },
12
+ body: JSON.stringify({ address: args.address })
13
+ });
14
+ if (!res.ok) {
15
+ throw new Error(`Faucet request failed: ${res.statusText}`);
16
+ }
17
+ return await res.json();
18
+ } else {
19
+ throw new Error('Faucet is only available on testnet.');
20
+ }
21
+ });
@@ -0,0 +1,34 @@
1
+ import { task } from '../core/dsl';
2
+ import fetch from 'node-fetch';
3
+
4
+ function maskAddress(address: string) {
5
+ return address ? address.slice(0, 6) + '...' + address.slice(-4) : '';
6
+ }
7
+
8
+ function containsSecret(obj: unknown): boolean {
9
+ const str = JSON.stringify(obj);
10
+ return /[A-Za-z0-9]{32,}/.test(str); // crude check for long secrets
11
+ }
12
+
13
+ task('get-balance', 'Get STX balance for the configured wallet address or a provided address')
14
+ .addParam('address', 'STX address to check (optional, defaults to wallet main address)', { type: 'string', required: false })
15
+ .setAction(async (args, env) => {
16
+ const address = (args.address as string) || env.wallet.address;
17
+ if (!address) throw new Error('No wallet address found.');
18
+ const network = env.network;
19
+ const apiUrl = network.client.baseUrl;
20
+ const url = `${apiUrl}/extended/v1/address/${address}/balances`;
21
+ const res = await fetch(url);
22
+ if (!res.ok) throw new Error(`Failed to fetch balance: ${res.statusText}`);
23
+ const data: unknown = await res.json();
24
+ const result = {
25
+ address: maskAddress(address),
26
+ stx: (data as { stx: { balance: string; locked: string; unlock_height: number } }).stx.balance,
27
+ locked: (data as { stx: { balance: string; locked: string; unlock_height: number } }).stx.locked,
28
+ unlock_height: (data as { stx: { balance: string; locked: string; unlock_height: number } }).stx.unlock_height
29
+ };
30
+ if (containsSecret(result)) {
31
+ console.warn('[mobilestacks] Warning: Output may contain sensitive data.');
32
+ }
33
+ return result;
34
+ });
@@ -0,0 +1,18 @@
1
+ import { task } from '../core/dsl';
2
+ import fetch from 'node-fetch';
3
+
4
+ task('get-contract-info', 'Get contract attributes and details from Stacks blockchain')
5
+ .addParam('contractAddress', 'Deployed contract address (STX...)', { type: 'string', required: true })
6
+ .addParam('contractName', 'Contract name', { type: 'string', required: true })
7
+ .addParam('network', 'Network (mainnet|testnet)', { type: 'string', required: false, defaultValue: 'testnet' })
8
+ .setAction(async (args) => {
9
+ const { contractAddress, contractName, network } = args;
10
+ const apiUrl = network === 'mainnet'
11
+ ? 'https://stacks-node-api.mainnet.stacks.co'
12
+ : 'https://stacks-node-api.testnet.stacks.co';
13
+ const url = `${apiUrl}/extended/v1/contract/${contractAddress}/${contractName}`;
14
+ const res = await fetch(url);
15
+ if (!res.ok) throw new Error(`Failed to fetch contract info: ${res.statusText}`);
16
+ const info = await res.json();
17
+ return info;
18
+ });
@@ -0,0 +1,39 @@
1
+ import { task } from '../core/dsl';
2
+ import fetch from 'node-fetch';
3
+
4
+ function maskAddress(address: string) {
5
+ return address ? address.slice(0, 6) + '...' + address.slice(-4) : '';
6
+ }
7
+
8
+ function containsSecret(obj: unknown): boolean {
9
+ const str = JSON.stringify(obj);
10
+ return /[A-Za-z0-9]{32,}/.test(str); // crude check for long secrets
11
+ }
12
+
13
+ task('get-tx-history', 'Get transaction history for the configured wallet address')
14
+ .addParam('limit', 'Number of transactions to fetch', { type: 'number', required: false, defaultValue: 10 })
15
+ .setAction(async (args, env) => {
16
+ const address = env.wallet.address;
17
+ if (!address) throw new Error('No wallet address found.');
18
+ const network = env.network;
19
+ const apiUrl = network.client.baseUrl;
20
+ const url = `${apiUrl}/extended/v1/address/${address}/transactions?limit=${args.limit}`;
21
+ const res = await fetch(url);
22
+ if (!res.ok) throw new Error(`Failed to fetch tx history: ${res.statusText}`);
23
+ const data = await res.json() as { results: unknown[] };
24
+ const txs = data.results.map((tx) => ({
25
+ tx_id: (tx as { tx_id: string }).tx_id,
26
+ type: (tx as { tx_type: string }).tx_type,
27
+ status: (tx as { tx_status: string }).tx_status,
28
+ fee_rate: (tx as { fee_rate: string }).fee_rate,
29
+ block_height: (tx as { block_height: number }).block_height
30
+ }));
31
+ // Mask address in logs and warn if secrets detected
32
+ if (containsSecret(txs)) {
33
+ console.warn('[mobilestacks] Warning: Output may contain sensitive data.');
34
+ }
35
+ return {
36
+ address: maskAddress(address),
37
+ transactions: txs
38
+ };
39
+ });
@@ -0,0 +1,27 @@
1
+ import { task } from '../core/dsl';
2
+ import { generateWallet } from '@stacks/wallet-sdk';
3
+
4
+ function maskAddress(address: string) {
5
+ return address ? address.slice(0, 6) + '...' + address.slice(-4) : '';
6
+ }
7
+
8
+ function containsSecret(obj: unknown): boolean {
9
+ const str = JSON.stringify(obj);
10
+ return /[A-Za-z0-9]{32,}/.test(str); // crude check for long secrets
11
+ }
12
+
13
+ task('list-accounts', 'List all accounts derived from the configured seed phrase')
14
+ .setAction(async (args, env) => {
15
+ if (!env.config.wallet.seedPhrase) {
16
+ throw new Error('No seed phrase configured.');
17
+ }
18
+ const wallet = await generateWallet({ secretKey: env.config.wallet.seedPhrase, password: '' });
19
+ const result = wallet.accounts.map((a: unknown) => ({
20
+ address: maskAddress((a as { address: string }).address || (a as { stxAddress: string }).stxAddress),
21
+ index: (a as { index: number }).index
22
+ }));
23
+ if (containsSecret(result)) {
24
+ console.warn('[mobilestacks] Warning: Output may contain sensitive data.');
25
+ }
26
+ return result;
27
+ });
@@ -0,0 +1,39 @@
1
+ import { task } from '../core/dsl';
2
+ import { broadcastTransaction, makeSTXTokenTransfer } from '@stacks/transactions';
3
+
4
+ function maskAddress(address: string) {
5
+ return address ? address.slice(0, 6) + '...' + address.slice(-4) : '';
6
+ }
7
+
8
+ function containsSecret(obj: unknown): boolean {
9
+ const str = JSON.stringify(obj);
10
+ return /[A-Za-z0-9]{32,}/.test(str); // crude check for long secrets
11
+ }
12
+
13
+ // This task sends STX to an address using the loaded SRE (env)
14
+ task('send-stx', 'Sends STX to an address')
15
+ .addParam('to', 'Recipient STX address', { type: 'string', required: true })
16
+ .addParam('amount', 'Amount in microSTX', { type: 'number', required: true })
17
+ .addParam('memo', 'Optional memo', { type: 'string', required: false, defaultValue: '' })
18
+ .setAction(async (args, env) => {
19
+ const to = args.to as string;
20
+ const amount = args.amount as number;
21
+ const memo = args.memo as string | undefined;
22
+ const { wallet, network } = env;
23
+ const tx = await makeSTXTokenTransfer({
24
+ recipient: to,
25
+ amount: BigInt(amount),
26
+ senderKey: wallet.privateKey,
27
+ network,
28
+ memo: memo || undefined
29
+ });
30
+ const result = await broadcastTransaction({ transaction: tx, network });
31
+ if (containsSecret(result)) {
32
+ console.warn('[mobilestacks] Warning: Output may contain sensitive data.');
33
+ }
34
+ // Mask any address fields if present
35
+ if (result && result.txid) {
36
+ result.txid = maskAddress(result.txid);
37
+ }
38
+ return result;
39
+ });
@@ -0,0 +1,33 @@
1
+ import { task } from '../core/dsl';
2
+ import fetch from 'node-fetch';
3
+ import fs from 'fs';
4
+
5
+ task('verify-contract', 'Verify a deployed Clarity contract on the Stacks explorer')
6
+ .addParam('contractAddress', 'Deployed contract address (STX...)', { type: 'string', required: true })
7
+ .addParam('contractName', 'Contract name', { type: 'string', required: true })
8
+ .addParam('source', 'Path to contract source file', { type: 'string', required: true })
9
+ .addParam('network', 'Network (mainnet|testnet)', { type: 'string', required: false, defaultValue: 'testnet' })
10
+ .setAction(async (args) => {
11
+ const contractAddress = args.contractAddress as string;
12
+ const contractName = args.contractName as string;
13
+ const source = args.source as string;
14
+ const network = args.network as string;
15
+ const codeBody = fs.readFileSync(source, 'utf8');
16
+ const apiUrl = network === 'mainnet'
17
+ ? 'https://stacks-node-api.mainnet.stacks.co'
18
+ : 'https://stacks-node-api.testnet.stacks.co';
19
+ // Fetch contract source from chain
20
+ const url = `${apiUrl}/extended/v1/contract/source/${contractAddress}/${contractName}`;
21
+ const res = await fetch(url);
22
+ if (!res.ok) throw new Error(`Failed to fetch on-chain contract source: ${res.statusText}`);
23
+ const onChain = await res.json() as { source_code?: string };
24
+ const verified = onChain.source_code && onChain.source_code.trim() === codeBody.trim();
25
+ const explorer = network === 'mainnet'
26
+ ? `https://explorer.stacks.co/txid/${contractAddress}.${contractName}`
27
+ : `https://explorer.stacks.co/txid/${contractAddress}.${contractName}?chain=testnet`;
28
+ return {
29
+ verified,
30
+ message: verified ? 'Contract source matches on-chain code!' : 'Source does NOT match on-chain code!',
31
+ explorer
32
+ };
33
+ });
@@ -0,0 +1,30 @@
1
+ import { z } from "zod";
2
+
3
+ export const NetworkConfigSchema = z.object({
4
+ url: z.string().url(),
5
+ name: z.string(),
6
+ explorerUrl: z.string().url().optional(),
7
+ faucetUrl: z.string().url().nullable().optional(),
8
+ });
9
+
10
+
11
+ // Wallet config: allow privateKey, seedPhrase, or both. If both, privateKey is used.
12
+ export const WalletConfigSchema = z.object({
13
+ privateKey: z.string().min(1, "Private key is required").optional(),
14
+ seedPhrase: z.string().optional(),
15
+ derivationPath: z.string().optional(),
16
+ address: z.string().optional(),
17
+ }).refine(
18
+ (data) => data.privateKey || data.seedPhrase,
19
+ { message: "Either privateKey or seedPhrase is required" }
20
+ );
21
+
22
+ export const MobilestacksConfigSchema = z.object({
23
+ networks: z.record(NetworkConfigSchema),
24
+ defaultNetwork: z.string(),
25
+ wallet: WalletConfigSchema,
26
+ });
27
+
28
+ export type NetworkConfig = z.infer<typeof NetworkConfigSchema>;
29
+ export type WalletConfig = z.infer<typeof WalletConfigSchema>;
30
+ export type MobilestacksConfig = z.infer<typeof MobilestacksConfigSchema>;