├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── README.md ├── TODO ├── bin ├── dev ├── run └── run.cmd ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── settings.json ├── src ├── apis │ ├── createAccount.ts │ ├── esr │ │ ├── index.ts │ │ ├── manager.ts │ │ ├── session.ts │ │ ├── storage.ts │ │ ├── utils.ts │ │ └── websocket.ts │ ├── getExplorer.ts │ ├── getFaucets.ts │ ├── getProtonAvatar.ts │ ├── getRamPrice.ts │ ├── lightApi.ts │ └── uri │ │ ├── fetch.ts │ │ ├── handleUri.ts │ │ ├── parseUri.ts │ │ └── signUri.ts ├── commands │ ├── account │ │ ├── create.ts │ │ └── index.ts │ ├── action │ │ └── index.ts │ ├── block │ │ └── get.ts │ ├── boilerplate.ts │ ├── chain │ │ ├── get.ts │ │ ├── info.ts │ │ ├── list.ts │ │ └── set.ts │ ├── contract │ │ ├── abi.ts │ │ ├── clear.ts │ │ ├── enableinline.ts │ │ └── set.ts │ ├── encode │ │ ├── name.ts │ │ └── symbol.ts │ ├── faucet │ │ ├── claim.ts │ │ └── index.ts │ ├── generate │ │ ├── action.ts │ │ ├── contract.ts │ │ ├── inlineaction.ts │ │ └── table.ts │ ├── key │ │ ├── add.ts │ │ ├── generate.ts │ │ ├── get.ts │ │ ├── list.ts │ │ ├── lock.ts │ │ ├── remove.ts │ │ ├── reset.ts │ │ └── unlock.ts │ ├── msig │ │ ├── approve.ts │ │ ├── cancel.ts │ │ ├── exec.ts │ │ └── propose.ts │ ├── permission │ │ ├── index.ts │ │ ├── link.ts │ │ └── unlink.ts │ ├── psr │ │ └── index.ts │ ├── ram │ │ ├── buy.ts │ │ └── index.ts │ ├── rpc │ │ └── accountsbyauthorizers.ts │ ├── scan │ │ └── index.ts │ ├── system │ │ ├── buyram.ts │ │ ├── delegatebw.ts │ │ ├── newaccount.ts │ │ ├── setramlimit.ts │ │ └── undelegatebw.ts │ ├── table │ │ └── index.ts │ ├── transaction │ │ ├── get.ts │ │ ├── index.ts │ │ └── push.ts │ └── version.ts ├── constants.ts ├── core │ ├── flags │ │ └── index.ts │ └── generators │ │ ├── common.ts │ │ ├── constructorParameters.ts │ │ ├── contract.ts │ │ ├── imports.ts │ │ ├── index.ts │ │ ├── parameters.ts │ │ ├── settings.ts │ │ └── table.ts ├── hooks │ └── command_incomplete.ts ├── index.ts ├── storage │ ├── config.ts │ ├── encryptor.ts │ ├── networks.ts │ └── passwordManager.ts ├── templates │ └── contract │ │ ├── .gitignore │ │ ├── contract.ts │ │ ├── package.json │ │ ├── playground.ts │ │ └── tsconfig.json └── utils │ ├── detailsError.ts │ ├── extractContract.ts │ ├── fileManagement.ts │ ├── index.ts │ ├── integer.ts │ ├── permissions.ts │ ├── prompt.ts │ ├── resources.ts │ ├── sortRequiredAuth.ts │ ├── template.ts │ ├── validateName.ts │ └── wait.ts ├── test ├── commands │ ├── boilerplate.test.ts │ ├── network.test.ts │ └── version.test.ts ├── mocha.opts └── tsconfig.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /lib 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "oclif", 4 | "oclif-typescript" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *-debug.log 2 | *-error.log 3 | /.nyc_output 4 | /dist 5 | /lib 6 | /tmp 7 | node_modules 8 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | @proton/cli 2 | =================== 3 | 4 | Proton CLI 5 | 6 | [![oclif](https://img.shields.io/badge/cli-oclif-brightgreen.svg)](https://oclif.io) 7 | [![Version](https://img.shields.io/npm/v/@proton/cli.svg)](https://npmjs.org/package/@proton/cli) 8 | [![Downloads/week](https://img.shields.io/npm/dw/@proton/cli.svg)](https://npmjs.org/package/@proton/cli) 9 | [![License](https://img.shields.io/npm/l/@proton/cli.svg)](https://github.com/ProtonProtocol/proton-cli/blob/master/package.json) 10 | 11 | 12 | * [Installation](#installation) 13 | * [Install NodeJS](#install-nodejs) 14 | * [Usage](#usage) 15 | * [Commands](#commands) 16 | 17 | # Installation 18 | Install CLI (NPM) 19 | ``` 20 | npm i -g @proton/cli 21 | ``` 22 | 23 | Install CLI (Yarn) 24 | ``` 25 | yarn global add @proton/cli 26 | ``` 27 | 28 | If you get a missing write access error on Mac/Linux, first run: 29 | ``` 30 | sudo chown -R $USER /usr/local/lib/node_modules 31 | sudo chown -R $USER /usr/local/bin 32 | ``` 33 | 34 | # Install NodeJS 35 | 36 | > You can skip this step if you already have NodeJS installed 37 | 38 | **1. Install NVM** 39 | 40 | MacOS/Linux/WSL: 41 | 42 | ``` 43 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash 44 | ``` 45 | 46 | Windows 7/10/11: 47 | 48 | Download nvm-setup.zip and run it [here](https://github.com/coreybutler/nvm-windows/releases). After installation, open a new PowerShell window as administrator. 49 | 50 | **2. Install NodeJS** 51 | ``` 52 | nvm install 16 53 | nvm use 16 54 | ``` 55 | 56 | # Usage 57 | 58 | ```sh-session 59 | $ npm install -g @proton/cli 60 | $ proton COMMAND 61 | running command... 62 | $ proton (--version) 63 | @proton/cli/0.1.94 darwin-arm64 node-v22.14.0 64 | $ proton --help [COMMAND] 65 | USAGE 66 | $ proton COMMAND 67 | ... 68 | ``` 69 | 70 | # Commands 71 | 72 | * [`proton account ACCOUNT`](#proton-account-account) 73 | * [`proton account:create ACCOUNT`](#proton-accountcreate-account) 74 | * [`proton action CONTRACT [ACTION] [DATA] [AUTHORIZATION]`](#proton-action-contract-action-data-authorization) 75 | * [`proton block:get BLOCKNUMBER`](#proton-blockget-blocknumber) 76 | * [`proton boilerplate [FOLDER]`](#proton-boilerplate-folder) 77 | * [`proton chain:get`](#proton-chainget) 78 | * [`proton chain:info`](#proton-chaininfo) 79 | * [`proton chain:list`](#proton-chainlist) 80 | * [`proton chain:set [CHAIN]`](#proton-chainset-chain) 81 | * [`proton contract:abi ACCOUNT`](#proton-contractabi-account) 82 | * [`proton contract:clear ACCOUNT`](#proton-contractclear-account) 83 | * [`proton contract:enableinline ACCOUNT`](#proton-contractenableinline-account) 84 | * [`proton contract:set ACCOUNT SOURCE`](#proton-contractset-account-source) 85 | * [`proton encode:name ACCOUNT`](#proton-encodename-account) 86 | * [`proton encode:symbol SYMBOL PRECISION`](#proton-encodesymbol-symbol-precision) 87 | * [`proton faucet`](#proton-faucet) 88 | * [`proton faucet:claim SYMBOL AUTHORIZATION`](#proton-faucetclaim-symbol-authorization) 89 | * [`proton generate:action`](#proton-generateaction) 90 | * [`proton generate:contract CONTRACTNAME`](#proton-generatecontract-contractname) 91 | * [`proton generate:inlineaction ACTIONNAME`](#proton-generateinlineaction-actionname) 92 | * [`proton generate:table TABLENAME`](#proton-generatetable-tablename) 93 | * [`proton help [COMMAND]`](#proton-help-command) 94 | * [`proton key:add [PRIVATEKEY]`](#proton-keyadd-privatekey) 95 | * [`proton key:generate`](#proton-keygenerate) 96 | * [`proton key:get PUBLICKEY`](#proton-keyget-publickey) 97 | * [`proton key:list`](#proton-keylist) 98 | * [`proton key:lock`](#proton-keylock) 99 | * [`proton key:remove [PRIVATEKEY]`](#proton-keyremove-privatekey) 100 | * [`proton key:reset`](#proton-keyreset) 101 | * [`proton key:unlock [PASSWORD]`](#proton-keyunlock-password) 102 | * [`proton msig:approve PROPOSER PROPOSAL AUTH`](#proton-msigapprove-proposer-proposal-auth) 103 | * [`proton msig:cancel PROPOSALNAME AUTH`](#proton-msigcancel-proposalname-auth) 104 | * [`proton msig:exec PROPOSER PROPOSAL AUTH`](#proton-msigexec-proposer-proposal-auth) 105 | * [`proton msig:propose PROPOSALNAME ACTIONS AUTH`](#proton-msigpropose-proposalname-actions-auth) 106 | * [`proton network`](#proton-network) 107 | * [`proton permission ACCOUNT`](#proton-permission-account) 108 | * [`proton permission:link ACCOUNT PERMISSION CONTRACT [ACTION]`](#proton-permissionlink-account-permission-contract-action) 109 | * [`proton permission:unlink ACCOUNT CONTRACT [ACTION]`](#proton-permissionunlink-account-contract-action) 110 | * [`proton psr URI`](#proton-psr-uri) 111 | * [`proton ram`](#proton-ram) 112 | * [`proton ram:buy BUYER RECEIVER BYTES`](#proton-rambuy-buyer-receiver-bytes) 113 | * [`proton rpc:accountsbyauthorizers AUTHORIZATIONS [KEYS]`](#proton-rpcaccountsbyauthorizers-authorizations-keys) 114 | * [`proton scan ACCOUNT`](#proton-scan-account) 115 | * [`proton table CONTRACT [TABLE] [SCOPE]`](#proton-table-contract-table-scope) 116 | * [`proton transaction JSON`](#proton-transaction-json) 117 | * [`proton transaction:get ID`](#proton-transactionget-id) 118 | * [`proton transaction:push TRANSACTION`](#proton-transactionpush-transaction) 119 | * [`proton version`](#proton-version) 120 | 121 | ## `proton account ACCOUNT` 122 | 123 | Get Account Information 124 | 125 | ``` 126 | USAGE 127 | $ proton account [ACCOUNT] [-r] [-t] 128 | 129 | FLAGS 130 | -r, --raw 131 | -t, --tokens Show token balances 132 | 133 | DESCRIPTION 134 | Get Account Information 135 | ``` 136 | 137 | _See code: [lib/commands/account/index.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/account/index.js)_ 138 | 139 | ## `proton account:create ACCOUNT` 140 | 141 | Create New Account 142 | 143 | ``` 144 | USAGE 145 | $ proton account:create [ACCOUNT] 146 | 147 | DESCRIPTION 148 | Create New Account 149 | ``` 150 | 151 | _See code: [lib/commands/account/create.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/account/create.js)_ 152 | 153 | ## `proton action CONTRACT [ACTION] [DATA] [AUTHORIZATION]` 154 | 155 | Execute Action 156 | 157 | ``` 158 | USAGE 159 | $ proton action [CONTRACT] [ACTION] [DATA] [AUTHORIZATION] 160 | 161 | ARGUMENTS 162 | CONTRACT 163 | ACTION 164 | DATA 165 | AUTHORIZATION Account to authorize with 166 | 167 | DESCRIPTION 168 | Execute Action 169 | ``` 170 | 171 | _See code: [lib/commands/action/index.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/action/index.js)_ 172 | 173 | ## `proton block:get BLOCKNUMBER` 174 | 175 | Get Block 176 | 177 | ``` 178 | USAGE 179 | $ proton block:get [BLOCKNUMBER] 180 | 181 | DESCRIPTION 182 | Get Block 183 | ``` 184 | 185 | _See code: [lib/commands/block/get.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/block/get.js)_ 186 | 187 | ## `proton boilerplate [FOLDER]` 188 | 189 | Boilerplate a new Proton Project with contract, frontend and tests 190 | 191 | ``` 192 | USAGE 193 | $ proton boilerplate [FOLDER] [-h] 194 | 195 | FLAGS 196 | -h, --help show CLI help 197 | 198 | DESCRIPTION 199 | Boilerplate a new Proton Project with contract, frontend and tests 200 | ``` 201 | 202 | _See code: [lib/commands/boilerplate.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/boilerplate.js)_ 203 | 204 | ## `proton chain:get` 205 | 206 | Get Current Chain 207 | 208 | ``` 209 | USAGE 210 | $ proton chain:get 211 | 212 | DESCRIPTION 213 | Get Current Chain 214 | 215 | ALIASES 216 | $ proton network 217 | ``` 218 | 219 | _See code: [lib/commands/chain/get.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/chain/get.js)_ 220 | 221 | ## `proton chain:info` 222 | 223 | Get Chain Info 224 | 225 | ``` 226 | USAGE 227 | $ proton chain:info 228 | 229 | DESCRIPTION 230 | Get Chain Info 231 | ``` 232 | 233 | _See code: [lib/commands/chain/info.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/chain/info.js)_ 234 | 235 | ## `proton chain:list` 236 | 237 | All Networks 238 | 239 | ``` 240 | USAGE 241 | $ proton chain:list 242 | 243 | DESCRIPTION 244 | All Networks 245 | ``` 246 | 247 | _See code: [lib/commands/chain/list.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/chain/list.js)_ 248 | 249 | ## `proton chain:set [CHAIN]` 250 | 251 | Set Chain 252 | 253 | ``` 254 | USAGE 255 | $ proton chain:set [CHAIN] 256 | 257 | ARGUMENTS 258 | CHAIN Specific chain 259 | 260 | DESCRIPTION 261 | Set Chain 262 | ``` 263 | 264 | _See code: [lib/commands/chain/set.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/chain/set.js)_ 265 | 266 | ## `proton contract:abi ACCOUNT` 267 | 268 | Get Contract ABI 269 | 270 | ``` 271 | USAGE 272 | $ proton contract:abi [ACCOUNT] 273 | 274 | DESCRIPTION 275 | Get Contract ABI 276 | ``` 277 | 278 | _See code: [lib/commands/contract/abi.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/contract/abi.js)_ 279 | 280 | ## `proton contract:clear ACCOUNT` 281 | 282 | Clean Contract 283 | 284 | ``` 285 | USAGE 286 | $ proton contract:clear [ACCOUNT] [-a] [-w] 287 | 288 | FLAGS 289 | -a, --abiOnly Only remove ABI 290 | -w, --wasmOnly Only remove WASM 291 | 292 | DESCRIPTION 293 | Clean Contract 294 | ``` 295 | 296 | _See code: [lib/commands/contract/clear.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/contract/clear.js)_ 297 | 298 | ## `proton contract:enableinline ACCOUNT` 299 | 300 | Enable Inline Actions on a Contract 301 | 302 | ``` 303 | USAGE 304 | $ proton contract:enableinline [ACCOUNT] [-p ] 305 | 306 | ARGUMENTS 307 | ACCOUNT Contract account to enable 308 | 309 | FLAGS 310 | -p, --authorization= Use a specific authorization other than contract@active 311 | 312 | DESCRIPTION 313 | Enable Inline Actions on a Contract 314 | ``` 315 | 316 | _See code: [lib/commands/contract/enableinline.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/contract/enableinline.js)_ 317 | 318 | ## `proton contract:set ACCOUNT SOURCE` 319 | 320 | Deploy Contract (WASM + ABI) 321 | 322 | ``` 323 | USAGE 324 | $ proton contract:set [ACCOUNT] [SOURCE] [-c] [-a] [-w] [-s] 325 | 326 | FLAGS 327 | -a, --abiOnly Only deploy ABI 328 | -c, --clear Removes WASM + ABI from contract 329 | -s, --disableInline Disable inline actions on contract 330 | -w, --wasmOnly Only deploy WASM 331 | 332 | DESCRIPTION 333 | Deploy Contract (WASM + ABI) 334 | ``` 335 | 336 | _See code: [lib/commands/contract/set.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/contract/set.js)_ 337 | 338 | ## `proton encode:name ACCOUNT` 339 | 340 | Encode Name 341 | 342 | ``` 343 | USAGE 344 | $ proton encode:name [ACCOUNT] 345 | 346 | DESCRIPTION 347 | Encode Name 348 | ``` 349 | 350 | _See code: [lib/commands/encode/name.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/encode/name.js)_ 351 | 352 | ## `proton encode:symbol SYMBOL PRECISION` 353 | 354 | Encode Symbol 355 | 356 | ``` 357 | USAGE 358 | $ proton encode:symbol [SYMBOL] [PRECISION] 359 | 360 | DESCRIPTION 361 | Encode Symbol 362 | ``` 363 | 364 | _See code: [lib/commands/encode/symbol.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/encode/symbol.js)_ 365 | 366 | ## `proton faucet` 367 | 368 | List all faucets 369 | 370 | ``` 371 | USAGE 372 | $ proton faucet 373 | 374 | DESCRIPTION 375 | List all faucets 376 | ``` 377 | 378 | _See code: [lib/commands/faucet/index.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/faucet/index.js)_ 379 | 380 | ## `proton faucet:claim SYMBOL AUTHORIZATION` 381 | 382 | Claim faucet 383 | 384 | ``` 385 | USAGE 386 | $ proton faucet:claim [SYMBOL] [AUTHORIZATION] 387 | 388 | ARGUMENTS 389 | SYMBOL 390 | AUTHORIZATION Authorization like account1@active 391 | 392 | DESCRIPTION 393 | Claim faucet 394 | ``` 395 | 396 | _See code: [lib/commands/faucet/claim.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/faucet/claim.js)_ 397 | 398 | ## `proton generate:action` 399 | 400 | Add extra actions to the smart contract 401 | 402 | ``` 403 | USAGE 404 | $ proton generate:action [-o ] [-c ] 405 | 406 | FLAGS 407 | -c, --contract= The name of the contract for table. 1-12 chars, only lowercase a-z and numbers 1-5 are 408 | possible 409 | -o, --output= The relative path to folder the the contract should be located. Current folder by default. 410 | 411 | DESCRIPTION 412 | Add extra actions to the smart contract 413 | ``` 414 | 415 | _See code: [lib/commands/generate/action.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/generate/action.js)_ 416 | 417 | ## `proton generate:contract CONTRACTNAME` 418 | 419 | Create new smart contract 420 | 421 | ``` 422 | USAGE 423 | $ proton generate:contract [CONTRACTNAME] [-o ] 424 | 425 | ARGUMENTS 426 | CONTRACTNAME The name of the contract. 1-12 chars, only lowercase a-z and numbers 1-5 are possible 427 | 428 | FLAGS 429 | -o, --output= The relative path to folder the the contract should be located. Current folder by default. 430 | 431 | DESCRIPTION 432 | Create new smart contract 433 | ``` 434 | 435 | _See code: [lib/commands/generate/contract.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/generate/contract.js)_ 436 | 437 | ## `proton generate:inlineaction ACTIONNAME` 438 | 439 | Add inline action for the smart contract 440 | 441 | ``` 442 | USAGE 443 | $ proton generate:inlineaction [ACTIONNAME] [-o ] [-c ] 444 | 445 | ARGUMENTS 446 | ACTIONNAME The name of the inline action's class. 447 | 448 | FLAGS 449 | -c, --contract= The name of the contract for table. 1-12 chars, only lowercase a-z and numbers 1-5 are 450 | possible 451 | -o, --output= The relative path to folder the the contract should be located. Current folder by default. 452 | 453 | DESCRIPTION 454 | Add inline action for the smart contract 455 | ``` 456 | 457 | _See code: [lib/commands/generate/inlineaction.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/generate/inlineaction.js)_ 458 | 459 | ## `proton generate:table TABLENAME` 460 | 461 | Add table for the smart contract 462 | 463 | ``` 464 | USAGE 465 | $ proton generate:table [TABLENAME] [-t ] [-s] [-o ] [-c ] 466 | 467 | ARGUMENTS 468 | TABLENAME The name of the contract's table. 1-12 chars, only lowercase a-z and numbers 1-5 are possible 469 | 470 | FLAGS 471 | -c, --contract= The name of the contract for table. 1-12 chars, only lowercase a-z and numbers 1-5 are 472 | possible 473 | -o, --output= The relative path to folder the the contract should be located. Current folder by default. 474 | -s, --singleton Create a singleton table? 475 | -t, --class= The name of Typescript class for the table 476 | 477 | DESCRIPTION 478 | Add table for the smart contract 479 | ``` 480 | 481 | _See code: [lib/commands/generate/table.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/generate/table.js)_ 482 | 483 | ## `proton help [COMMAND]` 484 | 485 | display help for proton 486 | 487 | ``` 488 | USAGE 489 | $ proton help [COMMAND] [--all] 490 | 491 | ARGUMENTS 492 | COMMAND command to show help for 493 | 494 | FLAGS 495 | --all see all commands in CLI 496 | 497 | DESCRIPTION 498 | display help for proton 499 | ``` 500 | 501 | _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.3.1/src/commands/help.ts)_ 502 | 503 | ## `proton key:add [PRIVATEKEY]` 504 | 505 | Manage Keys 506 | 507 | ``` 508 | USAGE 509 | $ proton key:add [PRIVATEKEY] 510 | 511 | DESCRIPTION 512 | Manage Keys 513 | ``` 514 | 515 | _See code: [lib/commands/key/add.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/key/add.js)_ 516 | 517 | ## `proton key:generate` 518 | 519 | Generate Key 520 | 521 | ``` 522 | USAGE 523 | $ proton key:generate 524 | 525 | DESCRIPTION 526 | Generate Key 527 | ``` 528 | 529 | _See code: [lib/commands/key/generate.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/key/generate.js)_ 530 | 531 | ## `proton key:get PUBLICKEY` 532 | 533 | Find private key for public key 534 | 535 | ``` 536 | USAGE 537 | $ proton key:get [PUBLICKEY] 538 | 539 | DESCRIPTION 540 | Find private key for public key 541 | ``` 542 | 543 | _See code: [lib/commands/key/get.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/key/get.js)_ 544 | 545 | ## `proton key:list` 546 | 547 | List All Key 548 | 549 | ``` 550 | USAGE 551 | $ proton key:list 552 | 553 | DESCRIPTION 554 | List All Key 555 | ``` 556 | 557 | _See code: [lib/commands/key/list.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/key/list.js)_ 558 | 559 | ## `proton key:lock` 560 | 561 | Lock Keys with password 562 | 563 | ``` 564 | USAGE 565 | $ proton key:lock 566 | 567 | DESCRIPTION 568 | Lock Keys with password 569 | ``` 570 | 571 | _See code: [lib/commands/key/lock.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/key/lock.js)_ 572 | 573 | ## `proton key:remove [PRIVATEKEY]` 574 | 575 | Remove Key 576 | 577 | ``` 578 | USAGE 579 | $ proton key:remove [PRIVATEKEY] 580 | 581 | DESCRIPTION 582 | Remove Key 583 | ``` 584 | 585 | _See code: [lib/commands/key/remove.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/key/remove.js)_ 586 | 587 | ## `proton key:reset` 588 | 589 | Reset password (Caution: deletes all private keys stored) 590 | 591 | ``` 592 | USAGE 593 | $ proton key:reset 594 | 595 | DESCRIPTION 596 | Reset password (Caution: deletes all private keys stored) 597 | ``` 598 | 599 | _See code: [lib/commands/key/reset.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/key/reset.js)_ 600 | 601 | ## `proton key:unlock [PASSWORD]` 602 | 603 | Unlock all keys (Caution: Your keys will be stored in plaintext on disk) 604 | 605 | ``` 606 | USAGE 607 | $ proton key:unlock [PASSWORD] 608 | 609 | DESCRIPTION 610 | Unlock all keys (Caution: Your keys will be stored in plaintext on disk) 611 | ``` 612 | 613 | _See code: [lib/commands/key/unlock.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/key/unlock.js)_ 614 | 615 | ## `proton msig:approve PROPOSER PROPOSAL AUTH` 616 | 617 | Multisig Approve 618 | 619 | ``` 620 | USAGE 621 | $ proton msig:approve [PROPOSER] [PROPOSAL] [AUTH] 622 | 623 | DESCRIPTION 624 | Multisig Approve 625 | ``` 626 | 627 | _See code: [lib/commands/msig/approve.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/msig/approve.js)_ 628 | 629 | ## `proton msig:cancel PROPOSALNAME AUTH` 630 | 631 | Multisig Cancel 632 | 633 | ``` 634 | USAGE 635 | $ proton msig:cancel [PROPOSALNAME] [AUTH] 636 | 637 | DESCRIPTION 638 | Multisig Cancel 639 | ``` 640 | 641 | _See code: [lib/commands/msig/cancel.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/msig/cancel.js)_ 642 | 643 | ## `proton msig:exec PROPOSER PROPOSAL AUTH` 644 | 645 | Multisig Execute 646 | 647 | ``` 648 | USAGE 649 | $ proton msig:exec [PROPOSER] [PROPOSAL] [AUTH] 650 | 651 | DESCRIPTION 652 | Multisig Execute 653 | ``` 654 | 655 | _See code: [lib/commands/msig/exec.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/msig/exec.js)_ 656 | 657 | ## `proton msig:propose PROPOSALNAME ACTIONS AUTH` 658 | 659 | Multisig Propose 660 | 661 | ``` 662 | USAGE 663 | $ proton msig:propose [PROPOSALNAME] [ACTIONS] [AUTH] [-b ] [-x ] 664 | 665 | FLAGS 666 | -b, --blocksBehind= [default: 30] 667 | -x, --expireSeconds= [default: 604800] 668 | 669 | DESCRIPTION 670 | Multisig Propose 671 | ``` 672 | 673 | _See code: [lib/commands/msig/propose.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/msig/propose.js)_ 674 | 675 | ## `proton network` 676 | 677 | Get Current Chain 678 | 679 | ``` 680 | USAGE 681 | $ proton network 682 | 683 | DESCRIPTION 684 | Get Current Chain 685 | 686 | ALIASES 687 | $ proton network 688 | ``` 689 | 690 | ## `proton permission ACCOUNT` 691 | 692 | Update Permission 693 | 694 | ``` 695 | USAGE 696 | $ proton permission [ACCOUNT] 697 | 698 | ARGUMENTS 699 | ACCOUNT Account to modify 700 | 701 | DESCRIPTION 702 | Update Permission 703 | ``` 704 | 705 | _See code: [lib/commands/permission/index.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/permission/index.js)_ 706 | 707 | ## `proton permission:link ACCOUNT PERMISSION CONTRACT [ACTION]` 708 | 709 | Link Auth 710 | 711 | ``` 712 | USAGE 713 | $ proton permission:link [ACCOUNT] [PERMISSION] [CONTRACT] [ACTION] [-p ] 714 | 715 | FLAGS 716 | -p, --permission= Permission to sign with (e.g. account@active) 717 | 718 | DESCRIPTION 719 | Link Auth 720 | ``` 721 | 722 | _See code: [lib/commands/permission/link.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/permission/link.js)_ 723 | 724 | ## `proton permission:unlink ACCOUNT CONTRACT [ACTION]` 725 | 726 | Unlink Auth 727 | 728 | ``` 729 | USAGE 730 | $ proton permission:unlink [ACCOUNT] [CONTRACT] [ACTION] [-p ] 731 | 732 | FLAGS 733 | -p, --permission= 734 | 735 | DESCRIPTION 736 | Unlink Auth 737 | ``` 738 | 739 | _See code: [lib/commands/permission/unlink.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/permission/unlink.js)_ 740 | 741 | ## `proton psr URI` 742 | 743 | Create Session 744 | 745 | ``` 746 | USAGE 747 | $ proton psr [URI] 748 | 749 | DESCRIPTION 750 | Create Session 751 | ``` 752 | 753 | _See code: [lib/commands/psr/index.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/psr/index.js)_ 754 | 755 | ## `proton ram` 756 | 757 | List Ram price 758 | 759 | ``` 760 | USAGE 761 | $ proton ram 762 | 763 | DESCRIPTION 764 | List Ram price 765 | ``` 766 | 767 | _See code: [lib/commands/ram/index.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/ram/index.js)_ 768 | 769 | ## `proton ram:buy BUYER RECEIVER BYTES` 770 | 771 | Claim faucet 772 | 773 | ``` 774 | USAGE 775 | $ proton ram:buy [BUYER] [RECEIVER] [BYTES] [-p ] 776 | 777 | ARGUMENTS 778 | BUYER Account paying for RAM 779 | RECEIVER Account receiving RAM 780 | BYTES Bytes of RAM to purchase 781 | 782 | FLAGS 783 | -p, --authorization= Use a specific authorization other than buyer@active 784 | 785 | DESCRIPTION 786 | Claim faucet 787 | ``` 788 | 789 | _See code: [lib/commands/ram/buy.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/ram/buy.js)_ 790 | 791 | ## `proton rpc:accountsbyauthorizers AUTHORIZATIONS [KEYS]` 792 | 793 | Get Accounts by Authorization 794 | 795 | ``` 796 | USAGE 797 | $ proton rpc:accountsbyauthorizers [AUTHORIZATIONS] [KEYS] 798 | 799 | DESCRIPTION 800 | Get Accounts by Authorization 801 | ``` 802 | 803 | _See code: [lib/commands/rpc/accountsbyauthorizers.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/rpc/accountsbyauthorizers.js)_ 804 | 805 | ## `proton scan ACCOUNT` 806 | 807 | Open Account in Proton Scan 808 | 809 | ``` 810 | USAGE 811 | $ proton scan [ACCOUNT] 812 | 813 | DESCRIPTION 814 | Open Account in Proton Scan 815 | ``` 816 | 817 | _See code: [lib/commands/scan/index.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/scan/index.js)_ 818 | 819 | ## `proton table CONTRACT [TABLE] [SCOPE]` 820 | 821 | Get Table Storage Rows 822 | 823 | ``` 824 | USAGE 825 | $ proton table [CONTRACT] [TABLE] [SCOPE] [-l ] [-u ] [-k ] [-r] [-p] [-c ] 826 | [-i ] 827 | 828 | FLAGS 829 | -c, --limit= [default: 100] 830 | -i, --indexPosition= [default: 1] 831 | -k, --keyType= 832 | -l, --lowerBound= 833 | -p, --showPayer 834 | -r, --reverse 835 | -u, --upperBound= 836 | 837 | DESCRIPTION 838 | Get Table Storage Rows 839 | ``` 840 | 841 | _See code: [lib/commands/table/index.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/table/index.js)_ 842 | 843 | ## `proton transaction JSON` 844 | 845 | Execute Transaction 846 | 847 | ``` 848 | USAGE 849 | $ proton transaction [JSON] 850 | 851 | DESCRIPTION 852 | Execute Transaction 853 | ``` 854 | 855 | _See code: [lib/commands/transaction/index.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/transaction/index.js)_ 856 | 857 | ## `proton transaction:get ID` 858 | 859 | Get Transaction by Transaction ID 860 | 861 | ``` 862 | USAGE 863 | $ proton transaction:get [ID] 864 | 865 | DESCRIPTION 866 | Get Transaction by Transaction ID 867 | ``` 868 | 869 | _See code: [lib/commands/transaction/get.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/transaction/get.js)_ 870 | 871 | ## `proton transaction:push TRANSACTION` 872 | 873 | Push Transaction 874 | 875 | ``` 876 | USAGE 877 | $ proton transaction:push [TRANSACTION] [-u ] 878 | 879 | FLAGS 880 | -u, --endpoint= Your RPC endpoint 881 | 882 | DESCRIPTION 883 | Push Transaction 884 | ``` 885 | 886 | _See code: [lib/commands/transaction/push.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/transaction/push.js)_ 887 | 888 | ## `proton version` 889 | 890 | Version of CLI 891 | 892 | ``` 893 | USAGE 894 | $ proton version 895 | 896 | DESCRIPTION 897 | Version of CLI 898 | ``` 899 | 900 | _See code: [lib/commands/version.js](https://github.com/ProtonProtocol/proton-cli/blob/v0.1.94/lib/commands/version.js)_ 901 | 902 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | Multisig actions 2 | Easy transfers -------------------------------------------------------------------------------- /bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('@oclif/command').run() 4 | .then(require('@oclif/command/flush')) 5 | .catch(require('@oclif/errors/handle')) 6 | -------------------------------------------------------------------------------- /bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const oclif = require('@oclif/core') 4 | 5 | oclif.run().then(require('@oclif/core/flush')).catch(require('@oclif/core/handle')) -------------------------------------------------------------------------------- /bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@proton/cli", 3 | "description": "Proton CLI", 4 | "version": "0.1.94", 5 | "author": "Syed Jafri @jafri", 6 | "bin": { 7 | "proton": "./bin/run" 8 | }, 9 | "bugs": "https://github.com/ProtonProtocol/proton-cli/issues", 10 | "files": [ 11 | "/bin", 12 | "/lib", 13 | "/npm-shrinkwrap.json", 14 | "/oclif.manifest.json" 15 | ], 16 | "homepage": "https://github.com/ProtonProtocol/proton-cli", 17 | "keywords": [ 18 | "oclif" 19 | ], 20 | "license": "MIT", 21 | "main": "lib/index.js", 22 | "oclif": { 23 | "commands": "./lib/commands", 24 | "bin": "proton", 25 | "plugins": [ 26 | "@oclif/plugin-help" 27 | ], 28 | "flexibleTaxonomy": true, 29 | "hooks": { 30 | "command_incomplete": "./lib/hooks/command_incomplete.js" 31 | }, 32 | "topics": { 33 | "generate": { 34 | "description": "Tools to generate smart contracts" 35 | } 36 | } 37 | }, 38 | "repository": "ProtonProtocol/proton-cli", 39 | "scripts": { 40 | "build": "npm run prepack && npm run postpack", 41 | "postpack": "rm -f oclif.manifest.json", 42 | "posttest": "eslint . --ext .ts --config .eslintrc", 43 | "prepack": "rm -rf lib && tsc -b && npx oclif manifest && npx oclif readme && shx cp -r src/templates lib", 44 | "testbase": "nyc --extension .ts mocha --forbid-only \"test/**/*.test.ts\"", 45 | "test": "npm run testbase -- \"test/**/*.test.ts\"", 46 | "testsome": "nyc --extension .ts mocha --forbid-only \"test/**/*.test.ts\" --exclude \"**/boilerplate.test.ts\"", 47 | "version": "npx oclif readme && git add README.md" 48 | }, 49 | "types": "lib/index.d.ts", 50 | "dependencies": { 51 | "@bloks/numbers": "^26.1.9", 52 | "@greymass/eosio": "^0.5.5", 53 | "@inquirer/input": "^0.0.18-alpha.0", 54 | "@oclif/command": "^1.8.16", 55 | "@oclif/config": "^1.18.3", 56 | "@oclif/core": "^1.7.0", 57 | "@oclif/plugin-help": "^3", 58 | "@proton/api": "^26.4.0", 59 | "@proton/js": "^27.5.1", 60 | "@proton/light-api": "^3.3.1", 61 | "@proton/link": "^4.1.10", 62 | "@proton/mnemonic": "^0.5.0", 63 | "@proton/signing-request": "^4.1.10", 64 | "@proton/wrap-constants": "^0.2.69", 65 | "bignumber.js": "^9.0.2", 66 | "colors": "^1.4.0", 67 | "conf": "^10.1.2", 68 | "ejs": "^3.1.6", 69 | "elliptic": "^6.5.4", 70 | "inquirer": "^8.2.2", 71 | "isomorphic-git": "^1.8.0", 72 | "lodash.isequal": "^4.5.0", 73 | "open": "^8.4.0", 74 | "pako": "^2.0.4", 75 | "qrcode": "^1.5.0", 76 | "rimraf": "^3.0.2", 77 | "shelljs": "^0.8.5", 78 | "text-encoding-shim": "^1.0.5", 79 | "ts-dedent": "^2.2.0", 80 | "ts-morph": "^14.0.0", 81 | "typescript": "^4.6", 82 | "uuid": "^8.3.2", 83 | "ws": "^8.5.0" 84 | }, 85 | "devDependencies": { 86 | "@oclif/test": "^1", 87 | "@types/chai": "^4.2.14", 88 | "@types/debug": "^4.1.5", 89 | "@types/ejs": "^3.1.0", 90 | "@types/elliptic": "^6.4.12", 91 | "@types/ini": "^1.3.30", 92 | "@types/inquirer": "^8.2.1", 93 | "@types/lodash.isequal": "^4.5.6", 94 | "@types/mocha": "^9.1.0", 95 | "@types/node": "^14", 96 | "@types/pako": "^1.0.3", 97 | "@types/rimraf": "^3.0.0", 98 | "@types/shelljs": "^0.8.11", 99 | "@types/uuid": "^8.3.4", 100 | "@types/ws": "^8.5.3", 101 | "chai": "^4", 102 | "eslint": "^7.0", 103 | "eslint-config-oclif": "^4.0", 104 | "eslint-config-oclif-typescript": "^1.0.2", 105 | "globby": "^10", 106 | "ini": "^2.0.0", 107 | "mocha": "^9.2.2", 108 | "nyc": "^15.1.0", 109 | "oclif": "^3.0.0", 110 | "rxjs": "^7.5.5", 111 | "shx": "^0.3.4", 112 | "ts-node": "^8", 113 | "tslib": "^1" 114 | }, 115 | "overrides": { 116 | "@oclif/core": { 117 | "ejs": { 118 | "jake": { 119 | "async": "3.2.3" 120 | } 121 | } 122 | } 123 | }, 124 | "engines": { 125 | "node": ">=14.0.0" 126 | } 127 | } -------------------------------------------------------------------------------- /settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "currentNetwork": { 3 | "chain": "proton", 4 | "endpoints": [ 5 | "https://proton.greymass.com" 6 | ] 7 | } 8 | } -------------------------------------------------------------------------------- /src/apis/createAccount.ts: -------------------------------------------------------------------------------- 1 | import { config } from "../storage/config"; 2 | import fetch from 'cross-fetch' 3 | 4 | export async function postData (url: string, data = {}): Promise { 5 | const response = await fetch(url, { 6 | method: 'POST', 7 | cache: 'no-cache', 8 | headers: { 9 | 'Content-Type': 'application/json' 10 | }, 11 | body: JSON.stringify(data) // body data type must match "Content-Type" header 12 | }); 13 | return response.json(); // parses JSON response into native JavaScript objects 14 | } 15 | 16 | const getMetalEndpoint = (chain: string) => { 17 | if (chain === 'proton') { 18 | return 'https://identity.api.prod.metalx.com' 19 | } else if (chain === 'proton-test') { 20 | return 'https://identity.api.dev.metalx.com' 21 | } else { 22 | throw new Error('Can only create new account on proton or proton testnet') 23 | } 24 | } 25 | 26 | export const createAccount = async (params: { 27 | email: string, 28 | name: string, 29 | chainAccount: string, 30 | activePublicKey: string, 31 | ownerPublicKey: string, 32 | verificationCode?: string 33 | }): Promise => { 34 | const metalEndpoint = getMetalEndpoint(config.get('currentChain')) 35 | return postData(`${metalEndpoint}/v2/users/create`, params) 36 | } -------------------------------------------------------------------------------- /src/apis/esr/index.ts: -------------------------------------------------------------------------------- 1 | export * from './manager' 2 | export * from './session' 3 | export * from './storage' 4 | export * from './utils' 5 | export * from './websocket' -------------------------------------------------------------------------------- /src/apis/esr/manager.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 3 | // @ts-nocheck 4 | 5 | import { Bytes, PrivateKey, PublicKey, Serializer } from '@greymass/eosio' 6 | import { SealedMessage } from '@proton/link' 7 | import { IProtonLinkSessionManagerSessionExtended } from '@/types' 8 | 9 | import RobustWebSocket from './websocket' 10 | import { ProtonLinkSessionManagerSession } from './session' 11 | import { ProtonLinkSessionManagerStorage } from './storage' 12 | import { unsealMessage } from './utils' 13 | 14 | export interface ProtonLinkSessionManagerEventHander { 15 | onIncomingRequest(payload: string, session: IProtonLinkSessionManagerSessionExtended): void 16 | onSocketEvent?(type: string, event: any): void 17 | } 18 | 19 | export interface ProtonLinkSessionManagerOptions { 20 | handler: ProtonLinkSessionManagerEventHander 21 | storage: ProtonLinkSessionManagerStorage 22 | } 23 | 24 | export class ProtonLinkSessionManager { 25 | private handler: ProtonLinkSessionManagerEventHander 26 | public storage: ProtonLinkSessionManagerStorage 27 | 28 | private socket?: RobustWebSocket 29 | 30 | public constructor(options: ProtonLinkSessionManagerOptions) { 31 | this.handler = options.handler 32 | this.storage = options.storage 33 | } 34 | 35 | public addSession(session: ProtonLinkSessionManagerSession) { 36 | this.storage.add(session) 37 | } 38 | 39 | public clearSessions() { 40 | this.storage.clear() 41 | } 42 | 43 | public removeSession(session: ProtonLinkSessionManagerSession) { 44 | this.storage.remove(session) 45 | } 46 | 47 | public updateLastUsed(publicKey: PublicKey) { 48 | this.storage.updateLastUsed(publicKey) 49 | } 50 | 51 | public connect() { 52 | return new Promise((resolve, reject) => { 53 | const linkUrl = `wss://${this.storage.linkUrl}/${this.storage.linkId}` 54 | const ws = new RobustWebSocket(linkUrl) 55 | 56 | const onSocketEvent = (type: string, event: any) => { 57 | try { 58 | if (this.handler && this.handler.onSocketEvent) { 59 | this.handler.onSocketEvent(type, event) 60 | } 61 | } catch (e) { 62 | console.error(type, event) 63 | 64 | reject(e) 65 | } 66 | } 67 | 68 | ws.addEventListener('open', (event: any) => { 69 | onSocketEvent('onopen', event) 70 | resolve(this.socket) 71 | }) 72 | 73 | ws.addEventListener('message', (event: any) => { 74 | onSocketEvent('onmessage', event) 75 | this.handleRequest(event.data) 76 | }) 77 | 78 | ws.addEventListener('error', (event: any) => { 79 | onSocketEvent('onerror', event) 80 | }) 81 | 82 | ws.addEventListener('close', (event: any) => { 83 | console.log('onclose') 84 | onSocketEvent('onclose', event) 85 | }) 86 | 87 | ws.addEventListener('ping', (event: any) => { 88 | onSocketEvent('onping', event) 89 | this.socket?.send('pong') 90 | }) 91 | 92 | this.socket = ws 93 | }).catch((error) => { 94 | console.error( 95 | 'SessionManager connect: caught error in promise', 96 | error.message, 97 | error.code, 98 | ) 99 | }) 100 | } 101 | 102 | public async disconnect() { 103 | console.error('SessionManager disconnect') 104 | this.socket?.close(1000) 105 | } 106 | 107 | public handleRequest(encoded: Bytes): string { 108 | // Decode the incoming message 109 | const message = Serializer.decode({ 110 | type: SealedMessage, 111 | data: encoded, 112 | }) 113 | 114 | // Unseal the message using the session managers request key 115 | const unsealed = unsealMessage( 116 | message.ciphertext, 117 | PrivateKey.from(this.storage.requestKey), 118 | message.from, 119 | message.nonce, 120 | ) 121 | 122 | // Ensure an active session for this key exists in storage 123 | const session = this.storage.getByPublicKey(message.from) 124 | if (!session) { 125 | throw new Error(`Unknown session using ${message.from}`) 126 | } 127 | 128 | // Updating session lastUsed timestamp 129 | this.updateLastUsed(message.from) 130 | 131 | // Fire callback for onIncomingRequest defined by client application 132 | this.handler.onIncomingRequest(unsealed, session) 133 | 134 | // Return the unsealed message 135 | return unsealed 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/apis/esr/session.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Checksum256, 3 | Checksum256Type, 4 | Name, 5 | NameType, 6 | PublicKey, 7 | PublicKeyType, 8 | } from '@greymass/eosio' 9 | import { LinkCreate } from '@proton/link' 10 | import { 11 | DEFAULT_SCHEME, 12 | SigningRequest, 13 | SigningRequestEncodingOptions, 14 | } from '@proton/signing-request' 15 | import zlib from 'pako' 16 | 17 | export interface IProtonLinkSessionManagerSessionExtended extends ProtonLinkSessionManagerSession { 18 | avatar?: string 19 | displayName?: string 20 | } 21 | 22 | export class ProtonLinkSessionManagerSession { 23 | public actor!: Name 24 | public permission!: Name 25 | public name!: Name 26 | public requestAccount!: Name 27 | public network!: Checksum256 28 | public publicKey!: PublicKey 29 | public created!: number 30 | public lastUsed!: number 31 | 32 | public constructor( 33 | network: Checksum256Type, 34 | actor: NameType, 35 | permission: NameType, 36 | publicKey: PublicKeyType, 37 | name: NameType, 38 | requestAccount: NameType, 39 | created?: number, 40 | lastUsed?: number, 41 | ) { 42 | this.network = Checksum256.from(network) 43 | this.actor = Name.from(actor) 44 | this.permission = Name.from(permission) 45 | this.publicKey = PublicKey.from(publicKey) 46 | this.name = Name.from(name) 47 | this.requestAccount = Name.from(requestAccount) 48 | this.created = created || Date.now() 49 | this.lastUsed = lastUsed || Date.now() 50 | } 51 | 52 | public updateLastUsed(time: number) { 53 | this.lastUsed = time 54 | } 55 | 56 | public static fromIdentityRequest( 57 | network: Checksum256Type, 58 | actor: NameType, 59 | permission: NameType, 60 | payload: string, 61 | options: SigningRequestEncodingOptions = { scheme: DEFAULT_SCHEME }, 62 | ) { 63 | const requestOptions = { 64 | ...options, 65 | zlib: options.zlib || zlib, 66 | } 67 | 68 | const request = SigningRequest.from(payload, requestOptions) 69 | 70 | if (!request.isIdentity()) { 71 | throw new Error('supplied request is not an identity request') 72 | } 73 | 74 | const linkInfo = request.getInfoKey('link', LinkCreate) 75 | 76 | if (!linkInfo || !linkInfo['request_key'] || !linkInfo['session_name']) { 77 | throw new Error('identity request does not contain link information') 78 | } 79 | 80 | return new ProtonLinkSessionManagerSession( 81 | network, 82 | actor, 83 | permission, 84 | String(linkInfo['request_key']), 85 | String(linkInfo['session_name']), 86 | request.getInfoKey('req_account'), 87 | ) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/apis/esr/storage.ts: -------------------------------------------------------------------------------- 1 | import { Checksum256, Name, PrivateKeyType, PublicKeyType } from '@greymass/eosio' 2 | import { IProtonLinkSessionManagerSessionExtended } from './session' 3 | import { ProtonLinkSessionManagerSession } from './session' 4 | 5 | export interface ProtonLinkSessionManagerStorageOptions { 6 | linkId: string 7 | linkUrl: string 8 | requestKey: PrivateKeyType 9 | sessions: ProtonLinkSessionManagerSession[] 10 | } 11 | 12 | type TSetSessionsSync = (storage: ProtonLinkSessionManagerStorageOptions) => Promise 13 | type TGetSessions = () => ProtonLinkSessionManagerSession[] 14 | 15 | type TProtonLinkSessionManagerStorageOptions = Omit< 16 | ProtonLinkSessionManagerStorageOptions, 17 | 'sessions' 18 | > 19 | 20 | export class ProtonLinkSessionManagerStorage { 21 | public linkId: string 22 | public linkUrl = 'cb.anchor.link' 23 | public requestKey: PrivateKeyType 24 | private setSessionsSync!: (storage: ProtonLinkSessionManagerStorageOptions) => Promise 25 | private getSessions!: () => IProtonLinkSessionManagerSessionExtended[] 26 | 27 | private findSessionIndex(session: IProtonLinkSessionManagerSessionExtended) { 28 | return this.sessions.findIndex((s: IProtonLinkSessionManagerSessionExtended) => { 29 | const matchingNetwork = session.network.equals(s.network) 30 | const matchingActor = session.actor.equals(s.actor) 31 | const matchingPermissions = session.permission.equals(s.permission) 32 | const matchingRequestor = session.requestAccount.equals(s.requestAccount) 33 | const matchingAppName = session.name.equals(s.name) 34 | const matchingPublicKey = session.publicKey.equals(s.publicKey) 35 | 36 | return ( 37 | matchingNetwork && 38 | matchingActor && 39 | matchingPermissions && 40 | matchingRequestor && 41 | matchingAppName && 42 | matchingPublicKey 43 | ) 44 | }) 45 | } 46 | 47 | public constructor( 48 | options: TProtonLinkSessionManagerStorageOptions, 49 | setSessionsSync: TSetSessionsSync, 50 | getSessions: TGetSessions, 51 | ) { 52 | this.linkId = options.linkId 53 | this.linkUrl = options.linkUrl 54 | this.requestKey = options.requestKey 55 | this.setSessionsSync = setSessionsSync 56 | this.getSessions = getSessions 57 | } 58 | 59 | public get sessions() { 60 | return this.getSessions() 61 | } 62 | 63 | public set sessions(sessions: IProtonLinkSessionManagerSessionExtended[]) { 64 | this.setSessionsSync({ 65 | linkId: this.linkId, 66 | linkUrl: this.linkUrl, 67 | requestKey: this.requestKey, 68 | sessions, 69 | }) 70 | } 71 | 72 | public add(session: IProtonLinkSessionManagerSessionExtended) { 73 | const newSessions = [...this.sessions] 74 | const existingIndex = this.findSessionIndex(session) 75 | 76 | if (existingIndex >= 0) { 77 | newSessions.splice(existingIndex, 1, session) 78 | } else { 79 | newSessions.push(session) 80 | } 81 | 82 | this.sessions = newSessions 83 | } 84 | 85 | public get( 86 | chainId: Checksum256, 87 | account: Name, 88 | permission: Name, 89 | ): IProtonLinkSessionManagerSessionExtended | undefined { 90 | return this.sessions.find( 91 | (s: IProtonLinkSessionManagerSessionExtended) => 92 | !(chainId === s.network && account === s.name && permission === s.permission), 93 | ) 94 | } 95 | 96 | public updateLastUsed(publicKey: PublicKeyType): boolean { 97 | const session = this.getByPublicKey(publicKey) 98 | 99 | if (!session) { 100 | return false 101 | } 102 | 103 | try { 104 | const newSession = new ProtonLinkSessionManagerSession( 105 | session.network, 106 | session.actor, 107 | session.permission, 108 | session.publicKey, 109 | session.name, 110 | session.requestAccount, 111 | session.created, 112 | session.lastUsed, 113 | ) 114 | 115 | newSession.updateLastUsed(Date.now()) 116 | 117 | this.add(newSession) 118 | 119 | return true 120 | } catch (_) { 121 | return false 122 | } 123 | } 124 | 125 | public getByPublicKey( 126 | publicKey: PublicKeyType, 127 | ): IProtonLinkSessionManagerSessionExtended | undefined { 128 | return this.sessions.find( 129 | (s: IProtonLinkSessionManagerSessionExtended) => 130 | // eslint-disable-next-line @typescript-eslint/no-base-to-string 131 | publicKey.toString() === s.publicKey.toString(), 132 | ) 133 | } 134 | 135 | public has(publicKey: PublicKeyType): boolean { 136 | return this.sessions.some( 137 | (s: IProtonLinkSessionManagerSessionExtended) => 138 | // eslint-disable-next-line @typescript-eslint/no-base-to-string 139 | publicKey.toString() === s.publicKey.toString(), 140 | ) 141 | } 142 | 143 | public clear() { 144 | this.sessions = [] 145 | } 146 | 147 | public remove(session: IProtonLinkSessionManagerSessionExtended) { 148 | this.sessions = this.sessions.filter( 149 | (s: IProtonLinkSessionManagerSessionExtended) => 150 | !( 151 | session.name.toString() === s.name.toString() && 152 | session.publicKey.toString() === s.publicKey.toString() && 153 | session.network.toString() === s.network.toString() && 154 | session.actor.toString() === s.actor.toString() && 155 | session.permission.toString() === s.permission.toString() && 156 | session.requestAccount.toString() === s.requestAccount.toString() 157 | ), 158 | ) 159 | } 160 | 161 | public serialize(): string { 162 | return JSON.stringify({ 163 | ...this, 164 | sessions: this.sessions, 165 | }) 166 | } 167 | 168 | public static unserialize( 169 | raw: string, 170 | setSessionsSync: TSetSessionsSync, 171 | getSessions: TGetSessions, 172 | ): ProtonLinkSessionManagerStorage { 173 | const data = JSON.parse(raw) 174 | 175 | return new ProtonLinkSessionManagerStorage(data, setSessionsSync, getSessions) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/apis/esr/utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import { Bytes, Checksum512, PrivateKey, PublicKey, Serializer, UInt64 } from '@greymass/eosio' 3 | import { AES_CBC } from '@jafri/asmcrypto.js' 4 | 5 | /** 6 | * Encrypt a message using AES and shared secret derived from given keys. 7 | * @internal 8 | */ 9 | export function unsealMessage( 10 | message: Bytes, 11 | privateKey: PrivateKey, 12 | publicKey: PublicKey, 13 | nonce: UInt64, 14 | ): string { 15 | const secret = privateKey.sharedSecret(publicKey) 16 | const key = Checksum512.hash(Serializer.encode({ object: nonce }).appending(secret.array)) 17 | const cbc = new AES_CBC(key.array.slice(0, 32), key.array.slice(32, 48)) 18 | const ciphertext = Bytes.from(cbc.decrypt(message.array)) 19 | 20 | return ciphertext.toString('utf8') 21 | } 22 | -------------------------------------------------------------------------------- /src/apis/esr/websocket.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import WebSocket from 'ws' 3 | 4 | interface Options { 5 | timeout: number 6 | // eslint-disable-next-line no-use-before-define 7 | shouldReconnect: (event: any, ws: RobustWebSocket) => number | undefined 8 | } 9 | 10 | export default class RobustWebSocket { 11 | public realWs?: WebSocket 12 | public url: string 13 | public attempts = 0 14 | public reconnects = -1 15 | public explicitlyClosed = false 16 | public binaryType: BinaryType = 'arraybuffer' 17 | 18 | public pendingReconnect?: number = undefined 19 | public connectTimeout?: number = undefined 20 | 21 | public listeners: Record = { 22 | open: [ 23 | (event: any) => { 24 | console.log('Open WS: ' + this.url) 25 | 26 | if (this.connectTimeout) { 27 | clearTimeout(this.connectTimeout) 28 | this.connectTimeout = undefined 29 | } 30 | event.reconnects += 1 31 | event.attempts = this.attempts 32 | 33 | this.explicitlyClosed = false 34 | this.clearPendingReconnectIfNeeded() 35 | }, 36 | ], 37 | close: [(event: any) => this.reconnect(event)], 38 | } 39 | 40 | public opts: Options = { 41 | // the time to wait before a successful connection 42 | // before the attempt is considered to have timed out 43 | timeout: 4000, 44 | // Given a CloseEvent or OnlineEvent and the RobustWebSocket state, 45 | // should a reconnect be attempted? Return the number of milliseconds to wait 46 | // to reconnect (or null or undefined to not), rather than true or false 47 | shouldReconnect(event, ws) { 48 | if ([1000, 4001].includes(event.code)) { 49 | return undefined 50 | } 51 | if (event.type === 'online') { 52 | return 0 53 | } 54 | 55 | return Math.pow(1.5, ws.attempts) * 300 56 | } 57 | } 58 | 59 | public constructor(url: string, opts: Partial = {}) { 60 | this.url = url 61 | 62 | this.opts = { 63 | ...this.opts, 64 | ...opts, 65 | } 66 | 67 | if (typeof this.opts.timeout !== 'number') { 68 | throw new Error( 69 | 'timeout must be the number of milliseconds to timeout a connection attempt', 70 | ) 71 | } 72 | 73 | if (typeof this.opts.shouldReconnect !== 'function') { 74 | throw new Error( 75 | 'shouldReconnect must be a function that returns the number of milliseconds to wait for a reconnect attempt, or null or undefined to not reconnect.', 76 | ) 77 | } 78 | 79 | this.newWebSocket(this.url) 80 | } 81 | 82 | public newWebSocket(url: string) { 83 | this.pendingReconnect = undefined 84 | 85 | this.realWs = new WebSocket(url) 86 | this.realWs.binaryType = this.binaryType as any 87 | 88 | // Only add once per event e.g. onping for ping 89 | const onEvent = (stdEvent: string) => (event: { data: ArrayBuffer }) => { 90 | this.dispatchEvent(event) 91 | 92 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 93 | // @ts-ignore 94 | const cb = this[`on${stdEvent}`] 95 | if (typeof cb === 'function') { 96 | return cb(event) 97 | } 98 | } 99 | 100 | for (const stdEvent of ['open', 'close', 'message', 'error', 'ping']) { 101 | if (this.realWs) { 102 | this.realWs.addEventListener(stdEvent as any, onEvent(stdEvent) as any) 103 | } 104 | } 105 | 106 | this.attempts += 1 107 | this.dispatchEvent({ 108 | type: 'connecting', 109 | attempts: this.attempts, 110 | reconnects: this.reconnects, 111 | }) 112 | 113 | this.connectTimeout = setTimeout(() => { 114 | this.connectTimeout = undefined 115 | 116 | this.dispatchEvent({ 117 | type: 'timeout', 118 | attempts: this.attempts, 119 | reconnects: this.reconnects, 120 | }) 121 | }, this.opts.timeout) as unknown as number 122 | } 123 | 124 | public clearPendingReconnectIfNeeded() { 125 | if (this.pendingReconnect) { 126 | clearTimeout(this.pendingReconnect) 127 | this.pendingReconnect = undefined 128 | } 129 | } 130 | 131 | public send(data: any) { 132 | if (this.realWs) { 133 | return this.realWs.send(data) 134 | } 135 | } 136 | 137 | public close(code?: number, reason?: string) { 138 | console.log( 139 | 'Close WS:' + 140 | `\n\tcode: ${code}` + 141 | `\n\reason:${reason}` 142 | ) 143 | 144 | if (typeof code !== 'number') { 145 | reason = code 146 | code = 1000 147 | } 148 | 149 | this.clearPendingReconnectIfNeeded() 150 | this.explicitlyClosed = true 151 | 152 | if (this.realWs) { 153 | this.realWs.close(code, reason) 154 | 155 | return 156 | } 157 | } 158 | 159 | public reconnect(event: any) { 160 | console.log( 161 | 'Reconnect WS:' + 162 | `\n\tcode: ${event.code}` + 163 | `\n\texplicitlyClosed:${this.explicitlyClosed}` + 164 | `\n\tdelay: ${this.opts.shouldReconnect(event, this)}`, 165 | ) 166 | 167 | if (this.explicitlyClosed) { 168 | this.attempts = 0 169 | return 170 | } 171 | 172 | const delay = this.opts.shouldReconnect(event, this) 173 | if (typeof delay === 'number') { 174 | this.pendingReconnect = setTimeout( 175 | () => this.newWebSocket(this.url), 176 | delay, 177 | ) as unknown as number 178 | } 179 | } 180 | 181 | // Taken from MDN https://developer.mozilla.org/en-US/docs/Web/API/EventTarget 182 | public addEventListener(type: string, callback: any) { 183 | if (!this.listeners[type]) { 184 | this.listeners[type] = [] 185 | } 186 | 187 | this.listeners[type].push(callback) 188 | } 189 | 190 | public removeEventListener(type: string, callback: any) { 191 | if (!this.listeners[type]) { 192 | return 193 | } 194 | const stack = this.listeners[type] 195 | for (let i = 0, l = stack.length; i < l; i += 1) { 196 | if (stack[i] === callback) { 197 | stack.splice(i, 1) 198 | 199 | return 200 | } 201 | } 202 | } 203 | 204 | public dispatchEvent(event: any) { 205 | if (!this.listeners[event.type]) { 206 | return 207 | } 208 | for (const listener of this.listeners[event.type]) { 209 | listener(event) 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/apis/getExplorer.ts: -------------------------------------------------------------------------------- 1 | import { config } from "../storage/config" 2 | 3 | export function getExplorer () { 4 | const chain: string = config.get('currentChain') 5 | 6 | if (chain === 'proton') { 7 | return 'https://protonscan.io' 8 | } else if (chain === 'proton-test') { 9 | return 'https://testnet.protonscan.io' 10 | } 11 | 12 | throw new Error('Chain not supported') 13 | } -------------------------------------------------------------------------------- /src/apis/getFaucets.ts: -------------------------------------------------------------------------------- 1 | import { network } from '../storage/networks' 2 | 3 | export async function getFaucets (): Promise { 4 | const { rows: faucets } = await network.rpc.get_table_rows({ 5 | code: 'token.faucet', 6 | scope: 'token.faucet', 7 | table: 'programs', 8 | limit: -1 9 | }) 10 | return faucets 11 | } -------------------------------------------------------------------------------- /src/apis/getProtonAvatar.ts: -------------------------------------------------------------------------------- 1 | import { encodeName } from '@bloks/utils' 2 | import { RpcInterfaces } from '@proton/js' 3 | import { network } from '../storage/networks' 4 | 5 | export async function getProtonAvatar (account: string): Promise { 6 | try { 7 | const result = await network.rpc.get_table_rows({ 8 | json: true, 9 | code: 'eosio.proton', 10 | scope: 'eosio.proton', 11 | table: 'usersinfo', 12 | table_key: '', 13 | key_type: 'i64', 14 | lower_bound: encodeName(account, false), 15 | index_position: 1, 16 | limit: 1 17 | }) 18 | 19 | return result.rows.length > 0 && result.rows[0].acc === account 20 | ? result.rows[0] 21 | : undefined 22 | } catch (e) { 23 | console.error('getProtonAvatar error', e) 24 | return undefined 25 | } 26 | } -------------------------------------------------------------------------------- /src/apis/getRamPrice.ts: -------------------------------------------------------------------------------- 1 | import { network } from '../storage/networks' 2 | import BN from 'bignumber.js' 3 | 4 | export async function getRamPrice (): Promise { 5 | const { rows } = await network.rpc.get_table_rows({ 6 | code: 'eosio', 7 | scope: 'eosio', 8 | table: 'globalram', 9 | limit: 1 10 | }) 11 | const ramPricePerByte = rows[0].ram_price_per_byte.split(' ')[0] 12 | return +new BN(ramPricePerByte).multipliedBy(1.1).toFixed(4, BN.ROUND_DOWN) 13 | } -------------------------------------------------------------------------------- /src/apis/lightApi.ts: -------------------------------------------------------------------------------- 1 | import * as LightApi from '@proton/light-api' 2 | import { config } from '../storage/config' 3 | 4 | export async function getLightAccount(account: string) { 5 | try { 6 | const chain = config.get('currentChain') 7 | const lightApi = new LightApi.JsonRpc(chain.toLowerCase().replace('-', '')) 8 | return lightApi.get_account_info(account) 9 | } catch (e) { 10 | return undefined 11 | } 12 | } 13 | 14 | export async function getLightBalances(account: string) { 15 | try { 16 | const chain = config.get('currentChain') 17 | const lightApi = new LightApi.JsonRpc(chain.toLowerCase().replace('-', '')) 18 | const { balances } = await lightApi.get_balances(account) 19 | return balances 20 | } catch (e) { 21 | return undefined 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/apis/uri/fetch.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'cross-fetch' 2 | 3 | export async function fetchWithTimeout(url: string, options: RequestInit & { timeout?: number }) { 4 | const { timeout = 5000 } = options 5 | 6 | const controller = new AbortController() 7 | const id = setTimeout(() => controller.abort(), timeout) 8 | 9 | const response = await fetch(url, { 10 | ...options, 11 | signal: controller.signal, 12 | }) 13 | clearTimeout(id) 14 | 15 | return response 16 | } 17 | -------------------------------------------------------------------------------- /src/apis/uri/handleUri.ts: -------------------------------------------------------------------------------- 1 | import { Authority } from '@proton/api' 2 | import { IProtonLinkSessionManagerSessionExtended } from '../esr' 3 | import { getProtonAvatar } from '../getProtonAvatar' 4 | import { parseURI } from './parseUri' 5 | 6 | export async function handleURI( 7 | uri: string, 8 | currentAuth: Authority, 9 | session?: IProtonLinkSessionManagerSessionExtended, 10 | ) { 11 | if (uri.indexOf('//link') !== -1) { 12 | return 13 | } 14 | 15 | const { request, chainId, resolved, scheme } = await parseURI( 16 | uri, 17 | { 18 | actor: currentAuth.actor, 19 | permission: currentAuth.permission, 20 | } 21 | ) 22 | 23 | const isIdentity = request.isIdentity() 24 | const account = session 25 | ? session.requestAccount && session.requestAccount.toString() 26 | : resolved.request.getInfoKey('req_account') 27 | const dataCallback = request.data.callback 28 | const accInfo = !account ? null : await getProtonAvatar(account) 29 | 30 | return { 31 | isIdentity, 32 | chainId, 33 | resolved, 34 | scheme, 35 | account: account && accInfo ? account : undefined, 36 | avatar: accInfo?.avatar, 37 | name: accInfo?.name, 38 | callback: dataCallback, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/apis/uri/parseUri.ts: -------------------------------------------------------------------------------- 1 | import zlib from 'pako' 2 | import { 3 | SigningRequest, 4 | PlaceholderName, 5 | PlaceholderPermission, 6 | ResolvedSigningRequest, 7 | ResolvedCallback, 8 | SigningRequestEncodingOptions, 9 | } from '@proton/signing-request' 10 | import { Authority } from '@proton/api' 11 | import { ABI, Name } from '@greymass/eosio' 12 | import { Key } from '@proton/js' 13 | import { fetchWithTimeout } from './fetch' 14 | import { network } from '../../storage/networks' 15 | import { ProtonLinkSessionManager, ProtonLinkSessionManagerSession } from '../esr' 16 | import passwordManager from '../../storage/passwordManager' 17 | 18 | const SUPPORTED_SCHEMES = ['proton', 'proton-dev', 'esr'] 19 | const WALLET_NAME = 'Proton CLI' 20 | 21 | function detectPlaceholders(req: any) { 22 | const [reqType, reqData] = req 23 | switch (reqType) { 24 | case 'action': { 25 | const matching = reqData.authorization.filter( 26 | (auth: any) => 27 | auth.actor === PlaceholderName || auth.permission === PlaceholderPermission, 28 | ) 29 | 30 | return matching.length > 0 31 | } 32 | case 'action[]': { 33 | const matching = reqData.filter( 34 | (r: any) => 35 | r.authorization.filter( 36 | (auth: any) => 37 | auth.actor === PlaceholderName || 38 | auth.permission === PlaceholderPermission, 39 | ).length > 0, 40 | ) 41 | 42 | return matching.length > 0 43 | } 44 | case 'transaction': { 45 | const matching = reqData.actions.filter( 46 | (r: any) => 47 | r.authorization.filter( 48 | (auth: any) => 49 | auth.actor === PlaceholderName || 50 | auth.permission === PlaceholderPermission, 51 | ).length > 0, 52 | ) 53 | 54 | return matching.length > 0 55 | } 56 | case 'identity': { 57 | // for now, always allow placeholders for identity 58 | return true 59 | } 60 | default: { 61 | throw new Error('unrecognized request type') 62 | } 63 | } 64 | } 65 | 66 | export async function callbackURIWithError(url: string, error = 'Unknown error') { 67 | if (!url) { 68 | return 69 | } 70 | 71 | try { 72 | await fetchWithTimeout(url, { 73 | method: 'POST', 74 | body: JSON.stringify({ 75 | rejected: error, 76 | }), 77 | timeout: 3000, 78 | }) 79 | } catch (e) { 80 | console.error(e) 81 | } 82 | } 83 | 84 | export async function callbackURIWithProcessed(callback: ResolvedCallback) { 85 | const { background, payload, url } = callback 86 | 87 | // If it's not a background call, return to state 88 | if (!background) { 89 | return 90 | } 91 | 92 | // Otherwise execute background call 93 | try { 94 | await fetchWithTimeout(url, { 95 | method: 'POST', 96 | body: JSON.stringify(payload), 97 | timeout: 3000, 98 | }) 99 | } catch (e) { 100 | console.error(e) 101 | } 102 | } 103 | 104 | export async function parseURI( 105 | uri: string, 106 | authorization: Authority 107 | ) { 108 | if (!uri) { 109 | throw new Error('No handleable URI') 110 | } 111 | 112 | let [scheme] = uri.split(':') 113 | if (!SUPPORTED_SCHEMES.includes(scheme)) { 114 | throw new Error(`Scheme must be ${SUPPORTED_SCHEMES.join(' or ')}`) 115 | } 116 | 117 | try { 118 | // Setup decompression 119 | const opts = { 120 | zlib, 121 | scheme: scheme as SigningRequestEncodingOptions['scheme'], 122 | abiProvider: { 123 | getAbi: async (account: Name) => { 124 | const { abi } = await network.rpc.get_abi(account.toString()) 125 | return ABI.from(abi) 126 | } 127 | }, 128 | } 129 | 130 | // Interpret the Signing Request 131 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 132 | // @ts-ignore 133 | const request = SigningRequest.from(uri, opts) 134 | 135 | // Retrieve ABIs for this request 136 | const abis = await request.fetchAbis() 137 | 138 | // Resolve the transaction 139 | let transactionHeader = {} 140 | if (!request.isIdentity()) { 141 | transactionHeader = await network.api.generateTapos( 142 | undefined, 143 | // @ts-ignore 144 | {}, 145 | 300, 146 | false, 147 | 3000, 148 | ) 149 | } 150 | 151 | if (request.isMultiChain()) { 152 | throw new Error('Multichain not supported') 153 | } 154 | 155 | const resolved = request.resolve(abis, authorization, transactionHeader) 156 | 157 | // Placeholders 158 | const req = JSON.parse(JSON.stringify(request.data.req)) 159 | const placeholders = detectPlaceholders(req) 160 | 161 | // Get the requested chain(s) 162 | const chainId = request.getChainId().toString() 163 | 164 | return { 165 | request, 166 | req, 167 | resolved, 168 | placeholders, 169 | chainId, 170 | scheme, 171 | } 172 | } catch (e) { 173 | console.error('parseURI', e) 174 | throw e 175 | } 176 | } 177 | 178 | export async function signRequest( 179 | chainId: string, 180 | resolved: ResolvedSigningRequest, 181 | scheme: string, 182 | sessionManager: ProtonLinkSessionManager, 183 | callbackUrl?: string, 184 | ) { 185 | try { 186 | if (!resolved || !resolved.request) { 187 | throw new Error('No resolved request found') 188 | } 189 | 190 | const signatureProvider = await network.getSignatureProvider() 191 | const requiredKeys = await network.api.authorityProvider.getRequiredKeys({ 192 | transaction: resolved.transaction, 193 | availableKeys: await passwordManager.getPublicKeys() 194 | }); 195 | 196 | const signed = await signatureProvider.sign({ 197 | chainId, 198 | requiredKeys, 199 | serializedTransaction: resolved.serializedTransaction, 200 | }) 201 | 202 | let broadcasted 203 | if (resolved.request.shouldBroadcast()) { 204 | broadcasted = await network.api.pushSignedTransaction(signed) 205 | } 206 | 207 | const callbackParams = resolved.getCallback( 208 | signed.signatures, 209 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 210 | // @ts-ignore 211 | broadcasted ? broadcasted.processed.block_num : 0, 212 | ) 213 | 214 | if (callbackParams) { 215 | if (resolved.request.isIdentity()) { 216 | const { info } = resolved.request.data 217 | const isLinkSession = info.some((i) => i.key === 'link') 218 | 219 | if (isLinkSession) { 220 | const session = ProtonLinkSessionManagerSession.fromIdentityRequest( 221 | chainId, // CHAIN ID 222 | callbackParams.payload.sa, // actor 223 | callbackParams.payload.sp, // permission 224 | resolved.request.toString(), // payload 225 | { 226 | scheme: scheme as SigningRequestEncodingOptions['scheme'], // scheme 227 | }, 228 | ) 229 | 230 | sessionManager.addSession(session) 231 | 232 | if (sessionManager.storage.requestKey && sessionManager.storage.linkId) { 233 | callbackParams.payload = { 234 | ...callbackParams.payload, 235 | link_ch: `https://${sessionManager.storage.linkUrl}/${sessionManager.storage.linkId}`, 236 | link_key: Key.PrivateKey.fromString(sessionManager.storage.requestKey.toString()) 237 | .getPublicKey() 238 | .toString(), 239 | link_name: WALLET_NAME, 240 | } 241 | } 242 | } 243 | } 244 | 245 | callbackURIWithProcessed(callbackParams) 246 | } 247 | 248 | // Handle info 249 | // const returnPath = resolved.request.getInfoKey('return_path') 250 | // if (returnPath && !(await isIosAppOnMac())) { 251 | // Linking.openURL(returnPath) 252 | // } 253 | } catch (err) { 254 | console.error(err) 255 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 256 | // @ts-ignore 257 | callbackURIWithError(callbackUrl, 'Request cancelled from WebAuth.com Wallet') 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/apis/uri/signUri.ts: -------------------------------------------------------------------------------- 1 | import { Authority } from "@proton/api" 2 | import { CliUx } from "@oclif/core" 3 | import { green } from "colors" 4 | import { IProtonLinkSessionManagerSessionExtended, ProtonLinkSessionManager } from "../esr" 5 | import { handleURI } from "./handleUri" 6 | import { signRequest } from "./parseUri" 7 | 8 | export const signUri = async (uri: string, auth: Authority, sessionManager: ProtonLinkSessionManager, session?: IProtonLinkSessionManagerSessionExtended) => { 9 | const res = await handleURI(uri, auth, session) 10 | await CliUx.ux.log(green('Transaction Request:')) 11 | await CliUx.ux.styledJSON(res!.resolved.resolvedTransaction) 12 | const accept = await CliUx.ux.confirm('Would you like to sign this transaction?') 13 | if (accept) { 14 | await signRequest( 15 | res!.chainId, 16 | res!.resolved, 17 | res!.scheme, 18 | sessionManager, 19 | res!.callback, 20 | ) 21 | } 22 | } -------------------------------------------------------------------------------- /src/commands/account/create.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import { CliUx } from '@oclif/core' 3 | import { network } from '../../storage/networks' 4 | import GenerateKey from '../key/generate' 5 | import { Key } from '@proton/js' 6 | import { createAccount } from '../../apis/createAccount' 7 | import AddPrivateKey from '../key/add' 8 | import { green } from 'colors' 9 | 10 | export default class CreateNewAccount extends Command { 11 | static description = 'Create New Account' 12 | 13 | static args = [ 14 | { name: 'account', required: true }, 15 | ] 16 | 17 | async run() { 18 | const { args } = this.parse(CreateNewAccount) 19 | 20 | // Validate account 21 | if (!RegExp('^[a-zA-Z12345.]+$').test(args.account)) { 22 | throw new Error('Account names can only contain the characters a-z and numbers 1-5.') 23 | } 24 | if (args.account.length > 12 || args.account.length < 4) { 25 | throw new Error('Account names must be between 4-12 characters long.') 26 | } 27 | 28 | // Check account doesnt exist 29 | try { 30 | await network.rpc.get_account(args.account) 31 | CliUx.ux.log(`Account ${args.account} already exists`) 32 | return 33 | } catch (e) {} 34 | 35 | // Create key 36 | let privateKey = await CliUx.ux.prompt('Enter private key for new account (leave empty to generate new key)', { type: 'hide', required: false }) 37 | if (!privateKey) { 38 | privateKey = await GenerateKey.run() 39 | await AddPrivateKey.run([privateKey]) 40 | } 41 | privateKey = Key.PrivateKey.fromString(privateKey).toString() 42 | const publicKey = Key.PrivateKey.fromString(privateKey).getPublicKey().toString() 43 | 44 | // Get some data 45 | const email = await CliUx.ux.prompt('Enter email for verification code', { required: true }) 46 | const displayName = await CliUx.ux.prompt('Enter display name for account', { required: true }) 47 | 48 | // Send request 49 | const data = { 50 | email: email.trim(), 51 | name: displayName, 52 | chainAccount: args.account, 53 | ownerPublicKey: publicKey, 54 | activePublicKey: publicKey, 55 | verificationCode: '' 56 | } 57 | let res = await createAccount(data) 58 | 59 | // Exit early if error 60 | if (res.error && res.error === 'mfa_required') { 61 | data.verificationCode = await CliUx.ux.prompt(`Enter 6-digit verification code (sent to ${email})`, { required: true }) 62 | } else { 63 | throw new Error(`Could not create account with error: ${res.error}`) 64 | } 65 | 66 | // Send verification 67 | res = await createAccount(data) 68 | if (res.user) { 69 | CliUx.ux.log(green(`Account ${args.account} successfully created!`)) 70 | } else { 71 | throw new Error(`Could not create account with error: ${res.error}`) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/commands/account/index.ts: -------------------------------------------------------------------------------- 1 | import { parseUtcTimestamp } from '@bloks/numbers' 2 | import { Command, flags } from '@oclif/command' 3 | import { CliUx } from '@oclif/core' 4 | import { cyan } from 'colors' 5 | import { network } from '../../storage/networks' 6 | import dedent from 'ts-dedent' 7 | import { parsePermissions } from '../../utils/permissions' 8 | import { generateResourceTable } from '../../utils/resources' 9 | import { getLightAccount, getLightBalances } from '../../apis/lightApi' 10 | 11 | export default class GetAccount extends Command { 12 | static description = 'Get Account Information' 13 | 14 | static args = [ 15 | { name: 'account', required: true }, 16 | ] 17 | 18 | static flags = { 19 | raw: flags.boolean({ char: 'r', default: false }), 20 | tokens: flags.boolean({ char: 't', default: false, description: 'Show token balances' }), 21 | } 22 | 23 | async run() { 24 | const { args, flags } = this.parse(GetAccount) 25 | 26 | const [account, lightAccount, balances] = await Promise.all([ 27 | network.rpc.get_account(args.account), 28 | getLightAccount(args.account), 29 | flags.tokens ? getLightBalances(args.account) : undefined 30 | ]) 31 | 32 | if (flags.raw) { 33 | CliUx.ux.styledJSON(account) 34 | } else { 35 | CliUx.ux.log(dedent` 36 | ${cyan('Created:')} 37 | ${parseUtcTimestamp(account.created)} 38 | 39 | ${cyan('Permissions:')} 40 | ${parsePermissions(account.permissions, lightAccount)} 41 | 42 | ${cyan('Resources:')} 43 | ${generateResourceTable(account)} 44 | 45 | ${account.voter_info && account.voter_info.producers && account.voter_info.producers.length 46 | ? dedent` 47 | ${cyan('Voting For:')} 48 | ${account.voter_info.producers.join(', ')} 49 | ` 50 | : '' 51 | } 52 | 53 | ${balances && flags.tokens 54 | ? dedent` 55 | ${cyan('Tokens:')} ${ 56 | balances 57 | .filter(balance => +balance.amount.split(' ')[0] > 0) 58 | .map(balance => `\n${balance.amount} ${balance.currency} - ${balance.contract}`).join('') 59 | } 60 | ` 61 | : '' 62 | } 63 | `.trim()) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/commands/action/index.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import { CliUx } from '@oclif/core' 3 | import { network } from '../../storage/networks' 4 | import dedent from 'ts-dedent' 5 | import { ABI } from '@greymass/eosio' 6 | import { parseDetailsError } from '../../utils/detailsError' 7 | 8 | export default class Action extends Command { 9 | static description = 'Execute Action' 10 | 11 | static args = [ 12 | { name: 'contract', required: true }, 13 | { name: 'action', required: false }, 14 | { name: 'data', required: false }, 15 | { name: 'authorization', required: false, description: 'Account to authorize with' }, 16 | ] 17 | 18 | async run() { 19 | const { args } = this.parse(Action) 20 | 21 | // Get ABI 22 | const { abi: rawAbi } = await network.rpc.get_abi(args.contract) 23 | const abi = ABI.from(rawAbi) 24 | 25 | // Guided flow 26 | if (!args.action) { 27 | const availableActions = rawAbi.actions.map((a) => { 28 | const resolved = abi.resolveType(a.name); 29 | const fields = resolved.fields!.map(field => `${field.name}: ${field.type.name}`).join(', ') 30 | return `• ${a.name} (${fields})` 31 | }).join('\n') 32 | 33 | CliUx.ux.log(dedent` 34 | Available actions: 35 | ${availableActions} 36 | `) 37 | return 38 | } 39 | 40 | // Resolved action 41 | const resolvedAction = abi.resolveType(args.action); 42 | 43 | // Check data 44 | if (!args.data) { 45 | const fields = resolvedAction.fields!.map(field => `${field.name}: ${field.type.name}`).join(', ') 46 | throw new Error(`Missing ${resolvedAction.name} data: { ${fields} }`) 47 | } 48 | 49 | // Check authorization 50 | if (!args.authorization) { 51 | throw new Error('Authorization missing (e.g. account@active)') 52 | } 53 | 54 | // Create authorization 55 | const [actor, permission] = args.authorization.split('@') 56 | const authorization = [{ 57 | actor, 58 | permission: permission || 'active' 59 | }] 60 | 61 | // Set data 62 | const data: any = {} 63 | const parsedArgsData = JSON.parse(args.data) 64 | 65 | if (Array.isArray(parsedArgsData)) { 66 | for (const [i, dataArg] of JSON.parse(args.data).entries()) { 67 | data[resolvedAction.fields![i].name] = dataArg 68 | } 69 | } else { 70 | for (const field of resolvedAction.fields!) { 71 | if (!field.type.isOptional && !parsedArgsData.hasOwnProperty(field.name)) { 72 | throw new Error(`Missing field ${field.name} on action ${resolvedAction.name}`); 73 | } 74 | 75 | if (parsedArgsData.hasOwnProperty(field.name)) { 76 | data[field.name] = parsedArgsData[field.name] 77 | } 78 | } 79 | } 80 | 81 | // Fetch rows 82 | const result = await network.transact({ 83 | actions: [{ 84 | account: args.contract, 85 | name: args.action, 86 | data, 87 | authorization 88 | }] 89 | }) 90 | CliUx.ux.styledJSON(result) 91 | } 92 | 93 | async catch(e: Error | any) { 94 | parseDetailsError(e) 95 | } 96 | } 97 | 98 | -------------------------------------------------------------------------------- /src/commands/block/get.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import { CliUx } from '@oclif/core' 3 | import { network } from '../../storage/networks' 4 | 5 | export default class GetBlock extends Command { 6 | static description = 'Get Block' 7 | 8 | static args = [ 9 | { name: 'blockNumber', required: true }, 10 | ] 11 | 12 | async run() { 13 | const { args } = this.parse(GetBlock) 14 | const result = await network.rpc.get_block(args.blockNumber) 15 | CliUx.ux.styledJSON(result) 16 | } 17 | 18 | async catch(e: Error) { 19 | CliUx.ux.styledJSON(e) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/commands/boilerplate.ts: -------------------------------------------------------------------------------- 1 | import { Command, flags } from '@oclif/command' 2 | 3 | import git from 'isomorphic-git' 4 | import http from 'isomorphic-git/http/node' 5 | import * as path from 'path' 6 | import * as fs from 'fs' 7 | import * as rimraf from 'rimraf' 8 | import { CliUx } from '@oclif/core' 9 | 10 | const BOILERPLATE_URL = 'https://github.com/ProtonProtocol/proton-boilerplate.git' 11 | const BOILERPLATE_BRANCH = 'master' 12 | 13 | export default class Boilerplate extends Command { 14 | static description = 'Boilerplate a new Proton Project with contract, frontend and tests' 15 | 16 | static flags = { 17 | help: flags.help({char: 'h'}), 18 | } 19 | 20 | static args = [ 21 | {name: 'folder'}, 22 | ] 23 | 24 | async run() { 25 | const {args} = this.parse(Boilerplate) 26 | 27 | const name = args.folder ?? 'proton-boilerplate' 28 | const dir = path.join(process.cwd(), name) 29 | 30 | this.log(`Boilerplateping to ${name} folder`) 31 | await git.clone({ 32 | fs, 33 | http, 34 | dir, 35 | url: BOILERPLATE_URL, 36 | ref: BOILERPLATE_BRANCH, 37 | singleBranch: true, 38 | depth: 1, 39 | }) 40 | rimraf.sync(path.join(dir, '.git')) 41 | } 42 | 43 | async catch(e: Error) { 44 | CliUx.ux.styledJSON(e) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/commands/chain/get.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import {CliUx} from '@oclif/core' 3 | import { network } from '../../storage/networks' 4 | 5 | export default class GetNetwork extends Command { 6 | static description = 'Get Current Chain' 7 | 8 | static aliases = ['network'] 9 | 10 | async run() { 11 | CliUx.ux.log('Current Network:') 12 | CliUx.ux.styledJSON(network.network) 13 | } 14 | 15 | async catch(e: Error) { 16 | CliUx.ux.styledJSON(e) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/commands/chain/info.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import {CliUx} from '@oclif/core' 3 | import { network } from '../../storage/networks' 4 | 5 | export default class GetAccount extends Command { 6 | static description = 'Get Chain Info' 7 | 8 | async run() { 9 | const account = await network.rpc.get_info() 10 | CliUx.ux.styledJSON(account) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/commands/chain/list.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import {networks} from '../../constants' 3 | import { CliUx } from '@oclif/core' 4 | 5 | export default class AllNetworks extends Command { 6 | static description = 'All Networks' 7 | 8 | async run() { 9 | CliUx.ux.log('All Networks:') 10 | CliUx.ux.styledJSON(networks) 11 | } 12 | 13 | async catch(e: Error) { 14 | CliUx.ux.styledJSON(e) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/commands/chain/set.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import { CliUx, Flags } from '@oclif/core' 3 | import { network } from '../../storage/networks' 4 | import * as inquirer from 'inquirer' 5 | import { networks } from '../../constants' 6 | 7 | export default class SetChain extends Command { 8 | static description = 'Set Chain' 9 | 10 | static args = [ 11 | { name: 'chain', required: false, description: 'Specific chain' }, 12 | ] 13 | 14 | async run() { 15 | const {args} = this.parse(SetChain) 16 | 17 | if (!args.chain) { 18 | let responses: any = await inquirer.prompt([{ 19 | name: 'chain', 20 | message: 'Select a chain', 21 | type: 'list', 22 | choices: networks.map(_ => _.chain), 23 | }]) 24 | args.chain = responses.chain 25 | } 26 | 27 | // Check chain is right 28 | const existingNetwork = networks.find(_ => _.chain === args.chain) 29 | if (!existingNetwork) { 30 | throw new Error(`No chain found with ${args.chain}`) 31 | } 32 | 33 | network.setChain(args.chain) 34 | } 35 | 36 | async catch(e: Error) { 37 | CliUx.ux.error(e) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/commands/contract/abi.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import {CliUx} from '@oclif/core' 3 | import { network } from '../../storage/networks' 4 | 5 | export default class GetABI extends Command { 6 | static description = 'Get Contract ABI' 7 | 8 | static args = [ 9 | { name: 'account', required: true }, 10 | ] 11 | 12 | async run() { 13 | const { args } = this.parse(GetABI) 14 | const abi = await network.rpc.get_abi(args.account) 15 | CliUx.ux.styledJSON(abi) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/commands/contract/clear.ts: -------------------------------------------------------------------------------- 1 | import { Command, flags } from '@oclif/command' 2 | import { CliUx } from '@oclif/core' 3 | import { network } from '../../storage/networks' 4 | import { config } from '../../storage/config' 5 | import { green } from 'colors' 6 | import { parseDetailsError } from '../../utils/detailsError' 7 | import { getExplorer } from '../../apis/getExplorer' 8 | 9 | export default class CleanContract extends Command { 10 | static description = 'Clean Contract' 11 | 12 | static args = [ 13 | { name: 'account', required: true, help: 'The account to cleanup the contract' }, 14 | ] 15 | 16 | static flags = { 17 | abiOnly: flags.boolean({ char: 'a', description: 'Only remove ABI' }), 18 | wasmOnly: flags.boolean({ char: 'w', description: 'Only remove WASM' }), 19 | } 20 | 21 | async run() { 22 | const { args, flags } = this.parse(CleanContract) 23 | 24 | // 3. Set code 25 | if (!flags.abiOnly) { 26 | try { 27 | const res = await network.transact({ 28 | actions: [{ 29 | account: 'eosio', 30 | name: 'setcode', 31 | data: { 32 | account: args.account, 33 | vmtype: 0, 34 | vmversion: 0, 35 | code: Buffer.from(''), 36 | }, 37 | authorization: [{ 38 | actor: args.account, 39 | permission: 'active', 40 | }], 41 | }], 42 | }) 43 | 44 | CliUx.ux.log(green(`WASM Successfully cleaned:`)) 45 | CliUx.ux.url(`View TX`, `https://${config.get('currentChain')}.ProtonScan.io/tx/${(res as any).transaction_id}?tab=traces`) 46 | } catch (e) { 47 | parseDetailsError(e) 48 | } 49 | } 50 | 51 | // 4. Set ABI 52 | if (!flags.wasmOnly) { 53 | try { 54 | const res = await network.transact({ 55 | actions: [{ 56 | account: 'eosio', 57 | name: 'setabi', 58 | data: { 59 | account: args.account, 60 | abi: '', 61 | }, 62 | authorization: [{ 63 | actor: args.account, 64 | permission: 'active', 65 | }], 66 | }], 67 | }) 68 | CliUx.ux.log(green(`ABI Successfully cleaned:`)) 69 | CliUx.ux.url(`View TX`, `${getExplorer()}/tx/${(res as any).transaction_id}?tab=traces`) 70 | } catch (e) { 71 | parseDetailsError(e) 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/commands/contract/enableinline.ts: -------------------------------------------------------------------------------- 1 | import { Command, flags } from '@oclif/command' 2 | import { CliUx } from '@oclif/core' 3 | import { RpcInterfaces } from '@proton/js' 4 | import { green, red } from 'colors' 5 | import { network } from '../../storage/networks' 6 | import { sortRequiredAuth } from '../../utils/sortRequiredAuth' 7 | 8 | export default class ContractEnableInline extends Command { 9 | static description = 'Enable Inline Actions on a Contract' 10 | 11 | static args = [ 12 | {name: 'account', required: true, description: 'Contract account to enable'}, 13 | ] 14 | 15 | static flags = { 16 | authorization: flags.string({ char: 'p', description: 'Use a specific authorization other than contract@active' }), 17 | } 18 | 19 | async run() { 20 | const {args, flags} = this.parse(ContractEnableInline) 21 | 22 | // Get active perm 23 | const account: RpcInterfaces.GetAccountResult = await network.rpc.get_account(args.account) 24 | const activePerm = account.permissions.find(perm => perm.perm_name === 'active') 25 | 26 | // Check if already exists 27 | const existingCode = activePerm?.required_auth.accounts.find(account => account.permission.actor === args.account && account.permission.permission === 'eosio.code') 28 | if (existingCode) { 29 | throw new Error('Inline actions already enabled') 30 | } 31 | 32 | // Add to required auth 33 | activePerm?.required_auth.accounts.push({ 34 | permission: { 35 | actor: args.account, 36 | permission: 'eosio.code' 37 | }, 38 | weight: activePerm.required_auth.threshold 39 | }) 40 | sortRequiredAuth(activePerm?.required_auth!) 41 | 42 | // Get signer 43 | const [actor, permission] = flags.authorization ? flags.authorization.split('@') : [args.account, 'active'] 44 | 45 | await network.transact({ 46 | actions: [{ 47 | account: 'eosio', 48 | name: 'updateauth', 49 | data: { 50 | account: args.account, 51 | permission: 'active', 52 | parent: 'owner', 53 | auth: activePerm?.required_auth 54 | }, 55 | authorization: [{ actor, permission }] 56 | }] 57 | }) 58 | 59 | // Log 60 | CliUx.ux.log(`${green('Success:')} Inline actions enabled`) 61 | } 62 | 63 | async catch(e: Error) { 64 | CliUx.ux.error(red(e.message)) 65 | } 66 | } -------------------------------------------------------------------------------- /src/commands/contract/set.ts: -------------------------------------------------------------------------------- 1 | import { Command, flags } from '@oclif/command' 2 | import { CliUx } from '@oclif/core' 3 | import { createWriteStream, mkdtemp, readdirSync, readFileSync, rmSync, unlink } from 'fs' 4 | import { join, basename } from 'path' 5 | import { prompt } from 'inquirer' 6 | import { tmpdir } from 'os' 7 | import https from 'https' 8 | import { URL } from 'url' 9 | import { Serialize, RpcInterfaces } from '@proton/js' 10 | import { network } from '../../storage/networks' 11 | import { config } from '../../storage/config' 12 | import { green, red, yellow } from 'colors' 13 | import { parseDetailsError } from '../../utils/detailsError' 14 | import { getExplorer } from '../../apis/getExplorer' 15 | import ContractEnableInline from './enableinline' 16 | import isEqual from 'lodash.isequal' 17 | 18 | const TIMEOUT = 10000 19 | 20 | function download(url: string, dest: string) { 21 | const uri = new URL(url) 22 | if (!dest) { 23 | dest = basename(uri.pathname) 24 | } 25 | 26 | return new Promise((resolve, reject) => { 27 | const request = https.get(uri.href).on('response', (res) => { 28 | if (res.statusCode === 200) { 29 | const file = createWriteStream(dest, { flags: 'wx' }) 30 | res.pipe(file) 31 | file 32 | .on('finish', () => { 33 | file.end() 34 | resolve() 35 | }) 36 | .on('error', (err: any) => { 37 | file.destroy() 38 | unlink(dest, () => reject(err)) 39 | }) 40 | 41 | } else { 42 | reject(new Error(`Download request failed, response status: ${res.statusCode} ${res.statusMessage}`)) 43 | } 44 | }).on('error', (err) => { 45 | reject(err) 46 | }) 47 | request.setTimeout(TIMEOUT, function () { 48 | request.destroy() 49 | reject(new Error(`Request timeout after ${TIMEOUT / 1000.0}s`)) 50 | }) 51 | }) 52 | } 53 | 54 | 55 | async function getDeployableFilesFromDir(dir: string) { 56 | let tmpFolder: string = ''; 57 | 58 | if (/^https:\/\/github\.com/.test(dir)) { 59 | CliUx.ux.log(yellow(`The source is GitHub. Starting to download files...`)) 60 | // The source is github need to fetch contract files first. 61 | 62 | const tmpFolderPromise = new Promise((resolve, reject) => { 63 | mkdtemp(join(tmpdir(), 'foo-'), (err, folder) => { 64 | if (err) { 65 | reject(err) 66 | }; 67 | resolve(folder) 68 | }) 69 | }) 70 | 71 | tmpFolder = await tmpFolderPromise 72 | const dirArr = dir.replace(/^https:\/\/github\.com\//, '').split('/'); 73 | dirArr.splice(2, 1); 74 | const contractName = dirArr[dirArr.length - 1]; 75 | 76 | const allDownloaded = await Promise.all(['wasm', 'abi'].map((ext) => { 77 | const rawFile = `https://raw.githubusercontent.com/${dirArr.join('/')}/${contractName}.${ext}`; 78 | return download(rawFile, join(tmpFolder, `${contractName}.${ext}`)) 79 | .then(() => true) 80 | .catch((err) => { 81 | CliUx.ux.log(red(`Cannot download ${contractName}.${ext}: ${err}`)) 82 | return false 83 | }); 84 | })) 85 | 86 | if (allDownloaded.every((status) => status)) { 87 | CliUx.ux.log(green(`Download completed`)) 88 | } 89 | 90 | dir = tmpFolder; 91 | } 92 | 93 | const dirCont = readdirSync(dir) 94 | const wasms = dirCont.filter(filePath => filePath.match(/.*\.(wasm)$/gi) as any) 95 | const abis = dirCont.filter(filePath => filePath.match(/.*\.(abi)$/gi) as any) 96 | 97 | // Validation 98 | if (wasms.length === 0) { 99 | throw new Error(`Cannot find a ".wasm file" in ${dir}`) 100 | } 101 | if (abis.length === 0) { 102 | throw new Error(`Cannot find a ".abi file" in ${dir}`) 103 | } 104 | if (wasms.length > 1 || abis.length > 1) { 105 | throw new Error(`Directory ${dir} must contain only 1 WASM and 1 ABI`) 106 | } 107 | 108 | return { 109 | wasmPath: join(dir, wasms[0]), 110 | abiPath: join(dir, abis[0]), 111 | tmpFolder: tmpFolder 112 | } 113 | } 114 | 115 | function extractStruct(abiStructs: RpcInterfaces.Abi['structs'], structName: string): RpcInterfaces.Abi['structs'][number] | undefined { 116 | return abiStructs.find((item) => item.name === structName); 117 | } 118 | 119 | async function compareTables(existingABI: RpcInterfaces.Abi, newAbi: RpcInterfaces.Abi): Promise<{ removed: string[], updated: string[] }> { 120 | const existingTables = existingABI.tables; 121 | const newTables = newAbi.tables; 122 | 123 | const tablesToCheck = existingTables.reduce((accum: { removed: string[], updated: string[] }, table) => { 124 | if (newTables.findIndex((item) => item.name === table.name) < 0) { 125 | accum.removed.push(table.name); 126 | } else { 127 | const existingStruct = extractStruct(existingABI.structs, table.type); 128 | const newStruct = extractStruct(newAbi.structs, table.type); 129 | if (!isEqual(existingStruct, newStruct)) { 130 | accum.updated.push(table.name); 131 | } 132 | } 133 | return accum; 134 | }, { removed: [], updated: [] }) 135 | return tablesToCheck 136 | } 137 | 138 | async function checkDataExists(account: string, tables: string[]): Promise { 139 | const data = await Promise.all( 140 | tables.map( 141 | async (tableName) => { 142 | let hasData: boolean 143 | try { 144 | const res = await network.rpc.get_table_by_scope({ 145 | code: account, 146 | table: tableName, 147 | }) 148 | hasData = res.rows.length > 0 149 | } catch (err) { 150 | hasData = false 151 | } 152 | return hasData ? tableName : null 153 | }) 154 | ) 155 | return data.filter((item) => item !== null) as string[]; 156 | } 157 | 158 | export default class SetContract extends Command { 159 | static description = 'Deploy Contract (WASM + ABI)' 160 | 161 | static args = [ 162 | { name: 'account', required: true, help: 'The account to publish the contract to' }, 163 | { name: 'source', required: true, help: 'Path of directory with WASM and ABI or URL for GitHub folder with WASM and ABI' }, 164 | ] 165 | 166 | static flags = { 167 | clear: flags.boolean({ char: 'c', description: 'Removes WASM + ABI from contract' }), // ! remove the flag new command is implemented instead 168 | abiOnly: flags.boolean({ char: 'a', description: 'Only deploy ABI' }), 169 | wasmOnly: flags.boolean({ char: 'w', description: 'Only deploy WASM' }), 170 | disableInline: flags.boolean({ char: 's', description: 'Disable inline actions on contract' }), 171 | } 172 | 173 | async run() { 174 | const { args, flags } = this.parse(SetContract) 175 | 176 | let wasm: Buffer = Buffer.from('') 177 | let abi: string = '' 178 | let folderToCleanup: string = '' 179 | let warning = '' 180 | let canDeploy: boolean = true 181 | 182 | // If not clearing, find files 183 | if (!flags.clear) { 184 | // 0. Get path of WASM and ABI 185 | const { wasmPath, abiPath, tmpFolder } = await getDeployableFilesFromDir(args.source) 186 | 187 | // 1. Prepare SETCODE 188 | // read the file and make a hex string out of it 189 | wasm = readFileSync(wasmPath) 190 | 191 | // 2. Prepare SETABI 192 | const abiBuffer = new Serialize.SerialBuffer() 193 | const abiDefinition = network.api.abiTypes.get('abi_def')! 194 | const abiFields = abiDefinition.fields.reduce( 195 | (acc: any, { name: fieldName }: any) => { 196 | return Object.assign(acc, { 197 | [fieldName]: acc[fieldName] || [], 198 | }) 199 | }, JSON.parse(readFileSync(abiPath, 'utf8')) 200 | ) 201 | CliUx.ux.log(yellow(`Checking for existing contract...`)) 202 | const existingABI = await network.rpc.get_abi(args.account) 203 | 204 | if (existingABI.abi) { 205 | const tablesToCheck = await compareTables(existingABI.abi, abiFields) 206 | 207 | if (tablesToCheck.removed.length > 0) { 208 | 209 | const removedTables = await checkDataExists(args.account, tablesToCheck.removed) 210 | if (removedTables.length) { 211 | warning += `The following tables you are going to remove have rows:\n ${removedTables.join('\n ')}\n` 212 | } 213 | } 214 | if (tablesToCheck.updated.length > 0) { 215 | const updatedTables = await checkDataExists(args.account, tablesToCheck.updated) 216 | if (updatedTables.length) { 217 | warning += `The following tables you are going to change have rows:\n ${updatedTables.join('\n ')}\n` 218 | } 219 | } 220 | } 221 | 222 | if (warning) { 223 | CliUx.ux.log(red(`${warning}Deploy of the contract may corrupt the data`)); 224 | const { confirmedToContinue } = await prompt<{ confirmedToContinue: boolean }>([ 225 | { 226 | name: 'confirmedToContinue', 227 | type: 'confirm', 228 | message: 'Are you sure you want to continue?', 229 | default: false, 230 | }, 231 | ]) 232 | canDeploy = confirmedToContinue 233 | } else { 234 | CliUx.ux.log(green(`No issue with the existing contract found. Continuing.`)) 235 | } 236 | 237 | abiDefinition.serialize( 238 | abiBuffer, 239 | abiFields 240 | ) 241 | abi = Buffer.from(abiBuffer.asUint8Array()).toString('hex') 242 | 243 | folderToCleanup = tmpFolder 244 | } 245 | 246 | if (canDeploy) { 247 | const deployText = flags.clear ? 'Cleared' : 'Deployed' 248 | 249 | // 3. Set code 250 | if (!flags.abiOnly) { 251 | try { 252 | const res = await network.transact({ 253 | actions: [{ 254 | account: 'eosio', 255 | name: 'setcode', 256 | data: { 257 | account: args.account, 258 | vmtype: 0, 259 | vmversion: 0, 260 | code: wasm, 261 | }, 262 | authorization: [{ 263 | actor: args.account, 264 | permission: 'active', 265 | }], 266 | }], 267 | }) 268 | 269 | CliUx.ux.log(green(`WASM Successfully ${deployText}:`)) 270 | CliUx.ux.url(`View TX`, `https://${config.get('currentChain')}.ProtonScan.io/tx/${(res as any).transaction_id}?tab=traces`) 271 | } catch (e) { 272 | parseDetailsError(e) 273 | } 274 | } 275 | 276 | // 4. Set ABI 277 | if (!flags.wasmOnly) { 278 | try { 279 | const res = await network.transact({ 280 | actions: [{ 281 | account: 'eosio', 282 | name: 'setabi', 283 | data: { 284 | account: args.account, 285 | abi: abi, 286 | }, 287 | authorization: [{ 288 | actor: args.account, 289 | permission: 'active', 290 | }], 291 | }], 292 | }) 293 | CliUx.ux.log(green(`ABI Successfully ${deployText}:`)) 294 | CliUx.ux.url(`View TX`, `${getExplorer()}/tx/${(res as any).transaction_id}?tab=traces`) 295 | } catch (e) { 296 | parseDetailsError(e) 297 | } 298 | } 299 | 300 | // 5. Enable inline 301 | if (!flags.disableInline) { 302 | await ContractEnableInline.run([args.account]) 303 | } 304 | } 305 | 306 | if (folderToCleanup) { 307 | rmSync(folderToCleanup, { recursive: true }); 308 | } 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /src/commands/encode/name.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import { CliUx } from '@oclif/core' 3 | import { Name } from '@greymass/eosio' 4 | import { parseDetailsError } from '../../utils/detailsError' 5 | 6 | export default class EncodeName extends Command { 7 | static description = 'Encode Name' 8 | 9 | static args = [ 10 | { name: 'account', required: true }, 11 | ] 12 | 13 | async run() { 14 | const { args } = this.parse(EncodeName) 15 | CliUx.ux.log(`${Name.from(args.account).value}`) 16 | } 17 | 18 | async catch(e: Error | any) { 19 | parseDetailsError(e) 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /src/commands/encode/symbol.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import { CliUx } from '@oclif/core' 3 | import { Asset } from '@greymass/eosio' 4 | import { parseDetailsError } from '../../utils/detailsError' 5 | 6 | export default class EncodeSymbol extends Command { 7 | static description = 'Encode Symbol' 8 | 9 | static args = [ 10 | { name: 'symbol', required: true }, 11 | { name: 'precision', required: true }, 12 | ] 13 | 14 | async run() { 15 | const { args } = this.parse(EncodeSymbol) 16 | CliUx.ux.log(`${Asset.Symbol.fromParts(args.symbol, args.precision).value}`) 17 | } 18 | 19 | async catch(e: Error | any) { 20 | parseDetailsError(e) 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/commands/faucet/claim.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import { CliUx } from '@oclif/core' 3 | import { green } from 'colors' 4 | import { getFaucets } from '../../apis/getFaucets' 5 | import { network } from '../../storage/networks' 6 | 7 | export default class ClaimFaucet extends Command { 8 | static description = 'Claim faucet' 9 | 10 | static args = [ 11 | { name: 'symbol', required: true }, 12 | { name: 'authorization', required: true, description: 'Authorization like account1@active' }, 13 | ] 14 | 15 | async run() { 16 | const { args } = this.parse(ClaimFaucet) 17 | 18 | const faucets = await getFaucets() 19 | const faucet = faucets.find(faucet => faucet.claimToken.quantity.split(' ')[1] === args.symbol) 20 | if (!faucet) { 21 | throw new Error(`No faucet with symbol ${args.symbol} found`) 22 | } 23 | 24 | const [actor, permission] = args.authorization.split('@') 25 | 26 | await network.transact({ 27 | actions: [{ 28 | account: 'token.faucet', 29 | name: 'claim', 30 | data: { 31 | programId: faucet.index, 32 | account: actor 33 | }, 34 | authorization: [{ 35 | actor, 36 | permission: permission || 'active' 37 | }] 38 | }] 39 | }) 40 | 41 | CliUx.ux.log(`${green('Success:')} Faucet claimed`) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/commands/faucet/index.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import { CliUx } from '@oclif/core' 3 | import { getFaucets } from '../../apis/getFaucets' 4 | 5 | export default class Faucet extends Command { 6 | static description = 'List all faucets' 7 | 8 | async run() { 9 | const faucets = await getFaucets() 10 | for (const faucet of faucets) { 11 | const [claimAmount, claimSymbol] = faucet.claimToken.quantity.split(' ') 12 | CliUx.ux.log(`${claimSymbol}: Claim ${claimAmount} every ${faucet.duration} seconds`) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/commands/generate/action.ts: -------------------------------------------------------------------------------- 1 | import { CliUx, Command, Flags } from '@oclif/core' 2 | import * as path from 'path'; 3 | import { green, red } from 'colors'; 4 | 5 | import { destinationFolder } from '../../core/flags'; 6 | import { extractContract, validateName } from '../../utils'; 7 | 8 | import { Project, ScriptTarget } from 'ts-morph'; 9 | import { addNamedImports, contractAddActions, FORMAT_SETTINGS } from '../../core/generators'; 10 | 11 | export const contractName = Flags.string({ 12 | char: 'c', 13 | description: 'The name of the contract for table. 1-12 chars, only lowercase a-z and numbers 1-5 are possible', 14 | }); 15 | 16 | export default class ContractActionsAddCommand extends Command { 17 | 18 | static description = 'Add extra actions to the smart contract'; 19 | 20 | static flags = { 21 | output: destinationFolder(), 22 | contract: contractName, 23 | } 24 | 25 | private project?: Project; 26 | 27 | async run() { 28 | const { flags } = await this.parse(ContractActionsAddCommand); 29 | 30 | if (flags.contract && !validateName(flags.contract)) { 31 | return this.error(`The provided contract name ${flags.contract} is wrong. Check --help information for more info`); 32 | } 33 | 34 | const CURR_DIR = process.cwd(); 35 | 36 | const targetPath = path.join(CURR_DIR, flags.output || ''); 37 | 38 | let contractFilePath = ''; 39 | let contractName = ''; 40 | try { 41 | [contractName, contractFilePath] = await extractContract(targetPath, flags.contract); 42 | } catch (err: any) { 43 | return this.error(red(err)); 44 | } 45 | 46 | this.project = new Project({ 47 | compilerOptions: { 48 | target: ScriptTarget.Latest 49 | }, 50 | }); 51 | 52 | const contractSource = this.project.addSourceFileAtPath(contractFilePath); 53 | const contractClass = contractSource.getClass(contractName) 54 | if (contractClass) { 55 | const result = await contractAddActions(contractClass); 56 | 57 | if (result.extraImports.length > 0) { 58 | addNamedImports(contractSource, 'proton-tsc', result.extraImports); 59 | } 60 | 61 | contractSource.formatText(FORMAT_SETTINGS); 62 | contractSource.saveSync(); 63 | 64 | CliUx.ux.log(green(`Actions were successfully added`)); 65 | 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/commands/generate/contract.ts: -------------------------------------------------------------------------------- 1 | import { CliUx, Command } from '@oclif/core'; 2 | import * as path from 'path'; 3 | import { green, red, yellow } from 'colors'; 4 | import * as shell from 'shelljs'; 5 | import { render } from 'ejs'; 6 | import { Project, PropertyAccessExpression, ScriptTarget, SyntaxKind } from "ts-morph"; 7 | 8 | import { validateName, createRootFolder, createFolderContent, IFilePreprocess, promptChoices } from '../../utils'; 9 | import { destinationFolder } from '../../core/flags'; 10 | import { addNamedImports, CONSTRUCTOR_PARAMETER_TYPES, contractAddActions, fixParameterType, FORMAT_SETTINGS, IContractActionToAdd } from '../../core/generators'; 11 | 12 | export default class ContractCreateCommand extends Command { 13 | static description = 'Create new smart contract'; 14 | 15 | static args = [ 16 | { 17 | name: 'contractName', 18 | required: true, 19 | description: 'The name of the contract. 1-12 chars, only lowercase a-z and numbers 1-5 are possible', 20 | }, 21 | ] 22 | 23 | static flags = { 24 | output: destinationFolder(), 25 | } 26 | 27 | async run() { 28 | const { flags, args } = await this.parse(ContractCreateCommand); 29 | 30 | if (!validateName(args.contractName)) { 31 | return this.error(`The provided contract name ${args.contractName} is wrong. Check --help information for more info`); 32 | } 33 | 34 | const data = { 35 | contractName: args.contractName 36 | } 37 | 38 | const CURR_DIR = process.cwd(); 39 | 40 | const templatePath = path.join(__dirname, '../..', 'templates', 'contract'); 41 | 42 | //@ts-ignore 43 | const targetPath = path.join(CURR_DIR, flags.output || args.contractName); 44 | 45 | const project = new Project({ 46 | compilerOptions: { 47 | target: ScriptTarget.Latest 48 | }, 49 | }); 50 | 51 | createRootFolder(targetPath); 52 | 53 | let actionsToAdd: IContractActionToAdd[] = []; 54 | await createFolderContent(templatePath, targetPath, { 55 | filePreprocess: async (file: IFilePreprocess) => { 56 | if (file.fileName === 'contract.ts') { 57 | file.fileName = `${args.contractName}.${file.fileName}`; 58 | 59 | const sourceFile = project.createSourceFile(file.fileName, ""); 60 | 61 | const contract = sourceFile.addClass({ 62 | name: data.contractName, 63 | isExported: true, 64 | extends: 'Contract', 65 | }); 66 | 67 | contract.addDecorator({ 68 | name: "contract" 69 | }); 70 | 71 | addNamedImports(sourceFile, 'proton-tsc', ["Contract"]); 72 | 73 | CliUx.ux.log("Let's add some actions to the class"); 74 | 75 | const result = await contractAddActions(contract); 76 | 77 | if (result.extraImports.length > 0) { 78 | addNamedImports(sourceFile, 'proton-tsc', result.extraImports); 79 | } 80 | 81 | if (result.actionsToAdd.length > 0) { 82 | actionsToAdd = result.actionsToAdd; 83 | } 84 | 85 | sourceFile.formatText(FORMAT_SETTINGS); 86 | file.content = sourceFile.getText(); 87 | } else if (file.fileName === 'playground.ts') { 88 | const sourceFile = project.createSourceFile(file.fileName, file.content); 89 | const mainFunction = sourceFile.getFunction('main'); 90 | if (mainFunction) { 91 | mainFunction.getDescendantsOfKind(SyntaxKind.CallExpression).forEach((item) => { 92 | const child = item.getFirstDescendant((item) => item.getKind() === SyntaxKind.PropertyAccessExpression) 93 | if (child && (child as PropertyAccessExpression).getName() === 'createContract') { 94 | item.getArguments().forEach((arg, idx) => { 95 | if (idx === 0) { 96 | arg.replaceWithText(`'${data.contractName}'`) 97 | } else if (idx === 1) { 98 | arg.replaceWithText(`'target/${data.contractName}.contract'`) 99 | } 100 | }); 101 | } 102 | }); 103 | if (actionsToAdd.length > 0) { 104 | 105 | actionsToAdd.forEach(action => { 106 | const values = action.parameters.map((parameter) => { 107 | const fixedType = fixParameterType(parameter.type); 108 | const paramType = CONSTRUCTOR_PARAMETER_TYPES.get(fixedType); 109 | let initializer = '""'; 110 | if (paramType) { 111 | if (parameter.isArray) { 112 | initializer = '[]'; 113 | } else { 114 | if (fixedType !== 'Name') { 115 | initializer = paramType.initializer; 116 | } 117 | } 118 | } 119 | return initializer; 120 | }) 121 | 122 | mainFunction.addStatements([ 123 | `await contract.actions.${action.name}([${values.join(',')}]).send('${data.contractName}@active');` 124 | ]); 125 | }) 126 | } 127 | } 128 | sourceFile.formatText(FORMAT_SETTINGS); 129 | file.content = sourceFile.getText(); 130 | } else { 131 | file.content = render(file.content, data); 132 | } 133 | return file 134 | } 135 | }); 136 | 137 | if (!await postProcessNode(targetPath)) { 138 | return this.error(red('Failed to install dependencies. Try to install manually.')); 139 | } 140 | 141 | CliUx.ux.log(green(`Contract ${args.contractName} successfully created!`)); 142 | 143 | CliUx.ux.log(`Next steps: 144 | 1. cd ${flags.output || args.contractName} 145 | 2. "proton generate:table" to generate a table 146 | 3. "proton generate:action" to generate an action 147 | `); 148 | } 149 | } 150 | 151 | async function postProcessNode(targetPath: string) { 152 | shell.cd(targetPath); 153 | 154 | const managers = ['npm']; 155 | if (shell.which('yarn')) { 156 | managers.push('yarn'); 157 | } 158 | let selectedManager = 'npm'; 159 | 160 | if (managers.length > 1) { 161 | selectedManager = await promptChoices( 162 | 'Choose your preferred package manager:', 163 | managers, 164 | selectedManager); 165 | } 166 | 167 | let cmd = ''; 168 | 169 | if (selectedManager === 'yarn') { 170 | cmd = 'yarn'; 171 | } else if (selectedManager === 'npm') { 172 | cmd = 'npm install'; 173 | } 174 | 175 | if (cmd) { 176 | CliUx.ux.log(yellow('Installing packages...')); 177 | 178 | const result = shell.exec(cmd); 179 | 180 | if (result.code !== 0) { 181 | return false; 182 | } 183 | } else { 184 | CliUx.ux.log(red('No yarn or npm found. Cannot run installation.')); 185 | } 186 | 187 | return true; 188 | } 189 | -------------------------------------------------------------------------------- /src/commands/generate/inlineaction.ts: -------------------------------------------------------------------------------- 1 | import { CliUx, Command, Flags } from '@oclif/core' 2 | import * as path from 'path'; 3 | import { red } from 'colors'; 4 | 5 | import { destinationFolder } from '../../core/flags'; 6 | import { checkFileExists, extractContract, validateName } from '../../utils'; 7 | 8 | import { Project, ScriptTarget, SourceFile } from 'ts-morph'; 9 | import { addNamedImports, constructorAddParameters, FORMAT_SETTINGS } from '../../core/generators'; 10 | 11 | export const contractName = Flags.string({ 12 | char: 'c', 13 | description: 'The name of the contract for table. 1-12 chars, only lowercase a-z and numbers 1-5 are possible', 14 | }); 15 | 16 | export default class ContractInlineActionCreateCommand extends Command { 17 | 18 | static description = 'Add inline action for the smart contract'; 19 | 20 | static args = [ 21 | { 22 | name: 'actionName', 23 | required: true, 24 | description: 'The name of the inline action\'s class.', 25 | }, 26 | ] 27 | 28 | static flags = { 29 | output: destinationFolder(), 30 | contract: contractName, 31 | } 32 | 33 | private data: any = {} 34 | private project?: Project; 35 | 36 | async run() { 37 | const { flags, args } = await this.parse(ContractInlineActionCreateCommand); 38 | 39 | if (flags.contract && !validateName(flags.contract)) { 40 | return this.error(`The provided contract name ${flags.contract} is wrong. Check --help information for more info`); 41 | } 42 | 43 | const CURR_DIR = process.cwd(); 44 | 45 | const targetPath = path.join(CURR_DIR, flags.output || ''); 46 | 47 | let contractFilePath = ''; 48 | let contractName = ''; 49 | try { 50 | [contractName, contractFilePath] = await extractContract(targetPath, flags.contract); 51 | } catch (err: any) { 52 | return this.error(red(err)); 53 | } 54 | 55 | this.data = { 56 | actionName: args.actionName, 57 | contractName: contractName, 58 | className: args.actionName, 59 | inlineFileName: `${contractName}.inline`, 60 | inlineFileNameWithExt: `${contractName}.inline.ts` 61 | } 62 | this.data.className = this.data.className.charAt(0).toUpperCase() + this.data.className.slice(1); 63 | 64 | const inlineFilePath = path.join(targetPath, this.data.inlineFileNameWithExt); 65 | 66 | this.project = new Project({ 67 | compilerOptions: { 68 | target: ScriptTarget.Latest 69 | }, 70 | }); 71 | 72 | try { 73 | await this.createInlineAction(inlineFilePath); 74 | } catch (e: any) { 75 | return this.error(red(e)); 76 | } 77 | } 78 | 79 | private async createInlineAction(inlineFilePath: string) { 80 | let sourceInlineActions: SourceFile | undefined; 81 | if (this.project) { 82 | if (checkFileExists(inlineFilePath)) { 83 | this.project.addSourceFilesAtPaths([inlineFilePath]); 84 | sourceInlineActions = this.project.getSourceFile(inlineFilePath); 85 | } else { 86 | sourceInlineActions = this.project.createSourceFile(inlineFilePath); 87 | } 88 | 89 | if (sourceInlineActions) { 90 | const classExists = sourceInlineActions.getClass(this.data.className); 91 | if (!classExists) { 92 | 93 | const inlineAction = sourceInlineActions.addClass({ 94 | name: this.data.className, 95 | isExported: true, 96 | extends: 'InlineAction', 97 | }); 98 | 99 | inlineAction.addDecorator({ 100 | name: "packer" 101 | }); 102 | 103 | const inlineActionContructor = inlineAction.addConstructor( 104 | { 105 | statements: ['super()'] 106 | } 107 | ); 108 | 109 | const extraImports = await constructorAddParameters(inlineActionContructor); 110 | 111 | const namedImports = ["InlineAction", "Name"]; 112 | 113 | if (extraImports.length > 0) { 114 | namedImports.push(...extraImports); 115 | } 116 | 117 | addNamedImports(sourceInlineActions, 'proton-tsc', namedImports); 118 | 119 | sourceInlineActions.formatText(FORMAT_SETTINGS); 120 | sourceInlineActions.saveSync(); 121 | CliUx.ux.log(`Inline action ${this.data.actionName} successfully created`); 122 | } else { 123 | throw `The inline action ${this.data.actionName} already exists. Try changing the name.`; 124 | } 125 | } 126 | } 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /src/commands/generate/table.ts: -------------------------------------------------------------------------------- 1 | import { CliUx, Command, Flags } from '@oclif/core' 2 | import * as path from 'path'; 3 | import { green, red } from 'colors'; 4 | import { prompt } from 'inquirer' 5 | 6 | import { destinationFolder } from '../../core/flags'; 7 | import { checkFileExists, extractContract, validateName } from '../../utils'; 8 | 9 | import { Project, ScriptTarget, SourceFile } from 'ts-morph'; 10 | import { 11 | addNamedImports, constructorAddParameter, FORMAT_SETTINGS, 12 | IParameter, tableAddPrimaryParameter, 13 | constructorAddParameters, parameterPrompt, parametersExtractImports 14 | } from '../../core/generators'; 15 | 16 | export const tableClass = Flags.string({ 17 | char: 't', 18 | description: 'The name of Typescript class for the table', 19 | }); 20 | 21 | export const isSingleton = Flags.boolean({ 22 | char: 's', 23 | description: 'Create a singleton table?', 24 | default: false 25 | }); 26 | 27 | export const contractName = Flags.string({ 28 | char: 'c', 29 | description: 'The name of the contract for table. 1-12 chars, only lowercase a-z and numbers 1-5 are possible', 30 | }); 31 | 32 | export default class ContractTableCreateCommand extends Command { 33 | 34 | static description = 'Add table for the smart contract'; 35 | 36 | static args = [ 37 | { 38 | name: 'tableName', 39 | required: true, 40 | description: 'The name of the contract\'s table. 1-12 chars, only lowercase a-z and numbers 1-5 are possible', 41 | }, 42 | ] 43 | 44 | static flags = { 45 | class: tableClass, 46 | singleton: isSingleton, 47 | output: destinationFolder(), 48 | contract: contractName, 49 | } 50 | 51 | private data: any = {} 52 | private project?: Project; 53 | 54 | async run() { 55 | const { flags, args } = await this.parse(ContractTableCreateCommand); 56 | 57 | if (!validateName(args.tableName)) { 58 | return this.error(`The provided table name ${args.tableName} is wrong. Check --help information for more info`); 59 | } 60 | 61 | if (flags.contract && !validateName(flags.contract)) { 62 | return this.error(`The provided contract name ${flags.contract} is wrong. Check --help information for more info`); 63 | } 64 | 65 | const CURR_DIR = process.cwd(); 66 | 67 | const targetPath = path.join(CURR_DIR, flags.output || ''); 68 | let contractFilePath = ''; 69 | let contractName = ''; 70 | try { 71 | [contractName, contractFilePath] = await extractContract(targetPath, flags.contract); 72 | } catch (err: any) { 73 | return this.error(red(err)); 74 | } 75 | 76 | this.data = { 77 | tableName: args.tableName, 78 | contractName: contractName, 79 | className: flags.class || args.tableName, 80 | isSingleton: false, 81 | tableFileName: `${contractName}.tables`, 82 | tableFileNameWithExt: `${contractName}.tables.ts` 83 | } 84 | 85 | CliUx.ux.log(green(`Starting to generate a table '${this.data.tableName}' for contract`)); 86 | CliUx.ux.log(green(`Let's collect some properties:`)); 87 | 88 | if (!flags.class) { 89 | this.data.className = this.data.className.charAt(0).toUpperCase() + this.data.className.slice(1) 90 | const { tableClass } = await prompt<{ tableClass: string; }>({ 91 | name: 'tableClass', 92 | type: 'input', 93 | message: `Enter the name of Typescript class for the table:`, 94 | default: this.data.className, 95 | }); 96 | this.data.className = tableClass; 97 | } 98 | 99 | if (!flags.singleton) { 100 | const { isSingleton } = await prompt<{ isSingleton: boolean }>([ 101 | { 102 | name: 'isSingleton', 103 | type: 'confirm', 104 | message: 'Is the table singleton?', 105 | default: false, 106 | }, 107 | ]); 108 | this.data.isSingleton = isSingleton; 109 | } 110 | 111 | const tableFilePath = path.join(targetPath, this.data.tableFileNameWithExt); 112 | 113 | this.project = new Project({ 114 | compilerOptions: { 115 | target: ScriptTarget.Latest 116 | }, 117 | }); 118 | 119 | try { 120 | await this.createTable(tableFilePath); 121 | } catch (e: any) { 122 | return this.error(red(e)); 123 | } 124 | 125 | try { 126 | this.updateContract(contractFilePath); 127 | } catch (e: any) { 128 | return this.error(red(e)); 129 | } 130 | } 131 | 132 | private async createTable(tableFilePath: string) { 133 | let sourceTables: SourceFile | undefined; 134 | if (this.project) { 135 | if (checkFileExists(tableFilePath)) { 136 | this.project.addSourceFilesAtPaths([tableFilePath]); 137 | sourceTables = this.project.getSourceFile(tableFilePath); 138 | } else { 139 | sourceTables = this.project.createSourceFile(tableFilePath); 140 | } 141 | 142 | if (sourceTables) { 143 | const classExists = sourceTables.getClass(this.data.className); 144 | if (!classExists) { 145 | 146 | const table = sourceTables.addClass({ 147 | name: this.data.className, 148 | isExported: true, 149 | extends: 'Table', 150 | }); 151 | 152 | const decorator = table.addDecorator({ 153 | name: "table" 154 | }); 155 | decorator.addArgument(`"${this.data.tableName}"`); 156 | 157 | if (this.data.isSingleton) { 158 | decorator.addArgument('singleton'); 159 | } 160 | 161 | const tableContructor = table.addConstructor( 162 | { 163 | statements: ['super()'] 164 | } 165 | ); 166 | 167 | const namedImports = ["Name", "Table"]; 168 | 169 | CliUx.ux.log(`Let's add a primary parameter for the table`); 170 | 171 | const primaryProperty = await parameterPrompt( 172 | [], 173 | { 174 | preset: { 175 | isArray: false, 176 | isNullable: false 177 | }, 178 | type: 'primary parameter' 179 | } 180 | ); 181 | 182 | const typesToImport = parametersExtractImports([primaryProperty]); 183 | if (typesToImport.length > 0) { 184 | namedImports.push(...typesToImport); 185 | } 186 | 187 | constructorAddParameter(tableContructor, primaryProperty); 188 | tableAddPrimaryParameter(table, primaryProperty); 189 | 190 | CliUx.ux.log(`————————————`); 191 | 192 | const { addMore } = await prompt<{ addMore: boolean }>([ 193 | { 194 | name: 'addMore', 195 | type: 'confirm', 196 | message: 'Do you want to one more parameter?', 197 | default: false, 198 | }, 199 | ]); 200 | 201 | if (addMore) { 202 | const existingProperties: IParameter[] = [primaryProperty]; 203 | const extraImports = await constructorAddParameters(tableContructor, existingProperties); 204 | if (extraImports.length > 0) { 205 | namedImports.push(...extraImports); 206 | } 207 | } 208 | 209 | addNamedImports(sourceTables, 'proton-tsc', namedImports); 210 | 211 | sourceTables.formatText(FORMAT_SETTINGS); 212 | sourceTables.saveSync(); 213 | CliUx.ux.log(`Table ${this.data.tableName} successfully created`); 214 | } else { 215 | throw `The table ${this.data.className} already exists. Try changing the name.`; 216 | } 217 | } 218 | } 219 | } 220 | 221 | private updateContract(contractFilePath: string) { 222 | CliUx.ux.log(`Adding the table to the contract ${this.data.contractName}`); 223 | if (this.project) { 224 | this.project.addSourceFilesAtPaths([contractFilePath]) 225 | const contractSource = this.project.getSourceFile(contractFilePath); 226 | if (contractSource) { 227 | const protonImportClass = this.data.isSingleton ? 'Singleton' : 'TableStore'; 228 | 229 | addNamedImports(contractSource, 'proton-tsc', [protonImportClass]) 230 | addNamedImports(contractSource, `./${this.data.tableFileName}`, [this.data.className]) 231 | 232 | const contractClass = contractSource.getClass(this.data.contractName); 233 | if (contractClass) { 234 | const methodName = `${this.data.className.toLowerCase()}${protonImportClass}`; 235 | const methodExists = contractClass.getProperty(methodName); 236 | if (!methodExists) { 237 | const maxIdx = contractClass.getProperties().length; 238 | contractClass.insertProperty(maxIdx, { 239 | name: methodName, 240 | type: `${protonImportClass}<${this.data.className}>`, 241 | initializer: `new ${protonImportClass}<${this.data.className}>(this.receiver)` 242 | }); 243 | } 244 | } 245 | 246 | contractSource.formatText(FORMAT_SETTINGS); 247 | contractSource.saveSync(); 248 | CliUx.ux.log(green(`Contract ${this.data.contractName} was successfully updated`)); 249 | } else { 250 | throw `Not contract ${this.data.contractName} found`; 251 | } 252 | } 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/commands/key/add.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import { CliUx } from '@oclif/core' 3 | import { red } from 'colors' 4 | import { config } from '../../storage/config' 5 | import passwordManager from '../../storage/passwordManager' 6 | import LockKey from './lock' 7 | 8 | export default class AddPrivateKey extends Command { 9 | static description = 'Manage Keys' 10 | 11 | static args = [ 12 | {name: 'privateKey', required: false}, 13 | ] 14 | 15 | async run() { 16 | const {args} = this.parse(AddPrivateKey) 17 | 18 | // Prompt whether to lock 19 | if (!config.get('isLocked')) { 20 | const toEncrypt = await CliUx.ux.confirm('Would you like to encrypt your stored keys with a password? (yes/no)') 21 | if (toEncrypt) { 22 | await LockKey.run() 23 | } 24 | } 25 | 26 | // Prompt if needed 27 | if (!args.privateKey) { 28 | args.privateKey = await CliUx.ux.prompt('Enter private key (starts with PVT_K1)', { type: 'hide' }) 29 | } 30 | 31 | await passwordManager.addPrivateKey(args.privateKey) 32 | } 33 | 34 | async catch(e: Error) { 35 | CliUx.ux.error(red(e.message)) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/commands/key/generate.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import {CliUx} from '@oclif/core' 3 | import { Mnemonic } from '@proton/mnemonic' 4 | import { red, yellow } from 'colors' 5 | 6 | export default class GenerateKey extends Command { 7 | static description = 'Generate Key' 8 | 9 | async run() { 10 | const mnemonic = new Mnemonic({ 11 | numWords: 12, 12 | }) 13 | const { publicKey, privateKey } = mnemonic.keyPairAtIndex(0) 14 | 15 | CliUx.ux.log(`\n${yellow('Note:')} Please store private key or mnemonic securely!`) 16 | CliUx.ux.styledJSON({ 17 | public: publicKey.toString(), 18 | private: privateKey.toString(), 19 | mnemonic: mnemonic.phrase 20 | }) 21 | 22 | return privateKey 23 | } 24 | 25 | async catch(e: Error) { 26 | CliUx.ux.error(red(e.message)) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/commands/key/get.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import { CliUx } from '@oclif/core' 3 | import { green, red } from 'colors' 4 | import passwordManager from '../../storage/passwordManager' 5 | 6 | export default class GetPrivateKey extends Command { 7 | static description = 'Find private key for public key' 8 | 9 | static args = [ 10 | {name: 'publicKey', required: true}, 11 | ] 12 | 13 | async run() { 14 | const { args } = this.parse(GetPrivateKey) 15 | const privateKey = await passwordManager.getPrivateKey(args.publicKey) 16 | if (privateKey) { 17 | CliUx.ux.log(`${green('Success:')} ${privateKey}`) 18 | } else { 19 | CliUx.ux.log(`${red('Failure:')} No matching private key found in saved keys`) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/commands/key/list.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import {CliUx} from '@oclif/core' 3 | import { Key } from '@proton/js' 4 | import passwordManager from '../../storage/passwordManager' 5 | 6 | export default class ListAllKeys extends Command { 7 | static description = 'List All Key' 8 | 9 | async run() { 10 | const privateKeys = await passwordManager.getPrivateKeys() 11 | const displayKeys = privateKeys.map(privateKey => { 12 | const parsedPrivateKey = Key.PrivateKey.fromString(privateKey) 13 | 14 | return { 15 | publicKey: parsedPrivateKey.getPublicKey().toString(), 16 | privateKey: parsedPrivateKey.toString() 17 | } 18 | }) 19 | CliUx.ux.styledJSON(displayKeys); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/commands/key/lock.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import { CliUx } from '@oclif/core' 3 | import { green, red } from 'colors' 4 | import * as crypto from 'crypto' 5 | import passwordManager from '../../storage/passwordManager' 6 | 7 | export default class LockKey extends Command { 8 | static description = 'Lock Keys with password' 9 | 10 | async run() { 11 | // Prompt password from user 12 | let enteredPassword = await CliUx.ux.prompt('Enter 32 character password (leave empty to create new)', { type: 'hide', required: false }) 13 | if (enteredPassword.length !== 0 && enteredPassword.length !== 32) { 14 | throw new Error('Password field must be empty or 32 characters long') 15 | } 16 | 17 | // Generate password if needed 18 | if (enteredPassword.length === 0) { 19 | enteredPassword = crypto.randomBytes(16).toString('hex') 20 | CliUx.ux.log(` 21 | Please safely store your 32 character password, you will need it to unlock your wallet: 22 | Password: ${green(enteredPassword)} 23 | `) 24 | } 25 | 26 | // Lock 27 | await passwordManager.lock(enteredPassword) 28 | 29 | // Log 30 | CliUx.ux.log(`${green('Success:')} Locked wallet`) 31 | } 32 | 33 | async catch(e: Error) { 34 | CliUx.ux.error(red(e.message)) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/commands/key/remove.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import { CliUx } from '@oclif/core' 3 | import { Key } from '@proton/js' 4 | import { green, red } from 'colors' 5 | import passwordManager from '../../storage/passwordManager' 6 | 7 | export default class RemoveKey extends Command { 8 | static description = 'Remove Key' 9 | 10 | static args = [ 11 | {name: 'privateKey', required: false}, 12 | ] 13 | 14 | async run() { 15 | const {args} = this.parse(RemoveKey) 16 | 17 | // Prompt if needed 18 | if (!args.privateKey) { 19 | args.privateKey = await CliUx.ux.prompt('Enter private key to delete (starts with PVT_K1)', { type: 'hide' }) 20 | } 21 | 22 | // Confirm 23 | const confirmed = await CliUx.ux.confirm('Are you sure you want to delete this private key? (yes/no)') 24 | if (!confirmed) { 25 | return 26 | } 27 | 28 | // Remove 29 | await passwordManager.removePrivateKey(args.privateKey) 30 | 31 | // Log 32 | CliUx.ux.log(`${green('Success:')} Removed private key for public key ${Key.PrivateKey.fromString(args.privateKey).getPublicKey()}`) 33 | } 34 | 35 | async catch(e: Error) { 36 | CliUx.ux.error(red(e.message)) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/commands/key/reset.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import { CliUx } from '@oclif/core' 3 | import { green, red } from 'colors' 4 | import { config } from '../../storage/config' 5 | 6 | export default class ResetKey extends Command { 7 | static description = 'Reset password (Caution: deletes all private keys stored)' 8 | 9 | async run() { 10 | const confirmed = await CliUx.ux.confirm(`${red('Caution:')} Are you sure you want to delete all your private keys? (yes/no)`) 11 | if (!confirmed) { 12 | return 13 | } 14 | 15 | const doubleConfirmed = await CliUx.ux.confirm(`${red('Caution:')} Are you REALLY sure? There is no coming back from this (yes/no)`) 16 | if (!doubleConfirmed) { 17 | return 18 | } 19 | 20 | config.reset('privateKeys', 'isLocked') 21 | CliUx.ux.log(`${green('Success:')} Reset password and deleted all stored private keys.`) 22 | } 23 | 24 | async catch(e: Error) { 25 | CliUx.ux.error(red(e.message)) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/commands/key/unlock.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import { CliUx } from '@oclif/core' 3 | import { green, red, yellow } from 'colors' 4 | import passwordManager from '../../storage/passwordManager' 5 | 6 | export default class UnlockKey extends Command { 7 | static description = 'Unlock all keys (Caution: Your keys will be stored in plaintext on disk)' 8 | 9 | static args = [ 10 | {name: 'password', required: false, hide: true }, 11 | ] 12 | 13 | async run() { 14 | // Get args 15 | const {args} = this.parse(UnlockKey) 16 | 17 | // Prompt if needed 18 | if (!args.password) { 19 | args.password = await CliUx.ux.prompt('Enter 32 character password', { type: 'hide' }) 20 | } 21 | 22 | // UnLock 23 | await passwordManager.unlock(args.password) 24 | 25 | // Print out success 26 | CliUx.ux.log(`${green('Success:')} Unlocked wallet`) 27 | CliUx.ux.log(`${yellow('Note:')} Your private keys are stored as plaintext on disk until you call keys:lock again`) 28 | } 29 | 30 | async catch(e: Error) { 31 | CliUx.ux.error(red(e.message)) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/commands/msig/approve.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { Command } from '@oclif/command' 3 | import { network } from '../../storage/networks' 4 | import { CliUx } from '@oclif/core' 5 | import { green, red } from 'colors' 6 | import { getExplorer } from '../../apis/getExplorer' 7 | 8 | export default class MultisigApprove extends Command { 9 | static description = 'Multisig Approve' 10 | 11 | static args = [ 12 | {name: 'proposer', required: true, help: 'Name of proposer'}, 13 | {name: 'proposal', required: true, help: 'Name of proposal'}, 14 | {name: 'auth', required: true, help: 'Signing authorization (e.g. user1@active)'}, 15 | ] 16 | 17 | async run() { 18 | const {args: { proposer, proposal, auth }} = this.parse(MultisigApprove) 19 | const [actor, permission] = auth.split('@') 20 | 21 | try { 22 | await network.transact({ 23 | actions: [{ 24 | account: 'eosio.msig', 25 | name: 'approve', 26 | data: { 27 | proposer, 28 | proposal_name: proposal, 29 | level: { actor, permission } 30 | }, 31 | authorization: [{ actor, permission }] 32 | }] 33 | }) 34 | CliUx.ux.log(green(`Multisig ${proposal} successfully approved.`)) 35 | CliUx.ux.url(`View Proposal`, `${getExplorer()}/msig/${actor}/${proposal}`) 36 | } catch (err: any) { 37 | return this.error(red(err)); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/commands/msig/cancel.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { Command } from '@oclif/command' 3 | import { network } from '../../storage/networks' 4 | import { CliUx } from '@oclif/core' 5 | import { green, red } from 'colors' 6 | 7 | export default class MultisigCancel extends Command { 8 | static description = 'Multisig Cancel' 9 | 10 | static args = [ 11 | {name: 'proposalName', required: true, help: 'Name of proposal'}, 12 | {name: 'auth', required: true, help: 'Your authorization'}, 13 | ] 14 | 15 | async run() { 16 | const {args: {proposalName, auth}} = this.parse(MultisigCancel) 17 | const [actor, permission] = auth.split('@') 18 | 19 | try { 20 | await network.transact({ 21 | actions: [{ 22 | account: 'eosio.msig', 23 | name: 'cancel', 24 | data: { 25 | proposer: actor, 26 | proposal_name: proposalName, 27 | canceler: actor 28 | }, 29 | authorization: [{ actor, permission: permission || 'active' }] 30 | }] 31 | }) 32 | CliUx.ux.log(green(`Multisig ${proposalName} successfully cancelled.`)) 33 | } catch (err: any) { 34 | return this.error(red(err)); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/commands/msig/exec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { Command } from '@oclif/command' 3 | import { network } from '../../storage/networks' 4 | import { CliUx } from '@oclif/core' 5 | import { green, red } from 'colors' 6 | 7 | export default class MultisigExecute extends Command { 8 | static description = 'Multisig Execute' 9 | 10 | static args = [ 11 | {name: 'proposer', required: true, help: 'Name of proposer'}, 12 | {name: 'proposal', required: true, help: 'Name of proposal'}, 13 | {name: 'auth', required: true, help: 'Your authorization (e.g. user1@active'}, 14 | ] 15 | 16 | async run() { 17 | const {args: {proposer, proposal, auth}} = this.parse(MultisigExecute) 18 | const [actor, permission] = auth.split('@') 19 | 20 | try { 21 | await network.transact({ 22 | actions: [{ 23 | account: 'eosio.msig', 24 | name: 'exec', 25 | data: { 26 | proposer: proposer, 27 | proposal_name: proposal, 28 | executer: actor 29 | }, 30 | authorization: [{ actor, permission }] 31 | }] 32 | }) 33 | CliUx.ux.log(green(`Multisig ${proposal} successfully executed.`)) 34 | } catch (err: any) { 35 | return this.error(red(err)); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/commands/msig/propose.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { Command, flags } from '@oclif/command' 3 | import { network } from '../../storage/networks' 4 | import { CliUx } from '@oclif/core' 5 | import { green, red } from 'colors' 6 | import { getExplorer } from '../../apis/getExplorer' 7 | import { Authorization } from '@proton/wrap-constants' 8 | 9 | export default class MultisigPropose extends Command { 10 | static description = 'Multisig Propose' 11 | 12 | static args = [ 13 | {name: 'proposalName', required: true, help: 'Name of proposal'}, 14 | {name: 'actions', required: true, help: 'Actions JSON'}, 15 | {name: 'auth', required: true, help: 'Your authorization'}, 16 | ] 17 | 18 | static flags: { [k: string]: flags.IFlag; } = { 19 | blocksBehind: flags.integer({char: 'b', default: 30}), 20 | expireSeconds: flags.integer({char: 'x', default: 60 * 60 * 24 * 7 }), 21 | } 22 | 23 | async run() { 24 | const {args: {proposalName, actions, auth}, flags} = this.parse(MultisigPropose) 25 | const [actor, permission] = auth.split('@') 26 | 27 | // Serialize action 28 | const parsedActions = JSON.parse(actions) 29 | const serializedActions = await network.api.serializeActions(parsedActions) 30 | const transactionSettings = await network.protonApi.generateTransactionSettings(flags.expireSeconds, flags.blocksBehind, 0) 31 | 32 | // Find required signers 33 | let requested: Authorization[] = [] 34 | for (const action of parsedActions) { 35 | for (const { actor, permission } of action.authorization) { 36 | const requiredAccountsLocal = await network.protonApi.getRequiredAccounts(actor, permission) 37 | requested = requested.concat(requiredAccountsLocal) 38 | } 39 | } 40 | requested = requested.filter((item, pos) => requested.findIndex(_ => _.actor === item.actor) === pos) 41 | 42 | try { 43 | await network.transact({ 44 | actions: [{ 45 | account: 'eosio.msig', 46 | name: 'propose', 47 | data: { 48 | proposer: actor, 49 | proposal_name: proposalName, 50 | requested, 51 | trx: { 52 | ...transactionSettings, 53 | actions: serializedActions, 54 | } 55 | }, 56 | authorization: [{ actor, permission: permission || 'active' }] 57 | }] 58 | }) 59 | CliUx.ux.log(green(`Multisig ${proposalName} successfully proposed.`)) 60 | CliUx.ux.url(`View Proposal`, `${getExplorer()}/msig/${actor}/${proposalName}`) 61 | } catch (err: any) { 62 | return this.error(red(err)); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/commands/permission/index.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import { CliUx } from '@oclif/core' 3 | import { Key, RpcInterfaces } from '@proton/js' 4 | import { green, red } from 'colors' 5 | import { Separator } from 'inquirer' 6 | import { getLightAccount } from '../../apis/lightApi' 7 | import { network } from '../../storage/networks' 8 | import { parsePermissions } from '../../utils/permissions' 9 | import { promptChoices, promptInteger, promptKey, promptAuthority, promptName } from '../../utils/prompt' 10 | import { sortRequiredAuth } from '../../utils/sortRequiredAuth' 11 | import { wait } from '../../utils/wait' 12 | 13 | const parseKey = (key: { weight: number, key: string }) => `+${key.weight} | ${key.key}` 14 | const parseAccount = (acc: { weight: number, permission: { actor: string, permission: string } }) => `A: | +${acc.weight} | ${acc.permission.actor}@${acc.permission.permission}` 15 | 16 | export default class UpdatePermission extends Command { 17 | static description = 'Update Permission' 18 | 19 | static args = [ 20 | { name: 'account', required: true, description: 'Account to modify' }, 21 | ] 22 | 23 | async run() { 24 | const { args } = this.parse(UpdatePermission) 25 | 26 | // Track 27 | let account: RpcInterfaces.GetAccountResult 28 | let lightAccount: any 29 | let permissionSnapshot 30 | let step 31 | let currentPermission: any 32 | 33 | const reset = async () => { 34 | // Sorts in-place and displays 35 | account = await network.rpc.get_account(args.account) 36 | lightAccount = await getLightAccount(args.account) 37 | await CliUx.ux.log('\n' + parsePermissions(account.permissions, lightAccount) + '\n') 38 | account.permissions.map(perm => { 39 | perm.required_auth.keys = perm.required_auth.keys.map(key => { 40 | key.key = Key.PublicKey.fromString(key.key).toString() 41 | return key 42 | }) 43 | return perm 44 | }) 45 | permissionSnapshot = JSON.stringify(account.permissions) 46 | 47 | // Reset 48 | step = 'selectPermission' 49 | currentPermission = undefined 50 | } 51 | await reset() 52 | 53 | // Save 54 | const save = async () => { 55 | sortRequiredAuth(currentPermission.required_auth) 56 | 57 | await CliUx.ux.log(green('\n' + 'Expected Permissions:')) 58 | await CliUx.ux.log(parsePermissions(account!.permissions, lightAccount, false) + '\n') 59 | 60 | const authority = await CliUx.ux.prompt(green(`Enter signing permission`), { default: `${args.account}@active` }) 61 | const [actor, permission] = authority.split('@') 62 | await network.transact({ 63 | actions: [{ 64 | account: 'eosio', 65 | name: 'updateauth', 66 | data: { 67 | account: args.account, 68 | permission: currentPermission.perm_name, 69 | parent: currentPermission.parent, 70 | auth: currentPermission.required_auth 71 | }, 72 | authorization: [{ actor, permission }] 73 | }] 74 | }) 75 | await CliUx.ux.log(`${green('Success:')} Permission updated`) 76 | step = 'displayPermission' 77 | await wait(1000) 78 | } 79 | 80 | const deleteCurrentPerm = async () => { 81 | const authority = await CliUx.ux.prompt(green(`Enter signing permission`), { default: `${args.account}@active` }) 82 | const [actor, permission] = authority.split('@') 83 | 84 | const authorization = [{ actor, permission }] 85 | const removeLinksActions = lightAccount 86 | ? lightAccount.linkauth 87 | .filter((_: any) => _.requirement === currentPermission.perm_name) 88 | .map((_: any) => ({ 89 | account: 'eosio', 90 | name: 'unlinkauth', 91 | data: { 92 | account: args.account, 93 | code: _.code, 94 | type: _.type 95 | }, 96 | authorization 97 | })) 98 | : [] 99 | 100 | const deleteActions = [ 101 | { 102 | account: 'eosio', 103 | name: 'deleteauth', 104 | data: { 105 | account: args.account, 106 | permission: currentPermission.perm_name, 107 | }, 108 | authorization: [{ actor, permission }] 109 | } 110 | ] 111 | 112 | await network.transact({ 113 | actions: removeLinksActions.concat(deleteActions) 114 | }) 115 | await CliUx.ux.log(`${green('Success:')} Permission deleted`) 116 | step = 'displayPermission' 117 | await wait(1000) 118 | } 119 | 120 | while (true) { 121 | if (step === 'displayPermission') { 122 | await reset() 123 | } 124 | 125 | if (step === 'selectPermission') { 126 | const extraOptions = ['Add New Permission'] 127 | if (permissionSnapshot !== JSON.stringify(account!.permissions)) { 128 | extraOptions.unshift(green('Save')) 129 | } 130 | 131 | const choices = account!.permissions.map((_: any) => _.perm_name) 132 | .concat([new Separator() as any]) 133 | .concat(extraOptions) 134 | 135 | const permission = await promptChoices('Choose permission to edit:', choices) 136 | 137 | if (permission === green('Save')) { 138 | await save() 139 | } else { 140 | if (permission === 'Add New Permission') { 141 | const permission = await promptName('permission', { default: 'newperm' }) 142 | const parentpermission = await promptChoices('Choose parent permission:', account!.permissions.map((_: any) => _.perm_name), 'active') 143 | account!.permissions.push({ 144 | perm_name: permission, 145 | parent: parentpermission, 146 | required_auth: { 147 | threshold: 1, 148 | keys: [], 149 | accounts: [], 150 | waits: [] 151 | } 152 | }) 153 | currentPermission = account!.permissions[account!.permissions.length - 1] 154 | } else { 155 | currentPermission = account!.permissions.find((_: any) => _.perm_name === permission) 156 | } 157 | 158 | step = 'editPermission' 159 | } 160 | } 161 | 162 | if (step === 'editPermission' && currentPermission) { 163 | // Authorities 164 | const keys = currentPermission.required_auth.keys.map(parseKey); 165 | const accounts = currentPermission.required_auth.accounts.map(parseAccount); 166 | 167 | // Weights 168 | const totalKeysWeight = currentPermission.required_auth.keys.reduce((acc: any, key: any) => acc + key.weight, 0) 169 | const totalAccountsWeight = currentPermission.required_auth.accounts.reduce((acc: any, account: any) => acc + account.weight, 0) 170 | const maxThreshold = totalKeysWeight + totalAccountsWeight 171 | 172 | // Options 173 | const extraOptions = [ 174 | 'Add New Key', 175 | 'Add New Account', 176 | 'Go Back' 177 | ] 178 | 179 | if (!['owner', 'active'].includes(currentPermission.perm_name)) { 180 | extraOptions.splice(2, 0, `Delete Permission`) 181 | } 182 | 183 | if (maxThreshold > currentPermission.required_auth.threshold) { 184 | extraOptions.splice(2, 0, `Edit Threshold (Current: ${currentPermission.required_auth.threshold}, Max: ${maxThreshold})`) 185 | } 186 | 187 | if (permissionSnapshot !== JSON.stringify(account!.permissions) && maxThreshold > 0) { 188 | extraOptions.unshift(green('Save')) 189 | } 190 | 191 | const choices = keys.concat(accounts) 192 | .concat([new Separator() as any]) 193 | .concat(extraOptions) 194 | let authorization = await promptChoices( 195 | `Choose permission ${currentPermission.perm_name}'s authorization to edit:`, 196 | choices, 197 | permissionSnapshot !== JSON.stringify(account!.permissions) ? green('Save') : undefined 198 | ) 199 | 200 | if (authorization === 'Go Back') { 201 | step = 'selectPermission' 202 | currentPermission = undefined 203 | account!.permissions = JSON.parse(permissionSnapshot as any) 204 | } 205 | else if (authorization.indexOf('PUB_') !== -1) { 206 | const rawKey = authorization.split(' | ')[1] 207 | const selectedKeyIndex = currentPermission.required_auth.keys.findIndex((key: any) => key.key === rawKey) 208 | const selectedKey = currentPermission.required_auth.keys[selectedKeyIndex] 209 | 210 | let goBack = false 211 | while (!goBack) { 212 | const option = await promptChoices(`Choose an action for ${authorization}:`, ['Edit Weight', 'Edit Key', 'Delete Key', 'Go Back']) 213 | if (option === 'Edit Weight') { 214 | selectedKey!.weight = await promptInteger('key weight') 215 | authorization = parseKey(selectedKey) 216 | } else if (option === 'Edit Key') { 217 | selectedKey!.key = await promptKey() 218 | authorization = parseKey(selectedKey) 219 | } else if (option === 'Delete Key') { 220 | currentPermission.required_auth.keys.splice(selectedKeyIndex, 1) 221 | goBack = true 222 | } else if (option === 'Go Back') { 223 | goBack = true 224 | } 225 | } 226 | } 227 | else if (authorization.indexOf('@') !== -1) { 228 | const [actor, permission] = authorization.split(' | ')[1].split('@') 229 | const selectedAccountIndex = currentPermission.required_auth.accounts.findIndex((account: any) => account.permission.actor === actor && account.permission.permission === permission) 230 | const selectedAccount = currentPermission.required_auth.accounts[selectedAccountIndex] 231 | 232 | let goBack = false 233 | while (!goBack) { 234 | const choices = ['Edit Weight', 'Edit Account', 'Go Back'] 235 | if (maxThreshold > currentPermission.required_auth.threshold) { 236 | choices.splice(2, 0, 'Delete Account') 237 | } 238 | 239 | const option = await promptChoices(`Choose an action for ${authorization}:`, choices) 240 | if (option === 'Edit Weight') { 241 | selectedAccount!.weight = await promptInteger('account weight') 242 | authorization = parseAccount(selectedAccount) 243 | } else if (option === 'Edit Account') { 244 | selectedAccount!.permission = await promptAuthority() 245 | authorization = parseAccount(selectedAccount) 246 | } else if (option === 'Delete Account') { 247 | currentPermission.required_auth.accounts.splice(selectedAccountIndex, 1) 248 | goBack = true 249 | } else if (option === 'Go Back') { 250 | goBack = true 251 | } 252 | } 253 | } 254 | else if (authorization === 'Delete Permission') { 255 | await deleteCurrentPerm() 256 | } 257 | else if (authorization.indexOf('Edit Threshold') !== -1) { 258 | currentPermission.required_auth.threshold = await promptInteger('threshold') 259 | } 260 | else if (authorization === 'Add New Key') { 261 | currentPermission.required_auth.keys.push({ 262 | weight: await promptInteger('key weight'), 263 | key: await promptKey() 264 | }) 265 | } 266 | else if (authorization === 'Add New Account') { 267 | currentPermission.required_auth.accounts.push({ 268 | weight: await promptInteger('account weight'), 269 | permission: await promptAuthority() 270 | }) 271 | } 272 | else if (authorization === green('Save')) { 273 | await save() 274 | } 275 | } 276 | } 277 | } 278 | 279 | async catch(e: Error) { 280 | CliUx.ux.error(red(e.message)) 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /src/commands/permission/link.ts: -------------------------------------------------------------------------------- 1 | import { Command, flags } from '@oclif/command' 2 | import { CliUx } from '@oclif/core' 3 | import { network } from '../../storage/networks' 4 | import { green } from 'colors' 5 | 6 | export default class LinkAuth extends Command { 7 | static description = 'Link Auth' 8 | 9 | static args = [ 10 | { name: 'account', required: true }, 11 | { name: 'permission', required: true }, 12 | { name: 'contract', required: true }, 13 | { name: 'action', required: false, default: '' }, 14 | ] 15 | 16 | static flags = { 17 | permission: flags.string({ char: 'p', default: '', description: 'Permission to sign with (e.g. account@active)' }) 18 | } 19 | 20 | async run() { 21 | const {args, flags} = this.parse(LinkAuth) 22 | 23 | const [actor, permission] = flags.permission.split('@') 24 | 25 | await network.transact({ 26 | actions: [{ 27 | account: 'eosio', 28 | name: 'linkauth', 29 | data: { 30 | account: args.account, 31 | requirement: args.permission, 32 | code: args.contract, 33 | type: args.action 34 | }, 35 | authorization: [{ 36 | actor: actor || args.account, 37 | permission: permission || 'active' 38 | }] 39 | }] 40 | }) 41 | 42 | await CliUx.ux.log(`${green('Success:')} Permission successfully linked.`) 43 | } 44 | 45 | async catch(e: Error) { 46 | CliUx.ux.error(e) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/commands/permission/unlink.ts: -------------------------------------------------------------------------------- 1 | import { Command, flags } from '@oclif/command' 2 | import { CliUx } from '@oclif/core' 3 | import { network } from '../../storage/networks' 4 | import { green } from 'colors' 5 | 6 | export default class UnlinkAuth extends Command { 7 | static description = 'Unlink Auth' 8 | 9 | static args = [ 10 | { name: 'account', required: true }, 11 | { name: 'contract', required: true }, 12 | { name: 'action', required: false, default: '' }, 13 | ] 14 | 15 | static flags = { 16 | permission: flags.string({ char: 'p', default: '' }) 17 | } 18 | 19 | async run() { 20 | const {args, flags} = this.parse(UnlinkAuth) 21 | 22 | const [actor, permission] = flags.permission.split('@') 23 | 24 | await network.transact({ 25 | actions: [{ 26 | account: 'eosio', 27 | name: 'unlinkauth', 28 | data: { 29 | account: args.account, 30 | code: args.contract, 31 | type: args.action 32 | }, 33 | authorization: [{ 34 | actor: actor || args.account, 35 | permission: permission || 'active' 36 | }] 37 | }] 38 | }) 39 | 40 | await CliUx.ux.log(`${green('Success:')} Permission successfully unlinked.`) 41 | } 42 | 43 | async catch(e: Error) { 44 | CliUx.ux.error(e) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/commands/psr/index.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import { CliUx } from '@oclif/core' 3 | import { green, red } from 'colors' 4 | import { ProtonLinkSessionManager } from '../../apis/esr/manager' 5 | import { IProtonLinkSessionManagerSessionExtended } from '../../apis/esr/session' 6 | import { ProtonLinkSessionManagerStorage, ProtonLinkSessionManagerStorageOptions } from '../../apis/esr/storage' 7 | import { v4 as uuid } from 'uuid' 8 | import { PrivateKey } from '@greymass/eosio' 9 | 10 | import { network } from '../../storage/networks' 11 | import { signUri } from '../../apis/uri/signUri' 12 | 13 | const DEFAULT_SERVICE = 'cb.anchor.link' 14 | 15 | export default class CreateSession extends Command { 16 | static description = 'Create Session' 17 | 18 | static args = [ 19 | {name: 'uri', required: true}, 20 | ] 21 | 22 | async run() { 23 | const {args} = this.parse(CreateSession) 24 | 25 | // Parse URI 26 | const uri = args.uri 27 | 28 | // Get account 29 | const promptAccount = await CliUx.ux.prompt('Enter account to login with (e.g. account@active)', { required: true }) 30 | const [actor, permission] = promptAccount.split('@') 31 | const auth = { actor, permission } 32 | 33 | // Create storage 34 | let localData: ProtonLinkSessionManagerStorageOptions = { 35 | linkId: uuid(), 36 | linkUrl: DEFAULT_SERVICE, 37 | requestKey: PrivateKey.generate('K1').toWif(), 38 | sessions: [], 39 | } 40 | 41 | const storage: ProtonLinkSessionManagerStorage = new ProtonLinkSessionManagerStorage( 42 | localData, 43 | async (storage: ProtonLinkSessionManagerStorageOptions) => { 44 | localData = storage 45 | }, 46 | () => localData.sessions 47 | ) 48 | 49 | // Create session manager 50 | const sessionManager = new ProtonLinkSessionManager({ 51 | handler: { 52 | onIncomingRequest: async (uri: string, session: IProtonLinkSessionManagerSessionExtended) => { 53 | // console.log('onIncomingRequest') 54 | await signUri(uri, auth, sessionManager, session) 55 | }, 56 | onSocketEvent: (type: string, event: any) => { 57 | // console.log(type, event) 58 | } 59 | }, 60 | storage 61 | }) 62 | await sessionManager.connect() 63 | 64 | // Account key 65 | const account = await network.rpc.get_account(actor) 66 | const perm = account.permissions.find(_ => _.perm_name === permission) 67 | if (!perm) { 68 | throw new Error(`No permission found for ${actor}@${permission}`) 69 | } 70 | 71 | await signUri(uri, auth, sessionManager) 72 | await CliUx.ux.log(`${green('Success:')} TX Signed`) 73 | 74 | // Prevent it from dying 75 | setTimeout(() => {}, 1000 * 60 * 60 * 24) 76 | } 77 | 78 | async catch(e: Error) { 79 | CliUx.ux.error(red(e.message)) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/commands/ram/buy.ts: -------------------------------------------------------------------------------- 1 | import { Command, flags } from '@oclif/command' 2 | import { CliUx } from '@oclif/core' 3 | import { green } from 'colors' 4 | import { network } from '../../storage/networks' 5 | 6 | export default class ClaimFaucet extends Command { 7 | static description = 'Claim faucet' 8 | 9 | static args = [ 10 | { name: 'buyer', required: true, description: 'Account paying for RAM' }, 11 | { name: 'receiver', required: true, description: 'Account receiving RAM' }, 12 | { name: 'bytes', required: true, description: 'Bytes of RAM to purchase' }, 13 | ] 14 | 15 | static flags = { 16 | authorization: flags.string({ char: 'p', description: 'Use a specific authorization other than buyer@active' }), 17 | } 18 | 19 | async run() { 20 | const { args, flags } = this.parse(ClaimFaucet) 21 | 22 | const [actor, permission] = flags.authorization 23 | ? flags.authorization.split('@') 24 | : [args.buyer] 25 | 26 | await network.transact({ 27 | actions: [{ 28 | account: 'eosio', 29 | name: 'buyrambytes', 30 | data: { 31 | payer: actor, 32 | receiver: args.receiver, 33 | bytes: args.bytes, 34 | }, 35 | authorization: [{ 36 | actor, 37 | permission: permission || 'active' 38 | }] 39 | }] 40 | }) 41 | 42 | CliUx.ux.log(`${green('Success:')} RAM Purchased`) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/commands/ram/index.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import { CliUx } from '@oclif/core' 3 | import { getRamPrice } from '../../apis/getRamPrice' 4 | 5 | export default class Ram extends Command { 6 | static description = 'List Ram price' 7 | 8 | async run() { 9 | const ramPrice = await getRamPrice() 10 | CliUx.ux.log(`RAM costs ${ramPrice.toFixed(4)} XPR / byte`) 11 | CliUx.ux.log(`RAM costs ${(ramPrice * 1024).toFixed(4)} XPR / KB`) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/commands/rpc/accountsbyauthorizers.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import { CliUx } from '@oclif/core' 3 | import { network } from '../../storage/networks' 4 | 5 | export default class AccountsByAuthorizer extends Command { 6 | static description = 'Get Accounts by Authorization' 7 | 8 | static args = [ 9 | { 10 | name: 'authorizations', required: true, default: [], parse: (input: string) => { 11 | const parsed = JSON.parse(input) 12 | return Array.isArray(parsed) ? parsed : [parsed] 13 | } 14 | }, 15 | { name: 'keys', required: false, default: [] }, 16 | ] 17 | 18 | async run() { 19 | const { args } = this.parse(AccountsByAuthorizer) 20 | console.log(args.authorizations, args.keys) 21 | const res = await network.rpc.get_accounts_by_authorizers(args.authorizations, args.keys) 22 | await CliUx.ux.styledJSON(res) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/commands/scan/index.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import open from 'open' 3 | import { getExplorer } from '../../apis/getExplorer' 4 | 5 | export default class Scan extends Command { 6 | static description = 'Open Account in Proton Scan' 7 | 8 | static args = [ 9 | { name: 'account', required: true }, 10 | ] 11 | 12 | async run() { 13 | const { args } = this.parse(Scan) 14 | 15 | const explorer: string = getExplorer() 16 | open(`${explorer}/account/${args.account}`) 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/commands/system/buyram.ts: -------------------------------------------------------------------------------- 1 | import { Command, flags } from '@oclif/command' 2 | import { network } from '../../storage/networks' 3 | import { CliUx } from '@oclif/core' 4 | import { green } from 'colors'; 5 | import { getExplorer } from '../../apis/getExplorer'; 6 | 7 | export default class BuyRam extends Command { 8 | static description = 'System Buy Ram' 9 | static hidden = true 10 | 11 | static args = [ 12 | {name: 'receiver', required: true}, 13 | {name: 'bytes', required: true}, 14 | ] 15 | 16 | async run() { 17 | const {args} = this.parse(BuyRam) 18 | 19 | const actions = [ 20 | { 21 | account: "eosio", 22 | name: "buyrambsys", 23 | authorization: [{ 24 | actor: "wlcm.proton", 25 | permission: "newacc" 26 | } 27 | ], 28 | data: { 29 | payer: "wlcm.proton", 30 | receiver: args.receiver, 31 | bytes: args.bytes, 32 | } 33 | } 34 | ] 35 | 36 | // Execute 37 | await network.transact({ actions }) 38 | 39 | this.log(`${green('Success:')} Bought ${args.bytes} bytes RAM for ${args.receiver}!`) 40 | await CliUx.ux.url('View Account on block explorer', `${getExplorer()}/account/${args.receiver}`) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/commands/system/delegatebw.ts: -------------------------------------------------------------------------------- 1 | import { Command, flags } from '@oclif/command' 2 | import { network } from '../../storage/networks' 3 | import { CliUx } from '@oclif/core' 4 | import { green } from 'colors'; 5 | import { getExplorer } from '../../apis/getExplorer'; 6 | 7 | export default class DelegateBandwidth extends Command { 8 | static description = 'System Delegate Bandwidth' 9 | static hidden = true 10 | 11 | static flags = { 12 | transfer: flags.boolean({char: 't', default: false}), 13 | } 14 | 15 | static args = [ 16 | {name: 'receiver', required: true}, 17 | {name: 'cpu', required: true}, 18 | {name: 'net', required: true}, 19 | ] 20 | 21 | async run() { 22 | const {args, flags} = this.parse(DelegateBandwidth) 23 | 24 | 25 | const actions = [ 26 | { 27 | account: "eosio", 28 | name: "delegatebw", 29 | authorization: [{ 30 | actor: "wlcm.proton", 31 | permission: "newacc" 32 | }], 33 | data: { 34 | from: "wlcm.proton", 35 | receiver: args.receiver, 36 | stake_cpu_quantity: args.cpu, 37 | stake_net_quantity: args.net, 38 | transfer: Number(flags.transfer), 39 | }, 40 | } 41 | ] 42 | 43 | // Execute 44 | await network.transact({ actions }) 45 | 46 | this.log(`${green('Success:')} Added resources to ${args.account}!`) 47 | await CliUx.ux.url('View Account on block explorer', `${getExplorer()}/account/${args.receiver}`) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/commands/system/newaccount.ts: -------------------------------------------------------------------------------- 1 | import { Command, flags } from '@oclif/command' 2 | import { network } from '../../storage/networks' 3 | import { CliUx } from '@oclif/core' 4 | import { green } from 'colors'; 5 | import { getExplorer } from '../../apis/getExplorer'; 6 | 7 | interface Permission { 8 | threshold: number; 9 | keys: { 10 | weight: number; 11 | key: string; 12 | }[]; 13 | accounts: { 14 | weight: number; 15 | permission: { 16 | actor: string; 17 | permission: string; 18 | }; 19 | }[]; 20 | waits: { 21 | weight: number; 22 | wait_sec: number; 23 | }[]; 24 | } 25 | 26 | const parsePermission = (entry: string) => { 27 | const perm: Permission = { 28 | threshold: 1, 29 | keys: [], 30 | accounts: [], 31 | waits: [], 32 | } 33 | 34 | if (entry.indexOf('EOS') !== -1 || entry.indexOf('PUB_K1') !== -1) { 35 | perm.keys.push({key: entry, weight: 1}) 36 | } else if (entry.indexOf('@') === -1) { 37 | perm.accounts.push({weight: 1, permission: {actor: entry, permission: 'active'}}) 38 | } else { 39 | const [actor, permission] = entry.split('@') 40 | perm.accounts.push({weight: 1, permission: {actor, permission}}) 41 | } 42 | 43 | return perm 44 | } 45 | 46 | const addCodeToPerm = (perm: Permission, actor: string) => { 47 | perm.accounts.push({weight: 1, permission: {actor, permission: 'eosio.code'}}) 48 | return perm 49 | } 50 | 51 | export default class NewAccount extends Command { 52 | static description = 'System New Account' 53 | static hidden = true 54 | 55 | static flags = { 56 | help: flags.help({char: 'h'}), 57 | net: flags.string({char: 'n', default: '10.0000 SYS'}), 58 | cpu: flags.string({char: 'c', default: '10.0000 SYS'}), 59 | ram: flags.integer({char: 'r', default: 12288}), 60 | transfer: flags.boolean({char: 't', default: false}), 61 | code: flags.boolean({default: false}), 62 | } 63 | 64 | static args = [ 65 | {name: 'account', required: true}, 66 | {name: 'owner', required: true}, 67 | {name: 'active', required: true}, 68 | ] 69 | 70 | async run() { 71 | const {args, flags} = this.parse(NewAccount) 72 | 73 | // Ensure account does not exist 74 | try { 75 | await network.rpc.get_account(args.account) 76 | this.log(`Account ${args.account} already exists!`) 77 | await CliUx.ux.url('View Account on block explorer', `${getExplorer()}/account/${args.account}#keys`) 78 | return 79 | } catch (error) { 80 | // Do nothing 81 | } 82 | 83 | const actions = [ 84 | { 85 | account: 'eosio', 86 | name: 'newaccount', 87 | authorization: [{ 88 | actor: 'proton', 89 | permission: 'active', 90 | }], 91 | data: { 92 | creator: 'proton', 93 | name: args.account, 94 | owner: parsePermission(args.owner), 95 | active: parsePermission(args.active), 96 | }, 97 | }, 98 | { 99 | account: 'eosio', 100 | name: 'buyrambsys', 101 | authorization: [{ 102 | actor: 'wlcm.proton', 103 | permission: 'newacc', 104 | }], 105 | data: { 106 | payer: 'wlcm.proton', 107 | receiver: args.account, 108 | bytes: flags.ram, 109 | }, 110 | }, 111 | { 112 | account: 'eosio', 113 | name: 'delegatebw', 114 | authorization: [{ 115 | actor: 'wlcm.proton', 116 | permission: 'newacc', 117 | }], 118 | data: { 119 | from: 'wlcm.proton', 120 | receiver: args.account, 121 | stake_net_quantity: flags.net, 122 | stake_cpu_quantity: flags.cpu, 123 | transfer: flags.transfer, 124 | }, 125 | }, 126 | ] 127 | 128 | // Add eosio.code 129 | if (flags.code) { 130 | actions[0].data.active = addCodeToPerm(actions[0].data.active!, args.account) 131 | } 132 | 133 | // Execute 134 | await network.transact({ actions }) 135 | 136 | this.log(`${green('Success:')} Account ${args.account} created!`) 137 | await CliUx.ux.url('View Account on block explorer', `${getExplorer()}/account/${args.account}#keys`) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/commands/system/setramlimit.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import { network } from '../../storage/networks' 3 | import { green } from 'colors'; 4 | 5 | export default class SetRamLimit extends Command { 6 | static description = 'System Set RAM Limit' 7 | static hidden = true 8 | 9 | static args = [ 10 | {name: 'account', required: true}, 11 | {name: 'ramlimit', required: true}, 12 | ] 13 | 14 | async run() { 15 | const {args} = this.parse(SetRamLimit) 16 | 17 | const actions = [ 18 | { 19 | account: "eosio", 20 | name: "ramlimitset", 21 | authorization: [{ 22 | actor: "admin.proton", 23 | permission: "light" 24 | }], 25 | data: { 26 | account: args.account, 27 | ramlimit: +args.ramlimit, 28 | }, 29 | } 30 | ] 31 | 32 | // Execute 33 | await network.transact({ actions }) 34 | 35 | this.log(`${green('Success:')} Set RAM limit for ${args.account} to ${args.ramlimit}!`) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/commands/system/undelegatebw.ts: -------------------------------------------------------------------------------- 1 | import { Command, flags } from '@oclif/command' 2 | import { network } from '../../storage/networks' 3 | import { CliUx } from '@oclif/core' 4 | import { green } from 'colors'; 5 | import { getExplorer } from '../../apis/getExplorer'; 6 | 7 | export default class UndelegateBandwidth extends Command { 8 | static description = 'System Undelegate Bandwidth' 9 | static hidden = true 10 | 11 | static args = [ 12 | {name: 'receiver', required: true}, 13 | {name: 'cpu', required: true}, 14 | {name: 'net', required: true}, 15 | ] 16 | 17 | async run() { 18 | const {args} = this.parse(UndelegateBandwidth) 19 | 20 | 21 | const actions = [ 22 | { 23 | account: "eosio", 24 | name: "undelegatebw", 25 | authorization: [{ 26 | actor: "wlcm.proton", 27 | permission: "newacc" 28 | }], 29 | data: { 30 | from: "wlcm.proton", 31 | receiver: args.receiver, 32 | stake_cpu_quantity: args.cpu, 33 | stake_net_quantity: args.net 34 | }, 35 | } 36 | ] 37 | 38 | // Execute 39 | await network.transact({ actions }) 40 | 41 | this.log(`${green('Success:')} Removed resources from ${args.account}!`) 42 | await CliUx.ux.url('View Account on block explorer', `${getExplorer()}/account/${args.receiver}`) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/commands/table/index.ts: -------------------------------------------------------------------------------- 1 | import { Command, flags } from '@oclif/command' 2 | import { CliUx } from '@oclif/core' 3 | import { network } from '../../storage/networks' 4 | import { prompt } from 'inquirer' 5 | 6 | export default class GetTable extends Command { 7 | static description = 'Get Table Storage Rows' 8 | 9 | static args = [ 10 | { name: 'contract', required: true }, 11 | { name: 'table', required: false }, 12 | { name: 'scope', required: false }, 13 | ] 14 | 15 | static flags = { 16 | lowerBound: flags.string({ char: 'l', default: undefined }), 17 | upperBound: flags.string({ char: 'u', default: undefined }), 18 | keyType: flags.string({ char: 'k', default: undefined }), 19 | reverse: flags.boolean({ char: 'r', default: false }), 20 | showPayer: flags.boolean({ char: 'p', default: false }), 21 | limit: flags.integer({ char: 'c', default: 100 }), 22 | indexPosition: flags.integer({ char: 'i', default: 1 }), 23 | } 24 | 25 | async run() { 26 | const { args, flags } = this.parse(GetTable) 27 | 28 | // Have user choose table if not present 29 | if (!args.table) { 30 | const { abi: { tables } } = await network.rpc.get_abi(args.contract) 31 | const { table } = await prompt<{ table: string }>([ 32 | { 33 | name: 'table', 34 | type: 'list', 35 | message: 'Which of these tables would you like to fetch?', 36 | choices: tables.map((t) => t.name), 37 | }, 38 | ]); 39 | args.table = table 40 | } 41 | 42 | // Fetch rows 43 | const rows = await network.rpc.get_table_rows({ 44 | json: true, 45 | code: args.contract, 46 | scope: args.scope || args.contract, 47 | table: args.table, 48 | lower_bound: flags.lowerBound, 49 | upper_bound: flags.upperBound, 50 | index_position: flags.indexPosition, 51 | key_type: flags.keyType, 52 | limit: flags.limit, 53 | reverse: flags.reverse, 54 | show_payer: flags.showPayer, 55 | }) 56 | CliUx.ux.styledJSON(rows) 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /src/commands/transaction/get.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import { CliUx } from '@oclif/core' 3 | import { network } from '../../storage/networks' 4 | 5 | export default class Transaction extends Command { 6 | static description = 'Get Transaction by Transaction ID' 7 | 8 | static args = [ 9 | { name: 'id', required: true }, 10 | ] 11 | 12 | async run() { 13 | const { args } = this.parse(Transaction) 14 | const result = await network.rpc.history_get_transaction(args.id) 15 | CliUx.ux.styledJSON(result) 16 | } 17 | 18 | async catch(e: Error) { 19 | CliUx.ux.styledJSON(e) 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /src/commands/transaction/index.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import { CliUx } from '@oclif/core' 3 | import { network } from '../../storage/networks' 4 | import { parseDetailsError } from '../../utils/detailsError' 5 | 6 | export default class Transaction extends Command { 7 | static description = 'Execute Transaction' 8 | 9 | static args = [ 10 | { name: 'json', required: true }, 11 | ] 12 | 13 | async run() { 14 | const { args } = this.parse(Transaction) 15 | 16 | // Fetch rows 17 | const result = await network.transact(args.json) 18 | CliUx.ux.styledJSON(result) 19 | } 20 | 21 | async catch(e: Error | any) { 22 | parseDetailsError(e) 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/commands/transaction/push.ts: -------------------------------------------------------------------------------- 1 | import { Command, flags } from '@oclif/command' 2 | import { CliUx } from '@oclif/core' 3 | import { network } from '../../storage/networks' 4 | import { parseDetailsError } from '../../utils/detailsError' 5 | 6 | export default class PushTransaction extends Command { 7 | static description = 'Push Transaction' 8 | 9 | static args = [ 10 | { name: 'transaction', required: true }, 11 | ] 12 | 13 | static flags = { 14 | endpoint: flags.string({ char: 'u', description: 'Your RPC endpoint' }), 15 | } 16 | 17 | async run() { 18 | const { args, flags } = this.parse(PushTransaction) 19 | 20 | // Fetch rows 21 | const result = await network.transact(JSON.parse(args.transaction), { endpoint: flags.endpoint }) 22 | CliUx.ux.styledJSON(result) 23 | } 24 | 25 | async catch(e: Error) { 26 | parseDetailsError(e) 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src/commands/version.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import { CliUx } from '@oclif/core' 3 | 4 | const packageJson = require('../../package.json') 5 | 6 | export default class Version extends Command { 7 | static description = 'Version of CLI' 8 | 9 | async run() { 10 | this.log(packageJson.version) 11 | } 12 | 13 | async catch(e: Error) { 14 | CliUx.ux.styledJSON(e) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const networks = [ 2 | { 3 | chain: 'proton', 4 | endpoints: [ 5 | 'https://proton.greymass.com' 6 | ] 7 | }, 8 | { 9 | chain: 'proton-test', 10 | endpoints: [ 11 | 'https://protontestnet.ledgerwise.io', 12 | 'https://proton-testnet.eosphere.io' 13 | ] 14 | } 15 | ] -------------------------------------------------------------------------------- /src/core/flags/index.ts: -------------------------------------------------------------------------------- 1 | import { Flags } from '@oclif/core'; 2 | 3 | 4 | 5 | export const destinationFolder = Flags.build({ 6 | char: 'o', 7 | description: 'The relative path to folder the the contract should be located. Current folder by default.', 8 | default: '' 9 | }); 10 | -------------------------------------------------------------------------------- /src/core/generators/common.ts: -------------------------------------------------------------------------------- 1 | export interface IParameter { 2 | name: string; 3 | type: string; 4 | isNullable?: boolean; 5 | isArray?: boolean; 6 | } 7 | 8 | export const PARAMETER_TYPES: Readonly = ['u64', 'i32', 'u32', 'u8', 'i8','string', 'Name' ,'Asset','Symbol']; 9 | 10 | export const PARAMETER_TYPES_TO_IMPORT: Set = new Set(['Name','Asset','Symbol']); 11 | 12 | export function fixParameterType(type: string): string { 13 | if (PARAMETER_TYPES.indexOf(type) < 0) { 14 | return 'u64'; 15 | } 16 | return type; 17 | } 18 | -------------------------------------------------------------------------------- /src/core/generators/constructorParameters.ts: -------------------------------------------------------------------------------- 1 | import { ConstructorDeclaration, ParameterDeclaration, Scope } from 'ts-morph'; 2 | import { fixParameterType, IParameter } from './common'; 3 | import { parameterAdd, parametersCollect, parametersExtractImports, parameterToDeclaration } from './parameters'; 4 | 5 | export const CONSTRUCTOR_PARAMETER_TYPES = new Map([ 6 | ['u64', { 7 | initializer: '0', 8 | }], 9 | ['i32', { 10 | initializer: '0', 11 | }], 12 | ['u32', { 13 | initializer: '0', 14 | }], 15 | ['u8', { 16 | initializer: '0', 17 | }], 18 | ['i8', { 19 | initializer: '0', 20 | }], 21 | ['Name', { 22 | initializer: 'new Name()', 23 | }], 24 | ['string', { 25 | initializer: '""', 26 | }], 27 | ]); 28 | 29 | export async function constructorAddParameters(tableContructor: ConstructorDeclaration, existingParameters: IParameter[] = []) { 30 | const parametersToAdd: IParameter[] = await parametersCollect(existingParameters); 31 | const extraImports: string[] = []; 32 | if (parametersToAdd.length > 0) { 33 | 34 | const typesToImport = parametersExtractImports(parametersToAdd); 35 | if (typesToImport.length > 0) { 36 | extraImports.push(...typesToImport); 37 | } 38 | 39 | parametersToAdd.forEach((property) => { 40 | constructorAddParameter(tableContructor, property); 41 | }); 42 | } 43 | 44 | return extraImports; 45 | } 46 | 47 | export function constructorAddParameter( 48 | contructor: ConstructorDeclaration, 49 | parameter: IParameter 50 | ): ParameterDeclaration { 51 | const fixedType = fixParameterType(parameter.type); 52 | 53 | const declaration = parameterToDeclaration(parameter); 54 | 55 | const paramType = CONSTRUCTOR_PARAMETER_TYPES.get(fixedType); 56 | let initializer = '""'; 57 | if (paramType) { 58 | if (parameter.isNullable) { 59 | initializer = 'null'; 60 | } else if (parameter.isArray) { 61 | initializer = '[]'; 62 | } else { 63 | initializer = paramType.initializer; 64 | } 65 | } 66 | 67 | const result = parameterAdd(contructor, { 68 | ...declaration, 69 | scope: Scope.Public, 70 | initializer 71 | }); 72 | return result; 73 | } 74 | 75 | -------------------------------------------------------------------------------- /src/core/generators/contract.ts: -------------------------------------------------------------------------------- 1 | import { ClassDeclaration, MethodDeclaration } from 'ts-morph'; 2 | import { prompt } from 'inquirer' 3 | import { promptName, validateName } from '../../utils'; 4 | import { IParameter } from './common'; 5 | import { parameterAdd, parametersCollect, parametersExtractImports, parameterToDeclaration } from './parameters'; 6 | import { CliUx } from '@oclif/core'; 7 | 8 | export interface IContractAction { 9 | methodName: string; 10 | name: string; 11 | } 12 | 13 | export interface IContractActionToAdd { 14 | name: string; 15 | parameters: IParameter[]; 16 | } 17 | 18 | export function contractGetActions(contractClass: ClassDeclaration): IContractAction[] { 19 | const existingActions = contractClass.getMethods() 20 | .filter((method) => method.getDecorator('action')) 21 | .map((method) => { 22 | let name = method.getDecorator('action')?.getArguments()[0].getText() || method.getName(); 23 | name = name.replace(/['"]/g, ''); 24 | return { 25 | methodName: method.getName(), 26 | name: name 27 | } 28 | }); 29 | 30 | return existingActions; 31 | } 32 | 33 | export function contractIsActionExists(actionName: string, existingActions: IContractAction[] = []): boolean { 34 | return existingActions.some((action) => action.methodName === actionName || action.name === actionName); 35 | } 36 | 37 | export async function contractPromptAction(actionsToAdd: string[], existingActions: IContractAction[] = []) { 38 | return promptName('action', { 39 | validate: (input) => { 40 | if (validateName(input)) { 41 | if ( 42 | actionsToAdd.indexOf(input) < 0 43 | && !contractIsActionExists(input, existingActions) 44 | ) { 45 | return true; 46 | } 47 | return `Action with this name was already added previously. Try another name.`; 48 | } 49 | return `The provided action name is wrong. 1-12 chars, only lowercase a-z and numbers 1-5 are possible`; 50 | } 51 | }); 52 | } 53 | 54 | export async function contractAddActions(contractClass: ClassDeclaration) { 55 | const existingActions = contractGetActions(contractClass); 56 | 57 | const actionsToAdd: IContractActionToAdd[] = []; 58 | const actionsNamesToAdd: string[] = []; 59 | 60 | const extraImports: string[] = []; 61 | 62 | let stop = false 63 | while (!stop) { 64 | const name = await contractPromptAction(actionsNamesToAdd, existingActions); 65 | actionsNamesToAdd.push(name); 66 | 67 | const { addParameters } = await prompt<{ addParameters: boolean }>([ 68 | { 69 | name: 'addParameters', 70 | type: 'confirm', 71 | message: 'Do you want to add parameters to the action?', 72 | default: false, 73 | }, 74 | ]); 75 | let parametersToAdd: IParameter[] = [] 76 | if (addParameters) { 77 | parametersToAdd = await parametersCollect(); 78 | } 79 | CliUx.ux.log(`————————————`); 80 | 81 | actionsToAdd.push({ 82 | name: name, 83 | parameters: parametersToAdd 84 | }); 85 | 86 | const { next } = await prompt<{ next: boolean }>([ 87 | { 88 | name: 'next', 89 | type: 'confirm', 90 | message: 'Do you want to add one more action?', 91 | default: false, 92 | }, 93 | ]); 94 | stop = !next; 95 | } 96 | 97 | if (actionsToAdd.length > 0) { 98 | actionsToAdd.forEach((action) => { 99 | const typesToImport = parametersExtractImports(action.parameters); 100 | if (typesToImport.length > 0) { 101 | extraImports.push(...typesToImport); 102 | } 103 | contractAddAction(contractClass, action); 104 | }); 105 | } 106 | 107 | return { 108 | extraImports, 109 | actionsToAdd 110 | }; 111 | } 112 | 113 | export function contractAddAction(contractClass: ClassDeclaration, action: IContractActionToAdd): MethodDeclaration { 114 | const existingActions = contractGetActions(contractClass); 115 | const methods = contractClass.getMethods(); 116 | if (contractIsActionExists(action.name, existingActions)) { 117 | throw `Method with the name ${action.name} already exists in the class`; 118 | } 119 | 120 | const method = contractClass.addMethod({ 121 | name: action.name, 122 | parameters: [], 123 | returnType: 'void', 124 | }); 125 | 126 | action.parameters.map((param) => parameterToDeclaration(param)).forEach((declaration) => { 127 | parameterAdd(method, declaration); 128 | }); 129 | 130 | method.addStatements([writer => 131 | writer.writeLine('// Add here a code of your contract') 132 | ]); 133 | 134 | method.addDecorator({ 135 | name: 'action', 136 | arguments: [`"${action.name}"`] 137 | }); 138 | 139 | if (methods.length === 0) { 140 | method.prependWhitespace(writer => writer.newLine()); 141 | } 142 | 143 | return method; 144 | } 145 | -------------------------------------------------------------------------------- /src/core/generators/imports.ts: -------------------------------------------------------------------------------- 1 | import { ImportDeclaration, SourceFile } from 'ts-morph'; 2 | 3 | export function addNamedImports(sourceFile: SourceFile, lib: string, importsToAdd: string[]): ImportDeclaration | undefined { 4 | importsToAdd = importsToAdd.filter((value, idx, self) => self.indexOf(value) === idx); 5 | let libImports = sourceFile.getImportDeclaration(lib); 6 | if (libImports) { 7 | const namedImports = libImports.getNamedImports().map((item) => item.getText()); 8 | const importsNotExist = importsToAdd.reduce((accum: string[], importItem: string) => { 9 | if (namedImports.indexOf(importItem) < 0) { 10 | if (accum.indexOf(importItem) < 0) { 11 | accum.push(importItem); 12 | } 13 | } 14 | return accum; 15 | }, []) 16 | if (importsNotExist.length > 0) { 17 | libImports.addNamedImports(importsNotExist) 18 | } 19 | } else { 20 | libImports = sourceFile.addImportDeclaration({ 21 | namedImports: importsToAdd, 22 | moduleSpecifier: lib, 23 | }); 24 | } 25 | return libImports; 26 | } 27 | -------------------------------------------------------------------------------- /src/core/generators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common'; 2 | export * from './settings'; 3 | export * from './imports'; 4 | export * from './contract'; 5 | export * from './table'; 6 | export * from './constructorParameters'; 7 | export * from './parameters'; 8 | 9 | -------------------------------------------------------------------------------- /src/core/generators/parameters.ts: -------------------------------------------------------------------------------- 1 | import { CliUx } from '@oclif/core'; 2 | import { prompt } from 'inquirer' 3 | import { OptionalKind, ParameterDeclarationStructure, ParameteredNode } from 'ts-morph'; 4 | import { promptChoices, promptName } from '../../utils'; 5 | import { fixParameterType, IParameter, PARAMETER_TYPES, PARAMETER_TYPES_TO_IMPORT } from './common'; 6 | 7 | export async function parameterPrompt(existingParameters: IParameter[], 8 | opts: { 9 | preset?: Partial, 10 | type?: string 11 | } = {} 12 | ): Promise { 13 | if (!opts.type) { 14 | opts.type = 'parameter'; 15 | } 16 | const parameterName = await promptName((opts.type as string), { 17 | validate: (input) => { 18 | if (!existingParameters.some((prop) => prop.name === input)) { 19 | return true; 20 | } 21 | return `Parameter with this name was already added previously. Try another name.`; 22 | } 23 | }); 24 | 25 | const types = PARAMETER_TYPES.slice(); 26 | 27 | const parameterType = await promptChoices( 28 | 'Choose parameter type:', 29 | types, 30 | 'u64'); 31 | 32 | let isArray: boolean = false; 33 | if (opts.preset?.isArray !== undefined) { 34 | isArray = opts.preset.isArray; 35 | } else { 36 | 37 | const { isArrayPrompt } = await prompt<{ isArrayPrompt: boolean }>([ 38 | { 39 | name: 'isArrayPrompt', 40 | type: 'confirm', 41 | message: 'Is the parameter an array?', 42 | default: false, 43 | }, 44 | ]); 45 | 46 | isArray = isArrayPrompt; 47 | } 48 | let isNullable: boolean = false; 49 | if (!isArray) { 50 | if (opts.preset?.isNullable !== undefined) { 51 | isNullable = opts.preset.isNullable; 52 | } else { 53 | const { isNullablePrompt } = await prompt<{ isNullablePrompt: boolean }>([ 54 | { 55 | name: 'isNullablePrompt', 56 | type: 'confirm', 57 | message: 'Can the parameter be nullable?', 58 | default: false, 59 | }, 60 | ]); 61 | isNullable = isNullablePrompt; 62 | } 63 | } 64 | 65 | return { 66 | name: parameterName, 67 | type: parameterType, 68 | isNullable: isNullable, 69 | isArray: isArray 70 | }; 71 | } 72 | 73 | export function parametersExtractImports(parameters: IParameter[]): string[] { 74 | return parameters.reduce((accum: string[], param) => { 75 | if (PARAMETER_TYPES_TO_IMPORT.has(param.type)) { 76 | accum.push(param.type); 77 | } 78 | return accum; 79 | }, []); 80 | } 81 | 82 | export async function parametersCollect(existingParameters: IParameter[] = []) { 83 | const parametersToAdd: IParameter[] = []; 84 | let stop = false 85 | while (!stop) { 86 | const property = await parameterPrompt(existingParameters); 87 | existingParameters.push(property); 88 | parametersToAdd.push(property); 89 | 90 | CliUx.ux.log(`————————————`); 91 | 92 | const { next } = await prompt<{ next: boolean }>([ 93 | { 94 | name: 'next', 95 | type: 'confirm', 96 | message: 'Do you want to add one more parameter?', 97 | default: false, 98 | }, 99 | ]); 100 | stop = !next; 101 | } 102 | 103 | return parametersToAdd; 104 | } 105 | 106 | export function parameterAdd(node: ParameteredNode, declaration: OptionalKind) { 107 | const hasParameters = node.getParameters().length > 0; 108 | const result = node.addParameter(declaration); 109 | result.prependWhitespace(writer => writer.newLine()); 110 | if (!hasParameters) { 111 | result.appendWhitespace(writer => writer.newLine()); 112 | } 113 | return result; 114 | } 115 | 116 | export function parameterToDeclaration(param: IParameter): OptionalKind { 117 | const fixedType = fixParameterType(param.type); 118 | const type = `${fixedType}${param.isArray ? '[]' : ''}${param.isNullable ? ' | null' : ''}`; 119 | return { 120 | name: param.name, 121 | type, 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/core/generators/settings.ts: -------------------------------------------------------------------------------- 1 | import { FormatCodeSettings } from 'ts-morph'; 2 | import { SemicolonPreference } from 'typescript'; 3 | 4 | export const FORMAT_SETTINGS: FormatCodeSettings = { 5 | insertSpaceAfterCommaDelimiter: true, 6 | insertSpaceAfterSemicolonInForStatements: true, 7 | insertSpaceBeforeAndAfterBinaryOperators: true, 8 | insertSpaceAfterKeywordsInControlFlowStatements: true, 9 | insertSpaceAfterFunctionKeywordForAnonymousFunctions: true, 10 | insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: true, 11 | insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: true, 12 | insertSpaceAfterOpeningAndBeforeClosingEmptyBraces: true, 13 | insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: true, 14 | insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: true, 15 | insertSpaceAfterTypeAssertion: true, 16 | indentMultiLineObjectLiteralBeginningOnBlankLine: true, 17 | semicolons: SemicolonPreference.Insert, 18 | ensureNewLineAtEndOfFile: true, 19 | } 20 | -------------------------------------------------------------------------------- /src/core/generators/table.ts: -------------------------------------------------------------------------------- 1 | import { ClassDeclaration, GetAccessorDeclaration } from 'ts-morph'; 2 | import { fixParameterType, IParameter } from './common'; 3 | 4 | export const TABLE_PARAMETER_TYPES = new Map([ 5 | ['Name', { 6 | getPrimary: (paramName: string) => { 7 | return `this.${paramName}.N`; 8 | } 9 | }], 10 | ['string', { 11 | getPrimary: (paramName: string) => { 12 | return `this.${paramName}`; 13 | } 14 | }], 15 | ['u64', { 16 | getPrimary: (paramName: string) => { 17 | return `this.${paramName}`; 18 | } 19 | }], 20 | ['i32', { 21 | getPrimary: (paramName: string) => { 22 | return `(this.${paramName})`; 23 | } 24 | }], 25 | ['u32', { 26 | getPrimary: (paramName: string) => { 27 | return `(this.${paramName})`; 28 | } 29 | }], 30 | ['u8', { 31 | getPrimary: (paramName: string) => { 32 | return `(this.${paramName})`; 33 | } 34 | }], 35 | ['i8', { 36 | getPrimary: (paramName: string) => { 37 | return `(this.${paramName})`; 38 | } 39 | }] 40 | ]) 41 | 42 | export function tableAddPrimaryParameter(table: ClassDeclaration, parameter: IParameter): GetAccessorDeclaration | undefined { 43 | const fixedType = fixParameterType(parameter.type); 44 | const paramType = TABLE_PARAMETER_TYPES.get(fixedType); 45 | if (paramType) { 46 | const result = table.addGetAccessor({ 47 | name: 'primary', 48 | returnType: 'u64', 49 | statements: [`return ${paramType.getPrimary(parameter.name)}`], 50 | }); 51 | result.addDecorator({ 52 | name: 'primary' 53 | }); 54 | return result; 55 | } 56 | return undefined; 57 | } 58 | -------------------------------------------------------------------------------- /src/hooks/command_incomplete.ts: -------------------------------------------------------------------------------- 1 | import { Hook, toConfiguredId, toStandardizedId } from '@oclif/core'; 2 | import { prompt } from 'inquirer'; 3 | 4 | const hook: Hook.CommandIncomplete = async function ({ config, matches, argv }) { 5 | const { command } = await prompt<{ command: string }>([ 6 | { 7 | name: 'command', 8 | type: 'list', 9 | message: 'Which of these commands would you like to run?', 10 | choices: matches.map((p) => toConfiguredId(p.id, config)), 11 | }, 12 | ]); 13 | 14 | if (argv.includes('--help') || argv.includes('-h')) { 15 | return config.runCommand('help', [toStandardizedId(command, config)]); 16 | } 17 | 18 | return config.runCommand(toStandardizedId(command, config), argv); 19 | }; 20 | 21 | export default hook; -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export {run} from '@oclif/command' 2 | -------------------------------------------------------------------------------- /src/storage/config.ts: -------------------------------------------------------------------------------- 1 | import Conf from 'conf' 2 | import { JSONSchemaType as JST } from 'json-schema-typed' 3 | import { networks } from '../constants'; 4 | 5 | const schema = { 6 | privateKeys: { 7 | type: JST.Array, 8 | items: { 9 | type: JST.String 10 | } 11 | }, 12 | isLocked: { 13 | type: JST.Boolean, 14 | }, 15 | tryKeychain: { 16 | type: JST.Boolean, 17 | }, 18 | networks: { 19 | type: JST.Array, 20 | items: { 21 | type: JST.Object, 22 | properties: { 23 | chain: { 24 | type: JST.String, 25 | }, 26 | endpoints: { 27 | type: JST.Array, 28 | items: { 29 | type: JST.String 30 | } 31 | } 32 | } 33 | } 34 | }, 35 | currentChain: { 36 | type: JST.String, 37 | } 38 | }; 39 | 40 | export const config = new Conf<{ 41 | privateKeys: string[], 42 | isLocked: boolean, 43 | tryKeychain: boolean, 44 | networks: { chain: string, endpoints: string[] }[], 45 | currentChain: string 46 | }>({ 47 | schema, 48 | configName: 'proton-cli', 49 | projectVersion: '0.0.2', 50 | defaults: { 51 | privateKeys: [], 52 | tryKeychain: false, 53 | isLocked: false, 54 | networks, 55 | currentChain: networks[0].chain 56 | }, 57 | migrations: { 58 | '0.0.1': store => { 59 | const networks = store.get('networks') 60 | store.set('networks', networks) 61 | } 62 | }, 63 | }); -------------------------------------------------------------------------------- /src/storage/encryptor.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from 'crypto' 2 | 3 | const IV_LENGTH = 16; // For AES, this is always 16 4 | 5 | class Encryptor { 6 | encrypt(key: string, text: string) { 7 | let iv = crypto.randomBytes(IV_LENGTH); 8 | let cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key), iv); 9 | let encrypted = cipher.update(text); 10 | 11 | encrypted = Buffer.concat([encrypted, cipher.final()]); 12 | 13 | return iv.toString('hex') + ':' + encrypted.toString('hex'); 14 | } 15 | 16 | decrypt(key: string, text: string) { 17 | const textParts = text.split(':'); 18 | const iv = Buffer.from(textParts.shift()!, 'hex'); 19 | const encryptedText = Buffer.from(textParts.join(':'), 'hex'); 20 | const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key), iv); 21 | 22 | let decrypted = decipher.update(encryptedText); 23 | decrypted = Buffer.concat([decrypted, decipher.final()]); 24 | 25 | return decrypted.toString(); 26 | } 27 | } 28 | 29 | export const encryptor = new Encryptor() -------------------------------------------------------------------------------- /src/storage/networks.ts: -------------------------------------------------------------------------------- 1 | import { CliUx } from '@oclif/core' 2 | import { JsonRpc, Api, JsSignatureProvider, RpcInterfaces, ApiInterfaces } from '@proton/js' 3 | import { green } from 'colors' 4 | import { networks } from '../constants' 5 | import { config } from './config' 6 | import passwordManager from './passwordManager' 7 | import { ApiClass } from '@proton/api' 8 | 9 | class Network { 10 | rpc!: JsonRpc 11 | api!: Api 12 | protonApi!: ApiClass 13 | 14 | constructor () { 15 | this.initialize() 16 | } 17 | 18 | get network () { 19 | const chain = config.get('currentChain') 20 | // const masterNetwork = 21 | // const networks = config.get('networks') as any 22 | return networks.find((network: any) => network.chain === chain)! 23 | } 24 | 25 | initialize () { 26 | this.rpc = new JsonRpc(this.network.endpoints) 27 | this.api = new Api({ rpc: this.rpc }) 28 | this.protonApi = new ApiClass(config.get('currentChain')) 29 | } 30 | 31 | async getSignatureProvider () { 32 | const privateKeys = await passwordManager.getPrivateKeys() 33 | return new JsSignatureProvider(privateKeys) 34 | } 35 | 36 | async transact (transaction: any, args: { endpoint?: string } = {}): Promise { 37 | const api = new Api({ 38 | rpc: args.endpoint 39 | ? new JsonRpc([args.endpoint]) 40 | : this.rpc, 41 | signatureProvider: await this.getSignatureProvider() 42 | }) 43 | return api.transact(transaction, { 44 | useLastIrreversible: true, 45 | expireSeconds: 3000, 46 | }) 47 | } 48 | 49 | setChain (chain: string) { 50 | const foundChain = networks.find(network => network.chain === chain) 51 | if (!foundChain) { 52 | throw new Error(`No chain with name ${chain} found, use network:all to see available chains.`) 53 | } 54 | config.set('currentChain', chain) 55 | this.initialize() 56 | CliUx.ux.log(`${green('Success:')} Switched to chain ${chain}`) 57 | } 58 | 59 | setEndpoint (endpoint: string) { 60 | config.set('currentEndpoint', endpoint) 61 | this.initialize() 62 | CliUx.ux.log(`${green('Success:')} Switched to endpoint ${endpoint}`) 63 | } 64 | } 65 | 66 | export const network = new Network() 67 | -------------------------------------------------------------------------------- /src/storage/passwordManager.ts: -------------------------------------------------------------------------------- 1 | import { Key, Numeric } from '@proton/js' 2 | import { CliUx } from '@oclif/core' 3 | import { config } from './config' 4 | import { encryptor } from './encryptor' 5 | import { green } from 'colors' 6 | 7 | class PasswordManager { 8 | password: string = "" 9 | 10 | async lock (password?: string) { 11 | // Check if already locked 12 | if (config.get('isLocked')) { 13 | throw new Error('Wallet is already locked') 14 | } 15 | 16 | // Use passed or existing 17 | const passwordToLockWith = password || this.password 18 | 19 | // Encrypt and save existing keys 20 | const privateKeys = config.get('privateKeys').map(key => encryptor.encrypt(passwordToLockWith, key)) 21 | config.set('privateKeys', privateKeys) 22 | 23 | // Update config 24 | config.set('isLocked', true) 25 | } 26 | 27 | async unlock (password?: string) { 28 | // Check if already unlocked 29 | if (!config.get('isLocked')) { 30 | throw new Error('Wallet is already unlocked') 31 | } 32 | 33 | // Use passed or existing 34 | const passwordToUnlockWith = password || this.password 35 | 36 | // Decrypt and save existing keys 37 | const privateKeys = config.get('privateKeys').map(key => encryptor.decrypt(passwordToUnlockWith, key)) 38 | config.set('privateKeys', privateKeys) 39 | 40 | // Update local 41 | this.password = passwordToUnlockWith 42 | 43 | // Update config 44 | config.set('isLocked', false) 45 | } 46 | 47 | async getPassword() { 48 | while (!this.password) { 49 | const enteredPassword = await CliUx.ux.prompt('Please enter your 32 character password', { type: 'hide' }) 50 | this.password = enteredPassword; 51 | } 52 | return this.password 53 | } 54 | 55 | async getPrivateKey (publicKey: string): Promise { 56 | const privateKeys = await this.getPrivateKeys() 57 | const privateKey = privateKeys.find(_ => Key.PrivateKey.fromString(_).getPublicKey().toString() === Key.PublicKey.fromString(publicKey).toString()) 58 | return privateKey 59 | } 60 | 61 | async getPrivateKeys (): Promise { 62 | let privateKeys = config.get('privateKeys') 63 | if (!privateKeys.length) { 64 | return [] 65 | } 66 | 67 | // If locked 68 | if (config.get('isLocked')) { 69 | const password = await this.getPassword() 70 | privateKeys = privateKeys.map(privateKey => encryptor.decrypt(password, privateKey)) 71 | } 72 | 73 | return privateKeys 74 | } 75 | 76 | async getPublicKeys (): Promise { 77 | const privateKeys = await this.getPrivateKeys() 78 | return privateKeys.map((_: string) => Key.PrivateKey.fromString(_).getPublicKey().toString()) 79 | } 80 | 81 | async addPrivateKey (privateKeyStr?: string) { 82 | // Validate key 83 | let privateKey: Key.PrivateKey 84 | if (privateKeyStr) { 85 | privateKey = Key.PrivateKey.fromString(privateKeyStr) 86 | } else { 87 | privateKey = Key.generateKeyPair(Numeric.KeyType.k1, {secureEnv: true}).privateKey 88 | } 89 | 90 | // Encrypt if locked 91 | privateKeyStr = privateKey.toString() 92 | if (config.get('isLocked')) { 93 | const password = await this.getPassword() 94 | privateKeyStr = encryptor.encrypt(password, privateKeyStr) 95 | } 96 | 97 | // Validate password 98 | let privateKeys: string[] = await this.getPrivateKeys() 99 | if (privateKeys.find(privateKey => privateKey === privateKeyStr)) { 100 | throw new Error('\nPrivate key already exists') 101 | } 102 | 103 | // Concat 104 | privateKeys = privateKeys.concat(privateKey.toString()) 105 | 106 | // Encrypt if locked 107 | if (config.get('isLocked')) { 108 | const password = await this.getPassword() 109 | privateKeys = privateKeys.map(key => encryptor.encrypt(password, key)) 110 | } 111 | 112 | // Set new 113 | config.set('privateKeys', privateKeys) 114 | 115 | // Log out 116 | CliUx.ux.log(`${green('Success:')} Added new private key for public key: ${privateKey.getPublicKey().toString()}\n`) 117 | } 118 | 119 | async removePrivateKey (privateKey: string) { 120 | const privateKeys: string[] = await this.getPrivateKeys() 121 | if (!privateKeys.find(_privateKey => _privateKey === privateKey)) { 122 | throw new Error('\nPrivate key does not exist') 123 | } 124 | 125 | if (privateKeys && privateKeys.length > 0) { 126 | config.set('privateKeys', privateKeys.filter((key: string) => key !== privateKey)); 127 | } else { 128 | CliUx.ux.error(`You are not allowed to delete your last key`) 129 | } 130 | } 131 | } 132 | 133 | const passwordManager = new PasswordManager() 134 | 135 | export default passwordManager -------------------------------------------------------------------------------- /src/templates/contract/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /src/templates/contract/contract.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from 'proton-tsc' 2 | 3 | @contract 4 | export class ContractTemplate extends Contract { 5 | 6 | @action("action1") 7 | action1(): void { 8 | // Add here a code of your contract 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/templates/contract/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%= contractName %>", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "build": "npx proton-asc <%= contractName %>.contract.ts", 7 | "playground": "npm run build && cross-env LOG_LEVEL=debug ts-node ./playground.ts" 8 | }, 9 | "dependencies": { 10 | "proton-tsc": "latest" 11 | }, 12 | "engines": { 13 | "npm": ">=7.0.0", 14 | "node": ">=16.0.0" 15 | }, 16 | "engineStrict": true, 17 | "devDependencies": { 18 | "@greymass/abi2core": "^1.1.0", 19 | "@greymass/eosio": "^0.5.5", 20 | "@proton/cli": "^0.1.74", 21 | "@proton/js": "^26.1.2", 22 | "@proton/vert": "^0.3.18", 23 | "@types/chai": "^4.3.0", 24 | "@types/mocha": "^9.1.0", 25 | "@types/node": "^17.0.22", 26 | "chai": "^4.3.6", 27 | "cross-env": "^7.0.3", 28 | "cross-fetch": "^3.1.5", 29 | "globby": "^10", 30 | "mocha": "^9.2.2", 31 | "npm-run-all": "^4.1.5", 32 | "ts-node": "^10.7.0", 33 | "ts-morph": "^14.0.0", 34 | "typescript": "^4.6.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/templates/contract/playground.ts: -------------------------------------------------------------------------------- 1 | import { Blockchain } from "@proton/vert"; 2 | 3 | async function wait(ms: number) { 4 | return new Promise(resolve => { 5 | setTimeout(resolve, ms); 6 | }); 7 | } 8 | 9 | async function main() { 10 | const blockchain = new Blockchain(); 11 | const contract = blockchain.createContract('contractName', 'target/contractName.contract'); 12 | await wait(0); 13 | 14 | // Put you actions calls here 15 | } 16 | 17 | main() 18 | -------------------------------------------------------------------------------- /src/templates/contract/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "noImplicitAny": true, 7 | "strictNullChecks": true, 8 | "alwaysStrict": true, 9 | "noUnusedLocals": false, 10 | "noImplicitReturns": true, 11 | "baseUrl": "./", 12 | "paths": {}, 13 | "typeRoots": ["node_modules/assemblyscript/std"], 14 | "types": ["assembly", "node", "chai", "mocha"], 15 | "allowJs": true, 16 | "esModuleInterop": true, 17 | "experimentalDecorators": true, 18 | "emitDecoratorMetadata": true, 19 | "skipLibCheck": true 20 | }, 21 | "exclude": [ 22 | "./node_modules/*" 23 | ] 24 | } -------------------------------------------------------------------------------- /src/utils/detailsError.ts: -------------------------------------------------------------------------------- 1 | import { CliUx } from "@oclif/core" 2 | import { red } from "colors" 3 | 4 | export const parseDetailsError = (e: Error | any) => { 5 | const error = e && e.details && e.details.length && e.details[0] && e.details[0].message 6 | if (error || typeof e === 'object') { 7 | CliUx.ux.log('\n' + red(error || e.message)) 8 | } else { 9 | CliUx.ux.styledJSON(e) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/extractContract.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import globby from 'globby'; 3 | 4 | import { buildContractFileName, checkFileExists } from './fileManagement'; 5 | 6 | export async function extractContract(targetPath: string, contract?: string): Promise { 7 | let contractName = ''; 8 | let contractFilePath = ''; 9 | if (contract) { 10 | contractName = contract; 11 | const contractFileName = buildContractFileName(contractName); 12 | contractFilePath = path.join(targetPath, contractFileName); 13 | if (!checkFileExists(contractFilePath)) { 14 | throw `The contract file ${contractFileName} does not exits. May be you forgot to create the contract first?`; 15 | } 16 | } else { 17 | const paths = await globby([path.join(targetPath, '*.contract.ts').replace(/\\/g, '/')]) 18 | if (!paths.length) { 19 | throw `The contract file is not found. May be you forgot to create the contract first?`; 20 | } 21 | if (paths.length > 1) { 22 | throw `Several contracts are found. Please provide a contract name explicitly. Check --help information for more info`; 23 | } 24 | contractFilePath = paths[0]; 25 | const res = contractFilePath.match(/^.+\/(.+)?\.contract\.ts$/); 26 | if (res) { 27 | contractName = res[1]; 28 | } 29 | } 30 | return [contractName, contractFilePath]; 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/fileManagement.ts: -------------------------------------------------------------------------------- 1 | import { CliUx } from '@oclif/core'; 2 | import { yellow } from 'colors'; 3 | import * as fs from 'fs'; 4 | import * as path from 'path'; 5 | 6 | export function createRootFolder(folderPath: string) { 7 | if (!fs.existsSync(folderPath)) { 8 | CliUx.ux.log(yellow(`${folderPath} is not found. Creating.`)) 9 | fs.mkdirSync(folderPath, { recursive: true }); 10 | } 11 | } 12 | 13 | export interface IFilePreprocess { 14 | fileName: string; 15 | content: string; 16 | } 17 | 18 | export interface IFolderContentOptions { 19 | filePreprocess?: (data: IFilePreprocess) => Promise, 20 | } 21 | 22 | export async function createFolderContent(templatePath: string, targetPath: string, options: IFolderContentOptions) { 23 | // read all files/folders (1 level) from template folder 24 | const filesToCreate = fs.readdirSync(templatePath); 25 | // loop each file/folder 26 | 27 | for (const file of filesToCreate) { 28 | const origFilePath = path.join(templatePath, file); 29 | 30 | // get stats about the current file 31 | const stats = fs.statSync(origFilePath); 32 | 33 | if (stats.isFile()) { 34 | // read file content and transform it using template engine 35 | let content = fs.readFileSync(origFilePath, 'utf8'); 36 | let fileName = file; 37 | if (options.filePreprocess) { 38 | const result = await options.filePreprocess({ fileName, content }); 39 | fileName = result.fileName; 40 | content = result.content; 41 | } 42 | const writePath = path.join(targetPath, fileName); 43 | // write file to destination folder 44 | 45 | writeFile(writePath, content); 46 | } else if (stats.isDirectory()) { 47 | // create folder in destination folder 48 | fs.mkdirSync(path.join(targetPath, file)); 49 | // copy files/folder inside current folder recursively 50 | await createFolderContent(path.join(templatePath, file), targetPath, options); 51 | } 52 | } 53 | } 54 | 55 | export function buildContractFileName(contractName: string, type: string = 'contract'): string { 56 | return `${contractName}.${type}.ts`; 57 | } 58 | 59 | export function checkFileExists(targetPath: string): boolean { 60 | const contractPath = path.join(targetPath); 61 | return fs.existsSync(contractPath); 62 | } 63 | 64 | export function writeFile(targetPath: string, content: string): void { 65 | fs.writeFileSync(targetPath, content, 'utf8'); 66 | } 67 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './detailsError'; 2 | export * from './integer'; 3 | export * as template from './template'; 4 | export * from './validateName'; 5 | export * from './fileManagement'; 6 | export * from './permissions'; 7 | export * from './prompt'; 8 | export * from './resources'; 9 | export * from './wait'; 10 | export * from './extractContract'; 11 | 12 | -------------------------------------------------------------------------------- /src/utils/integer.ts: -------------------------------------------------------------------------------- 1 | export function isPositiveInteger(str: string) { 2 | var n = Math.floor(Number(str)); 3 | return n !== Infinity && String(n) === str && n >= 0; 4 | } -------------------------------------------------------------------------------- /src/utils/permissions.ts: -------------------------------------------------------------------------------- 1 | import { Key } from "@proton/js" 2 | import { green, underline } from "colors" 3 | 4 | export const parsePermissions = (permissions: any, lightAccount: any, sort: boolean = true) => { 5 | // Links 6 | const links = lightAccount 7 | ? lightAccount.linkauth.map((auth: any) => ({ 8 | action: auth.type, 9 | contract: auth.code, 10 | perm_name: auth.requirement 11 | })) 12 | : [] 13 | 14 | // Print 15 | let text = '' 16 | 17 | if (sort) { 18 | permissions = permissions.sort( 19 | (a: any, b: any) => a.perm_name === 'owner' ? -2 : a.perm_name === 'active' ? -1 : 0 20 | ) 21 | } 22 | 23 | let lastIndent = 0 24 | let lastParent = '' 25 | for (const permission of permissions) { 26 | const permissionLinks = links.filter((_: any) => _.perm_name === permission.perm_name) 27 | 28 | if (lastParent !== permission.parent) { 29 | lastIndent += 2 30 | lastParent = permission.parent 31 | } 32 | if (lastParent !== '') { 33 | text += '\n\n' 34 | } 35 | text += ' '.repeat(lastIndent) + `${green(permission.perm_name)} (=${permission.required_auth.threshold}): ` 36 | text += permission.required_auth.keys.map((key: any) => '\n' + ' '.repeat(lastIndent) + ` +${key.weight} ${Key.PublicKey.fromString(key.key).toString()}`).join('') 37 | text += permission.required_auth.accounts.map((account: any) => '\n' + ' '.repeat(lastIndent) + ` +${account.weight} ${account.permission.actor}@${account.permission.permission}`).join('') 38 | 39 | if (permissionLinks.length) { 40 | text += '\n\n' + ' '.repeat(lastIndent) + ` ` + underline(`Links:`) 41 | text += permissionLinks.map((_: any) => '\n' + ' '.repeat(lastIndent) + ` ${_.contract || '*'}@${_.action || '*'}`).join('') 42 | } 43 | } 44 | 45 | return text 46 | } 47 | -------------------------------------------------------------------------------- /src/utils/prompt.ts: -------------------------------------------------------------------------------- 1 | import { Key } from '@proton/js'; 2 | import { prompt } from 'inquirer' 3 | import AddPrivateKey from '../commands/key/add'; 4 | import GenerateKey from '../commands/key/generate'; 5 | import { isPositiveInteger } from './integer'; 6 | 7 | export const promptInteger = async (text: string) => { 8 | const { weight } = await prompt<{ weight: number }>({ 9 | name: 'weight', 10 | type: 'input', 11 | message: `Enter new ${text}:`, 12 | default: 1, 13 | filter: (w: string) => +w, 14 | validate: (w: any) => isPositiveInteger(String(w)) 15 | }); 16 | return weight 17 | } 18 | 19 | export const promptAuthority = async (text = 'account authority (e.g. account@active)') => { 20 | const { account } = await prompt<{ account: string }>({ 21 | name: 'account', 22 | type: 'input', 23 | message: `Enter new ${text}` 24 | }); 25 | const [actor, permission] = account.split('@') 26 | return { actor, permission } 27 | } 28 | 29 | export const promptName = async (text: string, 30 | opts: { 31 | default?: string, 32 | validate?: (input: string) => boolean | string | Promise, 33 | } = {} 34 | ) => { 35 | if (!opts.validate) { 36 | opts.validate = (input) => true; 37 | } 38 | const { name } = await prompt<{ name: string; }>({ 39 | name: 'name', 40 | type: 'input', 41 | message: `Enter new ${text} name:`, 42 | default: opts.default, 43 | validate: opts.validate 44 | }); 45 | return name; 46 | } 47 | 48 | export const promptKey = async () => { 49 | let { publicKey } = await prompt<{ publicKey: string | undefined }>({ 50 | name: 'publicKey', 51 | type: 'input', 52 | message: 'Enter new public key (e.g. PUB_K1..., leave empty to create new):', 53 | }); 54 | 55 | if (!publicKey) { 56 | const privateKey = await GenerateKey.run() 57 | await AddPrivateKey.run([privateKey]) 58 | publicKey = Key.PrivateKey.fromString(privateKey).getPublicKey().toString() 59 | } 60 | 61 | return Key.PublicKey.fromString(publicKey).toString() 62 | } 63 | 64 | export const promptChoices = async (message: string, choices: string[], def?: string) => { 65 | const { choice } = await prompt<{ choice: string }>([ 66 | { 67 | name: 'choice', 68 | type: 'list', 69 | message: message, 70 | choices: choices, 71 | loop: false, 72 | pageSize: 20, 73 | default: def 74 | }, 75 | ]); 76 | return choice 77 | } 78 | -------------------------------------------------------------------------------- /src/utils/resources.ts: -------------------------------------------------------------------------------- 1 | import { parseNetAndRam, parseCpu } from "@bloks/numbers" 2 | import { CliUx } from "@oclif/core" 3 | import { RpcInterfaces } from "@proton/js" 4 | 5 | export const generateResourceTable = (account: RpcInterfaces.GetAccountResult) => { 6 | const resourceTable = [ 7 | { 8 | type: 'RAM', 9 | delegated: '', 10 | used: parseNetAndRam(+account.ram_usage), 11 | available: parseNetAndRam(+account.ram_quota - +account.ram_usage), 12 | max: parseNetAndRam(+account.ram_quota) 13 | }, 14 | { 15 | type: 'CPU', 16 | delegated: account.total_resources?.cpu_weight, 17 | used: parseCpu(+account.cpu_limit.current_used), 18 | available: parseCpu(+account.cpu_limit.available), 19 | max: parseCpu(+account.cpu_limit.max) 20 | }, 21 | { 22 | type: 'NET', 23 | delegated: account.total_resources?.net_weight, 24 | used: parseNetAndRam(+account.net_limit.current_used), 25 | available: parseNetAndRam(+account.net_limit.available), 26 | max: parseNetAndRam(+account.net_limit.max) 27 | } 28 | ] 29 | 30 | let resourceTableText = "" 31 | CliUx.ux.table(resourceTable, { 32 | type: { 33 | header: 'Type' 34 | }, 35 | used: { 36 | header: 'Used' 37 | }, 38 | available: { 39 | header: 'Available', 40 | }, 41 | max: { 42 | header: 'Max', 43 | }, 44 | delegated: { 45 | header: 'Delegated', 46 | } 47 | }, { 48 | printLine: (line) => { resourceTableText += line + '\n' }, 49 | }) 50 | return resourceTableText 51 | } 52 | -------------------------------------------------------------------------------- /src/utils/sortRequiredAuth.ts: -------------------------------------------------------------------------------- 1 | import { Numeric, Serialize } from "@proton/js" 2 | import { RpcInterfaces } from "@proton/js" 3 | 4 | 5 | export const decodeWaPublicKey = (waPublicKey: string) => { 6 | if (!waPublicKey.startsWith('PUB_WA_')) { 7 | throw new Error('Not WA Public Key (starts with PUB_WA_)') 8 | } 9 | 10 | const pubKey = Numeric.stringToPublicKey(waPublicKey) 11 | const ser = new Serialize.SerialBuffer({ array: pubKey.data }) 12 | 13 | const data = ser.getUint8Array(33) 14 | const userPresence = ser.get() 15 | const rpid = ser.getString() 16 | 17 | return { 18 | data, 19 | userPresence, 20 | rpid 21 | } 22 | } 23 | 24 | export const sortRequiredAuth = (required_auth: RpcInterfaces.RequiredAuth) => { 25 | required_auth.accounts = required_auth.accounts.sort((a: { permission: { actor: any; }; }, b: { permission: { actor: any; }; }) => a.permission.actor.localeCompare(b.permission.actor)) 26 | required_auth.waits = required_auth.waits.sort((a: { wait_sec: any; }, b: { wait_sec: any; }) => a.wait_sec.localeCompare(b.wait_sec)) 27 | required_auth.keys = required_auth.keys.sort((a: { key: string }, b: { key: string }) => { 28 | if (a.key.includes('PUB_WA_') && b.key.includes('PUB_WA_')) { 29 | const keyADecoded = decodeWaPublicKey(a.key) 30 | const keyBDecoded = decodeWaPublicKey(b.key) 31 | 32 | for (let i = 0; i < 33; i++) { 33 | if (keyADecoded.data[i] < keyBDecoded.data[i]) { 34 | return -1 35 | } else if (keyADecoded.data[i] > keyBDecoded.data[i]) { 36 | return 1 37 | } 38 | } 39 | 40 | if (keyADecoded.userPresence < keyBDecoded.userPresence) { 41 | return -1 42 | } else if (keyADecoded.userPresence > keyBDecoded.userPresence) { 43 | return 1 44 | } 45 | 46 | return keyADecoded.rpid.localeCompare(keyBDecoded.rpid) 47 | } 48 | 49 | return a.key.localeCompare(b.key) 50 | }) 51 | } -------------------------------------------------------------------------------- /src/utils/template.ts: -------------------------------------------------------------------------------- 1 | import * as ejs from 'ejs'; 2 | 3 | export function render(content: string, data: T) { 4 | return ejs.render(content, data as any); 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/validateName.ts: -------------------------------------------------------------------------------- 1 | export function validateName(value: string): boolean { 2 | return /^[a-z1-5]{1,12}$/.test(value); 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/wait.ts: -------------------------------------------------------------------------------- 1 | export async function wait (ms: number) { 2 | return new Promise(resolve => { 3 | setTimeout(resolve, ms); 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /test/commands/boilerplate.test.ts: -------------------------------------------------------------------------------- 1 | import * as rimraf from 'rimraf' 2 | import * as path from 'path' 3 | import * as fs from 'fs' 4 | import {expect, test} from '@oclif/test' 5 | 6 | // const TEST_DIR_NAME = 'testdir' 7 | // const TEST_DIR = path.join(process.cwd(), TEST_DIR_NAME) 8 | 9 | const DEFAULT_DIR_NAME = 'proton-boilerplate' 10 | const DEFAULT_DIR = path.join(process.cwd(), DEFAULT_DIR_NAME) 11 | 12 | const folders = ['atom', 'c++_tests', 'frontend', 'js_tests'] 13 | 14 | const folderExists = (baseDir: string) => (folder: string) => fs.existsSync(path.join(baseDir, folder)) 15 | 16 | describe('boilerplate', () => { 17 | test 18 | .command(['boilerplate']) 19 | .finally(() => rimraf.sync(DEFAULT_DIR)) 20 | .it('All folders exist', (_: any) => { 21 | const allExist = folders.every(folderExists(DEFAULT_DIR)) 22 | expect(allExist).to.equal(true) 23 | }) 24 | 25 | // test 26 | // .command(['boilerplate', TEST_DIR_NAME]) 27 | // .finally(() => rimraf.sync(TEST_DIR)) 28 | // .it('All folders exist', (_: any) => { 29 | // const allExist = folders.every(folderExists(TEST_DIR)) 30 | // expect(allExist).to.equal(true) 31 | // }) 32 | }) 33 | -------------------------------------------------------------------------------- /test/commands/network.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | import { CliUx } from '@oclif/core' 3 | import {getRpc, getApi} from '../../src/networks' 4 | 5 | describe('network', () => { 6 | test 7 | .stub(CliUx.ux, 'prompt', () => async () => 'proton-test') 8 | .stdout() 9 | .command(['network']) 10 | .it('RPC and API were set', async ctx => { 11 | expect(await getRpc()).to.be.ok 12 | expect((await getApi()).api).to.be.ok 13 | expect(JSON.parse(ctx.stdout)).to.be.deep.equal({ 14 | chain: 'proton-test', 15 | endpoints: [ 16 | 'https://protontestnet.greymass.com', 17 | ], 18 | }) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/commands/version.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | const packageJson = require('../../package.json') 4 | 5 | describe('version', () => { 6 | test 7 | .stdout() 8 | .command(['version']) 9 | .it('matches current version', ctx => { 10 | expect(ctx.stdout.replace('\n', '')).to.equal(packageJson.version) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ts-node/register 2 | --watch-extensions ts 3 | --recursive 4 | --reporter spec 5 | --timeout 5000 6 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "noEmit": true 5 | }, 6 | "references": [ 7 | {"path": ".."} 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "importHelpers": true, 5 | "module": "commonjs", 6 | "outDir": "lib", 7 | "rootDir": "src", 8 | "strict": true, 9 | "target": "es2020", 10 | "allowJs": true, 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "useUnknownInCatchVariables": true, 14 | "preserveSymlinks": true 15 | }, 16 | "include": [ 17 | "src/**/*" 18 | ], 19 | "exclude": [ 20 | "src/templates" 21 | ] 22 | } 23 | --------------------------------------------------------------------------------