Browse Source

Added deploy script

master
Tal Kol 2 years ago
parent
commit
f6ca83c40d
  1. 12
      README.md
  2. 3
      build/.gitignore
  3. 227
      build/build.ts
  4. 110
      build/deploy.ts
  5. 10
      build/main.deploy.ts
  6. 37
      package-lock.json
  7. 4
      package.json

12
README.md

@ -14,7 +14,6 @@ This project is part of a set of 3 typical repositories needed for a blockchain
* `contracts/*.fc` - Smart contracts for TON blockchain written in [FunC](https://ton.org/docs/#/func) language * `contracts/*.fc` - Smart contracts for TON blockchain written in [FunC](https://ton.org/docs/#/func) language
* `build/build.ts` - Build script to compile the FunC code to [Fift](https://ton-blockchain.github.io/docs/fiftbase.pdf) * `build/build.ts` - Build script to compile the FunC code to [Fift](https://ton-blockchain.github.io/docs/fiftbase.pdf)
* `build/*.fif` - Output Fift files for every contract that was compiled, not uploaded to git
* `build/deploy.ts` - Deploy script to deploy the compiled code to TON mainnet * `build/deploy.ts` - Deploy script to deploy the compiled code to TON mainnet
* `test/*.spec.ts` - Test suite for the contracts running on [Mocha](https://mochajs.org/) test runner * `test/*.spec.ts` - Test suite for the contracts running on [Mocha](https://mochajs.org/) test runner
@ -65,11 +64,18 @@ To setup your machine for development, please make sure you have the following:
* Build * Build
* In the root repo dir, run in terminal `npm run build` * In the root repo dir, run in terminal `npm run build`
* Compilation errors will appear on screen * Compilation errors will appear on screen
* Resulting build artifacts include:
* `mycontract.merged.fc` - merged and flattened FunC source code with all imports
* `mycontract.fif` - Fift file result of compilation (not very useful by itself)
* `mycontract.cell` - the binary code cell of the compiled contract (for deployment)
* Test * Test
* In the root repo dir, run in terminal `npm run test` * In the root repo dir, run in terminal `npm run test`
* Make sure to build before running tests * Don't forget to build (or rebuild) before running tests
* Tests are running inside Node.js by running TVM in web-assembly using `ton-contract-executor`
* Deploy * Deploy
* In the root repo dir, run in terminal `npm run deploy` * In the root repo dir, run in terminal `npm run deploy`
* Follow the on-screen instructions to deploy to mainnet * Follow the on-screen instructions to deploy to mainnet
* Each contract to deploy should have a script `build/mycontract.deploy.ts` to return its init data cell
* The deployment wallet is configured in `build/deploy.config.json` (file will be created if not found)

3
build/.gitignore vendored

@ -1,3 +1,4 @@
*.fif *.fif
*.fc *.fc
*.cell *.cell
deploy.config.json

227
build/build.ts

@ -1,5 +1,5 @@
// This is a simple generic build script in TypeScript that should work for most projects without modification // This is a simple generic build script in TypeScript that should work for most projects without modification
// It assumes that it is running from the repo root, and the directories are organized this way: // The script assumes that it is running from the repo root, and the directories are organized this way:
// ./build/ - directory for build artifacts exists // ./build/ - directory for build artifacts exists
// ./contracts/*.fc - root contracts that are deployed separately are here // ./contracts/*.fc - root contracts that are deployed separately are here
// ./contracts/imports/*.fc - shared utility code that should be imported as compilation dependency is here // ./contracts/imports/*.fc - shared utility code that should be imported as compilation dependency is here
@ -12,129 +12,134 @@ import process from "process";
import child_process from "child_process"; import child_process from "child_process";
import glob from "fast-glob"; import glob from "fast-glob";
console.log(`=================================================================`); async function main() {
console.log(`Build script running, let's find some FunC contracts to compile..`); console.log(`=================================================================`);
console.log(`Build script running, let's find some FunC contracts to compile..`);
// make sure func compiler is available
let funcVersion = "";
try {
funcVersion = child_process.execSync("func -V").toString();
} catch (e) {}
if (!funcVersion.includes(`Func build information`)) {
console.log(`\nFATAL ERROR: 'func' executable is not found, is it installed and in path?`);
process.exit(1);
}
// make sure fift cli is available
let fiftVersion = "";
try {
fiftVersion = child_process.execSync("fift -V").toString();
} catch (e) {}
if (!fiftVersion.includes(`Fift build information`)) {
console.log(`\nFATAL ERROR: 'fift' executable is not found, is it installed and in path?`);
process.exit(1);
}
const rootContracts = glob.sync(["contracts/*.fc", "contracts/*.func"]); // make sure func compiler is available
for (const rootContract of rootContracts) { let funcVersion = "";
// compile a new root contract try {
console.log(`\n* Found root contract '${rootContract}' - let's compile it:`); funcVersion = child_process.execSync("func -V").toString();
const contractName = path.parse(rootContract).name; } catch (e) {}
if (!funcVersion.includes(`Func build information`)) {
// delete existing build artifacts console.log(`\nFATAL ERROR: 'func' executable is not found, is it installed and in path?`);
const fiftArtifact = `build/${contractName}.fif`; process.exit(1);
if (fs.existsSync(fiftArtifact)) {
console.log(` - Deleting old build artifact '${fiftArtifact}'`);
fs.unlinkSync(fiftArtifact);
}
const mergedFuncArtifact = `build/${contractName}.merged.fc`;
if (fs.existsSync(mergedFuncArtifact)) {
console.log(` - Deleting old build artifact '${mergedFuncArtifact}'`);
fs.unlinkSync(mergedFuncArtifact);
}
const fiftCellArtifact = `build/${contractName}.cell.fif`;
if (fs.existsSync(fiftCellArtifact)) {
console.log(` - Deleting old build artifact '${fiftCellArtifact}'`);
fs.unlinkSync(fiftCellArtifact);
} }
const cellArtifact = `build/${contractName}.cell`;
if (fs.existsSync(cellArtifact)) { // make sure fift cli is available
console.log(` - Deleting old build artifact '${cellArtifact}'`); let fiftVersion = "";
fs.unlinkSync(cellArtifact); try {
fiftVersion = child_process.execSync("fift -V").toString();
} catch (e) {}
if (!fiftVersion.includes(`Fift build information`)) {
console.log(`\nFATAL ERROR: 'fift' executable is not found, is it installed and in path?`);
process.exit(1);
} }
// check if we have a tlb file // go over all the root contracts in the contracts directory
const tlbFile = `contracts/${contractName}.tlb`; const rootContracts = glob.sync(["contracts/*.fc", "contracts/*.func"]);
if (fs.existsSync(tlbFile)) { for (const rootContract of rootContracts) {
console.log(` - TL-B file '${tlbFile}' found, calculating crc32 on all ops..`); // compile a new root contract
const tlbContent = fs.readFileSync(tlbFile).toString(); console.log(`\n* Found root contract '${rootContract}' - let's compile it:`);
const tlbOpMessages = tlbContent.match(/^(\w+).*=\s*InternalMsgBody$/gm) ?? []; const contractName = path.parse(rootContract).name;
for (const tlbOpMessage of tlbOpMessages) {
const crc = crc32(tlbOpMessage); // delete existing build artifacts
const asQuery = `0x${(crc & 0x7fffffff).toString(16)}`; const fiftArtifact = `build/${contractName}.fif`;
const asResponse = `0x${((crc | 0x80000000) >>> 0).toString(16)}`; if (fs.existsSync(fiftArtifact)) {
console.log(` op '${tlbOpMessage.split(" ")[0]}': '${asQuery}' as query (&0x7fffffff), '${asResponse}' as response (|0x80000000)`); console.log(` - Deleting old build artifact '${fiftArtifact}'`);
fs.unlinkSync(fiftArtifact);
}
const mergedFuncArtifact = `build/${contractName}.merged.fc`;
if (fs.existsSync(mergedFuncArtifact)) {
console.log(` - Deleting old build artifact '${mergedFuncArtifact}'`);
fs.unlinkSync(mergedFuncArtifact);
}
const fiftCellArtifact = `build/${contractName}.cell.fif`;
if (fs.existsSync(fiftCellArtifact)) {
console.log(` - Deleting old build artifact '${fiftCellArtifact}'`);
fs.unlinkSync(fiftCellArtifact);
}
const cellArtifact = `build/${contractName}.cell`;
if (fs.existsSync(cellArtifact)) {
console.log(` - Deleting old build artifact '${cellArtifact}'`);
fs.unlinkSync(cellArtifact);
} }
} else {
console.log(` - Warning: TL-B file for contract '${tlbFile}' not found, are your op consts according to standard?`);
}
// create a merged fc file with source code from all dependencies // check if we have a tlb file
let sourceToCompile = ""; const tlbFile = `contracts/${contractName}.tlb`;
const importFiles = glob.sync([`contracts/imports/*.fc`, `contracts/imports/*.func`, `contracts/imports/${contractName}/*.fc`, `contracts/imports/${contractName}/*.func`]); if (fs.existsSync(tlbFile)) {
for (const importFile of importFiles) { console.log(` - TL-B file '${tlbFile}' found, calculating crc32 on all ops..`);
console.log(` - Adding import '${importFile}'`); const tlbContent = fs.readFileSync(tlbFile).toString();
sourceToCompile += `${fs.readFileSync(importFile).toString()}\n`; const tlbOpMessages = tlbContent.match(/^(\w+).*=\s*InternalMsgBody$/gm) ?? [];
} for (const tlbOpMessage of tlbOpMessages) {
console.log(` - Adding the contract itself '${rootContract}'`); const crc = crc32(tlbOpMessage);
sourceToCompile += `${fs.readFileSync(rootContract).toString()}\n`; const asQuery = `0x${(crc & 0x7fffffff).toString(16)}`;
fs.writeFileSync(mergedFuncArtifact, sourceToCompile); const asResponse = `0x${((crc | 0x80000000) >>> 0).toString(16)}`;
console.log(` - Build artifact created '${mergedFuncArtifact}'`); console.log(` op '${tlbOpMessage.split(" ")[0]}': '${asQuery}' as query (&0x7fffffff), '${asResponse}' as response (|0x80000000)`);
}
// run the func compiler to create a fif file } else {
console.log(` - Trying to compile '${mergedFuncArtifact}' with 'func' compiler..`); console.log(` - Warning: TL-B file for contract '${tlbFile}' not found, are your op consts according to standard?`);
const buildErrors = child_process.execSync(`func -APS -o build/${contractName}.fif ${mergedFuncArtifact} 2>&1 >&-`).toString(); }
if (buildErrors.length > 0) {
console.log(` - OH NO! Compilation Errors! The compiler output was:`);
console.log(`\n${buildErrors}`);
process.exit(1);
} else {
console.log(` - Compilation successful!`);
}
// make sure fif build artifact was created // create a merged fc file with source code from all dependencies
if (!fs.existsSync(fiftArtifact)) { let sourceToCompile = "";
console.log(` - For some reason '${fiftArtifact}' was not created!`); const importFiles = glob.sync([`contracts/imports/*.fc`, `contracts/imports/*.func`, `contracts/imports/${contractName}/*.fc`, `contracts/imports/${contractName}/*.func`]);
process.exit(1); for (const importFile of importFiles) {
} else { console.log(` - Adding import '${importFile}'`);
console.log(` - Build artifact created '${fiftArtifact}'`); sourceToCompile += `${fs.readFileSync(importFile).toString()}\n`;
} }
console.log(` - Adding the contract itself '${rootContract}'`);
sourceToCompile += `${fs.readFileSync(rootContract).toString()}\n`;
fs.writeFileSync(mergedFuncArtifact, sourceToCompile);
console.log(` - Build artifact created '${mergedFuncArtifact}'`);
// run the func compiler to create a fif file
console.log(` - Trying to compile '${mergedFuncArtifact}' with 'func' compiler..`);
const buildErrors = child_process.execSync(`func -APS -o build/${contractName}.fif ${mergedFuncArtifact} 2>&1 >&-`).toString();
if (buildErrors.length > 0) {
console.log(` - OH NO! Compilation Errors! The compiler output was:`);
console.log(`\n${buildErrors}`);
process.exit(1);
} else {
console.log(` - Compilation successful!`);
}
// create a temp cell.fif that will generate the cell // make sure fif build artifact was created
let fiftCellSource = `"Asm.fif" include\n`; if (!fs.existsSync(fiftArtifact)) {
fiftCellSource += `${fs.readFileSync(fiftArtifact).toString()}\n`; console.log(` - For some reason '${fiftArtifact}' was not created!`);
fiftCellSource += `boc>B "${cellArtifact}" B>file`; process.exit(1);
fs.writeFileSync(fiftCellArtifact, fiftCellSource); } else {
console.log(` - Build artifact created '${fiftArtifact}'`);
}
// run fift cli to create the cell // create a temp cell.fif that will generate the cell
try { let fiftCellSource = `"Asm.fif" include\n`;
child_process.execSync(`fift ${fiftCellArtifact}`); fiftCellSource += `${fs.readFileSync(fiftArtifact).toString()}\n`;
} catch (e) { fiftCellSource += `boc>B "${cellArtifact}" B>file`;
console.log(`FATAL ERROR: 'fift' executable failed, is FIFTPATH env variable defined?`); fs.writeFileSync(fiftCellArtifact, fiftCellSource);
process.exit(1);
} // run fift cli to create the cell
try {
child_process.execSync(`fift ${fiftCellArtifact}`);
} catch (e) {
console.log(`FATAL ERROR: 'fift' executable failed, is FIFTPATH env variable defined?`);
process.exit(1);
}
// make sure cell build artifact was created // make sure cell build artifact was created
if (!fs.existsSync(cellArtifact)) { if (!fs.existsSync(cellArtifact)) {
console.log(` - For some reason '${cellArtifact}' was not created!`); console.log(` - For some reason '${cellArtifact}' was not created!`);
process.exit(1); process.exit(1);
} else { } else {
console.log(` - Build artifact created '${cellArtifact}'`); console.log(` - Build artifact created '${cellArtifact}'`);
fs.unlinkSync(fiftCellArtifact); fs.unlinkSync(fiftCellArtifact);
}
} }
console.log(``);
} }
console.log(``); main();
// helpers // helpers

110
build/deploy.ts

@ -0,0 +1,110 @@
// This is a simple generic deploy script in TypeScript that should work for most projects without modification
// Every contract you want to deploy should have a mycontract.deploy.ts script that returns its init data
// The script assumes that it is running from the repo root, and the directories are organized this way:
// ./build/ - directory for build artifacts (mycontract.cell) and deploy init data scripts (mycontract.deploy.ts)
// ./build/deploy.config.json - JSON config file with secret mnemonic of deploying wallet (will be created if not found)
import fs from "fs";
import path from "path";
import glob from "fast-glob";
import { Address, Cell, CommonMessageInfo, contractAddress, fromNano, InternalMessage, SendMode, StateInit, toNano, TonClient, WalletContract, WalletV3R2Source } from "ton";
import { mnemonicNew, mnemonicToWalletKey } from "ton-crypto";
import { BN } from "bn.js";
async function main() {
console.log(`=================================================================`);
console.log(`Deploy script running, let's find some contracts to deploy..`);
// make sure we have a wallet mnemonic to deploy from (or create one if not found)
const deployConfigJson = `build/deploy.config.json`;
let deployerMnemonic;
if (!fs.existsSync(deployConfigJson)) {
console.log(`\n* Config file '${deployConfigJson}' not found, creating a new wallet for deploy..`);
deployerMnemonic = (await mnemonicNew(24)).join(" ");
const deployWalletJsonContent = { created: new Date().toISOString(), deployerMnemonic };
fs.writeFileSync(deployConfigJson, JSON.stringify(deployWalletJsonContent, null, 2));
console.log(` - Created new wallet in '${deployConfigJson}' - keep this file secret!`);
} else {
console.log(`\n* Config file '${deployConfigJson}' found and will be used for deployment!`);
const deployConfigJsonContent = require(__dirname + "/../" + deployConfigJson);
if (!deployConfigJsonContent.deployerMnemonic) {
console.log(` - ERROR: '${deployConfigJson}' does not have the key 'deployerMnemonic'`);
process.exit(1);
}
deployerMnemonic = deployConfigJsonContent.deployerMnemonic;
}
// open the wallet and make sure it has enough TON
const client = new TonClient({ endpoint: "https://toncenter.com/api/v2/jsonRPC" });
const walletKey = await mnemonicToWalletKey(deployerMnemonic.split(" "));
const walletContract = WalletContract.create(client, WalletV3R2Source.create({ publicKey: walletKey.publicKey, workchain: 0 }));
console.log(` - Wallet address used for deployment is: ${walletContract.address.toFriendly()}`);
const walletBalance = await client.getBalance(walletContract.address);
if (walletBalance.lt(toNano(1))) {
console.log(` - ERROR: Wallet has less than 1 TON for gas (${fromNano(walletBalance)} TON), please send some TON for gas first`);
process.exit(1);
}
// go over all the contracts we have deploy scripts for
const rootContracts = glob.sync(["build/*.deploy.ts"]);
for (const rootContract of rootContracts) {
// deploy a new root contract
console.log(`\n* Found root contract to deploy '${rootContract}':`);
const contractName = path.parse(path.parse(rootContract).name).name;
// prepare the init data cell
const deployInit = require(__dirname + "/../" + rootContract);
if (typeof deployInit.initData !== "function") {
console.log(` - ERROR: '${rootContract}' does not have 'initData()' function`);
process.exit(1);
}
const initDataCell = deployInit.initData() as Cell;
// prepare the init code cell
const cellArtifact = `build/${contractName}.cell`;
if (!fs.existsSync(cellArtifact)) {
console.log(` - ERROR: '${cellArtifact}' not found, did you build?`);
process.exit(1);
}
const initCodeCell = Cell.fromBoc(fs.readFileSync(cellArtifact))[0];
// deploy by sending an internal message to the deploying wallet
sleep(1000); // to make sure we don't fail due to throttling
const newContractAddress = contractAddress({ workchain: 0, initialData: initDataCell, initialCode: initCodeCell });
console.log(` - About to deploy contract to new address: ${newContractAddress.toFriendly()}`);
const seqno = await getSeqNo(client, walletContract.address);
sleep(1000); // to make sure we don't fail due to throttling
const transfer = await walletContract.createTransfer({
secretKey: walletKey.secretKey,
seqno: seqno,
sendMode: SendMode.PAY_GAS_SEPARATLY + SendMode.IGNORE_ERRORS,
order: new InternalMessage({
to: newContractAddress,
value: new BN(0.5),
bounce: false,
body: new CommonMessageInfo({ stateInit: new StateInit({ data: initDataCell, code: initCodeCell }) }),
}),
});
await client.sendExternalMessage(walletContract, transfer);
console.log(` - Contract deployed successfully!`);
}
console.log(``);
}
main();
// helpers
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function getSeqNo(client: TonClient, walletAddress: Address) {
if (await client.isContractDeployed(walletAddress)) {
let res = await client.callGetMethod(walletAddress, "seqno");
return parseInt(res.stack[0][1], 16);
} else {
return 0;
}
}

10
build/main.deploy.ts

@ -0,0 +1,10 @@
import * as main from "../contracts/main";
import { Address } from "ton";
// return the init Cell of the contract storage (according to load_data() contract method)
export function initData() {
return main.data({
ownerAddress: Address.parseFriendly("EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N").address,
counter: 0,
});
}

37
package-lock.json generated

@ -20,6 +20,7 @@
"prettier": "^2.6.2", "prettier": "^2.6.2",
"ton": "^9.6.3", "ton": "^9.6.3",
"ton-contract-executor": "^0.4.8", "ton-contract-executor": "^0.4.8",
"ton-crypto": "^3.1.0",
"ts-node": "^10.4.0", "ts-node": "^10.4.0",
"typescript": "^4.5.4" "typescript": "^4.5.4"
} }
@ -1438,9 +1439,9 @@
} }
}, },
"node_modules/ton-crypto": { "node_modules/ton-crypto": {
"version": "2.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/ton-crypto/-/ton-crypto-2.1.0.tgz", "resolved": "https://registry.npmjs.org/ton-crypto/-/ton-crypto-3.1.0.tgz",
"integrity": "sha512-PZnmCOShfgq9tCRM8E7hG8nCkpkOyZvDLPXmZN92ZEBrfTT0NKKf0imndkxG5DkgWMjc6IKfgpnEaJDH9qN6ZQ==", "integrity": "sha512-OgUuGoT8UKvm5jvRd/fiCo46MCrlOMt9Nr7nPQC3vkLjKmbDk+qJ4gQsO14IZwrOm5xkhDMlF5ZTVH/kN9y0gg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"jssha": "3.2.0", "jssha": "3.2.0",
@ -1457,6 +1458,17 @@
"jssha": "3.2.0" "jssha": "3.2.0"
} }
}, },
"node_modules/ton/node_modules/ton-crypto": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ton-crypto/-/ton-crypto-2.1.0.tgz",
"integrity": "sha512-PZnmCOShfgq9tCRM8E7hG8nCkpkOyZvDLPXmZN92ZEBrfTT0NKKf0imndkxG5DkgWMjc6IKfgpnEaJDH9qN6ZQ==",
"dev": true,
"dependencies": {
"jssha": "3.2.0",
"ton-crypto-primitives": "2.0.0",
"tweetnacl": "1.0.3"
}
},
"node_modules/ts-node": { "node_modules/ts-node": {
"version": "10.7.0", "version": "10.7.0",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz",
@ -2660,6 +2672,19 @@
"teslabot": "^1.3.0", "teslabot": "^1.3.0",
"ton-crypto": "2.1.0", "ton-crypto": "2.1.0",
"tweetnacl": "1.0.3" "tweetnacl": "1.0.3"
},
"dependencies": {
"ton-crypto": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ton-crypto/-/ton-crypto-2.1.0.tgz",
"integrity": "sha512-PZnmCOShfgq9tCRM8E7hG8nCkpkOyZvDLPXmZN92ZEBrfTT0NKKf0imndkxG5DkgWMjc6IKfgpnEaJDH9qN6ZQ==",
"dev": true,
"requires": {
"jssha": "3.2.0",
"ton-crypto-primitives": "2.0.0",
"tweetnacl": "1.0.3"
}
}
} }
}, },
"ton-compiler": { "ton-compiler": {
@ -2692,9 +2717,9 @@
} }
}, },
"ton-crypto": { "ton-crypto": {
"version": "2.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/ton-crypto/-/ton-crypto-2.1.0.tgz", "resolved": "https://registry.npmjs.org/ton-crypto/-/ton-crypto-3.1.0.tgz",
"integrity": "sha512-PZnmCOShfgq9tCRM8E7hG8nCkpkOyZvDLPXmZN92ZEBrfTT0NKKf0imndkxG5DkgWMjc6IKfgpnEaJDH9qN6ZQ==", "integrity": "sha512-OgUuGoT8UKvm5jvRd/fiCo46MCrlOMt9Nr7nPQC3vkLjKmbDk+qJ4gQsO14IZwrOm5xkhDMlF5ZTVH/kN9y0gg==",
"dev": true, "dev": true,
"requires": { "requires": {
"jssha": "3.2.0", "jssha": "3.2.0",

4
package.json

@ -6,8 +6,9 @@
"author": "", "author": "",
"scripts": { "scripts": {
"prettier": "npx prettier --write '{test,contracts,build}/**/*.{ts,js,json}'", "prettier": "npx prettier --write '{test,contracts,build}/**/*.{ts,js,json}'",
"test": "mocha --exit test/**/*.spec.ts",
"build": "ts-node ./build/build.ts", "build": "ts-node ./build/build.ts",
"test": "mocha --exit test/**/*.spec.ts" "deploy": "ts-node ./build/deploy.ts"
}, },
"devDependencies": { "devDependencies": {
"@types/bn.js": "^5.1.0", "@types/bn.js": "^5.1.0",
@ -21,6 +22,7 @@
"prettier": "^2.6.2", "prettier": "^2.6.2",
"ton": "^9.6.3", "ton": "^9.6.3",
"ton-contract-executor": "^0.4.8", "ton-contract-executor": "^0.4.8",
"ton-crypto": "^3.1.0",
"ts-node": "^10.4.0", "ts-node": "^10.4.0",
"typescript": "^4.5.4" "typescript": "^4.5.4"
}, },

Loading…
Cancel
Save