feat: add @stacks/wallet-sdk package

This commit is contained in:
Hank Stoever
2021-01-10 10:46:20 -08:00
committed by Reed Rosenbluth
parent f292065505
commit eb8d2a5043
27 changed files with 13372 additions and 0 deletions

568
package-lock.json generated
View File

@@ -3517,6 +3517,10 @@
"resolved": "packages/transactions",
"link": true
},
"node_modules/@stacks/wallet-sdk": {
"resolved": "packages/wallet-sdk",
"link": true
},
"node_modules/@strictsoftware/typedoc-plugin-monorepo": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@strictsoftware/typedoc-plugin-monorepo/-/typedoc-plugin-monorepo-0.4.2.tgz",
@@ -12188,6 +12192,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash-es": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.20.tgz",
"integrity": "sha512-JD1COMZsq8maT6mnuz1UMV0jvYD0E0aUsSOdrr1/nAG3dhqQXwRRgeW0cSqH1U43INKcqxaiVIQNOUDld7gRDA=="
},
"node_modules/lodash._reinterpolate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
@@ -17953,6 +17962,130 @@
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"dev": true
},
"node_modules/yalc": {
"version": "1.0.0-pre.49",
"resolved": "https://registry.npmjs.org/yalc/-/yalc-1.0.0-pre.49.tgz",
"integrity": "sha512-7fTnwsX4qKnr2h1LVTLQzc9gosFrGnJcBRPnNGsM+3YJSLAjB+i8XnqmNptdktjyc4hOzI+XzN1Wp2kXvKAPxA==",
"dev": true,
"dependencies": {
"chalk": "^4.1.0",
"detect-indent": "^6.0.0",
"fs-extra": "^8.0.1",
"glob": "^7.1.4",
"ignore": "^5.0.4",
"npm-packlist": "^1.4.1",
"yargs": "^16.1.1"
},
"bin": {
"yalc": "src/yalc.js"
}
},
"node_modules/yalc/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/yalc/node_modules/cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"dev": true,
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^7.0.0"
}
},
"node_modules/yalc/node_modules/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,
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/yalc/node_modules/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
},
"node_modules/yalc/node_modules/detect-indent": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",
"integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/yalc/node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/yalc/node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"engines": {
"node": ">=10"
}
},
"node_modules/yalc/node_modules/yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"dev": true,
"dependencies": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^20.2.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/yalc/node_modules/yargs-parser": {
"version": "20.2.9",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
"dev": true,
"engines": {
"node": ">=10"
}
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
@@ -18840,6 +18973,174 @@
"dependencies": {
"cross-fetch": "^3.0.6"
}
},
"packages/wallet-sdk": {
"version": "1.0.0-beta.21",
"license": "MIT",
"dependencies": {
"@stacks/encryption": "^1.0.1",
"@stacks/profile": "^1.1.1-alpha.0",
"@stacks/storage": "^1.1.1-alpha.0",
"@stacks/transactions": "^1.1.1-alpha.0",
"bip32": "2.0.6",
"bip39": "^3.0.2",
"bitcoinjs-lib": "^5.1.6",
"bn.js": "^5.1.1",
"c32check": "^1.0.1",
"jsontokens": "^3.0.0",
"triplesec": "^3.0.27",
"zone-file": "^1.0.0"
},
"devDependencies": {
"@types/node": "^13.13.10",
"yalc": "1.0.0-pre.49"
}
},
"packages/wallet-sdk/node_modules/@stacks/auth": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@stacks/auth/-/auth-1.2.3.tgz",
"integrity": "sha512-EDXX1hiQ9UnAjrX4D1UyeVZQF647RdtC6EYAIxzgn2L6XxARACGpn+d/fRpE1q411y94lF/2Oqj8MPSwDb/NHQ==",
"dependencies": {
"@stacks/common": "^1.2.2",
"@stacks/encryption": "^1.2.3",
"@stacks/network": "^1.2.2",
"@stacks/profile": "^1.2.3",
"codecov": "^3.7.2",
"cross-fetch": "^3.0.5",
"jsontokens": "^3.0.0",
"query-string": "^6.13.1"
}
},
"packages/wallet-sdk/node_modules/@stacks/common": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@stacks/common/-/common-1.2.2.tgz",
"integrity": "sha512-knCqq88EBRCN8AhS7+Sx2PJuRv0EFNChEpqLqCAchCHCQfp5bWad/47Zw+fLP9ccBwFXh4pl1wDtbQLBfDo0+A==",
"dependencies": {
"cross-fetch": "^3.0.6"
}
},
"packages/wallet-sdk/node_modules/@stacks/encryption": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@stacks/encryption/-/encryption-1.2.3.tgz",
"integrity": "sha512-g6p9FGXUHMFKnkbfgObcgP3LUHc/XqRnh+5wNw8kU/R9T0Wbwej2/RfZaxvyvYVu9GP3EXRDPVDmWJFSO1cqWw==",
"dependencies": {
"@stacks/common": "^1.2.2",
"bip39": "^3.0.2",
"bitcoinjs-lib": "^5.1.10",
"bn.js": "^5.1.2",
"elliptic": "^6.5.2",
"randombytes": "^2.1.0",
"ripemd160-min": "^0.0.6",
"sha.js": "^2.4.11"
}
},
"packages/wallet-sdk/node_modules/@stacks/network": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@stacks/network/-/network-1.2.2.tgz",
"integrity": "sha512-xcWwuRrLJn9qqi3PEBcP2UPZHQztTZd31C0aVlzYHttNMir/sY9SrUqSnw45z2Jo4O9pIYYPIiPRtdV91Ho3fw==",
"dependencies": {
"@stacks/common": "^1.2.2"
}
},
"packages/wallet-sdk/node_modules/@stacks/profile": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@stacks/profile/-/profile-1.2.3.tgz",
"integrity": "sha512-bVLoSz8LFiuUS4wnZgVoShNRPaSSBvrAc5SmPZNOtfonUgeNLmwKvskbc6x6exgn/m7wMNp1gY7n+BIBmfST9w==",
"dependencies": {
"@stacks/common": "^1.2.2",
"@stacks/encryption": "^1.2.3",
"@stacks/network": "^1.2.2",
"bitcoinjs-lib": "^5.1.10",
"jsontokens": "^3.0.0",
"schema-inspector": "^1.7.0",
"zone-file": "^1.0.0"
}
},
"packages/wallet-sdk/node_modules/@stacks/storage": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@stacks/storage/-/storage-1.3.3.tgz",
"integrity": "sha512-HSob/SSIF+doiMP8ScDYNNmTK/8vnd1HyNtZk3rpE3PSEqpU1bRPqMF3ehlzMcDsCjCvOx6Ow3OlWBWPQA4QhA==",
"dependencies": {
"@stacks/auth": "^1.2.3",
"@stacks/common": "^1.2.2",
"@stacks/encryption": "^1.2.3"
}
},
"packages/wallet-sdk/node_modules/@stacks/transactions": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-1.4.1.tgz",
"integrity": "sha512-7LFA9yQqlmN+oVJeYaj+NfZyuInJxF8ozJ8kypCmJ9rUrbbGC/es1KyseB96YBiiOh4eLUfRlD1j6boSdNR8aA==",
"dependencies": {
"@stacks/common": "^1.2.2",
"@stacks/network": "^1.2.2",
"@types/bn.js": "^4.11.6",
"@types/elliptic": "^6.4.12",
"@types/randombytes": "^2.0.0",
"@types/sha.js": "^2.4.0",
"bn.js": "^4.11.9",
"c32check": "^1.1.1",
"cross-fetch": "^3.0.5",
"elliptic": "^6.5.3",
"lodash": "^4.17.20",
"lodash-es": "4.17.20",
"randombytes": "^2.1.0",
"ripemd160-min": "^0.0.6",
"sha.js": "^2.4.11",
"smart-buffer": "^4.1.0"
}
},
"packages/wallet-sdk/node_modules/@stacks/transactions/node_modules/bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
},
"packages/wallet-sdk/node_modules/@types/node": {
"version": "13.13.52",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.52.tgz",
"integrity": "sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ==",
"dev": true
},
"packages/wallet-sdk/node_modules/async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"dependencies": {
"lodash": "^4.17.14"
}
},
"packages/wallet-sdk/node_modules/bn.js": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz",
"integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw=="
},
"packages/wallet-sdk/node_modules/progress": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz",
"integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=",
"engines": {
"node": ">=0.4.0"
}
},
"packages/wallet-sdk/node_modules/schema-inspector": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/schema-inspector/-/schema-inspector-1.7.0.tgz",
"integrity": "sha512-Cj4XP6O3QfDhOq7bIPpz3Ev+sjR++nqFsIggBVIk/8axqFc2p+XSwNBWih9Ut/p8k36f1uCyXB+TzumZUsxVBQ==",
"dependencies": {
"async": "~2.6.3"
}
},
"packages/wallet-sdk/node_modules/triplesec": {
"version": "3.0.27",
"resolved": "https://registry.npmjs.org/triplesec/-/triplesec-3.0.27.tgz",
"integrity": "sha512-FDhkxa3JYnPOerOd+8k+SBmm7cb7KkyX+xXwNFV3XV6dsQgHuRvjtbnzWfPJ2kimeR8ErjZfPd/6r7RH6epHDw==",
"dependencies": {
"iced-error": ">=0.0.9",
"iced-lock": "^1.0.1",
"iced-runtime": "^1.0.2",
"more-entropy": ">=0.0.7",
"progress": "~1.1.2",
"uglify-js": "^3.1.9"
}
}
},
"dependencies": {
@@ -22550,6 +22851,172 @@
}
}
},
"@stacks/wallet-sdk": {
"version": "file:packages/wallet-sdk",
"requires": {
"@stacks/encryption": "^1.0.1",
"@stacks/profile": "^1.1.1-alpha.0",
"@stacks/storage": "^1.1.1-alpha.0",
"@stacks/transactions": "^1.1.1-alpha.0",
"@types/node": "^13.13.10",
"bip32": "2.0.6",
"bip39": "^3.0.2",
"bitcoinjs-lib": "^5.1.6",
"bn.js": "^5.1.1",
"c32check": "^1.0.1",
"jsontokens": "^3.0.0",
"triplesec": "^3.0.27",
"yalc": "1.0.0-pre.49",
"zone-file": "^1.0.0"
},
"dependencies": {
"@stacks/auth": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@stacks/auth/-/auth-1.2.3.tgz",
"integrity": "sha512-EDXX1hiQ9UnAjrX4D1UyeVZQF647RdtC6EYAIxzgn2L6XxARACGpn+d/fRpE1q411y94lF/2Oqj8MPSwDb/NHQ==",
"requires": {
"@stacks/common": "^1.2.2",
"@stacks/encryption": "^1.2.3",
"@stacks/network": "^1.2.2",
"@stacks/profile": "^1.2.3",
"codecov": "^3.7.2",
"cross-fetch": "^3.0.5",
"jsontokens": "^3.0.0",
"query-string": "^6.13.1"
}
},
"@stacks/common": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@stacks/common/-/common-1.2.2.tgz",
"integrity": "sha512-knCqq88EBRCN8AhS7+Sx2PJuRv0EFNChEpqLqCAchCHCQfp5bWad/47Zw+fLP9ccBwFXh4pl1wDtbQLBfDo0+A==",
"requires": {
"cross-fetch": "^3.0.6"
}
},
"@stacks/encryption": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@stacks/encryption/-/encryption-1.2.3.tgz",
"integrity": "sha512-g6p9FGXUHMFKnkbfgObcgP3LUHc/XqRnh+5wNw8kU/R9T0Wbwej2/RfZaxvyvYVu9GP3EXRDPVDmWJFSO1cqWw==",
"requires": {
"@stacks/common": "^1.2.2",
"bip39": "^3.0.2",
"bitcoinjs-lib": "^5.1.10",
"bn.js": "^5.1.2",
"elliptic": "^6.5.2",
"randombytes": "^2.1.0",
"ripemd160-min": "^0.0.6",
"sha.js": "^2.4.11"
}
},
"@stacks/network": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@stacks/network/-/network-1.2.2.tgz",
"integrity": "sha512-xcWwuRrLJn9qqi3PEBcP2UPZHQztTZd31C0aVlzYHttNMir/sY9SrUqSnw45z2Jo4O9pIYYPIiPRtdV91Ho3fw==",
"requires": {
"@stacks/common": "^1.2.2"
}
},
"@stacks/profile": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@stacks/profile/-/profile-1.2.3.tgz",
"integrity": "sha512-bVLoSz8LFiuUS4wnZgVoShNRPaSSBvrAc5SmPZNOtfonUgeNLmwKvskbc6x6exgn/m7wMNp1gY7n+BIBmfST9w==",
"requires": {
"@stacks/common": "^1.2.2",
"@stacks/encryption": "^1.2.3",
"@stacks/network": "^1.2.2",
"bitcoinjs-lib": "^5.1.10",
"jsontokens": "^3.0.0",
"schema-inspector": "^1.7.0",
"zone-file": "^1.0.0"
}
},
"@stacks/storage": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@stacks/storage/-/storage-1.3.3.tgz",
"integrity": "sha512-HSob/SSIF+doiMP8ScDYNNmTK/8vnd1HyNtZk3rpE3PSEqpU1bRPqMF3ehlzMcDsCjCvOx6Ow3OlWBWPQA4QhA==",
"requires": {
"@stacks/auth": "^1.2.3",
"@stacks/common": "^1.2.2",
"@stacks/encryption": "^1.2.3"
}
},
"@stacks/transactions": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-1.4.1.tgz",
"integrity": "sha512-7LFA9yQqlmN+oVJeYaj+NfZyuInJxF8ozJ8kypCmJ9rUrbbGC/es1KyseB96YBiiOh4eLUfRlD1j6boSdNR8aA==",
"requires": {
"@stacks/common": "^1.2.2",
"@stacks/network": "^1.2.2",
"@types/bn.js": "^4.11.6",
"@types/elliptic": "^6.4.12",
"@types/randombytes": "^2.0.0",
"@types/sha.js": "^2.4.0",
"bn.js": "^4.11.9",
"c32check": "^1.1.1",
"cross-fetch": "^3.0.5",
"elliptic": "^6.5.3",
"lodash": "^4.17.20",
"lodash-es": "4.17.20",
"randombytes": "^2.1.0",
"ripemd160-min": "^0.0.6",
"sha.js": "^2.4.11",
"smart-buffer": "^4.1.0"
},
"dependencies": {
"bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
}
}
},
"@types/node": {
"version": "13.13.52",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.52.tgz",
"integrity": "sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ==",
"dev": true
},
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"requires": {
"lodash": "^4.17.14"
}
},
"bn.js": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz",
"integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw=="
},
"progress": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz",
"integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74="
},
"schema-inspector": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/schema-inspector/-/schema-inspector-1.7.0.tgz",
"integrity": "sha512-Cj4XP6O3QfDhOq7bIPpz3Ev+sjR++nqFsIggBVIk/8axqFc2p+XSwNBWih9Ut/p8k36f1uCyXB+TzumZUsxVBQ==",
"requires": {
"async": "~2.6.3"
}
},
"triplesec": {
"version": "3.0.27",
"resolved": "https://registry.npmjs.org/triplesec/-/triplesec-3.0.27.tgz",
"integrity": "sha512-FDhkxa3JYnPOerOd+8k+SBmm7cb7KkyX+xXwNFV3XV6dsQgHuRvjtbnzWfPJ2kimeR8ErjZfPd/6r7RH6epHDw==",
"requires": {
"iced-error": ">=0.0.9",
"iced-lock": "^1.0.1",
"iced-runtime": "^1.0.2",
"more-entropy": ">=0.0.7",
"progress": "~1.1.2",
"uglify-js": "^3.1.9"
}
}
}
},
"@strictsoftware/typedoc-plugin-monorepo": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@strictsoftware/typedoc-plugin-monorepo/-/typedoc-plugin-monorepo-0.4.2.tgz",
@@ -29723,6 +30190,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lodash-es": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.20.tgz",
"integrity": "sha512-JD1COMZsq8maT6mnuz1UMV0jvYD0E0aUsSOdrr1/nAG3dhqQXwRRgeW0cSqH1U43INKcqxaiVIQNOUDld7gRDA=="
},
"lodash._reinterpolate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
@@ -34491,6 +34963,102 @@
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"dev": true
},
"yalc": {
"version": "1.0.0-pre.49",
"resolved": "https://registry.npmjs.org/yalc/-/yalc-1.0.0-pre.49.tgz",
"integrity": "sha512-7fTnwsX4qKnr2h1LVTLQzc9gosFrGnJcBRPnNGsM+3YJSLAjB+i8XnqmNptdktjyc4hOzI+XzN1Wp2kXvKAPxA==",
"dev": true,
"requires": {
"chalk": "^4.1.0",
"detect-indent": "^6.0.0",
"fs-extra": "^8.0.1",
"glob": "^7.1.4",
"ignore": "^5.0.4",
"npm-packlist": "^1.4.1",
"yargs": "^16.1.1"
},
"dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"requires": {
"color-convert": "^2.0.1"
}
},
"cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"dev": true,
"requires": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^7.0.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
},
"detect-indent": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",
"integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==",
"dev": true
},
"wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"requires": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
}
},
"y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true
},
"yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"dev": true,
"requires": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^20.2.2"
}
},
"yargs-parser": {
"version": "20.2.9",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
"dev": true
}
}
},
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",

