diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 000000000..8e8d319c9 --- /dev/null +++ b/packages/cli/README.md @@ -0,0 +1,11 @@ +# `@stacks/cli` + +> TODO: description + +## Usage + +``` +const cli = require('@stacks/cli'); + +// TODO: DEMONSTRATE API +``` diff --git a/packages/cli/__tests__/cli.test.js b/packages/cli/__tests__/cli.test.js new file mode 100644 index 000000000..64d2b87a8 --- /dev/null +++ b/packages/cli/__tests__/cli.test.js @@ -0,0 +1,7 @@ +'use strict'; + +const cli = require('..'); + +describe('@stacks/cli', () => { + it('needs tests'); +}); diff --git a/packages/cli/package-lock.json b/packages/cli/package-lock.json new file mode 100644 index 000000000..38913d9b3 --- /dev/null +++ b/packages/cli/package-lock.json @@ -0,0 +1,2342 @@ +{ + "name": "@stacks/cli", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/bn.js": { + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "11.15.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.15.20.tgz", + "integrity": "sha512-DY2QwdrBqNlsxdMehwzUtSsWHgYYPLVCAuXvOcu3wkzYmchbRunQ7OEZFOrmFoBLfA1ysz2Ypr6vtNP9WQkUaQ==", + "dev": true + }, + "@typescript-eslint/eslint-plugin": { + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz", + "integrity": "sha512-4zY3Z88rEE99+CNvTbXSyovv2z9PNOVffTWD2W8QF5s2prBQtwN2zadqERcrHpcR7O/+KMI3fcTAmUUhK/iQcQ==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "2.34.0", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.0.0", + "tsutils": "^3.17.1" + }, + "dependencies": { + "@types/json-schema": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz", + "integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==", + "dev": true + }, + "@typescript-eslint/experimental-utils": { + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz", + "integrity": "sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/typescript-estree": "2.34.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + } + }, + "@typescript-eslint/typescript-estree": { + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz", + "integrity": "sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "eslint-visitor-keys": "^1.1.0", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "eslint-scope": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", + "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "lodash": { + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "regexpp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "dev": true + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + }, + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", + "dev": true + }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } + }, + "@typescript-eslint/parser": { + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.34.0.tgz", + "integrity": "sha512-03ilO0ucSD0EPTw2X4PntSIRFtDPWjrVq7C3/Z3VQHRC7+13YB55rcJI3Jt+YgeHbjUdJPcPa7b23rXCBokuyA==", + "dev": true, + "requires": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "2.34.0", + "@typescript-eslint/typescript-estree": "2.34.0", + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz", + "integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==", + "dev": true + }, + "@typescript-eslint/experimental-utils": { + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz", + "integrity": "sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/typescript-estree": "2.34.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + } + }, + "@typescript-eslint/typescript-estree": { + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz", + "integrity": "sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "eslint-visitor-keys": "^1.1.0", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "eslint-scope": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", + "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "lodash": { + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + }, + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", + "dev": true + }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } + }, + "bitcoinjs-lib": { + "version": "5.1.10", + "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-5.1.10.tgz", + "integrity": "sha512-CesUqtBtnYc+SOMsYN9jWQWhdohW1MpklUkF7Ukn4HiAyN6yxykG+cIJogfRt6x5xcgH87K1Q+Mnoe/B+du1Iw==", + "requires": { + "bech32": "^1.1.2", + "bip174": "^1.0.1", + "bip32": "^2.0.4", + "bip66": "^1.1.0", + "bitcoin-ops": "^1.4.0", + "bs58check": "^2.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.3", + "merkle-lib": "^2.0.10", + "pushdata-bitcoin": "^1.0.1", + "randombytes": "^2.0.1", + "tiny-secp256k1": "^1.1.1", + "typeforce": "^1.11.3", + "varuint-bitcoin": "^1.0.4", + "wif": "^2.0.1" + }, + "dependencies": { + "@types/node": { + "version": "10.12.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.18.tgz", + "integrity": "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==" + }, + "base-x": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.8.tgz", + "integrity": "sha512-Rl/1AWP4J/zRrk54hhlxH4drNxPJXYUaKffODVI53/dAsV4t9fBxyxYKAVPU1XBHxYwOWP9h9H0hM2MVw4YfJA==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bip174": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bip174/-/bip174-1.0.1.tgz", + "integrity": "sha512-Mq2aFs1TdMfxBpYPg7uzjhsiXbAtoVq44TNjEWtvuZBiBgc3m7+n55orYMtTAxdg7jWbL4DtH0MKocJER4xERQ==" + }, + "bip32": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/bip32/-/bip32-2.0.5.tgz", + "integrity": "sha512-zVY4VvJV+b2fS0/dcap/5XLlpqtgwyN8oRkuGgAS1uLOeEp0Yo6Tw2yUTozTtlrMJO3G8n4g/KX/XGFHW6Pq3g==", + "requires": { + "@types/node": "10.12.18", + "bs58check": "^2.1.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "tiny-secp256k1": "^1.1.3", + "typeforce": "^1.11.5", + "wif": "^2.0.6" + } + }, + "bip66": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", + "integrity": "sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "bitcoin-ops": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz", + "integrity": "sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow==" + }, + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" + }, + "bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=", + "requires": { + "base-x": "^3.0.2" + } + }, + "bs58check": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", + "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", + "requires": { + "bs58": "^4.0.0", + "create-hash": "^1.1.0", + "safe-buffer": "^5.1.2" + } + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "elliptic": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "requires": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "merkle-lib": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/merkle-lib/-/merkle-lib-2.0.10.tgz", + "integrity": "sha1-grjbrnXieneFOItz+ddyXQ9vMyY=" + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" + }, + "nan": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", + "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==" + }, + "pushdata-bitcoin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pushdata-bitcoin/-/pushdata-bitcoin-1.0.1.tgz", + "integrity": "sha1-FZMdPNlnreUiBvUjqnMxrvfUOvc=", + "requires": { + "bitcoin-ops": "^1.3.0" + } + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "tiny-secp256k1": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-1.1.5.tgz", + "integrity": "sha512-duE2hSLSQIpHGzmK48OgRrGTi+4OTkXLC6aa86uOYQ6LLCYZSarVKIAvEtY7MoXjoL6bOXMSerEGMzrvW4SkDw==", + "requires": { + "bindings": "^1.3.0", + "bn.js": "^4.11.8", + "create-hmac": "^1.1.7", + "elliptic": "^6.4.0", + "nan": "^2.13.2" + } + }, + "typeforce": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", + "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "varuint-bitcoin": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz", + "integrity": "sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==", + "requires": { + "safe-buffer": "^5.1.1" + } + }, + "wif": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz", + "integrity": "sha1-CNP1IFbGZnkplyb63g1DKudLRwQ=", + "requires": { + "bs58check": "<3.0.0" + } + } + } + }, + "eslint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "acorn": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", + "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", + "dev": true + }, + "acorn-jsx": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", + "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", + "dev": true + }, + "ajv": { + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz", + "integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "requires": { + "type-fest": "^0.11.0" + }, + "dependencies": { + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + } + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint-scope": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", + "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + }, + "espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "inquirer": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lodash": { + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", + "dev": true + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, + "rxjs": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", + "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + } + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + } + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", + "dev": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "v8-compile-cache": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", + "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + } + } + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + } + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "requires": { + "mime-db": "1.44.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + } + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + } + } + }, + "ts-node": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz", + "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==", + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + }, + "dependencies": { + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" + } + } + }, + "typescript": { + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", + "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==" + } + } +} diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 000000000..2765f0b59 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,98 @@ +{ + "name": "@stacks/cli", + "version": "1.0.0", + "description": "Stacks command line tool", + "keywords": [ + "stacks", + "command", + "blockchain", + "id", + "auth", + "authentication", + "bitcoin", + "blockchain auth", + "blockchain authentication", + "blockchainid", + "blockchain id", + "bitcoin auth", + "bitcoin authentication", + "bitcoin login", + "blockchain login", + "authorization", + "login", + "signin", + "sso", + "crypto", + "cryptography", + "token", + "blockstack", + "blockstack auth", + "profile", + "identity", + "ethereum" + ], + "author": { + "name": "Jude Nelson", + "email": "jude@blockstack.com", + "url": "https://blockstack.com" + }, + "homepage": "https://blockstack.org", + "contributors": [ + { + "name": "Ken Liao", + "email": "yukanliao@gmail.com" + } + ], + "license": "GPL-3.0-or-later", + "main": "lib/index.js", + "directories": { + "lib": "lib", + "test": "__tests__" + }, + "files": [ + "lib" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/blockstack/blockstack.js.git" + }, + "scripts": { + "build": "rimraf lib && tsc -b tsconfig.build.json", + "dev": "ts-node src/index.ts", + "test": "echo \"Error: run tests from root\" && exit 1" + }, + "bugs": { + "url": "https://github.com/blockstack/blockstack.js/issues" + }, + "devDependencies": { + "@types/inquirer": "^6.5.0", + "@types/cors": "^2.8.5", + "@types/express": "^4.16.1", + "@types/express-winston": "^3.0.1", + "@types/node-fetch": "^2.5.0", + "@types/ripemd160": "^2.0.0", + "minimist": ">=1.2.3" + }, + "dependencies": { + "@blockstack/stacks-transactions": "^0.6.0", + "ajv": "^4.11.5", + "bip32": "^2.0.4", + "bip39": "^3.0.2", + "bitcoinjs-lib": "^5.1.10", + "blockstack": "^19.2.2", + "c32check": "^0.0.6", + "cors": "^2.8.4", + "express": "^4.17.1", + "express-winston": "^3.1.0", + "inquirer": "^7.1.0", + "jsontokens": "^2.0.2", + "node-fetch": "^2.6.0", + "ripemd160": "^2.0.1", + "ts-node": "^8.10.2", + "winston": "^3.2.1", + "zone-file": "^0.2.2" + } +} diff --git a/packages/cli/src/argparse.ts b/packages/cli/src/argparse.ts new file mode 100644 index 000000000..5ada36ac7 --- /dev/null +++ b/packages/cli/src/argparse.ts @@ -0,0 +1,3187 @@ +import * as Ajv from 'ajv'; +import * as process from 'process'; +import * as fs from 'fs'; + +export const NAME_PATTERN = + '^([0-9a-z_.+-]{3,37})$'; + +export const NAMESPACE_PATTERN = + '^([0-9a-z_-]{1,19})$'; + +export const ADDRESS_CHARS = + '[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{1,35}'; + +export const C32_ADDRESS_CHARS = '[0123456789ABCDEFGHJKMNPQRSTVWXYZ]+'; + +export const ADDRESS_PATTERN = `^(${ADDRESS_CHARS})$`; + +export const ID_ADDRESS_PATTERN = `^ID-${ADDRESS_CHARS}$`; + +export const STACKS_ADDRESS_PATTERN = `^(${C32_ADDRESS_CHARS})$`; + +// hex private key +export const PRIVATE_KEY_PATTERN = + '^([0-9a-f]{64,66})$'; + +// hex private key, no compression +export const PRIVATE_KEY_UNCOMPRESSED_PATTERN = + '^([0-9a-f]{64})$'; + +// nosign:addr +export const PRIVATE_KEY_NOSIGN_PATTERN = + `^nosign:${ADDRESS_CHARS}$`; + +// m,pk1,pk2,...,pkn +export const PRIVATE_KEY_MULTISIG_PATTERN = + '^([0-9]+),([0-9a-f]{64,66},)*([0-9a-f]{64,66})$'; + +// segwit:p2sh:m,pk1,pk2,...,pkn +export const PRIVATE_KEY_SEGWIT_P2SH_PATTERN = + '^segwit:p2sh:([0-9]+),([0-9a-f]{64,66},)*([0-9a-f]{64,66})$'; + +// any private key pattern we support +export const PRIVATE_KEY_PATTERN_ANY = + `${PRIVATE_KEY_PATTERN}|${PRIVATE_KEY_MULTISIG_PATTERN}|${PRIVATE_KEY_SEGWIT_P2SH_PATTERN}|${PRIVATE_KEY_NOSIGN_PATTERN}`; + +export const PUBLIC_KEY_PATTERN = + '^([0-9a-f]{66,130})$'; + +export const INT_PATTERN = '^-?[0-9]+$'; + +export const ZONEFILE_HASH_PATTERN = '^([0-9a-f]{40})$'; + +export const URL_PATTERN = '^http[s]?://.+$'; + +export const SUBDOMAIN_PATTERN = + '^([0-9a-z_+-]{1,37})\.([0-9a-z_.+-]{3,37})$'; + +export const TXID_PATTERN = + '^([0-9a-f]{64})$'; + +export const BOOLEAN_PATTERN = '^(0|1|true|false)$'; + +export interface CLI_LOG_CONFIG_TYPE { + level: string, + handleExceptions: boolean, + timestamp: boolean, + stringify: boolean, + colorize: boolean, + json: boolean +}; + +export interface CLI_CONFIG_TYPE { + blockstackAPIUrl: string, + blockstackNodeUrl: string, + broadcastServiceUrl: string, + utxoServiceUrl: string, + logConfig: CLI_LOG_CONFIG_TYPE, + bitcoindUsername?: string, + bitcoindPassword?: string, +}; + +const LOG_CONFIG_DEFAULTS : CLI_LOG_CONFIG_TYPE = { + level: 'info', + handleExceptions: true, + timestamp: true, + stringify: true, + colorize: true, + json: true +}; + +const CONFIG_DEFAULTS : CLI_CONFIG_TYPE = { + blockstackAPIUrl: 'http://core.blockstack.org:20443', + blockstackNodeUrl: 'http://core.blockstack.org:20443', + broadcastServiceUrl: 'http://core.blockstack.org:20443/v2/transactions', + utxoServiceUrl: 'https://blockchain.info', + logConfig: LOG_CONFIG_DEFAULTS +}; + +const CONFIG_REGTEST_DEFAULTS : CLI_CONFIG_TYPE = { + blockstackAPIUrl: 'http://localhost:16268', + blockstackNodeUrl: 'http://localhost:16264', + broadcastServiceUrl: 'http://localhost:16269', + utxoServiceUrl: 'http://localhost:18332', + logConfig: LOG_CONFIG_DEFAULTS, + bitcoindPassword: 'blockstacksystem', + bitcoindUsername: 'blockstack' +}; + +const PUBLIC_TESTNET_HOST = 'testnet-master.blockstack.org'; + +const CONFIG_TESTNET_DEFAULTS = { + blockstackAPIUrl: `http://${PUBLIC_TESTNET_HOST}:20443`, + blockstackNodeUrl: `http://${PUBLIC_TESTNET_HOST}:20443`, + broadcastServiceUrl: `http://${PUBLIC_TESTNET_HOST}:20443/v2/transactions`, + utxoServiceUrl: `http://${PUBLIC_TESTNET_HOST}:18332`, + logConfig: Object.assign({}, LOG_CONFIG_DEFAULTS, { level: 'debug' }) +}; + +export const DEFAULT_CONFIG_PATH = '~/.blockstack-cli.conf'; +export const DEFAULT_CONFIG_REGTEST_PATH = '~/.blockstack-cli-regtest.conf'; +export const DEFAULT_CONFIG_TESTNET_PATH = '~/.blockstack-cli-testnet.conf'; + +export const DEFAULT_MAX_ID_SEARCH_INDEX = 256; + +interface CLI_PROP_ITEM { + name: string; + type: 'string'; + realtype: string; + pattern?: string; +} + + +interface CLI_PROP { + [index: string]: { + type: 'array', + items: CLI_PROP_ITEM[] + minItems: number, + maxItems: number, + help: string, + group: string + } +} + +// CLI usage +export const CLI_ARGS = { + type: 'object', + properties: { + announce: { + type: 'array', + items: [ + { + name: 'message_hash', + type: 'string', + realtype: 'zonefile_hash', + pattern: ZONEFILE_HASH_PATTERN + }, + { + name: 'owner_key', + type: 'string', + realtype: 'private_key', + pattern: `${PRIVATE_KEY_PATTERN_ANY}` + } + ], + minItems: 2, + maxItems: 2, + help: 'Broadcast a message on the blockchain for subscribers to read. ' + + 'The `MESSAGE_HASH` argument must be the hash of a previously-announced zone file. ' + + 'The `OWNER_KEY` used to sign the transaction must correspond to the Blockstack ID ' + + 'to which other users have already subscribed. `OWNER_KEY` can be a single private key ' + + 'or a serialized multisig private key bundle.\n' + + '\n' + + 'If this command succeeds, it will print a transaction ID. The rest of the Blockstack peer ' + + 'network will process it once the transaction reaches 7 confirmations.\n' + + '\n' + + 'Examples:\n' + + '\n' + + ' $ # Tip: You can obtain the owner key with the get_owner_keys command\n' + + ' $ export OWNER_KEY="136ff26efa5db6f06b28f9c8c7a0216a1a52598045162abfe435d13036154a1b01"\n' + + ' $ blockstack-cli announce 737c631c7c5d911c6617993c21fba731363f1cfe "$OWNER_KEY"\n' + + ' d51749aeec2803e91a2f8bdec8d3e413491fd816b4962372b214ab74acb0bba8\n' + + '\n' + + ' $ export OWNER_KEY="2,136ff26efa5db6f06b28f9c8c7a0216a1a52598045162abfe435d13036154a1b01,1885cba486a42960499d1f137ef3a475725ceb11f45d74631f9928280196f67401,2418981c7f3a91d4467a65a518e14fafa30e07e6879c11fab7106ea72b49a7cb01"\n' + + ' $ blockstack-cli announce 737c631c7c5d911c6617993c21fba731363f1cfe "$OWNER_KEY"\n' + + ' 8136a1114098893b28a693e8d84451abf99ee37ef8766f4bc59808eed76968c9\n' + + '\n', + group: 'Peer Services' + }, + authenticator: { + type: 'array', + items: [ + { + name: 'app_gaia_hub', + type: 'string', + realtype: 'url', + pattern: URL_PATTERN + }, + { + name: 'backup_phrase', + type: 'string', + realtype: '12_words_or_ciphertext', + pattern: '.+' + }, + { + name: 'profile_gaia_hub', + type: 'string', + realtype: 'url', + pattern: URL_PATTERN + }, + { + name: 'port', + type: 'string', + realtype: 'portnum', + pattern: '^[0-9]+' + } + ], + minItems: 2, + maxItems: 4, + help: 'Run an authentication endpoint for the set of names owned ' + + 'by the given backup phrase. Send applications the given Gaia hub URL on sign-in, ' + + 'so the application will use it to read/write user data.\n' + + '\n' + + 'You can supply your encrypted backup phrase instead of the raw backup phrase. If so, ' + + 'then you will be prompted for your password before any authentication takes place.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ export BACKUP_PHRASE="oak indicate inside poet please share dinner monitor glow hire source perfect"\n' + + ' $ export APP_GAIA_HUB="https://1.2.3.4"\n' + + ' $ export PROFILE_GAIA_HUB="https://hub.blockstack.org"\n' + + ' $ blockstack-cli authenticator "$APP_GAIA_HUB" "$BACKUP_PHRASE" "$PROFILE_GAIA_HUB" 8888\n' + + ' Press Ctrl+C to exit\n' + + ' Authentication server started on 8888\n', + group: 'Authentication' + }, + balance: { + type: 'array', + items: [ + { + name: 'address', + type: 'string', + realtype: 'address', + pattern: `${ADDRESS_PATTERN}|${STACKS_ADDRESS_PATTERN}` + } + ], + minItems: 1, + maxItems: 1, + help: 'Query the balance of an account. Returns the balances of each kind of token ' + + 'that the account owns. The balances will be in the *smallest possible units* of the ' + + 'token (i.e. satoshis for BTC, microStacks for Stacks, etc.).\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ blockstack-cli balance 16pm276FpJYpm7Dv3GEaRqTVvGPTdceoY4\n' + + ' {\n' + + ' "BTC": "123456"\n' + + ' "STACKS": "123456"\n' + + ' }\n' + + ' $ blockstack-cli balance SPZY1V53Z4TVRHHW9Z7SFG8CZNRAG7BD8WJ6SXD0\n' + + ' {\n' + + ' "BTC": "123456"\n' + + ' "STACKS": "123456"\n' + + ' }\n', + group: 'Account Management' + }, + call_contract_func: { + type: 'array', + items: [ + { + name: 'contract_address', + type: 'string', + realtype: 'address', + pattern: `${STACKS_ADDRESS_PATTERN}` + }, + { + name: 'contract_name', + type: 'string', + realtype: 'string', + pattern: '^[a-zA-Z]([a-zA-Z0-9]|[-_])*$', + }, + { + name: 'function_name', + type: 'string', + realtype: 'string', + pattern: '^[a-zA-Z]([a-zA-Z0-9]|[-_!?])*$', + }, + { + name: 'fee', + type: 'string', + realtype: 'integer', + pattern: '^[0-9]+$' + }, + { + name: 'nonce', + type: 'string', + realtype: 'integer', + pattern: '^[0-9]+$' + }, + { + name: 'payment_key', + type: 'string', + realtype: 'private_key', + pattern: `${PRIVATE_KEY_PATTERN_ANY}` + } + ], + minItems: 6, + maxItems: 6, + help: 'Call a function in a deployed Clarity smart contract.\n' + + '\n' + + 'If the command succeeds, it prints out a transaction ID.' + + '\n' + + 'Example:\n' + + ' $ export PAYMENT="bfeffdf57f29b0cc1fab9ea197bb1413da2561fe4b83e962c7f02fbbe2b1cd5401"\n' + + ' $ blockstack-cli call_contract_func SPBMRFRPPGCDE3F384WCJPK8PQJGZ8K9QKK7F59X contract_name' + + ' contract_function 1 0 "$PAYMENT"\n' + + ' a9d387a925fb0ba7a725fb1e11f2c3f1647473699dd5a147c312e6453d233456\n' + + '\n', + group: 'Account Management' + }, + call_read_only_contract_func: { + type: 'array', + items: [ + { + name: 'contract_address', + type: 'string', + realtype: 'address', + pattern: `${STACKS_ADDRESS_PATTERN}` + }, + { + name: 'contract_name', + type: 'string', + realtype: 'string', + pattern: '^[a-zA-Z]([a-zA-Z0-9]|[-_])*$', + }, + { + name: 'function_name', + type: 'string', + realtype: 'string', + pattern: '^[a-zA-Z]([a-zA-Z0-9]|[-_!?])*$', + }, + { + name: 'sender_address', + type: 'string', + realtype: 'address', + pattern: `${STACKS_ADDRESS_PATTERN}` + } + ], + minItems: 4, + maxItems: 4, + help: 'Call a read-only function in a deployed Clarity smart contract.\n' + + '\n' + + 'If the command succeeds, it prints out a Clarity value.' + + '\n' + + 'Example:\n' + + ' $ blockstack-cli call_read_only_contract_func SPBMRFRPPGCDE3F384WCJPK8PQJGZ8K9QKK7F59X contract_name' + + ' contract_function SPBMRFRPPGCDE3F384WCJPK8PQJGZ8K9QKK7F59X\n' + + '\n', + group: 'Account Management' + }, + convert_address: { + type: 'array', + items: [ + { + name: 'address', + type: 'string', + realtype: 'address', + pattern: `${ADDRESS_PATTERN}|${STACKS_ADDRESS_PATTERN}` + } + ], + minItems: 1, + maxItems: 1, + help: 'Convert a Bitcoin address to a Stacks address and vice versa.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ blockstack-cli convert_address 12qdRgXxgNBNPnDeEChy3fYTbSHQ8nfZfD\n' + + ' {\n' + + ' "STACKS": "SPA2MZWV9N67TBYVWTE0PSSKMJ2F6YXW7CBE6YPW",\n' + + ' "BTC": "12qdRgXxgNBNPnDeEChy3fYTbSHQ8nfZfD"\n' + + ' }\n' + + ' $ blockstack-cli convert_address SPA2MZWV9N67TBYVWTE0PSSKMJ2F6YXW7CBE6YPW\n' + + ' {\n' + + ' "STACKS": "SPA2MZWV9N67TBYVWTE0PSSKMJ2F6YXW7CBE6YPW",\n' + + ' "BTC": "12qdRgXxgNBNPnDeEChy3fYTbSHQ8nfZfD"\n' + + ' }\n', + group: 'Account Management' + }, + decrypt_keychain: { + type: 'array', + items: [ + { + name: 'encrypted_backup_phrase', + type: 'string', + realtype: 'encrypted_backup_phrase', + pattern: '^[^ ]+$' + }, + { + name: 'password', + type: 'string', + realtype: 'password', + pattern: '.+' + } + ], + minItems: 1, + maxItems: 2, + help: 'Decrypt an encrypted backup phrase with a password. Decrypts to a 12-word ' + + 'backup phrase if done correctly. The password will be prompted if not given.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ # password is "asdf"\n' + + ' $ blockstack-cli decrypt_keychain "bfMDtOucUGcJXjZo6vkrZWgEzue9fzPsZ7A6Pl4LQuxLI1xsVF0VPgBkMsnSLCmYS5YHh7R3mNtMmX45Bq9sNGPfPsseQMR0fD9XaHi+tBg=\n' + + ' Enter password:\n' + + ' section amount spend resemble spray verify night immune tattoo best emotion parrot', + group: 'Key Management' + }, + deploy_contract: { + type: 'array', + items: [ + { + name: 'source_file', + type: 'string', + realtype: 'path', + pattern: '.+' + }, + { + name: 'contract_name', + type: 'string', + realtype: 'string', + pattern: '^[a-zA-Z]([a-zA-Z0-9]|[-_])*$', + }, + { + name: 'fee', + type: 'string', + realtype: 'integer', + pattern: '^[0-9]+$' + }, + { + name: 'nonce', + type: 'string', + realtype: 'integer', + pattern: '^[0-9]+$' + }, + { + name: 'payment_key', + type: 'string', + realtype: 'private_key', + pattern: `${PRIVATE_KEY_PATTERN_ANY}` + } + ], + minItems: 5, + maxItems: 5, + help: 'Deploys a Clarity smart contract on the network.\n' + + '\n' + + 'If the command succeeds, it prints out a transaction ID.' + + '\n' + + 'Example:\n' + + ' $ export PAYMENT="bfeffdf57f29b0cc1fab9ea197bb1413da2561fe4b83e962c7f02fbbe2b1cd5401"\n' + + ' $ blockstack-cli deploy_contract ./my_contract.clar my_contract 1 0 "$PAYMENT"\n' + + ' a9d387a925fb0ba7a725fb1e11f2c3f1647473699dd5a147c312e6453d233456\n' + + '\n', + group: 'Account Management' + }, + docs: { + type: 'array', + items: [ + { + name: 'format', + type: 'string', + realtype: 'output_format', + pattern: '^json$' + } + ], + minItems: 0, + maxItems: 1, + help: 'Dump the documentation for all commands as JSON to standard out.', + group: 'CLI' + }, + encrypt_keychain: { + type: 'array', + items: [ + { + name: 'backup_phrase', + type: 'string', + realtype: 'backup_phrase', + pattern: '.+' + }, + { + name: 'password', + type: 'string', + realtype: 'password', + pattern: '.+' + } + ], + minItems: 1, + maxItems: 2, + help: 'Encrypt a 12-word backup phrase, which can be decrypted later with the ' + + '`decrypt_backup_phrase` command. The password will be prompted if not given.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ # password is "asdf"\n' + + ' $ blockstack-cli encrypt_keychain "section amount spend resemble spray verify night immune tattoo best emotion parrot"\n' + + ' Enter password:\n' + + ' Enter password again:\n' + + ' M+DnBHYb1fgw4N3oZ+5uTEAua5bAWkgTW/SjmmBhGGbJtjOtqVV+RrLJEJOgT35hBon4WKdGWye2vTdgqDo7+HIobwJwkQtN2YF9g3zPsKk=', + group: 'Key Management' + }, + gaia_dump_bucket: { + type: 'array', + items: [ + { + name: 'name_or_id_address', + type: 'string', + realtype: 'name_or_id_address', + pattern: `${ID_ADDRESS_PATTERN}|${NAME_PATTERN}|${SUBDOMAIN_PATTERN}` + }, + { + name: 'app_origin', + type: 'string', + realtype: 'url', + pattern: URL_PATTERN + }, + { + name: 'gaia_hub', + type: 'string', + realtype: 'url', + pattern: URL_PATTERN + }, + { + name: 'backup_phrase', + type: 'string', + realtype: '12_words_or_ciphertext' + }, + { + name: 'dump_dir', + type: 'string', + realtype: 'path', + pattern: '.+' + } + ], + minItems: 5, + maxItems: 5, + help: 'Download the contents of a Gaia hub bucket to a given directory. The `GAIA_HUB` argument ' + + 'must correspond to the *write* endpoint of the Gaia hub -- that is, you should be able to fetch ' + + '`$GAIA_HUB/hub_info`. If `DUMP_DIR` does not exist, it will be created.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ export BACKUP_PHRASE="section amount spend resemble spray verify night immune tattoo best emotion parrot\n' + + ' $ blockstack-cli gaia_dump_bucket hello.id.blockstack https://sample.app https://hub.blockstack.org "$BACKUP_PHRASE" ./backups\n' + + ' Download 3 files...\n' + + ' Download hello_world to ./backups/hello_world\n' + + ' Download dir/format to ./backups/dir\\x2fformat\n' + + ' Download /.dotfile to ./backups/\\x2f.dotfile\n' + + ' 3\n', + group: 'Gaia' + }, + gaia_getfile: { + type: 'array', + items: [ + { + name: 'blockstack_id', + type: 'string', + realtype: 'blockstack_id', + pattern: `${NAME_PATTERN}|${SUBDOMAIN_PATTERN}$` + }, + { + name: 'app_origin', + type: 'string', + realtype: 'url', + pattern: URL_PATTERN + }, + { + name: 'filename', + type: 'string', + realtype: 'filename', + pattern: '.+' + }, + { + name: 'app_private_key', + type: 'string', + realtype: 'private_key', + pattern: PRIVATE_KEY_UNCOMPRESSED_PATTERN + }, + { + name: 'decrypt', + type: 'string', + realtype: 'boolean', + pattern: BOOLEAN_PATTERN + }, + { + name: 'verify', + type: 'string', + realtype: 'boolean', + pattern: BOOLEAN_PATTERN + } + ], + minItems: 3, + maxItems: 6, + help: 'Get a file from another user\'s Gaia hub. Prints the file data to stdout. If you ' + + 'want to read an encrypted file, and/or verify a signed file, then you must pass an app ' + + 'private key, and pass 1 for `DECRYPT` and/or `VERIFY`. If the file is encrypted, and you do not ' + + 'pass an app private key, then this command downloads the ciphertext. If the file is signed, ' + + 'and you want to download its data and its signature, then you must run this command twice -- ' + + 'once to get the file contents at `FILENAME`, and once to get the signature (whose name will be `FILENAME`.sig).\n' + + '\n' + + 'Gaia is a key-value store, so it does not have any built-in notion of directories. However, ' + + 'most underlying storage systems do -- directory separators in the name of a file in ' + + 'Gaia may be internally treated as first-class directories (it depends on the Gaia hub\'s driver).' + + 'As such, repeated directory separators will be treated as a single directory separator by this command. ' + + 'For example, the file name `a/b.txt`, `/a/b.txt`, and `///a////b.txt` will be treated as identical.\n' + + '\n' + + 'Example without encryption:\n' + + '\n' + + ' $ # Get an unencrypted, unsigned file\n' + + ' $ blockstack-cli gaia_getfile ryan.id http://public.ykliao.com statuses.json\n' + + ' [{"id":0,"text":"Hello, Blockstack!","created_at":1515786983492}]\n' + + '\n' + + 'Example with encryption:\n' + + '\n' + + ' $ # Get an encrypted file without decrypting\n' + + ' $ blockstack-cli gaia_getfile ryan.id https://app.graphitedocs.com documentscollection.json\n' + + ' ' + + ' $ # Get an encrypted file, and decrypt it\n' + + ' $ # Tip: You can obtain the app key with the get_app_keys command\n' + + ' $ export APP_KEY="3ac770e8c3d88b1003bf4a0a148ceb920a6172bdade8e0325a1ed1480ab4fb19"\n' + + ' $ blockstack-cli gaia_getfile ryan.id https://app.graphitedocs.com documentscollection.json "$APP_KEY" 1 0\n', + group: 'Gaia' + }, + gaia_putfile: { + type: 'array', + items: [ + { + name: 'gaia_hub', + type: 'string', + realtype: 'url', + pattern: URL_PATTERN + }, + { + name: 'app_private_key', + type: 'string', + realtype: 'private_key', + pattern: PRIVATE_KEY_UNCOMPRESSED_PATTERN + }, + { + name: 'data_path', + type: 'string', + realtype: 'path', + pattern: '.+' + }, + { + name: 'gaia_filename', + type: 'string', + realtype: 'filename', + pattern: '.+' + }, + { + name: 'encrypt', + type: 'string', + realtype: 'boolean', + pattern: BOOLEAN_PATTERN + }, + { + name: 'sign', + type: 'string', + realtype: 'boolean', + pattern: BOOLEAN_PATTERN + } + ], + minItems: 4, + maxItems: 6, + help: 'Put a file into a given Gaia hub, authenticating with the given app private key. ' + + 'Optionally encrypt and/or sign the data with the given app private key. If the file is ' + + 'successfully stored, this command prints out the URLs at which it can be fetched.\n' + + '\n' + + 'Gaia is a key-value store, so it does not have any built-in notion of directories. However, ' + + 'most underlying storage systems do -- directory separators in the name of a file in ' + + 'Gaia may be internally treated as first-class directories (it depends on the Gaia hub\'s driver).' + + 'As such, repeated directory separators will be treated as a single directory separator by this command. ' + + 'For example, the file name `a/b.txt`, `/a/b.txt`, and `///a////b.txt` will be treated as identical.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ # Store 4 versions of a file: plaintext, encrypted, signed, and encrypted+signed\n' + + ' $ # Tip: You can obtain the app key with the get_app_keys command.\n' + + ' $ export APP_KEY="3ac770e8c3d88b1003bf4a0a148ceb920a6172bdade8e0325a1ed1480ab4fb19"\n' + + ' $ blockstack-cli gaia_putfile https://hub.blockstack.org "$APP_KEY" /path/to/file.txt file.txt\n' + + ' {\n' + + ' "urls": "https://gaia.blockstack.org/hub/19KAzYp4kSKozeAGMUsnuqkEGdgQQLEvwo/file.txt"\n' + + ' }\n' + + ' $ blockstack-cli gaia_putfile https://hub.blockstack.org "$APP_KEY" /path/to/file.txt file-encrypted.txt 1\n' + + ' {\n' + + ' "urls": "https://gaia.blockstack.org/hub/19KAzYp4kSKozeAGMUsnuqkEGdgQQLEvwo/file-encrypted.txt"\n' + + ' }\n' + + ' $ blockstack-cli gaia_putfile https://hub.blockstack.org "$APP_KEY" /path/to/file.txt file-signed.txt 0 1\n' + + ' {\n' + + ' "urls": "https://gaia.blockstack.org/hub/19KAzYp4kSKozeAGMUsnuqkEGdgQQLEvwo/file-signed.txt"\n' + + ' }\n' + + ' $ blockstack-cli gaia_putfile https://hub.blockstack.org "$APP_KEY" /path/to/file.txt file-encrypted-signed.txt 1 1\n' + + ' {\n' + + ' "urls": "https://gaia.blockstack.org/hub/19KAzYp4kSKozeAGMUsnuqkEGdgQQLEvwo/file-encrypted-signed.txt"\n' + + ' }\n', + group: 'Gaia' + }, + gaia_deletefile: { + type: 'array', + items: [ + { + name: 'gaia_hub', + type: 'string', + realtype: 'url', + pattern: URL_PATTERN + }, + { + name: 'app_private_key', + type: 'string', + realtype: 'private_key', + pattern: PRIVATE_KEY_UNCOMPRESSED_PATTERN + }, + { + name: 'gaia_filename', + type: 'string', + realtype: 'filename', + pattern: '.+' + }, + { + name: 'was_signed', + type: 'string', + realtype: 'boolean', + pattern: BOOLEAN_PATTERN + } + ], + minItems: 3, + maxItems: 4, + help: 'Delete a file in a Gaia hub, as well as its signature metadata (which is stored in a separate file).' + + '\n' + + 'Example:\n' + + '\n' + + ' $ # Tip: You can obtain the app key with the get_app_keys command.\n' + + ' $ export APP_KEY="3ac770e8c3d88b1003bf4a0a148ceb920a6172bdade8e0325a1ed1480ab4fb19"\n' + + ' $ blockstack-cli gaia_deletefile https://hub.blockstack.org "$APP_KEY" file.txt false\n' + + ' ok', + group: 'Gaia' + }, + gaia_listfiles: { + type: 'array', + items: [ + { + name: 'gaia_hub', + type: 'string', + realtype: 'url', + pattern: URL_PATTERN + }, + { + name: 'app_private_key', + type: 'string', + realtype: 'private_key', + pattern: PRIVATE_KEY_UNCOMPRESSED_PATTERN + } + ], + minItems: 2, + maxItems: 3, + help: 'List all the files in a Gaia hub bucket. You must have the private key for the bucket ' + + 'in order to list its contents. The command prints each file name on its own line, and when ' + + 'finished, it prints the number of files listed.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ # Tip: You can obtain the app key with the get_app_keys command.\n' + + ' $ export APP_KEY="3ac770e8c3d88b1003bf4a0a148ceb920a6172bdade8e0325a1ed1480ab4fb19"\n' + + ' $ blockstack-cli gaia_listfiles "https://hub.blockstack.org" "$APP_KEY"\n' + + ' hello_world\n' + + ' dir/format\n' + + ' /.dotfile\n' + + ' 3\n', + group: 'Gaia' + }, + gaia_restore_bucket: { + type: 'array', + items: [ + { + name: 'name_or_id_address', + type: 'string', + realtype: 'name_or_id_address', + pattern: `${ID_ADDRESS_PATTERN}|${NAME_PATTERN}|${SUBDOMAIN_PATTERN}` + }, + { + name: 'app_origin', + type: 'string', + realtype: 'url', + pattern: URL_PATTERN + }, + { + name: 'gaia_hub', + type: 'string', + realtype: 'url', + pattern: URL_PATTERN + }, + { + name: 'backup_phrase', + type: 'string', + realtype: '12_words_or_ciphertext' + }, + { + name: 'dump_dir', + type: 'string', + realtype: 'path', + pattern: '.+' + } + ], + minItems: 5, + maxItems: 5, + help: 'Upload the contents of a previously-dumped Gaia bucket to a new Gaia hub. The `GAIA_HUB` argument ' + + 'must correspond to the *write* endpoint of the Gaia hub -- that is, you should be able to fetch ' + + '`$GAIA_HUB/hub_info`. `DUMP_DIR` must contain the file contents created by a previous successful run of the gaia_dump_bucket command, ' + + 'and both `NAME_OR_ID_ADDRESS` and `APP_ORIGIN` must be the same as they were when it was run.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ export BACKUP_PHRASE="section amount spend resemble spray verify night immune tattoo best emotion parrot"\n' + + ' $ blockstack-cli gaia_restore_bucket hello.id.blockstack https://sample.app https://new.gaia.hub "$BACKUP_PHRASE" ./backups\n' + + ' Uploaded ./backups/hello_world to https://new.gaia.hub/hub/1Lr8ggSgdmfcb4764woYutUfFqQMjEoKHc/hello_world\n' + + ' Uploaded ./backups/dir\\x2fformat to https://new.gaia.hub/hub/1Lr8ggSgdmfcb4764woYutUfFqQMjEoKHc/dir/format\n' + + ' Uploaded ./backups/\\x2f.dotfile to https://new.gaia.hub/hub/1Lr8ggSgdmfcb4764woYutUfFqQMjEoKHc//.dotfile\n' + + ' 3\n', + group: 'Gaia' + }, + gaia_sethub: { + type: 'array', + items: [ + { + name: 'blockstack_id', + type: 'string', + realtype: 'blockstack_id', + pattern: `^${NAME_PATTERN}|${SUBDOMAIN_PATTERN}$` + }, + { + name: 'owner_gaia_hub', + type: 'string', + realtype: 'url', + pattern: URL_PATTERN + }, + { + name: 'app_origin', + type: 'string', + realtype: 'url', + pattern: URL_PATTERN + }, + { + name: 'app_gaia_hub', + type: 'string', + realtype: 'url', + pattern: URL_PATTERN + }, + { + name: 'backup_phrase', + type: 'string', + realtype: '12_words_or_ciphertext' + } + ], + minItems: 5, + maxItems: 5, + help: 'Set the Gaia hub for a particular application for a Blockstack ID. If the command succeeds, ' + + 'the URLs to your updated profile will be printed and your profile will contain an entry in its "apps" ' + + 'key that links the given `APP_ORIGIN` to the given `APP_GAIA_HUB`.\n' + + '\n' + + 'NOTE: Both `OWNER_GAIA_HUB` and `APP_GAIA_HUB` must be the *write* endpoints of their respective Gaia hubs.\n' + + '\n' + + 'Your 12-word phrase (in either raw or encrypted form) is required to re-sign and store your ' + + 'profile and to generate an app-specific key and Gaia bucket. If you give the encrypted backup phrase, you will be prompted for a password.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ export BACKUP_PHRASE="soap fog wealth upon actual blossom neither timber phone exile monkey vocal"\n' + + ' $ blockstack-cli gaia_sethub hello_world.id https://hub.blockstack.org https://my.cool.app https://my.app.gaia.hub "$BACKUP_PHRASE"\n' + + ' {\n' + + ' "profileUrls": {\n' + + ' "error": null,\n' + + ' "dataUrls": [\n' + + ' "https://gaia.blockstack.org/hub/1ArdkA2oLaKnbNbLccBaFhEV4pYju8hJ82/profile.json"\n' + + ' ]\n' + + ' }\n' + + ' }\n' + + ' \n' + + ' $ # You can check the new apps entry with curl and jq as follows:\n' + + ' $ curl -sL https://gaia.blockstack.org/hub/1ArdkA2oLaKnbNbLccBaFhEV4pYju8hJ82/profile.json | jq ".[0].decodedToken.payload.claim.apps"\n' + + ' {\n' + + ' "https://my.cool.app": "https://my.app.gaia.hub/hub/1EqzyQLJ15KG1WQmi5cf1HtmSeqS1Wb8tY/"\n' + + ' }\n' + + '\n', + group: 'Gaia' + }, + get_account_history: { + type: 'array', + items: [ + { + name: 'address', + type: 'string', + realtype: 'address', + pattern: STACKS_ADDRESS_PATTERN + }, + { + name: 'page', + type: 'string', + realtype: 'integer', + pattern: '^[0-9]+$' + } + ], + minItems: 2, + maxItems: 2, + help: 'Query the history of account debits and credits over a given block range. ' + + 'Returns the history one page at a time. An empty result indicates that the page ' + + 'number has exceeded the number of historic operations in the given block range.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ blockstack-cli get_account_history SP2H7VMY13ESQDAD5808QEY1EMGESMHZWBJRTN2YA 0\n' + + ' [\n' + + ' {\n' + + ' "address": "SP2H7VMY13ESQDAD5808QEY1EMGESMHZWBJRTN2YA",\n' + + ' "block_id": 56789\n' + + ' "credit_value": "100000000000",\n' + + ' "debit_value": "0",\n' + + ' "lock_transfer_block_id": 0,\n' + + ' "txid": "0e5db84d94adff5b771262b9df015164703b39bb4a70bf499a1602b858a0a5a1",\n' + + ' "type": "STACKS",\n' + + ' "vtxindex": 0\n' + + ' },\n' + + ' {\n' + + ' "address": "SP2H7VMY13ESQDAD5808QEY1EMGESMHZWBJRTN2YA",\n' + + ' "block_id": 56790,\n' + + ' "credit_value": "100000000000",\n' + + ' "debit_value": "64000000000",\n' + + ' "lock_transfer_block_id": 0,\n' + + ' "txid": "5a0c67144626f7bd4514e4de3f3bbf251383ca13887444f326bac4bc8b8060ee",\n' + + ' "type": "STACKS",\n' + + ' "vtxindex": 1\n' + + ' },\n' + + ' {\n' + + ' "address": "SP2H7VMY13ESQDAD5808QEY1EMGESMHZWBJRTN2YA",\n' + + ' "block_id": 56791,\n' + + ' "credit_value": "100000000000",\n' + + ' "debit_value": "70400000000",\n' + + ' "lock_transfer_block_id": 0,\n' + + ' "txid": "e54c271d6a9feb4d1859d32bc99ffd713493282adef5b4fbf50bca9e33fc0ecc",\n' + + ' "type": "STACKS",\n' + + ' "vtxindex": 2\n' + + ' },\n' + + ' {\n' + + ' "address": "SP2H7VMY13ESQDAD5808QEY1EMGESMHZWBJRTN2YA",\n' + + ' "block_id": 56792,\n' + + ' "credit_value": "100000000000",\n' + + ' "debit_value": "76800000000",\n' + + ' "lock_transfer_block_id": 0,\n' + + ' "txid": "06e0d313261baefec1e59783e256ab487e17e0e776e2fdab0920cc624537e3c8",\n' + + ' "type": "STACKS",\n' + + ' "vtxindex": 3\n' + + ' }\n' + + ' ]\n' + + '\n', + group: 'Account Management' + }, + get_account_at: { + type: 'array', + items: [ + { + name: 'address', + type: 'string', + realtype: 'address', + pattern: STACKS_ADDRESS_PATTERN + }, + { + name: 'blocknumber', + type: 'string', + realtype: 'integer', + pattern: '^[0-9]+$' + } + ], + minItems: 2, + maxItems: 2, + help: 'Query the list of token debits and credits on a given address that occurred ' + + 'at a particular block height. Does not include BTC debits and credits; only Stacks.\n' + + '\n' + + 'Example\n' + + '\n' + + ' $ blockstack-cli -t get_account_at SP2NTAQFECYGSTE1W47P71FG21H8F00KZZWFGEVKQ 56789\n' + + ' [\n' + + ' {\n' + + ' "debit_value": "0",\n' + + ' "block_id": 56789\n' + + ' "lock_transfer_block_id": 0,\n' + + ' "txid": "291817c78a865c1f72938695218a48174265b2358e89b9448edc89ceefd66aa0",\n' + + ' "address": "SP2NTAQFECYGSTE1W47P71FG21H8F00KZZWFGEVKQ",\n' + + ' "credit_value": "1000000000000000000",\n' + + ' "type": "STACKS",\n' + + ' "vtxindex": 0\n' + + ' }\n' + + ' ]\n' + + '\n', + group: 'Account Management' + }, + get_address: { + type: 'array', + items: [ + { + name: 'private_key', + type: 'string', + realtype: 'private_key', + pattern: `${PRIVATE_KEY_PATTERN_ANY}` + } + ], + minItems: 1, + maxItems: 1, + help: 'Get the address of a private key or multisig private key bundle. Gives the BTC and STACKS addresses\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ blockstack-cli get_address f5185b9ca93bdcb5753fded3b097dab8547a8b47d2be578412d0687a9a0184cb01\n' + + ' {\n' + + ' "BTC": "1JFhWyVPpZQjbPcXFtpGtTmU22u4fhBVmq",\n' + + ' "STACKS": "SP2YM3J4KQK09V670TD6ZZ1XYNYCNGCWCVVKSDFWQ"\n' + + ' }\n' + + ' $ blockstack-cli get_address 1,f5185b9ca93bdcb5753fded3b097dab8547a8b47d2be578412d0687a9a0184cb01,ff2ff4f4e7f8a1979ffad4fc869def1657fd5d48fc9cf40c1924725ead60942c01\n' + + ' {\n' + + ' "BTC": "363pKBhc5ipDws1k5181KFf6RSxhBZ7e3p",\n' + + ' "STACKS": "SMQWZ30EXVG6XEC1K4QTDP16C1CAWSK1JSWMS0QN"\n' + + ' }', + group: 'Key Management' + }, + get_blockchain_record: { + type: 'array', + items: [ + { + name: 'blockstack_id', + type: 'string', + realtype: 'blockstack_id', + pattern: `^${NAME_PATTERN}|${SUBDOMAIN_PATTERN}$` + } + ], + minItems: 1, + maxItems: 1, + help: 'Get the low-level blockchain-hosted state for a Blockstack ID. This command ' + + 'is used mainly for debugging and diagnostics. You should not rely on it to be stable.', + group: 'Querying Blockstack IDs' + }, + get_blockchain_history: { + type: 'array', + items: [ + { + name: 'blockstack_id', + type: 'string', + realtype: 'blockstack_id', + pattern: `${NAME_PATTERN}|${SUBDOMAIN_PATTERN}$` + }, + { + name: 'page', + type: 'string', + realtype: 'page_number', + pattern: '^[0-9]+$' + } + ], + minItems: 1, + maxItems: 2, + help: 'Get the low-level blockchain-hosted history of operations on a Blockstack ID. ' + + 'This command is used mainly for debugging and diagnostics, and is not guaranteed to ' + + 'be stable across releases.', + group: 'Querying Blockstack IDs' + }, + get_confirmations: { + type: 'array', + items: [ + { + name: 'txid', + type: 'string', + realtype: 'transaction_id', + pattern: TXID_PATTERN + } + ], + minItems: 1, + maxItems: 1, + help: 'Get the block height and number of confirmations for a transaction.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ blockstack-cli get_confirmations e41ce043ab64fd5a5fd382fba21acba8c1f46cbb1d7c08771ada858ce7d29eea\n' + + ' {\n' + + ' "blockHeight": 567890,\n' + + ' "confirmations": 7,\n' + + ' }\n' + + '\n', + group: 'Peer Services' + }, + get_namespace_blockchain_record: { + type: 'array', + items: [ + { + name: 'namespace_id', + type: 'string', + realtype: 'namespace_id', + pattern: NAMESPACE_PATTERN + } + ], + minItems: 1, + maxItems: 1, + help: 'Get the low-level blockchain-hosted state for a Blockstack namespace. This command ' + + 'is used mainly for debugging and diagnostics, and is not guaranteed to be stable across ' + + 'releases.', + group: 'Namespace Operations' + }, + get_app_keys: { + type: 'array', + items: [ + { + name: 'backup_phrase', + type: 'string', + realtype: '12_words_or_ciphertext' + }, + { + name: 'name_or_id_address', + type: 'string', + realtype: 'name-or-id-address', + pattern: `${NAME_PATTERN}|${SUBDOMAIN_PATTERN}|${ID_ADDRESS_PATTERN}` + }, + { + name: 'app_origin', + type: 'string', + realtype: 'url', + pattern: URL_PATTERN + } + ], + minItems: 3, + maxItems: 3, + help: 'Get the application private key from a 12-word backup phrase and a name or ID-address. ' + + 'This is the private key used to sign data in Gaia, and its address is the Gaia bucket ' + + 'address. If you provide your encrypted backup phrase, you will be asked to decrypt it. ' + + 'If you provide a name instead of an ID-address, its ID-address will be queried automatically ' + + '(note that this means that the name must already be registered).\n' + + '\n' + + 'NOTE: This command does NOT verify whether or not the name or ID-address was created by the ' + + 'backup phrase. You should do this yourself via the `get_owner_keys` command if you are not sure.\n' + + '\n' + + 'There are two derivation paths emitted by this command: a `keyInfo` path and a `legacyKeyInfo`' + + 'path. You should use the one that matches the Gaia hub read URL\'s address, if you have already ' + + 'signed in before. If not, then you should use the `keyInfo` path when possible.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ export BACKUP_PHRASE="one race buffalo dynamic icon drip width lake extra forest fee kit"\n' + + ' $ blockstack-cli get_app_keys "$BACKUP_PHRASE" example.id.blockstack https://my.cool.dapp\n' + + ' {\n' + + ' "keyInfo": {\n' + + ' "privateKey": "TODO",\n' + + ' "address": "TODO"\n' + + ' },\n' + + ' "legacyKeyInfo": {\n' + + ' "privateKey": "90f9ec4e13fb9a00243b4c1510075157229bda73076c7c721208c2edca28ea8b",\n' + + ' "address": "1Lr8ggSgdmfcb4764woYutUfFqQMjEoKHc"\n' + + ' },\n' + + ' "ownerKeyIndex": 0\n' + + ' }', + group: 'Key Management' + }, + get_owner_keys: { + type: 'array', + items: [ + { + name: 'backup_phrase', + type: 'string', + realtype: '12_words_or_ciphertext' + }, + { + name: 'index', + type: 'string', + realtype: 'integer', + pattern: '^[0-9]+$' + } + ], + minItems: 1, + maxItems: 2, + help: 'Get the list of owner private keys and ID-addresses from a 12-word backup phrase. ' + + 'Pass non-zero values for INDEX to generate the sequence of ID-addresses that can be used ' + + 'to own Blockstack IDs. If you provide an encrypted 12-word backup phrase, you will be ' + + 'asked for your password to decrypt it.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ # get the first 3 owner keys and addresses for a backup phrase\n' + + ' $ export BACKUP_PHRASE="soap fog wealth upon actual blossom neither timber phone exile monkey vocal"\n' + + ' $ blockstack-cli get_owner_keys "$BACKUP_PHRASE" 3\n' + + ' [\n' + + ' {\n' + + ' "privateKey": "14b0811d5cd3486d47279d8f3a97008647c64586b121e99862c18863e2a4183501",\n' + + ' "version": "v0.10-current",\n' + + ' "index": 0,\n' + + ' "idAddress": "ID-1ArdkA2oLaKnbNbLccBaFhEV4pYju8hJ82"\n' + + ' },\n' + + ' {\n' + + ' "privateKey": "1b3572d8dd6866828281ac6cf135f04153210c1f9b123743eccb795fd3095e4901",\n' + + ' "version": "v0.10-current",\n' + + ' "index": 1,\n' + + ' "idAddress": "ID-18pR3UpD1KFrnk88a3MGZmG2dLuZmbJZ25"\n' + + ' },\n' + + ' {\n' + + ' "privateKey": "b19b6d62356db96d570fb5f08b78f0aa7f384525ba3bdcb96fbde29b8e11710d01",\n' + + ' "version": "v0.10-current",\n' + + ' "index": 2,\n' + + ' "idAddress": "ID-1Gx4s7ggkjENw3wSY6bNd1CwoQKk857AqN"\n' + + ' }\n' + + ' ]\n' + + '\n', + group: 'Key Management' + }, + get_payment_key: { + type: 'array', + items: [ + { + name: 'backup_phrase', + type: 'string', + realtype: '12_words_or_ciphertext' + } + ], + minItems: 1, + maxItems: 1, + help: 'Get the payment private key from a 12-word backup phrase. If you provide an ' + + 'encrypted backup phrase, you will be asked for your password to decrypt it. This command ' + + 'will tell you your Bitcoin and Stacks token addresses as well.\n' + + '\n' + + 'Example\n' + + '\n' + + ' $ blockstack-cli get_payment_key "soap fog wealth upon actual blossom neither timber phone exile monkey vocal"\n' + + ' [\n' + + ' {\n' + + ' "privateKey": "4023435e33da4aff0775f33e7b258f257fb20ecff039c919b5782313ab73afb401",\n' + + ' "address": {\n' + + ' "BTC": "1ybaP1gaRwRSWRE4f8JXo2W8fiTZmA4rV",\n' + + ' "STACKS": "SP5B89ZJAQHBRXVYP15YB5PAY5E24FEW9K4Q63PE"\n' + + ' },\n' + + ' "index": 0\n' + + ' }\n' + + ' ]\n' + + '\n', + group: 'Key Management' + }, + get_stacks_wallet_key: { + type: 'array', + items: [ + { + name: 'backup_phrase', + type: 'string', + realtype: '24_words_or_ciphertext' + } + ], + minItems: 1, + maxItems: 1, + help: 'Get the payment private key from a 24-word backup phrase used by the Stacks wallet. If you provide an ' + + 'encrypted backup phrase, you will be asked for your password to decrypt it. This command ' + + 'will tell you your Bitcoin and Stacks token addresses as well.\n' + + '\n' + + 'Example\n' + + '\n' + + ' $ blockstack-cli get_stacks_payment_key "toast canal educate tissue express melody produce later gospel victory meadow outdoor hollow catch liberty annual gasp hat hello april equip thank neck cruise"\n' + + ' [\n' + + ' {\n' + + ' "privateKey": "a25cea8d310ce656c6d427068c77bad58327334f73e39c296508b06589bc4fa201",\n' + + ' "address": {\n' + + ' "BTC": "1ATAW6TAbTCKgU3xPgAcWQwjW9Q26Eambx",\n' + + ' "STACKS": "SP1KTQR7CTQNA20SV2VNTF9YABMR6RJERSES3KC6Z"\n' + + ' },\n' + + ' "index": 0\n' + + ' }\n' + + ' ]\n' + + '\n', + group: 'Key Management' + }, + get_zonefile: { + type: 'array', + items: [ + { + name: 'zonefile_hash', + type: 'string', + realtype: 'zonefile_hash', + pattern: ZONEFILE_HASH_PATTERN + } + ], + minItems: 1, + maxItems: 1, + help: 'Get a zone file by hash.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ blockstack-cli get_zonefile ee77ad484b7b229f09461e4c2b6d3bd3e152ba95\n' + + ' $ORIGIN ryanshea.id\n' + + ' $TTL 3600\n' + + ' _http._tcp URI 10 1 "https://gaia.blockstack.org/hub/15BcxePn59Y6mYD2fRLCLCaaHScefqW2No/1/profile.json"\n' + + '\n', + group: 'Peer Services' + }, + help: { + type: 'array', + items: [ + { + name: 'command', + type: 'string', + realtype: 'command' + } + ], + minItems: 0, + maxItems: 1, + help: 'Get the usage string for a CLI command', + group: 'CLI' + }, + lookup: { + type: 'array', + items: [ + { + name: 'blockstack_id', + type: 'string', + realtype: 'blockstack_id', + pattern: `${NAME_PATTERN}|${SUBDOMAIN_PATTERN}$` + } + ], + minItems: 1, + maxItems: 1, + help: 'Get and authenticate the profile and zone file for a Blockstack ID.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ blockstack-cli lookup example.id\n' + + '\n', + group: 'Querying Blockstack IDs' + }, + names: { + type: 'array', + items: [ + { + name: 'id_address', + type: 'string', + realtype: 'id-address', + pattern: ID_ADDRESS_PATTERN + } + ], + minItems: 1, + maxItems: 1, + help: 'Get the list of Blockstack IDs owned by an ID-address.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ blockstack-cli names ID-1FpBChfzHG3TdQQRKWAipbLragCUArueG9\n' + + '\n', + group: 'Querying Blockstack IDs' + }, + make_keychain: { + type: 'array', + items: [ + { + name: 'backup_phrase', + type: 'string', + realtype: '12_words_or_ciphertext' + } + ], + minItems: 0, + maxItems: 1, + help: 'Generate the owner and payment private keys, optionally from a given 12-word ' + + 'backup phrase. If no backup phrase is given, a new one will be generated. If you provide ' + + 'your encrypted backup phrase, you will be asked to decrypt it.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ blockstack-cli make_keychain\n' + + ' {\n' + + ' "mnemonic": "apart spin rich leader siren foil dish sausage fee pipe ethics bundle",\n' + + ' "keyInfo": {\n' + + ' "address": "SP3G19B6J50FH6JGXAKS06N6WA1XPJCKKM4JCHC2D"\n' + + ' "index": 0,\n' + + ' "privateKey": "56d30f2b605ed114c7dc45599ae521c525d07e1286fbab67452a6586ea49332a01"\n' + + ' }\n' + + ' }\n' + + '\n', + group: 'Key Management' + }, + make_zonefile: { + type: 'array', + items: [ + { + name: 'blockstack_id', + type: 'string', + realtype: 'blockstack_id', + pattern: `^${NAME_PATTERN}|${SUBDOMAIN_PATTERN}$` + }, + { + name: 'id_address', + type: 'string', + realtype: 'ID-address', + pattern: ID_ADDRESS_PATTERN + }, + { + name: 'gaia_url_prefix', + type: 'string', + realtype: 'url', + pattern: '.+' + }, + { + name: 'resolver_url', + type: 'string', + realtype: 'url', + pattern: '.+' + } + ], + minItems: 3, + maxItems: 4, + help: 'Generate a zone file for a Blockstack ID with the given profile URL. If you know ' + + 'the ID-address for the Blockstack ID, the profile URL usually takes the form of:\n' + + '\n' + + ' {GAIA_URL_PREFIX}/{ADDRESS}/profile.json\n' + + '\n' + + 'where `{GAIA_URL_PREFIX}` is the *read* endpoint of your Gaia hub (e.g. https://gaia.blockstack.org/hub) and ' + + '`{ADDRESS}` is the base58check part of your ID-address (i.e. the string following \'ID-\').\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ blockstack-cli make_zonefile example.id ID-1ArdkA2oLaKnbNbLccBaFhEV4pYju8hJ82 https://my.gaia.hub/hub\n' + + ' $ORIGIN example.id\n' + + ' $TTL 3600\n' + + ' _http._tcp IN URI 10 1 "https://my.gaia.hub/hub/1ArdkA2oLaKnbNbLccBaFhEV4pYju8hJ82/profile.json"\n' + + '\n', + group: 'Peer Services' + }, + name_import: { + type: 'array', + items: [ + { + name: 'blockstack_id', + type: 'string', + realtype: 'blockstack_id', + pattern: NAME_PATTERN + }, + { + name: 'id_address', + type: 'string', + realtype: 'id-address', + pattern: ID_ADDRESS_PATTERN + }, + { + name: 'gaia_url_prefix', + type: 'string', + realtype: 'url', + pattern: '.+' + }, + { + name: 'reveal_key', + type: 'string', + realtype: 'private_key', + pattern: `${PRIVATE_KEY_PATTERN_ANY}` + }, + { + name: 'zonefile', + type: 'string', + realtype: 'path', + pattern: '.+' + }, + { + name: 'zonefile_hash', + type: 'string', + realtype: 'zonefile_hash', + pattern: ZONEFILE_HASH_PATTERN + } + ], + minItems: 4, + maxItems: 6, + help: 'Import a name into a namespace you revealed. The `REVEAL_KEY` must be the same as ' + + 'the key that revealed the namespace. You can only import a name into a namespace if ' + + 'the namespace has not yet been launched (i.e. via `namespace_ready`), and if the ' + + 'namespace was revealed less than a year ago (52595 blocks ago).\n' + + '\n' + + 'A zone file will be generated for this name automatically, if "ZONEFILE" is not given. By default, ' + + 'the zone file will have a URL to the name owner\'s profile prefixed by `GAIA_URL_PREFIX`. If you ' + + 'know the *write* endpoint for the name owner\'s Gaia hub, you can find out the `GAIA_URL_PREFIX` ' + + 'to use with `curl $GAIA_HUB/hub_info`".\n' + + '\n' + + 'If you specify an argument for `ZONEFILE`, then the `GAIA_URL_PREFIX` argument is ignored in favor of ' + + 'your custom zone file on disk.\n' + + '\n' + + 'If you specify a valid zone file hash for `ZONEFILE_HASH` then it will be used in favor of ' + + 'both `ZONEFILE` and `GAIA_URL_PREFIX`. The zone file hash will be incorporated directly into the ' + + 'name-import transaction.\n' + + '\n' + + 'This command prints out a transaction ID if it succeeds, and it replicates the zone file (if given) ' + + 'to a transaction broadcaster (you can choose which one with -T). The zone file will be automatically ' + + 'broadcast to the Blockstack peer network when the transaction confirms. Alternatively, you can do so ' + + 'yourself with the `zonefile_push` command.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ export REVEAL_KEY="bfeffdf57f29b0cc1fab9ea197bb1413da2561fe4b83e962c7f02fbbe2b1cd5401"\n' + + ' $ export ID_ADDRESS="ID-18e1bqU7B5qUPY3zJgMLxDnexyStTeSnvV"\n' + + ' $ blockstack-cli name_import example.id "$ID_ADDRESS" https://gaia.blockstack.org/hub "$REVEAL_KEY"\n' + + ' f726309cea7a9db364307466dc0e0e759d5c0d6bad1405e2fd970740adc7dc45\n' + + '\n', + group: 'Namespace Operations' + }, + namespace_preorder: { + type: 'array', + items: [ + { + name: 'namespace_id', + type: 'string', + realtype: 'namespace_id', + pattern: NAMESPACE_PATTERN + }, + { + name: 'reveal_address', + type: 'string', + realtype: 'address', + pattern: ADDRESS_PATTERN + }, + { + name: 'payment_key', + type: 'string', + realtype: 'private_key', + pattern: `${PRIVATE_KEY_PATTERN_ANY}` + } + ], + minItems: 3, + maxItems: 3, + help: 'Preorder a namespace. This is the first of three steps to creating a namespace. ' + + 'Once this transaction is confirmed, you will need to use the `namespace_reveal` command ' + + 'to reveal the namespace (within 24 hours, or 144 blocks).', + group: 'Namespace Operations' + }, + namespace_reveal: { + type: 'array', + items: [ + { + name: 'namespace_id', + type: 'string', + realtype: 'namespace_id', + pattern: NAMESPACE_PATTERN + }, + { + name: 'reveal_address', + type: 'string', + realtype: 'address', + pattern: ADDRESS_PATTERN + }, + { + // version + name: 'version', + type: 'string', + realtype: '2-byte-integer', + pattern: INT_PATTERN + }, + { + // lifetime + name: 'lifetime', + type: 'string', + realtype: '4-byte-integer', + pattern: INT_PATTERN + }, + { + // coeff + name: 'coefficient', + type: 'string', + realtype: '1-byte-integer', + pattern: INT_PATTERN + }, + { + // base + name: 'base', + type: 'string', + realtype: '1-byte-integer', + pattern: INT_PATTERN + }, + { + // buckets + name: 'price_buckets', + type: 'string', + realtype: 'csv-of-16-nybbles', + pattern: '^([0-9]{1,2},){15}[0-9]{1,2}$' + }, + { + // non-alpha discount + name: 'nonalpha_discount', + type: 'string', + realtype: 'nybble', + pattern: INT_PATTERN + }, + { + // no-vowel discount + name: 'no_vowel_discount', + type: 'string', + realtype: 'nybble', + pattern: INT_PATTERN + }, + { + name: 'payment_key', + type: 'string', + realtype: 'private_key', + pattern: `${PRIVATE_KEY_PATTERN_ANY}` + } + ], + minItems: 10, + maxItems: 10, + help: 'Reveal a preordered namespace, and set the price curve and payment options. ' + + 'This is the second of three steps required to create a namespace, and must be done ' + + 'shortly after the associated `namespace_preorder` command.', + group: 'Namespace Operations' + }, + namespace_ready: { + type: 'array', + items: [ + { + name: 'namespace_id', + type: 'string', + realtype: 'namespace_id', + pattern: NAMESPACE_PATTERN + }, + { + name: 'reveal_key', + type: 'string', + realtype: 'private_key', + pattern: `${PRIVATE_KEY_PATTERN_ANY}` + } + ], + minItems: 2, + maxItems: 2, + help: 'Launch a revealed namespace. This is the third and final step of creating a namespace. ' + + 'Once launched, you will not be able to import names anymore.', + group: 'Namespace Operations' + }, + price: { + type: 'array', + items: [ + { + name: 'blockstack_id', + type: 'string', + realtype: 'blockstack_id', + pattern: NAME_PATTERN + } + ], + minItems: 1, + maxItems: 1, + help: 'Get the price of an on-chain Blockstack ID. Its namespace must already exist.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ blockstack-cli price example.id\n' + + ' {\n' + + ' "units": "BTC",\n' + + ' "amount": "5500"\n' + + ' }\n' + + '\n', + group: 'Querying Blockstack IDs' + }, + price_namespace: { + type: 'array', + items: [ + { + name: 'namespace_id', + type: 'string', + realtype: 'namespace_id', + pattern: NAMESPACE_PATTERN + } + ], + minItems: 1, + maxItems: 1, + help: 'Get the price of a namespace.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ # get the price of the .hello namespace\n' + + ' $ blockstack-cli price_namespace hello\n' + + ' {\n' + + ' "units": "BTC",\n' + + ' "amount": "40000000"\n' + + ' }\n' + + '\n', + group: 'Namespace Operations' + }, + profile_sign: { + type: 'array', + items: [ + { + name: 'profile', + type: 'string', + realtype: 'path' + }, + { + name: 'owner_key', + type: 'string', + realtype: 'private_key', + pattern: PRIVATE_KEY_PATTERN + } + ], + minItems: 2, + maxItems: 2, + help: 'Sign a profile on disk with a given owner private key. Print out the signed profile JWT.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ # Tip: you can get the owner key from your 12-word backup phrase using the get_owner_keys command\n' + + ' $ blockstack-cli profile_sign /path/to/profile.json 0ffd299af9c257173be8486ef54a4dd1373407d0629ca25ca68ff24a76be09fb01\n' + + '\n', + group: 'Profiles' + }, + profile_store: { + type: 'array', + items: [ + { + name: 'user_id', + type: 'string', + realtype: 'name-or-id-address', + pattern: `${NAME_PATTERN}|${SUBDOMAIN_PATTERN}|${ID_ADDRESS_PATTERN}` + }, + { + name: 'profile', + type: 'string', + realtype: 'path' + }, + { + name: 'owner_key', + type: 'string', + realtype: 'private_key', + pattern: PRIVATE_KEY_PATTERN + }, + { + name: 'gaia_hub', + type: 'string', + realtype: 'url' + } + ], + minItems: 4, + maxItems: 4, + help: 'Store a profile on disk to a Gaia hub. `USER_ID` can be either a Blockstack ID or ' + + 'an ID-address. The `GAIA_HUB` argument must be the *write* endpoint for the user\'s Gaia hub ' + + '(e.g. https://hub.blockstack.org). You can verify this by ensuring that you can run ' + + '`curl "$GAIA_HUB/hub_info"` successfully.', + group: 'Profiles' + }, + profile_verify: { + type: 'array', + items: [ + { + name: 'profile', + type: 'string', + realtype: 'path' + }, + { + name: 'id_address', + type: 'string', + realtype: 'id-address', + pattern: `${ID_ADDRESS_PATTERN}|${PUBLIC_KEY_PATTERN}` + } + ], + minItems: 2, + maxItems: 2, + help: 'Verify a JWT encoding a profile on disk using an ID-address. Prints out the contained profile on success.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ # get the raw profile JWT\n' + + ' $ curl -sL https://raw.githubusercontent.com/jcnelson/profile/master/judecn.id > /tmp/judecn.id.jwt\n' + + ' $ # Tip: you can get the ID-address for a name with the "whois" command\n' + + ' $ blockstack-cli profile_verify /tmp/judecn.id.jwt ID-16EMaNw3pkn3v6f2BgnSSs53zAKH4Q8YJg\n' + + '\n', + group: 'Profiles' + }, + renew: { + type: 'array', + items: [ + { + name: 'blockstack_id', + type: 'string', + realtype: 'on-chain-blockstack_id', + pattern: NAME_PATTERN + }, + { + name: 'owner_key', + type: 'string', + realtype: 'private_key', + pattern: `${PRIVATE_KEY_PATTERN_ANY}` + }, + { + name: 'payment_key', + type: 'string', + realtype: 'private_key', + pattern: `${PRIVATE_KEY_PATTERN_ANY}` + }, + { + name: 'new_id_address', + type: 'string', + realtype: 'id-address', + pattern: ID_ADDRESS_PATTERN + }, + { + name: 'zonefile', + type: 'string', + realtype: 'path' + }, + { + name: 'zonefile_hash', + type: 'string', + realtype: 'zonefile_hash', + pattern: ZONEFILE_HASH_PATTERN + } + ], + minItems: 3, + maxItems: 6, + help: 'Renew a name. Optionally transfer it to a new owner address (`NEW_ID_ADDRESS`), ' + + 'and optionally load up and give it a new zone file on disk (`ZONEFILE`). If the command ' + + 'succeeds, it prints out a transaction ID. You can use with the `get_confirmations` ' + + 'command to track its confirmations on the underlying blockchain -- once it reaches 7 ' + + 'confirmations, the rest of the Blockstack peer network will process it.\n' + + '\n' + + 'If you create a new zonefile for your name, you will need ' + + 'to later use `zonefile_push` to replicate the zone file to the Blockstack peer network ' + + 'once the transaction reaches 7 confirmations.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ # Tip: you can get your owner key from your backup phrase with "get_owner_keys".\n' + + ' $ # Tip: you can get your payment key from your backup phrase with "get_payment_key".\n' + + ' $ export OWNER="136ff26efa5db6f06b28f9c8c7a0216a1a52598045162abfe435d13036154a1b01"\n' + + ' $ export PAYMENT="bfeffdf57f29b0cc1fab9ea197bb1413da2561fe4b83e962c7f02fbbe2b1cd5401"\n' + + ' $ blockstack-cli renew hello_world.id "$OWNER" "$PAYMENT"\n' + + ' 3d8945ce76d4261678d76592b472ed639a10d4298f9d730af4edbbc3ec02882e\n' + + '\n' + + ' $ # Renew with a new owner\n' + + ' $ export NEW_OWNER="ID-141BcmFVbEuuMb7Bd6umXyV6ZD1WYomYDE"\n' + + ' $ blockstack-cli renew hello_world.id "$OWNER" "$PAYMENT" "$NEW_OWNER"\n' + + ' 33865625ef3f1b607111c0dfba9e58604927173bd2e299a343e19aa6d2cfb263\n' + + '\n' + + ' $ # Renew with a new zone file.\n' + + ' $ # Tip: you can create a new zonefile with the "make_zonefile" command.\n' + + ' $ export ZONEFILE_PATH="/path/to/new/zonefile.txt"\n' + + ' $ blockstack-cli renew hello_world.id "$OWNER" "$PAYMENT" --zonefile "$ZONEFILE_PATH"\n' + + ' e41ce043ab64fd5a5fd382fba21acba8c1f46cbb1d7c08771ada858ce7d29eea\n' + + ' $ # wait 7 confirmations\n' + + ' $ blockstack-cli get_confirmations e41ce043ab64fd5a5fd382fba21acba8c1f46cbb1d7c08771ada858ce7d29eea\n' + + ' {\n' + + ' "blockHeight": 567890,\n' + + ' "confirmations": 7,\n' + + ' }\n' + + ' $ blockstack-cli -H https://core.blockstack.org zonefile_push "$ZONEFILE_PATH"\n' + + ' [\n' + + ' "https://core.blockstack.org"\n' + + ' ]\n' + + '\n', + group: 'Blockstack ID Management' + }, + register: { + type: 'array', + items: [ + { + name: 'blockstack_id', + type: 'string', + realtype: 'on-chain-blockstack_id', + pattern: NAME_PATTERN + }, + { + name: 'owner_key', + type: 'string', + realtype: 'private_key', + pattern: PRIVATE_KEY_PATTERN + }, + { + name: 'payment_key', + type: 'string', + realtype: 'private_key', + pattern: `${PRIVATE_KEY_PATTERN_ANY}` + }, + { + name: 'gaia_hub', + type: 'string', + realtype: 'url' + }, + { + name: 'zonefile', + type: 'string', + realtype: 'path' + } + ], + minItems: 4, + maxItems: 5, + help: 'If you are trying to register a name for a *private key*, use this command.\n' + + '\n' + + 'Register a name to a single name-owning private key. After successfully running this command, ' + + 'and after waiting a couple hours, your name will be ready to use and will resolve to a ' + + 'signed empty profile hosted on the given Gaia hub (`GAIA_HUB`).\n' + + '\n' + + 'Behind the scenes, this will generate and send two transactions ' + + 'and generate and replicate a zone file with the given Gaia hub URL (`GAIA_HUB`). ' + + 'Note that the `GAIA_HUB` argument must correspond to the *write* endpoint of the Gaia hub ' + + '(i.e. you should be able to run \'curl "$GAIA_HUB/hub_info"\' and get back data). If you ' + + 'are using Blockstack PBC\'s default Gaia hub, pass "https://hub.blockstack.org" for this ' + + 'argument.\n' + + '\n' + + 'By default, this command generates a zone file automatically that points to the Gaia hub\'s ' + + 'read endpoint (which is queried on-the-fly from `GAIA_HUB`). If you instead want to have a custom zone file for this name, ' + + 'you can specify a path to it on disk with the `ZONEFILE` argument.\n' + + '\n' + + 'If this command completes successfully, your name will be ready to use once both transactions have 7+ confirmations. ' + + 'You can use the `get_confirmations` command to track the confirmations ' + + 'on the transaction IDs returned by this command.\n' + + '\n' + + 'WARNING: You should *NOT* use the payment private key (`PAYMENT_KEY`) while the name is being confirmed. ' + + 'If you do so, you could double-spend one of the pending transactions and lose your name.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ export OWNER="136ff26efa5db6f06b28f9c8c7a0216a1a52598045162abfe435d13036154a1b01"\n' + + ' $ export PAYMENT="bfeffdf57f29b0cc1fab9ea197bb1413da2561fe4b83e962c7f02fbbe2b1cd5401"\n' + + ' $ blockstack-cli register example.id "$OWNER" "$PAYMENT" https://hub.blockstack.org\n' + + ' 9bb908bfd4ab221f0829167a461229172184fc825a012c4e551533aa283207b1\n' + + '\n', + group: 'Blockstack ID Management' + }, + register_addr: { + type: 'array', + items: [ + { + name: 'blockstack_id', + type: 'string', + realtype: 'blockstack_id', + pattern: NAME_PATTERN + }, + { + name: 'id-address', + type: 'string', + realtype: 'id-address', + pattern: ID_ADDRESS_PATTERN + }, + { + name: 'payment_key', + type: 'string', + realtype: 'private_key', + pattern: `${PRIVATE_KEY_PATTERN_ANY}` + }, + { + name: 'gaia_url_prefix', + type: 'string', + realtype: 'url' + }, + { + name: 'zonefile', + type: 'string', + realtype: 'path' + } + ], + minItems: 4, + maxItems: 5, + help: 'If you are trying to register a name for an *ID-address*, use this command.\n' + + '\n' + + 'Register a name to someone\'s ID-address. After successfully running this ' + + 'command and waiting a couple of hours, the name will be registered on-chain and have a ' + + 'zone file with a URL to where the owner\'s profile should be. This command does NOT ' + + 'generate, sign, or replicate a profile for the name---the name owner will need to do this ' + + 'separately, once the name is registered.\n' + + '\n' + + 'Behind the scenes, this command will generate two ' + + 'transactions, and generate and replicate a zone file with the given Gaia hub read URL ' + + '(`GAIA_URL_PREFIX`). Note that the `GAIA_URL_PREFIX` argument must correspond to the *read* endpoint of the Gaia hub ' + + '(e.g. if you are using Blockstack PBC\'s default Gaia hub, this is "https://gaia.blockstack.org/hub"). ' + + 'If you know the *write* endpoint of the name owner\'s Gaia hub, you can find the right value for ' + + '`GAIA_URL_PREFIX` by running "curl $GAIA_HUB/hub_info".\n' + + '\n' + + 'No profile will be generated or uploaded by this command. Instead, this command generates ' + + 'a zone file that will include the URL to a profile based on the `GAIA_URL_PREFIX` argument.\n' + + '\n' + + 'The zone file will be generated automatically from the `GAIA_URL_PREFIX` argument. If you need ' + + 'to use a custom zone file, you can pass the path to it on disk via the `ZONEFILE` argument.\n' + + '\n' + + 'If this command completes successfully, the name will be ready to use in a couple of ' + + 'hours---that is, once both transactions have 7+ confirmations. ' + + 'You can use the `get_confirmations` command to track the confirmations.\n' + + '\n' + + 'WARNING: You should *NOT* use the payment private key (`PAYMENT_KEY`) while the name is being confirmed. ' + + 'If you do so, you could double-spend one of the pending transactions and lose the name.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ export ID_ADDRESS="ID-18e1bqU7B5qUPY3zJgMLxDnexyStTeSnvV"\n' + + ' $ export PAYMENT="bfeffdf57f29b0cc1fab9ea197bb1413da2561fe4b83e962c7f02fbbe2b1cd5401"\n' + + ' $ blockstack-cli register_addr example.id "$ID_ADDRESS" "$PAYMENT" https://gaia.blockstack.org/hub', + group: 'Blockstack ID Management' + }, + register_subdomain: { + type: 'array', + items: [ + { + name: 'blockstack_id', + type: 'string', + realtype: 'blockstack_id', + pattern: SUBDOMAIN_PATTERN + }, + { + name: 'owner_key', + type: 'string', + realtype: 'private_key', + pattern: PRIVATE_KEY_PATTERN + }, + { + name: 'gaia_hub', + type: 'string', + realtype: 'url' + }, + { + name: 'registrar', + type: 'string', + realtype: 'url' + }, + { + name: 'zonefile', + type: 'string', + realtype: 'path' + } + ], + minItems: 4, + maxItems: 5, + help: 'Register a subdomain. This will generate and sign a subdomain zone file record ' + + 'with the given `GAIA_HUB` URL and send it to the given subdomain registrar (`REGISTRAR`).\n' + + '\n' + + 'This command generates, signs, and uploads a profile to the `GAIA_HUB` url. Note that the `GAIA_HUB` ' + + 'argument must correspond to the *write* endpoint of your Gaia hub (i.e. you should be able ' + + 'to run \'curl "$GAIA_HUB/hub_info"\' successfully). If you are using Blockstack PBC\'s default ' + + 'Gaia hub, this argument should be "https://hub.blockstack.org".\n' + + '\n' + + 'WARNING: At this time, no validation will occur on the registrar URL. Be sure that the URL ' + + 'corresponds to the registrar for the on-chain name before running this command!\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ export OWNER="6e50431b955fe73f079469b24f06480aee44e4519282686433195b3c4b5336ef01"\n' + + ' $ # NOTE: https://registrar.blockstack.org is the registrar for personal.id!\n' + + ' $ blockstack-cli register_subdomain hello.personal.id "$OWNER" https://hub.blockstack.org https://registrar.blockstack.org\n', + group: 'Blockstack ID Management' + }, + revoke: { + type: 'array', + items: [ + { + name: 'blockstack_id', + type: 'string', + realtype: 'on-chain-blockstack_id', + pattern: NAME_PATTERN + }, + { + name: 'owner_key', + type: 'string', + realtype: 'private_key', + pattern: `${PRIVATE_KEY_PATTERN_ANY}` + }, + { + name: 'payment_key', + type: 'string', + realtype: 'private_key', + pattern: `${PRIVATE_KEY_PATTERN_ANY}` + } + ], + minItems: 3, + maxItems: 3, + help: 'Revoke a name. This renders it unusable until it expires (if ever). This command ' + + 'prints out the transaction ID if it succeeds. Once the transaction confirms, the name will ' + + 'be revoked by each node in the peer network. This command only works for on-chain names, not ' + + 'subdomains.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ # Tip: you can get your owner and payment keys from your 12-word backup phrase using the get_owner_keys and get_payment_key commands.\n' + + ' $ export OWNER="6e50431b955fe73f079469b24f06480aee44e4519282686433195b3c4b5336ef01"\n' + + ' $ export PAYMENT="bfeffdf57f29b0cc1fab9ea197bb1413da2561fe4b83e962c7f02fbbe2b1cd5401"\n' + + ' $ blockstack-cli revoke example.id "$OWNER" "$PAYMENT"\n' + + ' 233b559c97891affa010567bd582110508d0236b4e3f88d3b1d0731629e030b0\n' + + '\n', + group: 'Blockstack ID Management' + }, + send_btc: { + type: 'array', + items: [ + { + name: 'recipient_address', + type: 'string', + realtype: 'address', + pattern: ADDRESS_PATTERN + }, + { + name: 'amount', + type: 'string', + realtype: 'satoshis', + pattern: INT_PATTERN + }, + { + name: 'payment_key', + type: 'string', + realtype: 'private_key', + pattern: `${PRIVATE_KEY_PATTERN_ANY}` + } + ], + minItems: 3, + maxItems: 3, + help: 'Send some Bitcoin (in satoshis) from a payment key to an address. Up to the given ' + + 'amount will be spent, but likely less---the actual amount sent will be the amount given, ' + + 'minus the transaction fee. For example, if you want to send 10000 satoshis but the ' + + 'transaction fee is 2000 satoshis, then the resulting transaction will send 8000 satoshis ' + + 'to the given address. This is to ensure that this command does not *over*-spend your ' + + 'Bitcoin. If you want to check the amount before spending, pass the `-x` flag to see the ' + + 'raw transaction.\n' + + '\n' + + 'If the command succeeds, it prints out the transaction ID. You can track its confirmations ' + + 'with the `get_confirmations` command.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ export PAYMENT="bfeffdf57f29b0cc1fab9ea197bb1413da2561fe4b83e962c7f02fbbe2b1cd5401"\n' + + ' $ blockstack-cli send_btc 18qTSE5PPQmypwKKej7QX5Db2XAttgYeA1 123456 "$PAYMENT"\n' + + ' c7e239fd24da30e36e011e6bc7db153574a5b40a3a8dc3b727adb54ad038acc5\n' + + '\n', + group: 'Account Management' + }, + send_tokens: { + type: 'array', + items: [ + { + name: 'address', + type: 'string', + realtype: 'address', + pattern: STACKS_ADDRESS_PATTERN + }, + { + name: 'amount', + type: 'string', + realtype: 'integer', + pattern: '^[0-9]+$' + }, + { + name: 'fee', + type: 'string', + realtype: 'integer', + pattern: '^[0-9]+$' + }, + { + name: 'nonce', + type: 'string', + realtype: 'integer', + pattern: '^[0-9]+$' + }, + { + name: 'payment_key', + type: 'string', + realtype: 'private_key', + pattern: `${PRIVATE_KEY_PATTERN_ANY}` + }, + { + name: 'memo', + type: 'string', + realtype: 'string', + pattern: '^.{0,34}$' + } + ], + minItems: 5, + maxItems: 6, + help: 'Send a particular type of tokens to the given `ADDRESS`. Right now, only supported `TOKEN-TYPE` is `STACKS`. Optionally ' + + 'include a memo string (`MEMO`) up to 34 characters long.\n' + + '\n' + + 'If the command succeeds, it prints out a transaction ID. You can track the confirmations on the transaction ' + + 'via the `get_confirmations` command. Once the transaction has 7 confirmations, the Blockstack peer network ' + + 'will have processed it, and your payment key balance and recipient balance will be updated.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ # check balances of sender and recipient before sending.\n' + + ' $ # address of the key below is SP2SC16ASH76GX549PT7J5WQZA4GHMFBKYMBQFF9V\n' + + ' $ export PAYMENT="bfeffdf57f29b0cc1fab9ea197bb1413da2561fe4b83e962c7f02fbbe2b1cd5401"\n' + + ' $ blockstack-cli balance SP2SC16ASH76GX549PT7J5WQZA4GHMFBKYMBQFF9V\n' + + ' {\n' + + ' "STACKS": "10000000"\n' + + ' }\n' + + ' $ blockstack-cli balance SP1P10PS2T517S4SQGZT5WNX8R00G1ECTRKYCPMHY\n' + + ' {\n' + + ' "STACKS": "0"\n' + + ' }\n' + + '\n' + + ' $ # send tokens\n' + + ' $ blockstack-cli send_tokens SP1P10PS2T517S4SQGZT5WNX8R00G1ECTRKYCPMHY 12345 1 0 "$PAYMENT"\n' + + ' a9d387a925fb0ba7a725fb1e11f2c3f1647473699dd5a147c312e6453d233456\n' + + '\n' + + ' $ # wait for transaction to be confirmed\n' + + '\n' + + ' $ blockstack-cli balance SP2SC16ASH76GX549PT7J5WQZA4GHMFBKYMBQFF9V\n' + + ' {\n' + + ' "STACKS": "9987655"\n' + + ' }\n' + + ' $ blockstack-cli balance SP1P10PS2T517S4SQGZT5WNX8R00G1ECTRKYCPMHY\n' + + ' {\n' + + ' "STACKS": "12345"\n' + + ' }\n' + + '\n', + group: 'Account Management' + }, + transfer: { + type: 'array', + items: [ + { + name: 'blockstack_id', + type: 'string', + realtype: 'on-chain-blockstack_id', + pattern: NAME_PATTERN + }, + { + name: 'new_id_address', + type: 'string', + realtype: 'id-address', + pattern: ID_ADDRESS_PATTERN + }, + { + name: 'keep_zonefile', + type: 'string', + realtype: 'true-or-false', + pattern: '^true$|^false$' + }, + { + name: 'owner_key', + type: 'string', + realtype: 'private_key', + pattern: `${PRIVATE_KEY_PATTERN_ANY}` + }, + { + name: 'payment_key', + type: 'string', + realtype: 'private_key', + pattern: `${PRIVATE_KEY_PATTERN_ANY}` + } + ], + minItems: 5, + maxItems: 5, + help: 'Transfer a Blockstack ID to a new address (`NEW_ID_ADDRESS`). Optionally preserve ' + + 'its zone file (`KEEP_ZONEFILE`). If the command succeeds, it will print a transaction ID. ' + + 'Once the transaction reaches 7 confirmations, the Blockstack peer network will transfer the ' + + 'Blockstack ID to the new ID-address. You can track the transaction\'s confirmations with ' + + 'the `get_confirmations` command.\n' + + '\n' + + 'NOTE: This command only works for on-chain Blockstack IDs. It does not yet work for subdomains.\n' + + '\n' + + 'An ID-address can only own up to 25 Blockstack IDs. In practice, you should generate a new ' + + 'owner key and ID-address for each name you receive (via the `get_owner_keys` command).\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ # Tip: you can get your owner key from your backup phrase with "get_owner_keys".\n' + + ' $ # Tip: you can get your payment key from your backup phrase with "get_payment_key".\n' + + ' $ export OWNER="136ff26efa5db6f06b28f9c8c7a0216a1a52598045162abfe435d13036154a1b01"\n' + + ' $ export PAYMENT="bfeffdf57f29b0cc1fab9ea197bb1413da2561fe4b83e962c7f02fbbe2b1cd5401"\n' + + ' $ blockstack-cli transfer example.id ID-1HJA1AJvWef21XbQVL2AcTv71b6JHGPfDX true "$OWNER" "$PAYMENT"\n' + + ' e09dc158e586d0c09dbcdcba917ec394e6c6ac2b9c91c4b55f32f5973e4f08fc\n' + + '\n', + group: 'Blockstack ID Management' + }, + tx_preorder: { + type: 'array', + items: [ + { + name: 'blockstack_id', + type: 'string', + realtype: 'on-chain-blockstack_id', + pattern: NAME_PATTERN + }, + { + name: 'id_address', + type: 'string', + realtype: 'id-address', + pattern: ID_ADDRESS_PATTERN + }, + { + name: 'payment_key', + type: 'string', + realtype: 'private_key', + pattern: `${PRIVATE_KEY_PATTERN_ANY}` + } + ], + minItems: 3, + maxItems: 3, + help: 'Generate and send `NAME_PREORDER` transaction, for a Blockstack ID to be owned ' + + 'by a given `ID_ADDRESS`. The name cost will be paid for by the gven `PAYMENT_KEY`. The ' + + 'ID-address should be a never-before-seen address, since it will be used as a salt when ' + + 'generating the name preorder hash.\n' + + '\n' + + 'This is a low-level command that only experienced Blockstack developers should use. ' + + 'If you just want to register a name, use the "register" command.\n', + group: 'Blockstack ID Management' + }, + tx_register: { + type: 'array', + items: [ + { + name: 'blockstack_id', + type: 'string', + realtype: 'on-chain-blockstack_id', + pattern: NAME_PATTERN + }, + { + name: 'id_address', + type: 'string', + realtype: 'id-address', + pattern: ID_ADDRESS_PATTERN + }, + { + name: 'payment_key', + type: 'string', + realtype: 'private_key', + pattern: `${PRIVATE_KEY_PATTERN_ANY}` + }, + { + name: 'zonefile', + type: 'string', + realtype: 'path' + }, + { + name: 'zonefile_hash', + type: 'string', + realtype: 'zoenfile_hash', + pattern: ZONEFILE_HASH_PATTERN + } + ], + minItems: 3, + maxItems: 5, + help: 'Generate and send a NAME_REGISTRATION transaction, assigning the given `BLOCKSTACK_ID` ' + + 'to the given `ID_ADDRESS`. Optionally pair the Blockstack ID with a zone file (`ZONEFILE`) or ' + + 'the hash of the zone file (`ZONEFILE_HASH`). You will need to push the zone file to the peer ' + + 'network after the transaction confirms (i.e. with `zonefile_push`).\n' + + '\n' + + 'This is a low-level command that only experienced Blockstack developers should use. If you ' + + 'just want to register a name, you should use the `register` command.', + group: 'Blockstack ID Management' + }, + update: { + type: 'array', + items: [ + { + name: 'blockstack_id', + type: 'string', + realtype: 'on-chain-blockstack_id', + pattern: NAME_PATTERN + }, + { + name: 'zonefile', + type: 'string', + realtype: 'path' + }, + { + name: 'owner_key', + type: 'string', + realtype: 'private_key', + pattern: `${PRIVATE_KEY_PATTERN_ANY}` + }, + { + name: 'payment_key', + type: 'string', + realtype: 'private_key', + pattern: `${PRIVATE_KEY_PATTERN_ANY}` + }, + { + name: 'zonefile_hash', + type: 'string', + realtype: 'zonefile_hash', + pattern: ZONEFILE_HASH_PATTERN + } + ], + minItems: 4, + maxItems: 5, + help: 'Update the zonefile for an on-chain Blockstack ID. You can generate a well-formed ' + + 'zone file using the `make_zonefile` command, or you can supply your own. Zone files can be ' + + 'up to 40Kb. Alternatively, if you only want to announce the hash of a zone file (or any ' + + 'arbitrary 20-byte hex string), you can do so by passing a value for `ZONEFILE_HASH`. If `ZONEFILE_HASH` ' + + 'is given, then the value for `ZONEFILE` will be ignored.\n' + + '\n' + + 'If this command succeeds, it prints out a transaction ID. Once the transaction has 7 confirmations, ' + + 'the Blockstack peer network will set the name\'s zone file hash to the `RIPEMD160`(SHA256) hash of ' + + 'the given zone file (or it will simply set it to the hash given in `ZONEFILE_HASH`).\n' + + '\n' + + 'Once the transaction confirms, you will need to replicate the zone file to the Blockstack peer network. ' + + 'This can be done with the `zonefile_push` command. Until you do so, no Blockstack clients will be able ' + + 'to obtain the zone file announced by this command.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ # Tip: you can get your owner and payment keys from your 12-word backup phrase using the get_owner_keys and get_payment_key commands.\n' + + ' $ export OWNER="6e50431b955fe73f079469b24f06480aee44e4519282686433195b3c4b5336ef01"\n' + + ' $ export PAYMENT="bfeffdf57f29b0cc1fab9ea197bb1413da2561fe4b83e962c7f02fbbe2b1cd5401"\n' + + ' $ # make a new zone file\n' + + ' $ blockstack-cli make_zonefile example.id ID-1ArdkA2oLaKnbNbLccBaFhEV4pYju8hJ82 https://my.gaia.hub/hub > /tmp/zonefile.txt\n' + + ' \n' + + ' $ # update the name to reference this new zone file\n' + + ' $ blockstack-cli update example.id /tmp/zonefile.txt "$OWNER" "$PAYMENT"\n' + + ' 8e94a5b6647276727a343713d3213d587836e1322b1e38bc158406f5f8ebe3fd\n' + + ' \n' + + ' $ # check confirmations\n' + + ' $ blockstack-cli get_confirmations e41ce043ab64fd5a5fd382fba21acba8c1f46cbb1d7c08771ada858ce7d29eea\n' + + ' {\n' + + ' "blockHeight": 567890,\n' + + ' "confirmations": 7,\n' + + ' }\n' + + ' \n' + + ' $ # send out the new zone file to a Blockstack peer\n' + + ' $ blockstack-cli -H https://core.blockstack.org zonefile_push /tmp/zonefile.txt\n' + + ' [\n' + + ' "https://core.blockstack.org"\n' + + ' ]\n' + + '\n', + group: 'Blockstack ID Management' + }, + whois: { + type: 'array', + items: [ + { + name: 'blockstack_id', + type: 'string', + realtype: 'blockstack_id', + pattern: NAME_PATTERN + '|'+ SUBDOMAIN_PATTERN + } + ], + minItems: 1, + maxItems: 1, + help: 'Look up the zone file and owner of a Blockstack ID. Works with both on-chain and off-chain names.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ blockstack-cli whois example.id\n' + + ' {\n' + + ' "address": "1ArdkA2oLaKnbNbLccBaFhEV4pYju8hJ82",\n' + + ' "block_renewed_at": 567890,\n' + + ' "blockchain": "bitcoin",\n' + + ' "expire_block": 687010,\n' + + ' "grace_period": false,\n' + + ' "last_transaction_height": "567891",\n' + + ' "last_txid": "a564aa482ee43eb2bdfb016e01ea3b950bab0cfa39eace627d632e73c7c93e48",\n' + + ' "owner_script": "76a9146c1c2fc3cf74d900c51e9b5628205130d7b98ae488ac",\n' + + ' "renewal_deadline": 692010,\n' + + ' "resolver": null,\n' + + ' "status": "registered",\n' + + ' "zonefile": "$ORIGIN example.id\\n$TTL 3600\\n_http._tcp URI 10 1 \\"https://gaia.blockstack.org/hub/1ArdkA2oLaKnbNbLccBaFhEV4pYju8hJ82/profile.json\\"\\n",\n' + + ' "zonefile_hash": "ae4ee8e7f30aa890468164e667e2c203266f726e"\n' + + ' }\n' + + '\n', + group: 'Querying Blockstack IDs' + }, + zonefile_push: { + type: 'array', + items: [ + { + name: 'zonefile', + type: 'string', + realtype: 'path' + } + ], + minItems: 1, + maxItems: 1, + help: 'Push a zone file on disk to the Blockstack peer network. The zone file must ' + + 'correspond to a zone file hash that has already been announced. That is, you use this command ' + + 'in conjunction with the `register`, `update`, `renew`, or `name_import` commands.\n' + + '\n' + + 'Example:\n' + + '\n' + + ' $ blockstack-cli -H https://core.blockstack.org zonefile_push /path/to/zonefile.txt\n' + + ' [\n' + + ' "https://core.blockstack.org"\n' + + ' ]\n' + + '\n', + group: 'Peer Services' + }, + get_did_configuration: { + type: 'array', + items: [ + { + name: 'blockstack_id', + type: 'string', + realtype: 'blockstack_id', + pattern: NAME_PATTERN + '|'+ SUBDOMAIN_PATTERN + }, + { + name: 'domain', + type: 'string', + realtype: 'domain', + pattern: NAME_PATTERN + '|'+ SUBDOMAIN_PATTERN + }, + { + name: 'owner_key', + type: 'string', + realtype: 'private_key', + pattern: `${PRIVATE_KEY_PATTERN}` + } + ], + minItems: 3, + maxItems: 3, + help: 'Creates a DID configuration for the given blockstack id and domain to create a link between both.' + + 'The specification is define by the Decentralized Identity Foundation at https://identity.foundation/specs/did-configuration/\n' + + 'The DID configuration should be placed in the json file ".well_known/did_configuration"' + + '\n'+ + 'Example:\n'+ + '\n'+ + ' $ # Tip: you can get your owner keys from your 12-word backup phrase using the get_owner_keys command.\n' + + ' $ export PRIVATE_OWNER_KEY="6e50431b955fe73f079469b24f06480aee44e4519282686433195b3c4b5336ef01"\n' + + ' $ blockstack-cli get_did_configuration public_profile_for_testing.id.blockstack helloblockstack.com PRIVATE_OWNER_KEY\n' + + ' {\n' + + ' "entries": [\n'+ + ' {\n' + + ' "did": "did:stack:v0:SewTRvPZUEQGdr45QvEnVMGHZBhx3FT1Jj-0",\n' + + ' "jwt": "eyJ0eXAiOiJKV1QiL...."\n' + + ' }\n'+ + ' ]\n'+ + ' }\n'+ + '\n'+ + 'The decoded content of the jwt above is \n'+ + ' {\n'+ + ' "header": {\n' + + ' "typ": "JWT", "alg": "ES256K"\n' + + ' },\n'+ + ' "payload": {\n' + + ' "iss": "did:stack:v0:SewTRvPZUEQGdr45QvEnVMGHZBhx3FT1Jj-0",\n' + + ' "domain": "helloblockstack.com",\n' + + ' "exp": "2020-12-07T13:05:28.375Z"\n' + + ' },\n'+ + ' "signature": "NDY7ISzgAHKcZDvbxzTxQdVnf6xWMZ46w5vHcDpNx_1Fsyip0M6E6GMq_2YZ-gUcwmwlo8Ag9jgnfOkaBIFpoQ"\n' + + ' }\n' + + '\n', + group: 'DID' + } + } as CLI_PROP, + additionalProperties: false, + strict: true +}; + +// usage string for built-in options +export const USAGE = `Usage: ${process.argv[1]} [options] command [command arguments] +Options can be: + -c Path to a config file (defaults to + ${DEFAULT_CONFIG_PATH}) + + -d Print verbose debugging output + + -e Estimate the BTC cost of an transaction (in satoshis). + Do not generate or send any transactions. + + -m MAGIC_BYTES Use an alternative magic byte string instead of "id". + + -t Use the public testnet instead of mainnet. + + -i Use integration test framework instead of mainnet. + + -U Unsafe mode. No safety checks will be performed. + + -x Do not broadcast a transaction. Only generate and + print them to stdout. + + -B BURN_ADDR Use the given namespace burn address instead of the one + obtained from the Blockstack network (DANGEROUS) + + -D DENOMINATION Denominate the price to pay in the given units + (DANGEROUS) + + -C CONSENSUS_HASH Use the given consensus hash instead of one obtained + from the network + + -F FEE_RATE Use the given transaction fee rate instead of the one + obtained from the Bitcoin network + + -G GRACE_PERIOD Number of blocks in which a name can be renewed after it + expires (DANGEROUS) + + -H URL Use an alternative Blockstack Core API endpoint. + + -I URL Use an alternative Blockstack Core Indexer endpoint. + + -M MAX_INDEX Maximum keychain index to use when searching for an identity address + (default is ${DEFAULT_MAX_ID_SEARCH_INDEX}). + + -N PAY2NS_PERIOD Number of blocks in which a namespace receives the registration + and renewal fees after it is created (DANGEROUS) + + -P PRICE Use the given price to pay for names or namespaces + (DANGEROUS) + + -T URL Use an alternative Blockstack transaction broadcaster. + + -X URL Use an alternative UTXO service endpoint. + + -u USERNAME A username to be passed to bitcoind RPC endpoints + + -p PASSWORD A password to be passed to bitcoind RPC endpoints +`; + +/* + * Format help + */ +function formatHelpString(indent: number, limit: number, helpString: string) : string { + const lines = helpString.split('\n'); + let buf = ''; + let pad = ''; + for (let i = 0; i < indent; i++) { + pad += ' '; + } + + for (let i = 0; i < lines.length; i++) { + let linebuf = pad.slice(); + const words = lines[i].split(/ /).filter((word) => word.length > 0); + if (words.length == 0) { + buf += '\n'; + continue; + } + + if (words[0] === '$' || lines[i].substring(0, 4) === ' ') { + // literal line + buf += lines[i] + '\n'; + continue; + } + + for (let j = 0; j < words.length; j++) { + if (words[j].length === 0) { + // explicit line break + linebuf += '\n'; + break; + } + + if (linebuf.split('\n').slice(-1)[0].length + 1 + words[j].length > limit) { + linebuf += '\n'; + linebuf += pad; + } + linebuf += words[j] + ' '; + } + + buf += linebuf + '\n'; + } + return buf; +} + +/* + * Format command usage lines. + * Generate two strings: + * raw string: + * COMMAND ARG_NAME ARG_NAME ARG_NAME [OPTINONAL ARG NAME] + * keyword string: + * COMMAND --arg_name TYPE + * --arg_name TYPE + * [--arg_name TYPE] + */ +interface CLI_COMMAND_HELP { + raw: string; + kw: string +} + +function formatCommandHelpLines(commandName: string, commandArgs: Array) : CLI_COMMAND_HELP { + let rawUsage = ''; + let kwUsage = ''; + let kwPad = ''; + const commandInfo = CLI_ARGS.properties[commandName]; + + rawUsage = ` ${commandName} `; + for (let i = 0; i < commandArgs.length; i++) { + if (!commandArgs[i].name) { + console.log(commandName); + console.log(commandArgs[i]); + throw new Error('BUG: command info is missing a "name" field'); + } + if (i + 1 <= commandInfo.minItems) { + rawUsage += `${commandArgs[i].name.toUpperCase()} `; + } + else { + rawUsage += `[${commandArgs[i].name.toUpperCase()}] `; + } + } + + kwUsage = ` ${commandName} `; + for (let i = 0; i < commandName.length + 3; i++) { + kwPad += ' '; + } + + for (let i = 0; i < commandArgs.length; i++) { + if (!commandArgs[i].realtype) { + console.log(commandName); + console.log(commandArgs[i]); + throw new Error('BUG: command info is missing a "realtype" field'); + } + if (i + 1 <= commandInfo.minItems) { + kwUsage += `--${commandArgs[i].name} ${commandArgs[i].realtype.toUpperCase()}`; + } + else { + kwUsage += `[--${commandArgs[i].name} ${commandArgs[i].realtype.toUpperCase()}]`; + } + kwUsage += '\n'; + kwUsage += kwPad; + } + + return {'raw': rawUsage, 'kw': kwUsage} as CLI_COMMAND_HELP; +} + +/* + * Get the set of commands grouped by command group + */ +interface CLI_COMMAND_GROUP_ITEM { + command: string; + help: string +} + +interface CLI_COMMAND_GROUP { + [index: string] : CLI_COMMAND_GROUP_ITEM[] +}; + +function getCommandGroups() : CLI_COMMAND_GROUP { + const groups : CLI_COMMAND_GROUP = {}; + const commands = Object.keys(CLI_ARGS.properties); + for (let i = 0; i < commands.length; i++) { + const command = commands[i]; + const group = CLI_ARGS.properties[command].group; + + if (!groups.hasOwnProperty(group)) { + groups[group] = [ + { + 'command': command, + 'help': CLI_ARGS.properties[command].help + } as CLI_COMMAND_GROUP_ITEM + ]; + } + else { + groups[group].push( + { + 'command': command, + 'help': CLI_ARGS.properties[command].help + } as CLI_COMMAND_GROUP_ITEM + ); + } + } + return groups; +} + +/* + * Make all commands list + */ +export function makeAllCommandsList() : string { + const groups = getCommandGroups(); + const groupNames = Object.keys(groups).sort(); + + let res = `All commands (run '${process.argv[1]} help COMMAND' for details):\n`; + for (let i = 0; i < groupNames.length; i++) { + res += ` ${groupNames[i]}: `; + const cmds = []; + for (let j = 0; j < groups[groupNames[i]].length; j++) { + cmds.push(groups[groupNames[i]][j].command); + } + + // wrap at 80 characters + const helpLineSpaces = formatHelpString(4, 70, cmds.join(' ')); + const helpLineCSV = ' ' + helpLineSpaces.split('\n ') + .map((line) => line.trim().replace(/ /g, ', ')).join('\n ') + '\n'; + + res += '\n' + helpLineCSV; + res += '\n'; + } + return res.trim(); +} + +/* + * Make help for all commands + */ +export function makeAllCommandsHelp(): string { + const groups = getCommandGroups(); + const groupNames = Object.keys(groups).sort(); + + const helps = []; + let cmds = []; + for (let i = 0; i < groupNames.length; i++) { + for (let j = 0; j < groups[groupNames[i]].length; j++) { + cmds.push(groups[groupNames[i]][j].command); + } + } + + cmds = cmds.sort(); + for (let i = 0; i < cmds.length; i++) { + helps.push(makeCommandUsageString(cmds[i]).trim()); + } + + return helps.join('\n\n'); +} + +/* + * Make a usage string for a single command + */ +export function makeCommandUsageString(command?: string) : string { + let res = ''; + if (command === 'all') { + return makeAllCommandsHelp(); + } + if (!command) { + return makeAllCommandsList(); + } + + const commandInfo = CLI_ARGS.properties[command]; + if (!commandInfo || command === 'help') { + return makeAllCommandsList(); + } + + const help = commandInfo.help; + + const cmdFormat = formatCommandHelpLines(command, commandInfo.items); + const formattedHelp = formatHelpString(2, 78, help); + + // make help string for one command + res += `Command: ${command}\n`; + res += 'Usage:\n'; + res += `${cmdFormat.raw}\n`; + res += `${cmdFormat.kw}\n`; + res += formattedHelp; + return res.trim() + '\n'; +} + +/* + * Make the usage documentation + */ +export function makeUsageString() : string { + let res = `${USAGE}\n\nCommand reference\n`; + const groups = getCommandGroups(); + const groupNames = Object.keys(groups).sort(); + + for (let i = 0; i < groupNames.length; i++) { + const groupName = groupNames[i]; + const groupCommands = groups[groupName]; + + res += `Command group: ${groupName}\n\n`; + for (let j = 0; j < groupCommands.length; j++) { + const command = groupCommands[j].command; + const help = groupCommands[j].help; + + const commandInfo = CLI_ARGS.properties[command]; + + const cmdFormat = formatCommandHelpLines(command, commandInfo.items); + const formattedHelp = formatHelpString(4, 76, help); + + res += cmdFormat.raw; + res += '\n'; + res += cmdFormat.kw; + res += '\n'; + res += formattedHelp; + res += '\n'; + } + res += '\n'; + } + + return res; +} + +/* + * Print usage + */ +export function printUsage() { + console.error(makeUsageString()); +} + +/* + * Implement just enough getopt(3) to be useful. + * Only handles short options. + * Returns an object whose keys are option flags that map to true/false, + * or to a value. + * The key _ is mapped to the non-opts list. + */ +interface CLI_OPTS { + [index: string]: null | boolean | string | string[] +}; + +export function getCLIOpts(argv: string[], + opts: string = 'deitUxC:F:B:P:D:G:N:H:T:I:m:M:X:u:p:') : CLI_OPTS { + const optsTable : CLI_OPTS = {}; + const remainingArgv = []; + const argvBuff = argv.slice(0); + + for (let i = 0; i < opts.length; i++) { + if (opts[i] == ':') { + continue; + } + if (i+1 < opts.length && opts[i+1] == ':') { + optsTable[opts[i]] = null; + } + else { + optsTable[opts[i]] = false; + } + } + + for (const opt of Object.keys(optsTable)) { + for (let i = 0; i < argvBuff.length; i++) { + if (argvBuff[i] === null) { + break; + } + if (argvBuff[i] === '--') { + break; + } + + const argvOpt = `-${opt}`; + if (argvOpt === argvBuff[i]) { + if (optsTable[opt] === false) { + // boolean switch + optsTable[opt] = true; + argvBuff[i] = ''; + } + else { + // argument + optsTable[opt] = argvBuff[i+1]; + argvBuff[i] = ''; + argvBuff[i+1] = ''; + } + } + } + } + + for (let i = 0; i < argvBuff.length; i++) { + if (argvBuff[i].length > 0) { + if (argvBuff[i] === '--') { + continue; + } + remainingArgv.push(argvBuff[i]); + } + } + + optsTable['_'] = remainingArgv; + return optsTable; +} + +export function CLIOptAsString(opts: CLI_OPTS, key: string) : string | null { + if (opts[key] === null || opts[key] === undefined) { + return null; + } + else if (typeof opts[key] === 'string') { + return `${opts[key]}`; + } + else { + throw new Error(`Option '${key}' is not a string`); + } +} + +export function CLIOptAsBool(opts: CLI_OPTS, key: string) : boolean { + if (typeof opts[key] === 'boolean' || opts[key] === null) { + return !!opts[key]; + } + else { + throw new Error(`Option '${key}' is not a boolean`); + } +} + +function isStringArray(value: any): value is string[] { + if (value instanceof Array) { + return value + .map((s: any) => typeof s === 'string') + .reduce((x: boolean, y: boolean) => x && y, true); + } + else { + return false; + } +} + +export function CLIOptAsStringArray(opts: CLI_OPTS, key: string) : string[] | null { + const value : any = opts[key]; + if (value === null || value === undefined) { + return null; + } + else if (isStringArray(value)) { + return value; + } + else { + throw new Error(`Option '${key}' is not a string array`); + } +} + + +/* + * Use the CLI schema to get all positional and keyword args + * for a given command. + */ +export function getCommandArgs(command: string, argsList: Array) { + let commandProps = CLI_ARGS.properties[command].items; + if (!Array.isArray(commandProps)) { + commandProps = [commandProps]; + } + + const orderedArgs = []; + const foundArgs : Record = {}; + + // scan for keywords + for (let i = 0; i < argsList.length; i++) { + if (argsList[i].startsWith('--')) { + // keyword argument + const argName = argsList[i].slice(2); + let argValue = null; + + // dup? + if (foundArgs.hasOwnProperty(argName)) { + return { + 'status': false, + 'error': `duplicate argument ${argsList[i]}` + }; + } + + for (let j = 0; j < commandProps.length; j++) { + if (!commandProps[j].hasOwnProperty('name')) { + continue; + } + if (commandProps[j].name === argName) { + // found! + // end of args? + if (i + 1 >= argsList.length) { + return { + 'status': false, + 'error': `no value for argument ${argsList[i]}` + }; + } + + argValue = argsList[i+1]; + } + } + + if (argValue) { + // found an argument given as a keyword + i += 1; + foundArgs[argName] = argValue; + } + else { + return { + 'status': false, + 'error': `no such argument ${argsList[i]}` + }; + } + } + else { + // positional argument + orderedArgs.push(argsList[i]); + } + } + + // merge foundArgs and orderedArgs back into an ordered argument list + // that is conformant to the CLI specification. + const mergedArgs = []; + let orderedArgIndex = 0; + + for (let i = 0; i < commandProps.length; i++) { + if (orderedArgIndex < orderedArgs.length) { + if (!commandProps[i].hasOwnProperty('name')) { + // unnamed positional argument + mergedArgs.push(orderedArgs[orderedArgIndex]); + orderedArgIndex += 1; + } + else if (!foundArgs.hasOwnProperty(commandProps[i].name)) { + // named positional argument, NOT given as a keyword + mergedArgs.push(orderedArgs[orderedArgIndex]); + orderedArgIndex += 1; + } + else { + // keyword argument + mergedArgs.push(foundArgs[commandProps[i].name]); + } + } + else { + // keyword argument (possibly undefined) + mergedArgs.push(foundArgs[commandProps[i].name]); + } + } + + return { + 'status': true, + 'arguments': mergedArgs + }; +} + +/* + * Check command args + */ +export interface CheckArgsSuccessType { + success: true; + command: string; + args: Array +}; + +export interface CheckArgsFailType { + success: false; + error: string; + command: string; + usage: boolean +}; + +export function checkArgs(argList: Array) + : CheckArgsSuccessType | CheckArgsFailType { + if (argList.length <= 2) { + return { + 'success': false, + 'error': 'No command given', + 'usage': true, + 'command': '' + }; + } + + const commandName = argList[2]; + const allCommandArgs = argList.slice(3); + + if (!CLI_ARGS.properties.hasOwnProperty(commandName)) { + return { + 'success': false, + 'error': `Unrecognized command '${commandName}'`, + 'usage': true, + 'command': commandName + }; + } + + const parsedCommandArgs = getCommandArgs(commandName, allCommandArgs); + if (!parsedCommandArgs.status) { + return { + 'success': false, + 'error': parsedCommandArgs.error, + 'usage': true, + 'command': commandName + }; + } + + const commandArgs = parsedCommandArgs.arguments; + + // validate all required commands as given. + // if there are optional commands, then only validate + // them if they're given. + const commandSchema = JSON.parse(JSON.stringify(CLI_ARGS.properties[commandName])); + for (let i = commandSchema.minItems; i < commandSchema.maxItems; i++) { + if (i < commandArgs.length) { + if (commandArgs[i] === null || commandArgs[i] === undefined) { + // optional argument not given. Update the schema we're checking against + // to expect this. + commandArgs[i] = null; + commandSchema.items[i] = { type: 'null' }; + } + } + } + + const ajv = Ajv(); + const valid = ajv.validate(commandSchema, commandArgs); + if (!valid) { + let errorMsg = ''; + for (let i = 0; i < ajv.errors.length; i++) { + const msg = `Invalid command arguments: Schema "${ajv.errors[0].schemaPath}" failed validation (problem: "${ajv.errors[0].message}", cause: "${JSON.stringify(ajv.errors[0].params)}")\n`; + errorMsg += msg; + } + return { + 'success': false, + 'error': errorMsg, + 'usage': true, + 'command': commandName + }; + } + + return { + 'success': true, + 'command': commandName, + 'args': commandArgs + }; +} + +/** + * Load the config file and return a config dict. + * If no config file exists, then return the default config. + * + * @configPath (string) the path to the config file. + * @networkType (sring) 'mainnet', 'regtest', or 'testnet' + */ +export function loadConfig(configFile: string, networkType: string) : CLI_CONFIG_TYPE { + if (networkType !== 'mainnet' && networkType !== 'testnet' && networkType != 'regtest') { + throw new Error('Unregognized network'); + } + + let configRet : CLI_CONFIG_TYPE; + + if (networkType === 'mainnet') { + configRet = Object.assign({}, CONFIG_DEFAULTS); + } else if (networkType === 'regtest') { + configRet = Object.assign({}, CONFIG_REGTEST_DEFAULTS); + } else { + configRet = Object.assign({}, CONFIG_TESTNET_DEFAULTS); + } + + try { + configRet = JSON.parse(fs.readFileSync(configFile).toString()) as CLI_CONFIG_TYPE; + } + catch (e) { + ; + } + + return configRet; +} + diff --git a/packages/cli/src/auth.ts b/packages/cli/src/auth.ts new file mode 100644 index 000000000..b538197f9 --- /dev/null +++ b/packages/cli/src/auth.ts @@ -0,0 +1,706 @@ +import * as blockstack from 'blockstack'; +import * as express from 'express'; +import * as crypto from 'crypto'; +import * as jsontokens from 'jsontokens'; +import * as logger from 'winston'; + +import { + gaiaConnect, + gaiaUploadProfileAll, + makeAssociationToken, + getGaiaAddressFromProfile +} from './data'; + +import { + getApplicationKeyInfo, + getOwnerKeyInfo, + extractAppKey +} from './keys'; + +import { + nameLookup, + makeProfileJWT +} from './utils'; + +import { + CLINetworkAdapter +} from './network'; + +import { + GaiaHubConfig +} from 'blockstack/lib/storage/hub'; + +export const SIGNIN_CSS = ` +h1 { + font-family: monospace; + font-size: 24px; + font-style: normal; + font-variant: normal; + font-weight: 700; + line-height: 26.4px; +} +h3 { + font-family: monospace; + font-size: 14px; + font-style: normal; + font-variant: normal; + font-weight: 700; + line-height: 15.4px; +} +p { + font-family: monospace; + font-size: 14px; + font-style: normal; + font-variant: normal; + font-weight: 400; + line-height: 20px; +} +b { + background-color: #e8e8e8; +} +pre { + font-family: monospace; + font-size: 13px; + font-style: normal; + font-variant: normal; + font-weight: 400; + line-height: 18.5714px; +}`; + +export const SIGNIN_HEADER = `

