├── 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 | [![Website Link](https://img.shields.io/badge/%F0%9F%8C%90-website-green.svg)](https://0xweb.org) 10 | [![Documentation Link](https://img.shields.io/badge/%E2%9D%93-documentation-green.svg)](https://docs.0xweb.org) 11 | [![NPM version](https://badge.fury.io/js/0xweb.svg)](http://badge.fury.io/js/0xweb) 12 | [![CircleCI](https://circleci.com/gh/0xweb-org/0xweb.svg?style=svg)](https://circleci.com/gh/0xweb-org/0xweb) 13 | 14 | | | | 15 | |--|--| 16 | |[Demo: Backend](https://github.com/0xweb-org/examples-backend) | [![CircleCI](https://dl.circleci.com/status-badge/img/gh/0xweb-org/examples-backend/tree/master.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/0xweb-org/examples-backend/tree/master) | 17 | |[Demo: Storage](https://github.com/0xweb-org/examples-storage) | [![CircleCI](https://dl.circleci.com/status-badge/img/gh/0xweb-org/examples-storage/tree/master.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/0xweb-org/examples-storage/tree/master) | 18 | |[Demo: Hardhat](https://github.com/0xweb-org/examples-hardhat) | [![CircleCI](https://dl.circleci.com/status-badge/img/gh/0xweb-org/examples-hardhat/tree/master.svg?style=svg)](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) | [![CircleCI](https://dl.circleci.com/status-badge/img/gh/0xweb-org/examples-price/tree/master.svg?style=svg)](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 | --------------------------------------------------------------------------------