@taqueria/plugin-tezbox 0.61.4
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/LICENSE +201 -0
- package/README.md +15 -0
- package/docker.ts +8 -0
- package/index.d.mts +2 -0
- package/index.d.ts +2 -0
- package/index.js +720 -0
- package/index.js.map +1 -0
- package/index.mjs +703 -0
- package/index.mjs.map +1 -0
- package/index.ts +71 -0
- package/package.json +74 -0
- package/proxy.ts +1129 -0
- package/tsconfig.json +108 -0
- package/types.ts +34 -0
package/proxy.ts
ADDED
|
@@ -0,0 +1,1129 @@
|
|
|
1
|
+
import {
|
|
2
|
+
execCmd,
|
|
3
|
+
getArch,
|
|
4
|
+
getDockerImage,
|
|
5
|
+
SandboxAccountConfig,
|
|
6
|
+
SandboxConfig,
|
|
7
|
+
sendAsyncErr,
|
|
8
|
+
sendAsyncJsonRes,
|
|
9
|
+
sendAsyncRes,
|
|
10
|
+
} from '@taqueria/node-sdk';
|
|
11
|
+
import { generateSecretKey, InMemorySigner } from '@taquito/signer';
|
|
12
|
+
import BigNumber from 'bignumber.js';
|
|
13
|
+
import * as bip39 from 'bip39';
|
|
14
|
+
import { createHash } from 'crypto';
|
|
15
|
+
import { create } from 'domain';
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as hjson from 'hjson';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
import { getDefaultDockerImage } from './docker';
|
|
20
|
+
import { Opts } from './types';
|
|
21
|
+
|
|
22
|
+
type ConfigV1Environment = {
|
|
23
|
+
sandboxes?: string[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type Mutez = string | number; // Represents balance in mutez
|
|
27
|
+
|
|
28
|
+
type InstantiatedAccount = Omit<SandboxAccountConfig, 'encryptedKey'> & {
|
|
29
|
+
encryptedKey?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
interface TezboxAccount {
|
|
33
|
+
pkh: string;
|
|
34
|
+
pk: string;
|
|
35
|
+
sk: string;
|
|
36
|
+
balance: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type SandboxConfigV1 = SandboxConfig;
|
|
40
|
+
|
|
41
|
+
interface ProtocolMapping {
|
|
42
|
+
id: string; // e.g., "Proxford"
|
|
43
|
+
hash: string; // e.g., "PsDELPH1..."
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface DockerRunParams {
|
|
47
|
+
platform: string;
|
|
48
|
+
image: string;
|
|
49
|
+
containerName: string;
|
|
50
|
+
configDir: string;
|
|
51
|
+
dataDir: string;
|
|
52
|
+
port: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
enum BakingOption {
|
|
56
|
+
ENABLED = 'enabled',
|
|
57
|
+
DISABLED = 'disabled',
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Logger utility for standardized logging.
|
|
62
|
+
*/
|
|
63
|
+
const logger = {
|
|
64
|
+
info: (message: string) => console.log(message),
|
|
65
|
+
warn: (message: string) => console.warn(message),
|
|
66
|
+
error: (message: string) => console.error(message),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Extracts error message from unknown error type and prepends a prefix.
|
|
71
|
+
/**
|
|
72
|
+
* Extracts error message from unknown error type and optionally prepends a prefix.
|
|
73
|
+
*/
|
|
74
|
+
function getErrorMessage(prefix: string, error: unknown): string {
|
|
75
|
+
if (prefix === '') {
|
|
76
|
+
return error instanceof Error ? error.message : String(error);
|
|
77
|
+
}
|
|
78
|
+
if (typeof error === 'boolean') {
|
|
79
|
+
return `${prefix}:`;
|
|
80
|
+
}
|
|
81
|
+
const errorString = error instanceof Error ? error.message : String(error);
|
|
82
|
+
return `${prefix}: ${errorString}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Executes a shell command and standardizes error handling.
|
|
87
|
+
*/
|
|
88
|
+
/**
|
|
89
|
+
* Executes a shell command and standardizes error handling.
|
|
90
|
+
*/
|
|
91
|
+
async function runCommand(
|
|
92
|
+
cmd: string,
|
|
93
|
+
stderrHandler?: (stderr: string) => void | Promise<void>,
|
|
94
|
+
): Promise<{ stdout: string }> {
|
|
95
|
+
// logger.info(`Executing command: ${cmd}`);
|
|
96
|
+
try {
|
|
97
|
+
const { stdout, stderr } = await execCmd(cmd);
|
|
98
|
+
if (stderr.trim()) {
|
|
99
|
+
if (stderrHandler) {
|
|
100
|
+
await stderrHandler(stderr.trim());
|
|
101
|
+
} else {
|
|
102
|
+
throw new Error(stderr.trim());
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { stdout };
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw new Error(getErrorMessage(`Command failed`, error));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Checks if the given environment is configured for TezBox.
|
|
113
|
+
*/
|
|
114
|
+
function isTezBoxEnvironment(taskArgs: Opts): boolean {
|
|
115
|
+
const environment = taskArgs.config.environment[taskArgs.env];
|
|
116
|
+
if (!environment || typeof environment !== 'object') return false;
|
|
117
|
+
|
|
118
|
+
const sandboxes = (environment as ConfigV1Environment).sandboxes;
|
|
119
|
+
if (!Array.isArray(sandboxes) || sandboxes.length === 0) return false;
|
|
120
|
+
|
|
121
|
+
// Currently, we don't have a way to tell if a sandbox is TezBox-provided
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Throws warning indicating function is not implemented yet.
|
|
127
|
+
*/
|
|
128
|
+
async function instantiateDeclaredAccount(
|
|
129
|
+
declaredAccountName: string,
|
|
130
|
+
balanceInMutez: BigNumber,
|
|
131
|
+
): Promise<InstantiatedAccount> {
|
|
132
|
+
logger.warn(`instantiateDeclaredAccount is not implemented. Returning dummy data for ${declaredAccountName}.`);
|
|
133
|
+
// Return dummy data or default values
|
|
134
|
+
return {
|
|
135
|
+
encryptedKey: 'unencrypted:edpktdummykey123',
|
|
136
|
+
publicKeyHash: `tz1_dummy_public_key_hash_${declaredAccountName}`,
|
|
137
|
+
secretKey: 'unencrypted:edskdummysecretkey123',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Throws warning indicating function is not implemented yet.
|
|
143
|
+
*/
|
|
144
|
+
async function getInstantiatedAccounts(taskArgs: Opts): Promise<Record<string, InstantiatedAccount>> {
|
|
145
|
+
const sandboxConfig = getSandboxConfig(taskArgs);
|
|
146
|
+
if (!sandboxConfig.accounts) {
|
|
147
|
+
throw new Error('No instantiated accounts found in sandbox config.');
|
|
148
|
+
}
|
|
149
|
+
const accounts = sandboxConfig.accounts as Record<string, InstantiatedAccount>;
|
|
150
|
+
return Object.entries(accounts)
|
|
151
|
+
.filter(([key]) => key !== 'default')
|
|
152
|
+
.reduce((acc, [key, value]) => {
|
|
153
|
+
acc[key] = value;
|
|
154
|
+
return acc;
|
|
155
|
+
}, {} as Record<string, InstantiatedAccount>);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Gets the sandbox configuration for the given environment.
|
|
160
|
+
*/
|
|
161
|
+
function getSandboxConfig(taskArgs: Opts): SandboxConfigV1 {
|
|
162
|
+
if (!isTezBoxEnvironment(taskArgs)) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`This configuration doesn't appear to be configured to use TezBox environments. Check the ${taskArgs.env} environment in your .taq/config.json.`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const environment = taskArgs.config.environment[taskArgs.env] as ConfigV1Environment;
|
|
169
|
+
const sandboxName = environment.sandboxes?.[0];
|
|
170
|
+
if (sandboxName) {
|
|
171
|
+
const sandboxConfig = taskArgs.config.sandbox?.[sandboxName];
|
|
172
|
+
if (sandboxConfig) {
|
|
173
|
+
const retval: SandboxConfigV1 = {
|
|
174
|
+
blockTime: 1,
|
|
175
|
+
baking: BakingOption.ENABLED,
|
|
176
|
+
...sandboxConfig,
|
|
177
|
+
...sandboxConfig.annotations,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
return retval;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
throw new Error(`No sandbox configuration found for ${taskArgs.env} environment.`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Gets or creates instantiated accounts.
|
|
188
|
+
*/
|
|
189
|
+
async function getOrCreateInstantiatedAccounts(
|
|
190
|
+
taskArgs: Opts,
|
|
191
|
+
): Promise<Record<string, InstantiatedAccount>> {
|
|
192
|
+
let instantiatedAccounts: Record<string, InstantiatedAccount>;
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
// Attempt to get instantiated accounts
|
|
196
|
+
instantiatedAccounts = await getInstantiatedAccounts(taskArgs);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
// No instantiated accounts available, so we need to instantiate them
|
|
199
|
+
instantiatedAccounts = {};
|
|
200
|
+
const declaredAccounts = taskArgs.config.accounts as Record<string, Mutez>;
|
|
201
|
+
|
|
202
|
+
for (const [accountName, balanceInMutez] of Object.entries(declaredAccounts)) {
|
|
203
|
+
// Convert balance to BigNumber, removing any underscores used for formatting
|
|
204
|
+
const balanceInMutezBN = new BigNumber(balanceInMutez.toString().replace(/_/g, ''));
|
|
205
|
+
|
|
206
|
+
// Instantiate the declared account
|
|
207
|
+
const instantiatedAccount = await instantiateDeclaredAccount(accountName, balanceInMutezBN);
|
|
208
|
+
|
|
209
|
+
// Store the instantiated account
|
|
210
|
+
instantiatedAccounts[accountName] = instantiatedAccount;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Optionally, save the instantiated accounts to persist them for future runs
|
|
214
|
+
await saveInstantiatedAccounts(instantiatedAccounts, taskArgs.projectDir, taskArgs.env);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return instantiatedAccounts;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Saves instantiated accounts to the local configuration file.
|
|
222
|
+
* @param accounts Record of instantiated accounts
|
|
223
|
+
* @param projectDir Project directory path
|
|
224
|
+
* @param env Environment name
|
|
225
|
+
*/
|
|
226
|
+
async function saveInstantiatedAccounts(
|
|
227
|
+
accounts: Record<string, InstantiatedAccount>,
|
|
228
|
+
projectDir: string,
|
|
229
|
+
env: string,
|
|
230
|
+
): Promise<void> {
|
|
231
|
+
const configPath = path.join(projectDir, `.taq/config.local.${env}.json`);
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
// Read existing config or use an empty object if file doesn't exist
|
|
235
|
+
let config: Record<string, any> = {};
|
|
236
|
+
try {
|
|
237
|
+
const configContent = await fs.promises.readFile(configPath, 'utf8');
|
|
238
|
+
config = JSON.parse(configContent);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
// If file doesn't exist or there's an error reading it, we'll use an empty object
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Convert InstantiatedAccount to SandboxAccountConfig, omitting encryptedKey
|
|
244
|
+
const sandboxAccounts: Record<string, Omit<SandboxAccountConfig, 'encryptedKey'>> = {};
|
|
245
|
+
for (const [name, account] of Object.entries(accounts)) {
|
|
246
|
+
sandboxAccounts[name] = {
|
|
247
|
+
publicKeyHash: account.publicKeyHash,
|
|
248
|
+
secretKey: account.secretKey,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Update only the accounts property in the config
|
|
253
|
+
config.accounts = sandboxAccounts;
|
|
254
|
+
|
|
255
|
+
// Ensure the directory exists
|
|
256
|
+
await fs.promises.mkdir(path.dirname(configPath), { recursive: true });
|
|
257
|
+
|
|
258
|
+
// Write the updated config back to the file
|
|
259
|
+
await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
260
|
+
} catch (error) {
|
|
261
|
+
throw new Error(getErrorMessage('Failed to save instantiated accounts', error));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Generates a mnemonic phrase using BIP39.
|
|
267
|
+
* @param strength The entropy bit length, defaults to 128 (resulting in a 12-word mnemonic).
|
|
268
|
+
* @returns A promise that resolves to the generated mnemonic phrase.
|
|
269
|
+
*/
|
|
270
|
+
async function generateMnemonic(strength: number = 128): Promise<string> {
|
|
271
|
+
try {
|
|
272
|
+
const mnemonic = bip39.generateMnemonic(strength);
|
|
273
|
+
return mnemonic;
|
|
274
|
+
} catch (error) {
|
|
275
|
+
console.error('Error generating mnemonic:', error);
|
|
276
|
+
throw new Error('Failed to generate mnemonic');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Function to generate a new implicit account
|
|
281
|
+
async function createNewAccount() {
|
|
282
|
+
const mnemonic = await generateMnemonic();
|
|
283
|
+
|
|
284
|
+
// Generate the BIP39 seed from the mnemonic
|
|
285
|
+
const seed = await bip39.mnemonicToSeed(mnemonic);
|
|
286
|
+
|
|
287
|
+
// Convert the seed (Buffer) to a UInt8Array
|
|
288
|
+
const seedUInt8Array = new Uint8Array(seed);
|
|
289
|
+
|
|
290
|
+
// Generate the secret key
|
|
291
|
+
const secretKey = generateSecretKey(seedUInt8Array, "m/44'/1729'/0'/0'", 'ed25519');
|
|
292
|
+
|
|
293
|
+
// Derive the public key and public key hash from the secret key
|
|
294
|
+
const signer = new InMemorySigner(secretKey);
|
|
295
|
+
const publicKey = await signer.publicKey();
|
|
296
|
+
const publicKeyHash = await signer.publicKeyHash();
|
|
297
|
+
|
|
298
|
+
// Return the object with pk, pkh, sk, and balance
|
|
299
|
+
return {
|
|
300
|
+
pk: publicKey,
|
|
301
|
+
pkh: publicKeyHash,
|
|
302
|
+
sk: `unencrypted:${secretKey}`,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function createFunderAccount() {
|
|
307
|
+
return createNewAccount().then(account => ({
|
|
308
|
+
publicKeyHash: account.pkh,
|
|
309
|
+
secretKey: account.sk,
|
|
310
|
+
}));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function getPublicKeyFromSecretKey(secretKey: string) {
|
|
314
|
+
// Initialize the signer with the secret key
|
|
315
|
+
const signer = await InMemorySigner.fromSecretKey(secretKey.replace('unencrypted:', ''));
|
|
316
|
+
|
|
317
|
+
// Get the public key
|
|
318
|
+
const publicKey = await signer.publicKey();
|
|
319
|
+
|
|
320
|
+
return publicKey;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Prepares TezBox accounts configuration.
|
|
325
|
+
*/
|
|
326
|
+
/**
|
|
327
|
+
* Prepares TezBox accounts configuration.
|
|
328
|
+
*/
|
|
329
|
+
async function prepareTezBoxAccounts(
|
|
330
|
+
instantiatedAccounts: Record<string, InstantiatedAccount>,
|
|
331
|
+
declaredAccounts: Record<string, Mutez>,
|
|
332
|
+
): Promise<Record<string, TezboxAccount>> {
|
|
333
|
+
// Add funder account to instantiatedAccounts
|
|
334
|
+
// instantiatedAccounts['funder'] = await createFunderAccount();
|
|
335
|
+
|
|
336
|
+
const tezboxAccounts: Record<string, TezboxAccount> = {};
|
|
337
|
+
|
|
338
|
+
for (const [accountName, accountData] of Object.entries(instantiatedAccounts)) {
|
|
339
|
+
if (accountName === 'default') continue;
|
|
340
|
+
|
|
341
|
+
const secretKey = accountData.secretKey;
|
|
342
|
+
tezboxAccounts[accountName] = {
|
|
343
|
+
pkh: accountData.publicKeyHash,
|
|
344
|
+
pk: await getPublicKeyFromSecretKey(secretKey),
|
|
345
|
+
sk: secretKey,
|
|
346
|
+
balance: accountName === 'funder'
|
|
347
|
+
? new BigNumber(100000000000000).toString()
|
|
348
|
+
: new BigNumber(declaredAccounts[accountName].toString()).toString(),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return tezboxAccounts;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Writes accounts.hjson file.
|
|
357
|
+
*/
|
|
358
|
+
async function writeAccountsHjson(
|
|
359
|
+
tezboxAccounts: Record<string, TezboxAccount>,
|
|
360
|
+
tezBoxConfigDir: string,
|
|
361
|
+
): Promise<void> {
|
|
362
|
+
// TODO: Remove for debugging
|
|
363
|
+
await Promise.resolve();
|
|
364
|
+
|
|
365
|
+
// // Rearrange accounts record so that funder is first
|
|
366
|
+
// const funderAccount = tezboxAccounts['funder'];
|
|
367
|
+
// delete tezboxAccounts['funder'];
|
|
368
|
+
// tezboxAccounts = { funder: funderAccount, ...tezboxAccounts };
|
|
369
|
+
|
|
370
|
+
// Convert the accounts object to HJSON format
|
|
371
|
+
const hjsonContent = hjson.stringify(tezboxAccounts, {
|
|
372
|
+
quotes: 'min',
|
|
373
|
+
bracesSameLine: true,
|
|
374
|
+
separator: false,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Remove quotes around sk values and ensure proper indentation
|
|
378
|
+
const fixedHjsonContent = hjsonContent.replaceAll('"', '');
|
|
379
|
+
|
|
380
|
+
// Write the accounts.hjson file
|
|
381
|
+
const accountsHjsonPath = path.join(tezBoxConfigDir, 'accounts.hjson');
|
|
382
|
+
await fs.promises.writeFile(accountsHjsonPath, fixedHjsonContent, 'utf8');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Gets the declared accounts from the task arguments and removes underscores from Mutez values.
|
|
387
|
+
*/
|
|
388
|
+
function getDeclaredAccounts(taskArgs: Opts): Record<string, Mutez> {
|
|
389
|
+
const declaredAccounts = taskArgs.config.accounts as Record<string, Mutez>;
|
|
390
|
+
return Object.entries(declaredAccounts).reduce((acc, [key, value]) => {
|
|
391
|
+
acc[key] = value.toString().replace(/_/g, '');
|
|
392
|
+
return acc;
|
|
393
|
+
}, {} as Record<string, Mutez>);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Prepares accounts.hjson for TezBox.
|
|
398
|
+
*/
|
|
399
|
+
async function prepareAccountsHjson(taskArgs: Opts, tezBoxConfigDir: string): Promise<void> {
|
|
400
|
+
try {
|
|
401
|
+
// Get or create instantiated accounts
|
|
402
|
+
const instantiatedAccounts = await getOrCreateInstantiatedAccounts(taskArgs);
|
|
403
|
+
|
|
404
|
+
// Retrieve declared accounts
|
|
405
|
+
const declaredAccounts = getDeclaredAccounts(taskArgs);
|
|
406
|
+
|
|
407
|
+
// Prepare tezbox accounts
|
|
408
|
+
const tezboxAccounts = await prepareTezBoxAccounts(instantiatedAccounts, declaredAccounts);
|
|
409
|
+
|
|
410
|
+
// Write the accounts.hjson file
|
|
411
|
+
await writeAccountsHjson(tezboxAccounts, tezBoxConfigDir);
|
|
412
|
+
} catch (error) {
|
|
413
|
+
throw new Error(getErrorMessage(`Failed to prepare accounts`, error));
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Prepares bakers.hjson for TezBox.
|
|
419
|
+
*/
|
|
420
|
+
async function prepareBakersHjson(taskArgs: Opts, tezBoxConfigDir: string): Promise<void> {
|
|
421
|
+
try {
|
|
422
|
+
// Get declared accounts
|
|
423
|
+
const declaredAccounts = getDeclaredAccounts(taskArgs);
|
|
424
|
+
|
|
425
|
+
// Calculate total balance
|
|
426
|
+
const totalBalance = Object.values(declaredAccounts).reduce(
|
|
427
|
+
(sum, balance) => BigNumber.sum(sum, balance),
|
|
428
|
+
new BigNumber(0),
|
|
429
|
+
).toString();
|
|
430
|
+
|
|
431
|
+
// Prepare bakers object
|
|
432
|
+
const bakers = {
|
|
433
|
+
baker1: {
|
|
434
|
+
pkh: 'tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU',
|
|
435
|
+
pk: 'edpkuTXkJDGcFd5nh6VvMz8phXxU3Bi7h6hqgywNFi1vZTfQNnS1RV',
|
|
436
|
+
sk: 'unencrypted:edsk4ArLQgBTLWG5FJmnGnT689VKoqhXwmDPBuGx3z4cvwU9MmrPZZ',
|
|
437
|
+
balance: totalBalance,
|
|
438
|
+
},
|
|
439
|
+
baker2: {
|
|
440
|
+
pkh: 'tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN',
|
|
441
|
+
pk: 'edpktzNbDAUjUk697W7gYg2CRuBQjyPxbEg8dLccYYwKSKvkPvjtV9',
|
|
442
|
+
sk: 'unencrypted:edsk39qAm1fiMjgmPkw1EgQYkMzkJezLNewd7PLNHTkr6w9XA2zdfo',
|
|
443
|
+
balance: totalBalance,
|
|
444
|
+
},
|
|
445
|
+
baker3: {
|
|
446
|
+
pkh: 'tz1b7tUupMgCNw2cCLpKTkSD1NZzB5TkP2sv',
|
|
447
|
+
pk: 'edpkuFrRoDSEbJYgxRtLx2ps82UdaYc1WwfS9sE11yhauZt5DgCHbU',
|
|
448
|
+
sk: 'unencrypted:edsk2uqQB9AY4FvioK2YMdfmyMrer5R8mGFyuaLLFfSRo8EoyNdht3',
|
|
449
|
+
balance: totalBalance,
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// Convert the bakers object to HJSON format
|
|
454
|
+
const hjsonContent = hjson.stringify(bakers, {
|
|
455
|
+
quotes: 'min',
|
|
456
|
+
bracesSameLine: true,
|
|
457
|
+
separator: false,
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// Remove quotes around sk values and ensure proper indentation
|
|
461
|
+
const fixedHjsonContent = hjsonContent.replaceAll('"', '');
|
|
462
|
+
|
|
463
|
+
// Write the bakers.hjson file
|
|
464
|
+
const bakersHjsonPath = path.join(tezBoxConfigDir, 'bakers.hjson');
|
|
465
|
+
await fs.promises.writeFile(bakersHjsonPath, fixedHjsonContent, 'utf8');
|
|
466
|
+
} catch (error) {
|
|
467
|
+
throw new Error(getErrorMessage(`Failed to prepare bakers`, error));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Generates a project ID based on the project directory.
|
|
473
|
+
*/
|
|
474
|
+
function getProjectId(taskArgs: Opts): string {
|
|
475
|
+
return createHash('sha256').update(taskArgs.projectDir).digest('hex');
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Gets the docker container name for the sandbox.
|
|
480
|
+
*/
|
|
481
|
+
function getDockerContainerName(taskArgs: Opts): string {
|
|
482
|
+
const projectId = getProjectId(taskArgs);
|
|
483
|
+
return `taq-${taskArgs.env}-${projectId}`;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Gets the TezBox configuration directory.
|
|
488
|
+
*/
|
|
489
|
+
function getTezBoxConfigDir(taskArgs: Opts): string {
|
|
490
|
+
const containerName = getDockerContainerName(taskArgs);
|
|
491
|
+
return path.join(taskArgs.projectDir, `.taq/.${containerName}/config`);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Gets the TezBox data directory.
|
|
496
|
+
*/
|
|
497
|
+
function getTezBoxDataDir(taskArgs: Opts): string {
|
|
498
|
+
const containerName = getDockerContainerName(taskArgs);
|
|
499
|
+
return path.join(taskArgs.projectDir, `.taq/.${containerName}/data`);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Gets the docker image for TezBox.
|
|
504
|
+
*/
|
|
505
|
+
function getImage(taskArgs: Opts): string {
|
|
506
|
+
return getDockerImage(getDefaultDockerImage(taskArgs), 'TAQ_TEZBOX_IMAGE');
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Checks if the sandbox is running.
|
|
511
|
+
*/
|
|
512
|
+
async function isSandboxRunning(taskArgs: Opts): Promise<boolean> {
|
|
513
|
+
const containerName = getDockerContainerName(taskArgs);
|
|
514
|
+
const cmd = `docker ps --filter "name=${containerName}" --format "{{.ID}}"`;
|
|
515
|
+
const { stdout } = await runCommand(cmd);
|
|
516
|
+
return stdout.trim() !== '';
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Checks if the sandbox is already running.
|
|
521
|
+
*/
|
|
522
|
+
async function checkSandboxRunning(taskArgs: Opts): Promise<boolean> {
|
|
523
|
+
const running = await isSandboxRunning(taskArgs);
|
|
524
|
+
if (running) {
|
|
525
|
+
await sendAsyncRes('Sandbox is already running.');
|
|
526
|
+
}
|
|
527
|
+
return running;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function getPortNumber(taskArgs: Opts) {
|
|
531
|
+
const rpcUrl = getSandboxConfig(taskArgs).rpcUrl;
|
|
532
|
+
const match = rpcUrl.match(/:(\d+)/);
|
|
533
|
+
return match ? parseInt(match[1], 10) : 80;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function getContainerPort(taskArgs: Opts) {
|
|
537
|
+
return getPortNumber(taskArgs) + 1;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Gets the docker run parameters.
|
|
542
|
+
*/
|
|
543
|
+
async function getDockerRunParams(taskArgs: Opts): Promise<DockerRunParams> {
|
|
544
|
+
const image = getImage(taskArgs);
|
|
545
|
+
const containerName = getDockerContainerName(taskArgs);
|
|
546
|
+
const platform = await getArch();
|
|
547
|
+
const configDir = getTezBoxConfigDir(taskArgs);
|
|
548
|
+
const dataDir = getTezBoxDataDir(taskArgs);
|
|
549
|
+
const port = getContainerPort(taskArgs);
|
|
550
|
+
|
|
551
|
+
return { platform, image, containerName, configDir, dataDir, port };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Ensures directories exist.
|
|
556
|
+
*/
|
|
557
|
+
async function ensureDirectoriesExist(directories: string[]): Promise<void> {
|
|
558
|
+
await Promise.all(
|
|
559
|
+
directories.map(dir => fs.promises.mkdir(dir, { recursive: true })),
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Constructs the docker run command.
|
|
565
|
+
*/
|
|
566
|
+
function constructDockerRunCommand(params: DockerRunParams): string {
|
|
567
|
+
const { platform, image, containerName, configDir, port } = params;
|
|
568
|
+
|
|
569
|
+
const dockerOptions = [
|
|
570
|
+
'docker run',
|
|
571
|
+
'-d',
|
|
572
|
+
`--platform ${platform}`,
|
|
573
|
+
'-p 8732:8732',
|
|
574
|
+
`-p ${port}:20000`,
|
|
575
|
+
`--name ${containerName}`,
|
|
576
|
+
`-v "${configDir}:/tezbox/overrides"`,
|
|
577
|
+
image,
|
|
578
|
+
// 'qenabox', // TODO: restore once working upstream
|
|
579
|
+
];
|
|
580
|
+
|
|
581
|
+
return dockerOptions.join(' ');
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Validates the block time in the sandbox configuration.
|
|
586
|
+
*/
|
|
587
|
+
function validateBlockTime(taskArgs: Opts): number | null {
|
|
588
|
+
const sandboxConfig = getSandboxConfig(taskArgs);
|
|
589
|
+
const blockTime = sandboxConfig.blockTime;
|
|
590
|
+
if (blockTime === undefined || blockTime === null) {
|
|
591
|
+
logger.warn('Block time is not specified; skipping block_time override.');
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
return blockTime;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Writes sandbox parameters for a single protocol.
|
|
599
|
+
*/
|
|
600
|
+
async function writeSandboxParameters(
|
|
601
|
+
protocolId: string,
|
|
602
|
+
parameters: Record<string, string | number>,
|
|
603
|
+
tezBoxConfigDir: string,
|
|
604
|
+
): Promise<void> {
|
|
605
|
+
const protocolsDir = path.join(tezBoxConfigDir, 'protocols', protocolId);
|
|
606
|
+
await fs.promises.mkdir(protocolsDir, { recursive: true });
|
|
607
|
+
|
|
608
|
+
const hjsonContent = hjson.stringify(parameters, {
|
|
609
|
+
quotes: 'min',
|
|
610
|
+
bracesSameLine: true,
|
|
611
|
+
separator: false,
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
const sandboxParamsPath = path.join(protocolsDir, 'sandbox-parameters.hjson');
|
|
615
|
+
await fs.promises.writeFile(sandboxParamsPath, hjsonContent, 'utf8');
|
|
616
|
+
|
|
617
|
+
// Ensure the file has write permissions
|
|
618
|
+
try {
|
|
619
|
+
await fs.promises.chmod(sandboxParamsPath, 0o644);
|
|
620
|
+
} catch (error) {
|
|
621
|
+
logger.warn(getErrorMessage(`Failed to set file permissions for ${sandboxParamsPath}`, error));
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Applies block time override to a single protocol.
|
|
627
|
+
*/
|
|
628
|
+
async function applyBlockTimeOverrideToProtocol(
|
|
629
|
+
protocolId: string,
|
|
630
|
+
blockTime: number,
|
|
631
|
+
tezBoxConfigDir: string,
|
|
632
|
+
): Promise<void> {
|
|
633
|
+
const nonce_revelation_threshold = 16;
|
|
634
|
+
const minimal_block_delay = blockTime;
|
|
635
|
+
|
|
636
|
+
const parameters = {
|
|
637
|
+
minimal_block_delay: minimal_block_delay.toString(),
|
|
638
|
+
};
|
|
639
|
+
await writeSandboxParameters(protocolId, parameters, tezBoxConfigDir);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Applies block time override to multiple protocols.
|
|
644
|
+
*/
|
|
645
|
+
async function applyBlockTimeOverrideToProtocols(
|
|
646
|
+
protocolIds: string[],
|
|
647
|
+
blockTime: number,
|
|
648
|
+
tezBoxConfigDir: string,
|
|
649
|
+
): Promise<void> {
|
|
650
|
+
await Promise.all(
|
|
651
|
+
protocolIds.map(async protocolId => {
|
|
652
|
+
// Skip alpha protocol as it's a placeholder
|
|
653
|
+
if (/^alpha$/i.test(protocolId)) {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
await applyBlockTimeOverrideToProtocol(protocolId, blockTime, tezBoxConfigDir);
|
|
657
|
+
}),
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Gets protocol identifiers from TezBox configuration.
|
|
663
|
+
*/
|
|
664
|
+
async function getProtocolIds(taskArgs: Opts): Promise<string[]> {
|
|
665
|
+
const image = getImage(taskArgs);
|
|
666
|
+
|
|
667
|
+
// List the protocol directories inside the TezBox image
|
|
668
|
+
const cmd = `docker run --rm --entrypoint ls ${image} /tezbox/configuration/protocols`;
|
|
669
|
+
const { stdout } = await runCommand(cmd);
|
|
670
|
+
|
|
671
|
+
const protocolIds = stdout
|
|
672
|
+
.trim()
|
|
673
|
+
.split('\n')
|
|
674
|
+
.map(line => line.trim())
|
|
675
|
+
.filter(line => line !== '');
|
|
676
|
+
|
|
677
|
+
return protocolIds;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Reads and parses protocol.hjson for a given protocolId.
|
|
682
|
+
*/
|
|
683
|
+
async function readProtocolJson(image: string, protocolId: string): Promise<ProtocolMapping | null> {
|
|
684
|
+
const cmd = `docker run --rm --entrypoint cat ${image} /tezbox/configuration/protocols/${protocolId}/protocol.hjson`;
|
|
685
|
+
|
|
686
|
+
try {
|
|
687
|
+
const { stdout } = await runCommand(cmd);
|
|
688
|
+
|
|
689
|
+
if (!stdout.trim()) {
|
|
690
|
+
logger.warn(`protocol.hjson not found or empty for protocolId ${protocolId}; skipping this protocol.`);
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Parse the HJSON content
|
|
695
|
+
const protocolData = hjson.parse(stdout);
|
|
696
|
+
const protocolHash: string = protocolData.hash;
|
|
697
|
+
if (protocolHash) {
|
|
698
|
+
return { id: protocolId, hash: protocolHash };
|
|
699
|
+
} else {
|
|
700
|
+
logger.warn(`Protocol hash not found in protocol.hjson for protocolId ${protocolId}; skipping.`);
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
703
|
+
} catch (error) {
|
|
704
|
+
logger.warn(getErrorMessage(`Failed to read protocol.hjson for protocolId ${protocolId}`, error));
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Gets protocol mappings.
|
|
711
|
+
*/
|
|
712
|
+
async function getProtocolMappings(taskArgs: Opts): Promise<ProtocolMapping[]> {
|
|
713
|
+
const image = getImage(taskArgs);
|
|
714
|
+
const protocolIds = await getProtocolIds(taskArgs);
|
|
715
|
+
|
|
716
|
+
const protocolMappingsPromises = protocolIds.map(async protocolId => {
|
|
717
|
+
const mapping = await readProtocolJson(image, protocolId);
|
|
718
|
+
return mapping;
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
const protocolMappings = await Promise.all(protocolMappingsPromises);
|
|
722
|
+
return protocolMappings.filter((mapping): mapping is ProtocolMapping => mapping !== null);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Gets protocol hashes from octez-client.
|
|
727
|
+
*/
|
|
728
|
+
async function getOctezClientProtocols(taskArgs: Opts): Promise<string[]> {
|
|
729
|
+
const image = getImage(taskArgs);
|
|
730
|
+
const cmd = `docker run --rm --entrypoint octez-client ${image} -M mockup list mockup protocols`;
|
|
731
|
+
const { stdout } = await runCommand(cmd, stderr => {
|
|
732
|
+
const ignorableError = 'Base directory /tezbox/data/.tezos-client does not exist.';
|
|
733
|
+
|
|
734
|
+
if (stderr.trim() !== '' && !stderr.includes(ignorableError)) {
|
|
735
|
+
throw new Error(`Failed to list protocols: ${stderr.trim()}`);
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
const protocols = stdout
|
|
740
|
+
.trim()
|
|
741
|
+
.split('\n')
|
|
742
|
+
.filter(line => line.trim() !== '');
|
|
743
|
+
|
|
744
|
+
return protocols;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Prepares sandbox-parameters.hjson for block_time override.
|
|
749
|
+
*/
|
|
750
|
+
async function prepareSandboxParametersHjson(taskArgs: Opts, tezBoxConfigDir: string): Promise<void> {
|
|
751
|
+
try {
|
|
752
|
+
// Validate block time
|
|
753
|
+
const blockTime = validateBlockTime(taskArgs);
|
|
754
|
+
if (blockTime === null) {
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Get the protocol mappings from TezBox
|
|
759
|
+
const protocolMappings = await getProtocolMappings(taskArgs);
|
|
760
|
+
const hashToIdMap: Record<string, string> = {};
|
|
761
|
+
for (const mapping of protocolMappings) {
|
|
762
|
+
hashToIdMap[mapping.hash] = mapping.id;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Get the list of protocol hashes supported by octez-client
|
|
766
|
+
const protocolHashes = await getOctezClientProtocols(taskArgs);
|
|
767
|
+
|
|
768
|
+
// Map protocol hashes to TezBox protocol IDs
|
|
769
|
+
const protocolIdsToOverride = protocolHashes
|
|
770
|
+
.map(hash => hashToIdMap[hash])
|
|
771
|
+
.filter((id): id is string => id !== undefined);
|
|
772
|
+
|
|
773
|
+
if (protocolIdsToOverride.length === 0) {
|
|
774
|
+
logger.warn('No matching protocol IDs found; cannot set block_time override.');
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Debug: Log the protocol IDs to override
|
|
779
|
+
// logger.info(`Protocol IDs to override: ${protocolIdsToOverride.join(', ')}`);
|
|
780
|
+
|
|
781
|
+
// Apply block time override to each protocol ID
|
|
782
|
+
await applyBlockTimeOverrideToProtocols(protocolIdsToOverride, blockTime, tezBoxConfigDir);
|
|
783
|
+
} catch (error) {
|
|
784
|
+
const errorMessage = getErrorMessage(`Failed to prepare sandbox parameters:`, error);
|
|
785
|
+
throw new Error(errorMessage);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Prepares baker.hjson if baking is disabled.
|
|
791
|
+
*/
|
|
792
|
+
async function prepareBakerHjson(tezBoxConfigDir: string): Promise<void> {
|
|
793
|
+
const servicesDir = path.join(tezBoxConfigDir, 'services');
|
|
794
|
+
try {
|
|
795
|
+
await fs.promises.mkdir(servicesDir, { recursive: true });
|
|
796
|
+
|
|
797
|
+
const bakerConfig = {
|
|
798
|
+
autostart: false,
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
const hjsonContent = hjson.stringify(bakerConfig, {
|
|
802
|
+
quotes: 'all',
|
|
803
|
+
bracesSameLine: true,
|
|
804
|
+
separator: true,
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
const bakerConfigPath = path.join(servicesDir, 'baker.hjson');
|
|
808
|
+
await fs.promises.writeFile(bakerConfigPath, hjsonContent, 'utf8');
|
|
809
|
+
} catch (error) {
|
|
810
|
+
throw new Error(getErrorMessage(`Failed to prepare baker.hjson`, error));
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Prepares TezBox configuration overrides.
|
|
816
|
+
*/
|
|
817
|
+
async function prepareTezBoxOverrides(taskArgs: Opts): Promise<void> {
|
|
818
|
+
const tezBoxConfigDir = getTezBoxConfigDir(taskArgs);
|
|
819
|
+
|
|
820
|
+
try {
|
|
821
|
+
// Get sandbox configuration
|
|
822
|
+
const sandboxConfig = getSandboxConfig(taskArgs);
|
|
823
|
+
|
|
824
|
+
// Ensure the configuration directory exists
|
|
825
|
+
await fs.promises.mkdir(tezBoxConfigDir, { recursive: true });
|
|
826
|
+
|
|
827
|
+
// Prepare tasks
|
|
828
|
+
const tasks: Promise<void>[] = [];
|
|
829
|
+
|
|
830
|
+
// Prepare bakers.hjson
|
|
831
|
+
tasks.push(prepareBakersHjson(taskArgs, tezBoxConfigDir));
|
|
832
|
+
|
|
833
|
+
// Prepare accounts.hjson
|
|
834
|
+
if (taskArgs.config.accounts) {
|
|
835
|
+
tasks.push(prepareAccountsHjson(taskArgs, tezBoxConfigDir));
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Prepare sandbox-parameters.hjson for block_time
|
|
839
|
+
if (sandboxConfig.blockTime) {
|
|
840
|
+
tasks.push(prepareSandboxParametersHjson(taskArgs, tezBoxConfigDir));
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Prepare baker.hjson if baking is disabled
|
|
844
|
+
if (sandboxConfig.baking === BakingOption.DISABLED) {
|
|
845
|
+
tasks.push(prepareBakerHjson(tezBoxConfigDir));
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Run all preparations in parallel
|
|
849
|
+
await Promise.all(tasks);
|
|
850
|
+
} catch (error) {
|
|
851
|
+
throw new Error(getErrorMessage(`Failed to prepare TezBox overrides`, error));
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function getProxyContainerName(taskArgs: Opts) {
|
|
856
|
+
return `${getDockerContainerName(taskArgs)}-proxy`;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
async function startProxyServer(taskArgs: Opts): Promise<void> {
|
|
860
|
+
const containerPort = getContainerPort(taskArgs);
|
|
861
|
+
const proxyPort = getPortNumber(taskArgs);
|
|
862
|
+
const proxyContainerName = getProxyContainerName(taskArgs);
|
|
863
|
+
|
|
864
|
+
const proxyCmd = `docker run -d --name ${proxyContainerName} \
|
|
865
|
+
--network host \
|
|
866
|
+
caddy:2-alpine \
|
|
867
|
+
caddy reverse-proxy \
|
|
868
|
+
--from http://:${proxyPort} \
|
|
869
|
+
--to http://127.0.0.1:${containerPort} \
|
|
870
|
+
--access-log`;
|
|
871
|
+
|
|
872
|
+
try {
|
|
873
|
+
await runCommand(proxyCmd);
|
|
874
|
+
} catch (error) {
|
|
875
|
+
throw new Error(getErrorMessage(`Failed to start Caddy reverse proxy`, error));
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
async function stopProxyServer(taskArgs: Opts): Promise<void> {
|
|
880
|
+
const proxyContainerName = getProxyContainerName(taskArgs);
|
|
881
|
+
const cmd = `docker rm -f ${proxyContainerName}`;
|
|
882
|
+
await runCommand(cmd);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Starts the sandbox.
|
|
887
|
+
*/
|
|
888
|
+
async function startSandbox(taskArgs: Opts): Promise<void> {
|
|
889
|
+
try {
|
|
890
|
+
// Check for Docker availability
|
|
891
|
+
await checkDockerAvailability();
|
|
892
|
+
|
|
893
|
+
// Check if the sandbox is already running
|
|
894
|
+
if (await checkSandboxRunning(taskArgs)) {
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Get Docker run parameters
|
|
899
|
+
const params = await getDockerRunParams(taskArgs);
|
|
900
|
+
|
|
901
|
+
// Ensure necessary directories exist
|
|
902
|
+
await ensureDirectoriesExist([params.dataDir, params.configDir]);
|
|
903
|
+
|
|
904
|
+
// Prepare TezBox configuration overrides
|
|
905
|
+
await prepareTezBoxOverrides(taskArgs);
|
|
906
|
+
|
|
907
|
+
// Construct the Docker run command
|
|
908
|
+
const cmd = constructDockerRunCommand(params);
|
|
909
|
+
// logger.info(`Starting sandbox with command: ${cmd}`);
|
|
910
|
+
|
|
911
|
+
// Execute the Docker run command
|
|
912
|
+
await runCommand(cmd);
|
|
913
|
+
|
|
914
|
+
// Start the proxy server
|
|
915
|
+
await startProxyServer(taskArgs);
|
|
916
|
+
|
|
917
|
+
// Send a success response
|
|
918
|
+
await sendAsyncRes('Sandbox started successfully.');
|
|
919
|
+
} catch (error) {
|
|
920
|
+
await sendAsyncErr(getErrorMessage(`Failed to start sandbox`, error));
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Checks for Docker availability.
|
|
926
|
+
*/
|
|
927
|
+
async function checkDockerAvailability(): Promise<void> {
|
|
928
|
+
try {
|
|
929
|
+
await runCommand('docker --version');
|
|
930
|
+
} catch (error) {
|
|
931
|
+
throw new Error('Docker is not installed or not running. Please install and start Docker.');
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Removes the sandbox container.
|
|
937
|
+
*/
|
|
938
|
+
async function removeSandboxContainer(taskArgs: Opts): Promise<void> {
|
|
939
|
+
const containerName = getDockerContainerName(taskArgs);
|
|
940
|
+
const cmd = `docker rm -f ${containerName}`;
|
|
941
|
+
|
|
942
|
+
try {
|
|
943
|
+
await runCommand(cmd);
|
|
944
|
+
} catch (error) {
|
|
945
|
+
const errorMessage = getErrorMessage('', error);
|
|
946
|
+
if (errorMessage.includes('No such container')) {
|
|
947
|
+
// Container does not exist
|
|
948
|
+
await sendAsyncRes('Sandbox is not running or already stopped.');
|
|
949
|
+
} else {
|
|
950
|
+
throw new Error(errorMessage);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Stop the proxy server
|
|
955
|
+
await stopProxyServer(taskArgs);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* Stops the sandbox.
|
|
960
|
+
*/
|
|
961
|
+
async function stopSandbox(taskArgs: Opts): Promise<void> {
|
|
962
|
+
try {
|
|
963
|
+
// Attempt to stop and remove the sandbox container
|
|
964
|
+
await removeSandboxContainer(taskArgs);
|
|
965
|
+
|
|
966
|
+
// Optionally, clean up configuration directory if needed
|
|
967
|
+
const configDir = getTezBoxConfigDir(taskArgs);
|
|
968
|
+
await fs.promises.rm(configDir, { recursive: true, force: true });
|
|
969
|
+
|
|
970
|
+
// Send a success response
|
|
971
|
+
await sendAsyncRes('Sandbox stopped and cleaned up.');
|
|
972
|
+
} catch (error) {
|
|
973
|
+
await sendAsyncErr(getErrorMessage(`Failed to stop sandbox`, error));
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Restarts the sandbox.
|
|
979
|
+
*/
|
|
980
|
+
async function restartSandbox(taskArgs: Opts): Promise<void> {
|
|
981
|
+
try {
|
|
982
|
+
// Stop the sandbox if it's running
|
|
983
|
+
await removeSandboxContainer(taskArgs);
|
|
984
|
+
|
|
985
|
+
// Start the sandbox
|
|
986
|
+
await startSandbox(taskArgs);
|
|
987
|
+
|
|
988
|
+
// Send a success response
|
|
989
|
+
await sendAsyncRes('Sandbox restarted successfully.');
|
|
990
|
+
} catch (error) {
|
|
991
|
+
await sendAsyncErr(getErrorMessage(`Failed to restart sandbox`, error));
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* Lists protocols.
|
|
997
|
+
*/
|
|
998
|
+
async function listProtocols(taskArgs: Opts): Promise<void> {
|
|
999
|
+
try {
|
|
1000
|
+
const protocolHashes = await getOctezClientProtocols(taskArgs);
|
|
1001
|
+
const protocolObjects = protocolHashes.map(protocol => ({ protocol }));
|
|
1002
|
+
await sendAsyncJsonRes(protocolObjects);
|
|
1003
|
+
} catch (error) {
|
|
1004
|
+
await sendAsyncErr(getErrorMessage(`Failed to list protocols`, error));
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Lists accounts.
|
|
1010
|
+
*/
|
|
1011
|
+
async function listAccounts(taskArgs: Opts): Promise<void> {
|
|
1012
|
+
try {
|
|
1013
|
+
if (await isSandboxRunning(taskArgs)) {
|
|
1014
|
+
// List accounts from the sandbox
|
|
1015
|
+
const containerName = getDockerContainerName(taskArgs);
|
|
1016
|
+
const cmd = `docker exec ${containerName} octez-client list known addresses`;
|
|
1017
|
+
const { stdout } = await runCommand(cmd);
|
|
1018
|
+
|
|
1019
|
+
if (!stdout.trim()) {
|
|
1020
|
+
await sendAsyncRes('No accounts found.');
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const accounts = stdout
|
|
1025
|
+
.trim()
|
|
1026
|
+
.split('\n')
|
|
1027
|
+
.filter(line => line.trim() !== '')
|
|
1028
|
+
.map(line => {
|
|
1029
|
+
const [name, rest] = line.split(':');
|
|
1030
|
+
const address = rest ? rest.trim().split(' ')[0] : '';
|
|
1031
|
+
return { name: name.trim(), address };
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
await sendAsyncJsonRes(accounts);
|
|
1035
|
+
} else {
|
|
1036
|
+
await sendAsyncErr(`Sandbox is not running. Please start the sandbox before attempting to list accounts.`);
|
|
1037
|
+
}
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
await sendAsyncErr(getErrorMessage(`Failed to list accounts`, error));
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/**
|
|
1044
|
+
* Bakes a block in the sandbox.
|
|
1045
|
+
*/
|
|
1046
|
+
async function bakeBlock(taskArgs: Opts): Promise<void> {
|
|
1047
|
+
try {
|
|
1048
|
+
if (await isSandboxRunning(taskArgs)) {
|
|
1049
|
+
const containerName = getDockerContainerName(taskArgs);
|
|
1050
|
+
const cmd = `docker exec ${containerName} octez-client bake for baker1`;
|
|
1051
|
+
|
|
1052
|
+
if (taskArgs.watch) {
|
|
1053
|
+
console.log('Baking on demand as operations are injected.');
|
|
1054
|
+
console.log('Press CTRL-C to stop and exit.');
|
|
1055
|
+
console.log();
|
|
1056
|
+
|
|
1057
|
+
while (true) {
|
|
1058
|
+
console.log('Waiting for operations to be injected...');
|
|
1059
|
+
while (true) {
|
|
1060
|
+
const { stdout } = await runCommand(
|
|
1061
|
+
`docker exec ${containerName} octez-client rpc get /chains/main/mempool/pending_operations`,
|
|
1062
|
+
);
|
|
1063
|
+
const ops = JSON.parse(stdout);
|
|
1064
|
+
if (Array.isArray(ops.applied) && ops.applied.length > 0) break;
|
|
1065
|
+
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for 1 second before checking again
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
await runCommand(cmd);
|
|
1069
|
+
console.log('Block baked.');
|
|
1070
|
+
}
|
|
1071
|
+
} else {
|
|
1072
|
+
await runCommand(cmd);
|
|
1073
|
+
await sendAsyncRes('Block baked successfully.');
|
|
1074
|
+
}
|
|
1075
|
+
} else {
|
|
1076
|
+
try {
|
|
1077
|
+
await sendAsyncErr('Sandbox is not running. Please start the sandbox before attempting to bake a block.');
|
|
1078
|
+
} catch {
|
|
1079
|
+
// Nothing to see here.
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
await sendAsyncErr(getErrorMessage(`Failed to bake block`, error));
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* Main proxy function to handle tasks.
|
|
1089
|
+
*/
|
|
1090
|
+
export const proxy = async (taskArgs: Opts): Promise<void> => {
|
|
1091
|
+
if (!isTezBoxEnvironment(taskArgs)) {
|
|
1092
|
+
await sendAsyncErr(
|
|
1093
|
+
`This configuration doesn't appear to be configured to use TezBox environments. Check the ${taskArgs.env} environment in your .taq/config.json.`,
|
|
1094
|
+
);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
const taskName = taskArgs.task?.toLowerCase().trim();
|
|
1099
|
+
|
|
1100
|
+
const taskHandlers: Record<string, (args: Opts) => Promise<void>> = {
|
|
1101
|
+
'start sandbox': startSandbox,
|
|
1102
|
+
'stop sandbox': stopSandbox,
|
|
1103
|
+
'restart sandbox': restartSandbox,
|
|
1104
|
+
'list protocols': listProtocols,
|
|
1105
|
+
'list-protocols': listProtocols,
|
|
1106
|
+
'show protocols': listProtocols,
|
|
1107
|
+
'show-protocols': listProtocols,
|
|
1108
|
+
'list accounts': listAccounts,
|
|
1109
|
+
'list-accounts': listAccounts,
|
|
1110
|
+
'show accounts': listAccounts,
|
|
1111
|
+
'show-accounts': listAccounts,
|
|
1112
|
+
'bake': bakeBlock,
|
|
1113
|
+
'bake block': bakeBlock,
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
const handler = taskName ? taskHandlers[taskName] : undefined;
|
|
1117
|
+
|
|
1118
|
+
if (handler) {
|
|
1119
|
+
try {
|
|
1120
|
+
await handler(taskArgs);
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
await sendAsyncErr(getErrorMessage(`Error executing task '${taskArgs.task}'`, error));
|
|
1123
|
+
}
|
|
1124
|
+
} else {
|
|
1125
|
+
await sendAsyncErr(taskArgs.task ? `Unknown task: ${taskArgs.task}` : 'No task provided');
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
|
|
1129
|
+
export default proxy;
|