View File

@@ -0,0 +1,5 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

View File

@@ -0,0 +1,239 @@
# @stacks/wallet-sdk
`@stacks/wallet-sdk` is a library for building wallets for the Stacks blockchain.
<!-- TOC depthFrom:2 -->
- [Features](#features)
- [Key Concepts](#key-concepts)
- [Secret Key](#secret-key)
- [Wallet](#wallet)
- [Account](#account)
- [Derivation paths](#derivation-paths)
- [Usage](#usage)
- [Installation](#installation)
- [Generate a Secret Key](#generate-a-secret-key)
- [Generate a wallet](#generate-a-wallet)
- [Generating new accounts](#generating-new-accounts)
- [Restoring accounts for an existing Wallet](#restoring-accounts-for-an-existing-wallet)
- [Making an authentication response](#making-an-authentication-response)
- [Usage with `@stacks/transactions`](#usage-with-stackstransactions)
- [Getting an account's STX address](#getting-an-accounts-stx-address)
- [Signing Stacks Transactions](#signing-stacks-transactions)
<!-- /TOC -->
## Features
- Generate a wallet from scratch
- Encrypt a wallet with a password
- Restore a wallet and associated accounts
- Generate new accounts in a wallet
- Sign transactions for the Stacks blockchain
- Register usernames on BNS, the naming service built into the Stacks Blockchain
## Key Concepts
### Secret Key
A Secret Key is a 12 or 24 word mnemonic phrase, which can be used to deterministically generate a wallet and any number of addresses. When the same Secret Key is used, the exact same addresses will be generated. The Secret Key acts as an easily rememberable and highly secure mechanism for backing up a wallet.
Secret Keys conform to the [BIP 39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) standard.
### Wallet
A "wallet" is a set of private keys for an individual user. A wallet will contain any number of accounts.
### Account
Accounts act as a way for users to separate assets and data within their own account. You could think of accounts like different Google accounts while logged into Gmail. You can easily switch between different accounts, but they all have different data and information.
- Each account is associated with an individual Stacks address.
- Each account has its own balance and state on the blockchain.
- Accounts can have usernames.
- When a user logs in through a wallet, they choose an individual account from their wallet.
- Application data is completely segregated from different accounts.
- External parties have no way of knowing that two accounts belong to the same wallet
### Derivation paths
Private keys are generated according to the [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) and [BIP44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) standards.
The "coin type" for the Stacks blockchain is `5757`.
The private key for each account's STX address is derived from `m/44'/5757'/0'/0/n`, where `n` is the index of the account.
## Usage
### Installation
With NPM:
```bash
npm install @stacks/wallet-sdk --save
```
With Yarn:
```bash
yarn add @stacks/wallet-sdk
```
### Generate a Secret Key
By default, a random 24-word Secret Key is generated, using 256 bits of entropy. You can generate a random 12-word key by passing `128` as the `entropy` argument.
```ts
import { generateSecretKey } from '@stacks/wallet-sdk';
generateSecretKey();
// aunt birth lounge misery utility blind holiday walnut fuel make gift parent gap picnic exact various express sphere family nerve oil drill engage youth
generateSecretKey(128);
// winter crash infant long upset beauty cram tank short remove decade ladder
```
### Generate a wallet
Create a random Secret Key and a `Wallet` object. When a wallet is generated, the first account is automatically generated as well.
```ts
import { generateWallet, generateSecretKey } from '@stacks/wallet-sdk';
const password = 'password';
const secretKey = generateSecretKey();
const wallet = await generateWallet({
secretKey,
password,
});
```
A `Wallet` is a normal JavaScript object with the following properties:
```ts
interface Wallet {
/** Used when generating app private keys, which encrypt app-specific data */
salt: string;
/** The private key associated with the root of a BIP39 keychain */
rootKey: string;
/** A private key used to encrypt configuration data */
configPrivateKey: string;
/** The encrypted secret key */
encryptedSecretKey: string;
/** A list of accounts generated by this wallet */
accounts: Account[];
}
```
### Generating new accounts
Accounts allow users to use separate Stacks addresses from within the same wallet. Each account has it's own Stacks address and balance. When a user logs into an app, they choose a specific account.
When using `generateNewAccount`, the new account is created with next index, based on the existing accounts in a wallet. For example, if a wallet has 5 accounts, calling `generateNewAccount` will make the sixth account.
```ts
import { generateNewAccount } from '@stacks/wallet-sdk';
const account = generateNewAccount(wallet);
```
An `Account` is a JavaScript object with these properties:
```ts
interface Account {
/** The private key used for STX payments */
stxPrivateKey: string;
/** The private key used in Stacks 1.0 to register BNS names */
dataPrivateKey: string;
/** The salt is the same as the wallet-level salt. Used for app-specific keys */
salt: string;
/** A single username registered via BNS for this account */
username?: string;
/** A profile object that is publicly associated with this account's username */
profile?: Profile;
/** The root of the keychain used to generate app-specific keys */
appsKey: string;
/** The index of this account in the user's wallet. Zero-indexed */
index: number;
}
```
### Restoring accounts for an existing Wallet
When a user restores their wallet in a new app, you can automatically restore any previously-used accounts from the same wallet. It will also restore any usernames owned by this user.
The private keys used to encrypt this data is derived from the path `m/44/5757'/0'/1`. This data is stored in [Gaia](https://docs.blockstack.org/build-apps/references/gaia), the decentralized storage system in the Stacks network. Users can host their own Gaia hub, and this library's API can use that Gaia hub, if provided.
```ts
import { restoreWalletAccounts } from '@stacks/wallet-sdk';
const restoredWallet = await restoreWalletAccounts({
// `baseWallet` is returned from `generateWallet`
wallet: baseWallet,
gaiaHubUrl: 'https://hub.blockstack.org',
});
```
### Making an authentication response
With an account, you can generate an authentication response, which conforms to the Stacks authentication protocol. The resulting `authResponse` is a string, representing a signed JSON web token. [Learn more about the authentication protocol](https://docs.blockstack.org/build-apps/guides/authentication#authresponse-payload-schema).
```ts
// The transit public key is provided in an "authentication request"
const transitPublicKey = 'xxxx';
const authResponse = await makeAuthResponse({
gaiaHubUrl: 'https://hub.blockstack.org',
appDomain: 'https://example-app.com',
transitPublicKey,
scopes: ['publish_data'],
// `account` is one of the user's accounts
account,
});
```
### Usage with `@stacks/transactions`
This library is meant to be used in conjunction with the [`@stacks/transactions`](https://github.com/blockstack/stacks.js/tree/master/packages/transactions) library for signing transactions.
#### Getting an account's STX address
```ts
import { getStxAddress } from '@stacks/wallet-sdk';
import { TransactionVersion } from '@stacks/transactions';
// get an account from the user's wallet
const account = wallet.accounts[0];
const testnetAddress = getStxAddress({ account, transactionVersion: TransactionVersion.Testnet });
const mainnetAddress = getStxAddress({ account, transactionVersion: TransactionVersion.Mainnet });
```
#### Signing Stacks Transactions
You can generate signed transactions by following the documentation from `@stacks/transactions`. Use the `stxPrivateKey` of an account as the `senderKey` option when creating a transaction.
```ts
import {
makeSTXTokenTransfer,
makeStandardSTXPostCondition,
StacksMainnet,
broadcastTransaction,
} from '@stacks/transactions';
const BigNum = require('bn.js');
const network = new StacksMainnet();
// get an account from the user's wallet
const account = wallet.accounts[0];
const txOptions = {
recipient: 'SP3FGQ8Z7JY9BWYZ5WM53E0M9NK7WHJF0691NZ159',
amount: new BigNum(12345),
senderKey: account.stxPrivateKey,
network,
};
const transaction = await makeSTXTokenTransfer(txOptions);
```

View File

@@ -0,0 +1,24 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { pathsToModuleNameMapper } = require('ts-jest/utils');
// In the following statement, replace `./tsconfig` with the path to your `tsconfig` file
// which contains the path mapping (ie the `compilerOptions.paths` option):
const { compilerOptions } = require('./tests/tsconfig');
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
coverageDirectory: './coverage/',
collectCoverage: true,
globals: {
'ts-jest': {
diagnostics: {
ignoreCodes: ['TS151001'],
},
},
},
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths),
moduleFileExtensions: ['js', 'ts', 'd.ts'],
setupFilesAfterEnv: ['./tests/setup.ts'],
setupFiles: ['./tests/global-setup.ts'],
};

View File

@@ -0,0 +1,56 @@
{
"name": "@stacks/wallet-sdk",
"version": "1.0.0-beta.21",
"description": "A library for building Stacks blockchain wallets",
"main": "./dist/index.js",
"umd:main": "./dist/wallet-sdk.umd.production.js",
"module": "./dist/wallet-sdk.esm.js",
"author": "Hank Stoever",
"types": "./dist/wallet-sdk/src/index.d.ts",
"scripts": {
"clean": "shx rm -rf ./lib*/",
"dev": "cross-env NODE_ENV=development tsdx watch",
"build": "cross-env NODE_ENV=production tsdx build --format=cjs,esm,umd",
"build-all": "run-p build:*",
"build:cjs": "tsc --outDir ./lib -m commonjs -t es2017",
"build:esm": "tsc --outDir ./lib-esm -m es6 -t es2017",
"build:cjs:watch": "tsc --outDir ./lib -m commonjs -t es2017 --watch",
"build:esm:watch": "tsc --outDir ./lib-esm -m es6 -t es2017 --watch",
"test": "jest",
"test:watch": "jest --watch --coverage=false",
"lint": "yarn lint:eslint && yarn lint:prettier",
"lint:eslint": "eslint \"src/**/*.{ts,tsx}\"",
"lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix",
"lint:prettier": "prettier --check \"src/**/*.{ts,tsx}\" *.js",
"lint:prettier:fix": "prettier --write \"src/**/*.{ts,tsx}\" *.js",
"depcheck": "depcheck --ignores='@types/*,eslint*,safe-buffer,codecov,@typescript-eslint/*,@blockstack/*'",
"typecheck": "tsc --noEmit",
"prepublishOnly": "yarn build"
},
"unpkg": "./dist/wallet-sdk.cjs.production.min.js",
"license": "MIT",
"files": [
"dist"
],
"devDependencies": {
"@types/node": "^13.13.10",
"yalc": "1.0.0-pre.49"
},
"dependencies": {
"@stacks/encryption": "^1.0.1",
"@stacks/profile": "^1.1.1-alpha.0",
"@stacks/storage": "^1.1.1-alpha.0",
"@stacks/transactions": "^1.1.1-alpha.0",
"bip32": "2.0.6",
"bip39": "^3.0.2",
"bitcoinjs-lib": "^5.1.6",
"bn.js": "^5.1.1",
"c32check": "^1.0.1",
"jsontokens": "^3.0.0",
"triplesec": "^3.0.27",
"zone-file": "^1.0.0"
},
"publishConfig": {
"access": "public"
}
}

View File

@@ -0,0 +1,100 @@
import { BIP32Interface } from 'bip32';
import { createSha2Hash, ecPairToHexString } from '@stacks/encryption';
import { assertIsTruthy } from './utils';
import { ECPair } from 'bitcoinjs-lib';
import { Account } from './models/account';
import { WalletKeys } from './models/wallet';
const DATA_DERIVATION_PATH = `m/888'/0'`;
const WALLET_CONFIG_PATH = `m/44/5757'/0'/1`;
const STX_DERIVATION_PATH = `m/44'/5757'/0'/0`;
export const deriveWalletKeys = async (rootNode: BIP32Interface): Promise<WalletKeys> => {
assertIsTruthy(rootNode.privateKey);
const derived: WalletKeys = {
salt: await deriveSalt(rootNode),
rootKey: rootNode.toBase58(),
configPrivateKey: deriveConfigPrivateKey(rootNode).toString('hex'),
};
return derived;
};
/**
* Derive the `configPrivateKey` for a wallet.
*
* This key is derived from the path `m/5757'/0'/1`, using `1` for change option, following the bip44 recommendation
* for keys relating to non-public data.
*
* This key is used to encrypt configuration data related to a wallet, so the user's
* configuration can be synced across wallets.
*
* @param rootNode A keychain that was created using the wallet's seed phrase
*/
export const deriveConfigPrivateKey = (rootNode: BIP32Interface): Buffer => {
const derivedConfigKey = rootNode.derivePath(WALLET_CONFIG_PATH).privateKey;
if (!derivedConfigKey) {
throw new TypeError('Unable to derive config key for wallet identities');
}
return derivedConfigKey;
};
/**
* Before the Stacks Wallet, the authenticator used with Connect used a different format
* and path for the config file.
*
* The path for this key is `m/45'`
* @param rootNode A keychain that was created using the wallet's seed phrase
*/
export const deriveLegacyConfigPrivateKey = (rootNode: BIP32Interface): string => {
const derivedLegacyKey = rootNode.deriveHardened(45).privateKey;
if (!derivedLegacyKey) {
throw new TypeError('Unable to derive config key for wallet identities');
}
const configPrivateKey = derivedLegacyKey.toString('hex');
return configPrivateKey;
};
/**
* Generate a salt, which is used for generating an app-specific private key
* @param rootNode
*/
export const deriveSalt = async (rootNode: BIP32Interface) => {
const identitiesKeychain = rootNode.derivePath(DATA_DERIVATION_PATH);
const publicKeyHex = Buffer.from(identitiesKeychain.publicKey.toString('hex'));
const sha2Hash = await createSha2Hash();
const saltData = await sha2Hash.digest(publicKeyHex, 'sha256');
const salt = saltData.toString('hex');
return salt;
};
export const deriveAccount = ({
rootNode,
index,
salt,
}: {
rootNode: BIP32Interface;
index: number;
salt: string;
}): Account => {
const childKey = rootNode.derivePath(STX_DERIVATION_PATH).derive(index);
assertIsTruthy(childKey.privateKey);
const ecPair = ECPair.fromPrivateKey(childKey.privateKey);
const stxPrivateKey = ecPairToHexString(ecPair);
const identitiesKeychain = rootNode.derivePath(DATA_DERIVATION_PATH);
const identityKeychain = identitiesKeychain.deriveHardened(index);
if (!identityKeychain.privateKey) throw new Error('Must have private key to derive identities');
const dataPrivateKey = identityKeychain.privateKey.toString('hex');
const appsKey = identityKeychain.deriveHardened(0).toBase58();
return {
stxPrivateKey,
dataPrivateKey,
appsKey,
salt,
index,
};
};

View File

@@ -0,0 +1,23 @@
import { encryptMnemonic, decryptMnemonic } from '@stacks/encryption';
import { decrypt as triplesecDecrypt } from 'triplesec';
/**
* Decrypt an encrypted mnemonic phrase with a password.
* Legacy triplesec encrypted payloads are also supported.
* @param data - Buffer or hex-encoded string of the encrypted mnemonic
* @param password - Password for data
* @return the raw mnemonic phrase
*/
export function decrypt(dataBuffer: Buffer | string, password: string): Promise<string> {
return decryptMnemonic(dataBuffer, password, triplesecDecrypt);
}
/**
* Encrypt a raw mnemonic phrase to be password protected
* @param phrase - Raw mnemonic phrase
* @param password - Password to encrypt mnemonic with
* @return The encrypted phrase
* */
export function encrypt(phrase: string, password: string): Promise<Buffer> {
return encryptMnemonic(phrase, password);
}

View File

@@ -0,0 +1,58 @@
import { generateMnemonic, mnemonicToSeed } from 'bip39';
import { fromSeed } from 'bip32';
import randomBytes from 'randombytes';
import { Wallet, getRootNode } from './models/wallet';
import { encrypt } from './encryption';
import { deriveAccount, deriveWalletKeys } from './derive';
export type AllowedKeyEntropyBits = 128 | 256;
export const generateSecretKey = (entropy: AllowedKeyEntropyBits = 256) => {
const secretKey = generateMnemonic(entropy, randomBytes);
return secretKey;
};
/**
* Generate a new [[Wallet]].
* @param secretKey A 12 or 24 word mnemonic phrase. Must be a valid bip39 mnemonic.
* @param password A password used to encrypt the wallet
*/
export const generateWallet = async ({
secretKey,
password,
}: {
secretKey: string;
password: string;
}): Promise<Wallet> => {
const ciphertextBuffer = await encrypt(secretKey, password);
const encryptedSecretKey = ciphertextBuffer.toString('hex');
const rootPrivateKey = await mnemonicToSeed(secretKey);
const rootNode = fromSeed(rootPrivateKey);
const walletKeys = await deriveWalletKeys(rootNode);
const wallet = {
...walletKeys,
encryptedSecretKey,
accounts: [],
config: {
accounts: [],
},
};
return generateNewAccount(wallet);
};
export const generateNewAccount = (wallet: Wallet) => {
const accountIndex = wallet.accounts.length;
const newAccount = deriveAccount({
rootNode: getRootNode(wallet),
index: accountIndex,
salt: wallet.salt,
});
return {
...wallet,
accounts: [...wallet.accounts, newAccount],
};
};

View File

@@ -0,0 +1,6 @@
export * from './generate';
export * from './derive';
export * from './models/wallet';
export * from './models/account';
export * from './models/wallet-config';
export * from './encryption';

View File

@@ -0,0 +1,152 @@
import {
ecPairToAddress,
getPublicKeyFromPrivate,
hashCode,
hashSha256Sync,
publicKeyToAddress,
} from '@stacks/encryption';
import { makeAuthResponse as _makeAuthResponse } from '@stacks/auth';
import { TransactionVersion, getAddressFromPrivateKey } from '@stacks/transactions';
import { fromBase58 } from 'bip32';
import {
DEFAULT_PROFILE,
fetchAccountProfileUrl,
fetchProfileFromUrl,
Profile,
signAndUploadProfile,
} from './profile';
import { ECPair } from 'bitcoinjs-lib';
import { connectToGaiaHubWithConfig, getHubInfo, makeGaiaAssociationToken } from '../utils';
export interface Account {
/** The private key used for STX payments */
stxPrivateKey: string;
/** The private key used in Stacks 1.0 to register BNS names */
dataPrivateKey: string;
/** The salt is the same as the wallet-level salt. Used for app-specific keys */
salt: string;
/** A single username registered via BNS for this account */
username?: string;
/** A profile object that is publicly associated with this account's username */
profile?: Profile;
/** The root of the keychain used to generate app-specific keys */
appsKey: string;
/** The index of this account in the user's wallet */
index: number;
}
export const getStxAddress = ({
account,
transactionVersion = TransactionVersion.Testnet,
}: {
account: Account;
transactionVersion?: TransactionVersion;
}): string => {
return getAddressFromPrivateKey(account.stxPrivateKey, transactionVersion);
};
/**
* Get the display name of an account.
*
* If the account has a username, it will return the first part of the username, so `myname.id` => `myname`, and
* `myname.blockstack.id` => `myname`.
*
* If the account has no username, it returns `Account ${acount.index}`
*
*/
export const getAccountDisplayName = (account: Account) => {
if (account.username) {
return account.username.split('.')[0];
}
return `Account ${account.index + 1}`;
};
export const getGaiaAddress = (account: Account) => {
const publicKey = getPublicKeyFromPrivate(account.dataPrivateKey);
const address = publicKeyToAddress(publicKey);
return address;
};
export const getAppPrivateKey = ({
account,
appDomain,
}: {
account: Account;
appDomain: string;
}) => {
const hashBuffer = hashSha256Sync(Buffer.from(`${appDomain}${account.salt}`));
const hash = hashBuffer.toString('hex');
const appIndex = hashCode(hash);
const appsNode = fromBase58(account.appsKey);
const appKeychain = appsNode.deriveHardened(appIndex);
if (!appKeychain.privateKey) throw 'Needs private key';
return appKeychain.privateKey.toString('hex');
};
export const makeAuthResponse = async ({
account,
appDomain,
transitPublicKey,
scopes = [],
gaiaHubUrl,
}: {
account: Account;
appDomain: string;
transitPublicKey: string;
scopes?: string[];
gaiaHubUrl: string;
}) => {
const appPrivateKey = getAppPrivateKey({ account, appDomain });
const hubInfo = await getHubInfo(gaiaHubUrl);
const profileUrl = await fetchAccountProfileUrl({ account, gaiaHubUrl: hubInfo.read_url_prefix });
const profile = (await fetchProfileFromUrl(profileUrl)) || DEFAULT_PROFILE;
if (scopes.includes('publish_data')) {
if (!profile.apps) {
profile.apps = {};
}
const challengeSigner = ECPair.fromPrivateKey(Buffer.from(appPrivateKey, 'hex'));
const storageUrl = `${hubInfo.read_url_prefix}${ecPairToAddress(challengeSigner)}/`;
profile.apps[appDomain] = storageUrl;
if (!profile.appsMeta) {
profile.appsMeta = {};
}
profile.appsMeta[appDomain] = {
storage: storageUrl,
publicKey: challengeSigner.publicKey.toString('hex'),
};
const gaiaHubConfig = connectToGaiaHubWithConfig({
hubInfo,
privateKey: account.dataPrivateKey,
gaiaHubUrl,
});
await signAndUploadProfile({ profile, account, gaiaHubUrl, gaiaHubConfig });
}
const compressedAppPublicKey = getPublicKeyFromPrivate(appPrivateKey.slice(0, 64));
const associationToken = makeGaiaAssociationToken({
privateKey: account.dataPrivateKey,
childPublicKeyHex: compressedAppPublicKey,
});
return _makeAuthResponse(
account.dataPrivateKey,
{
...(profile || {}),
stxAddress: {
testnet: getStxAddress({ account, transactionVersion: TransactionVersion.Testnet }),
mainnet: getStxAddress({ account, transactionVersion: TransactionVersion.Mainnet }),
},
},
account.username || '',
{
profileUrl,
},
undefined,
appPrivateKey,
undefined,
transitPublicKey,
gaiaHubUrl,
undefined,
associationToken
);
};

View File

@@ -0,0 +1,52 @@
import { decryptContent } from '@stacks/encryption';
import { GaiaHubConfig } from '@stacks/storage';
import { fetchPrivate } from '@stacks/transactions';
import { deriveLegacyConfigPrivateKey } from '../derive';
import { getRootNode, Wallet } from './wallet';
export interface LegacyConfigApp {
origin: string;
scopes: string[];
lastLoginAt: number;
appIcon: string;
name: string;
}
interface LegacyConfigIdentity {
username?: string;
address: string;
apps: {
[origin: string]: LegacyConfigApp;
};
}
export interface LegacyWalletConfig {
identities: LegacyConfigIdentity[];
hideWarningForReusingIdentity?: boolean;
}
export async function fetchLegacyWalletConfig({
wallet,
gaiaHubConfig,
}: {
wallet: Wallet;
gaiaHubConfig: GaiaHubConfig;
}) {
const rootNode = getRootNode(wallet);
const legacyConfigKey = deriveLegacyConfigPrivateKey(rootNode);
try {
const response = await fetchPrivate(
`${gaiaHubConfig.url_prefix}${gaiaHubConfig.address}/wallet-config.json`
);
if (!response.ok) return null;
const encrypted = await response.text();
const configJSON = (await decryptContent(encrypted, {
privateKey: legacyConfigKey,
})) as string;
const config: LegacyWalletConfig = JSON.parse(configJSON);
return config;
} catch (error) {
console.error(error);
return null;
}
}

View File

@@ -0,0 +1,138 @@
import { getProfileURLFromZoneFile } from '../utils';
import { Account, getGaiaAddress } from './account';
import { signProfileToken, wrapProfileToken } from '@stacks/profile';
import { connectToGaiaHub, GaiaHubConfig, uploadToGaiaHub } from '@stacks/storage';
import { getPublicKeyFromPrivate } from '@stacks/encryption';
import { fetchPrivate } from '@stacks/common';
const PERSON_TYPE = 'Person';
const CONTEXT = 'http://schema.org';
const IMAGE_TYPE = 'ImageObject';
export interface ProfileImage {
'@type': typeof IMAGE_TYPE;
name: string;
contentUrl: string;
}
export interface Profile {
'@type': typeof PERSON_TYPE;
'@context': typeof CONTEXT;
apps?: {
[origin: string]: string;
};
appsMeta?: {
[origin: string]: {
publicKey: string;
storage: string;
};
};
name?: string;
image?: ProfileImage[];
[key: string]: any;
}
export const DEFAULT_PROFILE: Profile = {
'@type': 'Person',
'@context': 'http://schema.org',
};
export const DEFAULT_PROFILE_FILE_NAME = 'profile.json';
export const fetchAccountProfile = async ({
account,
gaiaHubUrl,
}: {
account: Account;
gaiaHubUrl: string;
}) => {
const url = await fetchAccountProfileUrl({ gaiaHubUrl, account });
return fetchProfileFromUrl(url);
};
export const fetchProfileFromUrl = async (profileUrl: string) => {
try {
const res = await fetchPrivate(profileUrl);
if (res.ok) {
const json = await res.json();
const { decodedToken } = json[0];
return decodedToken.payload?.claim as Profile;
}
if (res.status === 404) {
return null;
}
throw new Error('Network error when fetching profile');
} catch (error) {
return null;
}
};
export const fetchAccountProfileUrl = async ({
account,
gaiaHubUrl,
}: {
account: Account;
gaiaHubUrl: string;
}) => {
if (account.username) {
try {
const url = await getProfileURLFromZoneFile(account.username);
if (url) return url;
} catch (error) {
if (process.env.NODE_ENV !== 'test') {
console.warn('Error fetching profile URL from zone file:', error);
}
}
}
return `${gaiaHubUrl}${getGaiaAddress(account)}/profile.json`;
};
export function signProfileForUpload({ profile, account }: { profile: Profile; account: Account }) {
const privateKey = account.dataPrivateKey;
const publicKey = getPublicKeyFromPrivate(privateKey);
const token = signProfileToken(profile, privateKey, { publicKey });
const tokenRecord = wrapProfileToken(token);
const tokenRecords = [tokenRecord];
return JSON.stringify(tokenRecords, null, 2);
}
export async function uploadProfile({
gaiaHubUrl,
account,
signedProfileTokenData,
gaiaHubConfig,
}: {
gaiaHubUrl: string;
account: Account;
signedProfileTokenData: string;
gaiaHubConfig?: GaiaHubConfig;
}) {
const identityHubConfig =
gaiaHubConfig || (await connectToGaiaHub(gaiaHubUrl, account.dataPrivateKey));
await uploadToGaiaHub(
DEFAULT_PROFILE_FILE_NAME,
signedProfileTokenData,
identityHubConfig,
undefined,
undefined,
undefined,
true
);
}
export const signAndUploadProfile = async ({
profile,
gaiaHubUrl,
account,
gaiaHubConfig,
}: {
profile: Profile;
gaiaHubUrl: string;
account: Account;
gaiaHubConfig?: GaiaHubConfig;
}) => {
const signedProfileTokenData = signProfileForUpload({ profile, account });
await uploadProfile({ gaiaHubUrl, account, signedProfileTokenData, gaiaHubConfig });
};

View File

@@ -0,0 +1,158 @@
import { Wallet } from './wallet';
import { Account } from './account';
import { GaiaHubConfig, connectToGaiaHub, uploadToGaiaHub } from '@stacks/storage';
import { decryptContent, encryptContent, getPublicKeyFromPrivate } from '@stacks/encryption';
import { fetchPrivate } from '@stacks/common';
export interface ConfigApp {
origin: string;
scopes: string[];
lastLoginAt: number;
appIcon: string;
name: string;
}
export interface ConfigAccount {
username?: string;
apps: {
[origin: string]: ConfigApp;
};
}
export interface WalletConfig {
accounts: ConfigAccount[];
meta?: {
[key: string]: any;
};
}
export const createWalletGaiaConfig = async ({
gaiaHubUrl,
wallet,
}: {
gaiaHubUrl: string;
wallet: Wallet;
}): Promise<GaiaHubConfig> => {
return connectToGaiaHub(gaiaHubUrl, wallet.configPrivateKey);
};
export const getOrCreateWalletConfig = async ({
wallet,
gaiaHubConfig,
skipUpload,
}: {
wallet: Wallet;
gaiaHubConfig: GaiaHubConfig;
skipUpload?: boolean;
}): Promise<WalletConfig> => {
const config = await fetchWalletConfig({ wallet, gaiaHubConfig });
if (config) return config;
const newConfig = makeWalletConfig(wallet);
if (!skipUpload) {
await updateWalletConfig({ wallet, gaiaHubConfig });
}
return newConfig;
};
export const fetchWalletConfig = async ({
wallet,
gaiaHubConfig,
}: {
wallet: Wallet;
gaiaHubConfig: GaiaHubConfig;
}) => {
try {
const response = await fetchPrivate(
`${gaiaHubConfig.url_prefix}${gaiaHubConfig.address}/wallet-config.json`
);
if (!response.ok) return null;
const encrypted = await response.text();
const configJSON = (await decryptContent(encrypted, {
privateKey: wallet.configPrivateKey,
})) as string;
const config: WalletConfig = JSON.parse(configJSON);
return config;
} catch (error) {
console.error(error);
return null;
}
};
export const updateWalletConfig = async ({
wallet,
walletConfig: _walletConfig,
gaiaHubConfig,
}: {
wallet: Wallet;
walletConfig?: WalletConfig;
gaiaHubConfig: GaiaHubConfig;
}) => {
const walletConfig = _walletConfig || makeWalletConfig(wallet);
const encrypted = await encryptWalletConfig({ wallet, walletConfig });
await uploadToGaiaHub(
'wallet-config.json',
encrypted,
gaiaHubConfig,
undefined,
undefined,
undefined,
true
);
return walletConfig;
};
export function makeWalletConfig(wallet: Wallet): WalletConfig {
return {
accounts: wallet.accounts.map(account => ({
username: account.username,
apps: {},
})),
};
}
export const encryptWalletConfig = async ({
wallet,
walletConfig,
}: {
wallet: Wallet;
walletConfig: WalletConfig;
}) => {
const publicKey = getPublicKeyFromPrivate(wallet.configPrivateKey);
const encrypted = await encryptContent(JSON.stringify(walletConfig), { publicKey });
return encrypted;
};
export const updateWalletConfigWithApp = async ({
wallet,
account,
app,
gaiaHubConfig,
walletConfig,
}: {
wallet: Wallet;
account: Account;
app: ConfigApp;
gaiaHubConfig: GaiaHubConfig;
walletConfig: WalletConfig;
}) => {
wallet.accounts.forEach((account, index) => {
const configApp = walletConfig.accounts[index];
if (configApp) {
configApp.apps = configApp.apps || {};
configApp.username = account.username;
walletConfig.accounts[index] = configApp;
} else {
walletConfig.accounts.push({
username: account.username,
apps: {},
});
}
});
const configAccount = walletConfig.accounts[account.index];
configAccount.apps = configAccount.apps || {};
configAccount.apps[app.origin] = app;
walletConfig.accounts[account.index] = configAccount;
await updateWalletConfig({ wallet, walletConfig: walletConfig, gaiaHubConfig });
return walletConfig;
};

View File

@@ -0,0 +1,136 @@
import { fromBase58 } from 'bip32';
import { deriveAccount, deriveLegacyConfigPrivateKey } from '../derive';
import { connectToGaiaHubWithConfig, getHubInfo } from '../utils';
import { Account } from './account';
import { fetchLegacyWalletConfig } from './legacy-wallet-config';
import { fetchWalletConfig, updateWalletConfig, WalletConfig } from './wallet-config';
/**
* This object represents the keys that were derived from the root-level
* keychain of a wallet.
*/
export interface WalletKeys {
/** Used when generating app private keys, which encrypt app-specific data */
salt: string;
/** The private key associated with the root of a BIP39 keychain */
rootKey: string;
/** A private key used to encrypt configuration data */
configPrivateKey: string;
}
export interface Wallet extends WalletKeys {
/** The encrypted secret key */
encryptedSecretKey: string;
/** A list of accounts generated by this wallet */
accounts: Account[];
}
export interface LockedWallet {
encryptedSecretKey: string;
}
export const getRootNode = (wallet: Wallet) => {
return fromBase58(wallet.rootKey);
};
/**
* Restore wallet accounts by checking for encrypted WalletConfig files,
* stored in Gaia.
*
* This helps provide a better UX for users, so we can keep track of accounts they've
* created, and usernames they've used.
*/
export async function restoreWalletAccounts({
wallet,
gaiaHubUrl,
}: {
wallet: Wallet;
gaiaHubUrl: string;
}): Promise<Wallet> {
const hubInfo = await getHubInfo(gaiaHubUrl);
const rootNode = getRootNode(wallet);
const legacyGaiaConfig = connectToGaiaHubWithConfig({
hubInfo,
privateKey: deriveLegacyConfigPrivateKey(getRootNode(wallet)),
gaiaHubUrl,
});
const currentGaiaConfig = connectToGaiaHubWithConfig({
hubInfo,
privateKey: wallet.configPrivateKey,
gaiaHubUrl,
});
const [walletConfig, legacyWalletConfig] = await Promise.all([
fetchWalletConfig({ wallet, gaiaHubConfig: currentGaiaConfig }),
fetchLegacyWalletConfig({ wallet, gaiaHubConfig: legacyGaiaConfig }),
]);
// Restore from existing config
if (
walletConfig &&
walletConfig.accounts.length >= (legacyWalletConfig?.identities.length || 0)
) {
const newAccounts = walletConfig.accounts.map((account, index) => {
let existingAccount = wallet.accounts[index];
if (!existingAccount) {
existingAccount = deriveAccount({
rootNode,
index,
salt: wallet.salt,
});
}
return {
...existingAccount,
username: account.username,
};
});
return {
...wallet,
accounts: newAccounts,
};
}
// Restore from legacy config, and upload a new one
if (legacyWalletConfig) {
const newAccounts = legacyWalletConfig.identities.map((identity, index) => {
let existingAccount = wallet.accounts[index];
if (!existingAccount) {
existingAccount = deriveAccount({
rootNode,
index,
salt: wallet.salt,
});
}
return {
...existingAccount,
username: identity.username,
};
});
const meta: Record<string, boolean> = {};
if (legacyWalletConfig.hideWarningForReusingIdentity) {
meta.hideWarningForReusingIdentity = true;
}
const newConfig: WalletConfig = {
accounts: legacyWalletConfig.identities.map(identity => ({
username: identity.username,
apps: identity.apps,
})),
meta,
};
await updateWalletConfig({
wallet,
walletConfig: newConfig,
gaiaHubConfig: currentGaiaConfig,
});
return {
...wallet,
accounts: newAccounts,
};
}
return wallet;
}

View File

@@ -0,0 +1,11 @@
declare module 'zone-file' {
interface URI {
target: string;
}
export interface ZoneFile {
$origin: string;
uri: URI[];
}
export const parseZoneFile: (zoneFile: string) => ZoneFile;
}

View File

@@ -0,0 +1,149 @@
import { hexStringToECPair } from '@stacks/encryption';
import { getPublicKeyFromPrivate } from '@stacks/encryption';
import { ecPairToAddress, randomBytes } from '@stacks/encryption';
import { AssertionError } from 'assert';
import { parseZoneFile } from 'zone-file';
import { GaiaHubConfig } from '@stacks/storage';
import { TokenSigner, Json } from 'jsontokens';
import { fetchPrivate } from '@stacks/common';
export function assertIsTruthy<T>(val: T): asserts val is NonNullable<T> {
if (val === undefined || val === null) {
throw new AssertionError({ expected: true, actual: val });
}
}
interface NameInfoResponse {
address: string;
zonefile: string;
}
export const getProfileURLFromZoneFile = async (name: string) => {
const url = `https://core.blockstack.org/v1/names/${name}`;
const res = await fetchPrivate(url);
if (res.ok) {
const nameInfo: NameInfoResponse = await res.json();
const zone = parseZoneFile(nameInfo.zonefile);
return zone.uri[0].target;
}
return;
};
interface HubInfo {
challenge_text?: string;
read_url_prefix: string;
}
export const getHubInfo = async (gaiaHubUrl: string) => {
const response = await fetchPrivate(`${gaiaHubUrl}/hub_info`);
const data: HubInfo = await response.json();
return data;
};
export const getHubPrefix = async (gaiaHubUrl: string) => {
const { read_url_prefix } = await getHubInfo(gaiaHubUrl);
return read_url_prefix;
};
const makeGaiaAuthToken = ({
hubInfo,
privateKey,
gaiaHubUrl,
}: {
hubInfo: HubInfo;
privateKey: string;
gaiaHubUrl: string;
}) => {
const challengeText = hubInfo.challenge_text;
const iss = getPublicKeyFromPrivate(privateKey);
const salt = randomBytes(16).toString('hex');
const payload: GaiaAuthPayload = {
gaiaHubUrl,
iss,
salt,
};
if (challengeText) {
payload.gaiaChallenge = challengeText;
}
const token = new TokenSigner('ES256K', privateKey).sign(payload);
return `v1:${token}`;
};
interface ConnectToGaiaOptions {
hubInfo: HubInfo;
privateKey: string;
gaiaHubUrl: string;
}
export const connectToGaiaHubWithConfig = ({
hubInfo,
privateKey,
gaiaHubUrl,
}: ConnectToGaiaOptions): GaiaHubConfig => {
const readURL = hubInfo.read_url_prefix;
const token = makeGaiaAuthToken({ hubInfo, privateKey, gaiaHubUrl });
const address = ecPairToAddress(
hexStringToECPair(privateKey + (privateKey.length === 64 ? '01' : ''))
);
return {
url_prefix: readURL,
max_file_upload_size_megabytes: 100,
address,
token,
server: gaiaHubUrl,
};
};
interface GaiaAuthPayload {
gaiaHubUrl: string;
iss: string;
salt: string;
[key: string]: Json;
}
export const makeGaiaAssociationToken = ({
privateKey: secretKeyHex,
childPublicKeyHex,
}: {
privateKey: string;
childPublicKeyHex: string;
}): string => {
const LIFETIME_SECONDS = 365 * 24 * 3600;
const signerKeyHex = secretKeyHex.slice(0, 64);
const compressedPublicKeyHex = getPublicKeyFromPrivate(signerKeyHex);
const salt = randomBytes(16).toString('hex');
const payload = {
childToAssociate: childPublicKeyHex,
iss: compressedPublicKeyHex,
exp: LIFETIME_SECONDS + new Date().getTime() / 1000,
iat: Date.now() / 1000,
salt,
};
const tokenSigner = new TokenSigner('ES256K', signerKeyHex);
const token = tokenSigner.sign(payload);
return token;
};
/**
* When you already know the Gaia read URL, make a Gaia config that doesn't have to fetch `/hub_info`
*/
export const convertGaiaHubConfig = ({
gaiaHubConfig,
privateKey,
}: {
gaiaHubConfig: GaiaHubConfig;
privateKey: string;
}): GaiaHubConfig => {
const address = ecPairToAddress(
hexStringToECPair(privateKey + (privateKey.length === 64 ? '01' : ''))
);
return {
url_prefix: gaiaHubConfig.url_prefix,
max_file_upload_size_megabytes: 100,
address,
token: 'not_used',
server: 'not_used',
};
};

View File

@@ -0,0 +1,35 @@
import {
deriveWalletKeys,
deriveAccount,
getStxAddress,
deriveLegacyConfigPrivateKey,
} from '../src';
import { mnemonicToSeed } from 'bip39';
import { fromBase58, fromSeed } from 'bip32';
import { TransactionVersion } from '@stacks/transactions';
test('keys are serialized, and can be deserialized properly', async () => {
const secretKey =
'sound idle panel often situate develop unit text design antenna ' +
'vendor screen opinion balcony share trigger accuse scatter visa uniform brass ' +
'update opinion media';
const rootPrivateKey = await mnemonicToSeed(secretKey);
const rootNode1 = fromSeed(rootPrivateKey);
const derived = await deriveWalletKeys(rootNode1);
const rootNode = fromBase58(derived.rootKey);
const account = deriveAccount({ rootNode, index: 0, salt: derived.salt });
expect(getStxAddress({ account, transactionVersion: TransactionVersion.Mainnet })).toEqual(
'SP384CVPNDTYA0E92TKJZQTYXQHNZSWGCAG7SAPVB'
);
});
test('backwards compatible legacy config private key derivation', async () => {
const secretKey =
'sound idle panel often situate develop unit text design antenna ' +
'vendor screen opinion balcony share trigger accuse scatter visa uniform brass ' +
'update opinion media';
const rootPrivateKey = await mnemonicToSeed(secretKey);
const rootNode = fromSeed(rootPrivateKey);
const legacyKey = deriveLegacyConfigPrivateKey(rootNode);
expect(legacyKey).toEqual('767b51d866d068b02ce126afe3737896f4d0c486263d9b932f2822109565a3c6');
});

View File

@@ -0,0 +1,72 @@
import { validateMnemonic } from 'bip39';
import {
generateSecretKey,
generateWallet,
getGaiaAddress,
getStxAddress,
getAppPrivateKey,
} from '../src';
import { TransactionVersion } from '@stacks/transactions';
describe(generateSecretKey, () => {
test('generates a 24 word phrase by default', () => {
const key = generateSecretKey();
const words = key.split(' ');
expect(words.length).toEqual(24);
});
test('generates a 12 word seed if 128 bits entropy', () => {
const key = generateSecretKey(128);
expect(key.split(' ').length).toEqual(12);
});
test('generates a valid mnemonic', () => {
expect(validateMnemonic(generateSecretKey())).toBeTruthy();
expect(validateMnemonic(generateSecretKey(128))).toBeTruthy();
});
});
describe(generateWallet, () => {
test('backwards compatibility test', async () => {
const secretKey =
'sound idle panel often situate develop unit text design antenna ' +
'vendor screen opinion balcony share trigger accuse scatter visa uniform brass ' +
'update opinion media';
const wallet = await generateWallet({ secretKey, password: 'password' });
expect(wallet.salt).toEqual('c15619adafe7e75a195a1a2b5788ca42e585a3fd181ae2ff009c6089de54ed9e');
expect(wallet.rootKey).toEqual(
'xprv9s21ZrQH143K2KAnQL9secDrgY84y7bFrxFdtBjASeGwYyCRgDRjuJAbmnUCjRsGX8z7A7ML2Kj91Uv7aWe8n5suV5bUa6mvcysgCx9TGFc'
);
expect(wallet.configPrivateKey).toEqual(
'67e113e8ccf43fc8a724710620cf369f23c34c396c649615c31e1fd9aaf23d72'
);
expect(wallet.accounts.length).toEqual(1);
const [account] = wallet.accounts;
const appsKey =
'xprvA1y4zBndD83n6PWgVH6ivkTpNQ2WU1UGPg9hWa2q8sCANa7YrYMZFHWMhrbpsarx' +
'XMuQRa4jtaT2YXugwsKrjFgn765tUHu9XjyiDFEjB7f';
expect(account.appsKey).toEqual(appsKey);
expect(account.stxPrivateKey).toEqual(
'8721c6a5237f5e8d361161a7855aa56885a3e19e2ea6ee268fb14eabc5e2ed9001'
);
expect(account.dataPrivateKey).toEqual(
'a29c3e73dba79ab0f84cb792bafd65ec71f243ebe67a7ebd842ef5cdce3b21eb'
);
expect(getStxAddress({ account, transactionVersion: TransactionVersion.Testnet })).toEqual(
'ST384CVPNDTYA0E92TKJZQTYXQHNZSWGCAH0ER64E'
);
expect(getStxAddress({ account, transactionVersion: TransactionVersion.Mainnet })).toEqual(
'SP384CVPNDTYA0E92TKJZQTYXQHNZSWGCAG7SAPVB'
);
expect(getGaiaAddress(account)).toEqual('1JeTQ5cQjsD57YGcsVFhwT7iuQUXJR6BSk');
expect(getAppPrivateKey({ account, appDomain: 'https://banter.pub' })).toEqual(
'6f8b6a170f8b2ee57df5ead49b0f4c8acde05f9e1c4c6ef8223d6a42fabfa314'
);
});
});

View File

@@ -0,0 +1,5 @@
import { GlobalWithFetchMock } from 'jest-fetch-mock';
const customGlobal: GlobalWithFetchMock = (global as any) as GlobalWithFetchMock;
customGlobal.fetch = require('jest-fetch-mock');
customGlobal.fetchMock = customGlobal.fetch;

View File

@@ -0,0 +1,95 @@
import { Wallet, WalletConfig } from '../src';
export const mockWallet: Wallet = {
salt: 'c15619adafe7e75a195a1a2b5788ca42e585a3fd181ae2ff009c6089de54ed9e',
rootKey:
'xprv9s21ZrQH143K2KAnQL9secDrgY84y7bFrxFdtBjASeGwYyCRgDRjuJAbmnUCjRsGX8z7A7ML2Kj91Uv7aWe8n5suV5bUa6mvcysgCx9TGFc',
configPrivateKey: '67e113e8ccf43fc8a724710620cf369f23c34c396c649615c31e1fd9aaf23d72',
encryptedSecretKey:
'dcdbcd5218509dfcfc5866485b31a590fe96ede9790c3926f6eeaffaf99274f50be7d0a1e1d96ca9d3aa83e459c78b3e11c9a5b3c9ae49505e3c92357927388f5b9a33079b25ae9c37e97769e05bbafc628059e0a2e4d97b67891180df6bf19e',
accounts: [
{
stxPrivateKey: '8721c6a5237f5e8d361161a7855aa56885a3e19e2ea6ee268fb14eabc5e2ed9001',
dataPrivateKey: 'a29c3e73dba79ab0f84cb792bafd65ec71f243ebe67a7ebd842ef5cdce3b21eb',
appsKey:
'xprvA1y4zBndD83n6PWgVH6ivkTpNQ2WU1UGPg9hWa2q8sCANa7YrYMZFHWMhrbpsarxXMuQRa4jtaT2YXugwsKrjFgn765tUHu9XjyiDFEjB7f',
index: 0,
salt: 'c15619adafe7e75a195a1a2b5788ca42e585a3fd181ae2ff009c6089de54ed9e',
},
],
};
export const mockAccount = mockWallet.accounts[0];
export const mockWalletConfig: WalletConfig = {
accounts: [
{
username: 'hankstoever.id',
apps: {
'http://localhost:3000': {
origin: 'http://localhost:3000',
scopes: ['read_write'],
name: 'Tester',
appIcon: 'http://example.com/icon.png',
lastLoginAt: new Date().getTime(),
},
},
},
],
};
export const mockGaiaHubInfo = JSON.stringify({
read_url_prefix: 'https://gaia.blockstack.org/hub/',
challenge_text: '["gaiahub","0","gaia-0","blockstack_storage_please_sign"]',
latest_auth_version: 'v1',
});
export const mockProfileResponse = JSON.stringify([
{
token:
'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJqdGkiOiJmYmY1YjU4OC03NjA1LTQ4YWEtOWZkZi1iMTI2ODhhMGQwNDciLCJpYXQiOiIyMDIwLTA1LTE5VDEyOjQwOjA5LjQ4OVoiLCJleHAiOiIyMDIxLTA1LTE5VDEyOjQwOjA5LjQ4OVoiLCJzdWJqZWN0Ijp7InB1YmxpY0tleSI6IjAzZTkzYWU2NWQ2Njc1MDYxYTE2N2MzNGI4MzIxYmVmODc1OTQ0NjhlOWIyZGQxOWMwNWE2N2E3YjRjYWVmYTAxNyJ9LCJpc3N1ZXIiOnsicHVibGljS2V5IjoiMDNlOTNhZTY1ZDY2NzUwNjFhMTY3YzM0YjgzMjFiZWY4NzU5NDQ2OGU5YjJkZDE5YzA1YTY3YTdiNGNhZWZhMDE3In0sImNsYWltIjp7IkB0eXBlIjoiUGVyc29uIiwiQGNvbnRleHQiOiJodHRwOi8vc2NoZW1hLm9yZyIsImFwcHMiOnsiaHR0cHM6Ly9iYW50ZXIucHViIjoiaHR0cHM6Ly9nYWlhLmJsb2Nrc3RhY2sub3JnL2h1Yi8xRGt1QUNodWZZalRrVENlakpnU3p0dXFwNUtkeWtwV2FwLyIsImh0dHA6Ly8xMjcuMC4wLjE6MzAwMCI6Imh0dHBzOi8vZ2FpYS5ibG9ja3N0YWNrLm9yZy9odWIvMTVoQUxuRUo4ZnZYTmdSeXptVnNwRHlaY0dFeExHSE5TZi8iLCJodHRwczovL2Jsb2Nrc3RhY2suZ2l0aHViLmlvIjoiaHR0cHM6Ly9nYWlhLmJsb2Nrc3RhY2sub3JnL2h1Yi8xRUR1dktmenVOUlVlbnR6MXQ1amZ4VDlmVzFIUDJOSkdXLyJ9LCJhcGkiOnsiZ2FpYUh1YkNvbmZpZyI6eyJ1cmxfcHJlZml4IjoiaHR0cHM6Ly9nYWlhLmJsb2Nrc3RhY2sub3JnL2h1Yi8ifSwiZ2FpYUh1YlVybCI6Imh0dHBzOi8vaHViLmJsb2Nrc3RhY2sub3JnIn0sImFwcHNNZXRhIjp7Imh0dHBzOi8vYmFudGVyLnB1YiI6eyJzdG9yYWdlIjoiaHR0cHM6Ly9nYWlhLmJsb2Nrc3RhY2sub3JnL2h1Yi8xRGt1QUNodWZZalRrVENlakpnU3p0dXFwNUtkeWtwV2FwLyIsInB1YmxpY0tleSI6IjAyMDIxZWZkMGVjM2E0Y2ZkZGQ5ZjA1OTg4NGE4MzRiYjMzMmM4N2ZkYWRhMDUzODA3NzBiZmY1M2Q1ODQ2ZThhYSJ9fX19.GyFt9EZXwr8_jbunvrA38Rv80oBsgjokEPz4SXmZ724I8FCn22g7PK5tWBmpLCZAqVaaYgls9oJcX0vYN-BVsA',
decodedToken: {
header: {
typ: 'JWT',
alg: 'ES256K',
},
payload: {
jti: 'fbf5b588-7605-48aa-9fdf-b12688a0d047',
iat: '2020-05-19T12:40:09.489Z',
exp: '2021-05-19T12:40:09.489Z',
subject: {
publicKey: '03e93ae65d6675061a167c34b8321bef87594468e9b2dd19c05a67a7b4caefa017',
},
issuer: {
publicKey: '03e93ae65d6675061a167c34b8321bef87594468e9b2dd19c05a67a7b4caefa017',
},
claim: {
'@type': 'Person',
'@context': 'http://schema.org',
apps: {
'https://banter.pub':
'https://gaia.blockstack.org/hub/1DkuAChufYjTkTCejJgSztuqp5KdykpWap/',
'http://127.0.0.1:3000':
'https://gaia.blockstack.org/hub/15hALnEJ8fvXNgRyzmVspDyZcGExLGHNSf/',
'https://blockstack.github.io':
'https://gaia.blockstack.org/hub/1EDuvKfzuNRUentz1t5jfxT9fW1HP2NJGW/',
},
api: {
gaiaHubConfig: {
url_prefix: 'https://gaia.blockstack.org/hub/',
},
gaiaHubUrl: 'https://hub.blockstack.org',
},
appsMeta: {
'https://banter.pub': {
storage: 'https://gaia.blockstack.org/hub/1DkuAChufYjTkTCejJgSztuqp5KdykpWap/',
publicKey: '02021efd0ec3a4cfddd9f059884a834bb332c87fdada05380770bff53d5846e8aa',
},
},
},
},
signature:
'GyFt9EZXwr8_jbunvrA38Rv80oBsgjokEPz4SXmZ724I8FCn22g7PK5tWBmpLCZAqVaaYgls9oJcX0vYN-BVsA',
},
},
]);

View File

@@ -0,0 +1,88 @@
import '../setup';
import { decryptPrivateKey } from '@stacks/auth';
import { ecPairToAddress, getPublicKeyFromPrivate, makeECPrivateKey } from '@stacks/encryption';
import { decodeToken } from 'jsontokens';
import { getAppPrivateKey, getGaiaAddress, makeAuthResponse } from '../../src';
import { mockAccount, mockGaiaHubInfo } from '../mocks';
import { ECPair } from 'bitcoinjs-lib';
interface Decoded {
[key: string]: any;
}
const gaiaHubUrl = 'https://hub.blockstack.org';
test('generates the correct app private key', () => {
const expectedKey = '6f8b6a170f8b2ee57df5ead49b0f4c8acde05f9e1c4c6ef8223d6a42fabfa314';
const appPrivateKey = getAppPrivateKey({ account: mockAccount, appDomain: 'https://banter.pub' });
expect(appPrivateKey).toEqual(expectedKey);
});
describe(makeAuthResponse, () => {
test('generates an auth response', async () => {
const account = mockAccount;
const appDomain = 'https://banter.pub';
const transitPrivateKey = makeECPrivateKey();
const transitPublicKey = getPublicKeyFromPrivate(transitPrivateKey);
fetchMock.once(mockGaiaHubInfo).once('', { status: 404 });
const authResponse = await makeAuthResponse({
appDomain,
gaiaHubUrl,
transitPublicKey,
account,
});
const decoded = decodeToken(authResponse);
const { payload } = decoded as Decoded;
expect(payload.profile_url).toEqual(
`https://gaia.blockstack.org/hub/${getGaiaAddress(account)}/profile.json`
);
const appPrivateKey = await decryptPrivateKey(transitPrivateKey, payload.private_key);
const expectedKey = '6f8b6a170f8b2ee57df5ead49b0f4c8acde05f9e1c4c6ef8223d6a42fabfa314';
expect(appPrivateKey).toEqual(expectedKey);
});
test('adds to apps in profile if publish_data scope', async () => {
const account = mockAccount;
const appDomain = 'https://banter.pub';
const transitPrivateKey = makeECPrivateKey();
const transitPublicKey = getPublicKeyFromPrivate(transitPrivateKey);
fetchMock
.once(mockGaiaHubInfo)
.once('', { status: 404 }) // fetch existing profile
.once(JSON.stringify({ publicUrl: 'asdf' })); // Upload profile
const authResponse = await makeAuthResponse({
appDomain,
gaiaHubUrl,
transitPublicKey,
account,
scopes: ['publish_data'],
});
expect(fetchMock.mock.calls.length).toEqual(3);
const decoded = decodeToken(authResponse);
const { payload } = decoded as Decoded;
expect(payload.profile.apps['https://banter.pub']).toEqual(
'https://gaia.blockstack.org/hub/1DkuAChufYjTkTCejJgSztuqp5KdykpWap/'
);
const [uploadUrl, uploadRequest] = fetchMock.mock.calls[2];
if (!uploadRequest) throw 'Expected to upload profile';
expect(uploadUrl).toEqual(
`https://hub.blockstack.org/store/${getGaiaAddress(account)}/profile.json`
);
const profile = JSON.parse(uploadRequest.body as string);
const { apps, appsMeta } = profile[0].decodedToken.payload.claim;
expect(apps[appDomain]).not.toBeFalsy();
const appPrivateKey = await decryptPrivateKey(transitPrivateKey, payload.private_key);
const challengeSigner = ECPair.fromPrivateKey(Buffer.from(appPrivateKey as string, 'hex'));
const expectedDomain = `https://gaia.blockstack.org/hub/${ecPairToAddress(challengeSigner)}/`;
expect(apps[appDomain]).toEqual(expectedDomain);
expect(appsMeta[appDomain]).not.toBeFalsy();
expect(appsMeta[appDomain].storage).toEqual(expectedDomain);
expect(appsMeta[appDomain].publicKey).toEqual(challengeSigner.publicKey.toString('hex'));
});
});

View File

@@ -0,0 +1,147 @@
import '../setup';
import {
encryptWalletConfig,
createWalletGaiaConfig,
fetchWalletConfig,
getOrCreateWalletConfig,
ConfigApp,
updateWalletConfigWithApp,
updateWalletConfig,
WalletConfig,
} from '../../src/models/wallet-config';
import { mockWallet, mockWalletConfig, mockGaiaHubInfo } from '../mocks';
import { decryptContent } from '@stacks/encryption';
const gaiaHubUrl = 'https://gaia.blockstack.org/hub';
describe(fetchWalletConfig, () => {
test('returns no config if not found', async () => {
fetchMock.once(mockGaiaHubInfo).once('', { status: 404 });
const gaiaHubConfig = await createWalletGaiaConfig({
wallet: mockWallet,
gaiaHubUrl,
});
const config = await fetchWalletConfig({ wallet: mockWallet, gaiaHubConfig });
expect(config).toEqual(null);
});
test('returns config if file is present', async () => {
const wallet = mockWallet;
const encrypted = await encryptWalletConfig({ wallet, walletConfig: mockWalletConfig });
fetchMock.once(mockGaiaHubInfo).once(encrypted);
const gaiaHubConfig = await createWalletGaiaConfig({
wallet: mockWallet,
gaiaHubUrl,
});
const config = await fetchWalletConfig({ wallet: mockWallet, gaiaHubConfig });
expect(config).not.toBeFalsy();
if (!config) {
throw 'Must have config present';
}
expect(config.accounts.length).toEqual(1);
const account = config.accounts[0];
expect(account.apps['http://localhost:3000']).toEqual(
mockWalletConfig.accounts[0].apps['http://localhost:3000']
);
});
});
test('creates a config if not found', async () => {
fetchMock
.once(mockGaiaHubInfo)
.once('', { status: 404 })
.once(JSON.stringify({ publicUrl: 'asdf' }));
const wallet = mockWallet;
const gaiaHubConfig = await createWalletGaiaConfig({
wallet: mockWallet,
gaiaHubUrl,
});
const walletConfig = await getOrCreateWalletConfig({ wallet, gaiaHubConfig });
expect(Object.keys(walletConfig.accounts[0].apps).length).toEqual(0);
const [url, uploadResult] = fetchMock.mock.calls[2];
if (!uploadResult) throw 'Expected wallet config to be uploaded';
const { headers } = uploadResult;
if (!headers) throw 'Expected authorization header.';
const authHeader = (headers as Record<string, string>)['Authorization'];
expect(authHeader).toEqual(`bearer ${gaiaHubConfig.token}`);
expect(url).toEqual(
'https://gaia.blockstack.org/hub/store/1AdVqZKxeFLGxrK6TwDcZsdYsCKMMxmm8M/wallet-config.json'
);
const decrypted = await decryptContent(uploadResult.body as string, {
privateKey: wallet.configPrivateKey,
});
expect(JSON.parse(decrypted as string)).toEqual(walletConfig);
});
test('updates existing wallet config with auth info', async () => {
fetchMock
.once(mockGaiaHubInfo)
.once('', { status: 404 })
.once(JSON.stringify({ publicUrl: 'asdf' }))
.once(JSON.stringify({ publicUrl: 'asdf' }));
const wallet = mockWallet;
const [account] = wallet.accounts;
const gaiaHubConfig = await createWalletGaiaConfig({
wallet: mockWallet,
gaiaHubUrl,
});
const walletConfig = await getOrCreateWalletConfig({ wallet, gaiaHubConfig });
const app: ConfigApp = {
origin: 'http://localhost:5000',
scopes: ['read_write'],
lastLoginAt: new Date().getTime(),
name: 'Tester',
appIcon: 'asdf',
};
const newConfig = await updateWalletConfigWithApp({
wallet,
app,
gaiaHubConfig,
account,
walletConfig,
});
expect(fetchMock.mock.calls.length).toEqual(4);
const uploadResult = fetchMock.mock.calls[3][1];
if (!uploadResult) throw 'Expected wallet config to be uploaded';
const decrypted = await decryptContent(uploadResult.body as string, {
privateKey: wallet.configPrivateKey,
});
const config: WalletConfig = JSON.parse(decrypted as string);
expect(config).toEqual(newConfig);
expect(config.accounts[0].apps['http://localhost:5000']).toEqual(app);
expect(Object.keys(config.accounts[0].apps)).toEqual(['http://localhost:5000']);
});
test('can add meta info to wallet config', async () => {
fetchMock
.once(mockGaiaHubInfo)
.once('', { status: 404 })
.once(JSON.stringify({ publicUrl: 'asdf' }))
.once(JSON.stringify({ publicUrl: 'asdf' }));
const wallet = mockWallet;
const gaiaHubConfig = await createWalletGaiaConfig({
wallet: mockWallet,
gaiaHubUrl,
});
const walletConfig = await getOrCreateWalletConfig({ wallet, gaiaHubConfig });
const newConfig = {
...walletConfig,
meta: {
hideWarningForReusingIdentity: true,
},
};
await updateWalletConfig({ wallet, gaiaHubConfig, walletConfig: newConfig });
const uploadResult = fetchMock.mock.calls[3][1];
if (!uploadResult) throw 'Expected wallet config to be uploaded';
const decrypted = await decryptContent(uploadResult.body as string, {
privateKey: wallet.configPrivateKey,
});
const config: WalletConfig = JSON.parse(decrypted as string);
expect(config.meta).toEqual({ hideWarningForReusingIdentity: true });
});

View File

@@ -0,0 +1,12 @@
import { GlobalWithFetchMock } from 'jest-fetch-mock';
import { config as blockstackConfig } from '@stacks/common';
blockstackConfig.logLevel = 'none';
const customGlobal: GlobalWithFetchMock = (global as any) as GlobalWithFetchMock;
customGlobal.fetch = require('jest-fetch-mock');
customGlobal.fetchMock = customGlobal.fetch;
beforeEach(() => {
fetchMock.mockClear();
});

View File

@@ -0,0 +1,14 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"noEmit": true,
"rootDir": "../",
"paths": {
"@stacks/wallet-sdk": ["./src/index"]
}
},
"include": [
"./**/*",
"../src/**/*"
]
}

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es6",
"module": "es2015",
"moduleResolution": "node",
"declaration": true,
"outDir": "./lib",
"strict": true,
"skipLibCheck": true,
"baseUrl": "./src",
"allowSyntheticDefaultImports": true,
"lib": ["es2017", "dom"],
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noImplicitAny": true,
"noImplicitThis": true,
"alwaysStrict": true,
"sourceMap": true,
"paths": {
"@stacks/encryption": ["../../encryption/src"],
"@stacks/transactions": ["../../transactions/src"]
}
},
"include": ["./src/**/*"]
}

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es6",
"module": "es2015",
"moduleResolution": "node",
"declaration": true,
"outDir": "./lib",
"strict": true,
"skipLibCheck": true,
"baseUrl": "./src",
"allowSyntheticDefaultImports": true,
"lib": ["es2017", "dom"],
"sourceMap": true
},
"include": ["./src/**/*"]
}

10987
yarn.lock Normal file

File diff suppressed because it is too large Load Diff