├── src
├── export.ts
├── app
│ ├── types.ts
│ └── App.ts
├── cli.ts
├── utils
│ ├── $fmt.ts
│ ├── $path.ts
│ ├── $secret.ts
│ ├── $console.ts
│ ├── $os.ts
│ ├── Parameters.ts
│ ├── $import.ts
│ ├── $abiValues.ts
│ ├── CsvUtil.ts
│ ├── $machine.ts
│ ├── CommandUtil.ts
│ ├── $validate.ts
│ ├── $cli.ts
│ └── $abiInput.ts
├── module.js
├── commands
│ ├── list
│ │ ├── CVersion.ts
│ │ ├── CGas.ts
│ │ ├── CBlock.ts
│ │ ├── CServer.ts
│ │ ├── CRpc.ts
│ │ ├── CTools.ts
│ │ ├── CReset.ts
│ │ ├── CRestore.ts
│ │ ├── CSolidity.ts
│ │ ├── CHelp.ts
│ │ ├── CToken.ts
│ │ ├── CInfo.ts
│ │ ├── CNs.ts
│ │ ├── CConfig.ts
│ │ ├── CInstall.ts
│ │ ├── CAccount.ts
│ │ ├── CTransfer.ts
│ │ └── CTokens.ts
│ ├── ICommand.ts
│ ├── utils
│ │ └── $command.ts
│ └── CommandsHandler.ts
├── factories
│ └── ContractFactory.ts
├── services
│ ├── BaseService.ts
│ ├── InternalTokenService.ts
│ ├── BlockService.ts
│ ├── RpcService.ts
│ ├── AccountsService.ts
│ ├── ServerService.ts
│ └── ContractDumpService.ts
└── models
│ └── IPackageJson.ts
├── test
├── fixtures
│ ├── cli
│ │ └── user.json
│ ├── install
│ │ └── ContractBySource.sol
│ ├── hardhat
│ │ └── Foo.sol
│ └── contracts
│ │ └── StorageCounter.sol
├── config.js
├── gas.spec.ts
├── version.spec.ts
├── api.spec.ts
├── commands
│ ├── rpc.spec.ts
│ ├── block.spec.ts
│ ├── tokens.spec.ts
│ ├── SafeUtils.ts
│ ├── tx.spec.ts
│ ├── server.spec.ts
│ └── deploy.spec.ts
├── accounts.spec.ts
├── TestUtils.ts
├── services
│ └── contract.spec.ts
├── bootstrap.spec.ts
├── utils
│ └── input.spec.ts
└── install.spec.ts
├── images
└── background.jpg
├── index.js
├── .gitmodules
├── typings.json
├── tools
└── release.md
├── typings
├── globals
│ ├── mask
│ │ ├── index.d.ts
│ │ └── typings.json
│ ├── ruta
│ │ └── index.d.ts
│ ├── atma-utest
│ │ ├── typings.json
│ │ └── index.d.ts
│ └── assertion
│ │ ├── typings.json
│ │ └── index.d.ts
└── index.d.ts
├── index-dev.ts
├── .vscode
└── settings.json
├── .gitignore
├── tsconfig-typedoc.json
├── templates
└── hardhat.config.js
├── .npmignore
├── tsconfig-build.json
├── tsconfig.json
├── hardhat.config.js
├── .circleci
└── config.yml
├── .github
└── workflows
│ └── npm-publish.yml
└── readme.md
/src/export.ts:
--------------------------------------------------------------------------------
1 | export { App } from './app/App';
2 |
--------------------------------------------------------------------------------
/src/app/types.ts:
--------------------------------------------------------------------------------
1 | export type TAppProcessResult = {
2 | status?: 'wait'
3 | }
4 |
--------------------------------------------------------------------------------
/test/fixtures/cli/user.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "John Doe",
3 | "age": 30
4 | }
5 |
--------------------------------------------------------------------------------
/images/background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0xweb-org/0xweb/HEAD/images/background.jpg
--------------------------------------------------------------------------------
/src/cli.ts:
--------------------------------------------------------------------------------
1 | import { App } from './app/App'
2 |
3 | const app = new App();
4 | export = app;
5 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | let cli = require('./lib/cli');
4 |
5 | cli.runFromCli();
6 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "dequanto"]
2 | path = dequanto
3 | url = https://github.com/0xweb-org/dequanto
4 |
--------------------------------------------------------------------------------
/typings.json:
--------------------------------------------------------------------------------
1 | {
2 | "globalDependencies": {
3 | "assertion": "github:atmajs/assertion",
4 | "atma-utest": "github:atmajs/utest"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/tools/release.md:
--------------------------------------------------------------------------------
1 | Bump version:
2 |
3 | ```sh
4 | $version = atma bump
5 | ```
6 |
7 | Print version:
8 |
9 | ```sh
10 | echo $version
11 | ```
12 |
--------------------------------------------------------------------------------
/test/config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | suites: {
3 | node : {
4 | exec: 'node',
5 | tests: 'test/**.spec.ts'
6 | }
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/test/fixtures/install/ContractBySource.sol:
--------------------------------------------------------------------------------
1 | contract ContractBySource {
2 | function fooBySource () view external returns (uint256){
3 | return 0;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/typings/globals/mask/index.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { Mask } from 'mask';
3 |
4 | declare global {
5 | const mask: typeof Mask
6 | }
7 |
--------------------------------------------------------------------------------
/typings/globals/ruta/index.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import * as rutaLib from 'ruta';
3 |
4 | declare global {
5 | const ruta: typeof rutaLib
6 | }
7 |
--------------------------------------------------------------------------------
/typings/index.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 | ///
5 |
--------------------------------------------------------------------------------
/test/gas.spec.ts:
--------------------------------------------------------------------------------
1 | import { run } from 'shellbee'
2 |
3 | UTest({
4 | async 'check gas' () {
5 | let { stdout } = await run(`node ./index.js gas --chain polygon`);
6 | has_(stdout.join(''), /[\d\.]+\s*gwei/i);
7 | }
8 | })
9 |
--------------------------------------------------------------------------------
/src/utils/$fmt.ts:
--------------------------------------------------------------------------------
1 | import { TEth } from '@dequanto/models/TEth';
2 |
3 | export namespace $fmt {
4 | export function addressAbbr (address: TEth.Address) {
5 | return '0×' + address.slice(2, 6) + ' ••• ' + address.slice(-4);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/test/version.spec.ts:
--------------------------------------------------------------------------------
1 | import { run } from 'shellbee'
2 |
3 | UTest({
4 | async 'check version' () {
5 | let { stdout } = await run(`node ./index.js -v`);
6 | has_(stdout.join(''), /0xweb@\d{1,2}\.\d{1,2}.\d{1,2}/);
7 | }
8 | })
9 |
--------------------------------------------------------------------------------
/index-dev.ts:
--------------------------------------------------------------------------------
1 | import { App } from './src/app/App';
2 |
3 |
4 | let app = new App();
5 | let args = Array.from(process.argv);
6 | let i = args.findIndex(x => x?.includes('index-dev'));
7 |
8 | app
9 | .execute(args.slice(i + 1))
10 | .then(() => process.exit(0));
11 |
--------------------------------------------------------------------------------
/src/utils/$path.ts:
--------------------------------------------------------------------------------
1 | import { class_Uri } from 'atma-utils';
2 |
3 | export namespace $path {
4 | const __root = __dirname.replace(/[\\\/](lib|src).*$/, '');
5 | export function resolve(path: string) {
6 | return `file://` + class_Uri.combine(__root, path);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/test/fixtures/hardhat/Foo.sol:
--------------------------------------------------------------------------------
1 | contract Foo {
2 |
3 | uint256 public foo = 10;
4 |
5 | function getFoo () external view returns (uint256) {
6 | return foo;
7 | }
8 |
9 | function setFoo (uint256 foo_) external {
10 | foo = foo_;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/typings/globals/mask/typings.json:
--------------------------------------------------------------------------------
1 | {
2 | "resolution": "main",
3 | "tree": {
4 | "src": "https://raw.githubusercontent.com/atmajs/maskjs/master/typings.json",
5 | "raw": "github:atmajs/maskjs/typings.json",
6 | "main": "types/index.d.ts",
7 | "name": "mask"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/typings/globals/atma-utest/typings.json:
--------------------------------------------------------------------------------
1 | {
2 | "resolution": "main",
3 | "tree": {
4 | "src": "https://raw.githubusercontent.com/atmajs/utest/master/typings.json",
5 | "raw": "github:atmajs/utest",
6 | "main": "types/index.d.ts",
7 | "global": true,
8 | "name": "atma-utest"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/typings/globals/assertion/typings.json:
--------------------------------------------------------------------------------
1 | {
2 | "resolution": "main",
3 | "tree": {
4 | "src": "https://raw.githubusercontent.com/atmajs/assertion/master/typings.json",
5 | "raw": "github:atmajs/assertion",
6 | "main": "types/assertion.d.ts",
7 | "global": true,
8 | "name": "assertion"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "git.ignoreLimitWarning": true,
3 | "typescript.tsdk": "node_modules\\typescript\\lib",
4 | "files.exclude": {
5 | "**/.git": true,
6 | "**/.svn": true,
7 | "**/.hg": true,
8 | "**/dist": true,
9 | "**/bin": true,
10 | "**/.DS_Store": true
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | ts-temp/
4 | logs/
5 | artifacts/
6 | 0xweb/
7 | 0xc/
8 | 0xweb.json
9 | cache/
10 | bin/
11 | assets/
12 | contracts/oz/
13 | test/bin/
14 | de_**.bat
15 |
16 | # in progress
17 | src/www/
18 | index.dev.html
19 | index.html
20 |
21 | # local dev assets
22 | /dev/
23 | /tools/
24 |
25 |
26 | # html assets
27 | /viewer/
28 |
--------------------------------------------------------------------------------
/tsconfig-typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["src/**/*"],
3 | "exclude": ["node_modules", "ts-temp", "lib", "test", "typings"],
4 | "compilerOptions": {
5 | "lib": ["es2022", "dom"],
6 |
7 | "declaration": true,
8 | "target": "ES2022",
9 | "module": "commonjs",
10 | "sourceMap": false,
11 | "experimentalDecorators": true
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/utils/$secret.ts:
--------------------------------------------------------------------------------
1 | import { $machine } from './$machine';
2 | import { $cli } from './$cli';
3 |
4 | export namespace $secret {
5 | export async function get() {
6 |
7 | let pin = $cli.getParamValue('p');
8 | if (pin == null || pin.length === 0) {
9 | return null;
10 | }
11 | let id = await $machine.id();
12 | return `${id}:${pin}`;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/test/fixtures/contracts/StorageCounter.sol:
--------------------------------------------------------------------------------
1 | contract StorageCounter {
2 | uint256 count = 1;
3 |
4 | struct User {
5 | address owner;
6 | uint256 amount;
7 | }
8 | User user = User(address(this), 5);
9 |
10 | function getCountMethod () public view returns (uint256) {
11 | return count;
12 | }
13 |
14 | function updateUser (User calldata user_) external {
15 | user = user_;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/module.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | let cwd = process.cwd() + '/node_modules';
3 |
4 | /** Fix require paths, to be able to load local node_modules from global installation */
5 | if (typeof module !== 'undefined' && Array.isArray(module?.paths)) {
6 | module.paths.push(cwd);
7 | }
8 | if (typeof require !== 'undefined' && Array.isArray(require?.main?.paths)) {
9 | require.main.paths.push(cwd);
10 | }
11 | }());
12 |
13 |
14 | (function () {
15 | /**MODULE**/
16 | }());
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/commands/list/CVersion.ts:
--------------------------------------------------------------------------------
1 | import { $console } from '@core/utils/$console';
2 | import { File, env } from 'atma-io';
3 | import { ICommand } from '../ICommand';
4 |
5 | export const CVersion: ICommand = {
6 | command: '-v, --version',
7 | description: [
8 | 'Show package version'
9 | ],
10 | async process () {
11 | let path = env.applicationDir.combine(`/package.json`).toString();
12 | let json = await File.readAsync( path);
13 | $console.log(`${json.name}@${json.version}`);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/templates/hardhat.config.js:
--------------------------------------------------------------------------------
1 | require("@0xweb/hardhat");
2 |
3 | module.exports = {
4 | solidity: {
5 | compilers: [
6 | {
7 | version: "0.8.22",
8 | settings: {
9 | optimizer: {
10 | enabled: true,
11 | runs: 200
12 | }
13 | }
14 | }
15 | ]
16 | },
17 | networks: {
18 | hardhat: {
19 | chainId: 1337
20 | },
21 | localhost: {
22 | chainId: 1337
23 | }
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | assets/
2 | images/
3 | artifacts/
4 | test/
5 | bin/
6 | tools/
7 | /src/
8 | /0xc/
9 | typings/
10 | ts-temp/
11 | logs/
12 | cache/
13 | tsconfig-build.json
14 | tsconfig.json
15 | typings.json
16 | .travis.yml
17 | .gitmodules
18 | .circleci/
19 | .vscode/
20 | .github/
21 |
22 | dequanto/.vscode/
23 | dequanto/.circle/
24 | dequanto/.github/
25 | dequanto/actions/
26 | dequanto/0xc/
27 | dequanto/cache/
28 | dequanto/test/
29 | dequanto/dist/
30 | dequanto/lib/
31 | dequanto/typings/
32 | dequanto/*.json
33 | dequanto/*.js
34 | dequanto/readme.md
35 |
36 | readme.md
37 | 0xweb.json
38 | 0xweb/
39 | de_**.bat
40 | /hardhat.config.js
41 | /tsconfig-typedoc.json
42 | /index-dev.ts
43 |
--------------------------------------------------------------------------------
/src/utils/$console.ts:
--------------------------------------------------------------------------------
1 | import * as rl from 'readline';
2 | import alot from 'alot';
3 | import { $color } from '@dequanto/utils/$color';
4 | import { $logger } from '@dequanto/utils/$logger';
5 |
6 | export namespace $console {
7 |
8 | export function log(str: string | any) {
9 | $logger.log(str)
10 | }
11 | export function result(str: string | any) {
12 | $logger.result(str);
13 | }
14 | export function error(str: string | any) {
15 | $logger.error(str);
16 | }
17 |
18 | export function toast(str: string) {
19 | $logger.toast(str);
20 | }
21 |
22 | export function table(arr: (string | number | bigint)[][]) {
23 | return $logger.table(arr)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/factories/ContractFactory.ts:
--------------------------------------------------------------------------------
1 | import { IPackageItem } from '@core/models/IPackageJson';
2 | import { Web3Client } from '@dequanto/clients/Web3Client';
3 | import { ContractBase } from '@dequanto/contracts/ContractBase';
4 | import { ContractClassFactory } from '@dequanto/contracts/ContractClassFactory';
5 | import { TAbiItem } from '@dequanto/types/TAbi';
6 |
7 | export class ContractFactory {
8 | constructor (public client: Web3Client) {
9 |
10 | }
11 |
12 | async create (input: {
13 | pkg: IPackageItem,
14 | abi: TAbiItem[],
15 | }): Promise {
16 |
17 | let { pkg, abi } = input;
18 | let { contract } = ContractClassFactory.fromAbi(pkg.address, abi, this.client);
19 | return contract;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tsconfig-build.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": ["src/export.ts"],
3 | "exclude": ["node_modules", "dequanto"],
4 | "compilerOptions": {
5 | "baseUrl": "./",
6 | "paths": {
7 | "@core/*": [ "src/*" ],
8 | "@dequanto/*": [ "dequanto/src/*" ],
9 | "@dequanto-contracts/*": [ "dequanto/contracts/*" ]
10 | },
11 |
12 | "outDir": "ts-temp",
13 | "lib": ["es2020", "dom"],
14 | "target": "ES2021",
15 |
16 | "types": ["node"],
17 | "declaration": true,
18 | "sourceMap": false,
19 | "experimentalDecorators": true,
20 | "esModuleInterop": true,
21 | "allowSyntheticDefaultImports": true,
22 | "module": "AMD",
23 | "moduleResolution": "node",
24 | "useDefineForClassFields": false
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/utils/$os.ts:
--------------------------------------------------------------------------------
1 | import { $promise } from '@dequanto/utils/$promise';
2 |
3 |
4 | export namespace $os {
5 | export async function open(filePath: string) {
6 | const { spawn } = require('child_process');
7 | let command = (function () {
8 | switch (process.platform) {
9 | case 'darwin': {
10 | return 'open ' + filePath + ' && lsof -p $! +r 1 &>/dev/null';
11 | }
12 | case 'win32': {
13 | return 'start /wait ' + filePath;
14 | }
15 | default: {
16 | return 'xdg-open ' + filePath + ' && tail --pid=$! -f /dev/null';
17 | }
18 | }
19 | })();
20 |
21 | return $promise.fromCallback(spawn, command, {
22 | shell: true
23 | });
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/test/api.spec.ts:
--------------------------------------------------------------------------------
1 | import { Directory, File } from 'atma-io'
2 | import { App } from '../lib/0xweb.js'
3 |
4 | UTest({
5 | async '$before' () {
6 | await Directory.remove('/0xc/eth/WETH/');
7 | },
8 | async 'should install package via API' () {
9 | let app = new App();
10 | let result = await app.execute(['install', '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', '--name', 'WETH', '--chain', 'eth'])
11 |
12 | eq_(result.main, './0xc/eth/WETH/WETH.ts');
13 |
14 | let content = await File.readAsync(`/0xc/eth/WETH/WETH.ts`, { skipHooks: true });
15 | has_(content, 'class WETH extends ContractBase');
16 |
17 | let packagePath = '0xweb.json';
18 | let json = await File.readAsync(packagePath);
19 |
20 | has_(json.contracts.eth['0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'].main, 'WETH.ts');
21 | }
22 | })
23 |
--------------------------------------------------------------------------------
/test/commands/rpc.spec.ts:
--------------------------------------------------------------------------------
1 | import { HardhatProvider } from '@dequanto/hardhat/HardhatProvider';
2 | import { TestNode } from '../../dequanto/test/hardhat/TestNode';
3 | import { TestUtils } from '../TestUtils';
4 |
5 |
6 |
7 | let provider = new HardhatProvider();
8 | let client = provider.client('localhost');
9 | let owner1 = provider.deployer(0);
10 | let owner2 = provider.deployer(1);
11 |
12 | UTest({
13 | $config: {
14 | timeout: 2 * 60 * 1000
15 | },
16 |
17 | async '$before'() {
18 | await TestUtils.clean();
19 | await TestNode.start();
20 | },
21 | async 'should call simple rpc method' () {
22 |
23 | let result = await TestUtils.cli('rpc eth_chainId --chain hardhat --silent');
24 | let chainId = Number(result);
25 | eq_(chainId, 1337);
26 |
27 | let resultApi = await TestUtils.api(`/api/rpc/eth_chainId?chain=hardhat`);
28 | eq_(resultApi, chainId);
29 | },
30 |
31 | })
32 |
--------------------------------------------------------------------------------
/src/commands/list/CGas.ts:
--------------------------------------------------------------------------------
1 | import { $console } from '@core/utils/$console';
2 | import { $validate } from '@core/utils/$validate';
3 | import { $bigint } from '@dequanto/utils/$bigint';
4 | import { ICommand } from '../ICommand';
5 |
6 | export function CGas() {
7 | return {
8 | command: 'gas',
9 | description: [
10 | 'Print current GAS price for a chain'
11 | ],
12 | params: {
13 | '-c, --chain': {
14 | description: `Default: eth. Available: ${$validate.platforms().join(', ')}`
15 | }
16 | },
17 | async process(args, params, app) {
18 | let gasData = await app.chain.client.getGasPrice();
19 | let gwei = $bigint.toGweiFromWei(gasData.price);
20 | $console.table([
21 | ['Chain', app.chain.client.platform],
22 | ['Price', gwei.toString() + ' gwei']
23 | ]);
24 | }
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/test/commands/block.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestNode } from '../../dequanto/test/hardhat/TestNode';
2 | import { HardhatProvider } from '@dequanto/hardhat/HardhatProvider';
3 | import { TestUtils } from '../TestUtils';
4 |
5 | let hh = new HardhatProvider();
6 |
7 | UTest({
8 | $config: {
9 | timeout: 2 * 60 * 1000
10 | },
11 | async '$before'() {
12 | await TestNode.start();
13 | },
14 | async 'check block' () {
15 | let client = hh.client('localhost');
16 | let blockNrBefore = await client.getBlockNumber();
17 | await TestUtils.cli('hardhat mine 1');
18 | let blockNrAfter = await client.getBlockNumber();
19 | eq_(blockNrAfter - blockNrBefore, 1);
20 |
21 | let blockData = await client.getBlock(blockNrAfter);
22 |
23 | let blockCliResp = await TestUtils.cli('block get latest');
24 | has_(blockCliResp, blockData.hash);
25 |
26 | let blockApiResp = await TestUtils.api(`/api/block/get/latest?chain=hardhat`);
27 | eq_(blockApiResp.hash, blockData.hash);
28 | }
29 |
30 | });
31 |
--------------------------------------------------------------------------------
/src/services/BaseService.ts:
--------------------------------------------------------------------------------
1 | import { App } from '@core/app/App';
2 | import { $logger } from '@dequanto/utils/$logger';
3 |
4 | export class BaseService {
5 | public opts: { logger?: boolean } = {};
6 |
7 | constructor(public app: App) {
8 | this.opts.logger = app.config?.env !== 'api';
9 | }
10 |
11 |
12 | protected printLog (...args) {
13 | if (this.opts?.logger === false) {
14 | return;
15 | }
16 | $logger.log(...args);
17 | }
18 | /** @deprecated printLog instead */
19 | protected printResult (...args) {
20 | if (this.opts?.logger === false) {
21 | return;
22 | }
23 | $logger.result(...args);
24 | }
25 | protected printLogTable (...args: Parameters) {
26 | if (this.opts?.logger === false) {
27 | return;
28 | }
29 | $logger.table(...args);
30 | }
31 | protected printLogToast (str: string) {
32 | if (this.opts?.logger === false) {
33 | return;
34 | }
35 | $logger.toast(str);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "exclude": ["node_modules", "ts-temp"],
3 | "include": ["./**/*.ts", "./**/*.mask"],
4 | "compilerOptions": {
5 | "jsx": "preserve",
6 | "baseUrl": "./",
7 | "paths": {
8 | "@core/*": [ "src/*" ],
9 | "@dequanto/*": [ "dequanto/src/*" ],
10 | "@dequanto-contracts/*": [ "dequanto/src/prebuilt/*" ]
11 | },
12 | "outDir": "ts-temp",
13 | "lib": ["es2024", "DOM"],
14 | "typeRoots": [
15 | "node_modules/@types",
16 | "typings/globals",
17 | "typings-other"
18 | ],
19 | "types": ["node", "assertion", "atma-utest"],
20 |
21 | "target": "es2024",
22 | "module": "commonjs",
23 | "sourceMap": false,
24 | "experimentalDecorators": true,
25 | "esModuleInterop": true,
26 | "allowSyntheticDefaultImports": true,
27 | "useDefineForClassFields": false,
28 | "plugins": [
29 | {
30 | "name": "@astrojs/ts-plugin"
31 | }
32 | ]
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/utils/Parameters.ts:
--------------------------------------------------------------------------------
1 | import { $validate } from './$validate';
2 |
3 | export const Parameters = {
4 | account (opts?: { required?: boolean }) {
5 | return {
6 | '-a, --account': {
7 | description: 'Account name. Accounts should be unlocked with gray<-p, --pin> parameter',
8 | required: opts?.required ?? true,
9 | fallback: 'session-account',
10 | }
11 | }
12 | },
13 | pin (opts?: { required?: boolean }) {
14 | return {
15 | '-p, --pin': {
16 | description: 'Accounts storage is encrypted with a derived key from the pin and the local machine key. ',
17 | required: opts?.required ?? true
18 | },
19 | }
20 | },
21 | chain (opts?: { required?: boolean }) {
22 | return {
23 | '-c, --chain': {
24 | description: `Available chains: ${$validate.platforms().join(', ')}`,
25 | required: opts?.required ?? true,
26 | oneOf: $validate.platforms()
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/services/InternalTokenService.ts:
--------------------------------------------------------------------------------
1 | import memd from 'memd';
2 | import { env } from 'atma-io';
3 | import { ERC20 } from '@dequanto-contracts/openzeppelin/ERC20';
4 | import { IBlockchainExplorer } from '@dequanto/explorer/IBlockchainExplorer';
5 | import { Web3Client } from '@dequanto/clients/Web3Client';
6 | import { TAddress } from '@dequanto/models/TAddress';
7 |
8 | export class InternalTokenService {
9 |
10 | @memd.deco.memoize({
11 | trackRef: true,
12 | keyResolver (address: TAddress, client: Web3Client, explorer: IBlockchainExplorer) {
13 | return `${client.platform}:${address}`
14 | },
15 | persistence: new memd.FsTransport({
16 | path: env.appdataDir.combine('./0xc/cache/tokens.json').toString()
17 | })
18 | })
19 | async getTokenData (address: TAddress, client: Web3Client, explorer: IBlockchainExplorer) {
20 | let erc20 = new ERC20(address, client, explorer);
21 | let [ symbol, name, decimals ] = await Promise.all([
22 | erc20.symbol(),
23 | erc20.name(),
24 | erc20.decimals(),
25 | ]);
26 |
27 | return {
28 | symbol,
29 | name,
30 | decimals,
31 | address,
32 | platform: client.platform
33 | };
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/hardhat.config.js:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | require("@0xweb/hardhat");
23 |
24 | /**
25 | * @type import('hardhat/config').HardhatUserConfig
26 | */
27 | module.exports = {
28 | solidity: {
29 | compilers: [
30 | {
31 | version: "0.8.2",
32 | settings: {
33 | optimizer: {
34 | enabled: true,
35 | runs: 200,
36 | },
37 | },
38 | },
39 | {
40 | version: "0.8.12",
41 | settings: {
42 | optimizer: {
43 | enabled: true,
44 | runs: 200,
45 | },
46 | },
47 | },
48 | {
49 | version: "0.8.20",
50 | settings: {
51 | optimizer: {
52 | enabled: true,
53 | runs: 200,
54 | },
55 | },
56 | }
57 | ],
58 | },
59 | networks: {
60 | hardhat: {
61 | chainId: 1337
62 | },
63 | localhost: {
64 | chainId: 1337
65 | }
66 | }
67 | };
68 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | # See: https://circleci.com/docs/2.0/configuration-reference
2 | version: 2.1
3 |
4 | orbs:
5 | # See the orb documentation here: https://circleci.com/developer/orbs/orb/circleci/node
6 | node: circleci/node@5.1.0
7 |
8 | jobs:
9 | build-and-test:
10 | # These next lines define a Docker executor: https://circleci.com/docs/2.0/executor-types/
11 | # https://circleci.com/developer/images/image/cimg/node
12 | docker:
13 | - image: cimg/node:20.8.1
14 | steps:
15 | - checkout
16 | - run:
17 | name: Checkout submodules
18 | command: git submodule update --init --recursive
19 | - run:
20 | name: NPM Install
21 | command: npm install --force
22 | - run:
23 | name: Build Project
24 | command: npm run build
25 | - run:
26 | name: Run tests
27 | command: npm run test
28 |
29 | workflows:
30 | # Inside the workflow, you provide the jobs you want to run, e.g this workflow runs the build-and-test job above.
31 | # CircleCI will run this workflow on every commit.
32 | # https://circleci.com/docs/2.0/configuration-reference/#workflows
33 | TestRunner:
34 | jobs:
35 | - build-and-test:
36 | filters:
37 | branches:
38 | ignore:
39 | # runs the github action
40 | - release
41 |
--------------------------------------------------------------------------------
/src/utils/$import.ts:
--------------------------------------------------------------------------------
1 | import { File } from 'atma-io';
2 | import memd from 'memd';
3 | import { include } from 'includejs';
4 |
5 | declare let global;
6 |
7 | export function $import(path: string) {
8 | Initializer.init();
9 |
10 | return new Promise(resolve => {
11 | include
12 | .instance()
13 | .js(path.replace('.ts', '') + '::Module')
14 | .done(resp => {
15 | resolve(resp.Module)
16 | });
17 | });
18 | }
19 |
20 | class Initializer {
21 | @memd.deco.memoize()
22 | static init () {
23 |
24 | global.app.config['settings'] = {};
25 | global.app.config['settings']['atma-loader-ts'] = {
26 | "typescript": {
27 | "compilerOptions": {
28 | "module": "AMD",
29 | "sourceMap": false,
30 | "experimentalDecorators": true,
31 | "esModuleInterop": true,
32 | "allowSyntheticDefaultImports": true,
33 | "target": "ES2020"
34 | }
35 | }
36 | };
37 | File.Middleware.register('ts', 'read', 'atma-loader-ts');
38 |
39 | include.cfg({
40 | amd: true,
41 | extentionDefault: {
42 | js: "ts"
43 | },
44 | routes: {
45 | "@core": "src/{0}",
46 | "@dequanto": "dequanto/src/{0}",
47 | "@dequanto-contracts": "dequanto/src/prebuilt/{0}"
48 | }
49 | });
50 |
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/commands/list/CBlock.ts:
--------------------------------------------------------------------------------
1 | import { ICommand } from '../ICommand';
2 | import { type App } from '@core/app/App';
3 | import { Parameters } from '@core/utils/Parameters';
4 | import { BlockService } from '@core/services/BlockService';
5 |
6 |
7 | export function CBlock() {
8 | return {
9 | command: 'block',
10 | description: [
11 | 'Block utils'
12 | ],
13 | subcommands: [
14 | {
15 | command: 'get',
16 | description: [
17 | 'Get block info'
18 | ],
19 | arguments: [
20 | {
21 | name: 'NumberOrDate',
22 | description: `latest or or e.g. "20.01.2024 14:00:00"`,
23 | required: true
24 | }
25 | ],
26 | params: {
27 | ...Parameters.chain(),
28 | },
29 | api: {},
30 | async process(args: string[], params: any, app: App) {
31 |
32 | let service = new BlockService(app);
33 | let [ blockNr ] = args;
34 | return service.getBlock(blockNr)
35 | }
36 | },
37 | ],
38 | params: {
39 |
40 | },
41 |
42 | async process(args: string[], params, app: App) {
43 | console.warn(`A sub-command for "block" not found: ${args[0]}. Call "0xweb block ?" to view the list of commands`);
44 | }
45 | };
46 | }
47 |
--------------------------------------------------------------------------------
/typings/globals/atma-utest/index.d.ts:
--------------------------------------------------------------------------------
1 | // Generated by typings
2 | // Source: https://raw.githubusercontent.com/atmajs/utest/master/types/index.d.ts
3 | declare module "atma-utest" {
4 | export = UTest;
5 | }
6 |
7 | declare var UTest: IUtest;
8 |
9 | declare interface IUtest {
10 | (definition: IUTestDefinition): void
11 |
12 | domtest: IDomTest
13 | request (url, method, headers, data, callback)
14 | server: {
15 | render (template: string, ...args)
16 | }
17 |
18 | benchmark (model: IUTestDefinition)
19 | benchmarkVersions (model: IUTestDefinition)
20 |
21 | }
22 |
23 | interface IDomTest {
24 | (subject: HTMLElement | JQuery | any, testMarkup: string | any): PromiseLike
25 | use (astName: string): IDomTest
26 | process (subject: HTMLElement | JQuery | any, testMarkup: string | any): PromiseLike
27 | }
28 |
29 | interface IUTestDefinition {
30 | $config?: {
31 | timeout?: number
32 | errorableCallbacks?: boolean
33 | breakOnError?: boolean
34 |
35 | 'http.config'?: any
36 | 'http.eval'?: string
37 | 'http.include'?: any
38 | 'http.service'?: any
39 | 'http.process'?: any
40 | 'util.process'?: any
41 | }
42 | $before?: (done?: Function) => void | PromiseLike
43 | $after?: (done?: Function) => void | PromiseLike
44 | $teardown?: (done?: Function) => void | PromiseLike
45 |
46 |
47 |
48 | [key: string]: ITestCase | IUTestDefinition | any
49 | }
50 |
51 | interface ITestCase {
52 | (done?: Function, ...args: any[]): void | PromiseLike | any
53 | }
54 |
--------------------------------------------------------------------------------
/src/commands/list/CServer.ts:
--------------------------------------------------------------------------------
1 | import { ICommand } from '../ICommand';
2 | import { $promise } from '@dequanto/utils/$promise';
3 | import { ServerService } from '@core/services/ServerService';
4 | import type { App } from '@core/app/App';
5 | import { TAppProcessResult } from '@core/app/types';
6 |
7 |
8 | export function CServer() {
9 | return {
10 | command: 'server',
11 | description: [
12 | 'API server'
13 | ],
14 | subcommands: [
15 | {
16 | command: 'start',
17 | description: [
18 | 'Launches the API/UI Server'
19 | ],
20 | arguments: [
21 |
22 | ],
23 | params: {
24 | '--port': {
25 | description: 'Port number for the API server',
26 | type: 'number',
27 | default: 3000
28 | }
29 | },
30 | async process(args: string[], params: { port: number, dev: boolean }, app: App) {
31 |
32 | //-app.config.debug = params.dev;
33 | const service = new ServerService(app);
34 | await service.start(params);
35 | return { status: 'wait' } as TAppProcessResult
36 | }
37 | },
38 | ],
39 | params: {
40 |
41 | },
42 |
43 | async process(args: string[], params, app: App) {
44 | console.warn(`A sub-command for "server" not found: ${args[0]}. Call "0xweb server ?" to view the list of commands`);
45 | }
46 | };
47 | }
48 |
--------------------------------------------------------------------------------
/src/models/IPackageJson.ts:
--------------------------------------------------------------------------------
1 | import { TAddress } from '@dequanto/models/TAddress';
2 | import { TPlatform } from '@dequanto/models/TPlatform';
3 | import { TAbiItem } from '@dequanto/types/TAbi';
4 |
5 |
6 | export interface IPackageJson {
7 | deployments?: {
8 | [platform: string]: {
9 | name?: string
10 | // Deployments Path
11 | path: string
12 | }[]
13 | }
14 | contracts?: {
15 | [platform in TPlatform]: {
16 | [address: string]: {
17 | name: string
18 | main: string
19 | contractName?: string
20 | implementation: TAddress
21 | source?: { path: string } | {
22 | address: TAddress
23 | platform: TPlatform
24 | }
25 | }
26 | }
27 | }
28 | dependencies?: {
29 | [name: string]: {
30 | main: string
31 | contractName: string
32 | source: { path: string } | {
33 | address: TAddress
34 | platform: TPlatform
35 | },
36 | deployments: {
37 | [platform in TPlatform]: {
38 | address: TAddress
39 | implementation?: TAddress
40 | }
41 | }
42 | }
43 | }
44 | }
45 |
46 | export interface IPackageItem {
47 | platform: TPlatform
48 | address: TAddress
49 | name: string
50 | contractName: string
51 | id?: string
52 | main: string
53 | abi?: TAbiItem[]
54 | implementation?: TAddress
55 | source?: { path: string } | {
56 | address: TAddress
57 | platform: TPlatform
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/commands/list/CRpc.ts:
--------------------------------------------------------------------------------
1 | import { ICommand } from '../ICommand';
2 | import { Parameters } from '@core/utils/Parameters';
3 | import { App } from '@core/app/App';
4 | import { RpcService } from '@core/services/RpcService';
5 | import { $logger } from '@dequanto/utils/$logger';
6 |
7 | /**
8 | * Call any RPC method, e.g. https://www.quicknode.com/docs/ethereum/eth_getStorageAt
9 | */
10 | export function CRpc() {
11 | return {
12 | command: 'rpc',
13 | description: [
14 | 'Send RPC method'
15 | ],
16 | arguments: [
17 | {
18 | name: 'RpcMethodName',
19 | description: 'Method Name'
20 | },
21 | {
22 | query: true,
23 | description: 'Argument 1',
24 | required: false
25 | },
26 | {
27 | query: true,
28 | description: 'Argument 2',
29 | required: false
30 | },
31 | {
32 | query: true,
33 | description: '...',
34 | required: false
35 | }
36 | ],
37 | params: {
38 | ...Parameters.account({ required: false }),
39 | ...Parameters.chain(),
40 | '--arg0': {
41 | description: 'Api: Argument 1; Cli: simple arguments also supported',
42 | required: false
43 | },
44 | '--arg1': {
45 | description: 'Api: Argument 2; Cli: simple arguments also supported',
46 | required: false
47 | }
48 | },
49 | api: {},
50 | async process(args: any[], params?, app?: App) {
51 | let service = new RpcService(app);
52 | let result = await service.process(args, params, app);
53 | return result;
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/commands/ICommand.ts:
--------------------------------------------------------------------------------
1 | import { type App } from '@core/app/App';
2 |
3 |
4 | export interface ICommand {
5 | command: string
6 | example?: string
7 | description: string[]
8 | arguments?: {
9 | description: string
10 | name?: string
11 | type?: 'number' | 'string' | 'boolean'
12 | required?: boolean
13 | // If true, the argument is optional and will be treated as a query parameter in the API request
14 | query?: boolean
15 | }[]
16 | params?: {
17 | [definition: string]: {
18 | // will be parsed from definition (the last one)
19 | key?: string
20 | description: string
21 | required?: boolean
22 | type?: 'number' | 'string' | 'boolean' | 'address',
23 | map?: (input: string) => T
24 |
25 | oneOf?: (string | number)[]
26 | validate?: (any) => any
27 | default?: any
28 |
29 | // alias
30 | fallback?: string
31 | }
32 | }
33 | subcommands?: ICommand[]
34 | process: (args: any[], params?, app?: App, command?: ICommand) => Promise
35 |
36 | api?: {
37 | method?: 'get' | 'post'
38 | process?: (args: any[], params?, app?: App, command?: ICommand) => Promise
39 | }
40 | }
41 |
42 | export abstract class Command implements ICommand {
43 | command: string;
44 | description: string[];
45 | arguments?: { description: string; type?: 'string' | 'number' | 'boolean'; required?: boolean; }[];
46 | params?: {
47 | [definition: string]: {
48 | // will be parsed from definition (the last one)
49 | key?: string; description: string; required?: boolean; type?: 'string' | 'number' | 'boolean';
50 | };
51 | };
52 | subcommands?: ICommand[];
53 | process: (args: any[], params?: any, app?: App) => Promise;
54 | }
55 |
--------------------------------------------------------------------------------
/src/commands/list/CTools.ts:
--------------------------------------------------------------------------------
1 | import { ICommand } from '../ICommand';
2 | import { App } from '@core/app/App';
3 | import { $console } from '@core/utils/$console';
4 | import { $sig } from '@dequanto/utils/$sig';
5 | import { $is } from '@dequanto/utils/$is';
6 |
7 | export function CTools () {
8 | return {
9 | command: 'tool',
10 |
11 | description: [
12 | 'Utility tools.'
13 | ],
14 | subcommands: [
15 | {
16 | command: 'key-encode',
17 | example: '0xweb tool key-encode -p ',
18 | description: [
19 | 'Encode the private key with the PIN'
20 | ],
21 | arguments: [
22 | {
23 | name: '',
24 | description: 'Private Key',
25 | required: true,
26 | }
27 | ],
28 | params: {
29 | '-p, --pin': {
30 | description: 'Any password to encrypt the private key with',
31 | required: true
32 | },
33 | },
34 | async process (args: string[], params: { pin }, app: App) {
35 | let [ privateKey ] = args;
36 | if ($is.HexBytes32(privateKey) === false) {
37 | throw new Error(`Invalid private key ${privateKey}`);
38 | }
39 | if (params.pin == null || params.pin.length < 3) {
40 | throw new Error(`Invalid private pin ${params.pin}`);
41 | }
42 |
43 | const encryptedKey = await $sig.$key.encrypt(privateKey as any, params.pin);
44 | $console.log(encryptedKey)
45 | }
46 | },
47 | ],
48 |
49 | async process(args: string[], params, app: App) {
50 | console.warn(`Command for an "accounts" not found: ${args[0]}. Call "0xweb accounts --help" to view the list of commands`);
51 | }
52 | };
53 | };
54 |
55 |
--------------------------------------------------------------------------------
/src/services/BlockService.ts:
--------------------------------------------------------------------------------
1 | import { $require } from '@dequanto/utils/$require';
2 | import { BaseService } from './BaseService';
3 | import { TEth } from '@dequanto/models/TEth';
4 | import { $block } from '@dequanto/utils/$block';
5 | import { BlockDateResolver } from '@dequanto/blocks/BlockDateResolver';
6 | import { $date } from '@dequanto/utils/$date';
7 |
8 | export class BlockService extends BaseService {
9 |
10 | async getBlock(blockNr: string) {
11 | let { client } = this.app.chain;
12 |
13 | if (blockNr === 'latest') {
14 | this.printLogToast('Getting latest block number...');
15 | let nr = await client.getBlockNumber();
16 | blockNr = String(nr);
17 | }
18 |
19 | if (/[\.\-]\d+[\.\-]/.test(blockNr)) {
20 | // Date 'xx.01.xxxx'
21 | let date = $date.parse(blockNr);
22 | if (date == null || isNaN(date.getTime())) {
23 | throw new Error(`Could not parse date ${blockNr}`);
24 | }
25 | this.printLogToast(`Finding the block for ${date.toISOString()}`);
26 | let resolver = new BlockDateResolver(client);
27 | let nr = await resolver.getBlockNumberFor(date);
28 | blockNr = String(nr);
29 | }
30 |
31 | let nr = Number(blockNr);
32 |
33 | $require.Number(nr, 'BlockNumber is not a number');
34 | this.printLogToast(`Loading block bold<${nr}>`);
35 |
36 | let block = await client.getBlock(nr);
37 | this.printLogTable([
38 | ['Block', nr],
39 | ['Hash', block.hash],
40 | ['Parent', block.parentHash],
41 | ['Miner', block.miner],
42 | ['Time', $date.format($block.getDate(block), 'dd.MM.yyyy HH:mm:ss') + ` gray<(${ block.timestamp })>`],
43 | ['Transactions', block.transactions?.length ?? 0],
44 | ]);
45 | let hashes = block.transactions?.map((tx, i) => {
46 | return [`#${i + 1}`, tx as TEth.Hex];
47 | });
48 | this.printLogTable([
49 | ...hashes
50 | ]);
51 | return block;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/test/commands/tokens.spec.ts:
--------------------------------------------------------------------------------
1 | import { l } from '@dequanto/utils/$logger';
2 | import { File } from 'atma-io';
3 | import { run } from 'shellbee';
4 | import alot from 'alot';
5 | import { $bigint } from '@dequanto/utils/$bigint';
6 | import { $address } from '@dequanto/utils/$address';
7 |
8 | const ACCOUNTS_PATH = './test/bin/accounts.json';
9 | const CONFIG_PATH = './test/bin/config.json';
10 |
11 | const cliEx = async (command: string, params: any) => {
12 | params = {
13 | '--config-accounts': ACCOUNTS_PATH,
14 | '--config-global': CONFIG_PATH,
15 | '--pin': '12345',
16 | '--chain': 'hardhat',
17 | '--color': 'none',
18 | ...params
19 | };
20 | let paramsStr = alot.fromObject(params).map(x => `${x.key} ${x.value}`).toArray().join(' ');
21 | let cmdStr = `node ./index.js ${command} ${paramsStr}`;
22 | let { stdout } = await run({
23 | command: cmdStr,
24 | silent: true,
25 | })
26 | return stdout.join('\n');
27 | };
28 |
29 |
30 | UTest({
31 | $config: {
32 | timeout: 2 * 60 * 1000
33 | },
34 | async '$before' () {
35 | await File.removeAsync(CONFIG_PATH);
36 | },
37 | async 'add, find, remove token' () {
38 |
39 | l`Adding token to storage`;
40 | let stdAddOne = await cliEx(`tokens add`, {
41 | '--symbol': 'FRT',
42 | '--address': $address.ZERO,
43 | '--chain': 'hardhat'
44 | });
45 |
46 | let json = await File.readAsync (CONFIG_PATH);
47 | deepEq_(json.tokens, [
48 | {
49 | symbol: 'FRT',
50 | platforms: [
51 | {
52 | platform: 'hardhat',
53 | decimals: 18,
54 | address: $address.ZERO
55 | }
56 | ]
57 | }
58 | ]);
59 |
60 |
61 | l`Find token`;
62 | let stdFindOne = await cliEx(`tokens find FRT`, {
63 |
64 | });
65 | has_(stdFindOne, /Symbol \s*FRT/);
66 | has_(stdFindOne, /Address \s*0x0000000000000000000000000000000000000000/);
67 | }
68 | })
69 |
--------------------------------------------------------------------------------
/src/utils/$abiValues.ts:
--------------------------------------------------------------------------------
1 | import { TAbiItem } from '@dequanto/types/TAbi'
2 |
3 | export namespace $abiValues {
4 |
5 | type TLogParsed = {
6 | name: string
7 | arguments: {
8 | name: string
9 | value: any
10 | }[]
11 | event: {
12 | transactionHash: string
13 | blockHash: string
14 | blockNumber: number
15 | address: string
16 | data: string
17 | topics: string[]
18 | }
19 | }
20 |
21 | export function serializeLog (log: TLogParsed) {
22 | if (log.name && log.arguments) {
23 | return `${log.name}(${log.arguments.map(arg => `gray<${arg.name}=>${ serializeValue(arg.value) }`).join(', ')})`
24 | }
25 |
26 | let event = log.event;
27 | let lines = [
28 | `Tx: ${ event.transactionHash}`,
29 | `Block: ${ event.blockNumber}`,
30 | `Address: ${ event.address}`,
31 | `Topics: \n ${ event.topics.map(topic => ` ${topic}`) }`,
32 | `Data: \n ${ event.data }`
33 | ];
34 | return lines.join('\n')
35 | }
36 | export function serializeCalldata (calldata: { method: string, arguments: any[] }, abis: TAbiItem[]) {
37 | let methods = abis.filter(a => a.name === calldata.method);
38 | let method = methods.find(x =>x.inputs?.length === calldata.arguments.length);
39 | if (method == null) {
40 | return `${ calldata.method }(${ calldata.arguments.map(arg => serializeValue(arg))})`
41 | }
42 |
43 | let name = method.name;
44 | let args = calldata.arguments.map((arg, i) => `gray<${method.inputs[i].name}=>${serializeValue(arg)}`);
45 | return `${ name }(${ args.join(', ')})`;
46 | }
47 |
48 | function serializeValue (value: any) {
49 | if (value == null) {
50 | return 'NULL';
51 | }
52 | if (typeof value !== 'object') {
53 | return value;
54 | }
55 | // if (Array.isArray(value)) {
56 | // return `[ ${value.map(v => serializeValue(v)).join(', ')} ]`;
57 | // }
58 | return JSON.stringify(value);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/test/commands/SafeUtils.ts:
--------------------------------------------------------------------------------
1 | import { HardhatProvider } from '@dequanto/hardhat/HardhatProvider';
2 | import { TAddress } from '@dequanto/models/TAddress';
3 | import { $address } from '@dequanto/utils/$address';
4 | import { File } from 'atma-io';
5 | import memd from 'memd';
6 | import { TestUtils } from '../TestUtils';
7 |
8 | export class SafeUtils {
9 |
10 | static async create(owner: TAddress, name: string) {
11 | let path = await SafeUtils.prepare();
12 | let stdCreateSafe = await TestUtils.cli(`safe new`, {
13 | '--owner': owner,
14 | '--name': name,
15 | '--contracts': path
16 | });
17 |
18 | let match = /safe\/test\s+\[(?[^\]]+)\]/.exec(stdCreateSafe);
19 | let safeAddress = match?.groups?.address;
20 | eq_($address.isValid(safeAddress), true, 'Safe address not found');
21 | return safeAddress as TAddress;
22 | }
23 |
24 | @memd.deco.memoize()
25 | static async prepare () {
26 | let path = './test/bin/contracts.json';
27 | if (await File.existsAsync(path)) {
28 | await File.removeAsync(path);
29 | }
30 | let provider = new HardhatProvider();
31 | let client = provider.client('localhost');
32 |
33 | const { contract: proxyFactoryContract, abi: proxyFactoryAbi } = await provider.deploySol('/dequanto/test/fixtures/gnosis/proxies/GnosisSafeProxyFactory.sol', {
34 | client
35 | });
36 | const { contract: safeContract, abi: safeAbi } = await provider.deploySol('/dequanto/test/fixtures/gnosis/GnosisSafe.sol', {
37 | client
38 | });
39 | const { contract: multiSendContract, abi: multiSendAbi } = await provider.deploySol('/dequanto/test/fixtures/gnosis/libraries/MultiSend.sol', {
40 | client
41 | });
42 |
43 | let contracts = {
44 | Safe: safeContract.address,
45 | SafeProxyFactory: proxyFactoryContract.address,
46 | MultiSend: multiSendContract.address,
47 | };
48 |
49 | console.log(`Gnosis Safe Contracts`, contracts);
50 | await File.writeAsync(path, contracts);
51 | return path;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/utils/CsvUtil.ts:
--------------------------------------------------------------------------------
1 | export namespace CsvUtil {
2 | export function parse (str: string, delimiterChar?: string) {
3 | let arr = []
4 |
5 | let valueStart = 0;
6 | let quote = 34; //'"';
7 | let delimiter = delimiterChar?.charCodeAt(0) ?? 44; //',';
8 | let row = [];
9 | let state = State.row;
10 | for (let i = 0; i < str.length; i++) {
11 | let c = str.charCodeAt(i);
12 |
13 | if (c === quote) {
14 | if (state === State.row) {
15 | // starts
16 | valueStart = i + 1;
17 | state = State.valueQuoted;
18 | continue;
19 | }
20 | if (state === State.valueQuoted) {
21 |
22 | // ends
23 | row.push(str.substring(valueStart, i));
24 | state = State.row;
25 | continue;
26 | }
27 | }
28 | if (c === 10 /*'\n'*/ || c === 13 /*'\r'*/) {
29 | if (state === State.row) {
30 | if (row.length > 0) {
31 | arr.push(row);
32 | row = [];
33 | continue;
34 | }
35 | }
36 | }
37 | if (c === delimiter) {
38 | if (state === State.valueSimple) {
39 | // ends
40 | row.push(str.substring(valueStart, i));
41 | state = State.row;
42 | continue;
43 | }
44 | continue;
45 | }
46 | if (c === 92 /* \ */) {
47 | i++;
48 | continue;
49 | }
50 |
51 |
52 | if (state === State.row && c > 32) {
53 | valueStart = i;
54 | state = State.valueSimple;
55 | continue;
56 | }
57 | }
58 | if (state === State.valueSimple) {
59 | row.push(str.substring(valueStart));
60 | state = State.row;
61 | }
62 | if (row.length) {
63 | arr.push(row);
64 | }
65 |
66 | return arr;
67 | }
68 | }
69 |
70 | enum State {
71 | row = 0,
72 | valueQuoted = 1,
73 | valueSimple = 2,
74 | };
75 |
--------------------------------------------------------------------------------
/src/commands/list/CReset.ts:
--------------------------------------------------------------------------------
1 | import { $cli } from '@core/utils/$cli';
2 | import { $console } from '@core/utils/$console';
3 | import { File, env } from 'atma-io';
4 | import { ICommand } from '../ICommand';
5 |
6 | export function CReset() {
7 | return {
8 | command: 'reset',
9 | description: [
10 | 'Reset various things'
11 | ],
12 | subcommands: [
13 | {
14 | command: 'accounts',
15 | description: [
16 | 'Remove all accounts'
17 | ],
18 | params: {
19 | '--config-accounts': {
20 | description: 'Optional. File path. Default is gray<%appdata%/.dequanto/accounts.json>'
21 | }
22 | },
23 | async process() {
24 | let configPathAccounts = $cli.getParamValue('config-accounts')
25 | ?? env.appdataDir.combine('./.dequanto/accounts.json').toString();
26 |
27 | let exists = await File.existsAsync(configPathAccounts);
28 | if (exists === false) {
29 | throw new Error(`File does not exist: ${configPathAccounts}`);
30 | }
31 | await File.removeAsync(configPathAccounts);
32 | $console.result(`bold>`);
33 | }
34 | },
35 | {
36 | command: 'config',
37 | description: [
38 | 'Remove global config file: bold<%appdata%/.dequanto/config.yml>'
39 | ],
40 | params: {
41 |
42 | },
43 | async process() {
44 | let path = env.appdataDir.combine('./.dequanto/config.yml').toString();
45 |
46 | let exists = await File.existsAsync(path);
47 | if (exists === false) {
48 | throw new Error(`File does not exist: ${path}`);
49 | }
50 | await File.removeAsync(path);
51 | $console.result(`bold>`);
52 | }
53 | }
54 | ],
55 | async process() {
56 | throw new Error(`Subcommand is not set`);
57 | }
58 | };
59 | }
60 |
--------------------------------------------------------------------------------
/test/commands/tx.spec.ts:
--------------------------------------------------------------------------------
1 | import { HardhatProvider } from '@dequanto/hardhat/HardhatProvider';
2 | import { l } from '@dequanto/utils/$logger';
3 | import { File } from 'atma-io';
4 | import { TestNode } from '../../dequanto/test/hardhat/TestNode';
5 | import { TestUtils } from '../TestUtils';
6 |
7 |
8 |
9 | let provider = new HardhatProvider();
10 | let client = provider.client('localhost');
11 | let owner1 = provider.deployer(0);
12 | let owner2 = provider.deployer(1);
13 |
14 | UTest({
15 | $config: {
16 | timeout: 2 * 60 * 1000
17 | },
18 | async '$before'() {
19 | await TestUtils.clean();
20 | await TestNode.start();
21 | },
22 | async 'should sign, send signed tx json' () {
23 |
24 | let STDOUT_SILENT = true;
25 |
26 | await File.writeAsync('./test/bin/tx.json', {
27 | tx: {
28 | to: owner2.address,
29 | value: 1,
30 | }
31 | });
32 |
33 | l`Signing with ${owner1.address}`;
34 | let result = await TestUtils.cli(`tx sign ./test/bin/tx.json`, {
35 | '--account': owner1.key,
36 | '--chain': 'hardhat'
37 | }, { silent: STDOUT_SILENT });
38 |
39 |
40 | let json = await File.readAsync('./test/bin/tx.json', { cached: false });
41 | eq_(typeof json.tx.nonce, 'number', json.tx.nonce);
42 | has_(json.signature.r, /^0x/);
43 |
44 | let balanceBefore = await client.getBalance(owner2.address);
45 | l`Balance for ${owner2.address}: ${balanceBefore}ETH`
46 |
47 | let txSendStdout = await TestUtils.cli(`tx send ./test/bin/tx.json`, {
48 |
49 | }, { silent: STDOUT_SILENT });
50 |
51 | let tx = /Tx\s+(?0x\w+)/.exec(txSendStdout)?.groups.tx;
52 | has_(tx, /^0x[\w]{64}/);
53 |
54 |
55 | let balanceAfter = await client.getBalance(owner2.address);
56 | l`Balance for ${owner2.address}: bold>`
57 |
58 | eq_(balanceAfter - balanceBefore, 1n);
59 |
60 |
61 | let txViewStdout = await TestUtils.cli(`tx view ${tx}`, {
62 |
63 | }, { silent: STDOUT_SILENT });
64 |
65 | has_(txViewStdout, tx);
66 |
67 |
68 | let txDetails = await TestUtils.api(`/api/tx/view/${tx}?chain=hardhat`);
69 | eq_(txDetails.tx.hash, tx);
70 | eq_(txDetails.tx.from?.toLowerCase(), owner1.address.toLowerCase());
71 | }
72 | })
73 |
--------------------------------------------------------------------------------
/src/commands/list/CRestore.ts:
--------------------------------------------------------------------------------
1 | import di from 'a-di';
2 | import alot from 'alot';
3 | import { Generator } from '@dequanto/gen/Generator';
4 | import { ICommand } from '../ICommand';
5 | import { PackageService } from '@core/services/PackageService';
6 | import { $is } from '@dequanto/utils/$is';
7 | import { $console } from '@core/utils/$console';
8 |
9 | export function CRestore() {
10 | return {
11 | command: 'restore',
12 |
13 | description: [
14 | `Reinstall contracts from 0xweb.json`
15 | ],
16 |
17 | async process() {
18 |
19 |
20 | let packageService = di.resolve(PackageService);
21 | let packages = await packageService.getLocalPackages();
22 |
23 | await alot(packages)
24 | .forEachAsync(async (pkg, i) => {
25 | $console.toast(`Install ${pkg.name} ${pkg.platform}`);
26 |
27 | if ($is.Address(pkg.address) === false) {
28 | $console.log(`Skip ${pkg.name}(${pkg.address}) as not valid address`);
29 | return;
30 | }
31 | let pathPfx = '';
32 | let pathFilename = '';
33 | if (pkg.name.includes('/') === false) {
34 | // 0xweb i 0x123 --name chainlink/feed-eth
35 | // is installed into 0xweb/eth/chainlink/feed-eth/feed-eth.ts
36 | pathPfx = pkg.name;
37 | pathFilename = pkg.name;
38 | } else {
39 | pathPfx = pkg.name;
40 | pathFilename = pkg.name.substring(pkg.name.lastIndexOf('/') + 1);
41 | }
42 |
43 | let output = pkg.main.replace(`${pathPfx}/${pathFilename}.ts`, '');
44 | let generator = new Generator({
45 | name: pkg.name,
46 | platform: pkg.platform,
47 | source: {
48 | abi: pkg.address,
49 | },
50 | defaultAddress: pkg.address,
51 | implementation: pkg.implementation,
52 | output: output,
53 | saveAbi: true
54 | });
55 | await generator.generate();
56 | })
57 | .toArrayAsync({ threads: 1 })
58 | }
59 | };
60 | }
61 |
--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
3 |
4 | name: Node.js Package
5 |
6 | on:
7 | push:
8 | branches:
9 | - 'release'
10 |
11 | jobs:
12 | build-test-publish:
13 | runs-on: ubuntu-latest
14 | permissions:
15 | contents: read
16 | id-token: write
17 | steps:
18 | - uses: actions/checkout@v3
19 | with:
20 | submodules: recursive
21 | - uses: actions/setup-node@v3
22 | with:
23 | node-version: 20
24 | registry-url: 'https://registry.npmjs.org'
25 | cache: npm
26 |
27 | - run: npm ci
28 | - run: npm run build
29 | - run: npm test
30 | - run: npm publish --provenance --access public
31 | env:
32 | NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
33 | - name: Trigger storage example with latest 0xweb
34 | env:
35 | CIRCLECI_TOKEN: ${{ secrets.CIRCLECI_TOKEN }}
36 | run: |
37 | curl -X POST https://circleci.com/api/v2/project/github/0xweb-org/examples-storage/pipeline \
38 | -H "Circle-Token: $CIRCLECI_TOKEN" \
39 | -H "Content-Type: application/json" \
40 | -d '{ "branch": "master" }'
41 | - name: Trigger backend example with latest 0xweb
42 | env:
43 | CIRCLECI_TOKEN: ${{ secrets.CIRCLECI_TOKEN }}
44 | run: |
45 | curl -X POST https://circleci.com/api/v2/project/github/0xweb-org/examples-backend/pipeline \
46 | -H "Circle-Token: $CIRCLECI_TOKEN" \
47 | -H "Content-Type: application/json" \
48 | -d '{ "branch": "master" }'
49 |
50 | - name: Trigger hardhat example with latest 0xweb
51 | env:
52 | CIRCLECI_TOKEN: ${{ secrets.CIRCLECI_TOKEN }}
53 | run: |
54 | curl -X POST https://circleci.com/api/v2/project/github/0xweb-org/examples-hardhat/pipeline \
55 | -H "Circle-Token: $CIRCLECI_TOKEN" \
56 | -H "Content-Type: application/json" \
57 | -d '{ "branch": "master" }'
58 |
59 | - name: Trigger hardhat example with latest 0xweb
60 | env:
61 | CIRCLECI_TOKEN: ${{ secrets.CIRCLECI_TOKEN }}
62 | run: |
63 | curl -X POST https://circleci.com/api/v2/project/github/0xweb-org/examples-price/pipeline \
64 | -H "Circle-Token: $CIRCLECI_TOKEN" \
65 | -H "Content-Type: application/json" \
66 | -d '{ "branch": "master" }'
67 |
--------------------------------------------------------------------------------
/test/accounts.spec.ts:
--------------------------------------------------------------------------------
1 | import { config } from '@dequanto/config/Config';
2 | import { l } from '@dequanto/utils/$logger';
3 | import { $sig } from '@dequanto/utils/$sig';
4 | import { File } from 'atma-io';
5 | import { run } from 'shellbee';
6 |
7 |
8 | const ACCOUNTS_PATH = './test/bin/accounts.json';
9 | const ACCOUNT_PATH = './0xc/config/account.json';
10 | const COMMAND_RAW = (params) => `node ./index.js ${params}`;
11 | const COMMAND = (params) => COMMAND_RAW(`${params} --config-accounts ${ACCOUNTS_PATH} -p 12345`);
12 | const cli = async (params: string) => {
13 | let { stdout, stderr } = await run(COMMAND(params));
14 | return stdout.join('\n');
15 | };
16 | const cliRaw = async (params: string) => {
17 | let { stdout, stderr } = await run(COMMAND_RAW(params));
18 | return stdout.join('\n');
19 | };
20 |
21 | UTest({
22 | async '$before' () {
23 | await File.removeAsync(ACCOUNTS_PATH);
24 | await File.removeAsync(ACCOUNT_PATH);
25 | },
26 | async 'should add, list, remove account' () {
27 | let account = $sig.$account.generate();
28 |
29 | l`> add`
30 | let addedStdout = await cli(`accounts add -n foo -k ${account.key}`);
31 | has_(addedStdout, account.address);
32 |
33 | let jsonStr = await File.readAsync(ACCOUNTS_PATH, { skipHooks: true });
34 | hasNot_(jsonStr, account.address);
35 | hasNot_(jsonStr, 'foo');
36 |
37 | l`> list`
38 | let listStdout = await cli('accounts list');
39 | has_(listStdout, 'foo');
40 | has_(listStdout, account.address);
41 |
42 | l`> Check encrypted key`
43 | let str = await cli(`account view foo --encrypted-key`);
44 | has_(str, `p1:0x`);
45 | hasNot_(str, account.key);
46 |
47 | l`> Decrypt key`
48 | let key = /p1:0x[a-f\d]+/.exec(str)[0];
49 |
50 | config.pin = '12345';
51 |
52 | let address = await $sig.$account.getAddressFromKey(key as any);
53 | eq_(address, account.address);
54 |
55 | l`> Set default`
56 | let strDefaults = await cli(`accounts login foo`);
57 | has_(strDefaults, account.address);
58 |
59 | l`> Get default`
60 | let strDefaultsCurrent = await cliRaw(`account current -p 12345`);
61 |
62 |
63 | has_(strDefaultsCurrent, account.address);
64 |
65 |
66 | l`> remove`
67 | let removeStdout = await cli('accounts remove -n foo');
68 | hasNot_(removeStdout, 'foo');
69 | hasNot_(removeStdout, account.address);
70 |
71 |
72 | listStdout = await cli('accounts list');
73 | hasNot_(listStdout, 'foo');
74 | hasNot_(listStdout, account.address);
75 | }
76 | })
77 |
--------------------------------------------------------------------------------
/test/commands/server.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestUtils } from '../TestUtils';
2 | import { TestNode } from '../../dequanto/test/hardhat/TestNode';
3 | import type { Shell } from 'shellbee';
4 |
5 |
6 | let shell: Shell;
7 |
8 | UTest({
9 | $config: {
10 | timeout: 2 * 60 * 1000
11 | },
12 |
13 | async $before () {
14 | await TestUtils.clean();
15 | await TestNode.start();
16 |
17 | await TestUtils.cli(`accounts new --name deployerApiFoo`);
18 | await TestUtils.cli(`hardhat setBalance deployerApiFoo 1ether`)
19 |
20 | let stdAddOne = await TestUtils.cli(`deploy ./test/fixtures/hardhat/Foo.sol --name FooApi --chain hardhat --account deployerApiFoo`, {
21 |
22 | });
23 | has_(stdAddOne, /Deployed\s+0x\w+/);
24 |
25 |
26 | shell = await TestUtils.cliParallel(`server start --chain hardhat`, {}, {
27 | matchReady: /Local:/,
28 | silent: true,
29 | });
30 |
31 | await shell.onReadyAsync();
32 | },
33 |
34 | async $after () {
35 | await shell.terminate();
36 | },
37 |
38 | async 'get block'() {
39 |
40 | let json = await getJson('/api/block/get/latest?chain=hardhat');
41 |
42 | eq_(typeof json.number, 'number');
43 | has_(json.hash, /0x\w+/);
44 | },
45 |
46 | async 'fetch contract info' () {
47 | let json = await getJson('/api/c/list');
48 |
49 | let fooApi = json.find(c => c.name === 'FooApi');
50 | notEq_(fooApi, undefined, `FooApi not found`);
51 |
52 | let abi = await getJson('/api/c/abi/FooApi');
53 | has_(abi, [
54 | {
55 | name: 'getFoo',
56 | type: 'function',
57 | }
58 | ]);
59 |
60 | let value = await getJson(`/api/c/read/FooApi/getFoo`);
61 | eq_(value, '10');
62 | eq_(BigInt(value), 10n);
63 |
64 |
65 | let resp = await fetch(`http://localhost:3000/api/c/write/FooApi/setFoo`, {
66 | method: 'POST',
67 | headers: {
68 | 'Content-Type': 'application/json',
69 | },
70 | body: JSON.stringify({
71 | foo_: 11,
72 | account: 'deployerApiFoo'
73 | })
74 | });
75 | let { hash } = await resp.json();
76 | let tx = await getJson(`/api/tx/view/${hash}?chain=hardhat`);
77 |
78 | eq_(tx.status, 'success');
79 | eq_(tx.tx.hash, hash);
80 |
81 | let newValue = await getJson(`/api/c/read/FooApi/getFoo`);
82 | eq_(BigInt(newValue), 11n);
83 |
84 | }
85 | })
86 |
87 | async function getJson (path: string) {
88 | let resp = await fetch(`http://localhost:3000${path}`);
89 | let json = await resp.json();
90 | return json;
91 | }
92 |
--------------------------------------------------------------------------------
/src/services/RpcService.ts:
--------------------------------------------------------------------------------
1 | import { App } from '@core/app/App';
2 | import { $bigint } from '@dequanto/utils/$bigint';
3 | import { BaseService } from './BaseService';
4 |
5 | export class RpcService extends BaseService {
6 | async process(args: any[], params?, app?: App) {
7 |
8 | let client = app.chain.client;
9 | let [ method, ...methodArgs ] = args as [ string, ...any[]];
10 |
11 | for (let key in params) {
12 | let argMatch = /^arg(?\d+)$/.exec(key);
13 | if (argMatch) {
14 | let index = Number(argMatch.groups.index);
15 | methodArgs[index] = params[key];
16 | }
17 | }
18 |
19 | methodArgs = methodArgs.map(arg => {
20 | let typeMatch = /(?\w+):/.exec(arg);
21 | if (typeMatch == null) {
22 | let type = this.detectType(arg);
23 | return this.toValue(type, arg)
24 | }
25 |
26 | /** @TODO add type support*/
27 | let type = typeMatch.groups.type;
28 | let value = arg.replace(typeMatch[0], '');
29 | return this.toValue(type, value);
30 | });
31 |
32 | let result = await client.with(async wClient => {
33 | let rpc = wClient.rpc;
34 | if (method in rpc.fns === false) {
35 | rpc.extend([{
36 | name: method,
37 | call: method,
38 | }]);
39 | }
40 | let result = await rpc.fns[method](...methodArgs);
41 | return result;
42 | });
43 |
44 | if (typeof result !== 'object') {
45 | this.printResult(result);
46 | } else {
47 | this.printResult(JSON.stringify(result, null, 4))
48 | };
49 |
50 | return result;
51 | }
52 |
53 | private toValue(type: 'boolean' | string, str: string) {
54 | if (type === 'boolean') {
55 | str = str.toLowerCase();
56 | if ('true' === str || '1' === str) {
57 | return true;
58 | }
59 | if ('false' === str || '0' === str) {
60 | return false;
61 | }
62 | throw new Error(`Invalid boolean value: ${str}`);
63 | }
64 | if (/^uint/.test(str)) {
65 | try {
66 | let num = BigInt(str);
67 | return $bigint.toHex(num);
68 | } catch (error) {
69 | throw new Error(`Invalid bigint value: ${str}`);
70 | }
71 | }
72 |
73 | return str;
74 | }
75 | private detectType (str: string) {
76 | if (/^\d+$/.test(str)) {
77 | return 'uint256';
78 | }
79 | if (/^(true|false|0|1)$/i.test(str)) {
80 | return 'boolean';
81 | }
82 | return null;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/commands/list/CSolidity.ts:
--------------------------------------------------------------------------------
1 | import { App } from '@core/app/App';
2 | import { $console } from '@core/utils/$console';
3 | import { Parameters } from '@core/utils/Parameters';
4 | import { EoAccount, TAccount } from '@dequanto/models/TAccount';
5 | import { TxDataBuilder } from '@dequanto/txs/TxDataBuilder';
6 | import { $require } from '@dequanto/utils/$require';
7 | import { File, env } from 'atma-io';
8 | import { ICommand } from '../ICommand';
9 | import { HardhatProvider } from '@dequanto/hardhat/HardhatProvider';
10 | import { class_Uri } from 'atma-utils';
11 |
12 | export function CSolidity() {
13 | return {
14 | command: 'sol',
15 | description: [
16 | 'Solidity utilities'
17 | ],
18 | subcommands: [
19 | {
20 |
21 | command: 'yul',
22 | description: [
23 | 'Compile Solidity code to Yul'
24 | ],
25 | arguments: [
26 | {
27 | description: `Solidity path`,
28 | required: true
29 | }
30 | ],
31 | params: {
32 | '--output, -o': {
33 | description: 'Optional. Override the output file'
34 | }
35 | },
36 | async process(args: string[], params: any, app: App) {
37 | let [path] = args;
38 |
39 | $require.True(await File.existsAsync(path), `File bold<${path}> does not exist`);
40 |
41 | let provider = new HardhatProvider();
42 | let result = await provider.compileSol(path);
43 | //console.log('r', result);
44 | let metaInfoPath = result.output;
45 |
46 | $require.True(await File.existsAsync(metaInfoPath), `File bold<${metaInfoPath}> does not exist`);
47 |
48 | let dbgInfoPath = metaInfoPath.replace('.json', '.dbg.json');
49 | $require.True(await File.existsAsync(dbgInfoPath), `File bold<${metaInfoPath}> does not exist`);
50 |
51 | let dbgInfo = await File.readAsync(dbgInfoPath);
52 |
53 | let dbgInfoPathDir = new class_Uri(dbgInfoPath).toDir();
54 | let buildInfoPath = class_Uri.combine(dbgInfoPathDir, dbgInfo.buildInfo);
55 | $require.True(await File.existsAsync(buildInfoPath), `File bold<${metaInfoPath}> does not exist`);
56 |
57 | let buildInfo = await File.readAsync(buildInfoPath);
58 | //console.log(buildInfo.output.contracts['cache/foo.sol'].Foo.evm);
59 | }
60 | }
61 | ],
62 | async process() {
63 | throw new Error(`Subcommand is not set`);
64 | }
65 | };
66 | }
67 |
--------------------------------------------------------------------------------
/src/utils/$machine.ts:
--------------------------------------------------------------------------------
1 | /* @flow */
2 | import { exec, execSync } from 'child_process';
3 | import { createHash } from 'crypto';
4 |
5 | let { platform } = process;
6 | let win32RegBinPath = {
7 | native: '%windir%\\System32',
8 | mixed: '%windir%\\sysnative\\cmd.exe /c %windir%\\System32'
9 | };
10 | let guid = {
11 | darwin: 'ioreg -rd1 -c IOPlatformExpertDevice',
12 | win32: `${win32RegBinPath[isWindowsProcessMixedOrNativeArchitecture()]}\\REG.exe ` +
13 | 'QUERY HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography ' +
14 | '/v MachineGuid',
15 | linux: '( cat /var/lib/dbus/machine-id /etc/machine-id 2> /dev/null || hostname ) | head -n 1 || :',
16 | freebsd: 'kenv -q smbios.system.uuid || sysctl -n kern.hostuuid'
17 | };
18 |
19 | function isWindowsProcessMixedOrNativeArchitecture(): string {
20 | // detect if the node binary is the same arch as the Windows OS.
21 | // or if this is 32 bit node on 64 bit windows.
22 | if (process.platform !== 'win32') {
23 | return '';
24 | }
25 | if (process.arch === 'ia32' && process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432')) {
26 | return 'mixed';
27 | }
28 | return 'native';
29 | }
30 |
31 | function hash(guid: string): string {
32 | return createHash('sha256').update(guid).digest('hex');
33 | }
34 |
35 | function expose(result: string): string {
36 | switch (platform) {
37 | case 'darwin':
38 | return result
39 | .split('IOPlatformUUID')[1]
40 | .split('\n')[0].replace(/\=|\s+|\"/ig, '')
41 | .toLowerCase();
42 | case 'win32':
43 | return result
44 | .toString()
45 | .split('REG_SZ')[1]
46 | .replace(/\r+|\n+|\s+/ig, '')
47 | .toLowerCase();
48 | case 'linux':
49 | return result
50 | .toString()
51 | .replace(/\r+|\n+|\s+/ig, '')
52 | .toLowerCase();
53 | case 'freebsd':
54 | return result
55 | .toString()
56 | .replace(/\r+|\n+|\s+/ig, '')
57 | .toLowerCase();
58 | default:
59 | throw new Error(`Unsupported platform: ${process.platform}`);
60 | }
61 | }
62 |
63 | // export function machineIdSync(original: boolean): string {
64 | // let id: string = expose(execSync(guid[platform]).toString());
65 | // return original ? id : hash(id);
66 | // }
67 |
68 |
69 | export namespace $machine {
70 |
71 | export function id(original: boolean = false): Promise {
72 | return new Promise((resolve: Function, reject: Function): Object => {
73 | return exec(guid[platform], {}, (err: any, stdout: any, stderr: any) => {
74 | if (err) {
75 | return reject(
76 | new Error(`Error while obtaining machine id: ${err.stack}`)
77 | );
78 | }
79 | let id: string = expose(stdout.toString());
80 | return resolve(original ? id : hash(id));
81 | });
82 | });
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/commands/list/CHelp.ts:
--------------------------------------------------------------------------------
1 | import { App } from '@core/app/App';
2 | import { $console } from '@core/utils/$console';
3 | import { File, env } from 'atma-io';
4 | import { ICommand } from '../ICommand';
5 |
6 | export function CHelp() {
7 | const Help = {
8 | command: 'help, -h, --help',
9 | description: [
10 | 'Print this overview'
11 | ],
12 | async process(args, params: { command?: ICommand }, app: App) {
13 |
14 | let path = env.applicationDir.combine(`/package.json`).toString();
15 | let json = await File.readAsync(path);
16 |
17 | if (params.command != null) {
18 | Help.printCommand(params.command);
19 | return;
20 | }
21 |
22 | $console.log('');
23 | $console.log('bold> We provide our Demo Keys for etherscan and co. Please, replace them with yours: bold>');
24 |
25 | $console.log('');
26 | $console.log(`0xweb@${json.version} Commands`);
27 | $console.log('');
28 |
29 | for (let command of app.commands.list) {
30 | if (/help/.test(command.command)) {
31 | continue;
32 | }
33 | Help.printCommand(command, {}, { short: true });
34 | }
35 | $console.log('');
36 | $console.log('For more details use "0xweb COMMAND --help" or "0xweb COMMAND SUBCOMMAND --help"');
37 |
38 | },
39 |
40 | printCommand(command: ICommand, paramsDefinition?, opts?) {
41 | let str = print.command({
42 | ...command,
43 | params: paramsDefinition ?? command.params
44 | }, opts);
45 | $console.log(str);
46 | }
47 | };
48 | return Help;
49 | }
50 | namespace print {
51 | export function command(c: ICommand, opts: { short?: boolean }, prefix = '') {
52 | let lines = [];
53 |
54 | lines.push(`yellow>`);
55 | lines.push(c.description.map(x => ` ${x}`).join('\n'));
56 |
57 | if (opts?.short !== true && c.arguments?.length > 0) {
58 | lines.push(` italic`);
59 | c.arguments.forEach((arg, i) => {
60 | lines.push(` bold<${arg.name ?? i}${arg.required ? '*' : ''}>: ${arg.description}`);
61 | });
62 | }
63 | if (c.subcommands?.length > 0) {
64 | lines.push(` gray`);
65 | for (let sub of c.subcommands) {
66 | lines.push(print.command(sub, opts, prefix + ' '));
67 | }
68 | }
69 | if (opts?.short !== true && c.params && Object.keys(c.params).length > 0) {
70 | lines.push(` italic`);
71 | for (let key in c.params) {
72 | let arg = c.params[key];
73 | lines.push(` bold<${key}${arg.required ? '*' : ''}>: ${arg.description}`);
74 | }
75 | }
76 | //lines.push(``);
77 |
78 | return lines.map(x => `${prefix}${x}`).join('\n');
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/commands/list/CToken.ts:
--------------------------------------------------------------------------------
1 | import di from 'a-di';
2 | import { ICommand } from '../ICommand';
3 | import { $validate } from '@core/utils/$validate';
4 | import { App } from '@core/app/App';
5 | import { $console } from '@core/utils/$console';
6 | import { $is } from '@dequanto/utils/$is';
7 | import { IToken } from '@dequanto/models/IToken';
8 | import { TokenPriceService } from '@dequanto/tokens/TokenPriceService';
9 |
10 |
11 | export function CToken() {
12 | return {
13 | command: 'token',
14 |
15 | description: [
16 | 'Get token info'
17 | ],
18 | subcommands: [
19 | {
20 | command: 'price',
21 | example: '0xweb token price ETH -b ',
22 | description: [
23 | 'Get token price'
24 | ],
25 | arguments: [
26 | {
27 | name: '',
28 | description: 'Token symbol or token address',
29 | required: true,
30 | }
31 | ],
32 | params: {
33 | '-b, --block': {
34 | description: 'Price at specific block. Default: latest',
35 | map: Number
36 | }
37 | },
38 | async process(args: string[], params: any, app: App) {
39 | let [tokenMix] = args;
40 |
41 | $console.toast(`Loading token ${tokenMix}`);
42 | let token = await app.chain.tokens.getToken(tokenMix, true);
43 | if (token == null && $is.Address(tokenMix)) {
44 | token = {
45 | address: tokenMix,
46 | decimals: 18,
47 | platform: app.chain.client.platform,
48 | };
49 | }
50 | if (token == null) {
51 | throw new Error(`Token ${tokenMix} not found`);
52 | }
53 |
54 | $console.toast(`Loading price`);
55 | let service = di.resolve(TokenPriceService, app.chain.client, app.chain.explorer);
56 | let priceData = await service.getPrice(token, {
57 | block: params.block,
58 | });
59 |
60 | $console.table([
61 | ['Symbol', token.symbol],
62 | ['Address', token.address],
63 | ['Decimals', token.decimals.toString()],
64 | ['Price', `green<${priceData.price}>`]
65 | ]);
66 | }
67 | },
68 |
69 | ],
70 | params: {
71 | '-c, --chain': {
72 | description: `Default: eth. Available: ${$validate.platforms().join(', ')}`
73 | }
74 | },
75 |
76 | async process(args: string[], params, app: App) {
77 | console.warn(`Command for an "token" not found: ${args[0]}. Call "0xweb token --help" to view the list of commands`);
78 | }
79 | }
80 | };
81 |
--------------------------------------------------------------------------------
/src/utils/CommandUtil.ts:
--------------------------------------------------------------------------------
1 | import { class_Uri } from 'atma-utils';
2 | import { File } from 'atma-io';
3 |
4 | export namespace CommandUtil {
5 | export async function formatPaths(command: string, cwd: string) {
6 |
7 | command = command.trim();
8 |
9 | let redirect = '';
10 | let redirectIdx = command.indexOf('>>')
11 | if (redirectIdx > -1) {
12 | redirect = ' ' + command.substring(redirectIdx);
13 | command = command.substring(0, redirectIdx).trim();
14 | }
15 |
16 | command = ensureCwdIfCronbee(command, cwd);
17 |
18 | command = await rewriteAbsPath(command, cwd);
19 | command = ensureCwd(command, cwd);
20 | return command + redirect;
21 | }
22 | export function split(command: string): string[] {
23 | let args = [];
24 | for (let i = 0; i < command.length; i++) {
25 | let c = command[i];
26 | if (c === ' ') {
27 | continue;
28 | }
29 | if (c === '"') {
30 | let end = command.indexOf('"', i + 1);
31 | if (end === -1) {
32 | throw new Error(`Invalid command ${command}. Quote not closed`);
33 | }
34 | args.push(command.slice(i + 1, end));
35 | i = end + 1;
36 | continue;
37 | }
38 |
39 | let rgx = /(\s|$)/g;
40 |
41 | rgx.lastIndex = i;
42 | let match = rgx.exec(command);
43 | if (match == null) {
44 | throw new Error(`Invalid command ${command}. Param has no ending`);
45 | }
46 |
47 | args.push(command.slice(i, match.index));
48 | i = match.index;
49 | }
50 | return args;
51 | }
52 |
53 | async function rewriteAbsPath(command: string, cwd: string): Promise {
54 | let rgxCommand = /^[^\s]+/;
55 | let match = rgxCommand.exec(command);
56 | if (match == null) {
57 | return command;
58 | }
59 | if (match[0] === 'cronbee') {
60 | let args = await rewriteAbsPath(slice(command, match), cwd);
61 | command = `cronbee ${args}`;
62 | }
63 | let path = await getAbsPathIfNodeModule(match[0], cwd);
64 | if (path) {
65 | command = `${path} ${slice(command, match)}`;
66 | }
67 | return command;
68 | }
69 |
70 | function slice(str: string, match: RegExpMatchArray) {
71 | return str.substring(match.index + match[0].length + 1).trim();
72 | }
73 |
74 | async function getAbsPathIfNodeModule(name: string, cwd) {
75 | let path = class_Uri.combine(cwd, '/node_modules/.bin/', name);
76 | let exists = await File.existsAsync('file://' + path);
77 | if (exists) {
78 | return path;
79 | }
80 | return null;
81 | }
82 |
83 | function ensureCwd(str: string, cwd) {
84 | if (str.includes('-cwd') === false && process.platform !== 'win32') {
85 | return `cd ${cwd} && ${str}`;
86 | }
87 | return str;
88 | }
89 |
90 | function ensureCwdIfCronbee(command: string, cwd) {
91 | if (command.includes('cronbee') && command.includes('-cwd') === false) {
92 | return `${command} --cwd ${cwd}`;
93 | }
94 | return command;
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/test/TestUtils.ts:
--------------------------------------------------------------------------------
1 | import di from 'a-di';
2 | import alot from 'alot';
3 | import { Web3Client } from '@dequanto/clients/Web3Client';
4 | import { HardhatProvider } from '@dequanto/hardhat/HardhatProvider';
5 | import { Directory, File } from 'atma-io';
6 | import { Shell, run } from 'shellbee';
7 | import type { IShellParams } from 'shellbee/interface/IProcessParams';
8 | import { EoAccount } from '@dequanto/models/TAccount';
9 | import { ServerService } from '@core/services/ServerService';
10 | import { App } from '@core/app/App';
11 | import { HttpResponse } from 'atma-server';
12 |
13 | const ACCOUNTS_PATH = './test/bin/accounts.json';
14 | const CONFIG_PATH = './test/bin/config.json';
15 | const HH_CONTRACTS = './0xc/hardhat/';
16 | const HH_CACHE = './cache/';
17 | const HH_ASSETS = './assets/';
18 | const HH_OZ = './contracts/oz/';
19 |
20 | const PARAMS_DEF = {
21 | '--config-accounts': ACCOUNTS_PATH,
22 | '--config-global': CONFIG_PATH,
23 | '--pin': '12345',
24 | '--chain': 'hardhat',
25 | '--color': 'none'
26 | };
27 |
28 | export const TestUtils = {
29 | async clean() {
30 |
31 | await File.removeAsync(ACCOUNTS_PATH);
32 | await File.removeAsync(CONFIG_PATH);
33 | try { await Directory.removeAsync(HH_CONTRACTS); } catch { }
34 | try { await Directory.removeAsync(HH_CACHE); } catch { }
35 | try { await Directory.removeAsync(HH_ASSETS); } catch { }
36 | try { await Directory.removeAsync(HH_OZ); } catch { }
37 |
38 | },
39 | async api (url: string) {
40 | let app = new App();
41 | let service = new ServerService(app);
42 | let server = await service.createServer();
43 | let response = await server.execute(url, 'get') as HttpResponse;
44 | return JSON.parse(response.content);
45 | },
46 | async cli(command: string, params: Record = {}, opts?: { silent?: boolean }) {
47 | params = {
48 | ...PARAMS_DEF,
49 | ...params
50 | };
51 | let paramsStr = alot.fromObject(params).map(x => `${x.key} ${x.value}`).toArray().join(' ');
52 | let cmdStr = `node ./index.js ${command} ${paramsStr}`;
53 | let { stdout, stderr, lastCode } = await run({
54 | command: cmdStr,
55 | silent: opts?.silent ?? true,
56 | });
57 | if (lastCode !== 0) {
58 | console.error(stdout.join('\n'), stderr.join('\n'));
59 | throw new Error(`Process exit code ${lastCode}`)
60 | }
61 | return stdout.join('\n').trim();
62 | },
63 | async cliParallel(command: string, params: Record = {}, opts?: IShellParams): Promise {
64 | params = {
65 | ...PARAMS_DEF,
66 | ...params
67 | };
68 | let paramsStr = alot.fromObject(params).map(x => `${x.key} ${x.value}`).toArray().join(' ');
69 | let cmdStr = `node ./index.js ${command} ${paramsStr}`;
70 |
71 | let shell = new Shell({
72 | ...(opts ?? {}),
73 | command: cmdStr,
74 | //silent: true,
75 | });
76 | shell.run();
77 | return shell;
78 | },
79 | async deployFreeToken(client: Web3Client, opts?: { deployer?: EoAccount }) {
80 | let { contract } = await di.resolve(HardhatProvider).deploySol('/dequanto/test/fixtures/contracts/FreeToken.sol', {
81 | client,
82 | deployer: opts?.deployer
83 | });
84 | return contract;
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/test/commands/deploy.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestNode } from '../../dequanto/test/hardhat/TestNode';
2 | import { TestUtils } from '../TestUtils';
3 | import { Directory, File } from 'atma-io';
4 | import { l } from '@dequanto/utils/$logger';
5 | import { IDeployment } from '@dequanto/contracts/deploy/storage/DeploymentsStorage';
6 | import { TEth } from '@dequanto/models/TEth';
7 | import { $bigint } from '@dequanto/utils/$bigint';
8 |
9 | UTest({
10 | $config: {
11 | timeout: 60 * 1000
12 | },
13 | async '$before'() {
14 | await TestUtils.clean();
15 | await TestNode.start();
16 | await Directory.copyTo('./lib/', './node_modules/0xweb/lib/', { verbose: true });
17 | await Directory.copyTo('./dequanto/src/gen/', './node_modules/0xweb/dequanto/src/gen/', { verbose: true });
18 | },
19 | async 'compile contract'() {
20 | let stdAddOne = await TestUtils.cli(`compile ./test/fixtures/hardhat/Foo.sol`, {
21 | });
22 | l`The output should contain the path to the compiled artifact`;
23 | has_(stdAddOne, `artifacts/test/fixtures/hardhat/Foo.sol/Foo.json`);
24 |
25 | let classPath = './0xc/hardhat/Foo/Foo.ts';
26 | eq_(await File.existsAsync(classPath), true, `The ts file should be created at ${classPath}`);
27 | },
28 | async 'deploy contract'() {
29 | let client = await TestNode.client('localhost');
30 |
31 | return UTest({
32 | async $before () {
33 | let result = await TestUtils.cli(`accounts new --name deployerFoo`);
34 | let address = /Address\s+(?\w+)/.exec(result).groups.address as TEth.Address;
35 |
36 | await TestUtils.cli(`hardhat setBalance deployerFoo 1ether`)
37 |
38 | let b = await client.getBalance(address);
39 | eq_($bigint.toEther(b), 1);
40 | },
41 | async 'single contract' () {
42 | let stdAddOne = await TestUtils.cli(`deploy ./test/fixtures/hardhat/Foo.sol --account deployerFoo --chain hardhat --name FooSingle`, {
43 |
44 | });
45 | has_(stdAddOne, /Deployed\s+0x\w+/);
46 |
47 | let $0xweb = await File.readAsync('./0xweb.json');
48 | has_($0xweb, {
49 | "deployments": {
50 | "hardhat": [
51 | {
52 | "path": "./0xc/deployments/deployments-hardhat.json"
53 | }
54 | ]
55 | }
56 | });
57 | let $deployments = await File.readAsync('./0xc/deployments/deployments-hardhat.json');
58 | let $deploymentInfo = $deployments.find(x => x.id === 'FooSingle');
59 |
60 | notEq_($deploymentInfo, undefined, `The deployment info should be found in the deployments file`);
61 | eq_($deploymentInfo.name, 'Foo', `The deployment name should be equal the contract name in sol file: 'Foo'`);
62 |
63 | let fooResponse = await TestUtils.cli(`c read FooSingle getFoo`);
64 | has_(fooResponse, `10n`);
65 | },
66 | async 'with proxy' () {
67 | let stdAddOne = await TestUtils.cli(`deploy ./test/fixtures/hardhat/Foo.sol --account deployerFoo --chain hardhat --name FooProxy --proxy`, {
68 |
69 | });
70 | has_(stdAddOne, /Deployed\s+0x\w+/);
71 |
72 | // Deployed as proxy, the value set in constructor should affect the value in FooProxy, so remains 0
73 | let fooResponse = await TestUtils.cli(`c read FooProxy getFoo`);
74 | has_(fooResponse, /^0n$/m);
75 | }
76 | });
77 | },
78 |
79 | })
80 |
--------------------------------------------------------------------------------
/test/services/contract.spec.ts:
--------------------------------------------------------------------------------
1 | import { App } from '@core/app/App';
2 | import { CContract } from '@core/commands/list/CContract';
3 | import { ContractBase } from '@dequanto/contracts/ContractBase';
4 | import { HardhatProvider } from '@dequanto/hardhat/HardhatProvider';
5 | import { l } from '@dequanto/utils/$logger';
6 | import { run } from 'shellbee';
7 |
8 |
9 | const hh = new HardhatProvider();
10 | const client = hh.client();
11 | const app = await new App().setChain(client);
12 |
13 | let contract: ContractBase;
14 |
15 | UTest({
16 | async '$before'() {
17 | let path = './test/fixtures/contracts/StorageCounter.sol';
18 |
19 | l`Deploy: ${path}`;
20 | let deployment = await hh.deploySol(path, { client });
21 | contract = deployment.contract;
22 |
23 | let command = `node index i ${contract.address} --name Counter --chain hardhat --source ${path}`;
24 |
25 | l`Install: ${contract.address}`;
26 | let result = await run({
27 | command,
28 | silent: true,
29 | });
30 | },
31 | async 'list installation'() {
32 | let str = await app.execute([
33 | 'contract',
34 | 'list'
35 | ]);
36 | has_(JSON.stringify(str), contract.address);
37 | },
38 |
39 | 'calldata': {
40 | async 'simple'() {
41 | let result = await app.execute(
42 | 'contract calldata Counter getCountMethod'.split(' ')
43 | );
44 | eq_(result.data, '0xe3412189');
45 |
46 | let resultParsed = await app.execute(
47 | 'contract calldata-parse Counter 0xe3412189'.split(' ')
48 | );
49 | eq_(resultParsed.name, 'getCountMethod');
50 | console.log(resultParsed);
51 | },
52 | async 'with argument'() {
53 |
54 | l`Create calldata`
55 | let result = await app.execute([
56 | `contract`,
57 | `calldata`,
58 | `Counter`,
59 | `updateUser`,
60 | `--user_`,
61 | `{"owner": "0x123", "amount": "0x456"}`
62 | ]);
63 | eq_(result?.data, '0x48a6e18c00000000000000000000000000000000000000000000000000000000000001230000000000000000000000000000000000000000000000000000000000000456');
64 |
65 | l`Parse calldata`
66 | let resultParsed = await app.execute([
67 | `contract`,
68 | `calldata-parse`,
69 | `Counter`,
70 | result.data,
71 | ]);
72 |
73 | deepEq_(resultParsed, {
74 | name: 'updateUser',
75 | args: [
76 | {
77 | owner: '0x0000000000000000000000000000000000000123',
78 | amount: 1110n
79 | }
80 | ],
81 | params: {
82 | user_: {
83 | owner: '0x0000000000000000000000000000000000000123',
84 | amount: 1110n
85 | }
86 | },
87 | value: undefined
88 | });
89 |
90 | let deployer = hh.deployer();
91 | let receipt = await app.execute([
92 | `tx`,
93 | `send`,
94 | `--to`, `Counter`,
95 | `--data`, result.data,
96 | `--account`, deployer.key,
97 | ]);
98 | eq_(receipt.status, 1);
99 |
100 | let user = await app.execute([
101 | `contract`,
102 | `var`,
103 | `Counter`,
104 | 'user'
105 | ]);
106 | eq_(Number(user.owner), 0x123);
107 | eq_(user.amount, 0x456n);
108 | }
109 | }
110 | })
111 |
--------------------------------------------------------------------------------
/test/bootstrap.spec.ts:
--------------------------------------------------------------------------------
1 | import alot from 'alot';
2 | import { run } from 'shellbee'
3 | import { Directory, File } from 'atma-io'
4 |
5 |
6 |
7 | const path_ROOT = './bin/bootstrap/';
8 |
9 | UTest({
10 | $config: {
11 | timeout: 120_000
12 | },
13 |
14 | 'bootstrap': {
15 | async '$before' () {
16 | if (Directory.exists(path_ROOT)) {
17 | await Directory.removeAsync(path_ROOT);
18 | }
19 | },
20 | async 'initialize' () {
21 | let { stdout } = await run(`node index.js init -d ${path_ROOT} --atma`);
22 |
23 | let paths = [
24 | 'tsconfig.json',
25 | 'package.json',
26 | 'node_modules/dequanto/'
27 | ];
28 |
29 | await alot (paths).forEachAsync(async name => {
30 | let path = `${path_ROOT}${name}`;
31 | let exists = name.endsWith('/')
32 | ? await Directory.existsAsync(path)
33 | : await File.existsAsync(path);
34 |
35 | eq_(exists, true, `Path does not exist: ${path}`);
36 | }).toArrayAsync({ threads: 1 });
37 |
38 | let packageJson = await File.readAsync(`${path_ROOT}/package.json`);
39 |
40 | eq_('dequanto' in packageJson.dependencies, true, `No dequanto package in dependencies`);
41 | eq_('hardhat' in packageJson.dependencies, false, 'Command without --hardhat flag still has the hardhat library');
42 |
43 | await run({ command: `npm i atma@latest`, cwd: path_ROOT });
44 |
45 | let tsConfigFile = new File(`${path_ROOT}tsconfig.json`);
46 | let tsConfig = await tsConfigFile.readAsync();
47 | tsConfig.compilerOptions.paths['dequanto/*'] = ["node_modules/dequanto/src/*"];
48 | tsConfig.compilerOptions.paths['@dequanto/*'] = ["node_modules/dequanto/src/*"];
49 | await tsConfigFile.writeAsync(tsConfig)
50 | },
51 | },
52 | async 'install contract' () {
53 | let { stdout } = await run(`node index.js i 0x5f4ec3df9cbd43714fe2740f5e3616155c5b8419 --name chainlink/oracle-eth --output ${path_ROOT}0xc/ --chain eth`);
54 |
55 | let content = await File.readAsync(`${path_ROOT}/0xc/eth/chainlink/oracle-eth/oracle-eth.ts`, { skipHooks: true });
56 | has_(content, 'class ChainlinkOracleEth extends ContractBase');
57 |
58 | let packagePath = '0xweb.json';
59 | let json = await File.readAsync(packagePath);
60 |
61 | has_(json.contracts.eth['0x5f4ec3df9cbd43714fe2740f5e3616155c5b8419'].main, 'oracle-eth.ts');
62 | },
63 | async 'execute api' () {
64 | await File.writeAsync(`${path_ROOT}check.ts`, `
65 |
66 | import { ChainlinkOracleEth } from './0xc/eth/chainlink/oracle-eth/oracle-eth';
67 | import { Config } from 'dequanto/config/Config';
68 | import { $bigint } from 'dequanto/utils/$bigint';
69 |
70 | async function example () {
71 | await Config.fetch();
72 |
73 | let oracle = new ChainlinkOracleEth();
74 | let decimals = await oracle.decimals();
75 | let price = await oracle.latestAnswer();
76 |
77 | console.log(\`ETH Price \${$bigint.toEther(price, decimals)}\`);
78 | process.exit(0);
79 | }
80 | example();
81 | `);
82 |
83 | let { stdout } = await run({
84 | command: `npx atma run ./check.ts --config-global dev/null`,
85 | cwd: path_ROOT
86 | });
87 |
88 | let match = /ETH Price (?[\d\.]+)/.exec(stdout.join(''));
89 | let val = Number(match?.groups.price);
90 | eq_(isNaN(val), false, stdout.join(''));
91 | },
92 | async 'execute via cli' () {
93 | let { stdout } = await run(`node index.js contract read chainlink/oracle-eth latestAnswer --config-global dev/null --chain eth`);
94 | let str = stdout.join('');
95 | has_(str, /\d{10,}n/, `"${str}" - Should contain BigInt as the ETH price`);
96 | }
97 | })
98 |
--------------------------------------------------------------------------------
/src/commands/utils/$command.ts:
--------------------------------------------------------------------------------
1 | import { $cli } from '@core/utils/$cli';
2 | import { ICommand } from '../ICommand';
3 | import { $require } from '@dequanto/utils/$require';
4 | import { TEth } from '@dequanto/models/TEth';
5 |
6 | export namespace $command {
7 |
8 | /** e.g. "i, install" or "-n, --name" */
9 | export function getAliases (str: string) {
10 | return str
11 | .split(',')
12 | .map(x => x.trim())
13 | .map(x => {
14 | let name = x.replace(/^\-+/, '');
15 | let isFlag = x !== name;
16 | return {
17 | name,
18 | isFlag
19 | };
20 | });
21 | }
22 |
23 |
24 | export async function getParams (cliParams: any, paramsDef: ICommand['params']) {
25 | let params = {} as any;
26 | let keyMappings = {};
27 | let definitions = {} as { [key: string]: ICommand['params'][''] }
28 | for (let key in paramsDef) {
29 | let aliases = getAliases(key);
30 | let canonical = camelCase(aliases[aliases.length - 1].name);
31 |
32 | paramsDef[key].key = canonical;
33 | aliases.forEach(alias => {
34 | keyMappings[alias.name] = canonical;
35 | definitions[alias.name] = paramsDef[key];
36 | });
37 | }
38 |
39 | for (let key in cliParams) {
40 | let value = cliParams[key];
41 | let mappedKey = keyMappings[key];
42 | if (mappedKey == null) {
43 | params[key] = value;
44 | continue;
45 | }
46 | let def = definitions[key];
47 |
48 | params[mappedKey] = parseValue(value, def);
49 | }
50 |
51 | for (let key in paramsDef) {
52 | let definition = paramsDef[key];
53 | let value = params[definition.key];
54 | if (value != null) {
55 | if (definition.map != null) {
56 | params[definition.key] = definition.map(value);
57 | }
58 | }
59 |
60 | if (value == null && definition.default != null) {
61 | value = params[definition.key] = definition.default;
62 | }
63 | if (value == null && definition.required) {
64 | if (definition.fallback) {
65 | value = params[definition.key] = cliParams[definition.fallback];
66 | }
67 | if (value == null) {
68 | params[definition.key] = await $cli.ask(
69 | `\n${definition.description}\n--${definition.key}: `,
70 | definition.type
71 | );
72 | }
73 | }
74 | }
75 |
76 | return params;
77 | }
78 |
79 |
80 | function camelCase (str: string): string {
81 | return str.replace(/\-(\w)/g, (full, char) => {
82 | return char.toUpperCase();
83 | });
84 | }
85 | function parseValue(value: string, def: ICommand['params']['']) {
86 | if (def.type == null) {
87 | return value;
88 | }
89 | if (def.type === 'number') {
90 | if (typeof value === 'number') {
91 | return value;
92 | }
93 | let num = Number(value);
94 | if (isNaN(num)) {
95 | throw new Error(`Not a number (${value}) for "${def.description}"`);
96 | }
97 | return num;
98 | }
99 | if (def.type === 'boolean') {
100 | if (typeof value === 'boolean') {
101 | return value;
102 | }
103 | if (value == null || value === 'true' || value === '1') {
104 | return true;
105 | }
106 | return false;
107 | }
108 | if (def.type === 'address') {
109 | if (value != null) {
110 | $require.Address(value as TEth.Address);
111 | }
112 | }
113 |
114 | return value;
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/commands/list/CInfo.ts:
--------------------------------------------------------------------------------
1 | import type { App } from '@core/app/App';
2 | import { $console } from '@core/utils/$console';
3 | import { Parameters } from '@core/utils/Parameters';
4 | import { IWeb3ClientStatus } from '@dequanto/clients/interfaces/IWeb3ClientStatus';
5 | import { ICommand } from '../ICommand';
6 | import alot from 'alot';
7 |
8 | export function CInfo() {
9 | return {
10 | command: 'info',
11 | description: [
12 | 'Show various information'
13 | ],
14 | subcommands: [
15 | {
16 | command: 'network',
17 | description: [
18 | 'Show Network info'
19 | ],
20 | params: {
21 | ...Parameters.chain()
22 | },
23 | async process(args, params, app: App) {
24 | let client = app.chain.client;
25 | let info: IWeb3ClientStatus[] = await client.getNodeInfos();
26 |
27 | $console.table([
28 | [
29 | 'Nr.',
30 | 'URL',
31 | 'Current_Block',
32 | 'Highest_Block',
33 | 'Status',
34 | 'Syncing',
35 | 'Ping',
36 | 'Peers',
37 | 'Node'
38 | ],
39 | ...alot(info).mapMany(info => {
40 |
41 | let currentBlock = info.blockNumber;
42 | let highestBlock = currentBlock + (info.blockNumberBehind ?? 0);
43 | let diffBlock = highestBlock - currentBlock;
44 | let status = info.status;
45 | let syncingStr = '—';
46 | let currentBlockStr = '—';
47 | let highestBlockStr = '—';
48 |
49 | if (info.syncing) {
50 | currentBlock = info.syncing.currentBlock;
51 | highestBlock = Math.max(info.syncing.highestBlock);
52 | diffBlock = highestBlock - currentBlock;
53 | status = 'sync';
54 |
55 | let stages = info.syncing.stages as {stage_name, block_number}[];
56 |
57 | let chars = alot(stages).max(x => x.stage_name.length);
58 |
59 | syncingStr = stages
60 | .map(stage => `${ stage.stage_name.padEnd(chars, ' ') } ${ Number(stage.block_number) }`)
61 | .join('\n');
62 | }
63 |
64 | if (currentBlock != null) {
65 | currentBlockStr = currentBlock + (diffBlock !== 0 ? ` (${-diffBlock})` : '');
66 | highestBlockStr = highestBlock + '';
67 | }
68 |
69 | let infoRow = [
70 | info.i,
71 | info.url,
72 | currentBlockStr,
73 | highestBlockStr,
74 | status,
75 | syncingStr,
76 | info.pingMs ? info.pingMs + 'ms' : '',
77 | info.peers,
78 | (info.node ?? '—')
79 | ];
80 | let rows = [ infoRow ]
81 | if (info.error?.message) {
82 | rows.push([ info.error.message ]);
83 | rows.push(['']);
84 | }
85 |
86 | return rows;
87 | }).toArray()
88 | ]);
89 | }
90 | }
91 | ],
92 | async process() {
93 | throw new Error(`Subcommand is not set`);
94 | }
95 | };
96 | }
97 |
--------------------------------------------------------------------------------
/test/utils/input.spec.ts:
--------------------------------------------------------------------------------
1 | import { $abiInput } from '@core/utils/$abiInput';
2 | import { TAbiItem } from '@dequanto/types/TAbi';
3 | import { $abiParser } from '@dequanto/utils/$abiParser';
4 | import { l } from '@dequanto/utils/$logger';
5 | import { File } from 'atma-io';
6 |
7 |
8 | UTest({
9 |
10 | async 'should parse arguments' () {
11 | let abi = $abiParser.parseMethod('foo(string bar, uint256 baz)');
12 |
13 | return UTest({
14 | async 'simple' () {
15 | var [ arg1, arg2 ] = await $abiInput.parseArgumentsFromCli(abi, {
16 | bar: 'Hello, World!',
17 | baz: '42'
18 | });
19 | eq_(arg1, 'Hello, World!');
20 | eq_(arg2, 42n);
21 | eq_(typeof arg2, 'bigint');
22 |
23 |
24 | var [ arg1, arg2 ] = await $abiInput.parseArgumentsFromCli(abi, {
25 | 'arg:0': 'Hello, World',
26 | 'arg:1': '2.3^3'
27 | });
28 | eq_(arg1, 'Hello, World');
29 | eq_(arg2, 2300n);
30 | eq_(typeof arg2, 'bigint');
31 | }
32 | });
33 | },
34 |
35 | async 'should parse struct' () {
36 | let abi = $abiParser.parseMethod('foo((string name, uint age) user)');
37 |
38 | return UTest({
39 | async 'as json' () {
40 | var [ arg1 ] = await $abiInput.parseArgumentsFromCli(abi, {
41 | user: '{"name": "John Doe", "age": 30 }',
42 | });
43 | deepEq_(arg1, {
44 | name: 'John Doe', age: 30n
45 | });
46 | },
47 | async 'as args' () {
48 | var [ arg1 ] = await $abiInput.parseArgumentsFromCli(abi, {
49 | 'arg0.name': 'John Doe',
50 | 'arg0.age': '30'
51 | });
52 | deepEq_(arg1, {
53 | name: 'John Doe', age: 30n
54 | });
55 | },
56 | async 'as named arg' () {
57 | var [ arg1 ] = await $abiInput.parseArgumentsFromCli(abi, {
58 | 'user.name': 'John Doe',
59 | 'user.age': '30'
60 | });
61 | deepEq_(arg1, {
62 | name: 'John Doe', age: 30n
63 | });
64 | },
65 | async 'load from file' () {
66 | var [ arg1 ] = await $abiInput.parseArgumentsFromCli(abi, {
67 | 'arg0': 'load(./test/fixtures/cli/user.json)',
68 | });
69 | deepEq_(arg1, {
70 | name: 'John Doe', age: 30n
71 | });
72 | },
73 | async 'load from file with getter' () {
74 | let abi = $abiParser.parseMethod('foo(string name)');
75 | var [ arg1 ] = await $abiInput.parseArgumentsFromCli(abi, {
76 | 'arg0': 'load(./test/fixtures/cli/user.json).name',
77 | });
78 | deepEq_(arg1, 'John Doe');
79 | }
80 | });
81 | },
82 |
83 | async 'should parse array' () {
84 | let abi = $abiParser.parseMethod('foo(string[] memory bar, uint256[] memory baz)');
85 |
86 | return UTest({
87 | async 'simple' () {
88 | var [ arg1, arg2 ] = await $abiInput.parseArgumentsFromCli(abi, {
89 | bar: '["foo","bar"]',
90 | baz: '[1,2]'
91 | });
92 | deepEq_(arg1, ["foo","bar"]);
93 | deepEq_(arg2, [1n, 2n]);
94 | },
95 | async 'as struct' () {
96 | let abi = $abiParser.parseMethod('foo((string name, uint age)[] users)');
97 |
98 | var [ arg1 ] = await $abiInput.parseArgumentsFromCli(abi, {
99 | users: '[ {"name": "John Doe", "age": 30} ]',
100 | });
101 | deepEq_(arg1, [ {"name": "John Doe", "age": 30n} ]);
102 | },
103 | });
104 | },
105 |
106 | async 'should ask' () {
107 | let params = { name: 'John Doe', age: 30n };
108 | let provider = {
109 | get (abi: TAbiItem) {
110 | return params[abi.name]
111 | }
112 | };
113 | let abi = $abiParser.parseMethod('foo(string name, uint age)');
114 | var args = await $abiInput.parseArgumentsFromCli(abi, {
115 |
116 | }, { argumentProvider: provider });
117 | deepEq_(args, [ params.name, params.age ]);
118 | }
119 | })
120 |
--------------------------------------------------------------------------------
/src/commands/list/CNs.ts:
--------------------------------------------------------------------------------
1 | import { $cli } from '@core/utils/$cli';
2 | import { $console } from '@core/utils/$console';
3 | import { File, env } from 'atma-io';
4 | import { ICommand } from '../ICommand';
5 | import { Parameters } from '@core/utils/Parameters';
6 | import { App } from '@core/app/App';
7 | import { NameService } from '@dequanto/ns/NameService';
8 | import { $require } from '@dequanto/utils/$require';
9 | import { $logger } from '@dequanto/utils/$logger';
10 | import { $ns } from '@dequanto/ns/utils/$ns';
11 | import { Web3Client } from '@dequanto/clients/Web3Client';
12 | import { $is } from '@dequanto/utils/$is';
13 | import { $os } from '@core/utils/$os';
14 |
15 | export function CNs() {
16 | return {
17 | command: 'ns',
18 | description: [
19 | 'NameService utils: Supports ENS, SpaceID, UnstoppableDomains'
20 | ],
21 | subcommands: [
22 | {
23 | command: 'view',
24 | description: [
25 | 'Load a record'
26 | ],
27 | arguments: [
28 | {
29 | description: `Domain with optional path to get as RECORD. e.g. example.eth/foo`,
30 | required: true,
31 | }
32 | ],
33 | params: {
34 | ...Parameters.chain({ required: false })
35 | },
36 | async process(args: string[], params: any, app: App) {
37 | let [uri] = args;
38 | let client = params.chain ? app.chain.client : void 0;
39 | let { address, content } = await Resolver.get(uri, client);
40 |
41 | $logger.table([
42 | ['Address', address],
43 | ['Content', content]
44 | ]);
45 | }
46 | },
47 | {
48 | command: 'go',
49 | description: [
50 | 'Load a record and navigate to ipfs, https, etc.'
51 | ],
52 | arguments: [
53 | {
54 | description: `Domain with optional path to get as RECORD. e.g. example.eth/foo`,
55 | required: true,
56 | }
57 | ],
58 | params: {
59 | ...Parameters.chain({ required: false })
60 | },
61 | async process(args: string[], params: any, app: App) {
62 | let [uri] = args;
63 | let client = params.chain ? app.chain.client : void 0;
64 | let { address, content } = await Resolver.get(uri, client);
65 |
66 | $logger.table([
67 | ['Address', address],
68 | ['Content', content]
69 | ]);
70 |
71 | let nav = content;
72 | if ($is.empty(nav)) {
73 | $logger.error(`ContentHash or Record of ${uri} ${nav} is empty`);
74 | return;
75 | }
76 | if (nav.startsWith('ipfs://')) {
77 | let cid = nav.replace('ipfs://', '');
78 | let path = `https://cloudflare-ipfs.com/ipfs/${cid}/`;
79 | $console.log(`Opening cyan>`);
80 | await $os.open(path);
81 | return;
82 | }
83 | if (/^https?/.test(nav)) {
84 | $console.log(`Opening cyan>`);
85 | await $os.open(nav);
86 | return;
87 | }
88 | $console.log(`Go by clicking on ${nav}`);
89 | }
90 | },
91 | ],
92 | async process() {
93 | throw new Error(`Subcommand is not set`);
94 | }
95 | };
96 | }
97 |
98 | namespace Resolver {
99 | export async function get(uri: string, client?: Web3Client) {
100 | let ns = new NameService(client);
101 | let supports = ns.supports(uri);
102 | $require.True(supports, `${uri} is not supported by any of ${ns.providers.map(p => p.constructor.name).join(', ')}`);
103 |
104 | $logger.toast(`Loading address`);
105 | let address = await ns.getAddress($ns.getRoot(uri));
106 | $logger.toast(`Loading content`);
107 | let content = await ns.getContent(uri);
108 |
109 | return { address, content }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/typings/globals/assertion/index.d.ts:
--------------------------------------------------------------------------------
1 | // Generated by typings
2 | // Source: https://raw.githubusercontent.com/atmajs/assertion/master/types/assertion.d.ts
3 | declare module "assertion" {
4 | export = assert;
5 | }
6 |
7 | declare var assert: assertion.IAssert
8 |
9 | declare var eq_: assertion.equal
10 | declare var notEq_: assertion.notEqual
11 | declare var lt_: assertion.lessThan
12 | declare var lte_: assertion.lessThanOrEqual
13 | declare var gt_: assertion.greaterThan
14 | declare var gte_: assertion.greaterThanOrEqual
15 | declare var deepEq_: assertion.deepEqual
16 | declare var notDeepEq_: assertion.notDeepEqual
17 | declare var has_: assertion.has
18 | declare var hasNot_: assertion.hasNot
19 | declare var is_: assertion.is
20 | declare var isNot_: assertion.isNot
21 |
22 |
23 | declare namespace assertion {
24 | interface IAssert {
25 | (expression: boolean, message: string): void
26 | equal: equal
27 | notEqual: notEqual
28 | strictEqual: strictEqual
29 | notStrictEqual: notStrictEqual
30 | throws: throws
31 | notThrows: notThrows
32 | ifError: ifError
33 | lessThan: lessThan
34 | lessThanOrEqual: lessThanOrEqual
35 |
36 | greaterThan: greaterThan
37 | greaterThanOrEqual: greaterThanOrEqual
38 | deepEqual: deepEqual
39 | notDeepEqual: notDeepEqual
40 | has: has
41 | hasNot: hasNot
42 | is: is
43 |
44 | on: on
45 |
46 | /**
47 | * string: Await Name for better logging
48 | * number: expectation count
49 | * object: binded content
50 | * function: wrap any function
51 | */
52 | await (
53 | arg1?: string | number | Function | object,
54 | arg2?: string | number | Function | object,
55 | arg3?: string | number | Function | object,
56 | arg4?: string | number | Function | object,
57 | ): Function
58 |
59 |
60 | /**
61 | * string: Await Name for better logging
62 | * number: expectation count, default is `1`
63 | * object: binded content
64 | * function: wrap any function
65 | */
66 | avoid (
67 | arg1?: string | number | Function | object,
68 | arg2?: string | number | Function | object,
69 | arg3?: string | number | Function | object,
70 | arg4?: string | number | Function | object,
71 | );
72 | }
73 | interface equal {
74 | (a: any, b: any, message?: string)
75 | }
76 | interface notEqual {
77 | (a: any, b: any, message?: string)
78 | }
79 | interface strictEqual {
80 | (a: any, b: any, message?: string)
81 | }
82 | interface notStrictEqual {
83 | (a: any, b: any, message?: string)
84 | }
85 | interface throws {
86 | (a: Function, message?: string): Error
87 | }
88 | interface notThrows {
89 | (a: Function, message?: string): Error
90 | }
91 | interface ifError {
92 | (a: any, message?: string)
93 | }
94 | interface lessThan {
95 | (a: any, b: any, message?: string)
96 | }
97 | interface lessThanOrEqual {
98 | (a: any, b: any, message?: string)
99 | }
100 | interface greaterThan {
101 | (a: any, b: any, message?: string)
102 | }
103 | interface greaterThanOrEqual {
104 | (a: any, b: any, message?: string)
105 | }
106 | interface deepEqual {
107 | (a: any, b: any, message?: string)
108 | }
109 | interface notDeepEqual {
110 | (a: any, b: any, message?: string)
111 | }
112 | interface has {
113 | (a: any, b: any, message?: string)
114 | }
115 | interface is {
116 | (a: any, b:
117 | 'String' |
118 | 'Number' |
119 | 'Null' |
120 | 'Undefined' |
121 | 'Function' |
122 | 'RegExp' |
123 | 'Date' |
124 | 'Object' |
125 | 'CustomEvent' |
126 | null |
127 | any, message?: string)
128 | }
129 | interface isNot {
130 | (a: any, b:
131 | 'String' |
132 | 'Number' |
133 | 'Null' |
134 | 'Undefined' |
135 | 'Function' |
136 | 'RegExp' |
137 | 'Date' |
138 | 'Object' |
139 | 'CustomEvent' |
140 | null |
141 | any, message?: string)
142 | }
143 | interface hasNot {
144 | (a: any, b: any, message?: string)
145 | }
146 |
147 |
148 | /** Notice: when `fail` callback is defined the assertion doesn`t throw any error */
149 | interface on {
150 | (event: 'start' | 'fail' | 'success', callback: (error?: AssertionError) => void)
151 | }
152 |
153 |
154 | interface AssertionError extends Error {
155 | actual: any
156 | expected: any
157 | operator: string
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/src/utils/$validate.ts:
--------------------------------------------------------------------------------
1 | import { ICommand } from '@core/commands/ICommand';
2 | import { TPlatform } from '@dequanto/models/TPlatform';
3 | import { $config } from '@dequanto/utils/$config';
4 | import { $require } from '@dequanto/utils/$require';
5 | import { $console } from './$console';
6 | import { IBlockchainExplorerConfig } from '@dequanto/explorer/BlockchainExplorer';
7 |
8 | export namespace $validate {
9 |
10 | export function platforms () {
11 | let web3Config = $config.get('web3');
12 | $require.notNull(web3Config, `Configuration was not loaded, or is invalid. "web3" field not found`);
13 |
14 | let keys = Object.keys(web3Config);
15 | return keys;
16 | }
17 |
18 | export function platform (platform: TPlatform, message?: string) {
19 | $require.notNull(platform, message);
20 | $require.oneOf(platform, platforms(), message);
21 | }
22 |
23 | export namespace config {
24 |
25 | export function rpcNodes (platform: TPlatform) {
26 |
27 | let endpoints = $config.get(`web3.${platform}.endpoints`);
28 | if (Array.isArray(endpoints) && endpoints.length > 0 && /^(https?|wss?)/.test(endpoints[0].url)) {
29 | return;
30 | }
31 |
32 | let example = {
33 | web3: {
34 | [platform]: {
35 | endpoints: [
36 | {
37 | url: 'https://rpc-node.foo'
38 | },
39 | {
40 | url: 'wss://rpc-node.foo'
41 | }
42 | ]
43 | }
44 | }
45 | };
46 | let msg = `${platform} nodes not configured. Run "0xweb config -e" and set node urls in web3.${platform}.endpoints`;
47 | console.error(msg);
48 | console.error('Current: ', endpoints, 'Expected: ');
49 | console.dir(example, { depth: null });
50 | throw new Error(msg);
51 | }
52 |
53 | export function blockchainExplorer (platform: TPlatform) {
54 | if (platform === 'hardhat') {
55 | return;
56 | }
57 | let scan = $config.get (`blockchainExplorer.${platform}`);
58 | if (scan?.host || scan?.api) {
59 | return;
60 | }
61 |
62 | let example = {
63 | blockchainExplorer: {
64 | [platform]: {
65 | api: 'https://api.foo.some/api',
66 | key: 'YOUR_API_KEY_OPTIONAL'
67 | }
68 | }
69 | };
70 | let msg = `${platform} blockchain explorer not configured. Run "0xweb config -e" and set node host and key in web3.${platform}`;
71 | console.error(msg);
72 | console.error('Current: ', scan, 'Expected: ');
73 | console.dir(example, { depth: null });
74 | throw new Error(msg);
75 | }
76 | }
77 |
78 |
79 | export function args (command: ICommand, args: string[], options?: { inputFailed?: boolean }): Promise {
80 | let definition = command.arguments;
81 | if (definition == null || definition.length === 0) {
82 | return;
83 | }
84 | for (let i = 0; i < definition.length; i++) {
85 | let def = definition[i];
86 | if (def.required !== true) {
87 | return;
88 | }
89 |
90 | let val = args[i];
91 | if (val == null) {
92 | let str = `${def.name ?? i}`;
93 | if (def.description) {
94 | str += ` (${def.description})`
95 | }
96 |
97 | throw new Error(`Argument ${str} is required`);
98 | }
99 | }
100 | }
101 |
102 | export function params (command: ICommand, paramsDef: ICommand['params'], params) {
103 | for (let key in paramsDef) {
104 | let def = paramsDef[key];
105 | let val = params[key];
106 |
107 | if (Array.isArray(def.oneOf)) {
108 | $require.oneOf(val, def.oneOf);
109 | }
110 | if (def.validate) {
111 | try {
112 | def.validate(val);
113 | } catch (error) {
114 | $console.log(`Parameter '${def.key}' is invalid:`);
115 | $console.table([
116 | ['Info', def.description ],
117 | ]);
118 |
119 | throw error;
120 | }
121 | }
122 | }
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/commands/list/CConfig.ts:
--------------------------------------------------------------------------------
1 | import { ICommand } from '../ICommand';
2 | import { File } from 'atma-io';
3 | import { obj_getProperty, obj_setProperty } from 'atma-utils';
4 | import { App } from '@core/app/App';
5 | import { $console } from '@core/utils/$console';
6 | import { $promise } from '@dequanto/utils/$promise';
7 | import { $os } from '@core/utils/$os';
8 | import { $require } from '@dequanto/utils/$require';
9 |
10 |
11 | export function CConfig() {
12 | return {
13 |
14 | command: 'config',
15 |
16 | description: [
17 | 'View and edit web3 configuration'
18 | ],
19 | params: {
20 | '-v, --view': {
21 | description: 'Print current configuration. ',
22 | },
23 | '-e, --edit': {
24 | description: 'Open/create the configuration file in AppData or CWD folder to edit',
25 | },
26 | '--local': {
27 | description: 'Edit local config',
28 | },
29 | '--global': {
30 | description: 'Edit global config',
31 | },
32 | '--set': {
33 | description: 'Set config value, e.g. --set settings.target=cjs',
34 | type:'string',
35 | },
36 | '--get': {
37 | description: 'Get config value, e.g. --set settings.target',
38 | type:'string',
39 | },
40 | },
41 |
42 | async process(args: string[], params, app: App) {
43 |
44 | if (params.edit) {
45 |
46 | File.registerExtensions({
47 | 'yml': [
48 | "atma-io-middleware-yml:read",
49 | "atma-io-middleware-yml:write"
50 | ]
51 | }, false);
52 |
53 | let source = app.config.$sources.array.find(x =>x.data.name === 'main');
54 | $require.notNull(source, `Main config source not found`);
55 | let path = source.data.path;
56 |
57 |
58 | if (await File.existsAsync(path) === false) {
59 | let json = {};
60 | $console.log(`Create bold<${path}>`);
61 | await File.writeAsync(path, json);
62 | }
63 | let sysPath = new File(path).uri.toLocalFile();
64 | let github = `https://github.com/0xweb-org/dequanto/blob/master/configs/dequanto.yml`;
65 | $console.log(`Defaults yellow>`);
66 | $console.log(`Open cyan>`);
67 | await $os.open(sysPath);
68 | await $promise.wait(500);
69 | return;
70 | }
71 |
72 | if (params.set) {
73 | let json = {};
74 | let source = app.config.$sources.array.find(x =>x.data.name === 'main');
75 | $require.notNull(source, `Main config source not found`);
76 | let values = params.set.split(';');
77 | values.forEach(str => {
78 | let [ key, value ] = str.split('=');
79 | if (value === 'true') {
80 | obj_setProperty(json, key, true);
81 | return;
82 | }
83 | if (value === 'false') {
84 | obj_setProperty(json, key, false);
85 | return;
86 | }
87 | if (/^[\d\.]+$/.test(value)) {
88 | obj_setProperty(json, key, Number(value));
89 | return;
90 | }
91 | obj_setProperty(json, key, value);
92 | });
93 | $console.log(`Set the configuration at ${source.data.path} : ` + JSON.stringify(json, null, 2));
94 |
95 | await source.write(json, /*deepExtend*/ true);
96 | return;
97 | }
98 |
99 | if (params.get != null) {
100 | let json = app.config.toJSON();
101 | if (params.get?.length > 0) {
102 | json = obj_getProperty(json, params.get);
103 | }
104 | $console.result(json);
105 | return;
106 | }
107 | if (params.view) {
108 | $console.log('Current configuration:');
109 | $console.result(getJson());
110 | return;
111 | }
112 |
113 | function getJson() {
114 | let json = app.config.toJSON();
115 | delete json.e;
116 | delete json.edit;
117 | delete json.v;
118 | delete json.view;
119 | return json;
120 | }
121 | }
122 | };
123 | }
124 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | ## [`0xweb`](https://0xweb.org) - Contract package manager and CLI Web3 Toolkit
2 |
3 |
4 |
5 |
6 |
7 |
8 | ----
9 | [](https://0xweb.org)
10 | [](https://docs.0xweb.org)
11 | [](http://badge.fury.io/js/0xweb)
12 | [](https://circleci.com/gh/0xweb-org/0xweb)
13 |
14 | | | |
15 | |--|--|
16 | |[Demo: Backend](https://github.com/0xweb-org/examples-backend) | [](https://dl.circleci.com/status-badge/redirect/gh/0xweb-org/examples-backend/tree/master) |
17 | |[Demo: Storage](https://github.com/0xweb-org/examples-storage) | [](https://dl.circleci.com/status-badge/redirect/gh/0xweb-org/examples-storage/tree/master) |
18 | |[Demo: Hardhat](https://github.com/0xweb-org/examples-hardhat) | [](https://dl.circleci.com/status-badge/redirect/gh/0xweb-org/examples-hardhat/tree/master) |
19 | |[Demo: Price Loader](https://github.com/0xweb-org/examples-price) | [](https://dl.circleci.com/status-badge/redirect/gh/0xweb-org/examples-price/tree/master) |
20 |
21 |
22 |
23 | - Generate TypeScript or ES6 classes for contracts fetched from Etherscan and Co.
24 | - CLI commands to query the contracts or to submit transactions
25 | - RESTful API for the installed contracts
26 | - Deploy and track deployed contracts with ease
27 |
28 | > [Dequanto library 📦](https://github.com/0xweb-org/dequanto) is used for the wrapped classes
29 |
30 | Here the example of generated classes: [0xweb-org/0xweb-sample 🔗](https://github.com/0xweb-org/0xweb-sample)
31 |
32 |
33 | ### Install
34 |
35 | ```bash
36 | $ npm i 0xweb -g
37 |
38 | # Boostrap dequanto library in cwd
39 | $ 0xweb init
40 |
41 | # Download sources/ABI and generate TS classes
42 | $ 0xweb install 0x5f4ec3df9cbd43714fe2740f5e3616155c5b8419 --name chainlink/oracle-eth
43 | ```
44 |
45 | > Use the `--hardhat` flag, if you want to develop|compile|deploy|test contracts: `0xweb init --hardhat`
46 |
47 | #### API Usage
48 |
49 | > Use autogenerated TypeScript classes for much safer and faster backend implementation
50 |
51 | ```ts
52 | import { ChainlinkOracleEth } from '0xc/eth/chainlink/oracle-eth/oracle-eth';
53 | import { Config } from 'dequanto/config/Config';
54 | import { $bigint } from 'dequanto/utils/$bigint';
55 |
56 | await Config.fetch();
57 |
58 | let oracle = new ChainlinkOracleEth();
59 | let decimals = await oracle.decimals();
60 | let price = await oracle.latestAnswer();
61 |
62 | console.log(`ETH Price`, $bigint.toEther(price, decimals));
63 | ```
64 |
65 | ### CLI Usage
66 |
67 | > **READ** and **WRITE** to installed contracts directly from the command line
68 |
69 | ```bash
70 | $ 0xweb contract chainlink/oracle-eth latestAnswer
71 | ```
72 |
73 | #### WEB Usage
74 |
75 | The package has the built-in web interface to make the blockchain analyze and interaction simpler.
76 |
77 | 1. If you initialize the Transaction via CMD, you may want to sign the transaction with your browser wallet. For this, 0xweb spins up the HTTP server and redirects your browser to visit local page where the details of the transaction being shown, and you can sign and submit the transaction.
78 |
79 | 2. Launch local server to view the validated contract in UI interface to interact with.
80 |
81 | ```bash
82 | 0xweb server start
83 | ```
84 |
85 | ## Config
86 |
87 | > ❗❣️❗ We include our default KEYs for etherscan/co and infura. They are rate-limited. Please, create and insert your keys. Thank you!
88 |
89 | ```bash
90 | $ 0xweb config --edit
91 |
92 | ## optionally, you can provide the Nodes Endpoint with `--endpoint` flag
93 | $ 0xweb COMMAND --endpoint https://my-node-url-here
94 | ```
95 |
96 | ## [Commands overview 🔗](https://docs.0xweb.org/cli/commands-overview)
97 |
98 | ## Various Blockchain tools
99 |
100 | > Get the commands overview
101 |
102 | ```bash
103 | $ 0xweb --help
104 | $ 0xweb install --help
105 | ```
106 |
107 | ### `block`
108 |
109 | 1. Get current block info
110 |
111 | ```bash
112 | $ 0xweb block get latest
113 | ```
114 |
115 | ### `token`
116 |
117 | 1. Get Token Price
118 |
119 | ```bash
120 | $ 0xweb token price WETH
121 | ```
122 |
123 | ### `accounts`
124 |
125 | **🔐 Wallet** feature allows to store accounts in encrypted local storage. We use local machine KEY and provided PIN in arguments or environment to create cryptographically strong secrets 🔑 for account encryption.
126 |
127 | When calling contracts `WRITE` methods, you should first add an account to the wallet, and then use PIN to unlock the storage
128 |
129 | ```bash
130 | $ 0xweb account add --name foo --key the_private_key --pin foobar
131 | $ 0xweb token transfer USDC --from foo --to 0x123456 --amount 20 --pin foobar
132 | ```
133 |
134 | 🏁
135 |
136 | ----
137 | ©️ MIT License.
138 |
--------------------------------------------------------------------------------
/src/commands/list/CInstall.ts:
--------------------------------------------------------------------------------
1 | import di from 'a-di';
2 | import { env } from 'atma-io';
3 | import { Generator } from '@dequanto/gen/Generator';
4 | import { ICommand } from '../ICommand';
5 | import { TPlatform } from '@dequanto/models/TPlatform';
6 | import { class_Uri } from 'atma-utils';
7 | import { Parameters } from '@core/utils/Parameters';
8 | import { PackageService } from '@core/services/PackageService';
9 | import { $address } from '@dequanto/utils/$address';
10 | import { $is } from '@dequanto/utils/$is';
11 | import { $require } from '@dequanto/utils/$require';
12 | import { $validate } from '@core/utils/$validate';
13 | import { $platform } from '@dequanto/utils/$platform';
14 | import { TEth } from '@dequanto/models/TEth';
15 |
16 | export function CInstall() {
17 | return {
18 | command: 'i, install',
19 |
20 | description: [
21 | `Download contracts ABI and generate the TS class for it.`,
22 | `Supported chains: ${$validate.platforms().join(', ')}`,
23 | ],
24 | arguments: [
25 | {
26 | description: 'Contract address or path',
27 | required: true
28 | }
29 | ],
30 | params: {
31 | '-n, --name': {
32 | description: 'The class name.',
33 | required: true
34 | },
35 | '--imp, --implementation': {
36 | description: 'We can detect proxies by standard proxy implementations, in some edge cases you can set the implementation address manually.'
37 | },
38 | '--source': {
39 | description: 'Optional, the solidity source code'
40 | },
41 | '--contract-name': {
42 | description: 'Optionally the contract name to extract from the source. Otherwise default is taken'
43 | },
44 | '-g, --global': {
45 | description: 'Installs the contract globally, to be available via "0xweb" cli command from any CWD.',
46 | type: 'boolean',
47 | },
48 | ...Parameters.chain(),
49 | '-o, --output': {
50 | description: 'Output directory. Default: ./0xc/'
51 | },
52 | '--save-sources': {
53 | description: 'Optionally disables saving the solidity source code to the output directory.',
54 | type: 'boolean',
55 | default: true
56 | },
57 | '--target': {
58 | description: 'The output source: js | mjs | ts. Default is configured in "settings.generate.target"',
59 | type:'string'
60 | }
61 | },
62 |
63 | async process(args: string[], params: IInstallParams, app) {
64 | let platform: TPlatform = params.chain as TPlatform;
65 | let [ addressOrPath ] = args;
66 | if (/^\w+:0x/.test(addressOrPath)) {
67 | // eth:0x...
68 | let i = addressOrPath.indexOf(':');
69 | platform = addressOrPath.substring(0, i) as any;
70 | addressOrPath = addressOrPath.substring(i + 1);
71 | }
72 | let isByAddress = /0x[\da-f]+/i.test(addressOrPath);
73 | let address = isByAddress ? addressOrPath as TEth.Address : null;
74 | let sourcePath = isByAddress ? params.source : addressOrPath;
75 |
76 | $require.notNull(params.name, `--name should be set`);
77 | $validate.platform(platform, `Chain not set. Use as prefix "eth:0x.." or flag "--chain eth"`);
78 | $validate.config.blockchainExplorer(platform);
79 |
80 | if (params.global) {
81 | params.output = env.appdataDir.combine('.dequanto/0xc/').toDir();
82 | }
83 |
84 | let output = class_Uri.combine(params.output ?? `./0xc/`, $platform.toPath(platform));
85 |
86 | let generator = new Generator({
87 | name: params.name,
88 | contractName: params.contractName,
89 | target: params.target as any,
90 | platform,
91 | source: {
92 | abi: address,
93 | path: sourcePath,
94 | },
95 | defaultAddress: address,
96 | implementation: params.implementation,
97 | output,
98 | saveAbi: true,
99 | saveSources: params.saveSources ?? true,
100 | });
101 | let { main, implementation, contractName } = await generator.generate();
102 |
103 | let packageService = di.resolve(PackageService, app?.chain);
104 | let implementationAddress = $is.Address(implementation) && $address.eq(addressOrPath, implementation) === false ? implementation : void 0;
105 | await packageService.savePackage({
106 | platform,
107 | address: address,
108 | implementation: implementationAddress,
109 | name: params.name,
110 | contractName: contractName,
111 | main,
112 | source: isByAddress ? {
113 | platform: platform,
114 | address: implementationAddress ?? address,
115 | } : {
116 | path: sourcePath
117 | }
118 | }, { global: params.global != null });
119 | return { main };
120 | }
121 | };
122 | }
123 |
124 | interface IInstallParams {
125 | name: string
126 | contractName: string
127 | implementation: string
128 | target: 'js' |'mjs' | 'ts'
129 | chain: string
130 | output: string
131 | source: string
132 | global: boolean
133 | saveSources: boolean
134 | }
135 |
--------------------------------------------------------------------------------
/src/services/AccountsService.ts:
--------------------------------------------------------------------------------
1 | import { $cli } from '@core/utils/$cli';
2 | import { $console } from '@core/utils/$console';
3 | import { EoAccount, Erc4337Account, SafeAccount } from '@dequanto/models/TAccount';
4 | import { $buffer } from '@dequanto/utils/$buffer';
5 | import { $crypto } from '@dequanto/utils/$crypto';
6 | import { $is } from '@dequanto/utils/$is';
7 | import { $machine } from '@dequanto/utils/$machine';
8 | import { $require } from '@dequanto/utils/$require';
9 | import { $sig } from '@dequanto/utils/$sig';
10 | import type appcfg from 'appcfg';
11 | import { env, File } from 'atma-io';
12 |
13 | export class AccountsService {
14 | constructor (public config: appcfg) {
15 |
16 | }
17 | static DEFAULTS_PATH_LOCAL = `./0xc/config/account.json`;
18 | static DEFAULTS_PATH_GLOBAL = `%APPDATA%/.dequanto/account.json`;
19 |
20 | static async getDefaults (params: Record) {
21 | let isLocal = $cli.isLocal(params);
22 | let p = $cli.getParamValue('--pin, -p', params);
23 | if (p == null) {
24 | return null;
25 | }
26 | let path = AccountsService.getDefaultsDir({ isLocal });
27 | if (await File.existsAsync(path) === false) {
28 | return null;
29 | }
30 | let cipher = await File.readAsync (path, {
31 | skipHooks: true,
32 | encoding: 'utf8'
33 | });
34 | let secret = await $machine.id();
35 | let buffer = await $buffer.fromHex(cipher);
36 | let json = await $crypto.decrypt(buffer, { secret, encoding: 'utf8' });
37 | return JSON.parse(json);
38 | }
39 |
40 | static async saveAsDefaults (accountName: string, params: Record, config: appcfg) {
41 | $require.notNull(accountName, `At least account name is required`);
42 | let isLocal = $cli.isLocal(params);
43 | let account = {
44 | 'config-accounts': $cli.getParamValue('config-accounts', params) || void 0,
45 | 'session-account': accountName
46 | } as {
47 | 'config-accounts': string
48 | 'session-account': string
49 | };
50 | let path = AccountsService.getDefaultsDir({ isLocal });
51 | let secret = await $machine.id();
52 | let cipher = await $crypto.encrypt(JSON.stringify(account), { secret, encoding: 'hex' });
53 | await File.writeAsync(path, cipher, { skipHooks: true });
54 | }
55 |
56 | static getDefaultsDir (opts: { isLocal: boolean }) {
57 | if (opts.isLocal) {
58 | return AccountsService.DEFAULTS_PATH_LOCAL;
59 | }
60 | return AccountsService.DEFAULTS_PATH_GLOBAL.replace('%APPDATA%', env.appdataDir)
61 | }
62 |
63 | async add (params: EoAccount | SafeAccount | Erc4337Account) {
64 | let accounts = this.getAccounts();
65 | if (accounts.find(x => x.name === params.name)) {
66 | console.warn(`Account ${params.name} already exists`);
67 | } else {
68 | accounts.push(params);
69 | await this.save(accounts);
70 | }
71 | return accounts;
72 | }
73 |
74 | async remove (name: string) {
75 | let accounts = this.getAccounts();
76 | let index = accounts.findIndex(x => x.name === name)
77 | if (index === -1) {
78 | console.warn(`Account ${name} not found`);
79 | } else {
80 | accounts.splice(index, 1);
81 | await this.save(accounts);
82 | }
83 | return accounts;
84 | }
85 |
86 | async list (): Promise<(EoAccount | SafeAccount | Erc4337Account)[]> {
87 | let source = this.getConfig();
88 | let accounts = source.config?.accounts ?? [];
89 | return accounts;
90 | }
91 |
92 | async get(name: string): Promise<(EoAccount | SafeAccount | Erc4337Account)>
93 | async get(key: string): Promise<(EoAccount | SafeAccount | Erc4337Account)>
94 | async get(mix: string): Promise<(EoAccount | SafeAccount | Erc4337Account)> {
95 | if ($is.Hex(mix) && mix.length > 64) {
96 | return {
97 | address: await $sig.$account.getAddressFromKey(mix),
98 | key: mix
99 | };
100 | }
101 | let name = mix;
102 | let accounts = await this.list();
103 | let account = this.getAccount(name);
104 | if (account == null) {
105 | $console.log('Available accounts:');
106 | $console.log(accounts.map(x => x.name).join('\n'));
107 | throw new Error(`Account ${name} not found.`);
108 | }
109 | return account;
110 | }
111 | async create (name: string): Promise {
112 | let current = await this.getAccount(name);
113 | if (current != null) {
114 | $console.log(`Account green> already exists`);
115 | return null;
116 | }
117 | let account = $sig.$account.generate({ name, platform: 'eth' });
118 | await this.add(account);
119 | return account;
120 | }
121 |
122 |
123 | private getAccounts () {
124 | let source = this.getConfig();
125 | let accounts = source.config?.accounts ?? [];
126 | return accounts;
127 | }
128 | private getConfig () {
129 | let source = this.config.$sources.array.find(x =>x.data.name === 'accounts');
130 | if (source == null) {
131 | throw new Error(`Configuration source for accounts not found`);
132 | }
133 | if (source.config == null) {
134 | source.config = {};
135 | }
136 | return source;
137 | }
138 | private async getAccount (name: string): Promise {
139 | let accounts = await this.list();
140 | let account = accounts.find(x => x.name === name);
141 | return account;
142 | }
143 | private async save (accounts) {
144 | let source = this.getConfig();
145 | await source.write({ accounts }, false);
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/test/install.spec.ts:
--------------------------------------------------------------------------------
1 | import { run } from 'shellbee'
2 | import { Directory, env, File } from 'atma-io'
3 |
4 | UTest({
5 | $config: {
6 | timeout: 60 * 1000
7 | },
8 | async 'install contract (local)' () {
9 | let cwd = './test/bin/test-install/';
10 | let _basePath = `${cwd}0xc/eth/foo/DisperseContract/`;
11 | let _mainPath = `${_basePath}DisperseContract.ts`;
12 | return UTest({
13 | async $before () {
14 |
15 | if (await Directory.existsAsync(cwd)) {
16 | await Directory.removeAsync(cwd);
17 | }
18 |
19 | await Directory.ensureAsync(cwd);
20 |
21 | let { stdout } = await run({
22 | command: `node ../../../index.js i 0xd152f549545093347a162dce210e7293f1452150 --name foo/DisperseContract --chain eth`,
23 | cwd: cwd
24 | });
25 | },
26 | async 'check paths' () {
27 | let content = await File.readAsync(_mainPath, { skipHooks: true });
28 | has_(content, 'class FooDisperseContract extends ContractBase');
29 |
30 | let packagePath = `${cwd}0xweb.json`;
31 | let json = await File.readAsync(packagePath);
32 |
33 | has_(json.contracts.eth['0xd152f549545093347a162dce210e7293f1452150'].main, 'DisperseContract.ts');
34 | },
35 | async 'check abi' () {
36 | let { stdout } = await run({
37 | command: `node ../../../index.js c abi foo/DisperseContract --color none`,
38 | cwd
39 | });
40 | has_(stdout.join(''), `disperseTokenSimple(address token, address[] recipients, uint256[] values)`);
41 | },
42 | async 'restore' () {
43 | await Directory.removeAsync(_basePath);
44 |
45 | eq_(await File.existsAsync(_mainPath), false);
46 |
47 | let { stdout } = await run({
48 | command: `node ../../../index.js restore`,
49 | cwd
50 | });
51 | eq_(await File.existsAsync(_mainPath), true);
52 | }
53 | });
54 | },
55 | async 'install contract (global)' () {
56 |
57 | let _global = env.appdataDir.combine('.dequanto/').toDir();
58 | let _basePath = `${_global}/0xc/eth/ChainLinkEth/`;
59 | return UTest({
60 | async $before () {
61 | if (await Directory.existsAsync(_basePath)) {
62 | await Directory.removeAsync(_basePath);
63 | }
64 | let { stdout } = await run(`node index.js i 0x5f4ec3df9cbd43714fe2740f5e3616155c5b8419 --name ChainLinkEth --chain eth --global`);
65 | },
66 | async 'check global paths' () {
67 | let content = await File.readAsync(`${_basePath}/ChainLinkEth.ts`);
68 | has_(content, 'class ChainLinkEth extends ContractBase');
69 |
70 | let json = await File.readAsync(`${_global}/0xweb.json`);
71 |
72 | has_(json.contracts.eth['0x5f4ec3df9cbd43714fe2740f5e3616155c5b8419'].main, 'ChainLinkEth.ts');
73 | },
74 | async 'check global abi' () {
75 | let { stdout } = await run(`node index.js c abi ChainLinkEth --color none`);
76 | has_(stdout.join(''), `getRoundData`);
77 | }
78 | })
79 | },
80 | async 'install contract by solidity source' () {
81 | let _basePath = './0xc/eth/ContractBySource/';
82 | return UTest({
83 | async $before () {
84 |
85 | if (await Directory.existsAsync(_basePath)) {
86 | await Directory.removeAsync(_basePath);
87 | }
88 | let { stdout } = await run(`node index.js i ./test/fixtures/install/ContractBySource.sol --name ContractBySource --chain eth`);
89 | },
90 | async 'check paths' () {
91 | let content = await File.readAsync(`${_basePath}/ContractBySource.ts`, { skipHooks: true });
92 | has_(content, 'class ContractBySource extends ContractBase');
93 |
94 | let packagePath = '0xweb.json';
95 | let json = await File.readAsync(packagePath);
96 |
97 | has_(json.dependencies['ContractBySource'].main, 'ContractBySource.ts');
98 | },
99 | async 'check abi' () {
100 | let { stdout } = await run(`node index.js c abi ContractBySource --color none`);
101 | has_(stdout.join(''), `fooBySource()`);
102 | }
103 | });
104 | },
105 | async 'install contract by address with solidity source' () {
106 | let _basePath = './0xc/eth_goerli/ContractBySource/';
107 | return UTest({
108 | async $before () {
109 |
110 | if (await Directory.existsAsync(_basePath)) {
111 | await Directory.removeAsync(_basePath);
112 | }
113 | let { stdout, stderr } = await run(`node index.js i 0x1234 --source ./test/fixtures/install/ContractBySource.sol --name ContractBySource --chain eth:goerli`);
114 | console.log(`stdout: ${stderr.join('')} ${stdout.join('')}`);
115 | },
116 | async 'check paths' () {
117 | let content = await File.readAsync(`${_basePath}/ContractBySource.ts`, { skipHooks: true });
118 | has_(content, 'class ContractBySource extends ContractBase');
119 |
120 | let packagePath = '0xweb.json';
121 | let json = await File.readAsync(packagePath);
122 |
123 | has_(json.dependencies['ContractBySource'].main, 'ContractBySource.ts');
124 | },
125 | async 'check abi' () {
126 | let { stdout } = await run(`node index.js c abi ContractBySource --color none`);
127 | has_(stdout.join(''), `fooBySource()`);
128 | }
129 | });
130 | }
131 |
132 | })
133 |
--------------------------------------------------------------------------------
/src/commands/list/CAccount.ts:
--------------------------------------------------------------------------------
1 | import di from 'a-di';
2 | import { ICommand } from '../ICommand';
3 | import { AccountsService } from '@core/services/AccountsService';
4 | import { App } from '@core/app/App';
5 | import { $bigint } from '@dequanto/utils/$bigint';
6 | import { $console } from '@core/utils/$console';
7 | import { TAddress } from '@dequanto/models/TAddress';
8 | import { $address } from '@dequanto/utils/$address';
9 | import { $require } from '@dequanto/utils/$require';
10 | import { Parameters } from '@core/utils/Parameters';
11 | import { $sig } from '@dequanto/utils/$sig';
12 |
13 | export function CAccount () {
14 | return {
15 | command: 'account',
16 |
17 | description: [
18 | 'Account tools.'
19 | ],
20 | subcommands: [
21 | {
22 | command: 'balance',
23 | example: '0xweb account balance -p ',
24 | description: [
25 | 'Get account balance for ETH or any ERC20 token'
26 | ],
27 | arguments: [
28 | {
29 | name: '',
30 | description: 'Account name added with "accounts" command',
31 | required: true,
32 | },
33 | {
34 | name: '',
35 | description: 'Token Symbol or Address',
36 | required: true,
37 | }
38 | ],
39 | params: {
40 | '-b, --block': {
41 | description: 'Balance at specific block. Default: latest',
42 | map: Number
43 | }
44 | },
45 | async process (args: string[], params: any, app: App) {
46 | let [ accountName, tokenName ] = args;
47 |
48 | let address: TAddress;
49 | if ($address.isValid(accountName)) {
50 | address = accountName;
51 | } else {
52 | let accounts = di.resolve(AccountsService, app.config);
53 | let account = await accounts.get(accountName);
54 | address = account?.address;
55 | }
56 | $require.Address(address);
57 |
58 | $console.toast(`Loading token ${tokenName}`);
59 | let token = await app.chain.tokens.getToken(tokenName, true);
60 | if (token == null) {
61 | throw new Error(`Unknown token: ${tokenName} for ${app.chain.client.platform}`);
62 | }
63 |
64 | $console.toast(`Loading balance for ${address}`);
65 | let balance = await app.chain.token.balanceOf(address, token, { forBlock: params.block });
66 | let eth = $bigint.toEther(balance, token.decimals);
67 |
68 | $console.table([
69 | ['Symbol', tokenName],
70 | ['Address', token.address],
71 | ['Decimals', token.decimals.toString()],
72 | ['Balance', `green<${eth}>`]
73 | ]);
74 | }
75 | },
76 | {
77 | command: 'view',
78 | description: [
79 | 'View accounts details. '
80 | ],
81 | arguments: [
82 | {
83 | name: '',
84 | description: 'Account name added with "accounts" command',
85 | required: true,
86 | },
87 | ],
88 | params: {
89 | '--encrypted-key': {
90 | description: 'Prints also the KEY encoded with PIN',
91 | type: 'boolean',
92 | }
93 | },
94 | async process (args: string[], params: { encryptedKey?: boolean, pin: string }, app: App) {
95 | let [ accountName ] = args;
96 |
97 | let accounts = di.resolve(AccountsService, app.config);
98 | let account = await accounts.get(accountName);
99 | $require.notNull(account, `Account ${accountName} not found`);
100 | let tableData = [
101 | ['Account', accountName],
102 | ['Address', account.address],
103 | ];
104 |
105 | if (params.encryptedKey) {
106 | let key = (account as any).key;
107 | if (key) {
108 | if (/p1:/.test(key) === false) {
109 | const encrypted = await $sig.$key.encrypt(key, params.pin);
110 | key = encrypted;
111 | }
112 | tableData.push(['Key', key]);
113 | }
114 | }
115 | $console.table(tableData);
116 | }
117 | },
118 | {
119 | command: 'current',
120 | description: [
121 | 'Get current logged-in account'
122 | ],
123 | params: {
124 | ...Parameters.account({ required: true })
125 | },
126 | async process(args: string[], params, app: App) {
127 | $require.notEmpty(params.account, `No default account is loaded`);
128 | let service = di.resolve(AccountsService, app.config);
129 | let account = await service.get(params.account);
130 | $require.notNull(account, `${params.account} not found`)
131 | $console.table([
132 | ['Name', account.name],
133 | ['Address', account.address],
134 | ]);
135 | }
136 | },
137 | ],
138 | params: {
139 | ...Parameters.pin(),
140 | ...Parameters.chain({ required: false }),
141 | },
142 |
143 | async process(args: string[], params, app: App) {
144 | console.warn(`Command for an "accounts" not found: ${args[0]}. Call "0xweb accounts --help" to view the list of commands`);
145 | }
146 | };
147 | };
148 |
149 |
--------------------------------------------------------------------------------
/src/commands/CommandsHandler.ts:
--------------------------------------------------------------------------------
1 | import { ICommand } from './ICommand';
2 | import { $command } from './utils/$command';
3 | import { App } from '@core/app/App';
4 | import { $validate } from '@core/utils/$validate';
5 | import { CHelp } from './list/CHelp';
6 | import { $console } from '@core/utils/$console';
7 |
8 |
9 | export class CommandsHandler {
10 |
11 | private commands: Record = Object.create(null);
12 | private flags: Record = Object.create(null);
13 | public list: ICommand[] = [];
14 |
15 | register (command: ICommand | ICommand[]): this {
16 | if (Array.isArray(command)) {
17 | command.forEach(c => this.register(c));
18 | return this;
19 | }
20 | $command.getAliases(command.command).map(({ name, isFlag }) => {
21 | if (isFlag) {
22 | this.flags[name] = command;
23 | } else {
24 | this.commands[name] = command;
25 | }
26 | });
27 | this.list.push(command);
28 | return this;
29 | }
30 |
31 | async findCommand (cliArgs: string[], cliParams): Promise<{
32 | command?: ICommand,
33 | args?: any[],
34 | params?
35 | paramsDefinition?
36 | }> {
37 | let name = null;
38 | let command: ICommand;
39 | if (cliArgs.length === 0) {
40 | name = Object.keys(cliParams)[0];
41 | command = this.flags[name];
42 | } else {
43 | name = cliArgs[0];
44 | command = this.commands[name];
45 | }
46 | if (name == null) {
47 | command = this.commands['help'];
48 | }
49 | if (command == null) {
50 | $console.log(`Running "${ process.argv.join(' ')}"`);
51 | throw new Error(`Unknown command: ${name}`);
52 | }
53 |
54 | let {
55 | command: commandExtracted,
56 | args,
57 | paramsDefinition,
58 | isHelp
59 | } = this.extractCommand(command, cliArgs, cliParams)
60 |
61 | command = commandExtracted;
62 |
63 | if (isHelp) {
64 | let params;
65 | if (/help/.test(command.command) === false) {
66 | params = { command };
67 | command = this.commands['help'];
68 | }
69 | return { command, paramsDefinition, params: params ?? {} };
70 | }
71 |
72 | let params = await $command.getParams(cliParams, paramsDefinition);
73 | $validate.args(command, args);
74 | $validate.params(command, params, paramsDefinition);
75 | return { command, args, params };
76 | }
77 |
78 | private extractCommand (command: ICommand, cliArgs, cliParams): {
79 | command: ICommand,
80 | args: string[],
81 | paramsDefinition,
82 | breadcrumbs: string[],
83 | isHelp: boolean
84 | } {
85 | let args = cliArgs.slice(1);
86 | let paramsDefinition = command.params ?? {};
87 | let isHelp = 'help' in cliParams;
88 | let breadcrumbs = [];
89 | while (command.subcommands != null) {
90 | let name = args[0];
91 | let subCommand = command.subcommands.find(x => x.command === name);
92 | if (subCommand) {
93 | args = args.slice(1);
94 | command = subCommand;
95 | paramsDefinition = {
96 | ...(paramsDefinition ?? {}),
97 | ...(subCommand.params ?? {}),
98 | };
99 | breadcrumbs.push(name);
100 | continue;
101 | }
102 |
103 | if (typeof command.process === 'function') {
104 | // A command looks like to be a handler too
105 | break;
106 | }
107 |
108 | if (isHelp === false) {
109 | throw new Error(`Subcommand 'bold<${args[0]}>' of 'bold<${name}>' not found`);
110 | }
111 | break;
112 | }
113 | return {
114 | command,
115 | args,
116 | paramsDefinition,
117 | breadcrumbs,
118 | isHelp,
119 | };
120 | }
121 |
122 |
123 | // async process (cliArgs: string[], cliParams, app: App) {
124 | // let name = null;
125 | // let command: ICommand;
126 | // if (cliArgs.length === 0) {
127 | // name = Object.keys(cliParams)[0];
128 | // command = this.flags[name];
129 | // } else {
130 | // name = cliArgs[0];
131 | // command = this.commands[name];
132 | // }
133 | // if (name == null) {
134 | // command = this.commands['help'];
135 | // }
136 | // if (command == null) {
137 | // throw new Error(`Unknown command: ${name}`);
138 | // }
139 |
140 | // if (cliParams.help) {
141 | // let result = await CHelp().printCommand(command);
142 | // return result;
143 | // }
144 |
145 | // let args = cliArgs.slice(1);
146 |
147 |
148 |
149 | // let paramsDefinition = command.params ?? {};
150 |
151 | // if (command.subcommands) {
152 | // let subCommand = command.subcommands.find(x => x.command === args[0]);
153 | // if (subCommand == null) {
154 | // throw new Error(`Subcommand 'bold<${args[0]}>' not found`);
155 | // }
156 |
157 | // args = args.slice(1);
158 | // command = subCommand;
159 | // paramsDefinition = {
160 | // ...(paramsDefinition ?? {}),
161 | // ...(subCommand.params ?? {}),
162 | // };
163 |
164 | // console.log('1subcommand', command, '\n');
165 |
166 | // if (command.subcommands) {
167 | // let subCommand = command.subcommands.find(x => x.command === args[0]);
168 | // if (subCommand == null) {
169 | // throw new Error(`2n subcommand 'bold<${args[0]}>' not found`);
170 | // }
171 | // args = args.slice(1);
172 | // command = subCommand;
173 | // paramsDefinition = {
174 | // ...(paramsDefinition ?? {}),
175 | // ...(subCommand.params ?? {}),
176 | // };
177 | // console.log('2subcommand', command, '\n');
178 | // }
179 | // }
180 |
181 |
182 | // let params = await $command.getParams(cliParams, paramsDefinition);
183 | // $validate.args(command, args);
184 | // $validate.params(command, params, paramsDefinition);
185 | // return await command.process(args, params, app);
186 | // }
187 | };
188 |
--------------------------------------------------------------------------------
/src/commands/list/CTransfer.ts:
--------------------------------------------------------------------------------
1 | import di from 'a-di';
2 | import { ICommand } from '../ICommand';
3 | import { App } from '@core/app/App';
4 | import { $console } from '@core/utils/$console';
5 | import { $is } from '@dequanto/utils/$is';
6 | import { IToken } from '@dequanto/models/IToken';
7 | import { TokenTransferService } from '@dequanto/tokens/TokenTransferService';
8 | import { $bigint } from '@dequanto/utils/$bigint';
9 | import { FileServiceTransport } from '@dequanto/safe/transport/FileServiceTransport';
10 | import { EoAccount, TAccount } from '@dequanto/models/TAccount';
11 | import { Parameters } from '@core/utils/Parameters';
12 | import { l } from '@dequanto/utils/$logger';
13 | import { $address } from '@dequanto/utils/$address';
14 | import { TxWriter } from '@dequanto/txs/TxWriter';
15 |
16 |
17 | export function CTransfer() {
18 | return {
19 | command: 'transfer',
20 | example: '0xweb transfer 0.1 ETH --from 0x... --to 0x... ',
21 | description: [
22 | 'Transfer ETH or ERC20'
23 | ],
24 | arguments: [
25 | {
26 | name: '',
27 | description: 'Amount in ETHER, or percents. Supports negative values to leave rest amounts at sender account',
28 | required: true,
29 | },
30 | {
31 | name: '',
32 | description: 'Token symbol or token address',
33 | required: true,
34 | }
35 | ],
36 | params: {
37 | '-f, --from': {
38 | description: 'Senders name or address. ',
39 | required: true
40 | },
41 | '-t, --to': {
42 | description: 'Receivers name or address. ',
43 | required: true
44 | },
45 | ...Parameters.chain(),
46 | ...Parameters.pin(),
47 | '--safe-transport': {
48 | description: `Optionally the file path for multisig signatures, if collected manually, as per default Gnosis Safe Service is used.`,
49 | },
50 | '--sig-transport': {
51 | description: `Optionally the file where we save the tx and wait until the signature for the TX is provided.`,
52 | },
53 | '--tx-output': {
54 | description: `Save the TX to the file, and do not send it to the blockchain`,
55 | }
56 | },
57 | async process(args: string[], params: { from, to, chain, safeTransport?, sigTransport?, txOutput? }, app: App) {
58 | let [amountStr, tokenMix] = args;
59 |
60 |
61 | $console.toast(`Loading token ${tokenMix}`);
62 | let token = await app.chain.tokens.getToken(tokenMix, true);
63 | if (token == null && $is.Address(tokenMix)) {
64 | token = {
65 | address: tokenMix,
66 | decimals: 18,
67 | platform: app.chain.client.platform,
68 | };
69 | }
70 | if (token == null && tokenMix === app.chain.client.chainToken) {
71 | token = {
72 | symbol: tokenMix,
73 | address: $address.ZERO,
74 | decimals: 18,
75 | platform: app.chain.client.platform,
76 | };
77 | }
78 | if (token == null) {
79 | throw new Error(`Token ${tokenMix} not found`);
80 | }
81 |
82 | let accountFrom = await app.getAccount(params.from) as EoAccount;
83 | if (accountFrom == null) {
84 | throw new Error(`Account ${params.from} not found in storage`);
85 | }
86 | let accountTo = $is.Address(params.to)
87 | ? { address: params.to }
88 | : await app.getAccount(params.to);
89 |
90 | if (accountTo == null) {
91 | throw new Error(`Account ${params.to} not found in storage`);
92 | }
93 | if (accountTo.platform && accountTo.platform !== app.chain.client.platform) {
94 | //-throw new Error(`Chain mismatch. Account ${accountTo.address} required ${accountTo.platform}, but got ${app.chain.client.platform}`);
95 | }
96 |
97 | let service = di.resolve(TokenTransferService, app.chain.client);
98 |
99 | $console.toast(`Loading current balance for ${accountFrom.address}`);
100 | let balance = await service.getBalance(accountFrom.address, token);
101 | $console.log(`Account balance: ${$bigint.toEther(balance, token.decimals)}`);
102 |
103 |
104 | let amount: bigint | number;
105 | let percents = /^(?[\d\.]+)%$/.exec(amountStr);
106 | if (percents) {
107 | let p = Number(percents.groups.value);
108 | amount = $bigint.multWithFloat(balance, p / 100);
109 | } else {
110 | let num = Number(amountStr);
111 | if (isNaN(num)) {
112 | throw new Error(`Invalid amount number ${amountStr}`);
113 | }
114 | amount = num;
115 | }
116 |
117 | if (!amount) {
118 | throw new Error(`Invalid amount: ${amountStr}`);
119 | }
120 |
121 | $console.toast(`Transferring ${amount}${token.symbol} from ${accountFrom.address} to ${accountTo.address}`);
122 |
123 | let safeTransportFile = params.safeTransport;
124 | if (safeTransportFile) {
125 | service.$configWriter({
126 | safeTransport: new FileServiceTransport(app.chain.client, accountFrom, safeTransportFile)
127 | });
128 | }
129 | let sigTransportFile = params.sigTransport;
130 | if (sigTransportFile) {
131 | service.$configWriter({
132 | sigTransport: sigTransportFile
133 | });
134 | }
135 | let txOutput = params.txOutput;
136 | if (txOutput) {
137 | service.$configWriter({
138 | txOutput: txOutput
139 | });
140 | }
141 |
142 | let tx: TxWriter;
143 | if (amount === balance) {
144 | tx = await service.transferAll(accountFrom, accountTo.address, token);
145 | } else if (amount < 0) {
146 | tx = await service.transferAllWithRemainder(accountFrom, accountTo.address, token, amount);
147 | } else {
148 | tx = await service.transfer(accountFrom, accountTo.address, token, amount);
149 | }
150 |
151 | if (txOutput) {
152 | let path = await tx.onSaved;
153 | l`Transfer transaction green. To submit to the blockchain call "0xweb tx send ${path}"`;
154 | return;
155 | }
156 | let receipt = await tx.wait();
157 | l`Transfered. Receipt: bold<${receipt.transactionHash}>`;
158 | }
159 | };
160 | }
161 |
--------------------------------------------------------------------------------
/src/services/ServerService.ts:
--------------------------------------------------------------------------------
1 | import alot from 'alot';
2 | import memd from 'memd';
3 | import { env } from 'atma-io';
4 | import { Application, HttpResponse, HttpService, middleware } from 'atma-server';
5 | import { App } from '../app/App';
6 | import { ICommand } from '@core/commands/ICommand';
7 | import { $command } from '@core/commands/utils/$command';
8 |
9 | import { CRpc } from '@core/commands/list/CRpc';
10 | import { CContract } from '@core/commands/list/CContract';
11 | import { CTx } from '@core/commands/list/CTx';
12 | import { CBlock } from '@core/commands/list/CBlock';
13 |
14 | export class ServerService {
15 |
16 | server: Application;
17 |
18 | constructor (public app: App) {
19 |
20 | }
21 |
22 | @memd.deco.memoize()
23 | async createServer (params?: { dev?: boolean }) {
24 | const service = ServerCommands.toService([
25 | CContract(),
26 | CRpc(),
27 | CTx(),
28 | CBlock(),
29 | ], this.app);
30 |
31 | const debug = Boolean(params?.dev ?? false);
32 | this.server = await Application.create({
33 | configs: null,
34 | //debug: true, //Boolean(params?.dev ?? false),
35 | config: {
36 | debug,
37 | serializer: {
38 | json: {
39 | formatted: true
40 | }
41 | },
42 | rewriteRules: [
43 | {
44 | rule: debug
45 | ? '^/(contracts|contract|tx|devbook)(/[\\w\\-_\\/]+)? /index.dev.html'
46 | : '^/(contracts|contract|tx|devbook)(/[\\w\\-_\\/]+)? /',
47 | conditions: null,
48 | },
49 | // {
50 | // rule: debug
51 | // ? '^/(\\w+)/tx/(\\w+) /index.dev.html'
52 | // : '^/(\\w+)/tx/(\\w+) /',
53 | // conditions: null,
54 | // },
55 | {
56 | rule: debug
57 | ? '^/$ /index.dev.html'
58 | : '^/$ /',
59 | conditions: null,
60 | }
61 | ]
62 | },
63 |
64 | });
65 | await this.server.handlers.registerService('^/api', service);
66 | return this.server;
67 | }
68 |
69 | async start (params: {
70 | port: number,
71 | dev: boolean
72 | }) {
73 | let basePath = env.applicationDir.toDir();
74 | if (/0xweb\/?$/.test(basePath) === false) {
75 | basePath = env.currentDir.toDir();
76 | }
77 | let server = await this.createServer({ dev: params.dev });
78 | await server
79 | .processor({
80 | middleware: [
81 | middleware.bodyJson(),
82 | ],
83 | after: [
84 | middleware.static({
85 | base: basePath
86 | })
87 | ]
88 | })
89 | .listen(params.port);
90 | }
91 | }
92 |
93 |
94 |
95 | namespace ServerCommands {
96 | export function toService(commands: ICommand[], app: App) {
97 | let routes = alot(commands).mapMany(command => {
98 | return getRoutes('', command);
99 | }).toArray();
100 |
101 | let definition = alot(routes)
102 | .filter(x => x.command.api != null)
103 | .toDictionary(x => {
104 | return `$${x.command.api.method ?? 'get'} ${x.path}`;
105 | }, x => {
106 | return {
107 | meta: {
108 | origins: '*'
109 | },
110 | process: wrapProcessor(x.command.api.process ?? x.command.process, app, x.command)
111 | };
112 | });
113 |
114 | return HttpService(definition);
115 | }
116 |
117 | function wrapProcessor(
118 | process: (args: any[], params?, app?: App, command?: ICommand) => Promise,
119 | appFromCli: App,
120 | command: ICommand
121 | ) {
122 | return async function (req, res, params) {
123 |
124 | let appFromRequest: App = null;
125 | let platform = params.chain;
126 | if (platform) {
127 | appFromRequest = new App();
128 | await appFromRequest.ensureChain(platform);
129 | }
130 |
131 | let cliArgs = [];
132 | let cliParams = params;
133 | for (let key in params) {
134 | let index = /^cliArg(?\d+)$/.exec(key);
135 | if (index) {
136 | cliArgs[Number(index.groups.i)] = params[key];
137 | continue;
138 | }
139 | let named = command.arguments?.findIndex(x => x.name === key);
140 | if (named > -1) {
141 | cliArgs[named] = params[key];
142 | continue;
143 | }
144 | }
145 | if (req.body != null) {
146 | for (let key in req.body) {
147 | cliParams[key] = req.body[key];
148 | }
149 | }
150 |
151 | let app = appFromRequest ?? appFromCli;
152 | app.config ??= {} as any;
153 | app.config.env = 'api';
154 | let result = await process(cliArgs, cliParams, app, command);
155 | return new HttpResponse({
156 | content: JSON.stringify(result),
157 | mimeType: 'application/json; charset=utf-8'
158 | });
159 | }
160 | }
161 |
162 | function getRoutes(path: string, command: ICommand): { path: string, command: ICommand }[] {
163 | let aliases = $command.getAliases(command.command);
164 | let routes = alot(aliases).mapMany(({ name, isFlag }) => {
165 |
166 | let route = `${path}/${name}`;
167 | if (command.arguments) {
168 | for (let i = 0; i < command.arguments.length; i++) {
169 | let arg = command.arguments[i];
170 | if (arg.query) {
171 | // Should be not the URI segment, but the query parameter
172 | continue;
173 | }
174 | let name = arg.name ?? `cliArg${i}`;
175 | route += `/:${name}`;
176 | }
177 | }
178 | if (command.subcommands) {
179 | let subroutes = [];
180 | for (let sub of command.subcommands) {
181 | subroutes.push(...getRoutes(route, sub));
182 | }
183 | return subroutes;
184 | }
185 | return [{
186 | path: route,
187 | command
188 | }];
189 | }).toArray() as { path: string, command: ICommand }[];
190 |
191 | //console.log('Routes', routes.map(x => x.path));
192 | return routes;
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/src/utils/$cli.ts:
--------------------------------------------------------------------------------
1 | import * as readline from 'readline';
2 | import alot from 'alot';
3 | import { env } from 'atma-io';
4 | import { $command } from '@core/commands/utils/$command';
5 | import { $is } from '@dequanto/utils/$is';
6 | import { $console } from './$console';
7 | import { $color } from '@dequanto/utils/$color';
8 | import { obj_setProperty } from 'atma-utils';
9 |
10 | export namespace $cli {
11 |
12 | let $argv: (string | number | boolean)[];
13 |
14 |
15 | export function setParams (argv: string[]) {
16 | $argv = argv.map(arg => {
17 | return arg === '?' ? '--help' : arg;
18 | });
19 | }
20 |
21 | setParams(process.argv);
22 |
23 | /**
24 | *
25 | * @param flag '-c', '--chain', CSV is also supported: '-c, --chain'
26 | */
27 | export function getParamValue(flag: string, params?: { [key: string]: any }): string | null {
28 | let args = $argv;
29 | let aliases = $command.getAliases(flag);
30 | return alot(aliases)
31 | .map(command => {
32 | let valFromParams = params?.[command.name];
33 | if (valFromParams != null) {
34 | return valFromParams;
35 | }
36 | let i = args.findIndex(arg => {
37 | if (typeof arg !== 'string') {
38 | return false;
39 | }
40 | let inputArg = toCommand(arg);
41 | return inputArg.isFlag === command.isFlag && inputArg.name === command.name;
42 | });
43 | if (i > -1) {
44 | if (i + 1 === args.length) {
45 | // Last means a Flag
46 | return true;
47 | }
48 | let val = args[i + 1];
49 | let c = val?.[0];
50 | if (c === '-') {
51 | // Next is the option, so this one is a boolean flag
52 | return true;
53 | }
54 | return val;
55 | }
56 | return null;
57 | })
58 | .filter(x => x != null)
59 | .first();
60 | }
61 |
62 | export function isLocal (params?: Record) {
63 | if (params != null) {
64 | let local = $cli.getParamValue('--local', params) as any;
65 | if (local === true || local === '' || local === 'local') {
66 | return true;
67 | }
68 | let global = $cli.getParamValue('--global', params) as any;
69 | if (global === true || global === '' || global === 'global') {
70 | return false;
71 | }
72 | }
73 | let isLocal = env.applicationDir.toString().startsWith(env.currentDir.toString());
74 | return isLocal;
75 | }
76 |
77 | export function parse (argv: (string | boolean | number)[] = null) {
78 |
79 | if (argv == null) {
80 | argv = $argv;
81 | }
82 |
83 | let params = {} as any;
84 | let args = [];
85 | for (let i = 0; i < argv.length; i++) {
86 | let x = argv[i];
87 |
88 | if (typeof x === 'string' && x[0] === '-') {
89 |
90 | let key = x.replace(/^[\-]+/, '');
91 | let val;
92 | if (i < argv.length - 1 && argv[i + 1][0] !== '-') {
93 | val = argv[i + 1];
94 | i++;
95 | } else {
96 | val = true;
97 | }
98 | obj_setProperty(params, key, val);
99 | continue;
100 | }
101 |
102 | args.push(argv[i]);
103 | }
104 |
105 | // clean empty literals
106 | args = args.map(x => x.trim()).filter(Boolean);
107 |
108 | let i = args.findIndex(x => /\bindex(\.(ts|js))?$/i.test(x));
109 | if (i === -1) {
110 | i = args.findIndex(x => /\b0xweb$/i.test(x));
111 | }
112 | if (i > -1) {
113 | args = args.slice(i + 1);
114 | }
115 |
116 | return { params, args };
117 | }
118 |
119 | export function ask(question: string, type?: string) {
120 | return new Promise(resolve => {
121 | rl.question($color(question), (answer) => {
122 |
123 | let { error, value } = parseInput(answer, type);
124 | if (value != null) {
125 | resolve(value);
126 | return;
127 | }
128 | $console.log(`red ${error.message}`);
129 |
130 | ask(question, type).then(resolve);
131 | });
132 | })
133 | }
134 | //export function askAbiInput ()
135 |
136 | // remove "-"(s) from start
137 | function toCommand(flag: string) {
138 | let name = flag.replace(/^\-+/, '')
139 | return {
140 | name,
141 | isFlag: name !== flag
142 | };
143 | }
144 |
145 | const rl = readline.createInterface({
146 | input: process.stdin,
147 | output: process.stdout
148 | });
149 |
150 |
151 | function parseInput(input, type): { error?: Error, value?: any } {
152 | input = input.trim();
153 |
154 | if (!type) {
155 | return { value: input };
156 | }
157 |
158 | let rgxArray = /[(\d+)?]$/;
159 | if (rgxArray.test(type) && isBuffer(type) === false) {
160 | type = type.replace(rgxArray, '');
161 | let results = input.split(',').map(x => parseInput(x, type));
162 | let error = results.find(x => x.error)[0]?.error;
163 | if (error) {
164 | return { error };
165 | }
166 | return { value: results.map(x => x.value) }
167 | };
168 |
169 | if (type === 'address') {
170 | if ($is.Address(input) === false) {
171 | return { error: new Error(`Not an address`) }
172 | }
173 | return { value: input }
174 | }
175 | if (/int/.test(type)) {
176 | let isNumber = /^\-?\d+$/.test(input);
177 | if (isNumber == false) {
178 | isNumber = /^0x[a-fA-F0-9]+$/.test(input);
179 | }
180 | if ( isNumber === false) {
181 | return { error: new Error(`Not a number`) };
182 | }
183 | return { value: BigInt(input) };
184 | }
185 | if (isBuffer(type)) {
186 |
187 | let isHex = /^0x[a-fA-F0-9]+$/.test(input);
188 | if (isHex === false || input.length % 2 !== 0) {
189 | return { error: new Error(`Invalid HEX buffer string`) };
190 | }
191 | return { value: input }
192 | }
193 | if (type === 'bool') {
194 | if (/(true|1|yes)/i.test(input)) {
195 | return { value: true };
196 | }
197 | if (/(false|0|no)/i.test(input)) {
198 | return { value: false };
199 | }
200 | return { error: new Error(`Invalid Boolean. Expects on of: true, 1, yes, false, 0, no`) };
201 | }
202 |
203 | return { value: input };
204 | }
205 |
206 | function isBuffer (type: string) {
207 | return /byte/.test(type);
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/src/services/ContractDumpService.ts:
--------------------------------------------------------------------------------
1 | import alot from 'alot'
2 | import { App } from '@core/export'
3 | import { $console } from '@core/utils/$console'
4 | import { TAddress } from '@dequanto/models/TAddress'
5 | import { SlotsDump } from '@dequanto/solidity/SlotsDump'
6 | import { $is } from '@dequanto/utils/$is'
7 | import { $require } from '@dequanto/utils/$require'
8 | import { Directory, File } from 'atma-io'
9 | import di from 'a-di'
10 | import { PackageService } from './PackageService'
11 | import { SlotsStorage } from '@dequanto/solidity/SlotsStorage'
12 |
13 |
14 | export class IContractDumpServiceParams {
15 | output?: string
16 | implementation?: TAddress
17 | fields?: string
18 | sources?: string
19 | contractName?: string
20 | file?: string
21 | }
22 |
23 | export class ContractDumpService {
24 |
25 | constructor(public app: App) {
26 |
27 | }
28 |
29 | async dump (nameOrAddress: string | TAddress, params: IContractDumpServiceParams) {
30 |
31 | let { _address, _output, _implementation, _sources, _contractName } = await this.getContractData(nameOrAddress, params);
32 |
33 | $require.String(_output, 'Output file not defined');
34 | $require.notNull(this.app.chain, `--chain not specified`);
35 |
36 | let dump = new SlotsDump({
37 | address: _address,
38 | implementation: _implementation,
39 | contractName: _contractName,
40 | client: this.app.chain.client,
41 | explorer: this.app.chain.explorer,
42 | fields: params.fields?.split(',').map(x => x.trim()),
43 | sources: _sources,
44 | parser: {
45 | withConstants: true,
46 | withImmutables: true,
47 | }
48 | });
49 |
50 | let data = await dump.getStorage();
51 | let csv = data.memory.map(x => x.join(', ')).join('\n');
52 | let json = data.json;
53 |
54 | if (params.output !== 'std') {
55 | let csvFile = new File(`${_output}.csv`);
56 | let jsonFile = new File(`${_output}.json`);
57 |
58 | await Promise.all([
59 | csvFile.writeAsync(csv),
60 | jsonFile.writeAsync(json),
61 | ]);
62 |
63 | return {
64 | files: {
65 | csv: csvFile.uri.toString(),
66 | json: jsonFile.uri.toString()
67 | }
68 | };
69 | }
70 | return {
71 | json
72 | };
73 | }
74 |
75 | async dumpRestore(nameOrAddress: string, params: IContractDumpServiceParams) {
76 | let { _address, _output, _implementation, _sources } = await this.getContractData(nameOrAddress, params);
77 | let dump = new SlotsDump({
78 | address: _address,
79 | implementation: _implementation,
80 | client: this.app.chain.client,
81 | explorer: this.app.chain.explorer,
82 | fields: params.fields?.split(',').map(x => x.trim()),
83 | sources: _sources
84 | });
85 |
86 | if (/\.json$/.test(params.file)) {
87 | let json = await File.readAsync(params.file);
88 | if (Array.isArray(json) && Array.isArray(json[0])) {
89 | let table = json;
90 | await dump.restoreStorageFromTable(table);
91 | return;
92 | }
93 | await dump.restoreStorageFromJSON(json);
94 | return;
95 | }
96 | if (/.csv$/.test(params.file)) {
97 | let csv = await File.readAsync(params.file);
98 | let table = csv.split('\n').map(x => x.trim()).filter(Boolean).map(row => {
99 | return row.split(',').map(x => x.trim()).filter(Boolean);
100 | }) as [string,string][];
101 | await dump.restoreStorageFromTable(table)
102 | return;
103 | }
104 | throw new Error(`File not supported: ${params.file}`);
105 | }
106 |
107 |
108 | private async getContractData (nameOrAddress: string, params: IContractDumpServiceParams) {
109 | let _address: TAddress;
110 | let _implementation: TAddress;
111 | let _sources;
112 | let _sourcesPath = params.sources;
113 | let _contractName = params.contractName;
114 |
115 | // file-output without extensions ()
116 | let _output: string
117 | if ($is.Address(nameOrAddress)) {
118 | _address = nameOrAddress;
119 | _output = params.output;
120 | } else {
121 | let pkg = await this.getPackage(nameOrAddress);
122 | _address = pkg.address;
123 | _output = params.output ?? `./dump/${pkg.name}/storage`;
124 | _implementation = pkg.implementation ?? params.implementation;
125 | _sourcesPath ??= pkg.main.replace(/[^\/]+$/, `${pkg.name}/`);
126 | _contractName ??= pkg.contractName;
127 |
128 | await this.app.ensureChain(pkg.platform);
129 | }
130 |
131 | if (_sourcesPath != null) {
132 | let isFile = /\.sol$/.test(_sourcesPath);
133 | if (isFile === false) {
134 | let exists = await Directory.existsAsync(_sourcesPath);
135 | $require.True(exists, `Sources directory ${_sourcesPath} does not exist`);
136 |
137 | let files = await Directory.readFilesAsync(_sourcesPath, '**.sol');
138 | let filesContent = await alot(files).mapAsync(async file => {
139 | return {
140 | path: file.uri.toString(),
141 | content: await file.readAsync(),
142 | }
143 | }).toDictionaryAsync(x => x.path, x => ({ content: x.content }));
144 | _sources = {
145 | files: filesContent,
146 | };
147 | } else {
148 | let file = new File(_sourcesPath);
149 | let exists = await file.existsAsync();
150 | $require.True(exists, `Sources file ${_sourcesPath} does not exist`);
151 |
152 | let path = file.uri.toString();
153 | let content = await file.readAsync ();
154 | _sources = {
155 | files: {
156 | [ path ]: content,
157 | },
158 | };
159 | if (_contractName == null) {
160 | let rgx = /\bcontract \s*(?\w+)/g;
161 | do {
162 | let match = rgx.exec(content);
163 | if (match == null) {
164 | break;
165 | }
166 | _contractName = match.groups.contractName;
167 | } while (true);
168 | }
169 | }
170 |
171 | return {
172 | _output,
173 | _address,
174 | _implementation,
175 | _sources,
176 | //_sourcesPath,
177 | _contractName,
178 | };
179 |
180 | }
181 | }
182 |
183 | private async getPackage (name: string) {
184 | let packageService = di.resolve(PackageService, this.app.chain);
185 | let pkg = await packageService.getPackage(name);
186 | if (pkg == null) {
187 | throw new Error(`Package ${name} not found. gray<0xweb c list> to view all installed contracts`);
188 | }
189 | if (this.app.chain == null) {
190 | this.app.chain = packageService.chain;
191 | }
192 | return pkg;
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/src/app/App.ts:
--------------------------------------------------------------------------------
1 | import di from 'a-di';
2 | import memd from 'memd';
3 | import AppConfig from 'appcfg'
4 | import { Config } from '@dequanto/config/Config';
5 | import { CommandsHandler } from '../commands/CommandsHandler';
6 | import { CVersion } from '../commands/list/CVersion';
7 | import { CInstall } from '../commands/list/CInstall';
8 | import { CConfig } from '../commands/list/CConfig';
9 | import { CHelp } from '../commands/list/CHelp';
10 | import { CAccounts } from '../commands/list/CAccounts';
11 | import { $cli } from '@core/utils/$cli';
12 | import { CBlock } from '../commands/list/CBlock';
13 | import { IPlatformTools, PlatformFactory } from '@dequanto/chains/PlatformFactory';
14 | import { CAccount } from '../commands/list/CAccount';
15 | import { $console } from '@core/utils/$console';
16 | import { CReset } from '../commands/list/CReset';
17 | import { CContract } from '../commands/list/CContract';
18 | import { CInit } from '../commands/list/CInit';
19 | import { CToken } from '../commands/list/CToken';
20 | import { CGas } from '../commands/list/CGas';
21 | import { CSafe } from '@core/commands/list/CSafe';
22 | import { TAddress } from '@dequanto/models/TAddress';
23 | import { CTransfer } from '@core/commands/list/CTransfer';
24 | import { $color_options } from '@dequanto/utils/$color';
25 | import { CTokens } from '@core/commands/list/CTokens';
26 | import { IAccount } from '@dequanto/models/TAccount';
27 | import { CTx } from '@core/commands/list/CTx';
28 | import { IWeb3EndpointOptions } from '@dequanto/clients/interfaces/IWeb3EndpointOptions';
29 | import { CInfo } from '@core/commands/list/CInfo';
30 | import { TPlatform } from '@dequanto/models/TPlatform';
31 | import { CRestore } from '@core/commands/list/CRestore';
32 | import { CHardhat } from '@core/commands/list/CHardhat';
33 | import { CRpc } from '@core/commands/list/CRpc';
34 | import { $logger, ELogLevel } from '@dequanto/utils/$logger';
35 | import { CSolidity } from '@core/commands/list/CSolidity';
36 | import { CTools } from '@core/commands/list/CTools';
37 | import { CNs } from '@core/commands/list/CNs';
38 | import { Web3Client } from '@dequanto/clients/Web3Client';
39 | import { ChainAccountService } from '@dequanto/ChainAccountService';
40 | import { TokenService } from '@dequanto/tokens/TokenService';
41 | import { TokenTransferService } from '@dequanto/tokens/TokenTransferService';
42 | import { TokensService } from '@dequanto/tokens/TokensService';
43 | import { AccountsService } from '@core/services/AccountsService';
44 | import { BlockchainExplorerFactory } from '@dequanto/explorer/BlockchainExplorerFactory';
45 | import { CServer } from '@core/commands/list/CServer';
46 | import { IConfigData } from '@dequanto/config/interface/IConfigData';
47 | import { TAppProcessResult } from './types';
48 |
49 |
50 | declare const global;
51 |
52 | export class App {
53 | commands = new CommandsHandler();
54 | config: AppConfig & IConfigData & { env?: 'cli' | 'api' };
55 |
56 | chain?: IPlatformTools
57 |
58 | constructor() {
59 | if (global.app instanceof App === false) {
60 | global.app = this;
61 | } else {
62 | this.config = global.app.config;
63 | this.chain = global.app.chain;
64 | }
65 | }
66 |
67 | async execute (argv?: string[]): Promise {
68 | if (argv?.length > 0) {
69 | $cli.setParams(argv);
70 | }
71 |
72 | if ($cli.getParamValue('--color') === 'none') {
73 | $color_options({ type: 'none' });
74 | }
75 | if ($cli.getParamValue('--silent')) {
76 | $logger.config({ level: ELogLevel.ERROR })
77 | }
78 |
79 | let { params: cliParams, args: cliArgs } = $cli.parse();
80 |
81 | $console.toast('Loading config');
82 | let defaults = await AccountsService.getDefaults(cliParams);
83 | if (defaults) {
84 | for (let key in defaults) {
85 | if (key in cliParams === false) {
86 | cliParams[key] = defaults[key];
87 | }
88 | }
89 | }
90 | if ($cli.isLocal(cliParams)) {
91 | cliParams['isLocal'] = true;
92 | }
93 | this.config = await Config.fetch(cliParams);
94 |
95 | this
96 | .commands
97 | .register(CInstall())
98 | .register(CRestore())
99 | .register(CInit())
100 | .register(CContract())
101 | .register(CAccounts())
102 | .register(CAccount())
103 | .register(CSafe())
104 | .register(CToken())
105 | .register(CTokens())
106 | .register(CTransfer())
107 | .register(CTx())
108 | .register(CHardhat())
109 | .register(CBlock())
110 | .register(CGas())
111 | .register(CRpc())
112 | .register(CNs())
113 | .register(CConfig())
114 | .register(CSolidity())
115 | .register(CTools())
116 | .register(CServer())
117 | .register(CVersion)
118 | .register(CReset())
119 | .register(CInfo())
120 | .register(CHelp())
121 |
122 | ;
123 |
124 | let { command, params, args, paramsDefinition } = await this.commands.findCommand(cliArgs, cliParams);
125 |
126 | let platform = $cli.getParamValue('-c, --chain', params);
127 | if (platform) {
128 | let opts = {};
129 | let endpoint = $cli.getParamValue('--endpoint,--endpoints', params);
130 | if (endpoint) {
131 | let urls = endpoint.split(/[,;]/).map(x => x.trim()).filter(Boolean);
132 | opts.endpoints = urls.map(x => ({ url: x }));
133 | }
134 |
135 | this.chain = await di
136 | .resolve(PlatformFactory)
137 | .get(platform as TPlatform, opts);
138 | }
139 |
140 | $console.toast(`Process command gray<${ command.command }>`);
141 |
142 | let result = await command.process(args, params, this);
143 |
144 | // flush all caches on exit
145 | //- await memd.Cache.flushAllAsync();
146 | return result;
147 | }
148 |
149 | async runFromCli () {
150 | try {
151 | let result = await this.execute();
152 | if (result?.status === 'wait') {
153 | // do not exit on long running commands
154 | return;
155 | }
156 | process.exit(0);
157 | } catch (error) {
158 | $console.error(`red<${error.message}>`);
159 |
160 | let stack = error.stack.split('\n').slice(1).join('\n');
161 | $console.error(`gray<${stack}>`);
162 | process.exit(1);
163 | }
164 | }
165 |
166 | async getAccount (mix: TAddress | string): Promise {
167 | //let accounts = di.resolve(AccountsService, app.config);
168 | let account = await this.chain.accounts.get(mix);
169 | return account as T;
170 | }
171 |
172 | async ensureChain (platform: TPlatform) {
173 | if (this.chain == null) {
174 | this.chain = await di
175 | .resolve(PlatformFactory)
176 | .get(platform);
177 | }
178 | }
179 | async setChain (client: Web3Client): Promise {
180 | const explorer = BlockchainExplorerFactory.get(client.platform);
181 | this.chain = {
182 | platform: client.network,
183 | client,
184 | tokens: new TokensService(client.network, explorer),
185 | token: new TokenService(client),
186 | explorer: explorer,
187 | accounts: new ChainAccountService(),
188 | transfer: new TokenTransferService(client),
189 | };
190 | return this;
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/src/commands/list/CTokens.ts:
--------------------------------------------------------------------------------
1 | import di from 'a-di';
2 | import alot from 'alot';
3 | import { ICommand } from '../ICommand';
4 | import { $validate } from '@core/utils/$validate';
5 | import { App } from '@core/app/App';
6 | import { $console } from '@core/utils/$console';
7 | import { IToken } from '@dequanto/models/IToken';
8 | import { $require } from '@dequanto/utils/$require';
9 | import { ERC20 } from '@dequanto-contracts/openzeppelin/ERC20';
10 | import { TokensService } from '@dequanto/tokens/TokensService';
11 | import { TokenPriceService } from '@dequanto/tokens/TokenPriceService';
12 | import { $bigint } from '@dequanto/utils/$bigint';
13 | import { TEth } from '@dequanto/models/TEth';
14 |
15 |
16 | export function CTokens() {
17 | return {
18 | command: 'tokens',
19 |
20 | description: [
21 | 'Manage known tokens.'
22 | ],
23 | subcommands: [
24 | {
25 | command: 'add',
26 | example: '0xweb tokens add --address 0x... --symbol FRT --decimals 18 --chain eth',
27 | description: [
28 | 'Add a new token to the known list.'
29 | ],
30 | params: {
31 | '-a, --address': {
32 | description: 'Tokens address',
33 | required: true,
34 | validate: $require.Address,
35 | },
36 | '-s, --symbol': {
37 | description: 'Tokens symbol',
38 | required: true
39 | },
40 | '-d, --decimals': {
41 | description: 'Tokens decimals. Default: 18',
42 | default: 18,
43 | type: 'number'
44 | }
45 | },
46 | async process(args: string[], params: any, app: App) {
47 | let { address, symbol, decimals, chain } = params;
48 |
49 | await app.chain.tokens.addKnownToken({
50 | address,
51 | symbol,
52 | decimals,
53 | platform: chain
54 | });
55 | $console.result(`Added token ${symbol} [${address}] in ${chain}`);
56 | }
57 | },
58 | {
59 | command: 'find',
60 | example: '0xweb tokens find USDC',
61 | description: [
62 | 'Get a token by Symbol or Address, and print the info'
63 | ],
64 | arguments: [
65 | {
66 | name: ''
67 | }
68 | ],
69 | params: {
70 |
71 | },
72 | async process(args: string[], params: any, app: App) {
73 | let [query] = args;
74 |
75 | try {
76 | let token = await app.chain.tokens.getKnownToken(query);
77 | $console.table([
78 | ['Symbol', token.symbol],
79 | ['Address', token.address],
80 | ['Decimals', token.decimals],
81 | ['Platform', token.platform],
82 | ]);
83 | } catch (error) {
84 | throw new Error(`Token '${query}' not found for '${params.chain}'`);
85 | }
86 | }
87 | },
88 | {
89 | command: 'for',
90 | example: '0xweb tokens for 0x...',
91 | description: [
92 | 'Get all tokens for the address'
93 | ],
94 | arguments: [
95 | {
96 | address: ''
97 | }
98 | ],
99 | params: {
100 |
101 | },
102 | async process(args: TEth.Address[], params: any, app: App) {
103 |
104 | let [eoa] = args;
105 | $require.Address(eoa, 'Provide the valid address to get the tokens for');
106 |
107 | $console.toast('Loading Transfer events...');
108 | let erc20 = new ERC20(null, app.chain.client);
109 | let transfers = await erc20.getPastLogsTransfer({
110 | params: {
111 | to: eoa
112 | }
113 | });
114 |
115 | let tokenAddresses = alot(transfers)
116 | .map(x => x.address)
117 | .distinct()
118 | .toArray();
119 |
120 | $console.log(`Got bold> tokens for ${eoa}`);
121 |
122 | $console.toast(`Loading tokens info...`);
123 | let tokensService = di.resolve(TokensService, app.chain.platform);
124 | let tokens = await alot(tokenAddresses)
125 | .mapAsync(async address => {
126 | return tokensService.getKnownToken(address);
127 | })
128 | .toArrayAsync({ errors: 'include' });
129 |
130 | let knownTokens = tokens
131 | .filter(x => x instanceof Error === false)
132 |
133 | $console.log(`Got bold> known ERC20 tokens`);
134 |
135 | let priceService = di.resolve(TokenPriceService, app.chain.client, app.chain.explorer);
136 |
137 | $console.toast(`Loading account balances...`);
138 | let balances = await alot(knownTokens)
139 | .mapAsync(async token => {
140 | $console.toast(`Loading balance for ${token.symbol}...`);
141 | let balance = await new ERC20(token.address, app.chain.client).balanceOf(eoa)
142 | let priceInfo = await priceService.getPrice(token, {
143 | amountWei: balance
144 | });
145 | return {
146 | token,
147 | balance: $bigint.toEther(balance, token.decimals),
148 | priceInfo
149 | };
150 | })
151 | .toArrayAsync();
152 |
153 | let table = balances.map(result => {
154 | return [
155 | result.token.symbol ?? result.token.name,
156 | result.token.address,
157 | `${result.balance}`,
158 | result.priceInfo.error
159 | ? result.priceInfo.error.message
160 | : `${result.priceInfo.price}$`,
161 | ]
162 | });
163 | $console.table([
164 | ['Token', 'Address', 'Balance(Ξ)', '$'],
165 | ...table,
166 | ]);
167 | }
168 | },
169 | ],
170 | params: {
171 | '-c, --chain': {
172 | description: `Default: eth. Available: ${$validate.platforms().join(', ')}`,
173 | required: true,
174 | oneOf: $validate.platforms()
175 | }
176 | },
177 |
178 | async process(args: string[], params, app: App) {
179 | console.warn(`Command for an "token" not found: ${args[0]}. Call "0xweb token --help" to view the list of commands`);
180 | }
181 | };
182 | }
183 |
184 |
--------------------------------------------------------------------------------
/src/utils/$abiInput.ts:
--------------------------------------------------------------------------------
1 | import alot from 'alot';
2 | import type { TEth } from '@dequanto/models/TEth';
3 | import { $cli } from './$cli';
4 | import { $abiType } from '@dequanto/utils/$abiType';
5 | import { $types } from '@dequanto/solidity/utils/$types';
6 | import { obj_getProperty, obj_setProperty } from 'atma-utils';
7 | import { $bigint } from '@dequanto/utils/$bigint';
8 | import { $require } from '@dequanto/utils/$require';
9 | import { $address } from '@dequanto/utils/$address';
10 | import { File } from 'atma-io';
11 |
12 | export namespace $abiInput {
13 | interface IArgumentProvider {
14 | get (abi: TEth.Abi.Input): Promise
15 | }
16 |
17 | const cliInputAsker = {
18 | get (abi) {
19 | return $cli.ask(`Value for bold<${abi.name}> gray<(>bold>gray<)>: `, abi.type);
20 | }
21 | };
22 | const apiInputAsker = {
23 | get (abi) {
24 | throw new Error(`Value for "${abi.name}" as ${abi.type} not provided`);
25 | }
26 | };
27 |
28 | export async function parseArgumentsFromCli (abi: TEth.Abi.Item, params: Record, opts?:{
29 | argumentProvider?: IArgumentProvider
30 | env?: 'api' | 'cli'
31 | }): Promise {
32 | let argumentProvider = opts?.argumentProvider;
33 | if (argumentProvider == null) {
34 | argumentProvider = opts?.env === 'api' ? apiInputAsker : cliInputAsker;
35 | }
36 | return getArguments(abi, params, argumentProvider);
37 | }
38 |
39 | async function getArguments (abi: TEth.Abi.Item, params: Record, argumentProvider: IArgumentProvider) {
40 | let args = await alot(abi.inputs).mapAsync(async (x, i) => {
41 | let arg = await getArgument(x, i, params, argumentProvider);
42 | if (arg != null) {
43 | return deserialize(x, arg)
44 | }
45 | return arg;
46 | }).toArrayAsync({ threads: 1 });
47 | return args;
48 | }
49 |
50 | function deserialize (abi: TEth.Abi.Input, value: any) {
51 | if (abi.type === 'bool') {
52 | if (value == null || value === '') {
53 | return false;
54 | }
55 | if (typeof value === 'string') {
56 | return value.toLowerCase() === 'true' || value === '1' || value === value;
57 | }
58 | return Boolean(value);
59 | }
60 | if (abi.type === 'address') {
61 | if (value == null || value === '') {
62 | return $address.ZERO;
63 | }
64 | return $require.Address(value, `${abi.name} should be a valid address`);
65 | }
66 | if (abi.type === 'string') {
67 | return String(value);
68 | }
69 | let bytesRgxMatch = /^bytes(?\d+)$/.exec(abi.type);
70 | if (bytesRgxMatch != null) {
71 | // should be a hex string, but lets skip checks for now, in case it could be any other valid bytes input, like ArrayBuffer, etc.
72 | return value;
73 | }
74 |
75 |
76 | let numberRgxMatch = /^u?int(?\d+)?$/.exec(abi.type);
77 | if (numberRgxMatch != null) {
78 | let size = Number(numberRgxMatch.groups?.size || '256');
79 | if (size <= 16) {
80 | return Number(value);
81 | }
82 | if (typeof value === 'number') {
83 | return BigInt(value);
84 | }
85 | return $bigint.parse(value);
86 | }
87 |
88 |
89 | if ($types.isArray(abi.type)) {
90 | $require.Array(value, `${abi.name} should be an array`);
91 |
92 | let abiItem = getArrayAbiItem(abi);
93 | return value.map(v => deserialize(abiItem, v));
94 | }
95 |
96 | if (Array.isArray(abi.components)) {
97 | if (value == null) {
98 | return null;
99 | }
100 | if (typeof value !== 'object') {
101 | throw new Error(`Argument "${abi.name}" is not an object(is ${typeof value})`);
102 | }
103 |
104 | return alot(abi.components).toDictionary(x => x.name, x => deserialize(x, value[x.name]));
105 | }
106 |
107 | return value;
108 | }
109 |
110 | function getArrayAbiItem (abi: TEth.Abi.Input) {
111 | let arrayBase = $abiType.array.getBaseType(abi.type);
112 | return {
113 | ...abi,
114 | type: arrayBase
115 | };
116 | }
117 |
118 | async function getValue (abi: TEth.Abi.Input, idx: number, params: Record) {
119 | let pfx = getPfx(abi, idx, params);
120 | if (pfx == null) {
121 | return null;
122 | }
123 |
124 | let x = params[pfx];
125 | if (x != null) {
126 | if (isJsonInput(abi, x)) {
127 | return parseJsonInput(abi, x);
128 | }
129 | if (isCsvInput(abi, x)) {
130 | return parseCsvInput(abi, x);
131 | }
132 | if (isFileInput(x)) {
133 | return await loadFileInput(x);
134 | }
135 | return x;
136 | }
137 |
138 | let obj = {};
139 | for (let key in params) {
140 | if (key.startsWith(pfx + '.')) {
141 | let value = params[key];
142 | let property = key.substring(pfx.length + 1);
143 | obj_setProperty(obj, property, value);
144 | }
145 | }
146 |
147 | return obj;
148 | }
149 |
150 | function getPfx (abi: TEth.Abi.Input, idx: number, params: Record) {
151 | let arr = [
152 | `arg${idx}`,
153 | `arg:${idx}`,
154 | `arg:${abi.name}`,
155 | `${abi.name}`
156 | ];
157 | for (let key in params) {
158 | for (let pfx of arr) {
159 | if (key.startsWith(pfx + '.') || key === pfx) {
160 | return pfx;
161 | }
162 | }
163 | }
164 | return null;
165 | }
166 | function isJsonInput (abi: TEth.Abi.Input, value: string) {
167 | return (Array.isArray(abi.components) || $types.isArray(abi.type)) && typeof value === 'string' && (value[0] === '{' || value[0] === '[');
168 | }
169 | function parseJsonInput (abi: TEth.Abi.Input, value: string) {
170 | try {
171 | return JSON.parse(value);
172 | } catch (error) {
173 | throw new Error(`Argument "${abi.name}" ${value} is not a valid JSON string. ${ error.message }`);
174 | }
175 | }
176 | function isCsvInput (abi: TEth.Abi.Input, value: string) {
177 | return $types.isArray(abi.type) && typeof value ==='string' && value.includes(',');
178 | }
179 | function parseCsvInput (abi: TEth.Abi.Input, value: string) {
180 | return value.split(',').map(x => x.trim());
181 | }
182 |
183 | let rgxFileInput = /^load\((?[^)]+\.\w+)\)(?[\.\w+]+)?$/
184 | function isFileInput (value: string) {
185 | return rgxFileInput.test(value);
186 | }
187 | async function loadFileInput (value: string) {
188 | let match = rgxFileInput.exec(value);
189 | let { path, getter } = match.groups!;
190 | let content = await File.readAsync(path);
191 | if (getter) {
192 | getter = getter.replace(/^\./g, '');
193 | return obj_getProperty(content, getter);
194 | }
195 | return content;
196 | }
197 |
198 | async function getArgument (abi: TEth.Abi.Input, idx: number, params: Record, argumentProvider: IArgumentProvider) {
199 | let value = await getValue(abi, idx, params);
200 | if (value != null) {
201 | return value;
202 | }
203 |
204 | return await argumentProvider.get(abi);
205 | }
206 | }
207 |
--------------------------------------------------------------------------------