Blockstack CLI Sign-in


`; +export const SIGNIN_DESC = '

Sign-in request for "{appName}"

'; +export const SIGNIN_SCOPES = '

Requested scopes: "{appScopes}"

'; +export const SIGNIN_FMT_NAME = '

{blockstackID} ({idAddress})

'; +export const SIGNIN_FMT_ID = '

{idAddress} (anonymous)

'; +export const SIGNIN_FOOTER = ''; + +export interface NamedIdentityType { + name: string; + idAddress: string; + privateKey: string; + index: number; + profile: Object; + profileUrl: string +}; + +interface AuthRequestType { + jti: string; + iat: number; + exp: number; + iss: null | string; + public_keys: string[]; + domain_name: string; + manifest_uri: string; + redirect_uri: string; + version: string; + do_not_include_profile: boolean; + supports_hub_url: boolean; + scopes: string[] +}; + + +// new ecdsa private key each time +const authTransitNonce = crypto.randomBytes(32).toString('hex'); + + +/* + * Get the app private key + */ +async function getAppPrivateKey(network: CLINetworkAdapter, + mnemonic: string, + id: NamedIdentityType, + appOrigin: string +): Promise { + const appKeyInfo = await getApplicationKeyInfo(network, mnemonic, id.idAddress, appOrigin, id.index); + let appPrivateKey; + try { + const existingAppAddress = getGaiaAddressFromProfile(network, id.profile, appOrigin); + appPrivateKey = extractAppKey(network, appKeyInfo, existingAppAddress); + } + catch (e) { + appPrivateKey = extractAppKey(network, appKeyInfo); + } + + return appPrivateKey; +} + +/* + * Make a sign-in link + */ +async function makeSignInLink(network: CLINetworkAdapter, + authPort: number, + mnemonic: string, + authRequest: AuthRequestType, + hubUrl: string, + id: NamedIdentityType) : Promise { + + const appOrigin = authRequest.domain_name; + const appPrivateKey = await getAppPrivateKey(network, mnemonic, id, appOrigin); + + const associationToken = makeAssociationToken(appPrivateKey, id.privateKey); + const authResponseTmp = blockstack.makeAuthResponse( + id.privateKey, + {}, + id.name, + { email: null, profileUrl: id.profileUrl }, + null, + appPrivateKey, + undefined, + authRequest.public_keys[0], + hubUrl, + blockstack.config.network.blockstackAPIUrl, + associationToken + ); + + // pass along some helpful data from the authRequest + const authResponsePayload = jsontokens.decodeToken(authResponseTmp).payload; + const id_public = Object.assign({}, id); + id_public.profile = {}; + id_public.privateKey = undefined; + + (authResponsePayload as any).metadata = { + id: id_public, + profileUrl: id.profileUrl, + appOrigin: appOrigin, + redirect_uri: authRequest.redirect_uri, + scopes: authRequest.scopes, + salt: crypto.randomBytes(16).toString('hex'), + nonce: authTransitNonce + // fill in more CLI-specific fields here + }; + + const tokenSigner = new jsontokens.TokenSigner('ES256k', id.privateKey); + const authResponse = tokenSigner.sign(authResponsePayload); + + return blockstack.updateQueryStringParameter( + `http://localhost:${authPort}/signin`, 'authResponse', authResponse); +} + +/* + * Make the sign-in page + */ +async function makeAuthPage(network: CLINetworkAdapter, + authPort: number, + mnemonic: string, + hubUrl: string, + manifest: any, + authRequest: AuthRequestType, + ids: Array +) : Promise { + + let signinBody = SIGNIN_HEADER; + const signinDescription = SIGNIN_DESC + .replace(/{appName}/, manifest.name || '(Unknown app)'); + + const signinScopes = SIGNIN_SCOPES + .replace(/{appScopes}/, authRequest.scopes.length > 0 + ? authRequest.scopes.join(', ') + : '(none)'); + + signinBody = `${signinBody}${signinDescription}${signinScopes}`; + + for (let i = 0; i < ids.length; i++) { + let signinEntry; + if (ids[i].name) { + signinEntry = SIGNIN_FMT_NAME + .replace(/{authRedirect}/, await makeSignInLink( + network, + authPort, + mnemonic, + authRequest, + hubUrl, + ids[i])) + .replace(/{blockstackID}/, ids[i].name) + .replace(/{idAddress}/, ids[i].idAddress); + } + else { + signinEntry = SIGNIN_FMT_ID + .replace(/{authRedirect}/, await makeSignInLink( + network, + authPort, + mnemonic, + authRequest, + hubUrl, + ids[i])) + .replace(/{idAddress}/, ids[i].idAddress); + } + + signinBody = `${signinBody}${signinEntry}`; + } + + signinBody = `${signinBody}${SIGNIN_FOOTER}`; + return signinBody; +} + + +/* + * Find all identity addresses that have names attached to them. + * Fills in identities. + */ +async function loadNamedIdentitiesLoop(network: CLINetworkAdapter, + mnemonic: string, + index: number, + identities: NamedIdentityType[]) : Promise { + + // 65536 is a ridiculously huge number + if (index > 65536) { + throw new Error('Too many names'); + } + + const keyInfo = await getOwnerKeyInfo(network, mnemonic, index); + const nameList = await network.getNamesOwned(keyInfo.idAddress.slice(3)); + if (nameList.length === 0) { + // out of names + return identities; + } + for (let i = 0; i < nameList.length; i++) { + const identity: NamedIdentityType = { + name: nameList[i], + idAddress: keyInfo.idAddress, + privateKey: keyInfo.privateKey, + index: index, + profile: {}, + profileUrl: '' + }; + identities.push(identity); + } + return await loadNamedIdentitiesLoop(network, mnemonic, index + 1, identities); +} + +/* + * Load all named identities for a mnemonic. + * Keep loading until we find an ID-address that does not have a name. + */ +export function loadNamedIdentities(network: CLINetworkAdapter, mnemonic: string) + : Promise> { + return loadNamedIdentitiesLoop(network, mnemonic, 0, []); +} + + +/* + * Generate identity info for an unnamed ID + */ +async function loadUnnamedIdentity(network: CLINetworkAdapter, mnemonic: string, index: number): Promise { + const keyInfo = await getOwnerKeyInfo(network, mnemonic, index); + const idInfo = { + name: '', + idAddress: keyInfo.idAddress, + privateKey: keyInfo.privateKey, + index: index, + profile: {}, + profileUrl: '' + }; + return idInfo; +} + +/* + * Send a JSON HTTP response + */ +function sendJSON(res: express.Response, data: Object, statusCode: number) { + logger.info(`Respond ${statusCode}: ${JSON.stringify(data)}`); + res.writeHead(statusCode, {'Content-Type' : 'application/json'}); + res.write(JSON.stringify(data)); + res.end(); +} + + +/* + * Get all of a 12-word phrase's identities, profiles, and Gaia connections. + * Returns a Promise to an Array of NamedIdentityType instances. + * + * NOTE: should be the *only* promise chain running! + */ +async function getIdentityInfo(network: CLINetworkAdapter, mnemonic: string, _appGaiaHub: string, _profileGaiaHub: string) + : Promise { + + network.setCoerceMainnetAddress(true); // for lookups in regtest + let identities : NamedIdentityType[]; + + try { + // load up all of our identity addresses and profile URLs + identities = await loadNamedIdentities(network, mnemonic); + const nameInfoPromises = identities.map(id => { + const lookup: Promise<{ profile: any, profileUrl?: string, zonefile?: string } | null> = + nameLookup(network, id.name, true).catch(() => null); + return lookup; + }); + + let nameDatas = await Promise.all(nameInfoPromises); + + network.setCoerceMainnetAddress(false); + nameDatas = nameDatas.filter((p) => p !== null && p !== undefined); + + for (let i = 0; i < nameDatas.length; i++) { + if (nameDatas[i].hasOwnProperty('error') && (nameDatas[i] as any).error) { + // no data for this name + identities[i].profileUrl = ''; + } + else { + identities[i].profileUrl = nameDatas[i].profileUrl; + identities[i].profile = nameDatas[i].profile; + } + } + + const nextIndex = identities.length + 1; + + // ignore identities with no data + identities = identities.filter((id) => !!id.profileUrl); + + // add in the next non-named identity + identities.push(await loadUnnamedIdentity(network, mnemonic, nextIndex)); + + } catch(e) { + network.setCoerceMainnetAddress(false); + throw e; + } + + return identities; +} + + +/* + * Handle GET /auth?authRequest=... + * If the authRequest is verifiable and well-formed, and if we can fetch the application + * manifest, then we can render an auth page to the user. + * Serves back the sign-in page on success. + * Serves back an error page on error. + * Returns a Promise that resolves to nothing. + * + * NOTE: should be the *only* promise chain running! + */ +export async function handleAuth(network: CLINetworkAdapter, + mnemonic: string, + gaiaHubUrl: string, + profileGaiaHub: string, + port: number, + req: express.Request, + res: express.Response +) : Promise { + + const authToken = req.query.authRequest as string; + if (!authToken) { + return Promise.resolve().then(() => { + sendJSON(res, { error: 'No authRequest given' }, 400); + }); + } + + let errorMsg = ''; + let identities : NamedIdentityType[] = []; + + try { + identities = await getIdentityInfo(network, mnemonic, gaiaHubUrl, profileGaiaHub); + + errorMsg = 'Unable to verify authentication token'; + const valid = await blockstack.verifyAuthRequest(authToken); + + if (!valid) { + errorMsg = 'Invalid authentication token: could not verify'; + throw new Error(errorMsg); + } + errorMsg = 'Unable to fetch app manifest'; + const appManifest = await blockstack.fetchAppManifest(authToken); + + errorMsg = 'Unable to decode token'; + const decodedAuthToken = jsontokens.decodeToken(authToken); + const decodedAuthPayload = decodedAuthToken.payload; + if (!decodedAuthPayload) { + errorMsg = 'Invalid authentication token: no payload'; + throw new Error(errorMsg); + } + + errorMsg = 'Unable to make auth page'; + + // make sign-in page + const authPage = await makeAuthPage( + network, port, mnemonic, gaiaHubUrl, appManifest, decodedAuthPayload as AuthRequestType, identities); + + res.writeHead(200, {'Content-Type': 'text/html', 'Content-Length': authPage.length}); + res.write(authPage); + res.end(); + } catch (e) { + if (!errorMsg) { + errorMsg = e.message; + } + + console.log(e.stack); + logger.error(errorMsg); + sendJSON(res, { error: `Unable to authenticate app request: ${errorMsg}` }, 400); + } +} + +/* + * Update a named identity's profile with new app data, if necessary. + * Indicates whether or not the profile was changed. + */ +function updateProfileApps(network: CLINetworkAdapter, + id: NamedIdentityType, + appOrigin: string, + appGaiaConfig: GaiaHubConfig, + profile?: any +): Promise<{ profile: any, changed: boolean }> { + + let needProfileUpdate = false; + + // go get the profile from the profile URL in the id + const profilePromise = Promise.resolve().then(() => { + if (profile === null || profile === undefined) { + return nameLookup(network, id.name) + .catch((_e) => null); + } else { + return { profile: profile }; + } + }); + + return profilePromise.then((profileData) => { + if (profileData) { + profile = profileData.profile; + } + + if (!profile) { + // instantiate + logger.debug(`Profile for ${id.name} is ${JSON.stringify(profile)}`); + logger.debug(`Instantiating profile for ${id.name}`); + needProfileUpdate = true; + profile = { + 'type': '@Person', + 'account': [], + 'apps': {} + }; + } + + // do we need to update the Gaia hub read URL in the profile? + if (profile.apps === null || profile.apps === undefined) { + needProfileUpdate = true; + + logger.debug(`Adding multi-reader Gaia links to profile for ${id.name}`); + profile.apps = {}; + } + + const gaiaPrefix = `${appGaiaConfig.url_prefix}${appGaiaConfig.address}/`; + + if (!profile.apps.hasOwnProperty(appOrigin) || !profile.apps[appOrigin]) { + needProfileUpdate = true; + logger.debug(`Setting Gaia read URL ${gaiaPrefix} for ${appOrigin} ` + + `in profile for ${id.name}`); + + profile.apps[appOrigin] = gaiaPrefix; + } + else if (!profile.apps[appOrigin].startsWith(gaiaPrefix)) { + needProfileUpdate = true; + logger.debug(`Overriding Gaia read URL for ${appOrigin} from ${profile.apps[appOrigin]} ` + + `to ${gaiaPrefix} in profile for ${id.name}`); + + profile.apps[appOrigin] = gaiaPrefix; + } + + return { profile, changed: needProfileUpdate }; + }); +} + +/* + * Updates a named identitie's profile's API settings, if necessary. + * Indicates whether or not the profile data changed. + */ +function updateProfileAPISettings(network: CLINetworkAdapter, + id: NamedIdentityType, + appGaiaConfig: GaiaHubConfig, + profile?: any +): Promise<{ profile: any, changed: boolean }> { + + let needProfileUpdate = false; + + // go get the profile from the profile URL in the id + const profilePromise = Promise.resolve().then(() => { + if (profile === null || profile === undefined) { + return nameLookup(network, id.name) + .catch((_e) => null); + } + else { + return { profile: profile }; + } + }); + + return profilePromise.then((profileData) => { + if (profileData) { + profile = profileData.profile; + } + + if (!profile) { + // instantiate + logger.debug(`Profile for ${id.name} is ${JSON.stringify(profile)}`); + logger.debug(`Instantiating profile for ${id.name}`); + needProfileUpdate = true; + profile = { + 'type': '@Person', + 'account': [], + 'api': {} + }; + } + + // do we need to update the API settings in the profile? + if (profile.api === null || profile.api === undefined) { + needProfileUpdate = true; + + logger.debug(`Adding API settings to profile for ${id.name}`); + profile.api = { + gaiaHubConfig: { + url_prefix: appGaiaConfig.url_prefix + }, + gaiaHubUrl: appGaiaConfig.server + }; + } + + if (!profile.hasOwnProperty('api') || !profile.api.hasOwnProperty('gaiaHubConfig') || + !profile.api.gaiaHubConfig.hasOwnProperty('url_prefix') || !profile.api.gaiaHubConfig.url_prefix || + !profile.api.hasOwnProperty('gaiaHubUrl') || !profile.api.gaiaHubUrl) { + + logger.debug(`Existing profile for ${id.name} is ${JSON.stringify(profile)}`); + logger.debug(`Updating API settings to profile for ${id.name}`); + profile.api = { + gaiaHubConfig: { + url_prefix: appGaiaConfig.url_prefix + }, + gaiaHubUrl: appGaiaConfig.server + }; + } + + return { profile, changed: needProfileUpdate }; + }); +} + +/* + * Handle GET /signin?encAuthResponse=... + * Takes an encrypted authResponse from the page generated on GET /auth?authRequest=...., + * verifies it, updates the name's profile's app's entry with the latest Gaia + * hub information (if necessary), and redirects the user back to the application. + * + * If adminKey is given, then the new app private key will be automatically added + * as an authorized writer to the Gaia hub. + * + * Redirects the user on success. + * Sends the user an error page on failure. + * Returns a Promise that resolves to nothing. + */ +export async function handleSignIn(network: CLINetworkAdapter, + mnemonic: string, + appGaiaHub: string, + profileGaiaHub: string, + req: express.Request, + res: express.Response +): Promise { + + const authResponseQP = req.query.authResponse as string; + if (!authResponseQP) { + return Promise.resolve().then(() => { + sendJSON(res, { error: 'No authResponse given' }, 400); + }); + } + const nameLookupUrl = `${network.blockstackAPIUrl}/v1/names/`; + + let errorMsg = ''; + let errorStatusCode = 400; + let authResponsePayload : any; + + let id : NamedIdentityType; + let profileUrl : string; + let appOrigin : string; + let redirectUri : string; + let scopes : string[]; + let authResponse : string; + let hubConfig : GaiaHubConfig; + let needProfileAPIUpdate = false; + let profileAPIUpdate : boolean; + + try { + const valid = await blockstack.verifyAuthResponse(authResponseQP, nameLookupUrl); + if (!valid) { + errorMsg = `Unable to verify authResponse token ${authResponseQP}`; + throw new Error(errorMsg); + } + + const authResponseToken = jsontokens.decodeToken(authResponseQP); + authResponsePayload = authResponseToken.payload; + + id = authResponsePayload.metadata.id; + profileUrl = authResponsePayload.metadata.profileUrl; + appOrigin = authResponsePayload.metadata.appOrigin; + redirectUri = authResponsePayload.metadata.redirect_uri; + scopes = authResponsePayload.metadata.scopes; + const nonce = authResponsePayload.metadata.nonce; + + if (nonce != authTransitNonce) { + throw new Error('Invalid auth response: not generated by this authenticator'); + } + + // restore + id.privateKey = (await getOwnerKeyInfo(network, mnemonic, id.index)).privateKey; + + const appPrivateKey = await getAppPrivateKey(network, mnemonic, id, appOrigin); + + // remove sensitive (CLI-specific) information + authResponsePayload.metadata = { + profileUrl: profileUrl + }; + + authResponse = new jsontokens.TokenSigner('ES256K', id.privateKey).sign(authResponsePayload); + + logger.debug(`App ${appOrigin} requests scopes ${JSON.stringify(scopes)}`); + + // connect to the app gaia hub + const appHubConfig = await gaiaConnect(network, appGaiaHub, appPrivateKey); + + hubConfig = appHubConfig; + let newProfileData = await updateProfileAPISettings(network, id, hubConfig); + + needProfileAPIUpdate = newProfileData.changed; + profileAPIUpdate = newProfileData.profile; + newProfileData = await updateProfileApps(network, id, appOrigin, hubConfig, profileAPIUpdate); + + const profile = newProfileData.profile; + const needProfileSigninUpdate = newProfileData.changed && scopes.includes('store_write'); + + logger.debug(`Resulting profile for ${id.name} is ${JSON.stringify(profile)}`); + + let gaiaUrls: any; + + // sign and replicate new profile if we need to. + // otherwise do nothing + if (needProfileSigninUpdate) { + logger.debug(`Upload new profile with new sign-in data to ${profileGaiaHub}`); + const profileJWT = makeProfileJWT(profile, id.privateKey); + gaiaUrls = await gaiaUploadProfileAll( + network, [profileGaiaHub], profileJWT, id.privateKey, id.name); + } + else if (needProfileAPIUpdate) { + // API settings changed, but we won't be adding an app entry + logger.debug(`Upload new profile with new API settings to ${profileGaiaHub}`); + const profileJWT = makeProfileJWT(profileAPIUpdate, id.privateKey); + gaiaUrls = await gaiaUploadProfileAll( + network, [profileGaiaHub], profileJWT, id.privateKey, id.name); + } + else { + logger.debug(`Gaia read URL for ${appOrigin} is ${profile.apps[appOrigin]}`); + gaiaUrls = { dataUrls: [], error: null }; + } + + if (gaiaUrls.hasOwnProperty('error') && gaiaUrls.error) { + errorMsg = `Failed to upload new profile: ${gaiaUrls.error}`; + errorStatusCode = 502; + throw new Error(errorMsg); + } + + // success! + // redirect to application + logger.debug(`Handled sign-in to ${appOrigin} using ${id.name}`); + const appUri = blockstack.updateQueryStringParameter(redirectUri, 'authResponse', authResponse); + + logger.info(`Redirect to ${appUri}`); + res.writeHead(302, {'Location': appUri}); + res.end(); + + } catch(e) { + logger.error(e); + logger.error(errorMsg); + sendJSON(res, { error: `Unable to process signin request: ${errorMsg}` }, errorStatusCode); + } +} diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts new file mode 100644 index 000000000..a7d8a30d6 --- /dev/null +++ b/packages/cli/src/cli.ts @@ -0,0 +1,3783 @@ +import * as blockstack from 'blockstack'; +import * as bitcoin from 'bitcoinjs-lib'; +import * as process from 'process'; +import * as fs from 'fs'; +import * as winston from 'winston'; +import * as logger from 'winston'; +import * as cors from 'cors'; +import * as RIPEMD160 from 'ripemd160'; +const BN = require('bn.js'); +import * as crypto from 'crypto'; +import * as bip39 from 'bip39'; +import * as express from 'express'; +import * as path from 'path'; +import * as inquirer from 'inquirer'; +import fetch from 'node-fetch'; +import { + makeSTXTokenTransfer, + makeContractDeploy, + makeContractCall, + callReadOnlyFunction, + broadcastTransaction, + estimateTransfer, + estimateContractDeploy, + estimateContractFunctionCall, + StacksMainnet, + StacksTestnet, + TokenTransferOptions, + ContractDeployOptions, + ContractCallOptions, + ReadOnlyFunctionOptions, + ContractCallPayload, + ClarityValue, + ClarityAbi, + getAbi, + validateContractCall, + PostConditionMode, + cvToString, +} from '@blockstack/stacks-transactions'; + +const c32check = require('c32check'); + +import { + UserData +} from 'blockstack/lib/auth/authApp'; + +import { + GaiaHubConfig +} from 'blockstack/lib/storage/hub'; + +import { + getOwnerKeyInfo, + getPaymentKeyInfo, + getStacksWalletKeyInfo, + getApplicationKeyInfo, + extractAppKey, + STRENGTH, + STX_WALLET_COMPATIBLE_SEED_STRENGTH, + PaymentKeyInfoType, + OwnerKeyInfoType, + StacksKeyInfoType +} from './keys'; + +import { + CLI_ARGS, + getCLIOpts, + CLIOptAsString, + CLIOptAsStringArray, + CLIOptAsBool, + checkArgs, + loadConfig, + makeCommandUsageString, + makeAllCommandsList, + USAGE, + DEFAULT_CONFIG_PATH, + DEFAULT_CONFIG_REGTEST_PATH, + DEFAULT_CONFIG_TESTNET_PATH, + ID_ADDRESS_PATTERN, + STACKS_ADDRESS_PATTERN, + DEFAULT_MAX_ID_SEARCH_INDEX +} from './argparse'; + +import { + encryptBackupPhrase, + decryptBackupPhrase +} from './encrypt'; + +import { + CLINetworkAdapter, + CLI_NETWORK_OPTS, + getNetwork, + NameInfoType, + PriceType +} from './network'; + +import { + gaiaAuth, + gaiaConnect, + gaiaUploadProfileAll, + makeZoneFileFromGaiaUrl, + getGaiaAddressFromProfile +} from './data'; + +import { + SafetyError, + JSONStringify, + getPrivateKeyAddress, + canonicalPrivateKey, + sumUTXOs, + hash160, + checkUrl, + decodePrivateKey, + makeProfileJWT, + broadcastTransactionAndZoneFile, + getNameInfoEasy, + nameLookup, + getpass, + getBackupPhrase, + mkdirs, + getIDAddress, + IDAppKeys, + getIDAppKeys, + hasKeys, + UTXO, + makeDIDConfiguration, + makePromptsFromArgList, + parseClarityFunctionArgAnswers, + ClarityFunctionArg, + generateExplorerTxPageUrl +} from './utils'; + +import { + handleAuth, + handleSignIn +} from './auth'; + +// global CLI options +let txOnly = false; +let estimateOnly = false; +let safetyChecks = true; +let receiveFeesPeriod = 52595; +let gracePeriod = 5000; +let noExit = false; +let maxIDSearchIndex = DEFAULT_MAX_ID_SEARCH_INDEX; + +let BLOCKSTACK_TEST = process.env.BLOCKSTACK_TEST ? true : false; + +export function getMaxIDSearchIndex() { + return maxIDSearchIndex; +} + +export interface WhoisInfoType { + address: string; + blockchain: string; + block_renewed_at: number; + did: string; + expire_block: number; + grace_period: number; + last_transaction_height: number; + last_txid: string; + owner_address: string; + owner_script: string; + renewal_deadline: number; + resolver: string | null; + status: string; + zonefile: string | null; + zonefile_hash: string | null; +} + +/* + * Get a name's record information + * args: + * @name (string) the name to query + */ +function whois(network: CLINetworkAdapter, args: string[]) : Promise { + const name = args[0]; + return network.getNameInfo(name) + .then((nameInfo : NameInfoType) => { + if (BLOCKSTACK_TEST) { + // the test framework expects a few more fields. + // these are for compatibility with the old CLI. + // you are not required to understand them. + return Promise.all([network.getNameHistory(name, 0), network.getBlockHeight()]) + .then(([nameHistory, blockHeight] : [any, number]) => { + if (nameInfo.renewal_deadline > 0 && nameInfo.renewal_deadline <= blockHeight) { + return {'error': 'Name expired'}; + } + + const blocks : string[] = Object.keys(nameHistory); + const lastBlock : number = parseInt(blocks.sort().slice(-1)[0]); + const blockRenewedAt : number = parseInt(nameHistory[lastBlock].slice(-1)[0].last_renewed); + const ownerScript = bitcoin.address.toOutputScript( + network.coerceMainnetAddress(nameInfo.address)).toString('hex'); + + const whois : WhoisInfoType = { + address: nameInfo.address, + blockchain: nameInfo.blockchain, + block_renewed_at: blockRenewedAt, + did: nameInfo.did, + expire_block: nameInfo.expire_block, + grace_period: nameInfo.grace_period, + last_transaction_height: lastBlock, + last_txid: nameInfo.last_txid, + owner_address: nameInfo.address, + owner_script: ownerScript, + renewal_deadline: nameInfo.renewal_deadline, + resolver: nameInfo.resolver, + status: nameInfo.status, + zonefile: nameInfo.zonefile, + zonefile_hash: nameInfo.zonefile_hash + }; + return whois; + }) + .then((whoisInfo : any) => JSONStringify(whoisInfo, true)); + } + else { + return Promise.resolve().then(() => JSONStringify(nameInfo, true)); + } + }) + .catch((error : Error) => { + if (error.message === 'Name not found') { + return JSONStringify({'error': 'Name not found'}, true); + } + else { + throw error; + } + }); +} + +/* + * Get a name's price information + * args: + * @name (string) the name to query + */ +function price(network: CLINetworkAdapter, args: string[]) : Promise { + const name = args[0]; + return network.getNamePrice(name) + .then((priceInfo : PriceType) => JSONStringify( + { units: priceInfo.units, amount: priceInfo.amount.toString() })); +} + +/* + * Get a namespace's price information + * args: + * @namespaceID (string) the namespace to query + */ +function priceNamespace(network: CLINetworkAdapter, args: string[]) : Promise { + const namespaceID = args[0]; + return network.getNamespacePrice(namespaceID) + .then((priceInfo : PriceType) => JSONStringify( + { units: priceInfo.units, amount: priceInfo.amount.toString() })); +} + +/* + * Get names owned by an address + * args: + * @address (string) the address to query + */ +function names(network: CLINetworkAdapter, args: string[]) : Promise { + const IDaddress = args[0]; + if (!IDaddress.startsWith('ID-')) { + throw new Error('Must be an ID-address'); + } + + const address = IDaddress.slice(3); + return network.getNamesOwned(address) + .then((namesList : string[]) => JSONStringify(namesList)); +} + +/* + * Look up a name's profile and zonefile + * args: + * @name (string) the name to look up + */ +function lookup(network: CLINetworkAdapter, args: string[]) : Promise { + network.setCoerceMainnetAddress(true); + + const name = args[0]; + return nameLookup(network, name) + .then((nameLookupInfo) => JSONStringify(nameLookupInfo)) + .catch((e : Error) => JSONStringify({ error: e.message })); +} + +/* + * Get a name's blockchain record + * args: + * @name (string) the name to query + */ +function getNameBlockchainRecord(network: CLINetworkAdapter, args: string[]) : Promise { + const name = args[0]; + return Promise.resolve().then(() => { + return network.getBlockchainNameRecord(name); + }) + .then((nameInfo : any) => { + return JSONStringify(nameInfo); + }) + .catch((e : Error) => { + if (e.message === 'Bad response status: 404') { + return JSONStringify({ 'error': 'Name not found'}, true); + } + else { + throw e; + } + }); +} + +/* + * Get all of a name's history + */ +async function getAllNameHistoryPages(network: CLINetworkAdapter, name: string, page: number) { + let history = {}; + try { + const results = await network.getNameHistory(name, page); + if (Object.keys(results).length == 0) { + return history; + } + else { + history = Object.assign(history, results); + const rest = await getAllNameHistoryPages(network, name, page + 1); + history = Object.assign(history, rest); + return history; + } + } + catch (_e) { + return history; + } +} + +/* + * Get a name's history entry or entries + * args: + * @name (string) the name to query + * @page (string) the page to query (OPTIONAL) + */ +function getNameHistoryRecord(network: CLINetworkAdapter, args: string[]) : Promise { + const name = args[0]; + let page : number; + + if (args.length >= 2 && args[1] !== null && args[1] !== undefined) { + page = parseInt(args[1]); + return Promise.resolve().then(() => { + return network.getNameHistory(name, page); + }) + .then((nameHistory : any) => { + return JSONStringify(nameHistory); + }); + } + else { + // all pages + return getAllNameHistoryPages(network, name, 0) + .then((history : any) => JSONStringify(history)); + } +} + +/* + * Get a namespace's blockchain record + * args: + * @namespaceID (string) the namespace to query + */ +function getNamespaceBlockchainRecord(network: CLINetworkAdapter, args: string[]) : Promise { + const namespaceID = args[0]; + return Promise.resolve().then(() => { + return network.getNamespaceInfo(namespaceID); + }) + .then((namespaceInfo : any) => { + return JSONStringify(namespaceInfo); + }) + .catch((e : Error) => { + if (e.message === 'Namespace not found') { + return JSONStringify({'error': 'Namespace not found'}, true); + } + else { + throw e; + } + }); +} + +/* + * Get a zone file by hash. + * args: + * @zonefile_hash (string) the hash of the zone file to query + */ +function getZonefile(network: CLINetworkAdapter, args: string[]) : Promise { + const zonefileHash = args[0]; + return network.getZonefile(zonefileHash); +} + +/* + * Generate and optionally send a name-preorder + * args: + * @name (string) the name to preorder + * @IDaddress (string) the address to own the name + * @paymentKey (string) the payment private key + * @preorderTxOnly (boolean) OPTIONAL: used internally to only return a tx (overrides CLI) + */ +function txPreorder(network: CLINetworkAdapter, args: string[], preorderTxOnly: boolean = false) : Promise { + const name = args[0]; + const IDaddress = args[1]; + const paymentKey = decodePrivateKey(args[2]); + const paymentAddress = getPrivateKeyAddress(network, paymentKey); + + if (!IDaddress.startsWith('ID-')) { + throw new Error('Recipient ID-address must start with ID-'); + } + const address = IDaddress.slice(3); + + const namespaceID = name.split('.').slice(-1)[0]; + + const txPromise = blockstack.transactions.makePreorder( + name, address, paymentKey, !hasKeys(paymentKey)); + + const paymentUTXOsPromise = network.getUTXOs(paymentAddress); + + const estimatePromise = paymentUTXOsPromise.then((utxos : UTXO[]) => { + const numUTXOs = utxos.length; + return blockstack.transactions.estimatePreorder( + name, network.coerceAddress(address), + network.coerceAddress(paymentAddress), numUTXOs); + }); + + if (estimateOnly) { + return estimatePromise + .then((cost: number) => JSONStringify({cost: cost})); + } + + if (!safetyChecks) { + if (txOnly || preorderTxOnly) { + return txPromise; + } + else { + return txPromise + .then((tx) => { + return network.broadcastTransaction(tx); + }); + } + } + + const paymentBalance = paymentUTXOsPromise.then((utxos : UTXO[]) => { + return sumUTXOs(utxos); + }); + + const nameInfoPromise = getNameInfoEasy(network, name); + const blockHeightPromise = network.getBlockHeight(); + + const safetyChecksPromise = Promise.all([ + nameInfoPromise, + blockHeightPromise, + blockstack.safety.isNameValid(name), + blockstack.safety.isNameAvailable(name), + blockstack.safety.addressCanReceiveName(network.coerceAddress(address)), + blockstack.safety.isInGracePeriod(name), + network.getNamespaceBurnAddress(namespaceID, true, receiveFeesPeriod), + network.getNamespaceBurnAddress(namespaceID, false, receiveFeesPeriod), + paymentBalance, + estimatePromise, + blockstack.safety.namespaceIsReady(namespaceID), + network.getNamePrice(name), + network.getAccountBalance(paymentAddress, 'STACKS') + ]) + .then(([nameInfo, + _blockHeight, + isNameValid, + isNameAvailable, + addressCanReceiveName, + isInGracePeriod, + givenNamespaceBurnAddress, + trueNamespaceBurnAddress, + paymentBalance, + estimate, + isNamespaceReady, + namePrice, + STACKSBalance]) => { + if (isNameValid && isNamespaceReady && + (isNameAvailable || !nameInfo) && + addressCanReceiveName && !isInGracePeriod && paymentBalance >= estimate && + trueNamespaceBurnAddress === givenNamespaceBurnAddress && + (namePrice.units === 'BTC' || (namePrice.units == 'STACKS' + && namePrice.amount.cmp(STACKSBalance) <= 0))) { + return {'status': true}; + } + else { + return { + 'status': false, + 'error': 'Name cannot be safely preordered', + 'isNameValid': isNameValid, + 'isNameAvailable': isNameAvailable, + 'addressCanReceiveName': addressCanReceiveName, + 'isInGracePeriod': isInGracePeriod, + 'paymentBalanceBTC': paymentBalance, + 'paymentBalanceStacks': STACKSBalance.toString(), + 'nameCostUnits': namePrice.units, + 'nameCostAmount': namePrice.amount.toString(), + 'estimateCostBTC': estimate, + 'isNamespaceReady': isNamespaceReady, + 'namespaceBurnAddress': givenNamespaceBurnAddress, + 'trueNamespaceBurnAddress': trueNamespaceBurnAddress + }; + } + }); + + return safetyChecksPromise + .then((safetyChecksResult : any) => { + if (!safetyChecksResult.status) { + return new Promise((resolve : any) => resolve(JSONStringify(safetyChecksResult, true))); + } + + if (txOnly || preorderTxOnly) { + return txPromise; + } + + return txPromise.then((tx : string) => { + return network.broadcastTransaction(tx); + }) + .then((txidHex : string) => { + return txidHex; + }); + }); +} + + +/* + * Generate and optionally send a name-register + * args: + * @name (string) the name to register + * @IDaddress (string) the address that owns this name + * @paymentKey (string) the payment private key + * @zonefile (string) if given, the raw zone file or the path to the zone file data to use + * @zonefileHash (string) if given, this is the raw zone file hash to use + * (in which case, @zonefile will be ignored) + * @registerTxOnly (boolean) OPTIONAL: used internally to coerce returning only the tx + */ +function txRegister(network: CLINetworkAdapter, args: string[], registerTxOnly: boolean = false) : Promise { + const name = args[0]; + const IDaddress = args[1]; + const paymentKey = decodePrivateKey(args[2]); + + if (!IDaddress.startsWith('ID-')) { + throw new Error('Recipient ID-address must start with ID-'); + } + const address = IDaddress.slice(3); + const namespaceID = name.split('.').slice(-1)[0]; + + let zonefilePath = null; + let zonefileHash = null; + let zonefile = null; + + if (args.length > 3 && !!args[3]) { + zonefilePath = args[3]; + } + + if (args.length > 4 && !!args[4]) { + zonefileHash = args[4]; + zonefilePath = null; + + logger.debug(`Using zone file hash ${zonefileHash} instead of zone file`); + } + + if (!!zonefilePath) { + try { + zonefile = fs.readFileSync(zonefilePath).toString(); + } + catch(e) { + // zone file path as raw zone file + zonefile = zonefilePath; + } + } + + const paymentAddress = getPrivateKeyAddress(network, paymentKey); + const paymentUTXOsPromise = network.getUTXOs(paymentAddress); + + const estimatePromise = paymentUTXOsPromise.then((utxos : UTXO[]) => { + const numUTXOs = utxos.length; + return blockstack.transactions.estimateRegister( + name, network.coerceAddress(address), + network.coerceAddress(paymentAddress), true, numUTXOs); + }); + + const txPromise = blockstack.transactions.makeRegister( + name, address, paymentKey, zonefile, zonefileHash, !hasKeys(paymentKey)); + + if (estimateOnly) { + return estimatePromise + .then((cost: number) => JSONStringify({cost: cost})); + } + + if (!safetyChecks) { + if (txOnly || registerTxOnly) { + return txPromise; + } + else { + return txPromise + .then((tx: string) => { + return network.broadcastTransaction(tx); + }); + } + } + + const paymentBalancePromise = paymentUTXOsPromise.then((utxos : UTXO[]) => { + return sumUTXOs(utxos); + }); + + const nameInfoPromise = getNameInfoEasy(network, name); + const blockHeightPromise = network.getBlockHeight(); + + const safetyChecksPromise = Promise.all([ + nameInfoPromise, + blockHeightPromise, + blockstack.safety.isNameValid(name), + blockstack.safety.isNameAvailable(name), + blockstack.safety.addressCanReceiveName( + network.coerceAddress(address)), + blockstack.safety.isInGracePeriod(name), + blockstack.safety.namespaceIsReady(namespaceID), + paymentBalancePromise, + estimatePromise + ]) + .then(([nameInfo, + _blockHeight, + isNameValid, + isNameAvailable, + addressCanReceiveName, + isInGracePeriod, + isNamespaceReady, + paymentBalance, + estimateCost]) => { + if (isNameValid && isNamespaceReady && + (isNameAvailable || !nameInfo) && + addressCanReceiveName && !isInGracePeriod && estimateCost < paymentBalance) { + return {'status': true}; + } + else { + return { + 'status': false, + 'error': 'Name cannot be safely registered', + 'isNameValid': isNameValid, + 'isNameAvailable': isNameAvailable, + 'addressCanReceiveName': addressCanReceiveName, + 'isInGracePeriod': isInGracePeriod, + 'isNamespaceReady': isNamespaceReady, + 'paymentBalanceBTC': paymentBalance, + 'estimateCostBTC': estimateCost + }; + } + }); + + return safetyChecksPromise + .then((safetyChecksResult : any) => { + if (!safetyChecksResult.status) { + if (registerTxOnly) { + return new Promise((resolve : any) => resolve(JSONStringify(safetyChecksResult, true))); + } + } + + if (txOnly || registerTxOnly) { + return txPromise; + } + + return txPromise.then((tx : string) => { + return network.broadcastTransaction(tx); + }) + .then((txidHex : string) => { + return txidHex; + }); + }); +} + + +// helper to be used with txPreorder and txRegister to determine whether or not the operation failed +function checkTxStatus(txOrJson: string) : boolean { + try { + const json = JSON.parse(txOrJson); + return !!json.status; + } + catch(e) { + return true; + } +} + +/* + * Generate a zone file for a name, given its Gaia hub URL + * Optionally includes a _resolver entry + * args: + * @name (string) the blockstack ID + * @idAddress (string) the ID address that owns the name + * @gaiaHub (string) the URL to the write endpoint to store the name's profile + */ +function makeZonefile(network: CLINetworkAdapter, args: string[]) : Promise { + const name = args[0]; + const idAddress = args[1]; + const gaiaHub = args[2]; + let resolver = ''; + + if (!idAddress.startsWith('ID-')) { + throw new Error('ID-address must start with ID-'); + } + + if (args.length > 3 && !!args[3]) { + resolver = args[3]; + } + + const address = idAddress.slice(3); + const mainnetAddress = network.coerceMainnetAddress(address); + const profileUrl = `${gaiaHub.replace(/\/+$/g, '')}/${mainnetAddress}/profile.json`; + try { + checkUrl(profileUrl); + } + catch(e) { + return Promise.resolve().then(() => JSONStringify({ + 'status': false, + 'error': e.message, + 'hints': [ + 'Make sure the Gaia hub URL does not have any trailing /\'s', + 'Make sure the Gaia hub URL scheme is present and well-formed' + ] + }, true)); + } + + const zonefile = blockstack.makeProfileZoneFile(name, profileUrl); + return Promise.resolve().then(() => { + if (!resolver) { + return zonefile; + } + + // append _resolver record + // TODO: zone-file doesn't do this right, so we have to append manually + return `${zonefile.replace(/\n+$/, '')}\n_resolver\tIN\tURI\t10\t1\t"${resolver}"`; + }); +} + +/* + * Generate and optionally send a name-update + * args: + * @name (string) the name to update + * @zonefile (string) the path to the zonefile to use + * @ownerKey (string) the owner private key + * @paymentKey (string) the payment private key + * @zonefileHash (string) the zone file hash to use, if given + * (will be used instead of the zonefile) + */ +function update(network: CLINetworkAdapter, args: string[]) : Promise { + const name = args[0]; + let zonefilePath = args[1]; + const ownerKey = decodePrivateKey(args[2]); + const paymentKey = decodePrivateKey(args[3]); + + let zonefile = null; + let zonefileHash = ''; + + if (args.length > 4 && !!args[4]) { + zonefileHash = args[4]; + zonefilePath = null; + logger.debug(`Using zone file hash ${zonefileHash} instead of zone file`); + } + + if (zonefilePath) { + zonefile = fs.readFileSync(zonefilePath).toString(); + } + + const ownerAddress = getPrivateKeyAddress(network, ownerKey); + const paymentAddress = getPrivateKeyAddress(network, paymentKey); + + const ownerUTXOsPromise = network.getUTXOs(ownerAddress); + const paymentUTXOsPromise = network.getUTXOs(paymentAddress); + + const estimatePromise = Promise.all([ + ownerUTXOsPromise, paymentUTXOsPromise]) + .then(([ownerUTXOs, paymentUTXOs] : [UTXO[], UTXO[]]) => { + const numOwnerUTXOs = ownerUTXOs.length; + const numPaymentUTXOs = paymentUTXOs.length; + return blockstack.transactions.estimateUpdate( + name, network.coerceAddress(ownerAddress), + network.coerceAddress(paymentAddress), + numOwnerUTXOs + numPaymentUTXOs - 1); + }); + + const txPromise = blockstack.transactions.makeUpdate( + name, ownerKey, paymentKey, zonefile, zonefileHash, !hasKeys(ownerKey) || !hasKeys(paymentKey)); + + if (estimateOnly) { + return estimatePromise + .then((cost: number) => String(cost)); + } + + if (!safetyChecks) { + if (txOnly) { + return txPromise; + } + else { + return txPromise + .then((tx : string) => { + return network.broadcastTransaction(tx); + }); + } + } + + const paymentBalancePromise = paymentUTXOsPromise.then((utxos : UTXO[]) => { + return sumUTXOs(utxos); + }); + + const safetyChecksPromise = Promise.all([ + blockstack.safety.isNameValid(name), + blockstack.safety.ownsName(name, network.coerceAddress(ownerAddress)), + blockstack.safety.isInGracePeriod(name), + estimatePromise, + paymentBalancePromise + ]) + .then(([isNameValid, ownsName, isInGracePeriod, estimateCost, paymentBalance]) => { + if (isNameValid && ownsName && !isInGracePeriod && estimateCost < paymentBalance) { + return {'status': true}; + } + else { + return { + 'status': false, + 'error': 'Name cannot be safely updated', + 'isNameValid': isNameValid, + 'ownsName': ownsName, + 'isInGracePeriod': isInGracePeriod, + 'estimateCostBTC': estimateCost, + 'paymentBalanceBTC': paymentBalance + }; + } + }); + + return safetyChecksPromise + .then((safetyChecksResult) => { + if (!safetyChecksResult.status) { + return new Promise((resolve) => resolve(JSONStringify(safetyChecksResult, true))); + } + + if (txOnly) { + return txPromise; + } + + return txPromise.then((tx : string) => { + return network.broadcastTransaction(tx); + }) + .then((txidHex : string) => { + return txidHex; + }); + }); +} + +/* + * Generate and optionally send a name-transfer + * args: + * @name (string) the name to transfer + * @IDaddress (string) the new owner address + * @keepZoneFile (boolean) keep the zone file or not + * @ownerKey (string) the owner private key + * @paymentKey (string) the payment private key + */ +function transfer(network: CLINetworkAdapter, args: string[]) : Promise { + const name = args[0]; + const IDaddress = args[1]; + const keepZoneFile = (args[2].toLowerCase() === 'true'); + const ownerKey = decodePrivateKey(args[3]); + const paymentKey = decodePrivateKey(args[4]); + const ownerAddress = getPrivateKeyAddress(network, ownerKey); + const paymentAddress = getPrivateKeyAddress(network, paymentKey); + + if (!IDaddress.startsWith('ID-')) { + throw new Error('Recipient ID-address must start with ID-'); + } + const address = IDaddress.slice(3); + + const ownerUTXOsPromise = network.getUTXOs(ownerAddress); + const paymentUTXOsPromise = network.getUTXOs(paymentAddress); + + const estimatePromise = Promise.all([ + ownerUTXOsPromise, paymentUTXOsPromise]) + .then(([ownerUTXOs, paymentUTXOs] : [UTXO[], UTXO[]]) => { + const numOwnerUTXOs = ownerUTXOs.length; + const numPaymentUTXOs = paymentUTXOs.length; + return blockstack.transactions.estimateTransfer( + name, network.coerceAddress(address), + network.coerceAddress(ownerAddress), + network.coerceAddress(paymentAddress), + numOwnerUTXOs + numPaymentUTXOs - 1); + }); + + const txPromise = blockstack.transactions.makeTransfer( + name, address, ownerKey, paymentKey, keepZoneFile, !hasKeys(ownerKey) || !hasKeys(paymentKey)); + + if (estimateOnly) { + return estimatePromise + .then((cost: number) => String(cost)); + } + + if (!safetyChecks) { + if (txOnly) { + return txPromise; + } + else { + return txPromise + .then((tx : string) => { + return network.broadcastTransaction(tx); + }); + } + } + + const paymentBalancePromise = paymentUTXOsPromise.then((utxos : UTXO[]) => { + return sumUTXOs(utxos); + }); + + const safetyChecksPromise = Promise.all([ + blockstack.safety.isNameValid(name), + blockstack.safety.ownsName(name, network.coerceAddress(ownerAddress)), + blockstack.safety.addressCanReceiveName(network.coerceAddress(address)), + blockstack.safety.isInGracePeriod(name), + paymentBalancePromise, + estimatePromise + ]) + .then(([isNameValid, ownsName, addressCanReceiveName, + isInGracePeriod, paymentBalance, estimateCost]) => { + if (isNameValid && ownsName && addressCanReceiveName && + !isInGracePeriod && estimateCost < paymentBalance) { + return {'status': true}; + } + else { + return { + 'status': false, + 'error': 'Name cannot be safely transferred', + 'isNameValid': isNameValid, + 'ownsName': ownsName, + 'addressCanReceiveName': addressCanReceiveName, + 'isInGracePeriod': isInGracePeriod, + 'estimateCostBTC': estimateCost, + 'paymentBalanceBTC': paymentBalance + }; + } + }); + + return safetyChecksPromise + .then((safetyChecksResult) => { + if (!safetyChecksResult.status) { + return new Promise((resolve) => resolve(JSONStringify(safetyChecksResult, true))); + } + + if (txOnly) { + return txPromise; + } + + return txPromise.then((tx : string) => { + return network.broadcastTransaction(tx); + }) + .then((txidHex : string) => { + return txidHex; + }); + }); +} + +/* + * Get the last zone file hash + */ +function getLastZonefileHash(network: CLINetworkAdapter, name: string): Promise { + return getAllNameHistoryPages(network, name, 0) + .then((nameHistory : any) => { + let zfh = ''; + const blockHeights = Object.keys(nameHistory).sort().reverse(); + for (let i = 0; i < blockHeights.length; i++) { + const blockHeight = blockHeights[i]; + for (let j = nameHistory[blockHeight].length - 1; j >= 0; j--) { + const entry = nameHistory[blockHeight][j]; + if (!!entry.value_hash) { + zfh = entry.value_hash; + break; + } + } + if (!!zfh) { + break; + } + } + + if (!!zfh) { + return zfh; + } + else { + throw new Error(`Failed to find a zone file hash for ${name}`); + } + }); +} + +/* + * Generate and optionally send a name-renewal + * args: + * @name (string) the name to renew + * @ownerKey (string) the owner private key + * @paymentKey (string) the payment private key + * @address (string) OPTIONAL: the new owner address + * @zonefilePath (string) OPTIONAL: the path to the new zone file + * @zonefileHash (string) OPTINOAL: use the given zonefile hash. Supercedes zonefile. + */ +async function renew(network: CLINetworkAdapter, args: string[]) : Promise { + const name = args[0]; + const ownerKey = decodePrivateKey(args[1]); + const paymentKey = decodePrivateKey(args[2]); + const ownerAddress = getPrivateKeyAddress(network, ownerKey); + const paymentAddress = getPrivateKeyAddress(network, paymentKey); + const namespaceID = name.split('.').slice(-1)[0]; + + let newAddress = ''; + let zonefilePath = ''; + let zonefileHash = ''; + let zonefile = ''; + let blankZonefileHash = true; + + if (args.length >= 4 && !!args[3]) { + // ID-address + newAddress = args[3].slice(3); + } + else { + newAddress = getPrivateKeyAddress(network, ownerKey); + } + + if (args.length >= 5 && !!args[4]) { + blankZonefileHash = false; + zonefilePath = args[4]; + } + + if (args.length >= 6 && !!args[5]) { + blankZonefileHash = false; + zonefileHash = args[5]; + zonefilePath = null; + logger.debug(`Using zone file hash ${zonefileHash} instead of zone file`); + } + + if (zonefilePath) { + zonefile = fs.readFileSync(zonefilePath).toString(); + } + + const ownerUTXOsPromise = network.getUTXOs(ownerAddress); + const paymentUTXOsPromise = network.getUTXOs(paymentAddress); + + const estimatePromise = Promise.all([ + ownerUTXOsPromise, paymentUTXOsPromise]) + .then(([ownerUTXOs, paymentUTXOs] : [UTXO[], UTXO[]]) => { + const numOwnerUTXOs = ownerUTXOs.length; + const numPaymentUTXOs = paymentUTXOs.length; + return blockstack.transactions.estimateRenewal( + name, network.coerceAddress(newAddress), + network.coerceAddress(ownerAddress), + network.coerceAddress(paymentAddress), true, + numOwnerUTXOs + numPaymentUTXOs - 1); + }); + + let zfh: string; + if (!!zonefile) { + const sha256 = bitcoin.crypto.sha256(new Buffer(zonefile)); + zfh = (new RIPEMD160()).update(sha256).digest('hex'); + } else if (!!zonefileHash || blankZonefileHash) { + // already have the hash + zfh = zonefileHash; + } else { + zfh = await getLastZonefileHash(network, name); + } + + const txPromise = blockstack.transactions.makeRenewal( + name, newAddress, ownerKey, paymentKey, zonefile, zfh, !hasKeys(ownerKey) || !hasKeys(paymentKey)); + + if (estimateOnly) { + return estimatePromise + .then((cost: number) => String(cost)); + } + + if (!safetyChecks) { + if (txOnly) { + return txPromise; + } + else { + return txPromise + .then((tx : string) => { + return network.broadcastTransaction(tx); + }); + } + } + + const paymentBalancePromise = paymentUTXOsPromise.then((utxos) => { + return sumUTXOs(utxos); + }); + + const canReceiveNamePromise = Promise.resolve().then(() => { + if (newAddress) { + return blockstack.safety.addressCanReceiveName(network.coerceAddress(newAddress)); + } + else { + return true; + } + }); + + const safetyChecksPromise = Promise.all([ + blockstack.safety.isNameValid(name), + blockstack.safety.ownsName(name, network.coerceAddress(ownerAddress)), + network.getNamespaceBurnAddress(namespaceID, true, receiveFeesPeriod), + network.getNamespaceBurnAddress(namespaceID, false, receiveFeesPeriod), + canReceiveNamePromise, + network.getNamePrice(name), + network.getAccountBalance(paymentAddress, 'STACKS'), + estimatePromise, + paymentBalancePromise + ]) + .then(([isNameValid, ownsName, givenNSBurnAddr, trueNSBurnAddr, + addressCanReceiveName, nameCost, + accountBalance, estimateCost, paymentBalance]) => { + if (isNameValid && ownsName && addressCanReceiveName && + trueNSBurnAddr === givenNSBurnAddr && + (nameCost.units === 'BTC' || (nameCost.units == 'STACKS' && + nameCost.amount.cmp(accountBalance) <= 0)) && + estimateCost < paymentBalance) { + return {'status': true}; + } + else { + return { + 'status': false, + 'error': 'Name cannot be safely renewed', + 'isNameValid': isNameValid, + 'ownsName': ownsName, + 'addressCanReceiveName': addressCanReceiveName, + 'estimateCostBTC': estimateCost, + 'nameCostUnits': nameCost.units, + 'nameCostAmount': nameCost.amount.toString(), + 'paymentBalanceBTC': paymentBalance, + 'paymentBalanceStacks': accountBalance.toString(), + 'namespaceBurnAddress': givenNSBurnAddr, + 'trueNamespaceBurnAddress': trueNSBurnAddr + }; + } + }); + + return safetyChecksPromise + .then((safetyChecksResult) => { + if (!safetyChecksResult.status) { + return new Promise((resolve) => resolve(JSONStringify(safetyChecksResult, true))); + } + + if (txOnly) { + return txPromise; + } + + return txPromise.then((tx : string) => { + return network.broadcastTransaction(tx); + }) + .then((txidHex : string) => { + return txidHex; + }); + }); +} + +/* + * Generate and optionally send a name-revoke + * args: + * @name (string) the name to revoke + * @ownerKey (string) the owner private key + * @paymentKey (string) the payment private key + */ +function revoke(network: CLINetworkAdapter, args: string[]) : Promise { + const name = args[0]; + const ownerKey = decodePrivateKey(args[1]); + const paymentKey = decodePrivateKey(args[2]); + const paymentAddress = getPrivateKeyAddress(network, paymentKey); + const ownerAddress = getPrivateKeyAddress(network, ownerKey); + + const ownerUTXOsPromise = network.getUTXOs(ownerAddress); + const paymentUTXOsPromise = network.getUTXOs(paymentAddress); + + const estimatePromise = Promise.all([ + ownerUTXOsPromise, paymentUTXOsPromise]) + .then(([ownerUTXOs, paymentUTXOs]) => { + const numOwnerUTXOs = ownerUTXOs.length; + const numPaymentUTXOs = paymentUTXOs.length; + return blockstack.transactions.estimateRevoke( + name, network.coerceAddress(ownerAddress), + network.coerceAddress(paymentAddress), + numOwnerUTXOs + numPaymentUTXOs - 1); + }); + + const txPromise = blockstack.transactions.makeRevoke( + name, ownerKey, paymentKey, !hasKeys(ownerKey) || !hasKeys(paymentKey)); + + if (estimateOnly) { + return estimatePromise + .then((cost: number) => String(cost)); + } + + if (!safetyChecks) { + if (txOnly) { + return txPromise; + } + else { + return txPromise + .then((tx) => { + return network.broadcastTransaction(tx); + }); + } + } + + const paymentBalancePromise = paymentUTXOsPromise.then((utxos : UTXO[]) => { + return sumUTXOs(utxos); + }); + + const safetyChecksPromise = Promise.all([ + blockstack.safety.isNameValid(name), + blockstack.safety.ownsName(name, network.coerceAddress(ownerAddress)), + blockstack.safety.isInGracePeriod(name), + estimatePromise, + paymentBalancePromise + ]) + .then(([isNameValid, ownsName, isInGracePeriod, estimateCost, paymentBalance]) => { + if (isNameValid && ownsName && !isInGracePeriod && estimateCost < paymentBalance) { + return {'status': true}; + } + else { + return { + 'status': false, + 'error': 'Name cannot be safely revoked', + 'isNameValid': isNameValid, + 'ownsName': ownsName, + 'isInGracePeriod': isInGracePeriod, + 'estimateCostBTC': estimateCost, + 'paymentBalanceBTC': paymentBalance + }; + } + }); + + return safetyChecksPromise + .then((safetyChecksResult) => { + if (!safetyChecksResult.status) { + return new Promise((resolve : any) => resolve(JSONStringify(safetyChecksResult, true))); + } + + if (txOnly) { + return txPromise; + } + + return txPromise.then((tx : string) => { + return network.broadcastTransaction(tx); + }) + .then((txidHex : string) => { + return txidHex; + }); + }); +} + +/* + * Generate and optionally send a namespace-preorder + * args: + * @namespace (string) the namespace to preorder + * @address (string) the address to reveal the namespace + * @paymentKey (string) the payment private key + */ +function namespacePreorder(network: CLINetworkAdapter, args: string[]) : Promise { + const namespaceID = args[0]; + const address = args[1]; + const paymentKey = decodePrivateKey(args[2]); + const paymentAddress = getPrivateKeyAddress(network, paymentKey); + + const txPromise = blockstack.transactions.makeNamespacePreorder( + namespaceID, address, paymentKey, !hasKeys(paymentKey)); + + const paymentUTXOsPromise = network.getUTXOs(paymentAddress); + + const estimatePromise = paymentUTXOsPromise.then((utxos : UTXO[]) => { + const numUTXOs = utxos.length; + return blockstack.transactions.estimateNamespacePreorder( + namespaceID, network.coerceAddress(address), + network.coerceAddress(paymentAddress), numUTXOs); + }); + + if (estimateOnly) { + return estimatePromise + .then((cost: number) => String(cost)); + } + + if (!safetyChecks) { + if (txOnly) { + return txPromise; + } + else { + return txPromise + .then((tx : string) => { + return network.broadcastTransaction(tx); + }); + } + } + + const paymentBalance = paymentUTXOsPromise.then((utxos : UTXO[]) => { + return sumUTXOs(utxos); + }); + + const safetyChecksPromise = Promise.all([ + blockstack.safety.isNamespaceValid(namespaceID), + blockstack.safety.isNamespaceAvailable(namespaceID), + network.getNamespacePrice(namespaceID), + network.getAccountBalance(paymentAddress, 'STACKS'), + paymentBalance, + estimatePromise + ]) + .then(([isNamespaceValid, isNamespaceAvailable, namespacePrice, + STACKSBalance, paymentBalance, estimate]) => { + if (isNamespaceValid && isNamespaceAvailable && + (namespacePrice.units === 'BTC' || + (namespacePrice.units === 'STACKS' && + namespacePrice.amount.cmp(STACKSBalance) <= 0)) && + paymentBalance >= estimate) { + return {'status': true}; + } + else { + return { + 'status': false, + 'error': 'Namespace cannot be safely preordered', + 'isNamespaceValid': isNamespaceValid, + 'isNamespaceAvailable': isNamespaceAvailable, + 'paymentBalanceBTC': paymentBalance, + 'paymentBalanceStacks': STACKSBalance.toString(), + 'namespaceCostUnits': namespacePrice.units, + 'namespaceCostAmount': namespacePrice.amount.toString(), + 'estimateCostBTC': estimate + }; + } + }); + + return safetyChecksPromise + .then((safetyChecksResult : any) => { + if (!safetyChecksResult.status) { + return new Promise((resolve : any) => resolve(JSONStringify(safetyChecksResult))); + } + + if (txOnly) { + return txPromise; + } + + return txPromise.then((tx : string) => { + return network.broadcastTransaction(tx); + }) + .then((txidHex : string) => { + return txidHex; + }); + }); +} + +/* + * Generate and optionally send a namespace-reveal + * args: + * @name (string) the namespace to reveal + * @revealAddr (string) the reveal address + * @version (int) the namespace version bits + * @lifetime (int) the name lifetime + * @coeff (int) the multiplicative price coefficient + * @base (int) the price base + * @bucketString (string) the serialized bucket exponents + * @nonalphaDiscount (int) the non-alpha price discount + * @noVowelDiscount (int) the no-vowel price discount + * @paymentKey (string) the payment private key + */ +function namespaceReveal(network: CLINetworkAdapter, args: string[]) : Promise { + const namespaceID = args[0]; + const revealAddr = args[1]; + const version = parseInt(args[2]); + let lifetime = parseInt(args[3]); + const coeff = parseInt(args[4]); + const base = parseInt(args[5]); + const bucketString = args[6]; + const nonalphaDiscount = parseInt(args[7]); + const noVowelDiscount = parseInt(args[8]); + const paymentKey = decodePrivateKey(args[9]); + + const buckets = bucketString.split(',') + .map((x) => {return parseInt(x);}); + + if (lifetime < 0) { + lifetime = 2**32 - 1; + } + + if (nonalphaDiscount === 0) { + throw new Error('Cannot have a 0 non-alpha discount (pass 1 for no discount)'); + } + + if (noVowelDiscount === 0) { + throw new Error('Cannot have a 0 no-vowel discount (pass 1 for no discount)'); + } + + const namespace = new blockstack.transactions.BlockstackNamespace(namespaceID); + + namespace.setVersion(version); + namespace.setLifetime(lifetime); + namespace.setCoeff(coeff); + namespace.setBase(base); + namespace.setBuckets(buckets); + namespace.setNonalphaDiscount(nonalphaDiscount); + namespace.setNoVowelDiscount(noVowelDiscount); + + const paymentAddress = getPrivateKeyAddress(network, paymentKey); + const paymentUTXOsPromise = network.getUTXOs(paymentAddress); + + const estimatePromise = paymentUTXOsPromise.then((utxos : UTXO[]) => { + const numUTXOs = utxos.length; + return blockstack.transactions.estimateNamespaceReveal( + namespace, network.coerceAddress(revealAddr), + network.coerceAddress(paymentAddress), numUTXOs); + }); + + const txPromise = blockstack.transactions.makeNamespaceReveal( + namespace, revealAddr, paymentKey, !hasKeys(paymentKey)); + + if (estimateOnly) { + return estimatePromise + .then((cost: number) => String(cost)); + } + + if (!safetyChecks) { + if (txOnly) { + return txPromise; + } + else { + return txPromise + .then((tx : string) => { + return network.broadcastTransaction(tx); + }); + } + } + + const paymentBalancePromise = paymentUTXOsPromise.then((utxos : UTXO[]) => { + return sumUTXOs(utxos); + }); + + const safetyChecksPromise = Promise.all([ + blockstack.safety.isNamespaceValid(namespaceID), + blockstack.safety.isNamespaceAvailable(namespaceID), + paymentBalancePromise, + estimatePromise + ]) + .then(([isNamespaceValid, isNamespaceAvailable, + paymentBalance, estimate]) => { + + if (isNamespaceValid && isNamespaceAvailable && + paymentBalance >= estimate) { + return {'status': true}; + } + else { + return { + 'status': false, + 'error': 'Namespace cannot be safely revealed', + 'isNamespaceValid': isNamespaceValid, + 'isNamespaceAvailable': isNamespaceAvailable, + 'paymentBalanceBTC': paymentBalance, + 'estimateCostBTC': estimate + }; + } + }); + + return safetyChecksPromise + .then((safetyChecksResult : any) => { + if (!safetyChecksResult.status) { + return new Promise((resolve : any) => resolve(JSONStringify(safetyChecksResult, true))); + } + + if (txOnly) { + return txPromise; + } + + return txPromise.then((tx : string) => { + return network.broadcastTransaction(tx); + }) + .then((txidHex : string) => { + return txidHex; + }); + }); +} + +/* + * Generate and optionally send a namespace-ready + * args: + * @namespaceID (string) the namespace ID + * @revealKey (string) the hex-encoded reveal key + */ +function namespaceReady(network: CLINetworkAdapter, args: string[]) : Promise { + const namespaceID = args[0]; + const revealKey = decodePrivateKey(args[1]); + const revealAddress = getPrivateKeyAddress(network, revealKey); + + const txPromise = blockstack.transactions.makeNamespaceReady( + namespaceID, revealKey, !hasKeys(revealKey)); + + const revealUTXOsPromise = network.getUTXOs(revealAddress); + + const estimatePromise = revealUTXOsPromise.then((utxos) => { + const numUTXOs = utxos.length; + return blockstack.transactions.estimateNamespaceReady( + namespaceID, numUTXOs); + }); + + if (estimateOnly) { + return estimatePromise + .then((cost: number) => String(cost)); + } + + if (!safetyChecks) { + if (txOnly) { + return txPromise; + } + else { + return txPromise + .then((tx : string) => { + return network.broadcastTransaction(tx); + }); + } + } + + const revealBalancePromise = revealUTXOsPromise.then((utxos : UTXO[]) => { + return sumUTXOs(utxos); + }); + + const safetyChecksPromise = Promise.all([ + blockstack.safety.isNamespaceValid(namespaceID), + blockstack.safety.namespaceIsReady(namespaceID), + blockstack.safety.revealedNamespace(namespaceID, revealAddress), + revealBalancePromise, + estimatePromise + ]) + .then(([isNamespaceValid, isNamespaceReady, isRevealer, + revealerBalance, estimate]) => { + if (isNamespaceValid && !isNamespaceReady && isRevealer && + revealerBalance >= estimate) { + return {'status': true}; + } + else { + return { + 'status': false, + 'error': 'Namespace cannot be safely launched', + 'isNamespaceValid': isNamespaceValid, + 'isNamespaceReady': isNamespaceReady, + 'isPrivateKeyRevealer': isRevealer, + 'revealerBalanceBTC': revealerBalance, + 'estimateCostBTC': estimate + }; + } + }); + + return safetyChecksPromise + .then((safetyChecksResult : any) => { + if (!safetyChecksResult.status) { + return new Promise((resolve : any) => resolve(JSONStringify(safetyChecksResult, true))); + } + + if (txOnly) { + return txPromise; + } + + return txPromise.then((tx : string) => { + return network.broadcastTransaction(tx); + }) + .then((txidHex : string) => { + return txidHex; + }); + }); +} + + +/* + * Generate and send a name-import transaction + * @name (string) the name to import + * @IDrecipientAddr (string) the recipient of the name + * @gaiaHubURL (string) the URL to the name's gaia hub + * @importKey (string) the key to pay for the import + * @zonefile (string) OPTIONAL: the path to the zone file to use (supercedes gaiaHubUrl) + * @zonefileHash (string) OPTIONAL: the hash of the zone file (supercedes gaiaHubUrl and zonefile) + */ +function nameImport(network: CLINetworkAdapter, args: string[]) : Promise { + const name = args[0]; + const IDrecipientAddr = args[1]; + const gaiaHubUrl = args[2]; + const importKey = decodePrivateKey(args[3]); + const zonefilePath = args[4]; + let zonefileHash = args[5]; + let zonefile = ''; + + if (safetyChecks && (typeof importKey !== 'string')) { + // multisig import not supported, unless we're testing + throw new Error('Invalid argument: multisig is not supported at this time'); + } + + if (!IDrecipientAddr.startsWith('ID-')) { + throw new Error('Recipient ID-address must start with ID-'); + } + + const recipientAddr = IDrecipientAddr.slice(3); + + if (zonefilePath && !zonefileHash) { + zonefile = fs.readFileSync(zonefilePath).toString(); + } + + else if (!zonefileHash && !zonefilePath) { + // make zone file and hash from gaia hub url + const mainnetAddress = network.coerceMainnetAddress(recipientAddr); + const profileUrl = `${gaiaHubUrl}/${mainnetAddress}/profile.json`; + try { + checkUrl(profileUrl); + } + catch(e) { + return Promise.resolve().then(() => JSONStringify({ + 'status': false, + 'error': e.message, + 'hints': [ + 'Make sure the Gaia hub URL does not have any trailing /\'s', + 'Make sure the Gaia hub URL scheme is present and well-formed' + ] + }, true)); + } + + zonefile = blockstack.makeProfileZoneFile(name, profileUrl); + zonefileHash = hash160(Buffer.from(zonefile)).toString('hex'); + } + + const namespaceID = name.split('.').slice(-1)[0]; + const importAddress = getPrivateKeyAddress(network, importKey); + + const txPromise = blockstack.transactions.makeNameImport( + name, recipientAddr, zonefileHash, importKey, !hasKeys(importKey)); + + const importUTXOsPromise = network.getUTXOs(importAddress); + + const estimatePromise = importUTXOsPromise.then((utxos : UTXO[]) => { + const numUTXOs = utxos.length; + return blockstack.transactions.estimateNameImport( + name, recipientAddr, zonefileHash, numUTXOs); + }); + + if (estimateOnly) { + return estimatePromise + .then((cost: number) => String(cost)); + } + + if (!safetyChecks) { + if (txOnly) { + return txPromise; + } + else { + return txPromise + .then((tx : string) => { + return broadcastTransactionAndZoneFile(network, tx, zonefile); + }) + .then((resp : any) => { + if (resp.status && resp.hasOwnProperty('txid')) { + // just return txid + return resp.txid; + } + else { + // some error + return JSONStringify(resp, true); + } + }); + } + } + + const importBalancePromise = importUTXOsPromise.then((utxos) => { + return sumUTXOs(utxos); + }); + + const safetyChecksPromise = Promise.all([ + blockstack.safety.namespaceIsReady(namespaceID), + blockstack.safety.namespaceIsRevealed(namespaceID), + blockstack.safety.addressCanReceiveName(recipientAddr), + importBalancePromise, + estimatePromise + ]) + .then(([isNamespaceReady, isNamespaceRevealed, addressCanReceive, + importBalance, estimate]) => { + if (!isNamespaceReady && isNamespaceRevealed && addressCanReceive && + importBalance >= estimate) { + return {'status': true}; + } + else { + return { + 'status': false, + 'error': 'Name cannot be safetly imported', + 'isNamespaceReady': isNamespaceReady, + 'isNamespaceRevealed': isNamespaceRevealed, + 'addressCanReceiveName': addressCanReceive, + 'importBalanceBTC': importBalance, + 'estimateCostBTC': estimate + }; + } + }); + + return safetyChecksPromise + .then((safetyChecksResult : any) => { + if (!safetyChecksResult.status) { + return new Promise((resolve : any) => resolve(JSONStringify(safetyChecksResult, true))); + } + + if (txOnly) { + return txPromise; + } + + return txPromise + .then((tx : string) => { + return broadcastTransactionAndZoneFile(network, tx, zonefile); + }) + .then((resp : any) => { + if (resp.status && resp.hasOwnProperty('txid')) { + // just return txid + return resp.txid; + } + else { + // some error + return JSONStringify(resp, true); + } + }); + }); +} + + +/* + * Announce a message to subscribed peers by means of an Atlas zone file + * @messageHash (string) the hash of the already-sent message + * @senderKey (string) the key that owns the name that the peers have subscribed to + */ +function announce(network: CLINetworkAdapter, args: string[]) : Promise { + const messageHash = args[0]; + const senderKey = decodePrivateKey(args[1]); + + const senderAddress = getPrivateKeyAddress(network, senderKey); + + const txPromise = blockstack.transactions.makeAnnounce( + messageHash, senderKey, !hasKeys(senderKey)); + + const senderUTXOsPromise = network.getUTXOs(senderAddress); + + const estimatePromise = senderUTXOsPromise.then((utxos : UTXO[]) => { + const numUTXOs = utxos.length; + return blockstack.transactions.estimateAnnounce(messageHash, numUTXOs); + }); + + if (estimateOnly) { + return estimatePromise + .then((cost: number) => String(cost)); + } + + if (!safetyChecks) { + if (txOnly) { + return txPromise; + } + else { + return txPromise + .then((tx) => { + return network.broadcastTransaction(tx); + }); + } + } + + const senderBalancePromise = senderUTXOsPromise.then((utxos : UTXO[]) => { + return sumUTXOs(utxos); + }); + + const safetyChecksPromise = Promise.all( + [senderBalancePromise, estimatePromise]) + .then(([senderBalance, estimate]) => { + if (senderBalance >= estimate) { + return {'status': true}; + } + else { + return { + 'status': false, + 'error': 'Announcement cannot be safely sent', + 'senderBalanceBTC': senderBalance, + 'estimateCostBTC': estimate + }; + } + }); + + return safetyChecksPromise + .then((safetyChecksResult : any) => { + if (!safetyChecksResult.status) { + return new Promise((resolve : any) => resolve(JSONStringify(safetyChecksResult, true))); + } + + if (txOnly) { + return txPromise; + } + + return txPromise.then((tx : string) => { + return network.broadcastTransaction(tx); + }) + .then((txidHex : string) => { + return txidHex; + }); + }); +} + + +/* + * Register a name the easy way. Send the preorder + * and register transactions to the broadcaster, as + * well as the zone file. Also create and replicate + * the profile to the Gaia hub. + * @arg name (string) the name to register + * @arg ownerKey (string) the hex-encoded owner private key (must be singlesig) + * @arg paymentKey (string) the hex-encoded payment key to purchase this name + * @arg gaiaHubUrl (string) the write endpoint of the gaia hub URL to use + * @arg zonefile (string) OPTIONAL the path to the zone file to give this name. + * supercedes gaiaHubUrl + */ +function register(network: CLINetworkAdapter, args: string[]) : Promise { + const name = args[0]; + const ownerKey = args[1]; + const paymentKey = decodePrivateKey(args[2]); + const gaiaHubUrl = args[3]; + + const address = getPrivateKeyAddress(network, ownerKey); + const emptyProfile : any = {type: '@Person', account: []}; + + let zonefilePromise : Promise; + + if (args.length > 4 && !!args[4]) { + const zonefilePath = args[4]; + zonefilePromise = Promise.resolve().then(() => fs.readFileSync(zonefilePath).toString()); + } + else { + // generate one + zonefilePromise = makeZoneFileFromGaiaUrl(network, name, gaiaHubUrl, ownerKey); + } + + let preorderTx = ''; + let registerTx = ''; + let broadcastResult : any = null; + let zonefile = ''; + + return zonefilePromise.then((zf : string) => { + zonefile = zf; + + // carry out safety checks for preorder and register + const preorderSafetyCheckPromise = txPreorder( + network, [name, `ID-${address}`, args[2]], true); + + const registerSafetyCheckPromise = txRegister( + network, [name, `ID-${address}`, args[2], zf], true); + + return Promise.all([preorderSafetyCheckPromise, registerSafetyCheckPromise]); + }) + .then(([preorderSafetyChecks, registerSafetyChecks]) => { + if (!checkTxStatus(preorderSafetyChecks) || !checkTxStatus(registerSafetyChecks)) { + try { + preorderSafetyChecks = JSON.parse(preorderSafetyChecks); + } + catch (e) { + } + + try { + registerSafetyChecks = JSON.parse(registerSafetyChecks); + } + catch (e) { + } + + // one or both safety checks failed + throw new SafetyError({ + 'status': false, + 'error': 'Failed to generate one or more transactions', + 'preorderSafetyChecks': preorderSafetyChecks, + 'registerSafetyChecks': registerSafetyChecks + }); + } + + // will have only gotten back the raw tx (which we'll discard anyway, + // since we have to use the right UTXOs) + return blockstack.transactions.makePreorder(name, address, paymentKey, !hasKeys(paymentKey)); + }) + .then((rawTx : string) => { + preorderTx = rawTx; + return rawTx; + }) + .then((rawTx : string) => { + // make it so that when we generate the NAME_REGISTRATION operation, + // we consume the change output from the NAME_PREORDER. + network.modifyUTXOSetFrom(rawTx); + return rawTx; + }) + .then(() => { + // now we can make the NAME_REGISTRATION + return blockstack.transactions.makeRegister(name, address, paymentKey, zonefile, null, !hasKeys(paymentKey)); + }) + .then((rawTx : string) => { + registerTx = rawTx; + return rawTx; + }) + .then((rawTx : string) => { + // make sure we don't double-spend the NAME_REGISTRATION before it is broadcasted + network.modifyUTXOSetFrom(rawTx); + }) + .then(() => { + if (txOnly) { + return Promise.resolve().then(() => { + const txData = { + preorder: preorderTx, + register: registerTx, + zonefile: zonefile + }; + return txData; + }); + } + else { + return network.broadcastNameRegistration(preorderTx, registerTx, zonefile); + } + }) + .then((txResult : any) => { + // sign and upload profile + broadcastResult = txResult; + const signedProfileData = makeProfileJWT(emptyProfile, ownerKey); + return gaiaUploadProfileAll( + network, [gaiaHubUrl], signedProfileData, ownerKey); + }) + .then((gaiaUrls) => { + if (gaiaUrls.hasOwnProperty('error')) { + return JSONStringify({ + 'profileUrls': gaiaUrls, + 'txInfo': broadcastResult + }, true); + } + return JSONStringify({ + 'profileUrls': gaiaUrls.dataUrls, + 'txInfo': broadcastResult + }); + }) + .catch((e : Error) => { + if (e.hasOwnProperty('safetyErrors')) { + // safety error; return as JSON + return e.message; + } + else { + throw e; + } + }); +} + +/* + * Register a name the easy way to an ID-address. Send the preorder + * and register transactions to the broadcaster, as + * well as the zone file. + * @arg name (string) the name to register + * @arg ownerAddress (string) the ID-address of the owner + * @arg paymentKey (string) the hex-encoded payment key to purchase this name + * @arg gaiaHubUrl (string) the gaia hub URL to use + * @arg zonefile (string) OPTIONAL the path to the zone file to give this name. + * supercedes gaiaHubUrl + */ +function registerAddr(network: CLINetworkAdapter, args: string[]) : Promise { + const name = args[0]; + const IDaddress = args[1]; + const paymentKey = decodePrivateKey(args[2]); + const gaiaHubUrl = args[3]; + + const address = IDaddress.slice(3); + const mainnetAddress = network.coerceMainnetAddress(address); + + let zonefile = ''; + if (args.length > 4 && !!args[4]) { + const zonefilePath = args[4]; + zonefile = fs.readFileSync(zonefilePath).toString(); + } + else { + // generate one + const profileUrl = `${gaiaHubUrl.replace(/\/+$/g, '')}/${mainnetAddress}/profile.json`; + try { + checkUrl(profileUrl); + } + catch(e) { + return Promise.resolve().then(() => JSONStringify({ + 'status': false, + 'error': e.message, + 'hints': [ + 'Make sure the Gaia hub URL does not have any trailing /\'s', + 'Make sure the Gaia hub URL scheme is present and well-formed' + ] + })); + } + + zonefile = blockstack.makeProfileZoneFile(name, profileUrl); + } + + let preorderTx = ''; + let registerTx = ''; + + // carry out safety checks for preorder and register + const preorderSafetyCheckPromise = txPreorder( + network, [name, `ID-${address}`, args[2]], true); + + const registerSafetyCheckPromise = txRegister( + network, [name, `ID-${address}`, args[2], zonefile], true); + + return Promise.all([preorderSafetyCheckPromise, registerSafetyCheckPromise]) + .then(([preorderSafetyChecks, registerSafetyChecks]) => { + if (!checkTxStatus(preorderSafetyChecks) || !checkTxStatus(registerSafetyChecks)) { + // one or both safety checks failed + throw new SafetyError({ + 'status': false, + 'error': 'Failed to generate one or more transactions', + 'preorderSafetyChecks': preorderSafetyChecks, + 'registerSafetyChecks': registerSafetyChecks + }); + } + + // will have only gotten back the raw tx (which we'll discard anyway, + // since we have to use the right UTXOs) + return blockstack.transactions.makePreorder(name, address, paymentKey, !hasKeys(paymentKey)); + }) + .then((rawTx : string) => { + preorderTx = rawTx; + return rawTx; + }) + .then((rawTx : string) => { + // make it so that when we generate the NAME_REGISTRATION operation, + // we consume the change output from the NAME_PREORDER. + network.modifyUTXOSetFrom(rawTx); + return rawTx; + }) + .then(() => { + // now we can make the NAME_REGISTRATION + return blockstack.transactions.makeRegister(name, address, paymentKey, zonefile, null, !hasKeys(paymentKey)); + }) + .then((rawTx : string) => { + registerTx = rawTx; + return rawTx; + }) + .then((rawTx : string) => { + // make sure we don't double-spend the NAME_REGISTRATION before it is broadcasted + network.modifyUTXOSetFrom(rawTx); + }) + .then(() => { + if (txOnly) { + return Promise.resolve().then(() => { + const txData = { + preorder: preorderTx, + register: registerTx, + zonefile: zonefile + }; + return txData; + }); + } + else { + return network.broadcastNameRegistration(preorderTx, registerTx, zonefile); + } + }) + .then((txResult : any) => { + // succcess! + return JSONStringify({ + 'txInfo': txResult + }); + }) + .catch((e : Error) => { + if (e.hasOwnProperty('safetyErrors')) { + // safety error; return as JSON + return e.message; + } + else { + throw e; + } + }); +} + + +/* + * Register a subdomain name the easy way. Send the + * zone file and signed subdomain records to the subdomain registrar. + * @arg name (string) the name to register + * @arg ownerKey (string) the hex-encoded owner private key (must be single-sig) + * @arg gaiaHubUrl (string) the write endpoint of the gaia hub URL to use + * @arg registrarUrl (string) OPTIONAL the registrar URL + * @arg zonefile (string) OPTIONAL the path to the zone file to give this name. + * supercedes gaiaHubUrl + */ +function registerSubdomain(network: CLINetworkAdapter, args: string[]) : Promise { + const name = args[0]; + const ownerKey = decodePrivateKey(args[1]); + const gaiaHubUrl = args[2]; + const registrarUrl = args[3]; + + const address = getPrivateKeyAddress(network, ownerKey); + const mainnetAddress = network.coerceMainnetAddress(address); + const emptyProfile : any = {type: '@Person', account: []}; + const onChainName = name.split('.').slice(-2).join('.'); + const subName = name.split('.')[0]; + + let zonefilePromise : Promise; + + if (args.length > 4 && !!args[4]) { + const zonefilePath = args[4]; + zonefilePromise = Promise.resolve().then(() => fs.readFileSync(zonefilePath).toString()); + } + else { + // generate one + zonefilePromise = makeZoneFileFromGaiaUrl(network, name, gaiaHubUrl, args[1]); + } + + const api_key = process.env.API_KEY || null; + + const onChainNamePromise = getNameInfoEasy(network, onChainName); + const registrarStatusPromise = fetch(`${registrarUrl}/index`) + .then((resp : any) => resp.json()); + + const profileUploadPromise = Promise.resolve().then(() => { + // sign and upload profile + const signedProfileData = makeProfileJWT(emptyProfile, args[1]); + return gaiaUploadProfileAll( + network, [gaiaHubUrl], signedProfileData, args[1]); + }) + .then((gaiaUrls : {dataUrls?: string[], error?: string}) => { + if (!!gaiaUrls.error) { + return { profileUrls: null, error: gaiaUrls.error }; + } + else { + return { profileUrls: gaiaUrls.dataUrls }; + } + }); + + let safetyChecksPromise = null; + if (safetyChecks) { + safetyChecksPromise = Promise.all([ + onChainNamePromise, + blockstack.safety.isNameAvailable(name), + registrarStatusPromise + ]) + .then(([onChainNameInfo, isNameAvailable, registrarStatus]) => { + if (safetyChecks) { + const registrarName = + (!!registrarStatus && registrarStatus.hasOwnProperty('domainName')) ? + registrarStatus.domainName : + ''; + + if (!onChainNameInfo || !isNameAvailable || + (registrarName !== '' && registrarName !== onChainName)) { + return { + 'status': false, + 'error': 'Subdomain cannot be safely registered', + 'onChainNameInfo': onChainNameInfo, + 'isNameAvailable': isNameAvailable, + 'onChainName': onChainName, + 'registrarName': registrarName + }; + } + } + return { 'status': true }; + }); + } + else { + safetyChecksPromise = Promise.resolve().then(() => { + return { + 'status': true + }; + }); + } + + return Promise.all([safetyChecksPromise, zonefilePromise]) + .then(([safetyChecks, zonefile] : [any, string]) => { + if (safetyChecks.status) { + const request = { + 'zonefile': zonefile, + 'name': subName, + 'owner_address': mainnetAddress + }; + + const options = { + method: 'POST', + headers: { + 'Content-type': 'application/json', + 'Authorization': '' + }, + body: JSON.stringify(request) + }; + + if (!!api_key) { + options.headers.Authorization = `bearer ${api_key}`; + } + + const registerPromise = fetch(`${registrarUrl}/register`, options) + .then(resp => resp.json()); + + return Promise.all([registerPromise, profileUploadPromise]) + .then(([registerInfo, profileUploadInfo] : [any, any]) => { + if (!profileUploadInfo.error) { + return JSONStringify({ + 'txInfo': registerInfo, + 'profileUrls': profileUploadInfo.profileUrls + }); + } + else { + return JSONStringify({ + 'error': profileUploadInfo.error + }, true); + } + }); + } + else { + return Promise.resolve().then(() => JSONStringify(safetyChecks, true)); + } + }); +} + +/* + * Sign a profile. + * @path (string) path to the profile + * @privateKey (string) the owner key (must be single-sig) + */ +function profileSign(network: CLINetworkAdapter, args: string[]) : Promise { + const profilePath = args[0]; + const profileData = JSON.parse(fs.readFileSync(profilePath).toString()); + return Promise.resolve().then(() => makeProfileJWT(profileData, args[1])); +} + +/* + * Verify a profile with an address or public key + * @path (string) path to the profile + * @publicKeyOrAddress (string) public key or address + */ +function profileVerify(network: CLINetworkAdapter, args: string[]) : Promise { + const profilePath = args[0]; + let publicKeyOrAddress = args[1]; + + // need to coerce mainnet + if (publicKeyOrAddress.match(ID_ADDRESS_PATTERN)) { + publicKeyOrAddress = network.coerceMainnetAddress(publicKeyOrAddress.slice(3)); + } + + const profileString = fs.readFileSync(profilePath).toString(); + + return Promise.resolve().then(() => { + let profileToken = null; + + try { + const profileTokens = JSON.parse(profileString); + profileToken = profileTokens[0].token; + } + catch (e) { + // might be a raw token + profileToken = profileString; + } + + if (!profileToken) { + throw new Error(`Data at ${profilePath} does not appear to be a signed profile`); + } + + const profile = blockstack.extractProfile(profileToken, publicKeyOrAddress); + return JSONStringify(profile); + }); +} + + +/* + * Store a signed profile for a name or an address. + * * verify that the profile was signed by the name's owner address + * * verify that the private key matches the name's owner address + * + * Assumes that the URI records are all Gaia hubs + * + * @nameOrAddress (string) name or address that owns the profile + * @path (string) path to the signed profile token + * @privateKey (string) owner private key for the name + * @gaiaUrl (string) this is the write endpoint of the Gaia hub to use + */ +function profileStore(network: CLINetworkAdapter, args: string[]) : Promise { + const nameOrAddress = args[0]; + const signedProfilePath = args[1]; + const privateKey = decodePrivateKey(args[2]); + const gaiaHubUrl = args[3]; + + const signedProfileData = fs.readFileSync(signedProfilePath).toString(); + + const ownerAddress = getPrivateKeyAddress(network, privateKey); + const ownerAddressMainnet = network.coerceMainnetAddress(ownerAddress); + + let nameInfoPromise : Promise<{address: string}>; + let name = ''; + + if (nameOrAddress.startsWith('ID-')) { + // ID-address + nameInfoPromise = Promise.resolve().then(() => { + return { + 'address': nameOrAddress.slice(3) + }; + }); + } + else { + // name; find the address + nameInfoPromise = getNameInfoEasy(network, nameOrAddress); + name = nameOrAddress; + } + + const verifyProfilePromise = profileVerify(network, + [signedProfilePath, `ID-${ownerAddressMainnet}`]); + + return Promise.all([nameInfoPromise, verifyProfilePromise]) + .then(([nameInfo, _verifiedProfile] : [NameInfoType, any]) => { + if (safetyChecks && (!nameInfo || + network.coerceAddress(nameInfo.address) !== network.coerceAddress(ownerAddress))) { + throw new Error('Name owner address either could not be found, or does not match ' + + `private key address ${ownerAddress}`); + } + return gaiaUploadProfileAll( + network, [gaiaHubUrl], signedProfileData, args[2], name); + }) + .then((gaiaUrls : {dataUrls?: string[], error?: string}) => { + if (gaiaUrls.hasOwnProperty('error')) { + return JSONStringify(gaiaUrls, true); + } + else { + return JSONStringify({'profileUrls': gaiaUrls.dataUrls}); + } + }); +} + +/* + * Push a zonefile to the Atlas network + * @zonefileDataOrPath (string) the zonefile data to push, or the path to the data + */ +function zonefilePush(network: CLINetworkAdapter, args: string[]) : Promise { + const zonefileDataOrPath = args[0]; + let zonefileData = null; + + try { + zonefileData = fs.readFileSync(zonefileDataOrPath).toString(); + } catch(e) { + zonefileData = zonefileDataOrPath; + } + + return network.broadcastZoneFile(zonefileData) + .then((result : any) => { + return JSONStringify(result); + }); +} + +/* + * Get the app private key(s) from a backup phrase and an ID-address + * args: + * @mnemonic (string) the 12-word phrase + * @nameOrIDAddress (string) the name or ID-address + * @appOrigin (string) the application's origin URL + */ +async function getAppKeys(network: CLINetworkAdapter, args: string[]) : Promise { + const mnemonic = await getBackupPhrase(args[0]); + const nameOrIDAddress = args[1]; + const origin = args[2]; + const idAddress = await getIDAddress(network, nameOrIDAddress); + const networkInfo = await getApplicationKeyInfo(network, mnemonic, idAddress, origin); + return JSONStringify(networkInfo); +} + +/* + * Get the owner private key(s) from a backup phrase + * args: + * @mnemonic (string) the 12-word phrase + * @max_index (integer) (optional) the profile index maximum + */ +async function getOwnerKeys(network: CLINetworkAdapter, args: string[]) : Promise { + const mnemonic = await getBackupPhrase(args[0]); + let maxIndex = 1; + if (args.length > 1 && !!args[1]) { + maxIndex = parseInt(args[1]); + } + + const keyInfo: OwnerKeyInfoType[] = []; + for (let i = 0; i < maxIndex; i++) { + keyInfo.push(await getOwnerKeyInfo(network, mnemonic, i)); + } + + return JSONStringify(keyInfo); +} + +/* + * Get the payment private key from a backup phrase + * args: + * @mnemonic (string) the 12-word phrase + */ +async function getPaymentKey(network: CLINetworkAdapter, args: string[]) : Promise { + const mnemonic = await getBackupPhrase(args[0]); + // keep the return value consistent with getOwnerKeys + const keyObj = await getPaymentKeyInfo(network, mnemonic); + const keyInfo: PaymentKeyInfoType[] = []; + keyInfo.push(keyObj); + return JSONStringify(keyInfo); +} + +/* + * Get the payment private key from a backup phrase used by the Stacks wallet + * args: + * @mnemonic (string) the 24-word phrase + */ +async function getStacksWalletKey(network: CLINetworkAdapter, args: string[]) : Promise { + const mnemonic = await getBackupPhrase(args[0]); + // keep the return value consistent with getOwnerKeys + const keyObj = await getStacksWalletKeyInfo(network, mnemonic); + const keyInfo: StacksKeyInfoType[] = []; + keyInfo.push(keyObj); + return JSONStringify(keyInfo); +} + +/* + * Make a private key and output it + * args: + * @mnemonic (string) OPTIONAL; the 12-word phrase + */ +async function makeKeychain(network: CLINetworkAdapter, args: string[]) : Promise { + let mnemonic: string; + if (args[0]) { + mnemonic = await getBackupPhrase(args[0]); + } else { + mnemonic = await bip39.generateMnemonic( + STX_WALLET_COMPATIBLE_SEED_STRENGTH, + crypto.randomBytes + ); + } + + const stacksKeyInfo = await getStacksWalletKeyInfo(network, mnemonic); + return JSONStringify({ + 'mnemonic': mnemonic, + 'keyInfo': stacksKeyInfo + }); +} + +/* + * Get an address's tokens and their balances. + * Takes either a Bitcoin or Stacks address + * args: + * @address (string) the address + */ +function balance(network: CLINetworkAdapter, args: string[]) : Promise { + let address = args[0]; + + if (BLOCKSTACK_TEST) { + // force testnet address if we're in regtest or testnet mode + address = network.coerceAddress(address); + } + + // temporary hack to use network config from stacks-transactions lib + const txNetwork = network.isMainnet() ? new StacksMainnet() : new StacksTestnet(); + txNetwork.coreApiUrl = network.blockstackAPIUrl; + + return fetch(txNetwork.getAccountApiUrl(address)) + .then((response) => response.json()) + .then((response) => { + let balanceHex = response.balance; + if(balanceHex.startsWith('0x')) { + balanceHex = balanceHex.substr(2); + } + const balance = new BN(balanceHex, 16); + const res = { + balance: balance.toString(10), + nonce: response.nonce + }; + return Promise.resolve(JSONStringify(res)); + }); +} + +/* + * Get a page of the account's history + * args: + * @address (string) the account address + * @page (int) the page of the history to fetch (optional) + */ +function getAccountHistory(network: CLINetworkAdapter, args: string[]) : Promise { + const address = c32check.c32ToB58(args[0]); + + if (args.length >= 2 && !!args[1]) { + const page = parseInt(args[1]); + return Promise.resolve().then(() => { + return network.getAccountHistoryPage(address, page); + }) + .then(accountStates => JSONStringify(accountStates.map((s : any) => { + const new_s = { + address: c32check.b58ToC32(s.address), + credit_value: s.credit_value.toString(), + debit_value: s.debit_value.toString() + }; + return new_s; + }))); + } + else { + // all pages + let history : any[] = []; + + function getAllAccountHistoryPages(page: number) : Promise { + return network.getAccountHistoryPage(address, page) + .then((results : any[]) => { + if (results.length == 0) { + return history; + } + else { + history = history.concat(results); + return getAllAccountHistoryPages(page + 1); + } + }); + } + + return getAllAccountHistoryPages(0) + .then((accountStates: any[]) => JSONStringify(accountStates.map((s : any) => { + const new_s = { + address: c32check.b58ToC32(s.address), + credit_value: s.credit_value.toString(), + debit_value: s.debit_value.toString() + }; + return new_s; + }))); + } +} + +/* + * Get the account's state(s) at a particular block height + * args: + * @address (string) the account address + * @blockHeight (int) the height at which to query + */ +function getAccountAt(network: CLINetworkAdapter, args: string[]) : Promise { + const address = c32check.c32ToB58(args[0]); + const blockHeight = parseInt(args[1]); + + return Promise.resolve().then(() => { + return network.getAccountAt(address, blockHeight); + }) + .then(accountStates => accountStates.map((s : any) => { + const new_s = { + address: c32check.b58ToC32(s.address), + credit_value: s.credit_value.toString(), + debit_value: s.debit_value.toString() + }; + return new_s; + })) + .then(history => JSONStringify(history)); +} + +/* + * Sends BTC from one private key to another address + * args: + * @recipientAddress (string) the recipient's address + * @amount (string) the amount of BTC to send + * @privateKey (string) the private key that owns the BTC + */ +function sendBTC(network: CLINetworkAdapter, args: string[]) : Promise { + const destinationAddress = args[0]; + const amount = parseInt(args[1]); + const paymentKeyHex = decodePrivateKey(args[2]); + + if (amount <= 5500) { + throw new Error('Invalid amount (must be greater than 5500)'); + } + + let paymentKey; + if (typeof paymentKeyHex === 'string') { + // single-sig + paymentKey = blockstack.PubkeyHashSigner.fromHexString(paymentKeyHex); + } + else { + // multi-sig or segwit + paymentKey = paymentKeyHex; + } + + const txPromise = blockstack.transactions.makeBitcoinSpend(destinationAddress, paymentKey, amount, !hasKeys(paymentKeyHex)) + .catch((e : Error) => { + if (e.name === 'InvalidAmountError') { + return JSONStringify({ + 'status': false, + 'error': e.message + }, true); + } + else { + throw e; + } + }); + + if (txOnly) { + return txPromise; + } + else { + return txPromise.then((tx : string) => { + return network.broadcastTransaction(tx); + }) + .then((txid : string) => { + return txid; + }); + } +} + + +/* + * Send tokens from one account private key to another account's address. + * args: + * @recipientAddress (string) the recipient's account address + * @tokenAmount (int) the number of tokens to send + * @fee (int) the transaction fee to be paid + * @nonce (int) integer nonce needs to be incremented after each transaction from an account + * @privateKey (string) the hex-encoded private key to use to send the tokens + * @memo (string) OPTIONAL: a 34-byte memo to include + */ +async function sendTokens(network: CLINetworkAdapter, args: string[]) : Promise { + const recipientAddress = args[0]; + const tokenAmount = new BN(args[1]); + const fee = new BN(args[2]); + const nonce = new BN(args[3]); + const privateKey = args[4]; + + let memo = ''; + + if (args.length > 4 && !!args[5]) { + memo = args[5]; + } + + // temporary hack to use network config from stacks-transactions lib + const txNetwork = network.isMainnet() ? new StacksMainnet() : new StacksTestnet(); + txNetwork.coreApiUrl = network.blockstackAPIUrl; + + const options: TokenTransferOptions = { + recipient: recipientAddress, + amount: tokenAmount, + senderKey: privateKey, + fee, + nonce, + memo, + network: txNetwork + } + + const tx = await makeSTXTokenTransfer(options); + + if (estimateOnly) { + return estimateTransfer(tx, txNetwork).then((cost) => { + return cost.toString(10) + }) + } + + if (txOnly) { + return Promise.resolve(tx.serialize().toString('hex')); + } + + return broadcastTransaction(tx, txNetwork).then((response) => { + return { + txid: tx.txid(), + transaction: generateExplorerTxPageUrl(tx.txid(), txNetwork), + }; + }).catch((error) => { + return error.toString(); + }); +} + +/* + * Depoly a Clarity smart contract. + * args: + * @source (string) path to the contract source file + * @contractName (string) the name of the contract + * @fee (int) the transaction fee to be paid + * @nonce (int) integer nonce needs to be incremented after each transaction from an account + * @privateKey (string) the hex-encoded private key to use to send the tokens + */ +async function contractDeploy(network: CLINetworkAdapter, args: string[]) : Promise { + const sourceFile = args[0]; + const contractName = args[1]; + const fee = new BN(args[2]); + const nonce = new BN(args[3]); + const privateKey = args[4]; + + const source = fs.readFileSync(sourceFile).toString(); + + // temporary hack to use network config from stacks-transactions lib + const txNetwork = network.isMainnet() ? new StacksMainnet() : new StacksTestnet(); + txNetwork.coreApiUrl = network.blockstackAPIUrl; + + const options: ContractDeployOptions = { + contractName, + codeBody: source, + senderKey: privateKey, + fee, + nonce, + network: txNetwork, + postConditionMode: PostConditionMode.Allow + } + + const tx = await makeContractDeploy(options); + + if (estimateOnly) { + return estimateContractDeploy(tx, txNetwork).then((cost) => { + return cost.toString(10) + }) + } + + if (txOnly) { + return Promise.resolve(tx.serialize().toString('hex')); + } + + return broadcastTransaction(tx, txNetwork).then(() => { + return { + txid: tx.txid(), + transaction: generateExplorerTxPageUrl(tx.txid(), txNetwork), + }; + }).catch((error) => { + return error.toString(); + }); +} + +/* + * Call a Clarity smart contract function. + * args: + * @contractAddress (string) the address of the contract + * @contractName (string) the name of the contract + * @functionName (string) the name of the function to call + * @fee (int) the transaction fee to be paid + * @nonce (int) integer nonce needs to be incremented after each transaction from an account + * @privateKey (string) the hex-encoded private key to use to send the tokens + */ +async function contractFunctionCall(network: CLINetworkAdapter, args: string[]) : Promise { + const contractAddress = args[0]; + const contractName = args[1]; + const functionName = args[2]; + const fee = new BN(args[3]); + const nonce = new BN(args[4]); + const privateKey = args[5]; + + // temporary hack to use network config from stacks-transactions lib + const txNetwork = network.isMainnet() ? new StacksMainnet() : new StacksTestnet(); + txNetwork.coreApiUrl = network.blockstackAPIUrl; + + let abi: ClarityAbi; + let abiArgs: ClarityFunctionArg[]; + let functionArgs: ClarityValue[] = []; + + return getAbi( + contractAddress, + contractName, + txNetwork + ).then((responseAbi) => { + abi = responseAbi; + const filtered = abi.functions.filter(fn => fn.name === functionName); + if (filtered.length === 1) { + abiArgs = filtered[0].args; + return makePromptsFromArgList(abiArgs); + } else { + return null; + } + }) + .then((prompts) => inquirer.prompt(prompts)) + .then((answers) => { + functionArgs = parseClarityFunctionArgAnswers(answers, abiArgs); + + const options: ContractCallOptions = { + contractAddress, + contractName, + functionName, + functionArgs, + senderKey: privateKey, + fee, + nonce, + network: txNetwork, + postConditionMode: PostConditionMode.Allow + } + + return makeContractCall(options); + }).then((tx) => { + if (!validateContractCall(tx.payload as ContractCallPayload, abi)) { + throw new Error('Failed to validate function arguments against ABI'); + } + + if (estimateOnly) { + return estimateContractFunctionCall(tx, txNetwork).then((cost) => { + return cost.toString(10) + }) + } + + if (txOnly) { + return Promise.resolve(tx.serialize().toString('hex')); + } + + return broadcastTransaction(tx, txNetwork).then(() => { + return { + txid: tx.txid(), + transaction: generateExplorerTxPageUrl(tx.txid(), txNetwork), + }; + }).catch((error) => { + return error.toString(); + }); + }); +} + +/* + * Call a read-only Clarity smart contract function. + * args: + * @contractAddress (string) the address of the contract + * @contractName (string) the name of the contract + * @functionName (string) the name of the function to call + * @senderAddress (string) the sender address + */ +async function readOnlyContractFunctionCall(network: CLINetworkAdapter, args: string[]) : Promise { + const contractAddress = args[0]; + const contractName = args[1]; + const functionName = args[2]; + const senderAddress = args[3]; + + // temporary hack to use network config from stacks-transactions lib + const txNetwork = network.isMainnet() ? new StacksMainnet() : new StacksTestnet(); + txNetwork.coreApiUrl = network.blockstackAPIUrl; + + let abi: ClarityAbi; + let abiArgs: ClarityFunctionArg[]; + let functionArgs: ClarityValue[] = []; + + return getAbi( + contractAddress, + contractName, + txNetwork + ).then((responseAbi) => { + abi = responseAbi; + const filtered = abi.functions.filter(fn => fn.name === functionName); + if (filtered.length === 1) { + abiArgs = filtered[0].args; + return makePromptsFromArgList(abiArgs); + } else { + return null; + } + }) + .then((prompts) => inquirer.prompt(prompts)) + .then((answers) => { + functionArgs = parseClarityFunctionArgAnswers(answers, abiArgs); + + const options: ReadOnlyFunctionOptions = { + contractAddress, + contractName, + functionName, + functionArgs, + senderAddress, + network: txNetwork, + } + + return callReadOnlyFunction(options); + }).then((returnValue) => { + return cvToString(returnValue); + }).catch((error) => { + return error.toString(); + }); +} + +/* + * Get the number of confirmations of a txid. + * args: + * @txid (string) the transaction ID as a hex string + */ +function getConfirmations(network: CLINetworkAdapter, args: string[]) : Promise { + const txid = args[0]; + return Promise.all([network.getBlockHeight(), network.getTransactionInfo(txid)]) + .then(([blockHeight, txInfo]) => { + return JSONStringify({ + 'blockHeight': txInfo.block_height, + 'confirmations': blockHeight - txInfo.block_height + 1 + }); + }) + .catch((e) => { + if (e.message.toLowerCase() === 'unconfirmed transaction') { + return JSONStringify({ + 'blockHeight': 'unconfirmed', + 'confirmations': 0 + }); + } + else { + throw e; + } + }); +} + +/* + * Get the address of a private key + * args: + * @private_key (string) the hex-encoded private key or key bundle + */ +function getKeyAddress(network: CLINetworkAdapter, args: string[]) : Promise { + const privateKey = decodePrivateKey(args[0]); + return Promise.resolve().then(() => { + const addr = getPrivateKeyAddress(network, privateKey); + return JSONStringify({ + 'BTC': addr, + 'STACKS': c32check.b58ToC32(addr) + }); + }); +} + +function getDidConfiguration(network: CLINetworkAdapter, args: string[]) : Promise { + const privateKey = decodePrivateKey(args[2]); + return makeDIDConfiguration(network, args[0], args[1], args[2]).then(didConfiguration => { + return JSONStringify(didConfiguration); + }); +} + +/* + * Get a file from Gaia. + * args: + * @username (string) the blockstack ID of the user who owns the data + * @origin (string) the application origin + * @path (string) the file to read + * @appPrivateKey (string) OPTIONAL: the app private key to decrypt/verify with + * @decrypt (string) OPTINOAL: if '1' or 'true', then decrypt + * @verify (string) OPTIONAL: if '1' or 'true', then search for and verify a signature file + * along with the data + */ +function gaiaGetFile(network: CLINetworkAdapter, args: string[]) : Promise { + const username = args[0]; + const origin = args[1]; + const path = args[2]; + let appPrivateKey = args[3]; + let decrypt = false; + let verify = false; + + if (!!appPrivateKey && args.length > 4 && !!args[4]) { + decrypt = (args[4].toLowerCase() === 'true' || args[4].toLowerCase() === '1'); + } + + if (!!appPrivateKey && args.length > 5 && !!args[5]) { + verify = (args[5].toLowerCase() === 'true' || args[5].toLowerCase() === '1'); + } + + if (!appPrivateKey) { + // make a fake private key (it won't be used) + appPrivateKey = 'fda1afa3ff9ef25579edb5833b825ac29fae82d03db3f607db048aae018fe882'; + } + + // force mainnet addresses + blockstack.config.network.layer1 = bitcoin.networks.bitcoin; + return gaiaAuth(network, appPrivateKey, null) + .then((_userData : UserData) => blockstack.getFile(path, { + decrypt: decrypt, + verify: verify, + app: origin, + username: username})) + .then((data: ArrayBuffer | Buffer | string) => { + if (data instanceof ArrayBuffer) { + return Buffer.from(data); + } + else { + return data; + } + }); +} + +/* + * Put a file into a Gaia hub + * args: + * @hubUrl (string) the URL to the write endpoint of the gaia hub + * @appPrivateKey (string) the private key used to authenticate to the gaia hub + * @dataPath (string) the path (on disk) to the data to store + * @gaiaPath (string) the path (in Gaia) where the data will be stored + * @encrypt (string) OPTIONAL: if '1' or 'true', then encrypt the file + * @sign (string) OPTIONAL: if '1' or 'true', then sign the file and store the signature too. + */ +function gaiaPutFile(network: CLINetworkAdapter, args: string[]) : Promise { + const hubUrl = args[0]; + const appPrivateKey = args[1]; + const dataPath = args[2]; + const gaiaPath = path.normalize(args[3].replace(/^\/+/, '')); + + let encrypt = false; + let sign = false; + + if (args.length > 4 && !!args[4]) { + encrypt = (args[4].toLowerCase() === 'true' || args[4].toLowerCase() === '1'); + } + if (args.length > 5 && !!args[5]) { + sign = (args[5].toLowerCase() === 'true' || args[5].toLowerCase() === '1'); + } + + const data = fs.readFileSync(dataPath); + + // force mainnet addresses + // TODO + blockstack.config.network.layer1 = bitcoin.networks.bitcoin; + return gaiaAuth(network, appPrivateKey, hubUrl) + .then((_userData : UserData) => { + return blockstack.putFile(gaiaPath, data, { encrypt: encrypt, sign: sign }); + }) + .then((url : string) => { + return JSONStringify({'urls': [url]}); + }); +} + +/* + * Delete a file in a Gaia hub + * args: + * @hubUrl (string) the URL to the write endpoint of the gaia hub + * @appPrivateKey (string) the private key used to authenticate to the gaia hub + * @gaiaPath (string) the path (in Gaia) to delete + * @wasSigned (string) OPTIONAL: if '1' or 'true'. Delete the signature file as well. + */ +function gaiaDeleteFile(network: CLINetworkAdapter, args: string[]): Promise { + const hubUrl = args[0]; + const appPrivateKey = args[1]; + const gaiaPath = path.normalize(args[2].replace(/^\/+/, '')); + + let wasSigned = false; + + if (args.length > 3 && !!args[3]) { + wasSigned = (args[3].toLowerCase() === 'true' || args[3].toLowerCase() === '1'); + } + + // force mainnet addresses + // TODO + blockstack.config.network.layer1 = bitcoin.networks.bitcoin; + return gaiaAuth(network, appPrivateKey, hubUrl) + .then((_userData: UserData) => { + return blockstack.deleteFile(gaiaPath, {wasSigned: wasSigned}); + }) + .then(() => { + return JSONStringify('ok'); + }); +} + +/* + * List files in a Gaia hub + * args: + * @hubUrl (string) the URL to the write endpoint of the gaia hub + * @appPrivateKey (string) the private key used to authenticate to the gaia hub + */ +function gaiaListFiles(network: CLINetworkAdapter, args: string[]) : Promise { + const hubUrl = args[0]; + const appPrivateKey = args[1]; + + // force mainnet addresses + // TODO + let count = 0; + blockstack.config.network.layer1 = bitcoin.networks.bitcoin; + return gaiaAuth(network, canonicalPrivateKey(appPrivateKey), hubUrl) + .then((_userData : UserData) => { + return blockstack.listFiles((name : string) => { + // print out incrementally + console.log(name); + count += 1; + return true; + }); + }) + .then(() => JSONStringify(count)); +} + + +/* + * Group array items into batches + */ +function batchify(input: T[], batchSize: number = 50): T[][] { + const output = []; + let currentBatch = []; + for (let i = 0; i < input.length; i++) { + currentBatch.push(input[i]); + if (currentBatch.length >= batchSize) { + output.push(currentBatch); + currentBatch = []; + } + } + if (currentBatch.length > 0) { + output.push(currentBatch); + } + return output; +} + +/* + * Dump all files from a Gaia hub bucket to a directory on disk. + * args: + * @nameOrIDAddress (string) the name or ID address that owns the bucket to dump + * @appOrigin (string) the application for which to dump data + * @hubUrl (string) the URL to the write endpoint of the gaia hub + * @mnemonic (string) the 12-word phrase or ciphertext + * @dumpDir (string) the directory to hold the dumped files + */ +function gaiaDumpBucket(network: CLINetworkAdapter, args: string[]) : Promise { + const nameOrIDAddress = args[0]; + const appOrigin = args[1]; + const hubUrl = args[2]; + const mnemonicOrCiphertext = args[3]; + let dumpDir = args[4]; + + if (dumpDir.length === 0) { + throw new Error('Invalid directory (not given)'); + } + if (dumpDir[0] !== '/') { + // relative path. make absolute + const cwd = fs.realpathSync('.'); + dumpDir = path.normalize(`${cwd}/${dumpDir}`); + } + + mkdirs(dumpDir); + + function downloadFile(hubConfig: GaiaHubConfig, fileName: string) : Promise { + const gaiaReadUrl = `${hubConfig.url_prefix.replace(/\/+$/, '')}/${hubConfig.address}`; + const fileUrl = `${gaiaReadUrl}/${fileName}`; + const destPath = `${dumpDir}/${fileName.replace(/\//g, '\\x2f')}`; + + console.log(`Download ${fileUrl} to ${destPath}`); + return fetch(fileUrl) + .then((resp : any) => { + if (resp.status !== 200) { + throw new Error(`Bad status code for ${fileUrl}: ${resp.status}`); + } + + // javascript can be incredibly stupid at fetching data despite being a Web language... + const contentType = resp.headers.get('Content-Type'); + if (contentType === null + || contentType.startsWith('text') + || contentType === 'application/json') { + return resp.text(); + } else { + return resp.arrayBuffer(); + } + }) + .then((filebytes : Buffer | ArrayBuffer) => { + return new Promise((resolve, reject) => { + try { + fs.writeFileSync(destPath, Buffer.from(filebytes), { encoding: null, mode: 0o660 }); + resolve(); + } + catch(e) { + reject(e); + } + }); + }); + } + + // force mainnet addresses + // TODO: better way of doing this + blockstack.config.network.layer1 = bitcoin.networks.bitcoin; + + const fileNames: string[] = []; + let gaiaHubConfig : GaiaHubConfig; + let appPrivateKey : string; + let ownerPrivateKey : string; + + return getIDAppKeys(network, nameOrIDAddress, appOrigin, mnemonicOrCiphertext) + .then((keyInfo : IDAppKeys) => { + appPrivateKey = keyInfo.appPrivateKey; + ownerPrivateKey = keyInfo.ownerPrivateKey; + return gaiaAuth(network, appPrivateKey, hubUrl, ownerPrivateKey); + }) + .then((_userData : UserData) => { + return gaiaConnect(network, hubUrl, appPrivateKey); + }) + .then((hubConfig : GaiaHubConfig) => { + gaiaHubConfig = hubConfig; + return blockstack.listFiles((name) => { + fileNames.push(name); + return true; + }); + }) + .then((fileCount : number) => { + console.log(`Download ${fileCount} files...`); + const fileBatches : string[][] = batchify(fileNames); + let filePromiseChain : Promise = Promise.resolve(); + for (let i = 0; i < fileBatches.length; i++) { + const filePromises = fileBatches[i].map((fileName) => downloadFile(gaiaHubConfig, fileName)); + const batchPromise = Promise.all(filePromises); + filePromiseChain = filePromiseChain.then(() => batchPromise); + } + + return filePromiseChain.then(() => JSONStringify(fileCount)); + }); +} + +/* + * Restore all of the files in a Gaia bucket dump to a new Gaia hub + * args: + * @nameOrIDAddress (string) the name or ID address that owns the bucket to dump + * @appOrigin (string) the origin of the app for which to restore data + * @hubUrl (string) the URL to the write endpoint of the new gaia hub + * @mnemonic (string) the 12-word phrase or ciphertext + * @dumpDir (string) the directory to hold the dumped files + */ +function gaiaRestoreBucket(network: CLINetworkAdapter, args: string[]) : Promise { + const nameOrIDAddress = args[0]; + const appOrigin = args[1]; + const hubUrl = args[2]; + const mnemonicOrCiphertext = args[3]; + let dumpDir = args[4]; + + if (dumpDir.length === 0) { + throw new Error('Invalid directory (not given)'); + } + if (dumpDir[0] !== '/') { + // relative path. make absolute + const cwd = fs.realpathSync('.'); + dumpDir = path.normalize(`${cwd}/${dumpDir}`); + } + + const fileList = fs.readdirSync(dumpDir); + const fileBatches = batchify(fileList, 10); + + let appPrivateKey : string; + let ownerPrivateKey : string; + + // force mainnet addresses + // TODO better way of doing this + blockstack.config.network.layer1 = bitcoin.networks.bitcoin; + + return getIDAppKeys(network, nameOrIDAddress, appOrigin, mnemonicOrCiphertext) + .then((keyInfo : IDAppKeys) => { + appPrivateKey = keyInfo.appPrivateKey; + ownerPrivateKey = keyInfo.ownerPrivateKey; + return gaiaAuth(network, appPrivateKey, hubUrl, ownerPrivateKey); + }) + .then((_userData : UserData) => { + let uploadPromise : Promise = Promise.resolve(); + for (let i = 0; i < fileBatches.length; i++) { + const uploadBatchPromises = fileBatches[i].map((fileName : string) => { + const filePath = path.join(dumpDir, fileName); + const dataBuf = fs.readFileSync(filePath); + const gaiaPath = fileName.replace(/\\x2f/g, '/'); + return blockstack.putFile(gaiaPath, dataBuf, { encrypt: false, sign: false }) + .then((url : string) => { + console.log(`Uploaded ${fileName} to ${url}`); + }); + }); + uploadPromise = uploadPromise.then(() => Promise.all(uploadBatchPromises)); + } + return uploadPromise; + }) + .then(() => JSONStringify(fileList.length)); +} + +/* + * Set the Gaia hub for an application for a blockstack ID. + * args: + * @blockstackID (string) the blockstack ID of the user + * @profileHubUrl (string) the URL to the write endpoint of the user's profile gaia hub + * @appOrigin (string) the application's Origin + * @hubUrl (string) the URL to the write endpoint of the app's gaia hub + * @mnemonic (string) the 12-word backup phrase, or the ciphertext of it + */ +async function gaiaSetHub(network: CLINetworkAdapter, args: string[]) : Promise { + network.setCoerceMainnetAddress(true); + + const blockstackID = args[0]; + const ownerHubUrl = args[1]; + const appOrigin = args[2]; + const hubUrl = args[3]; + const mnemonicPromise = getBackupPhrase(args[4]); + + const nameInfoPromise = getNameInfoEasy(network, blockstackID) + .then((nameInfo : NameInfoType) => { + if (!nameInfo) { + throw new Error('Name not found'); + } + return nameInfo; + }); + + const profilePromise = blockstack.lookupProfile(blockstackID); + + const [nameInfo, nameProfile, mnemonic]: [NameInfoType, any, string] = + await Promise.all([nameInfoPromise, profilePromise, mnemonicPromise]); + + if (!nameProfile) { + throw new Error('No profile found'); + } + if (!nameInfo) { + throw new Error('Name not found'); + } + if (!nameInfo.zonefile) { + throw new Error('No zone file found'); + } + + if (!nameProfile.apps) { + nameProfile.apps = {}; + } + + // get owner ID-address + const ownerAddress = network.coerceMainnetAddress(nameInfo.address); + const idAddress = `ID-${ownerAddress}`; + + // get owner and app key info + const appKeyInfo = await getApplicationKeyInfo(network, mnemonic, idAddress, appOrigin); + const ownerKeyInfo = await getOwnerKeyInfo(network, mnemonic, appKeyInfo.ownerKeyIndex); + + // do we already have an address set for this app? + let existingAppAddress : string; + let appPrivateKey : string; + try { + existingAppAddress = getGaiaAddressFromProfile(network, nameProfile, appOrigin); + appPrivateKey = extractAppKey(network, appKeyInfo, existingAppAddress); + } + catch (e) { + console.log(`No profile application entry for ${appOrigin}`); + appPrivateKey = extractAppKey(network, appKeyInfo); + } + + appPrivateKey = `${canonicalPrivateKey(appPrivateKey)}01`; + const appAddress = network.coerceMainnetAddress(getPrivateKeyAddress(network, appPrivateKey)); + + if (existingAppAddress && appAddress !== existingAppAddress) { + throw new Error(`BUG: ${existingAppAddress} !== ${appAddress}`); + } + + const profile = nameProfile; + const ownerPrivateKey = ownerKeyInfo.privateKey; + + const ownerGaiaHubPromise = gaiaConnect(network, ownerHubUrl, ownerPrivateKey); + const appGaiaHubPromise = gaiaConnect(network, hubUrl, appPrivateKey); + + const [ownerHubConfig, appHubConfig] : [GaiaHubConfig, GaiaHubConfig] = + await Promise.all([ownerGaiaHubPromise, appGaiaHubPromise]); + + if (!ownerHubConfig.url_prefix) { + throw new Error('Invalid owner hub config: no url_prefix defined'); + } + + if (!appHubConfig.url_prefix) { + throw new Error('Invalid app hub config: no url_prefix defined'); + } + + const gaiaReadUrl = appHubConfig.url_prefix.replace(/\/+$/, ''); + + const newAppEntry : Record = {}; + newAppEntry[appOrigin] = `${gaiaReadUrl}/${appAddress}/`; + + const apps = Object.assign({}, profile.apps ? profile.apps : {}, newAppEntry); + profile.apps = apps; + + // sign the new profile + const signedProfile = makeProfileJWT(profile, ownerPrivateKey); + const profileUrls : {dataUrls?: string[], error?: string} = await gaiaUploadProfileAll( + network, [ownerHubUrl], signedProfile, ownerPrivateKey, blockstackID); + + if (profileUrls.error) { + return JSONStringify({ + error: profileUrls.error + }); + } + else { + return JSONStringify({ + profileUrls: profileUrls.dataUrls + }); + } +} + + +/* + * Convert an address between mainnet and testnet, and between + * base58check and c32check. + * args: + * @address (string) the input address. can be in any format + */ +function addressConvert(network: CLINetworkAdapter, args: string[]) : Promise { + const addr = args[0]; + let b58addr : string; + let c32addr : string; + let testnetb58addr : string; + let testnetc32addr : string; + + if (addr.match(STACKS_ADDRESS_PATTERN)) { + c32addr = addr; + b58addr = c32check.c32ToB58(c32addr); + } + else if (addr.match(/[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+/)) { + c32addr = c32check.b58ToC32(addr); + b58addr = addr; + } + else { + throw new Error(`Unrecognized address ${addr}`); + } + + if (network.isTestnet()) { + testnetb58addr = network.coerceAddress(b58addr); + testnetc32addr = c32check.b58ToC32(testnetb58addr); + } + + return Promise.resolve().then(() => { + const result : any = { + mainnet: { + STACKS: c32addr, + BTC: b58addr + }, + testnet: undefined + }; + + if (network.isTestnet()) { + result.testnet = { + STACKS: testnetc32addr, + BTC: testnetb58addr + }; + } + + return JSONStringify(result); + }); +} + +/* + * Run an authentication daemon on a given port. + * args: + * @gaiaHubUrl (string) the write endpoint of your app Gaia hub, where app data will be stored + * @mnemonic (string) your 12-word phrase, optionally encrypted. If encrypted, then + * a password will be prompted. + * @profileGaiaHubUrl (string) the write endpoint of your profile Gaia hub, where your profile + * will be stored (optional) + * @port (number) the port to listen on (optional) + */ +function authDaemon(network: CLINetworkAdapter, args: string[]) : Promise { + const gaiaHubUrl = args[0]; + const mnemonicOrCiphertext = args[1]; + let port = 3000; // default port + let profileGaiaHub = gaiaHubUrl; + + if (args.length > 2 && !!args[2]) { + profileGaiaHub = args[2]; + } + + if (args.length > 3 && !!args[3]) { + port = parseInt(args[3]); + } + + if (port < 0 || port > 65535) { + return Promise.resolve().then(() => JSONStringify({ error: 'Invalid port' })); + } + + const mnemonicPromise = getBackupPhrase(mnemonicOrCiphertext); + + return mnemonicPromise + .then((mnemonic : string) => { + noExit = true; + + // load up all of our identity addresses, profiles, profile URLs, and Gaia connections + const authServer = express(); + authServer.use(cors()); + + authServer.get(/^\/auth\/*$/, (req: express.Request, res: express.Response) => { + return handleAuth(network, mnemonic, gaiaHubUrl, profileGaiaHub, port, req, res); + }); + + authServer.get(/^\/signin\/*$/, (req: express.Request, res: express.Response) => { + return handleSignIn(network, mnemonic, gaiaHubUrl, profileGaiaHub, req, res); + }); + + authServer.listen(port, () => console.log(`Authentication server started on ${port}`)); + return 'Press Ctrl+C to exit'; + }) + .catch((e : Error) => { + return JSONStringify({ error: e.message }); + }); +} + +/* + * Encrypt a backup phrase + * args: + * @backup_phrase (string) the 12-word phrase to encrypt + * @password (string) the password (will be interactively prompted if not given) + */ +function encryptMnemonic(network: CLINetworkAdapter, args: string[]) : Promise { + const mnemonic = args[0]; + if (mnemonic.split(/ +/g).length !== 12) { + throw new Error('Invalid backup phrase: must be 12 words'); + } + + const passwordPromise = new Promise((resolve, reject) => { + let pass = ''; + if (args.length === 2 && !!args[1]) { + pass = args[1]; + resolve(pass); + } + else { + if (!process.stdin.isTTY) { + // password must be given as an argument + const errMsg = 'Password argument required on non-interactive mode'; + reject(new Error(errMsg)); + } + else { + // prompt password + getpass('Enter password: ', (pass1 : string) => { + getpass('Enter password again: ', (pass2 : string) => { + if (pass1 !== pass2) { + const errMsg = 'Passwords do not match'; + reject(new Error(errMsg)); + } + else { + resolve(pass1); + } + }); + }); + } + } + }); + + return passwordPromise + .then((pass : string) => encryptBackupPhrase(mnemonic, pass)) + .then((cipherTextBuffer : Buffer) => cipherTextBuffer.toString('base64')) + .catch((e : Error) => { + return JSONStringify({ error: e.message}); + }); +} + +/* Decrypt a backup phrase + * args: + * @encrypted_backup_phrase (string) the encrypted base64-encoded backup phrase + * @password 9string) the password (will be interactively prompted if not given) + */ +function decryptMnemonic(network: CLINetworkAdapter, args: string[]) : Promise { + const ciphertext = args[0]; + + const passwordPromise = new Promise((resolve, reject) => { + if (args.length === 2 && !!args[1]) { + const pass = args[1]; + resolve(pass); + } + else { + if (!process.stdin.isTTY) { + // password must be given + reject(new Error('Password argument required in non-interactive mode')); + } + else { + // prompt password + getpass('Enter password: ', (p) => { + resolve(p); + }); + } + } + }); + + return passwordPromise + .then((pass : string) => decryptBackupPhrase(Buffer.from(ciphertext, 'base64'), pass)) + .catch((e : Error) => { + return JSONStringify({ error: 'Failed to decrypt (wrong password or corrupt ciphertext), ' + + `details: ${e.message}` }); + }); +} + +/* Print out all documentation on usage in JSON + */ +type DocsArgsType = { + name: string; + type: string; + value: string; + format: string; +}; + +type FormattedDocsType = { + command: string; + args: DocsArgsType[]; + usage: string; + group: string +}; + +function printDocs(_network: CLINetworkAdapter, _args: string[]) : Promise { + return Promise.resolve().then(() => { + const formattedDocs : FormattedDocsType[] = []; + const commandNames : string[] = Object.keys(CLI_ARGS.properties); + for (let i = 0; i < commandNames.length; i++) { + const commandName = commandNames[i]; + const args : DocsArgsType[] = []; + const usage = CLI_ARGS.properties[commandName].help; + const group = CLI_ARGS.properties[commandName].group; + + for (let j = 0; j < CLI_ARGS.properties[commandName].items.length; j++) { + const argItem = CLI_ARGS.properties[commandName].items[j]; + args.push({ + name: argItem.name, + type: argItem.type, + value: argItem.realtype, + format: argItem.pattern ? argItem.pattern : '.+' + } as DocsArgsType); + } + + formattedDocs.push({ + command: commandName, + args: args, + usage: usage, + group: group + } as FormattedDocsType); + } + return JSONStringify(formattedDocs); + }); +} + +type CommandFunction = (network: CLINetworkAdapter, args: string[]) => Promise; + +/* + * Decrypt a backup phrase + * args: + * @p +/* + * Global set of commands + */ +const COMMANDS : Record = { + 'authenticator': authDaemon, + 'announce': announce, + 'balance': balance, + 'call_contract_func': contractFunctionCall, + 'call_read_only_contract_func': readOnlyContractFunctionCall, + 'convert_address': addressConvert, + 'decrypt_keychain': decryptMnemonic, + 'deploy_contract': contractDeploy, + 'docs': printDocs, + 'encrypt_keychain': encryptMnemonic, + 'gaia_deletefile': gaiaDeleteFile, + 'gaia_dump_bucket': gaiaDumpBucket, + 'gaia_getfile': gaiaGetFile, + 'gaia_listfiles': gaiaListFiles, + 'gaia_putfile': gaiaPutFile, + 'gaia_restore_bucket': gaiaRestoreBucket, + 'gaia_sethub': gaiaSetHub, + 'get_address': getKeyAddress, + 'get_account_at': getAccountAt, + 'get_account_history': getAccountHistory, + 'get_blockchain_record': getNameBlockchainRecord, + 'get_blockchain_history': getNameHistoryRecord, + 'get_confirmations': getConfirmations, + 'get_did_configuration': getDidConfiguration, + 'get_namespace_blockchain_record': getNamespaceBlockchainRecord, + 'get_app_keys': getAppKeys, + 'get_owner_keys': getOwnerKeys, + 'get_payment_key': getPaymentKey, + 'get_stacks_wallet_key': getStacksWalletKey, + 'get_zonefile': getZonefile, + 'lookup': lookup, + 'make_keychain': makeKeychain, + 'make_zonefile': makeZonefile, + 'names': names, + 'name_import': nameImport, + 'namespace_preorder': namespacePreorder, + 'namespace_reveal': namespaceReveal, + 'namespace_ready': namespaceReady, + 'price': price, + 'price_namespace': priceNamespace, + 'profile_sign': profileSign, + 'profile_store': profileStore, + 'profile_verify': profileVerify, + 'register': register, + 'register_addr': registerAddr, + 'register_subdomain': registerSubdomain, + 'renew': renew, + 'revoke': revoke, + 'send_btc': sendBTC, + 'send_tokens': sendTokens, + 'transfer': transfer, + 'tx_preorder': txPreorder, + 'tx_register': txRegister, + 'update': update, + 'whois': whois, + 'zonefile_push': zonefilePush +}; + +/* + * CLI main entry point + */ +export function CLIMain() { + const argv = process.argv; + const opts = getCLIOpts(argv); + + const cmdArgs : any = checkArgs(CLIOptAsStringArray(opts, '_') ? CLIOptAsStringArray(opts, '_') : []); + if (!cmdArgs.success) { + if (cmdArgs.error) { + console.log(cmdArgs.error); + } + if (cmdArgs.usage) { + if (cmdArgs.command) { + console.log(makeCommandUsageString(cmdArgs.command)); + console.log('Use "help" to list all commands.'); + } + else { + console.log(USAGE); + console.log(makeAllCommandsList()); + } + } + process.exit(1); + } + else { + txOnly = CLIOptAsBool(opts, 'x'); + estimateOnly = CLIOptAsBool(opts, 'e'); + safetyChecks = !CLIOptAsBool(opts, 'U'); + receiveFeesPeriod = opts['N'] ? + parseInt(CLIOptAsString(opts, 'N')) : receiveFeesPeriod; + gracePeriod = opts['G'] ? + parseInt(CLIOptAsString(opts, 'N')) : gracePeriod; + maxIDSearchIndex = opts['M'] ? + parseInt(CLIOptAsString(opts, 'M')) : maxIDSearchIndex; + + const debug = CLIOptAsBool(opts, 'd'); + const consensusHash = CLIOptAsString(opts, 'C'); + const integration_test = CLIOptAsBool(opts, 'i'); + const testnet = CLIOptAsBool(opts, 't'); + const magicBytes = CLIOptAsString(opts, 'm'); + const apiUrl = CLIOptAsString(opts, 'H'); + const transactionBroadcasterUrl = CLIOptAsString(opts, 'T'); + const nodeAPIUrl = CLIOptAsString(opts, 'I'); + const utxoUrl = CLIOptAsString(opts, 'X'); + const bitcoindUsername = CLIOptAsString(opts, 'u'); + const bitcoindPassword = CLIOptAsString(opts, 'p'); + + if (integration_test) { + BLOCKSTACK_TEST = integration_test; + } + + const configPath = CLIOptAsString(opts, 'c') ? CLIOptAsString(opts, 'c') : + (integration_test ? DEFAULT_CONFIG_REGTEST_PATH : + (testnet ? DEFAULT_CONFIG_TESTNET_PATH : DEFAULT_CONFIG_PATH)); + + const namespaceBurnAddr = CLIOptAsString(opts, 'B'); + const feeRate = CLIOptAsString(opts, 'F') ? parseInt(CLIOptAsString(opts, 'F')) : 0; + const priceToPay = CLIOptAsString(opts, 'P') ? CLIOptAsString(opts, 'P') : '0'; + const priceUnits = CLIOptAsString(opts, 'D'); + + const networkType = testnet ? 'testnet' : (integration_test ? 'regtest' : 'mainnet'); + + const configData = loadConfig(configPath, networkType); + + if (debug) { + configData.logConfig.level = 'debug'; + } + else { + configData.logConfig.level = 'info'; + } + if (bitcoindUsername) { + configData.bitcoindUsername = bitcoindUsername; + } + if (bitcoindPassword) { + configData.bitcoindPassword = bitcoindPassword; + } + + if (utxoUrl) { + configData.utxoServiceUrl = utxoUrl; + } + + winston.configure({ level: configData.logConfig.level, transports: [new winston.transports.Console(configData.logConfig)] }); + + const cliOpts : CLI_NETWORK_OPTS = { + consensusHash: consensusHash ? consensusHash : null, + feeRate: feeRate ? feeRate : null, + namespaceBurnAddress: namespaceBurnAddr ? namespaceBurnAddr : null, + priceToPay: priceToPay ? priceToPay : null, + priceUnits: priceUnits ? priceUnits : null, + receiveFeesPeriod: receiveFeesPeriod ? receiveFeesPeriod : null, + gracePeriod: gracePeriod ? gracePeriod : null, + altAPIUrl: (apiUrl ? apiUrl : configData.blockstackAPIUrl), + altTransactionBroadcasterUrl: (transactionBroadcasterUrl ? + transactionBroadcasterUrl : + configData.broadcastServiceUrl), + nodeAPIUrl: (nodeAPIUrl ? nodeAPIUrl : configData.blockstackNodeUrl) + }; + + // wrap command-line options + const wrappedNetwork = getNetwork(configData, (!!BLOCKSTACK_TEST || !!integration_test || !!testnet)); + const blockstackNetwork = new CLINetworkAdapter(wrappedNetwork, cliOpts); + if (magicBytes) { + blockstackNetwork.MAGIC_BYTES = magicBytes; + } + + blockstack.config.network = blockstackNetwork; + blockstack.config.logLevel = 'error'; + + if (cmdArgs.command === 'help') { + console.log(makeCommandUsageString(cmdArgs.args[0])); + process.exit(0); + } + + const method = COMMANDS[cmdArgs.command]; + let exitcode = 0; + + method(blockstackNetwork, cmdArgs.args) + .then((result : string | Buffer) => { + try { + // if this is a JSON object with 'status', set the exit code + if (result instanceof Buffer) { + return result; + } + else { + const resJson : any = JSON.parse(result); + if (resJson.hasOwnProperty('status') && !resJson.status) { + exitcode = 1; + } + return result; + } + } + catch(e) { + return result; + } + }) + .then((result : string | Buffer) => { + if (result instanceof Buffer) { + process.stdout.write(result); + } + else { + console.log(result); + } + }) + .then(() => { + if (!noExit) { + process.exit(exitcode); + } + }) + .catch((e : Error) => { + console.error(e.stack); + console.error(e.message); + if (!noExit) { + process.exit(1); + } + }); + } +} diff --git a/packages/cli/src/data.ts b/packages/cli/src/data.ts new file mode 100644 index 000000000..a5c40b73b --- /dev/null +++ b/packages/cli/src/data.ts @@ -0,0 +1,356 @@ +import * as blockstack from 'blockstack'; +import * as URL from 'url'; +import * as crypto from 'crypto'; +import * as jsontokens from 'jsontokens'; + +const ZoneFile = require('zone-file'); + +import { + canonicalPrivateKey, + getPrivateKeyAddress, + checkUrl, + SafetyError, + getPublicKeyFromPrivateKey +} from './utils'; + +import { + CLINetworkAdapter, + NameInfoType +} from './network'; + +import { + UserData +} from 'blockstack/lib/auth/authApp'; + +import { + GaiaHubConfig +} from 'blockstack/lib/storage/hub'; + + +/* + * Set up a session for Gaia. + * Generate an authentication response like what the browser would do, + * and store the relevant data to our emulated localStorage. + */ +function makeFakeAuthResponseToken(appPrivateKey: string | null, + hubURL: string | null, + associationToken?: string) { + const ownerPrivateKey = '24004db06ef6d26cdd2b0fa30b332a1b10fa0ba2b07e63505ffc2a9ed7df22b4'; + const transitPrivateKey = 'f33fb466154023aba2003c17158985aa6603db68db0f1afc0fcf1d641ea6c2cb'; + const transitPublicKey = '0496345da77fb5e06757b9c4fd656bf830a3b293f245a6cc2f11f8334ebb690f1' + + '9582124f4b07172eb61187afba4514828f866a8a223e0d5c539b2e38a59ab8bb3'; + + // eslint-disable-next-line + window.localStorage.setItem('blockstack-transit-private-key', transitPrivateKey) + + const authResponse = blockstack.makeAuthResponse( + ownerPrivateKey, + {type: '@Person', accounts: []}, + null, + {}, + null, + appPrivateKey, + undefined, + transitPublicKey, + hubURL, + blockstack.config.network.blockstackAPIUrl, + associationToken + ); + + return authResponse; +} + +/* + * Make an association token for the given address. + * TODO belongs in a "gaia.js" library + */ +export function makeAssociationToken(appPrivateKey: string, identityKey: string) : string { + const appPublicKey = getPublicKeyFromPrivateKey(`${canonicalPrivateKey(appPrivateKey)}01`); + const FOUR_MONTH_SECONDS = 60 * 60 * 24 * 31 * 4; + const salt = crypto.randomBytes(16).toString('hex'); + const identityPublicKey = getPublicKeyFromPrivateKey(identityKey); + const associationTokenClaim = { + childToAssociate: appPublicKey, + iss: identityPublicKey, + exp: FOUR_MONTH_SECONDS + ((new Date().getTime())/1000), + salt + }; + const associationToken = new jsontokens.TokenSigner('ES256K', identityKey) + .sign(associationTokenClaim); + return associationToken; +} + +/* + * Authenticate to Gaia. Used for reading, writing, and listing files. + * Process a (fake) session token and set up a Gaia hub connection. + * Returns a Promise that resolves to the (fake) userData + */ +export function gaiaAuth(network: CLINetworkAdapter, + appPrivateKey: string | null, + hubUrl: string | null, + ownerPrivateKey?: string) : Promise { + // Gaia speaks mainnet only! + if (!network.isMainnet()) { + throw new Error('Gaia only works with mainnet networks.'); + } + + let associationToken; + if (ownerPrivateKey && appPrivateKey) { + associationToken = makeAssociationToken(appPrivateKey, ownerPrivateKey); + } + + const authSessionToken = makeFakeAuthResponseToken(appPrivateKey, hubUrl, associationToken); + const nameLookupUrl = `${network.blockstackAPIUrl}/v1/names/`; + const transitPrivateKey = 'f33fb466154023aba2003c17158985aa6603db68db0f1afc0fcf1d641ea6c2cb'; // same as above + return blockstack.handlePendingSignIn(nameLookupUrl, authSessionToken, transitPrivateKey); +} + + +/* + * Connect to Gaia hub and generate a hub config. + * Used for reading and writing profiles. + * Make sure we use a mainnet address always, even in test mode. + * Returns a Promise that resolves to a GaiaHubConfig + */ +export function gaiaConnect(network: CLINetworkAdapter, + gaiaHubUrl: string, + privateKey: string, + ownerPrivateKey?: string +) { + const addressMainnet = network.coerceMainnetAddress( + getPrivateKeyAddress(network, `${canonicalPrivateKey(privateKey)}01`)); + const addressMainnetCanonical = network.coerceMainnetAddress( + getPrivateKeyAddress(network, canonicalPrivateKey(privateKey))); + + let associationToken; + if (ownerPrivateKey) { + associationToken = makeAssociationToken(privateKey, ownerPrivateKey); + } + + return blockstack.connectToGaiaHub(gaiaHubUrl, canonicalPrivateKey(privateKey), associationToken) + .then((hubConfig) => { + // ensure that hubConfig always has a mainnet address, even if we're in testnet + if (network.coerceMainnetAddress(hubConfig.address) === addressMainnet) { + hubConfig.address = addressMainnet; + } + else if (network.coerceMainnetAddress(hubConfig.address) === addressMainnetCanonical) { + hubConfig.address = addressMainnetCanonical; + } + else { + throw new Error('Invalid private key: ' + + `${network.coerceMainnetAddress(hubConfig.address)} is neither ` + + `${addressMainnet} or ${addressMainnetCanonical}`); + } + return hubConfig; + }); +} + +/* + * Find the profile.json path for a name + * @network (object) the network to use + * @blockstackID (string) the blockstack ID to query + * + * Returns a Promise that resolves to the filename to use for the profile + * Throws an exception if the profile URL could not be determined + */ +function gaiaFindProfileName(network: CLINetworkAdapter, + hubConfig: GaiaHubConfig, + blockstackID?: string +): Promise { + if (!blockstackID || blockstackID === null || blockstackID === undefined) { + return Promise.resolve().then(() => 'profile.json'); + } + else { + return network.getNameInfo(blockstackID) + .then((nameInfo : NameInfoType) => { + let profileUrl; + try { + const zonefileJSON = ZoneFile.parseZoneFile(nameInfo.zonefile); + if (zonefileJSON.uri && zonefileJSON.hasOwnProperty('$origin')) { + profileUrl = blockstack.getTokenFileUrl(zonefileJSON); + } + } + catch(e) { + throw new Error(`Could not determine profile URL for ${String(blockstackID)}: could not parse zone file`); + } + + if (profileUrl === null || profileUrl === undefined) { + throw new Error(`Could not determine profile URL for ${String(blockstackID)}: no URL in zone file`); + } + + // profile URL path must match Gaia hub's URL prefix and address + // (the host can be different) + const gaiaReadPrefix = `${hubConfig.url_prefix}${hubConfig.address}`; + const gaiaReadUrlPath = String(URL.parse(gaiaReadPrefix).path); + const profileUrlPath = String(URL.parse(profileUrl).path); + + if (!profileUrlPath.startsWith(gaiaReadUrlPath)) { + throw new Error(`Could not determine profile URL for ${String(blockstackID)}: wrong Gaia hub` + + ` (${gaiaReadPrefix} does not correspond to ${profileUrl})`); + } + + const profilePath = profileUrlPath.substring(gaiaReadUrlPath.length + 1); + return profilePath; + }); + } +} + + +/* + * Upload profile data to a Gaia hub. + * + * Legacy compat: + * If a blockstack ID is given, then the zone file will be queried and the profile URL + * inspected to make sure that we handle the special (legacy) case where a profile.json + * file got stored to $GAIA_URL/$ADDRESS/$INDEX/profile.json (where $INDEX is a number). + * In such cases, the profile will be stored to $INDEX/profile.json, instead of just + * profile.json. + * + * @network (object) the network to use + * @gaiaHubUrl (string) the base scheme://host:port URL to the Gaia hub + * @gaiaData (string) the data to upload + * @privateKey (string) the private key to use to sign the challenge + * @blockstackID (string) optional; the blockstack ID for which this profile will be stored. + */ +export function gaiaUploadProfile(network: CLINetworkAdapter, + gaiaHubURL: string, + gaiaData: string, + privateKey: string, + blockstackID?: string +) { + let hubConfig : GaiaHubConfig; + return gaiaConnect(network, gaiaHubURL, privateKey) + .then((hubconf : GaiaHubConfig) => { + // make sure we use the *right* gaia path. + // if the blockstackID is given, then we should inspect the zone file to + // determine if the Gaia profile URL contains an index. If it does, then + // we need to preserve it! + hubConfig = hubconf; + return gaiaFindProfileName(network, hubConfig, blockstackID); + }) + .then((profilePath : string) => { + return blockstack.uploadToGaiaHub(profilePath, gaiaData, hubConfig); + }); +} + +/* + * Upload profile data to all Gaia hubs, given a zone file. + * @network (object) the network to use + * @gaiaUrls (array) list of Gaia URLs + * @gaiaData (string) the data to store + * @privateKey (string) the hex-encoded private key + * @return a promise with {'dataUrls': [urls to the data]}, or {'error': ...} + */ +export function gaiaUploadProfileAll(network: CLINetworkAdapter, + gaiaUrls: string[], + gaiaData: string, + privateKey: string, + blockstackID?: string +) : Promise<{dataUrls?: string[], error?: string}> { + const sanitizedGaiaUrls = gaiaUrls.map((gaiaUrl) => { + const urlInfo = URL.parse(gaiaUrl); + if (!urlInfo.protocol) { + return ''; + } + if (!urlInfo.host) { + return ''; + } + // keep flow happy + return `${String(urlInfo.protocol)}//${String(urlInfo.host)}`; + }) + .filter((gaiaUrl) => gaiaUrl.length > 0); + + const uploadPromises = sanitizedGaiaUrls.map((gaiaUrl) => + gaiaUploadProfile(network, gaiaUrl, gaiaData, privateKey, blockstackID)); + + return Promise.all(uploadPromises) + .then((publicUrls) => { + return { error: null, dataUrls: publicUrls }; + }) + .catch((e) => { + return { error: `Failed to upload: ${e.message}`, dataUrls: null }; + }); +} + +/* + * Make a zone file from a Gaia hub---reach out to the Gaia hub, get its read URL prefix, + * and generate a zone file with the profile mapped to the Gaia hub. + * + * @network (object) the network connection + * @name (string) the name that owns the zone file + * @gaiaHubUrl (string) the URL to the gaia hub write endpoint + * @ownerKey (string) the owner private key + * + * Returns a promise that resolves to the zone file with the profile URL + */ +export function makeZoneFileFromGaiaUrl(network: CLINetworkAdapter, name: string, + gaiaHubUrl: string, ownerKey: string) { + + const address = getPrivateKeyAddress(network, ownerKey); + const mainnetAddress = network.coerceMainnetAddress(address); + + return gaiaConnect(network, gaiaHubUrl, ownerKey) + .then((hubConfig) => { + if (!hubConfig.url_prefix) { + throw new Error('Invalid hub config: no read_url_prefix defined'); + } + const gaiaReadUrl = hubConfig.url_prefix.replace(/\/+$/, ''); + const profileUrl = `${gaiaReadUrl}/${mainnetAddress}/profile.json`; + try { + checkUrl(profileUrl); + } + catch(e) { + throw new SafetyError({ + 'status': false, + 'error': e.message, + 'hints': [ + 'Make sure the Gaia hub read URL scheme is present and well-formed.', + `Check the "read_url_prefix" field of ${gaiaHubUrl}/hub_info` + ] + }); + } + return blockstack.makeProfileZoneFile(name, profileUrl); + }); +} + +/* + * Given a Gaia bucket URL, extract its address + */ +export function getGaiaAddressFromURL(appUrl: string): string { + const matches = appUrl.match(/([13][a-km-zA-HJ-NP-Z0-9]{26,35})/); + if (!matches) { + throw new Error('Failed to parse gaia address'); + } + return matches[matches.length - 1]; +} + + +/* + * Given a profile and an app origin, find its app address + * Returns the address on success + * Throws on error or not found + */ +export function getGaiaAddressFromProfile(network: CLINetworkAdapter, profile: any, appOrigin: string): string { + if (!profile) { + throw new Error('No profile'); + } + if (!profile.apps) { + throw new Error('No profile apps'); + } + if (!profile.apps[appOrigin]) { + throw new Error(`No app entry for ${appOrigin}`); + } + + // do we already have an address set for this app? + const appUrl = profile.apps[appOrigin]; + let existingAppAddress; + // what's the address? + try { + existingAppAddress = network.coerceMainnetAddress(getGaiaAddressFromURL(appUrl)); + } + catch (e) { + throw new Error(`Failed to parse app URL ${appUrl}`); + } + + return existingAppAddress; +} diff --git a/packages/cli/src/encrypt.ts b/packages/cli/src/encrypt.ts new file mode 100644 index 000000000..100b677b7 --- /dev/null +++ b/packages/cli/src/encrypt.ts @@ -0,0 +1,10 @@ +import * as blockstack from 'blockstack'; + +export function encryptBackupPhrase(plaintextBuffer: string, password: string) : Promise { + return blockstack.encryptMnemonic(plaintextBuffer, password); +} + +export function decryptBackupPhrase(dataBuffer: string | Buffer, password: string) : Promise { + return blockstack.decryptMnemonic(dataBuffer, password); +} + diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts new file mode 100644 index 000000000..1c69bd703 --- /dev/null +++ b/packages/cli/src/index.ts @@ -0,0 +1,31 @@ +#!/usr/bin/env node + +export { CLIMain } from './cli'; + +// implement just enough of window to be useful to blockstack.js. +// do this here, so we can be *sure* it's in RAM. +const localStorageRAM : Record = {}; + +// @ts-ignore +declare let global : any; + +global['window'] = { + location: { + origin: 'localhost' + }, + localStorage: { + getItem: function(itemName : string) { + return localStorageRAM[itemName]; + }, + setItem: function(itemName : string, itemValue : any) { + localStorageRAM[itemName] = itemValue; + }, + removeItem: function(itemName : string) { + delete localStorageRAM[itemName]; + } + } +}; + +global['localStorage'] = global['window'].localStorage; + +require('.').CLIMain(); diff --git a/packages/cli/src/keys.ts b/packages/cli/src/keys.ts new file mode 100644 index 000000000..520645ae7 --- /dev/null +++ b/packages/cli/src/keys.ts @@ -0,0 +1,265 @@ +// TODO: most of this code should be in blockstack.js +// Will remove most of this code once the wallet functionality is there instead. + +import * as blockstack from 'blockstack'; +import * as bitcoin from 'bitcoinjs-lib'; +import * as bip39 from 'bip39'; + +const c32check = require('c32check'); + +import { + getPrivateKeyAddress +} from './utils'; + +import { + getMaxIDSearchIndex +} from './cli'; + +import { + CLINetworkAdapter +} from './network'; + +import * as bip32 from 'bip32'; +import { BIP32Interface } from 'bip32'; + +export const STRENGTH = 128; // 12 words +export const STX_WALLET_COMPATIBLE_SEED_STRENGTH = 256; +export const DERIVATION_PATH = 'm/44\'/5757\'/0\'/0/0'; + +export type OwnerKeyInfoType = { + privateKey: string; + version: string; + index: number; + idAddress: string; +}; + +export type PaymentKeyInfoType = { + privateKey: string; + address: { + BTC: string; + STACKS: string; + }; + index: number +}; + +export type StacksKeyInfoType = { + privateKey: string; + address: string; + btcAddress: string; + index: number; +}; + +export type AppKeyInfoType = { + keyInfo: { + privateKey: string; + address: string; + }; + legacyKeyInfo: { + privateKey: string; + address: string; + }; + ownerKeyIndex: number +}; + +async function walletFromMnemonic(mnemonic: string): Promise { + const seed = await bip39.mnemonicToSeed(mnemonic); + return new blockstack.BlockstackWallet(bip32.fromSeed(seed)); +} + +function getNodePrivateKey(node: BIP32Interface): string { + return blockstack.ecPairToHexString(bitcoin.ECPair.fromPrivateKey(node.privateKey)); +} + +/* + * Get the owner key information for a 12-word phrase, at a specific index. + * @network (object) the blockstack network + * @mnemonic (string) the 12-word phrase + * @index (number) the account index + * @version (string) the derivation version string + * + * Returns an object with: + * .privateKey (string) the hex private key + * .version (string) the version string of the derivation + * .idAddress (string) the ID-address + */ +export async function getOwnerKeyInfo(network: CLINetworkAdapter, + mnemonic : string, + index : number, + version : string = 'v0.10-current'): Promise { + + const wallet = await walletFromMnemonic(mnemonic); + const identity = wallet.getIdentityAddressNode(index); + const addr = network.coerceAddress(blockstack.BlockstackWallet.getAddressFromBIP32Node(identity)); + const privkey = getNodePrivateKey(identity); + return { + privateKey: privkey, + version: version, + index: index, + idAddress: `ID-${addr}` + } as OwnerKeyInfoType; +} + +/* + * Get the payment key information for a 12-word phrase. + * @network (object) the blockstack network + * @mnemonic (string) the 12-word phrase + * + * Returns an object with: + * .privateKey (string) the hex private key + * .address (string) the address of the private key + */ +export async function getPaymentKeyInfo(network: CLINetworkAdapter, mnemonic : string): Promise { + const wallet = await walletFromMnemonic(mnemonic); + const privkey = wallet.getBitcoinPrivateKey(0); + const addr = getPrivateKeyAddress(network, privkey); + const result: PaymentKeyInfoType = { + privateKey: privkey, + address: { + BTC: addr, + STACKS: c32check.b58ToC32(addr) + }, + index: 0 + }; + return result; +} + +/* + * Get the payment key information for a 24-word phrase used by the Stacks wallet. + * @network (object) the blockstack network + * @mnemonic (string) the 24-word phrase + * + * Returns an object with: + * .privateKey (string) the hex private key + * .address (string) the address of the private key + */ +export async function getStacksWalletKeyInfo(network: CLINetworkAdapter, mnemonic : string): Promise { + const seed = await bip39.mnemonicToSeed(mnemonic); + const master = bip32.fromSeed(seed); + const child = master.derivePath('m/44\'/5757\'/0\'/0/0'); // taken from stacks-wallet. See https://github.com/blockstack/stacks-wallet + const ecPair = bitcoin.ECPair.fromPrivateKey(child.privateKey); + const privkey = blockstack.ecPairToHexString(ecPair); + + const addr = getPrivateKeyAddress(network, privkey); + let btcAddress: string; + if (network.isTestnet()) { + // btcAddress = const { address } = bitcoin.payments.p2pkh({ pubkey: keyPair.publicKey }); + const { address } = bitcoin.payments.p2pkh({ pubkey: ecPair.publicKey, network: bitcoin.networks.regtest }); + btcAddress = address; + } else { + const { address } = bitcoin.payments.p2pkh({ pubkey: ecPair.publicKey, network: bitcoin.networks.bitcoin }); + btcAddress = address; + } + const result: StacksKeyInfoType = { + privateKey: privkey, + address: c32check.b58ToC32(addr), + btcAddress, + index: 0 + }; + return result; +} + +/* + * Find the index of an ID address, given the mnemonic. + * Returns the index if found + * Returns -1 if not found + */ +export async function findIdentityIndex(network: CLINetworkAdapter, mnemonic: string, idAddress: string, maxIndex?: number) : Promise { + if (!maxIndex) { + maxIndex = getMaxIDSearchIndex(); + } + + if (idAddress.substring(0,3) !== 'ID-') { + throw new Error('Not an identity address'); + } + + const wallet = await walletFromMnemonic(mnemonic); + for (let i = 0; i < maxIndex; i++) { + const identity = wallet.getIdentityAddressNode(i); + const addr = blockstack.BlockstackWallet.getAddressFromBIP32Node(identity); + + if (network.coerceAddress(addr) === + network.coerceAddress(idAddress.slice(3))) { + return i; + } + } + + return -1; +} + +/* + * Get the Gaia application key from a 12-word phrase + * @network (object) the blockstack network + * @mmemonic (string) the 12-word phrase + * @idAddress (string) the ID-address used to sign in + * @appDomain (string) the application's Origin + * + * Returns an object with + * .keyInfo (object) the app key info with the current derivation path + * .privateKey (string) the app's hex private key + * .address (string) the address of the private key + * .legacyKeyInfo (object) the app key info with the legacy derivation path + * .privateKey (string) the app's hex private key + * .address (string) the address of the private key + */ +export async function getApplicationKeyInfo(network: CLINetworkAdapter, + mnemonic : string, + idAddress: string, + appDomain: string, + idIndex?: number) : Promise { + if (!idIndex) { + idIndex = -1; + } + + if (idIndex < 0) { + idIndex = await findIdentityIndex(network, mnemonic, idAddress); + if (idIndex < 0) { + throw new Error('Identity address does not belong to this keychain'); + } + } + + const wallet = await walletFromMnemonic(mnemonic); + const identityOwnerAddressNode = wallet.getIdentityAddressNode(idIndex); + const appsNode = blockstack.BlockstackWallet.getAppsNode(identityOwnerAddressNode); + + //const appPrivateKey = blockstack.BlockstackWallet.getAppPrivateKey( + // appsNode.toBase58(), wallet.getIdentitySalt(), appDomain); + const legacyAppPrivateKey = blockstack.BlockstackWallet.getLegacyAppPrivateKey( + appsNode.toBase58(), wallet.getIdentitySalt(), appDomain); + + // TODO: figure out when we can start using the new derivation path + const res : AppKeyInfoType = { + keyInfo: { + privateKey: 'TODO', // appPrivateKey, + address: 'TODO' // getPrivateKeyAddress(network, `${appPrivateKey}01`) + }, + legacyKeyInfo: { + privateKey: legacyAppPrivateKey, + address: getPrivateKeyAddress(network, `${legacyAppPrivateKey}01`) + }, + ownerKeyIndex: idIndex + }; + return res; +} + +/* + * Extract the "right" app key + */ +export function extractAppKey( + network: CLINetworkAdapter, + appKeyInfo: { keyInfo: { privateKey: string, address: string }, legacyKeyInfo: { privateKey : string, address: string } }, + appAddress?: string +) : string { + if (appAddress) { + if (network.coerceMainnetAddress(appKeyInfo.keyInfo.address) === network.coerceMainnetAddress(appAddress)) { + return appKeyInfo.keyInfo.privateKey; + } + if (network.coerceMainnetAddress(appKeyInfo.legacyKeyInfo.address) === network.coerceMainnetAddress(appAddress)) { + return appKeyInfo.legacyKeyInfo.privateKey; + } + } + + const appPrivateKey = (appKeyInfo.keyInfo.privateKey === 'TODO' || !appKeyInfo.keyInfo.privateKey ? + appKeyInfo.legacyKeyInfo.privateKey : + appKeyInfo.keyInfo.privateKey); + return appPrivateKey; +} diff --git a/packages/cli/src/network.ts b/packages/cli/src/network.ts new file mode 100644 index 000000000..fcaa169ca --- /dev/null +++ b/packages/cli/src/network.ts @@ -0,0 +1,322 @@ +import blockstack from 'blockstack'; +import * as bitcoin from 'bitcoinjs-lib'; +const BN = require('bn.js'); +import fetch from 'node-fetch'; + +import { + CLI_CONFIG_TYPE +} from './argparse'; + +import { + BlockstackNetwork +} from 'blockstack/lib/network'; + + +const SATOSHIS_PER_BTC = 1e8; + +export interface CLI_NETWORK_OPTS { + consensusHash: string | null; + feeRate: number | null; + namespaceBurnAddress: string | null; + priceToPay: string | null; + priceUnits: string | null; + receiveFeesPeriod: number | null; + gracePeriod: number | null; + altAPIUrl: string | null; + altTransactionBroadcasterUrl: string | null; + nodeAPIUrl: string | null; +}; + +export interface PriceType { + units: 'BTC' | 'STACKS'; + amount: import('bn.js') +}; + +export type NameInfoType = { + address: string; + blockchain: string; + did: string; + expire_block: number; + grace_period: number; + last_txid: string; + renewal_deadline: number; + resolver: string | null; + status: string; + zonefile: string | null; + zonefile_hash: string | null; +}; + +/* + * Adapter class that allows us to use data obtained + * from the CLI. + */ +export class CLINetworkAdapter extends BlockstackNetwork { + consensusHash: string | null; + feeRate: number | null; + namespaceBurnAddress: string | null; + priceToPay: string | null; + priceUnits: string | null; + gracePeriod: number | null; + receiveFeesPeriod: number | null; + nodeAPIUrl: string; + optAlwaysCoerceAddress : boolean; + + constructor(network: BlockstackNetwork, opts: CLI_NETWORK_OPTS) { + const optsDefault : CLI_NETWORK_OPTS = { + consensusHash: null, + feeRate: null, + namespaceBurnAddress: null, + priceToPay: null, + priceUnits: null, + receiveFeesPeriod: null, + gracePeriod: null, + altAPIUrl: network.blockstackAPIUrl, + altTransactionBroadcasterUrl: network.broadcastServiceUrl, + nodeAPIUrl: null + }; + + opts = Object.assign({}, optsDefault, opts); + + super(opts.altAPIUrl, opts.altTransactionBroadcasterUrl, network.btc, network.layer1); + this.consensusHash = opts.consensusHash; + this.feeRate = opts.feeRate; + this.namespaceBurnAddress = opts.namespaceBurnAddress; + this.priceToPay = opts.priceToPay; + this.priceUnits = opts.priceUnits; + this.receiveFeesPeriod = opts.receiveFeesPeriod; + this.gracePeriod = opts.gracePeriod; + this.nodeAPIUrl = opts.nodeAPIUrl; + + this.optAlwaysCoerceAddress = false; + } + + isMainnet() : boolean { + return this.layer1.pubKeyHash === bitcoin.networks.bitcoin.pubKeyHash; + } + + isTestnet() : boolean { + return this.layer1.pubKeyHash === bitcoin.networks.testnet.pubKeyHash; + } + + setCoerceMainnetAddress(value: boolean) { + this.optAlwaysCoerceAddress = value; + } + + coerceMainnetAddress(address: string) : string { + const addressInfo = bitcoin.address.fromBase58Check(address); + const addressHash = addressInfo.hash; + const addressVersion = addressInfo.version; + let newVersion = 0; + + if (addressVersion === this.layer1.pubKeyHash) { + newVersion = 0; + } + else if (addressVersion === this.layer1.scriptHash) { + newVersion = 5; + } + return bitcoin.address.toBase58Check(addressHash, newVersion); + } + + getFeeRate() : Promise { + if (this.feeRate) { + // override with CLI option + return Promise.resolve(this.feeRate); + } + if (this.isTestnet()) { + // in regtest mode + return Promise.resolve(Math.floor(0.00001000 * SATOSHIS_PER_BTC)); + } + return super.getFeeRate(); + } + + getConsensusHash() : Promise { + // override with CLI option + if (this.consensusHash) { + return new Promise((resolve: any) => resolve(this.consensusHash)); + } + return super.getConsensusHash().then((c: string) => c); + } + + getGracePeriod() : Promise { + if (this.gracePeriod) { + return new Promise((resolve: any) => resolve(this.gracePeriod)); + } + return super.getGracePeriod().then((g: number) => g); + } + + getNamePrice(name: string) : Promise { + // override with CLI option + if (this.priceUnits && this.priceToPay) { + return new Promise((resolve: any) => resolve({ + units: String(this.priceUnits), + amount: new BN(this.priceToPay) + } as PriceType)); + } + return super.getNamePrice(name) + .then((priceInfo : PriceType) => { + // use v2 scheme + if (!priceInfo.units) { + priceInfo = { + units: 'BTC', + amount: new BN(String(priceInfo)) + }; + } + return priceInfo; + }); + } + + getNamespacePrice(namespaceID: string) : Promise { + // override with CLI option + if (this.priceUnits && this.priceToPay) { + return new Promise((resolve: any) => resolve({ + units: String(this.priceUnits), + amount: new BN(String(this.priceToPay)) + } as PriceType)); + } + return super.getNamespacePrice(namespaceID) + .then((priceInfo : PriceType) => { + // use v2 scheme + if (!priceInfo.units) { + priceInfo = { + units: 'BTC', + amount: new BN(String(priceInfo)) + } as PriceType; + } + return priceInfo; + }); + } + + getNamespaceBurnAddress(namespace: string, useCLI: boolean = true, receiveFeesPeriod : number = -1) : Promise { + // override with CLI option + if (this.namespaceBurnAddress && useCLI) { + return new Promise((resolve: any) => resolve(this.namespaceBurnAddress)); + } + + return Promise.all([ + fetch(`${this.blockstackAPIUrl}/v1/namespaces/${namespace}`), + this.getBlockHeight() + ]) + .then(([resp, blockHeight] : [any, number]) => { + if (resp.status === 404) { + throw new Error(`No such namespace '${namespace}'`); + } else if (resp.status !== 200) { + throw new Error(`Bad response status: ${resp.status}`); + } else { + return Promise.all([resp.json(), blockHeight]); + } + }) + .then(([namespaceInfo, blockHeight] : [any, number]) => { + let address = '1111111111111111111114oLvT2'; // default burn address + if (namespaceInfo.version === 2) { + // pay-to-namespace-creator if this namespace is less than $receiveFeesPeriod blocks old + if (receiveFeesPeriod < 0) { + receiveFeesPeriod = this.receiveFeesPeriod; + } + + if (namespaceInfo.reveal_block + receiveFeesPeriod > blockHeight) { + address = namespaceInfo.address; + } + } + return address; + }) + .then((address : string) => this.coerceAddress(address)); + } + + getNameInfo(name: string) : Promise { + // optionally coerce addresses + return super.getNameInfo(name) + .then((ni : any) => { + const nameInfo : NameInfoType = { + address: this.optAlwaysCoerceAddress ? this.coerceMainnetAddress(ni.address) : ni.address, + blockchain: ni.blockchain, + did: ni.did, + expire_block: ni.expire_block, + grace_period: ni.grace_period, + last_txid: ni.last_txid, + renewal_deadline: ni.renewal_deadline, + resolver: ni.resolver, + status: ni.status, + zonefile: ni.zonefile, + zonefile_hash: ni.zonefile_hash + }; + return nameInfo; + }); + } + + getBlockchainNameRecord(name: string) : Promise { + // TODO: send to blockstack.js + const url = `${this.blockstackAPIUrl}/v1/blockchains/bitcoin/names/${name}`; + return fetch(url) + .then((resp) => { + if (resp.status !== 200) { + throw new Error(`Bad response status: ${resp.status}`); + } + else { + return resp.json(); + } + }) + .then((nameInfo) => { + // coerce all addresses + const fixedAddresses : Record = {}; + for (const addrAttr of ['address', 'importer_address', 'recipient_address']) { + if (nameInfo.hasOwnProperty(addrAttr) && nameInfo[addrAttr]) { + fixedAddresses[addrAttr] = this.coerceAddress(nameInfo[addrAttr]); + } + } + return Object.assign(nameInfo, fixedAddresses); + }); + } + + getNameHistory(name: string, page: number) : Promise> { + // TODO: send to blockstack.js + const url = `${this.blockstackAPIUrl}/v1/names/${name}/history?page=${page}`; + return fetch(url) + .then((resp) => { + if (resp.status !== 200) { + throw new Error(`Bad response status: ${resp.status}`); + } + return resp.json(); + }) + .then((historyInfo) => { + // coerce all addresses + const fixedHistory : Record= {}; + for (const historyBlock of Object.keys(historyInfo)) { + const fixedHistoryList : any[] = []; + for (const historyEntry of historyInfo[historyBlock]) { + const fixedAddresses : Record = {}; + let fixedHistoryEntry : any = {}; + for (const addrAttr of ['address', 'importer_address', 'recipient_address']) { + if (historyEntry.hasOwnProperty(addrAttr) && historyEntry[addrAttr]) { + fixedAddresses[addrAttr] = this.coerceAddress(historyEntry[addrAttr]); + } + } + fixedHistoryEntry = Object.assign(historyEntry, fixedAddresses); + fixedHistoryList.push(fixedHistoryEntry); + } + fixedHistory[historyBlock] = fixedHistoryList; + } + return fixedHistory; + }); + } +} + +/* + * Instantiate a network using settings from the config file. + */ +export function getNetwork(configData: CLI_CONFIG_TYPE, regTest: boolean) : BlockstackNetwork { + if (regTest) { + const network = new blockstack.network.LocalRegtest( + configData.blockstackAPIUrl, configData.broadcastServiceUrl, + new blockstack.network.BitcoindAPI(configData.utxoServiceUrl, + { username: configData.bitcoindUsername || 'blockstack', password: configData.bitcoindPassword || 'blockstacksystem' })); + + return network; + } else { + const network = new BlockstackNetwork( + configData.blockstackAPIUrl, configData.broadcastServiceUrl, + new blockstack.network.BlockchainInfoApi(configData.utxoServiceUrl)); + + return network; + } +} + diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts new file mode 100644 index 000000000..c5957696a --- /dev/null +++ b/packages/cli/src/utils.ts @@ -0,0 +1,881 @@ +import * as logger from 'winston'; +import * as bitcoinjs from 'bitcoinjs-lib'; +import * as URL from 'url'; +import * as readline from 'readline'; +import * as stream from 'stream'; +import * as fs from 'fs'; +import * as blockstack from 'blockstack'; +import { decodeToken, SECP256K1Client, TokenSigner, TokenVerifier } from 'jsontokens' +import { + getTypeString, + ClarityAbiType, + isClarityAbiPrimitive, + isClarityAbiBuffer, + isClarityAbiResponse, + isClarityAbiOptional, + isClarityAbiTuple, + isClarityAbiList, + ClarityValue, + intCV, + uintCV, + bufferCVFromString, + trueCV, + falseCV, + standardPrincipalCV, + StacksNetwork, + TransactionVersion, +} from '@blockstack/stacks-transactions'; + +const ZoneFile = require('zone-file'); + +import { + PRIVATE_KEY_NOSIGN_PATTERN, + PRIVATE_KEY_PATTERN, + PRIVATE_KEY_MULTISIG_PATTERN, + PRIVATE_KEY_SEGWIT_P2SH_PATTERN, + ID_ADDRESS_PATTERN +} from './argparse'; + +import { + TransactionSigner +} from 'blockstack'; + +import { + decryptBackupPhrase +} from './encrypt'; + +import { + getOwnerKeyInfo, + getApplicationKeyInfo, + extractAppKey +} from './keys'; + +import { + NameInfoType, + CLINetworkAdapter +} from './network'; + +export interface UTXO { + value?: number; + confirmations?: number; + tx_hash: string; + tx_output_n: number; +}; + +class CLITransactionSigner implements TransactionSigner { + address: string; + isComplete: boolean; + + constructor(address: string = '') { + this.address = address; + this.isComplete = false; + } + + getAddress() : Promise { + return Promise.resolve().then(() => this.address); + } + + signTransaction(_txIn: bitcoinjs.TransactionBuilder, _signingIndex: number) : Promise { + return Promise.resolve().then(() => {}); + } + + signerVersion() : number { return 0; } +} + +export class NullSigner extends CLITransactionSigner { }; + + +export class MultiSigKeySigner extends CLITransactionSigner { + redeemScript: Buffer; + privateKeys: string[]; + m: number; + + constructor(redeemScript: string, privateKeys: string[]) { + super(); + this.redeemScript = Buffer.from(redeemScript, 'hex'); + this.privateKeys = privateKeys; + this.isComplete = true; + try { + // try to deduce m (as in m-of-n) + const chunks = bitcoinjs.script.decompile(this.redeemScript); + const firstOp = chunks[0]; + this.m = parseInt(bitcoinjs.script.toASM([firstOp]).slice(3), 10); + this.address = bitcoinjs.address.toBase58Check( + bitcoinjs.crypto.hash160(this.redeemScript), + blockstack.config.network.layer1.scriptHash); + } catch (e) { + logger.error(e); + throw new Error('Improper redeem script for multi-sig input.'); + } + } + + getAddress() : Promise { + return Promise.resolve().then(() => this.address); + } + + signTransaction(txIn: bitcoinjs.TransactionBuilder, signingIndex: number) : Promise { + return Promise.resolve().then(() => { + const keysToUse = this.privateKeys.slice(0, this.m); + keysToUse.forEach((keyHex) => { + const ecPair = blockstack.hexStringToECPair(keyHex); + txIn.sign(signingIndex, ecPair, this.redeemScript); + }); + }); + } + + signerVersion() : number { return 0; } +} + + +export class SegwitP2SHKeySigner extends CLITransactionSigner { + redeemScript: Buffer; + witnessScript: Buffer; + privateKeys: string[]; + m: number; + + constructor(redeemScript: string, witnessScript: string, m: number, privateKeys: string[]) { + super(); + this.redeemScript = Buffer.from(redeemScript, 'hex'); + this.witnessScript = Buffer.from(witnessScript, 'hex'); + this.address = bitcoinjs.address.toBase58Check( + bitcoinjs.crypto.hash160(this.redeemScript), + blockstack.config.network.layer1.scriptHash); + + this.privateKeys = privateKeys; + this.m = m; + this.isComplete = true; + } + + getAddress() : Promise { + return Promise.resolve().then(() => this.address); + } + + findUTXO(txIn: bitcoinjs.TransactionBuilder, signingIndex: number, utxos: UTXO[]) : UTXO { + // NOTE: this is O(n*2) complexity for n UTXOs when signing an n-input transaction + // NOTE: as of bitcoinjs-lib 4.x, the "tx" field is private + const private_tx = (txIn as any).__TX; + const txidBuf = new Buffer(private_tx.ins[signingIndex].hash.slice()); + const outpoint = private_tx.ins[signingIndex].index; + + txidBuf.reverse(); // NOTE: bitcoinjs encodes txid as big-endian + const txid = txidBuf.toString('hex'); + + for (let i = 0; i < utxos.length; i++) { + if (utxos[i].tx_hash === txid && utxos[i].tx_output_n === outpoint) { + if (!utxos[i].value) { + throw new Error(`UTXO for hash=${txid} vout=${outpoint} has no value`); + } + return utxos[i]; + } + } + throw new Error(`No UTXO for input hash=${txid} vout=${outpoint}`); + } + + signTransaction(txIn: bitcoinjs.TransactionBuilder, signingIndex: number) : Promise { + // This is an interface issue more than anything else. Basically, in order to + // form the segwit sighash, we need the UTXOs. If we knew better, we would have + // blockstack.js simply pass the consumed UTXO into this method. But alas, we do + // not. Therefore, we need to re-query them. This is probably fine, since we're + // not pressured for time when it comes to generating transactions. + return Promise.resolve().then(() => { + return this.getAddress(); + }) + .then((address) => { + return blockstack.config.network.getUTXOs(address); + }) + .then((utxos) => { + const utxo = this.findUTXO(txIn, signingIndex, utxos); + if (this.m === 1) { + // p2sh-p2wpkh + const ecPair = blockstack.hexStringToECPair(this.privateKeys[0]); + txIn.sign(signingIndex, ecPair, this.redeemScript, null, utxo.value); + } + else { + // p2sh-p2wsh + const keysToUse = this.privateKeys.slice(0, this.m); + keysToUse.forEach((keyHex) => { + const ecPair = blockstack.hexStringToECPair(keyHex); + txIn.sign(signingIndex, ecPair, this.redeemScript, null, utxo.value, this.witnessScript); + }); + } + }); + } + + signerVersion() : number { return 0; } +} + +export class SafetyError extends Error { + safetyErrors: AnyJson; + constructor(safetyErrors: AnyJson) { + super(JSONStringify(safetyErrors, true)); + this.safetyErrors = safetyErrors; + } +} + +function isCLITransactionSigner(signer: string | CLITransactionSigner) : signer is CLITransactionSigner { + return (signer as CLITransactionSigner).signerVersion !== undefined; +} + +export function hasKeys(signer: string | CLITransactionSigner) : boolean { + if (isCLITransactionSigner(signer)) { + const s = signer as CLITransactionSigner; + return s.isComplete; + } + else { + return true; + } +} + +/* + * Parse a string into a NullSigner + * The string has the format "nosign:address" + * @return a NullSigner instance + */ +export function parseNullSigner(addrString: string) : NullSigner { + if (!addrString.startsWith('nosign:')) { + throw new Error('Invalid nosign string'); + } + + const addr = addrString.slice('nosign:'.length); + return new NullSigner(addr); +} + + +/* + * Parse a string into a MultiSigKeySigner. + * The string has the format "m,pk1,pk2,...,pkn" + * @serializedPrivateKeys (string) the above string + * @return a MultiSigKeySigner instance + */ +export function parseMultiSigKeys(serializedPrivateKeys: string) : MultiSigKeySigner { + const matches = serializedPrivateKeys.match(PRIVATE_KEY_MULTISIG_PATTERN); + if (!matches) { + throw new Error('Invalid multisig private key string'); + } + + const m = parseInt(matches[1]); + const parts = serializedPrivateKeys.split(','); + const privkeys = []; + for (let i = 1; i < 256; i++) { + const pk = parts[i]; + if (!pk) { + break; + } + + if (!pk.match(PRIVATE_KEY_PATTERN)) { + throw new Error('Invalid private key string'); + } + + privkeys.push(pk); + } + + // generate public keys + const pubkeys = privkeys.map((pk) => { + return Buffer.from(getPublicKeyFromPrivateKey(pk), 'hex'); + }); + + // generate redeem script + const multisigInfo = bitcoinjs.payments.p2ms({ m, pubkeys }); + return new MultiSigKeySigner(multisigInfo.output.toString('hex'), privkeys); +} + + +/* + * Parse a string into a SegwitP2SHKeySigner + * The string has the format "segwit:p2sh:m,pk1,pk2,...,pkn" + * @serializedPrivateKeys (string) the above string + * @return a MultiSigKeySigner instance + */ +export function parseSegwitP2SHKeys(serializedPrivateKeys: string) : SegwitP2SHKeySigner { + const matches = serializedPrivateKeys.match(PRIVATE_KEY_SEGWIT_P2SH_PATTERN); + if (!matches) { + throw new Error('Invalid segwit p2sh private key string'); + } + + const m = parseInt(matches[1]); + const parts = serializedPrivateKeys.split(','); + const privkeys = []; + for (let i = 1; i < 256; i++) { + const pk = parts[i]; + if (!pk) { + break; + } + + if (!pk.match(PRIVATE_KEY_PATTERN)) { + throw new Error('Invalid private key string'); + } + + privkeys.push(pk); + } + + // generate public keys + const pubkeys = privkeys.map((pk) => { + return Buffer.from(getPublicKeyFromPrivateKey(pk), 'hex'); + }); + + // generate redeem script for p2wpkh or p2sh, depending on how many keys + let redeemScript : string; + let witnessScript = ''; + if (m === 1) { + // p2wpkh + const p2wpkh = bitcoinjs.payments.p2wpkh({ pubkey: pubkeys[0] }); + const p2sh = bitcoinjs.payments.p2sh({ redeem: p2wpkh }); + + redeemScript = p2sh.redeem.output.toString('hex'); + } + else { + // p2wsh + const p2ms = bitcoinjs.payments.p2ms({ m, pubkeys }); + const p2wsh = bitcoinjs.payments.p2wsh({ redeem: p2ms }); + const p2sh = bitcoinjs.payments.p2sh({ redeem: p2wsh }); + + redeemScript = p2sh.redeem.output.toString('hex'); + witnessScript = p2wsh.redeem.output.toString('hex'); + } + + return new SegwitP2SHKeySigner(redeemScript, witnessScript, m, privkeys); +} + +/* + * Decode one or more private keys from a string. + * Can be used to parse single private keys (as strings), + * or multisig bundles (as CLITransactionSigners) + * @serializedPrivateKey (string) the private key, encoded + * @return a CLITransactionSigner or a String + */ +export function decodePrivateKey(serializedPrivateKey: string) : string | CLITransactionSigner { + const nosignMatches = serializedPrivateKey.match(PRIVATE_KEY_NOSIGN_PATTERN); + if (!!nosignMatches) { + // no private key + return parseNullSigner(serializedPrivateKey); + } + + const singleKeyMatches = serializedPrivateKey.match(PRIVATE_KEY_PATTERN); + if (!!singleKeyMatches) { + // one private key + return serializedPrivateKey; + } + + const multiKeyMatches = serializedPrivateKey.match(PRIVATE_KEY_MULTISIG_PATTERN); + if (!!multiKeyMatches) { + // multisig bundle + return parseMultiSigKeys(serializedPrivateKey); + } + + const segwitP2SHMatches = serializedPrivateKey.match(PRIVATE_KEY_SEGWIT_P2SH_PATTERN); + if (!!segwitP2SHMatches) { + // segwit p2sh bundle + return parseSegwitP2SHKeys(serializedPrivateKey); + } + + throw new Error('Unparseable private key'); +} + +type AnyJson = + | string + | number + | boolean + | null + | { [property: string]: AnyJson } + | AnyJson[]; + +/* + * JSON stringify helper + * -- if stdout is a TTY, then pretty-format the JSON + * -- otherwise, print it all on one line to make it easy for programs to consume + */ +export function JSONStringify(obj: AnyJson, stderr: boolean = false) : string { + if ((!stderr && process.stdout.isTTY) || (stderr && process.stderr.isTTY)) { + return JSON.stringify(obj, null, 2); + } + else { + return JSON.stringify(obj); + } +} + +/* + * Get a private key's public key, while honoring the 01 to compress it. + * @privateKey (string) the hex-encoded private key + */ +export function getPublicKeyFromPrivateKey(privateKey: string) : string { + const ecKeyPair = blockstack.hexStringToECPair(privateKey); + return ecKeyPair.publicKey.toString('hex'); +} + +/* + * Get a private key's address. Honor the 01 to compress the public key + * @privateKey (string) the hex-encoded private key + */ +export function getPrivateKeyAddress(network: CLINetworkAdapter, privateKey: string | CLITransactionSigner) : string { + if (isCLITransactionSigner(privateKey)) { + const pkts = privateKey as CLITransactionSigner; + return pkts.address; + } + else { + const pk = privateKey as string; + const ecKeyPair = blockstack.hexStringToECPair(pk); + return network.coerceAddress(blockstack.ecPairToAddress(ecKeyPair)); + } +} + +/* + * Is a name a sponsored name (a subdomain)? + */ +export function isSubdomain(name: string) : boolean { + return !!name.match(/^[^\.]+\.[^.]+\.[^.]+$/); +} + +/* + * Get the canonical form of a hex-encoded private key + * (i.e. strip the trailing '01' if present) + */ +export function canonicalPrivateKey(privkey: string) : string { + if (privkey.length == 66 && privkey.slice(-2) === '01') { + return privkey.substring(0,64); + } + return privkey; +} + +/* + * Get the sum of a set of UTXOs' values + * @txIn (object) the transaction + */ +export function sumUTXOs(utxos: UTXO[]) : number { + return utxos.reduce((agg, x) => agg + x.value, 0); +} + +/* + * Hash160 function for zone files + */ +export function hash160(buff: Buffer) : Buffer { + return bitcoinjs.crypto.hash160(buff); +} + +/* + * Normalize a URL--remove duplicate /'s from the root of the path. + * Throw an exception if it's not well-formed. + */ +export function checkUrl(url: string) : string { + const urlinfo = URL.parse(url); + if (!urlinfo.protocol) { + throw new Error(`Malformed full URL: missing scheme in ${url}`); + } + + if (!urlinfo.path || urlinfo.path.startsWith('//')) { + throw new Error(`Malformed full URL: path root has multiple /'s: ${url}`); + } + + return url; +} + +/* + * Sign a profile into a JWT + */ +export function makeProfileJWT(profileData: Object, privateKey: string) : string { + const signedToken = blockstack.signProfileToken(profileData, privateKey); + const wrappedToken = blockstack.wrapProfileToken(signedToken); + const tokenRecords = [wrappedToken]; + return JSONStringify(tokenRecords as unknown as AnyJson); +} + +export async function makeDIDConfiguration(network:CLINetworkAdapter, blockstackID: string, domain: string, privateKey:string): Promise<{entries:{did:string, jwt:string}[]}> { + + const tokenSigner = new TokenSigner("ES256K", privateKey) + const nameInfo = await network.getNameInfo(blockstackID); + const did = nameInfo.did + const payload = { + iss: did, + domain, + exp: new Date( + new Date().setFullYear( + new Date().getFullYear() + 1 + ) + ) + } + + const jwt = tokenSigner.sign(payload) + return {"entries": [ + { + did, + jwt + } + ]} +} +/* + * Broadcast a transaction and a zone file. + * Returns an object that encodes the success/failure of doing so. + * If zonefile is None, then only the transaction will be sent. + */ +export async function broadcastTransactionAndZoneFile(network: CLINetworkAdapter, + tx: string, + zonefile?: string) { + let txid : string; + return Promise.resolve().then(() => { + return network.broadcastTransaction(tx); + }) + .then((_txid : string) => { + txid = _txid; + if (zonefile) { + return network.broadcastZoneFile(zonefile, txid); + } + else { + return { 'status': true }; + } + }) + .then((resp) => { + if (!resp.status) { + return { + 'status': false, + 'error': 'Failed to broadcast zone file', + 'txid': txid + }; + } + else { + return { + 'status': true, + 'txid': txid + }; + } + }) + .catch((e) => { + return { + 'status': false, + 'error': 'Caught exception sending transaction or zone file', + 'message': e.message, + 'stacktrace': e.stack + }; + }); +} + + +/* + * Easier-to-use getNameInfo. Returns null if the name does not exist. + */ +export function getNameInfoEasy(network: CLINetworkAdapter, name: string) : Promise { + const nameInfoPromise = network.getNameInfo(name) + .then((nameInfo : NameInfoType) => nameInfo) + .catch((error : Error) : null => { + if (error.message === 'Name not found') { + return null; + } else { + throw error; + } + }); + + return nameInfoPromise; +} + +/* + * Look up a name's zone file, profile URL, and profile + * Returns a Promise to the above, or throws an error. + */ +export async function nameLookup(network: CLINetworkAdapter, name: string, includeProfile: boolean = true) + : Promise<{ profile: any, profileUrl?: string, zonefile?: string }> { + + const nameInfoPromise = getNameInfoEasy(network, name); + const profilePromise = includeProfile ? + blockstack.lookupProfile(name).catch(() => null) : + Promise.resolve().then(() => null); + + const zonefilePromise = nameInfoPromise.then((nameInfo : NameInfoType) => nameInfo ? nameInfo.zonefile : null); + + const [profile, zonefile, nameInfo] = await Promise.all([profilePromise, zonefilePromise, nameInfoPromise]); + let profileObj = profile; + + if (!nameInfo) { + throw new Error('Name not found'); + } + if (nameInfo.hasOwnProperty('grace_period') && nameInfo.grace_period) { + throw new Error(`Name is expired at block ${nameInfo.expire_block} ` + + `and must be renewed by block ${nameInfo.renewal_deadline}`); + } + + let profileUrl = null; + try { + const zonefileJSON = ZoneFile.parseZoneFile(zonefile); + if (zonefileJSON.uri && zonefileJSON.hasOwnProperty('$origin')) { + profileUrl = blockstack.getTokenFileUrl(zonefileJSON); + } + } + catch (e) { + profileObj = null; + } + + const ret = { + zonefile: zonefile, + profile: profileObj, + profileUrl: profileUrl + }; + return ret; +} + +/* + * Get a password. Don't echo characters to stdout. + * Password will be passed to the given callback. + */ +export function getpass(promptStr: string, cb: (passwd: string) => void) { + const silentOutput = new stream.Writable({ + write: (_chunk, _encoding, callback) => { + callback(); + } + }); + + const rl = readline.createInterface({ + input: process.stdin, + output: silentOutput, + terminal: true + }); + + process.stderr.write(promptStr); + rl.question('', (passwd) => { + rl.close(); + process.stderr.write('\n'); + cb(passwd); + }); + + return; +} + +/* + * Extract a 12-word backup phrase. If the raw 12-word phrase is given, it will + * be returned. If the ciphertext is given, the user will be prompted for a password + * (if a password is not given as an argument). + */ +export async function getBackupPhrase(backupPhraseOrCiphertext: string, password?: string) : Promise { + if (backupPhraseOrCiphertext.split(/ +/g).length > 1) { + // raw backup phrase + return backupPhraseOrCiphertext; + } + else { + // ciphertext + const pass: string = await new Promise((resolve, reject) => { + if (!process.stdin.isTTY && !password) { + // password must be given + reject(new Error('Password argument required in non-interactive mode')); + } + else { + // prompt password + getpass('Enter password: ', (p) => { + resolve(p); + }); + } + }); + return await decryptBackupPhrase(Buffer.from(backupPhraseOrCiphertext, 'base64'), pass); + } +} + +/* + * mkdir -p + * path must be absolute + */ +export function mkdirs(path: string) : void { + if (path.length === 0 || path[0] !== '/') { + throw new Error('Path must be absolute'); + } + + const pathParts = path.replace(/^\//, '').split('/'); + let tmpPath = '/'; + for (let i = 0; i <= pathParts.length; i++) { + try { + const statInfo = fs.lstatSync(tmpPath); + if ((statInfo.mode & fs.constants.S_IFDIR) === 0) { + throw new Error(`Not a directory: ${tmpPath}`); + } + } + catch (e) { + if (e.code === 'ENOENT') { + // need to create + fs.mkdirSync(tmpPath); + } + else { + throw e; + } + } + if (i === pathParts.length) { + break; + } + tmpPath = `${tmpPath}/${pathParts[i]}`; + } +} + +/* + * Given a name or ID address, return a promise to the ID Address + */ +export async function getIDAddress(network: CLINetworkAdapter, nameOrIDAddress: string) : Promise { + if (nameOrIDAddress.match(ID_ADDRESS_PATTERN)) { + return nameOrIDAddress; + } + else { + // need to look it up + const nameInfo = await network.getNameInfo(nameOrIDAddress); + return `ID-${nameInfo.address}`; + } +} + +/* + * Find all identity addresses until we have one that matches the given one. + * Loops forever if not found + */ +export async function getOwnerKeyFromIDAddress(network: CLINetworkAdapter, + mnemonic: string, + idAddress: string +) : Promise { + let index = 0; + while(true) { + const keyInfo = await getOwnerKeyInfo(network, mnemonic, index); + if (keyInfo.idAddress === idAddress) { + return keyInfo.privateKey; + } + index++; + } +} + +/* + * Given a name or an ID address and a possibly-encrypted mnemonic, get the owner and app + * private keys. + * May prompt for a password if mnemonic is encrypted. + */ +export interface IDAppKeys { + ownerPrivateKey: string; + appPrivateKey: string; + mnemonic: string +}; + +export async function getIDAppKeys(network: CLINetworkAdapter, + nameOrIDAddress: string, + appOrigin: string, + mnemonicOrCiphertext: string, +) : Promise { + const mnemonic = await getBackupPhrase(mnemonicOrCiphertext); + const idAddress = await getIDAddress(network, nameOrIDAddress); + const appKeyInfo = await getApplicationKeyInfo(network, mnemonic, idAddress, appOrigin); + const appPrivateKey = extractAppKey(network, appKeyInfo); + const ownerPrivateKey = await getOwnerKeyFromIDAddress(network, mnemonic, idAddress); + const ret = { + appPrivateKey, + ownerPrivateKey, + mnemonic + }; + return ret; +} + +interface InquirerPrompt { + type: string; + name: string; + message: string; + choices?: string[]; +} + +export function makePromptsFromArgList(expectedArgs: ClarityFunctionArg[]): InquirerPrompt[] { + const prompts = []; + for (let i = 0; i < expectedArgs.length; i++) { + prompts.push(argToPrompt(expectedArgs[i])); + } + return prompts; +} + +export interface ClarityFunctionArg { + name: string; + type: ClarityAbiType; +} + +export function argToPrompt(arg: ClarityFunctionArg): InquirerPrompt { + const name = arg.name; + const type = arg.type; + const typeString = getTypeString(type); + if (isClarityAbiPrimitive(type)) { + if (type === 'uint128') { + return { + type: 'input', + name, + message: `Enter value for function argument "${name}" of type ${typeString}` + } + } else if (type === 'int128') { + return { + type: 'input', + name, + message: `Enter value for function argument "${name}" of type ${typeString}` + } + } else if (type === 'bool') { + return { + type: 'list', + name, + message: `Enter value for function argument "${name}" of type ${typeString}`, + choices: ['True', 'False'] + } + } else if (type === 'principal') { + return { + type: 'input', + name, + message: `Enter value for function argument "${name}" of type ${typeString}` + } + } else { + throw new Error(`Contract function contains unsupported Clarity ABI type: ${typeString}`); + } + } else if (isClarityAbiBuffer(type)) { + return { + type: 'input', + name, + message: `Enter value for function argument "${name}" of type ${typeString}` + } + } else if (isClarityAbiResponse(type)) { + throw new Error(`Contract function contains unsupported Clarity ABI type: ${typeString}`); + } else if (isClarityAbiOptional(type)) { + throw new Error(`Contract function contains unsupported Clarity ABI type: ${typeString}`); + } else if (isClarityAbiTuple(type)) { + throw new Error(`Contract function contains unsupported Clarity ABI type: ${typeString}`); + } else if (isClarityAbiList(type)) { + throw new Error(`Contract function contains unsupported Clarity ABI type: ${typeString}`); + } else { + throw new Error(`Contract function contains unsupported Clarity ABI type: ${typeString}`); + } +} + +export function parseClarityFunctionArgAnswers(answers: any, expectedArgs: ClarityFunctionArg[]): ClarityValue[] { + const functionArgs: ClarityValue[] = []; + for (let i = 0; i < expectedArgs.length; i++) { + const expectedArg = expectedArgs[i]; + const answer = answers[expectedArg.name]; + functionArgs.push(answerToClarityValue(answer, expectedArg)); + } + return functionArgs; +} + +export function answerToClarityValue(answer: any, arg: ClarityFunctionArg): ClarityValue { + const type = arg.type; + const typeString = getTypeString(type); + if (isClarityAbiPrimitive(type)) { + if (type === 'uint128') { + return uintCV(answer); + } else if (type === 'int128') { + return intCV(answer); + } else if (type === 'bool') { + return answer == 'True' ? trueCV() : falseCV(); + } else if (type === 'principal') { + // TODO handle contract principals + return standardPrincipalCV(answer); + } else { + throw new Error(`Contract function contains unsupported Clarity ABI type: ${typeString}`); + } + } else if (isClarityAbiBuffer(type)) { + return bufferCVFromString(answer); + } else if (isClarityAbiResponse(type)) { + throw new Error(`Contract function contains unsupported Clarity ABI type: ${typeString}`); + } else if (isClarityAbiOptional(type)) { + throw new Error(`Contract function contains unsupported Clarity ABI type: ${typeString}`); + } else if (isClarityAbiTuple(type)) { + throw new Error(`Contract function contains unsupported Clarity ABI type: ${typeString}`); + } else if (isClarityAbiList(type)) { + throw new Error(`Contract function contains unsupported Clarity ABI type: ${typeString}`); + } else { + throw new Error(`Contract function contains unsupported Clarity ABI type: ${typeString}`); + } +} + +export function generateExplorerTxPageUrl(txid: string, network: StacksNetwork): string { + if (network.version === TransactionVersion.Mainnet) { + return `https://explorer.blockstack.org/txid/0x${txid}`; + } else if (network.version === TransactionVersion.Testnet) { + return `https://testnet-explorer.now.sh/txid/0x${txid}`; + } +} diff --git a/packages/cli/tsconfig.build.json b/packages/cli/tsconfig.build.json new file mode 100644 index 000000000..61ab30ed3 --- /dev/null +++ b/packages/cli/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "moduleResolution": "node", + "noEmit": false, + "rootDir": "./src", + "outDir": "./lib" + }, + "include": [ + "src/**/*" + ] +}