From cba3171ad4e0f3dcb0fd14f2ac4a45fa5c1a693f Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Thu, 3 Oct 2024 02:10:17 -0700 Subject: [PATCH] libname: A reimagining of the nanocurrency-web package. Greater Changes * Completely replace crypto-js dependency with the browser native Web Crypto API. * Overhaul wallet system to align more with object-oriented design and to consolidate related but separated functionality. * Add support for Ledger hardware wallets. * Encrypt wallet secrets by default on initialization, and add lock/unlock feature to encrypt/decrypt on-demand. * Add rolodex feature to enable contact management including signature verification using contact name associated with a public key in your rolodex. * Add online functions to interact with nodes like fetching account info, generating PoW, and sending blocks for processing. * Add sweep feature similar to the offering from Nault which sends all funds in a specified range of accounts in a wallet to a specific address. * Refactor key derivation functions to align closer with spec as designed in the relevant BIPs. * Adopt GPLv3 license and achieve REUSE compliance. Lesser Changes * Refactor as ESM instead of CJS and build for ESMNext target to enable more modern browser features. * Utilize browser native features like bigint instead of bignumber.js dependency. * Deprecate 'box' feature and with it the dependency on byte-base64. * Replace chai/mocha test framework with built-in Node.js test runner, and add tons of tests. * Replace webpack (and dependency ts-loader) with esbuild which offers faster builds and minification. * Add online function to get the next unopened account of a wallet with the idea that a business could distribute addresses to customers in a custodial manner. * Add tons of type checking to validate inputs and outputs. * Deprecate lots of unused code or replace with native features. --- .gitignore | 131 +- .npmignore | 11 - .travis.yml | 8 - AUTHORS.md | 11 + LICENSE | 21 - LICENSES/GPL-3.0-or-later.txt | 232 ++ LICENSES/MIT.txt | 9 + README.md | 466 ++-- index.ts | 398 --- lib/address-generator.ts | 29 - lib/address-importer.ts | 168 -- lib/bip32-key-derivation.ts | 60 - lib/bip39-mnemonic.ts | 185 -- lib/block-signer.ts | 188 -- lib/box.ts | 69 - lib/ed25519.ts | 268 -- lib/nano-address.ts | 150 -- lib/nano-converter.ts | 48 - lib/signer.ts | 48 - lib/util/convert.ts | 148 -- lib/util/curve25519.ts | 1414 ----------- lib/util/util.ts | 30 - package-lock.json | 2767 ++++----------------- package-lock.json.license | 2 + package.json | 84 +- package.json.license | 2 + src/lib/account.ts | 208 ++ src/lib/bip32-key-derivation.ts | 76 + src/lib/bip39-mnemonic.ts | 199 ++ lib/words.ts => src/lib/bip39-wordlist.ts | 14 +- src/lib/block.ts | 333 +++ src/lib/constants.ts | 45 + src/lib/convert.ts | 312 +++ src/lib/curve25519.ts | 695 ++++++ src/lib/ed25519.ts | 255 ++ src/lib/entropy.ts | 120 + src/lib/ledger.ts | 360 +++ src/lib/node.ts | 70 + src/lib/rolodex.ts | 88 + src/lib/safe.ts | 122 + src/lib/tools.ts | 175 ++ src/lib/wallet.ts | 577 +++++ src/main.ts | 11 + test/TEST_VECTORS.js | 151 ++ test/create-wallet.test.mjs | 100 + test/derive-accounts.test.mjs | 91 + test/import-wallet.test.mjs | 175 ++ test/lock-unlock-wallet.mjs | 245 ++ test/manage-rolodex.mjs | 120 + test/refresh-accounts.test.mjs | 126 + test/sign-blocks.test.mjs | 151 ++ test/test.mjs | 415 --- test/tools.test.mjs | 148 ++ tsconfig.json | 27 +- tsconfig.json.license | 2 + webpack.config.js | 24 - 56 files changed, 6051 insertions(+), 6331 deletions(-) delete mode 100644 .npmignore delete mode 100644 .travis.yml create mode 100644 AUTHORS.md delete mode 100644 LICENSE create mode 100644 LICENSES/GPL-3.0-or-later.txt create mode 100644 LICENSES/MIT.txt delete mode 100644 index.ts delete mode 100644 lib/address-generator.ts delete mode 100644 lib/address-importer.ts delete mode 100644 lib/bip32-key-derivation.ts delete mode 100644 lib/bip39-mnemonic.ts delete mode 100644 lib/block-signer.ts delete mode 100644 lib/box.ts delete mode 100644 lib/ed25519.ts delete mode 100644 lib/nano-address.ts delete mode 100644 lib/nano-converter.ts delete mode 100644 lib/signer.ts delete mode 100644 lib/util/convert.ts delete mode 100644 lib/util/curve25519.ts delete mode 100644 lib/util/util.ts create mode 100644 package-lock.json.license create mode 100644 package.json.license create mode 100644 src/lib/account.ts create mode 100644 src/lib/bip32-key-derivation.ts create mode 100644 src/lib/bip39-mnemonic.ts rename lib/words.ts => src/lib/bip39-wordlist.ts (98%) create mode 100644 src/lib/block.ts create mode 100644 src/lib/constants.ts create mode 100644 src/lib/convert.ts create mode 100644 src/lib/curve25519.ts create mode 100644 src/lib/ed25519.ts create mode 100644 src/lib/entropy.ts create mode 100644 src/lib/ledger.ts create mode 100644 src/lib/node.ts create mode 100644 src/lib/rolodex.ts create mode 100644 src/lib/safe.ts create mode 100644 src/lib/tools.ts create mode 100644 src/lib/wallet.ts create mode 100644 src/main.ts create mode 100644 test/TEST_VECTORS.js create mode 100644 test/create-wallet.test.mjs create mode 100644 test/derive-accounts.test.mjs create mode 100644 test/import-wallet.test.mjs create mode 100644 test/lock-unlock-wallet.mjs create mode 100644 test/manage-rolodex.mjs create mode 100644 test/refresh-accounts.test.mjs create mode 100644 test/sign-blocks.test.mjs delete mode 100644 test/test.mjs create mode 100644 test/tools.test.mjs create mode 100644 tsconfig.json.license delete mode 100644 webpack.config.js diff --git a/.gitignore b/.gitignore index e4f46fd..596358f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,63 +1,68 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# TypeScript v1 declaration files -typings/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env - -# next.js build output -.next - -dist +# SPDX-FileCopyrightText: 2024 Chris Duncan +# SPDX-License-Identifier: GPL-3.0-or-later + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# next.js build output +.next + +# esbuild +build/ +dist/ diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 76fec4a..0000000 --- a/.npmignore +++ /dev/null @@ -1,11 +0,0 @@ -.git/ -node_modules/ -test/ -lib/ -.gitignore -.editorconfig -.travis.yml -webpack.config.js -tsconfig.json -package-lock.json -index.ts diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9360c53..0000000 --- a/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language : node_js -node_js : - - stable -install: - - npm install - - npm run build -script: - - npm run test diff --git a/AUTHORS.md b/AUTHORS.md new file mode 100644 index 0000000..2665ade --- /dev/null +++ b/AUTHORS.md @@ -0,0 +1,11 @@ + + + +Miro Metsänheimo +Chris Duncan (zoso.dev) diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 6bd716a..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2022 Miro Metsänheimo - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/LICENSES/GPL-3.0-or-later.txt b/LICENSES/GPL-3.0-or-later.txt new file mode 100644 index 0000000..f6cdd22 --- /dev/null +++ b/LICENSES/GPL-3.0-or-later.txt @@ -0,0 +1,232 @@ +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +Preamble + +The GNU General Public License is a free, copyleft license for software and other kinds of works. + +The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and modification follow. + +TERMS AND CONDITIONS + +0. Definitions. + +“This License” refers to version 3 of the GNU General Public License. + +“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. + +“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations. + +To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work. + +A “covered work” means either the unmodified Program or a work based on the Program. + +To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. + +To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. + +1. Source Code. +The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work. + +A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. + +The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. + +The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. +All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. +No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. + +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. +You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”. + + c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. + +6. Conveying Non-Source Forms. +You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: + + a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. + + d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. + +A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. + +“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). + +The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. + +7. Additional Terms. +“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or authors of the material; or + + e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. + +8. Termination. +You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. + +9. Acceptance Not Required for Having Copies. +You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. +Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. + +An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. + +11. Patents. +A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”. + +A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. + +In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. + +A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. +If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. + +13. Use with the GNU Affero General Public License. +Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. + +14. Revised Versions of this License. +The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. + +Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. +If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”. + +You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . + +The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt new file mode 100644 index 0000000..02c1e2e --- /dev/null +++ b/LICENSES/MIT.txt @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2022 Miro Metsänheimo + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index ede22d8..887278b 100644 --- a/README.md +++ b/README.md @@ -1,302 +1,282 @@ -# nanocurrency-web - -[![Build Status](https://travis-ci.org/numsu/nanocurrency-web-js.svg?branch=master)](https://travis-ci.org/numsu/nanocurrency-web-js) -[![npm version](https://badge.fury.io/js/nanocurrency-web.svg)](https://badge.fury.io/js/nanocurrency-web) -[![GitHub license](https://img.shields.io/github/license/numsu/nanocurrency-web-js)](https://github.com/numsu/nanocurrency-web-js/blob/master/LICENSE) - -Toolkit for Nano cryptocurrency client side offline implementations allowing you to build web- and mobile applications using Nano without ever compromising the user's keys by sending them out of their own device. - -The toolkit supports creating and importing wallets and signing blocks on-device. Meaning that the user's keys should never be required to leave the device. And much more! + + +# libnemo +libnemo is a fork of the nanocurrency-web toolkit. It is used for client-side +implementations of Nano cryptocurrency wallets and enables building web-based +applications that can work even while offline. libnemo supports managing +wallets, deriving accounts, signing blocks, and more. + +It utilizes the Web Crypto API which is native to all modern browsers; as such, +it has only a required dependency in order to work with the BLAKE2b algorithm. +Optionally, Ledger device dependencies can be installed to enable Ledger +hardware wallet support. ## Features - -* Generate new HD wallets (BIP32/44 hierarchial deterministic) with a BIP39 mnemonic phrase (Used in Ledger hardware wallet) -* Generate new "legacy" Nano wallets with mnemonic phrases (Used in the Natrium wallet) -* Import HD wallets with a mnemonic phrase or a seed -* Import "legacy" wallets with the Nano mnemonic phrase or seed -* Sign send-, receive- and change (representative) blocks with a private key -* Convert Nano units -* Verify the signature of a block -* Sign any strings with the private key -* Verify the signature of any string with the public key -* Validate addresses and mnemonic words -* Runs in all web browsers and mobile frameworks built with Javascript (doesn't require server-side NodeJS functions) - ---- +* Generate new BIP-32 hierarchial deterministic (HD) wallets with a BIP-39 +mnemonic phrase and the Nano path registered with BIP-44. Used by Ledger +hardware wallet. +* Generate new BLAKE2b wallets with a BIP-39 mnemonic phrases. Original method +described by nano spec. +* Import wallets with a mnemonic phrase or a seed. +* Derive indexed accounts with a Nano address and a public-private keypair. +* Create, sign, and verify send, receive, and change blocks. +* Get account info and process blocks on the network while online. +* Manage known addresses with a rolodex. +* Sign and verify arbitrary strings with relevant keys. +* Validate entropy, seeds, mnemonic phrases, and Nano addresses. +* Convert Nano unit denominations. +* Run in modern web browsers and mobile frameworks built with Javascript without +server-side NodeJS functions. ## Installation ### From NPM ```console -npm install nanocurrency-web -``` -### In web - -```html - - +npm install libnemo ``` ## Usage - -| WARNING: do not use any of the keys or addresses listed below to send real assets! | -| --- | +#### ⚠️ The examples below should never be used for real transactions! ⚠️ ### Wallets and accounts -The wallet is a hexadecimal string called a seed. From this seed you can deterministically derive millions of unique accounts. The first account in a wallet starts at index 0. - -The library is able to generate, import and derive accounts for HD wallets and "legacy" Nano wallets. A HD wallet seed length is 128 hexadecimal characters while a "legacy" Nano wallet seed is 64 characters long. - -These are the two most common used wallets in different applications. A best bet would be to support both of them. For example, when an user wants to import a wallet, you could always generate both wallets and check if either wallet's account at index 0 has a frontier using [the accounts_frontiers RPC](https://docs.nano.org/commands/rpc-protocol/#accounts_frontiers) command. +At its core, a wallet is a hexadecimal string called a seed. From this seed, +millions of unique accounts can be deterministically derived. The first account +in a wallet starts at index 0. + +For clarity, the following terms are used throughout the library: + * BIP-32 - Defines how hierarchical determinstic (HD) wallets are generated + * BIP-39 - Defines how mnemonic phrases are generated + * BIP-44 - Expands on BIP-32 to define how an enhanced derivation path can + allow a single wallet to store multiple currencies + +libnemo is able to generate and import HD and BLAKE2b wallets, and it can derive +accounts for both. An HD wallet seed is 128 characters while a BLAKE2b wallet +seed is 64 characters. For enhanced security, libnemo requires a password to +create or import wallets, and wallets are initialized in a locked state. More +advanced implementations can provide their own CryptoKey instead of a password. +Refer to the documentation on each class factory method for specific usage. ```javascript -import { wallet } from 'nanocurrency-web' +import { Bip44Wallet, Blake2bWallet } from 'libnemo' -// Generates a new wallet with a mnemonic phrase, seed and an account -// You can also generate your own entropy for the mnemonic or set a seed password -// Notice, that losing the password will make the mnemonic phrase void -const wallet = wallet.generate(entropy?, password?) +const wallet = await Bip44Wallet.create(password) +const wallet = await Bip44Wallet.fromEntropy(password, entropy, salt?) +const wallet = await Bip44Wallet.fromMnemonic(password, mnemonic, salt?) +const wallet = await Bip44Wallet.fromSeed(password, seed) -// Generates a legacy wallet with a mnemonic phrase, seed and an account -// You can provide your own seed to be used instead -const wallet = wallet.generateLegacy(seed?) - -// Import a wallet with the mnemonic phrase -const wallet = wallet.fromMnemonic(mnemonic, seedPassword?) - -// Import a wallet with the legacy mnemonic phrase -const wallet = wallet.fromLegacyMnemonic(mnemonic) - -// Import a wallet with a seed, the mnemonic phrase will be undefined since it's not possible to infer it from the seed -const wallet = wallet.fromSeed(seed) - -// Import a wallet with a legacy seed -const wallet = wallet.fromLegacySeed(seed) - -// Derive private keys for a seed, from and to are number indexes. The first account index is 0. -const accounts = wallet.accounts(seed, from, to) - -// Derive private keys for a legacy seed, from and to are number indexes. The first account index is 0. -const accounts = wallet.legacyAccounts(seed, from, to) +const wallet = await Bip44Wallet.create(password) +const wallet = await Bip44Wallet.fromSeed(password, seed) +const wallet = await Bip44Wallet.fromMnemonic(password, mnemonic) ``` ```javascript -// The returned wallet JSON format is as follows. The mnemonic phrase will be undefined when importing with a seed, unless it's imported with a legacy seed -{ - mnemonic: 'edge defense waste choose enrich upon flee junk siren film clown finish luggage leader kid quick brick print evidence swap drill paddle truly occur', - seed: '0dc285fde768f7ff29b66ce7252d56ed92fe003b605907f7a4f683c3dc8586d34a914d3c71fc099bb38ee4a59e5b081a3497b7a323e90cc68f67b5837690310c', - accounts: [ - { - accountIndex: 0, - privateKey: '3be4fc2ef3f3b7374e6fc4fb6e7bb153f8a2998b3b3dab50853eabe128024143', - publicKey: '5b65b0e8173ee0802c2c3e6c9080d1a16b06de1176c938a924f58670904e82c4', - address: 'nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d' - } - ] +try { + const unlockResult = await wallet.unlock(password) +} catch(err) { + console.log(err) } -``` - -### Blocks -There are three different types of blocks; send, receive and change. While all of these are called "state" blocks, they are interpreted differently based on the data they contain. +console.log(unlockResult) // true if successfully unlocked -A send block means that the amount of Nano decreases in the account while a receive block means that the amount increases. If the amount stays the same, it's interpreted as a change (representative) block. You are able to change the representative also at the same time when sending or receiving. All blocks are signed with the account's private key. +const { mnemonic, seed } = wallet -The functions are designed to have user friendly usage, but they will return the block exactly the way as the network accepts them. All that is left is to publish the block to the network with the [process RPC command](https://docs.nano.org/commands/rpc-protocol/#process). +const accounts = await wallet.accounts(from?, to?) +const firstAccount = accounts[0] +const { address, publicKey, privateKey, index } = firstAccount -Always fetch the most up to date information for the account from the network using the [account_info RPC command](https://docs.nano.org/commands/rpc-protocol/#account_info). - -If the account hasn't been opened yet (this is the first block), you will need to use the "genesis" as frontier: `0000000000000000000000000000000000000000000000000000000000000000`. - -#### Signing a send block +const nodeUrl = 'https://nano-node.example.com/' +await firstAccount.refresh(nodeUrl) // online +const { frontier, balance, representative } = firstAccount +``` +### Blocks +Blocks do not contain transaction amounts. Instead, they contain stateful +balance changes only. For example, if sending Ӿ5 from an account with a balance +of Ӿ20, the send block would contain `balance: Ӿ15` (psuedocode for +demonstration purposes and not a literal depiction). This can be difficult to +track, so libnemo provides the convenience of specifying an amount to send or +receive and calculates the balance change itself. + +All blocks are 'state' types, but they are interpreted as one of three different +subtypes based on the data they contain: send, receive, or change +representative. libnemo implements them as the following classes: + +* SendBlock: the Nano balance of the account decreases +* ReceivBlock: the Nano balance of the account increases and requires a matching +SendBlock +* ChangeBlock: the representative for the account changes while the Nano balance +does not + +_Nano protocol allows changing the representative at the same time as a balance +change. libnemo does not implement this for purposes of clarity; all +ChangeBlock objects will maintain the same Nano balance._ + +Always fetch the most up to date information for the account from the network +using the +[account_info RPC command](https://docs.nano.org/commands/rpc-protocol/#account_info) +which can then be used to populate the block parameters. + +Blocks require a small proof-of-work that must be calculated for the block to be +accepted by the network. This can be provided when creating the block, or a +public node that allows the +[work_generate RPC command](https://docs.nano.org/commands/rpc-protocol/#work_generate) +can be used. + +Finally, the block must be signed with the private key of the account. libnemo +enables this to be done offline if desired. After being signed, the block can +be published to the network with the +[process RPC command](https://docs.nano.org/commands/rpc-protocol/#process). + +#### Creating blocks ```javascript -import { block } from 'nanocurrency-web' - -const privateKey = '781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3' -const data = { - // Current balance from account info - walletBalanceRaw: '5618869000000000000000000000000', - - // Your wallet address - fromAddress: 'nano_1e5aqegc1jb7qe964u4adzmcezyo6o146zb8hm6dft8tkp79za3sxwjym5rx', - - // The address to send to - toAddress: 'nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p', - - // From account info - representativeAddress: 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou', - - // Previous block, from account info - frontier: '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', - - // The amount to send in RAW - amountRaw: '2000000000000000000000000000000', - - // Generate work on server-side or with a DPOW service - // This is optional, you don't have to generate work before signing the transaction - work: 'fbffed7c73b61367', -} - -// Returns a correctly formatted and signed block ready to be sent to the blockchain -const signedBlock = block.send(data, privateKey) +import { SendBlock, ReceiveBlock, ChangeBlock } from 'libnemo' + +const send = new SendBlock( + 'nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d', // sender + '5618869000000000000000000000000', // current balance + 'nano_3phqgrqbso99xojkb1bijmfryo7dy1k38ep1o3k3yrhb7rqu1h1k47yu78gz', // recipient + '2000000000000000000000000000000', // amount to send + 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou', // representative + '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', // hash of previous block + 'fbffed7c73b61367' // PoW nonce (optional at first but required to process) +) + +const receive = new ReceiveBlock( + 'nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d', // recipient + '18618869000000000000000000000000', // current balance + 'CBC911F57B6827649423C92C88C0C56637A4274FF019E77E24D61D12B5338783', // origin (hash of matching send block) + '7000000000000000000000000000000', // amount that was sent + 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou', // representative + '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', // hash of previous block + 'c5cf86de24b24419' // PoW nonce (optional at first but required to process) +) + +const change = new ChangeBlock( + 'nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d', // account redelegating vote weight + '3000000000000000000000000000000', // current balance + 'nano_1anrzcuwe64rwxzcco8dkhpyxpi8kd7zsjc1oeimpc3ppca4mrjtwnqposrs', // new representative + '128106287002E595F479ACD615C818117FCB3860EC112670557A2467386249D4', // hash of previous block + '0000000000000000' // PoW nonce (optional at first but required to process) +) ``` -#### Signing a receive block +#### Signing a block ```javascript -import { block } from 'nanocurrency-web' - -const privateKey = '781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3' -const data = { - // Your current balance in RAW from account info - walletBalanceRaw: '18618869000000000000000000000000', - - // Your address - toAddress: 'nano_3kyb49tqpt39ekc49kbej51ecsjqnimnzw1swxz4boix4ctm93w517umuiw8', - - // From account info - representativeAddress: 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou', - - // From account info - frontier: '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', - - // From the pending transaction - transactionHash: 'CBC911F57B6827649423C92C88C0C56637A4274FF019E77E24D61D12B5338783', - - // From the pending transaction in RAW - amountRaw: '7000000000000000000000000000000', - - // Generate the work server-side or with a DPOW service - // This is optional, you don't have to generate work before signing the transaction - work: 'c5cf86de24b24419', +const privateKey = '3BE4FC2EF3F3B7374E6FC4FB6E7BB153F8A2998B3B3DAB50853EABE128024143' +try { + await block.sign(privateKey) +} catch (err) { + console.log(err) } - -// Returns a correctly formatted and signed block ready to be sent to the blockchain -const signedBlock = block.receive(data, privateKey) ``` -#### Signing a change (representative) block +#### Caculating proof-of-work from an online service ```javascript -import { block } from 'nanocurrency-web' - -const privateKey = '781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3' -const data = { - // Your current balance, from account info - walletBalanceRaw: '3000000000000000000000000000000', - - // Your wallet address - address: 'nano_3igf8hd4sjshoibbbkeitmgkp1o6ug4xads43j6e4gqkj5xk5o83j8ja9php', - - // The new representative - representativeAddress: 'nano_1anrzcuwe64rwxzcco8dkhpyxpi8kd7zsjc1oeimpc3ppca4mrjtwnqposrs', - - // Previous block, from account info - frontier: '128106287002E595F479ACD615C818117FCB3860EC112670557A2467386249D4', - - // Generate work on the server side or with a DPOW service - // This is optional, you don't have to generate work before signing the transaction - work: '0000000000000000', +const node = new Node('https://nano-node.example.com/') +try { + await block.pow('https://nano-node.example.com/') +} catch (err) { + console.log(err) } - -// Returns a correctly formatted and signed block ready to be sent to the blockchain -const signedBlock = block.representative(data, privateKey) ``` -### Tools - -#### Converting Nano units - -Supported unit values are RAW, NANO, MRAI, KRAI, RAW. +#### Processing a block on the network ```javascript -import { tools } from 'nanocurrency-web' - -// Convert 1 Nano to RAW -const converted = tools.convert('1', 'NANO', 'RAW') - -// Convert 1 RAW to Nano -const converted = tools.convert('1000000000000000000000000000000', 'RAW', 'NANO') +const node = new Node('https://nano-node.example.com', 'nodes-api-key') +try { + const hash = await block.process('https://nano-node.example.com/') +} catch (err) { + console.log(err) +} ``` -#### Verifying signatures and signing anything with the private key -Cryptocurrencies rely on asymmetric cryptographgy. This means that you can use the public key to validate the signature of the block that is signed with the private key. - -For example implementing client side login with the password being the user's e-mail signed with their private key: +### Tools +#### Converting Nano denominations +Raw values are the native unit of exchange throughout libnemo and are +represented by the primitive bigint data type. Other supported denominations +are as follows: +| Unit | Raw | +|-------|-----| +| RAI | 1024 raw | +| NYANO | 1024 raw | +| KRAI | 1027 raw | +| PICO | 1027 raw | +| MRAI | 1030 raw | +| NANO | 1030 raw | +| KNANO | 1033 raw | +| MNANO | 1036 raw | ```javascript -import { tools } from 'nanocurrency-web' - -const privateKey = '781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3' -const signed = tools.sign(privateKey, 'foo@bar.com') - -// On the backend, verify that the signed value matches the hashed signature in your database +import { Tools } from 'libnemo' +// Denominations are case-insensitive +const oneNanoToRaw = Tools.convert('1', 'NANO', 'RAW') // 1000000000000000000000000000000 +const oneNonillionRawToNano = Tools.convert('1000000000000000000000000000000', 'RAW', 'NANO') // 1 +const oneThousandNyanoToPico = Tools.convert('1000', 'nYaNo', 'pico') //1 +const oneThousandPicoToNano = Tools.convert('1000', 'pico', 'NANO') // 1 ``` -You can also validate Nano blocks: -```javascript -import { tools } from 'nanocurrency-web' +#### Verifying signatures and signing anything with the private key +Since cryptocurrencies like Nano uses asymmetric keys to sign and verify blocks +and transactions, a Nano account itself can be used to sign arbitrary data +with its private key and verify signatures from other accounts with their public +keys. -const valid = tools.verifyBlock(publicKey, block) -``` +For example, a client-side login can be implemented by challenging an account +owner to sign their email address using their private key: -You are able to challenge an user to prove ownership of a Nano address simply by making the user sign any string with the private key and then validating the signature with the public key. You are even able to derive the public key from the Nano address. ```javascript -import { tools } from 'nanocurrency-web' - -const nanoAddress = 'nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d' -const privateKey = '3be4fc2ef3f3b7374e6fc4fb6e7bb153f8a2998b3b3dab50853eabe128024143' -const data = 'sign this' +import { Tools } from 'libnemo' -// Make the user sign the data -const signature = tools.sign(privateKey, data) - -// Infer the user's public key from the address (if not already known) -const publicKey = tools.addressToPublicKey(nanoAddress) - -// Verify the signature using the public key, the signature and the original data -const validSignature = tools.verify(publicKey, signature, data) +const privateKey = '3BE4FC2EF3F3B7374E6FC4FB6E7BB153F8A2998B3B3DAB50853EABE128024143' +const publicKey = '5B65B0E8173EE0802C2C3E6C9080D1A16B06DE1176C938A924F58670904E82C4' +const signature = await Tools.sign(privateKey, 'johndoe@example.com') +const isValid = await Tools.verify(publicKey, signature, 'johndoe@example.com') ``` -#### Validating values -You are able to validate Nano addresses and mnemonic words. - +Ownership of a Nano address can also be proven by challenging the account owner +to sign an arbitrary string and then validating the signature with the Nano +account address. ```javascript -import { tools } from 'nanocurrency-web' +import { Tools } from 'libnemo' -// Validate Nano address -const valid = tools.validateAddress('nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d') +const address = 'nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d' +const privateKey = '3BE4FC2EF3F3B7374E6FC4FB6E7BB153F8A2998B3B3DAB50853EABE128024143' +const randomData = new Entropy().hex -// Validate mnemonic phrases -const valid = tools.validateMnemonic('edge defense waste choose enrich upon flee junk siren film clown finish luggage leader kid quick brick print evidence swap drill paddle truly occur') +const signature = await Tools.sign(privateKey, randomData) +const publicKey = new Account(address).publicKey +const isValid = await Tools.verify(publicKey, signature, randomData) ``` -#### Encrypting and decrypting strings -You are able to encrypt and decrypt strings to implement end-to-end encryption using the Diffie-Hellman key exchange by using the Nano address and private key. The public and private keys are converted to Curve25519 keys which are suitable for encryption within the library. - +#### Validate a Nano account address ```javascript -import { box } from 'nanocurrency-web' +import { Tools } from 'libnemo' -// Encrypt on device 1 -const encrypted = box.encrypt(message, recipientAddress, senderPrivateKey) - -// Send the encrypted message to the recipient and decrypt on device 2 -const decrypted = box.decrypt(encrypted, senderAddress, recipientPrivateKey) +const valid = Account.validate('nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d') ``` -## Contributions - -You are welcome to contribute to the module. To develop, use the following commands. - -* `npm install` to install dependencies -* `npm run build` to build and pack the code -* `npm run test` to run tests - -Fork the project, make your changes and request them to be merged with a pull request. Issues are also welcome. If you have any questions, you can find me lurking around the Nano discord server. +## Tests +Test vectors were retrieved from the following publicly-available locations: + * Nano (BIP-44): https://docs.nano.org/integration-guides/key-management/#test-vectors + * Trezor (BIP-39): https://github.com/trezor/python-mnemonic/blob/master/vectors.json + * BIP-32: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#user-content-Test_Vectors +Another set of test vectors were created for libnemo based on the Trezor set. +These extra test vectors were generated purely to test uncommon yet valid +mnemonic phrase lengths like 15 or 18 words. +#### ⚠️ The test vectors should never be used for real transactions! ⚠️ + +## Building +* `npm run build`: compile and build +* `npm run test`: all of the above, run tests, and print results to the console +* `npm run test:coverage`: all of the above, calculate code coverage, and print +code coverage to the console +* `npm run test:coverage:report`: all of the above, and open an HTML code +coverage report in the browser (requires lcov and xdg-open) ## Donations - -If this helped you in your endeavours and you feel like supporting the developer, please donate some Nano: - -`nano_1iic4ggaxy3eyg89xmswhj1r5j9uj66beka8qjcte11bs6uc3wdwr7i9hepm` - -If you prefer the old fashioned way, I also have a [GitHub Sponsors account](https://github.com/sponsors/numsu). +If you find this library helpful, please consider tipping the developer. +``` +nano_1zosoqs47yt47bnfg7sdf46kj7asn58b7uzm9ek95jw7ccatq37898u1zoso +``` diff --git a/index.ts b/index.ts deleted file mode 100644 index 8217f9e..0000000 --- a/index.ts +++ /dev/null @@ -1,398 +0,0 @@ -import BigNumber from 'bignumber.js' - -import AddressGenerator from './lib/address-generator' -import AddressImporter, { Account, Wallet } from './lib/address-importer' -import BlockSigner, { BlockData, ReceiveBlock, RepresentativeBlock, SendBlock, SignedBlock } from './lib/block-signer' -import Box from './lib/box' -import NanoAddress from './lib/nano-address' -import NanoConverter from './lib/nano-converter' -import Signer from './lib/signer' -import Convert from './lib/util/convert' - -const wallet = { - - /** - * Generate a new Nano cryptocurrency wallet - * - * This function generates a wallet from random entropy. Wallet includes - * a BIP39 mnemonic phrase in line with the Nano Ledger implementation and - * a seed, the account is derived using BIP32 deterministic hierarchial algorithm - * with input parameters 44'/165' and index 0. - * - * The Nano address is derived from the public key using standard Nano encoding. - * The address is prefixed with 'nano_'. - * - * Generation uses CryptoJS to generate random entropy by default. You can give your own entropy - * as a parameter and it will be used instead. - * - * An optional seed password can be used to encrypt the mnemonic phrase so the seed - * cannot be derived correctly without the password. Recovering the wallet without the - * password is not possible. - * - * @param {string} [entropy] - (Optional) 64 byte hexadecimal string entropy to be used instead of generating it - * @param {string} [seedPassword] - (Optional) seed password - * @returns {Wallet} The wallet - */ - generate: (entropy?: string, seedPassword?: string): Wallet => { - return AddressGenerator.generateWallet(entropy, seedPassword) - }, - - /** - * Generate a new Nano cryptocurrency wallet - * - * This function generates a legacy wallet from a random seed. Wallet includes - * a mnemonic phrase and a seed, the account is derived from the seed at index 0. - * - * The Nano address is derived from the public key using standard Nano encoding. - * The address is prefixed with 'nano_'. - * - * Generation uses CryptoJS to generate random seed by default. You can give your own seed - * as a parameter and it will be used instead. - * - * @param {string} [seed] - (Optional) 64 byte hexadecimal string seed to be used instead of generating it - * @returns {Wallet} The wallet - */ - generateLegacy: (seed?: string): Wallet => { - return AddressGenerator.generateLegacyWallet(seed) - }, - - /** - * Import a Nano cryptocurrency wallet from a mnemonic phrase - * - * This function imports a wallet from a mnemonic phrase. Wallet includes the mnemonic phrase, - * a seed derived with BIP39 standard and an account derived using BIP32 deterministic hierarchial - * algorithm with input parameters 44'/165' and index 0. - * - * The Nano address is derived from the public key using standard Nano encoding. - * The address is prefixed with 'nano_'. - * - * @param {string} mnemonic - The mnemonic phrase. Words are separated with a space - * @param {string} [seedPassword] - (Optional) seed password - * @throws Throws an error if the mnemonic phrase doesn't pass validations - * @returns {Wallet} The wallet - */ - fromMnemonic: (mnemonic: string, seedPassword?: string): Wallet => { - return AddressImporter.fromMnemonic(mnemonic, seedPassword) - }, - - /** - * Import a Nano cryptocurrency wallet from a legacy mnemonic phrase - * - * This function imports a wallet from an old Nano mnemonic phrase. Wallet includes the mnemonic - * phrase, a seed which represents the mnemonic and an account derived from the seed at index 0. - * - * The Nano address is derived from the public key using standard Nano encoding. - * The address is prefixed with 'nano_'. - * - * @param {string} mnemonic - The mnemonic phrase. Words are separated with a space - * @throws Throws an error if the mnemonic phrase doesn't pass validations - * @returns {Wallet} The wallet - */ - fromLegacyMnemonic: (mnemonic: string): Wallet => { - return AddressImporter.fromLegacyMnemonic(mnemonic) - }, - - /** - * Import a Nano cryptocurrency wallet from a seed - * - * This function imports a wallet from a seed. Wallet includes the seed and an account derived using - * BIP39 standard and an account derived using BIP32 deterministic hierarchial algorithm with input - * parameters 44'/165' and index 0. - * - * The Nano address is derived from the public key using standard Nano encoding. - * The address is prefixed with 'nano_'. - * - * @param {string} seed - The seed - * @returns {Wallet} The wallet, without the mnemonic phrase because it's not possible to infer backwards - */ - fromSeed: (seed: string): Wallet => { - return AddressImporter.fromSeed(seed) - }, - - /** - * Import Nano cryptocurrency accounts from a legacy hex seed - * - * This function imports a wallet from a seed. The private key is derived from the seed using - * simply a blake2b hash function. The public key is derived from the private key using the ed25519 curve - * algorithm. - * - * The Nano address is derived from the public key using standard Nano encoding. - * The address is prefixed with 'nano_'. - * - * @param {string} seed - The seed - * @returns {Wallet} The wallet - */ - fromLegacySeed: (seed: string): Wallet => { - return AddressImporter.fromLegacySeed(seed) - }, - - /** - * Derive accounts for the seed - * - * This function derives Nano accounts with the BIP32 deterministic hierarchial algorithm - * from the given seed with input parameters 44'/165' and indexes based on the from and to - * parameters. - * - * @param {string} seed - The seed - * @param {number} from - The start index - * @param {number} to - The end index - * @returns {Account[]} a list of accounts - */ - accounts: (seed: string, from: number, to: number): Account[] => { - return AddressImporter.fromSeed(seed, from, to).accounts - }, - - /** - * Derive accounts for the legacy hex seed - * - * This function derives Nano accounts with the given seed with indexes - * based on the from and to parameters. - * - * @param {string} seed - The seed - * @param {number} from - The start index - * @param {number} to - The end index - * @returns {Account[]} a list of accounts - */ - legacyAccounts: (seed: string, from: number, to: number): Account[] => { - return AddressImporter.fromLegacySeed(seed, from, to).accounts - }, - -} - -const block = { - - /** - * Sign a send block with the input parameters - * - * For a send block, put your own address to the 'fromAddress' property and - * the recipient address to the 'toAddress' property. - * All the NANO amounts should be input in RAW format. The addresses should be - * valid Nano addresses. Fetch the current balance, frontier (previous block) and - * representative address from the network. - * - * The return value of this function is ready to be published to the network. - * - * NOTICE: Always fetch up-to-date account info from the network - * before signing the block. - * - * @param {SendBlock} data The data for the block - * @param {string} privateKey Private key to sign the block - * @returns {SignedBlock} the signed block - */ - send: (data: SendBlock, privateKey: string): SignedBlock => { - return BlockSigner.send(data, privateKey) - }, - - - /** - * Sign a receive block with the input parameters - * - * For a receive block, put your own address to the 'toAddress' property. - * All the NANO amounts should be input in RAW format. The addresses should be - * valid Nano addresses. Fetch the current balance, frontier (previous block) and - * representative address from the network. - * Input the receive amount and transaction hash from the pending block. - * - * The return value of this function is ready to be published to the network. - * - * NOTICE: Always fetch up-to-date account info from the network - * before signing the block. - * - * @param {ReceiveBlock} data The data for the block - * @param {string} privateKey Private key to sign the block - * @returns {SignedBlock} the signed block - */ - receive: (data: ReceiveBlock, privateKey: string): SignedBlock => { - return BlockSigner.receive(data, privateKey) - }, - - - /** - * Sign a representative change block with the input parameters - * - * For a change block, put your own address to the 'address' property. - * All the NANO amounts should be input in RAW format. The addresses should be - * valid Nano addresses. Fetch the current balance, previous block from the - * network. Set the new representative address - * as the representative. - * - * NOTICE: Always fetch up-to-date account info from the network - * before signing the block. - * - * @param {RepresentativeBlock} data The data for the block - * @param {string} privateKey Private key to sign the block - * @returns {SignedBlock} the signed block - */ - representative: (data: RepresentativeBlock, privateKey: string): SignedBlock => { - const block: SendBlock = { - ...data, - fromAddress: data.address, - amountRaw: '0', - toAddress: 'nano_1111111111111111111111111111111111111111111111111111hifc8npp', // Burn address - } - - return BlockSigner.send(block, privateKey) - }, - -} - -const tools = { - - /** - * Convert Nano values - * - * Possible units are RAW, NANO, MRAI, KRAI, RAI - * - * @param {string | BigNumber} input The input value - * @param {string} inputUnit The unit of the input value - * @param {string} outputUnit The unit you wish to convert to - * @returns {string} The converted value - */ - convert: (input: string | BigNumber, inputUnit: string, outputUnit: string): string => { - return NanoConverter.convert(input, inputUnit, outputUnit) - }, - - /** - * Sign any strings with the user's private key - * - * @param {string} privateKey The private key to sign with - * @param {...string} input Data to sign - * @returns {string} The signature - */ - sign: (privateKey: string, ...input: string[]): string => { - const data = input.map(Convert.stringToHex) - return Signer.sign(privateKey, ...data) - }, - - /** - * Verifies the signature of any input string - * - * @param {string} publicKey The public key to verify with - * @param {string} signature The signature to verify - * @param {...string} input Data to verify - * @returns {boolean} valid or not - */ - verify: (publicKey: string, signature: string, ...input: string[]): boolean => { - const data = input.map(Convert.stringToHex) - return Signer.verify(publicKey, signature, ...data) - }, - - /** - * Verifies the signature of any input string - * - * @param {string} publicKey The public key to verify with - * @param {BlockData} block The block to verify - * @returns {boolean} valid or not - */ - verifyBlock: (publicKey: string, block: BlockData): boolean => { - const preamble = 0x6.toString().padStart(64, '0') - return Signer.verify(publicKey, block.signature, - preamble, - NanoAddress.nanoAddressToHexString(block.account), - block.previous, - NanoAddress.nanoAddressToHexString(block.representative), - Convert.dec2hex(block.balance, 16).toUpperCase(), - block.link) - }, - - /** - * Validate a Nano address - * - * @param {string} input The address to validate - * @returns {boolean} valid or not - */ - validateAddress: (input: string): boolean => { - return NanoAddress.validateNanoAddress(input) - }, - - /** - * Validate mnemonic words - * - * @param {string} input The address to validate - * @returns {boolean} valid or not - */ - validateMnemonic: (input: string): boolean => { - return AddressImporter.validateMnemonic(input) - }, - - /** - * Convert a Nano address to a public key - * - * @param {string} input Nano address to convert - * @returns {string} the public key - */ - addressToPublicKey: (input: string): string => { - return NanoAddress.addressToPublicKey(input) - }, - - /** - * Convert a public key to a Nano address - * - * @param {string} input Public key to convert - * @returns {string} the nano address - */ - publicKeyToAddress: (input: string): string => { - return NanoAddress.deriveAddress(input) - }, - - /** - * Hash a string or array of strings with blake2b - * - * @param {string | string[]} input string to hash - * @returns {string} hashed string - */ - blake2b: (input: string | string[]): string => { - if (Array.isArray(input)) { - return Convert.ab2hex(Signer.generateHash(input.map(Convert.stringToHex))) - } else { - return Convert.ab2hex(Signer.generateHash([Convert.stringToHex(input)])) - } - }, - -} - -const box = { - - /** - * Encrypt a message using a Nano address private key for - * end-to-end encrypted messaging. - * - * Encrypts the message using the recipient's public key, - * the sender's private key and a random nonce generated - * inside the library. The message can be opened with the - * recipient's private key and the sender's public key by - * using the decrypt method. - * - * @param {string} message string to encrypt - * @param {string} address nano address of the recipient - * @param {string} privateKey private key of the sender - * @returns {string} encrypted message encoded in Base64 - */ - encrypt: (message: string, address: string, privateKey: string): string => { - return Box.encrypt(message, address, privateKey) - }, - - /** - * Decrypt a message using a Nano address private key. - * - * Decrypts the message by using the sender's public key, - * the recipient's private key and the nonce which is included - * in the encrypted message. - * - * @param {string} encrypted string to decrypt - * @param {string} address nano address of the sender - * @param {string} privateKey private key of the recipient - * @returns {string} decrypted message encoded in UTF-8 - */ - decrypt: (encrypted: string, address: string, privateKey: string): string => { - return Box.decrypt(encrypted, address, privateKey) - } - -} - -export { - wallet, - block, - tools, - box, -} diff --git a/lib/address-generator.ts b/lib/address-generator.ts deleted file mode 100644 index 3977205..0000000 --- a/lib/address-generator.ts +++ /dev/null @@ -1,29 +0,0 @@ -import AddressImporter, { Wallet } from './address-importer' -import Bip39Mnemonic from './bip39-mnemonic' - -export default class AddressGenerator { - - /** - * Generates a hierarchial deterministic BIP32/39/44 wallet - * - * @param {string} [entropy] - (Optional) Custom entropy if the caller doesn't want a default generated entropy - * @param {string} [seedPassword] - (Optional) Password for the seed - */ - static generateWallet = (entropy = '', seedPassword: string = ''): Wallet => { - const mnemonicSeed = Bip39Mnemonic.createWallet(entropy, seedPassword) - const wallet = AddressImporter.fromSeed(mnemonicSeed.seed, 0, 0) - return { - ...wallet, - mnemonic: mnemonicSeed.mnemonic, - } - } - - /** - * Generates a legacy Nano wallet - */ - static generateLegacyWallet = (seed?: string): Wallet => { - const mnemonicSeed = Bip39Mnemonic.createLegacyWallet(seed) - return AddressImporter.fromLegacySeed(mnemonicSeed.seed, 0, 0, mnemonicSeed.mnemonic) - } - -} diff --git a/lib/address-importer.ts b/lib/address-importer.ts deleted file mode 100644 index 5e3fd4c..0000000 --- a/lib/address-importer.ts +++ /dev/null @@ -1,168 +0,0 @@ -import Bip32KeyDerivation from './bip32-key-derivation' -import Bip39Mnemonic from './bip39-mnemonic' -import Ed25519 from './ed25519' -import NanoAddress from './nano-address' -import Signer from './signer' -import Convert from './util/convert' - -export default class AddressImporter { - - /** - * Import a wallet using a mnemonic phrase - * - * @param {string} mnemonic - The mnemonic words to import the wallet from - * @param {string} [seedPassword] - (Optional) The password to use to secure the mnemonic - * @returns {Wallet} - The wallet derived from the mnemonic phrase - */ - static fromMnemonic = (mnemonic: string, seedPassword = ''): Wallet => { - if (!Bip39Mnemonic.validateMnemonic(mnemonic)) { - throw new Error('Invalid mnemonic phrase') - } - - const seed = Bip39Mnemonic.mnemonicToSeed(mnemonic, seedPassword) - const accounts = this.accounts(seed, 0, 0) - - return { - mnemonic, - seed, - accounts, - } - } - - /** - * Import a legacy wallet using a mnemonic phrase - * - * @param {string} mnemonic - The mnemonic words to import the wallet from - * @returns {Wallet} - The wallet derived from the mnemonic phrase - */ - static fromLegacyMnemonic = (mnemonic: string): Wallet => { - if (!Bip39Mnemonic.validateMnemonic(mnemonic)) { - throw new Error('Invalid mnemonic phrase') - } - - const seed = Bip39Mnemonic.mnemonicToLegacySeed(mnemonic) - return this.fromLegacySeed(seed, 0, 0, mnemonic) - } - - /** - * Validate mnemonic words - * - * @param mnemonic {string} mnemonic - The mnemonic words to validate - */ - static validateMnemonic = (mnemonic: string): boolean => { - return Bip39Mnemonic.validateMnemonic(mnemonic) - } - - /** - * Import a wallet using a seed - * - * @param {string} seed - The seed to import the wallet from - * @param {number} [from] - (Optional) The start index of the private keys to derive from - * @param {number} [to] - (Optional) The end index of the private keys to derive to - * @returns {Wallet} The wallet derived from the mnemonic phrase - */ - static fromSeed = (seed: string, from = 0, to = 0): Wallet => { - if (seed.length !== 128) { - throw new Error('Invalid seed length, must be a 128 byte hexadecimal string') - } - if (!/^[0-9a-fA-F]+$/i.test(seed)) { - throw new Error('Seed is not a valid hexadecimal string') - } - - const accounts = this.accounts(seed, from, to) - - return { - mnemonic: undefined, - seed, - accounts, - } - } - - - /** - * Import a wallet using a legacy seed - * - * @param {string} seed - The seed to import the wallet from - * @param {number} [from] - (Optional) The start index of the private keys to derive from - * @param {number} [to] - (Optional) The end index of the private keys to derive to - * @returns {Wallet} The wallet derived from the seed - */ - static fromLegacySeed = (seed: string, from: number = 0, to: number = 0, mnemonic?: string): Wallet => { - if (seed.length !== 64) { - throw new Error('Invalid seed length, must be a 64 byte hexadecimal string') - } - if (!/^[0-9a-fA-F]+$/i.test(seed)) { - throw new Error('Seed is not a valid hexadecimal string') - } - - const accounts = this.legacyAccounts(seed, from, to) - return { - mnemonic: mnemonic || Bip39Mnemonic.deriveMnemonic(seed), - seed, - accounts, - } - } - - /** - * Derives BIP32 private keys - * - * @param {string} seed - The seed to use for private key derivation - * @param {number} from - The start index of private keys to derive from - * @param {number} to - The end index of private keys to derive to - */ - private static accounts = (seed: string, from: number, to: number): Account[] => { - const accounts = [] - - for (let i = from; i <= to; i++) { - const privateKey = Bip32KeyDerivation.derivePath(`44'/165'/${i}'`, seed).key - const keyPair = new Ed25519().generateKeys(privateKey) - const address = NanoAddress.deriveAddress(keyPair.publicKey) - accounts.push({ - accountIndex: i, - privateKey: keyPair.privateKey, - publicKey: keyPair.publicKey, - address, - }) - } - - return accounts - } - - /** - * Derives legacy private keys - * - * @param {string} seed - The seed to use for private key derivation - * @param {number} from - The start index of private keys to derive from - * @param {number} to - The end index of private keys to derive to - */ - private static legacyAccounts = (seed: string, from: number, to: number): Account[] => { - const accounts: Account[] = [] - for (let i = from; i <= to; i++) { - const privateKey = Convert.ab2hex(Signer.generateHash([ seed, Convert.dec2hex(i, 4) ])) - const keyPair = new Ed25519().generateKeys(privateKey) - const address = NanoAddress.deriveAddress(keyPair.publicKey) - accounts.push({ - accountIndex: i, - privateKey: keyPair.privateKey, - publicKey: keyPair.publicKey, - address, - }) - } - - return accounts - } - -} - -export interface Wallet { - mnemonic: string - seed: string - accounts: Account[] -} - -export interface Account { - accountIndex: number - privateKey: string - publicKey: string - address: string -} diff --git a/lib/bip32-key-derivation.ts b/lib/bip32-key-derivation.ts deleted file mode 100644 index 3320051..0000000 --- a/lib/bip32-key-derivation.ts +++ /dev/null @@ -1,60 +0,0 @@ -//@ts-ignore -import { algo, enc } from 'crypto-js' - -import Convert from './util/convert' - -const ED25519_CURVE = 'ed25519 seed' -const HARDENED_OFFSET = 0x80000000 - -export default class Bip32KeyDerivation { - - static derivePath = (path: string, seed: string): Chain => { - const { key, chainCode } = this.getKeyFromSeed(seed) - const segments = path - .split('/') - .map(v => v.replace('\'', '')) - .map(el => parseInt(el, 10)) - return segments.reduce( - (parentKeys, segment) => - this.CKDPriv(parentKeys, segment + HARDENED_OFFSET), - { key, chainCode } - ) - } - - private static getKeyFromSeed = (seed: string): Chain => { - return this.derive( - enc.Hex.parse(seed), - enc.Utf8.parse(ED25519_CURVE)) - } - - private static CKDPriv = ({ key, chainCode }: Chain, index: number) => { - const ib = [] - ib.push((index >> 24) & 0xff) - ib.push((index >> 16) & 0xff) - ib.push((index >> 8) & 0xff) - ib.push(index & 0xff) - const data = '00' + key + Convert.ab2hex(new Uint8Array(ib).buffer) - - return this.derive( - enc.Hex.parse(data), - enc.Hex.parse(chainCode)) - } - - private static derive = (data: string, base: string): Chain => { - const hmac = algo.HMAC.create(algo.SHA512, base) - const I = hmac.update(data).finalize().toString() - const IL = I.slice(0, I.length / 2) - const IR = I.slice(I.length / 2) - - return { - key: IL, - chainCode: IR, - } - } - -} - -export interface Chain { - key: string - chainCode: string -} diff --git a/lib/bip39-mnemonic.ts b/lib/bip39-mnemonic.ts deleted file mode 100644 index 7539247..0000000 --- a/lib/bip39-mnemonic.ts +++ /dev/null @@ -1,185 +0,0 @@ -//@ts-ignore -import { PBKDF2, SHA256, algo, enc, lib } from 'crypto-js' - -import Convert from './util/convert' -import Util from './util/util' -import words from './words' - -export default class Bip39Mnemonic { - - /** - * Creates a BIP39 wallet - * - * @param {string} [entropy] - (Optional) the entropy to use instead of generating - * @returns {MnemonicSeed} The mnemonic phrase and a seed derived from the (generated) entropy - */ - static createWallet = (entropy: string, password: string): MnemonicSeed => { - if (entropy) { - if (entropy.length !== 64) { - throw new Error('Invalid entropy length, must be a 32 bit hexadecimal string') - } - if (!/^[0-9a-fA-F]+$/i.test(entropy)) { - throw new Error('Entopy is not a valid hexadecimal string') - } - } - - if (!entropy) { - entropy = this.randomHex(32) - } - - const mnemonic = this.deriveMnemonic(entropy) - const seed = this.mnemonicToSeed(mnemonic, password) - - return { - mnemonic, - seed, - } - } - - /** - * Creates an old Nano wallet - * - * @param {string} seed - (Optional) the seed to be used for the wallet - * @returns {MnemonicSeed} The mnemonic phrase and a generated seed if none provided - */ - static createLegacyWallet = (seed?: string): MnemonicSeed => { - if (seed) { - if (seed.length !== 64) { - throw new Error('Invalid entropy length, must be a 32 bit hexadecimal string') - } - if (!/^[0-9a-fA-F]+$/i.test(seed)) { - throw new Error('Entopy is not a valid hexadecimal string') - } - } - - if (!seed) { - seed = this.randomHex(32) - } - - const mnemonic = this.deriveMnemonic(seed) - - return { - mnemonic, - seed, - } - } - - static deriveMnemonic = (entropy: string): string => { - const entropyBinary = Convert.hexStringToBinary(entropy) - const entropySha256Binary = Convert.hexStringToBinary(this.calculateChecksum(entropy)) - const entropyBinaryWithChecksum = entropyBinary + entropySha256Binary - - const mnemonicWords = [] - for (let i = 0; i < entropyBinaryWithChecksum.length; i += 11) { - mnemonicWords.push(words[parseInt(entropyBinaryWithChecksum.substr(i, 11), 2)]) - } - - return mnemonicWords.join(' ') - } - - /** - * Validates a mnemonic phrase - * - * @param {string} mnemonic - The mnemonic phrase to validate - * @returns {boolean} Is the mnemonic phrase valid - */ - static validateMnemonic = (mnemonic: string): boolean => { - const wordArray = Util.normalizeUTF8(mnemonic).split(' ') - if (wordArray.length % 3 !== 0) { - return false - } - - const bits = wordArray.map((w: string) => { - const wordIndex = words.indexOf(w) - if (wordIndex === -1) { - return false - } - return (Convert.dec2bin(wordIndex)).padStart(11, '0') - }).join('') - - const dividerIndex = Math.floor(bits.length / 33) * 32 - const entropyBits = bits.slice(0, dividerIndex) - const checksumBits = bits.slice(dividerIndex) - const entropyBytes = entropyBits.match(/(.{1,8})/g).map((bin: string) => parseInt(bin, 2)) - - if (entropyBytes.length < 16) { - return false - } - - if (entropyBytes.length > 32) { - return false - } - - if (entropyBytes.length % 4 !== 0) { - return false - } - - const entropyHex = Convert.bytesToHexString(entropyBytes) - const newChecksum = this.calculateChecksum(entropyHex) - const inputChecksum = Convert.binaryToHexString(checksumBits) - - if (parseInt(newChecksum, 16) != parseInt(inputChecksum, 16)) { - return false - } - - return true - } - - /** - * Converts the mnemonic phrase to an old Nano seed - * - * @param {string} mnemonic Mnemonic phrase separated by spaces - */ - static mnemonicToLegacySeed = (mnemonic: string): string => { - const wordArray = Util.normalizeUTF8(mnemonic).split(' ') - const bits = wordArray.map((w: string) => { - const wordIndex = words.indexOf(w) - if (wordIndex === -1) { - return false - } - return (Convert.dec2bin(wordIndex)).padStart(11, '0') - }).join('') - - const dividerIndex = Math.floor(bits.length / 33) * 32 - const entropyBits = bits.slice(0, dividerIndex) - const entropyBytes = entropyBits.match(/(.{1,8})/g).map((bin: string) => parseInt(bin, 2)) - const entropyHex = Convert.bytesToHexString(entropyBytes) - - return entropyHex - } - - /** - * Converts the mnemonic phrase to a BIP39 seed - * - * @param {string} mnemonic Mnemonic phrase separated by spaces - */ - static mnemonicToSeed = (mnemonic: string, password: string): string => { - const normalizedMnemonic = Util.normalizeUTF8(mnemonic) - const normalizedPassword = 'mnemonic' + Util.normalizeUTF8(password) - - return PBKDF2( - normalizedMnemonic, - normalizedPassword, - { - keySize: 512 / 32, - iterations: 2048, - hasher: algo.SHA512, - }) - .toString(enc.Hex) - } - - private static randomHex = (length: number): string => { - return lib.WordArray.random(length).toString() - } - - private static calculateChecksum = (entropyHex: string): string => { - const entropySha256 = SHA256(enc.Hex.parse(entropyHex)).toString() - return entropySha256.substr(0, entropySha256.length / 32) - } - -} - -interface MnemonicSeed { - mnemonic: string, - seed: string, -} diff --git a/lib/block-signer.ts b/lib/block-signer.ts deleted file mode 100644 index d4e9330..0000000 --- a/lib/block-signer.ts +++ /dev/null @@ -1,188 +0,0 @@ -import BigNumber from 'bignumber.js' - -import NanoAddress from './nano-address' -import NanoConverter from './nano-converter' -import Signer from './signer' -import Convert from './util/convert' - -export default class BlockSigner { - - static readonly preamble: string = 0x6.toString().padStart(64, '0') - - /** - * Sign a receive block - * - * @param {ReceiveBlock} data The data required to sign a receive block - * @param {string} privateKey Private key to sign the data with - * @returns {SignedBlock} the signed block to publish to the blockchain - */ - static receive = (data: ReceiveBlock, privateKey: string): SignedBlock => { - const validateInputRaw = (input: string) => !!input && !isNaN(+input) - if (!validateInputRaw(data.walletBalanceRaw)) { - throw new Error('Invalid format in wallet balance') - } - - if (!validateInputRaw(data.amountRaw)) { - throw new Error('Invalid format in send amount') - } - - if (!NanoAddress.validateNanoAddress(data.toAddress)) { - throw new Error('Invalid toAddress') - } - - if (!NanoAddress.validateNanoAddress(data.representativeAddress)) { - throw new Error('Invalid representativeAddress') - } - - if (!data.transactionHash) { - throw new Error('No transaction hash') - } - - if (!data.frontier) { - throw new Error('No frontier') - } - - if (!privateKey) { - throw new Error('Please input the private key to sign the block') - } - - const balanceNano = NanoConverter.convert(data.walletBalanceRaw, 'RAW', 'NANO') - const amountNano = NanoConverter.convert(data.amountRaw, 'RAW', 'NANO') - const newBalanceNano = new BigNumber(balanceNano).plus(new BigNumber(amountNano)) - const newBalanceRaw = NanoConverter.convert(newBalanceNano, 'NANO', 'RAW') - const newBalanceHex = Convert.dec2hex(newBalanceRaw, 16).toUpperCase() - const account = NanoAddress.nanoAddressToHexString(data.toAddress) - const link = data.transactionHash - const representative = NanoAddress.nanoAddressToHexString(data.representativeAddress) - - const signature = Signer.sign( - privateKey, - this.preamble, - account, - data.frontier, - representative, - newBalanceHex, - link) - - return { - type: 'state', - account: data.toAddress, - previous: data.frontier, - representative: data.representativeAddress, - balance: newBalanceRaw, - link: link, - signature: signature, - work: data.work || '', - } - } - - /** - * Sign a send block - * - * @param {SendBlock} data The data required to sign a send block - * @param {string} privateKey Private key to sign the data with - * @returns {SignedBlock} the signed block to publish to the blockchain - */ - static send = (data: SendBlock, privateKey: string): SignedBlock => { - const validateInputRaw = (input: string) => !!input && !isNaN(+input) - if (!validateInputRaw(data.walletBalanceRaw)) { - throw new Error('Invalid format in wallet balance') - } - - if (!validateInputRaw(data.amountRaw)) { - throw new Error('Invalid format in send amount') - } - - if (!NanoAddress.validateNanoAddress(data.toAddress)) { - throw new Error('Invalid toAddress') - } - - if (!NanoAddress.validateNanoAddress(data.fromAddress)) { - throw new Error('Invalid fromAddress') - } - - if (!NanoAddress.validateNanoAddress(data.representativeAddress)) { - throw new Error('Invalid representativeAddress') - } - - if (!data.frontier) { - throw new Error('Frontier is not set') - } - - if (!privateKey) { - throw new Error('Please input the private key to sign the block') - } - - const balanceNano = NanoConverter.convert(data.walletBalanceRaw, 'RAW', 'NANO') - const amountNano = NanoConverter.convert(data.amountRaw, 'RAW', 'NANO') - const newBalanceNano = new BigNumber(balanceNano).minus(new BigNumber(amountNano)) - const newBalanceRaw = NanoConverter.convert(newBalanceNano, 'NANO', 'RAW') - const newBalanceHex = Convert.dec2hex(newBalanceRaw, 16).toUpperCase() - const account = NanoAddress.nanoAddressToHexString(data.fromAddress) - const link = NanoAddress.nanoAddressToHexString(data.toAddress) - const representative = NanoAddress.nanoAddressToHexString(data.representativeAddress) - - const signature = Signer.sign( - privateKey, - this.preamble, - account, - data.frontier, - representative, - newBalanceHex, - link) - - return { - type: 'state', - account: data.fromAddress, - previous: data.frontier, - representative: data.representativeAddress, - balance: newBalanceRaw, - link: link, - signature: signature, - work: data.work || '', - } - } - -} - -export interface ReceiveBlock { - walletBalanceRaw: string - toAddress: string - transactionHash: string - frontier: string - representativeAddress: string - amountRaw: string - work?: string -} - -export interface SendBlock { - walletBalanceRaw: string - fromAddress: string - toAddress: string - representativeAddress: string - frontier: string - amountRaw: string - work?: string -} - -export interface RepresentativeBlock { - walletBalanceRaw: string - address: string - representativeAddress: string - frontier: string - work?: string -} - -export interface SignedBlock extends BlockData { - type: 'state' - work?: string -} - -export interface BlockData { - account: string - previous: string - representative: string - balance: string - link: string - signature: string -} diff --git a/lib/box.ts b/lib/box.ts deleted file mode 100644 index 3389466..0000000 --- a/lib/box.ts +++ /dev/null @@ -1,69 +0,0 @@ -import * as base64 from 'byte-base64' -//@ts-ignore -import { lib } from 'crypto-js' - -import Ed25519 from './ed25519' -import NanoAddress from './nano-address' -import Convert from './util/convert' -import Curve25519 from './util/curve25519' - -export default class Box { - - static readonly NONCE_LENGTH = 24 - - static encrypt(message: string, address: string, privateKey: string) { - if (!message) { - throw new Error('No message to encrypt') - } - - const publicKey = NanoAddress.addressToPublicKey(address) - const { privateKey: convertedPrivateKey, publicKey: convertedPublicKey } = new Ed25519().convertKeys({ - privateKey, - publicKey, - }) - - const nonce = Convert.hex2ab(lib.WordArray.random(this.NONCE_LENGTH).toString()) - const encrypted = new Curve25519().box( - Convert.decodeUTF8(message), - nonce, - Convert.hex2ab(convertedPublicKey), - Convert.hex2ab(convertedPrivateKey), - ) - - const full = new Uint8Array(nonce.length + encrypted.length) - full.set(nonce) - full.set(encrypted, nonce.length) - - return base64.bytesToBase64(full) - } - - static decrypt(encrypted: string, address: string, privateKey: string) { - if (!encrypted) { - throw new Error('No message to decrypt') - } - - const publicKey = NanoAddress.addressToPublicKey(address) - const { privateKey: convertedPrivateKey, publicKey: convertedPublicKey } = new Ed25519().convertKeys({ - privateKey, - publicKey, - }) - - const decodedEncryptedMessageBytes = base64.base64ToBytes(encrypted) - const nonce = decodedEncryptedMessageBytes.slice(0, this.NONCE_LENGTH) - const encryptedMessage = decodedEncryptedMessageBytes.slice(this.NONCE_LENGTH, encrypted.length) - - const decrypted = new Curve25519().boxOpen( - encryptedMessage, - nonce, - Convert.hex2ab(convertedPublicKey), - Convert.hex2ab(convertedPrivateKey), - ) - - if (!decrypted) { - throw new Error('Could not decrypt message') - } - - return Convert.encodeUTF8(decrypted) - } - -} diff --git a/lib/ed25519.ts b/lib/ed25519.ts deleted file mode 100644 index 12f6903..0000000 --- a/lib/ed25519.ts +++ /dev/null @@ -1,268 +0,0 @@ -//@ts-ignore -import { blake2b, blake2bFinal, blake2bInit, blake2bUpdate } from 'blakejs' - -import Convert from './util/convert' -import Curve25519 from './util/curve25519' -import Util from './util/util' - -export default class Ed25519 { - - curve: Curve25519 - X: Int32Array - Y: Int32Array - L: Uint8Array - - constructor() { - this.curve = new Curve25519() - this.X = this.curve.gf([0xd51a, 0x8f25, 0x2d60, 0xc956, 0xa7b2, 0x9525, 0xc760, 0x692c, 0xdc5c, 0xfdd6, 0xe231, 0xc0a4, 0x53fe, 0xcd6e, 0x36d3, 0x2169]) - this.Y = this.curve.gf([0x6658, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666]) - this.L = new Uint8Array([0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10]) - } - - pack(r: Uint8Array, p: Int32Array[]): void { - const CURVE = this.curve - const tx = CURVE.gf(), - ty = CURVE.gf(), - zi = CURVE.gf() - CURVE.inv25519(zi, p[2]) - CURVE.M(tx, p[0], zi) - CURVE.M(ty, p[1], zi) - CURVE.pack25519(r, ty) - r[31] ^= CURVE.par25519(tx) << 7 - } - - modL(r: Uint8Array, x: Uint32Array | Float64Array): void { - let carry, i, j, k - for (i = 63; i >= 32; --i) { - carry = 0 - for (j = i - 32, k = i - 12; j < k; ++j) { - x[j] += carry - 16 * x[i] * this.L[j - (i - 32)] - carry = (x[j] + 128) >> 8 - x[j] -= carry * 256 - } - x[j] += carry - x[i] = 0 - } - - carry = 0 - for (j = 0; j < 32; j++) { - x[j] += carry - (x[31] >> 4) * this.L[j] - carry = x[j] >> 8 - x[j] &= 255 - } - - for (j = 0; j < 32; j++) { - x[j] -= carry * this.L[j] - } - - for (i = 0; i < 32; i++) { - x[i + 1] += x[i] >>> 8 - r[i] = x[i] & 0xff - } - } - - reduce(r: Uint8Array): void { - const x = new Uint32Array(64) - for (let i = 0; i < 64; i++) { - x[i] = r[i] - } - - this.modL(r, x) - } - - scalarmult(p: Int32Array[], q: Int32Array[], s: Uint8Array): void { - const CURVE = this.curve - CURVE.set25519(p[0], CURVE.gf0) - CURVE.set25519(p[1], CURVE.gf1) - CURVE.set25519(p[2], CURVE.gf1) - CURVE.set25519(p[3], CURVE.gf0) - for (let i = 255; i >= 0; --i) { - const b = (s[(i / 8) | 0] >>> (i & 7)) & 1 - CURVE.cswap(p, q, b) - CURVE.add(q, p) - CURVE.add(p, p) - CURVE.cswap(p, q, b) - } - } - - scalarbase(p: Int32Array[], s: Uint8Array): void { - const CURVE = this.curve - const q = [CURVE.gf(), CURVE.gf(), CURVE.gf(), CURVE.gf()] - CURVE.set25519(q[0], this.X) - CURVE.set25519(q[1], this.Y) - CURVE.set25519(q[2], CURVE.gf1) - CURVE.M(q[3], this.X, this.Y) - this.scalarmult(p, q, s) - } - - /** - * Generate an ed25519 keypair - * @param {String} seed A 32 byte cryptographic secure random hexadecimal string. This is basically the secret key - * @param {Object} Returns sk (Secret key) and pk (Public key) as 32 byte hexadecimal strings - */ - generateKeys(seed: string): KeyPair { - const pk = new Uint8Array(32) - const p = [this.curve.gf(), this.curve.gf(), this.curve.gf(), this.curve.gf()] - const h = blake2b(Convert.hex2ab(seed), undefined, 64).slice(0, 32) - - h[0] &= 0xf8 - h[31] &= 0x7f - h[31] |= 0x40 - - this.scalarbase(p, h) - this.pack(pk, p) - - return { - privateKey: seed, - publicKey: Convert.ab2hex(pk), - } - } - - /** - * Convert ed25519 keypair to curve25519 keypair suitable for Diffie-Hellman key exchange - * - * @param {KeyPair} keyPair ed25519 keypair - * @returns {KeyPair} keyPair Curve25519 keypair - */ - convertKeys(keyPair: KeyPair): KeyPair { - const publicKey = Convert.ab2hex(this.curve.convertEd25519PublicKeyToCurve25519(Convert.hex2ab(keyPair.publicKey))) - if (!publicKey) { - return null - } - const privateKey = Convert.ab2hex(this.curve.convertEd25519SecretKeyToCurve25519(Convert.hex2ab(keyPair.privateKey))) - return { - publicKey, - privateKey, - } - } - - /** - * Generate a message signature - * @param {Uint8Array} msg Message to be signed as byte array - * @param {Uint8Array} privateKey Secret key as byte array - * @param {Uint8Array} Returns the signature as 64 byte typed array - */ - sign(msg: Uint8Array, privateKey: Uint8Array): Uint8Array { - const signedMsg = this.naclSign(msg, privateKey) - const sig = new Uint8Array(64) - - for (let i = 0; i < sig.length; i++) { - sig[i] = signedMsg[i] - } - - return sig - } - - /** - * Verify a message signature - * @param {Uint8Array} msg Message to be signed as byte array - * @param {Uint8Array} publicKey Public key as byte array - * @param {Uint8Array} signature Signature as byte array - * @param {Uint8Array} Returns the signature as 64 byte typed array - */ - verify(msg: Uint8Array, publicKey: Uint8Array, signature: Uint8Array): boolean { - const CURVE = this.curve - const p = [CURVE.gf(), CURVE.gf(), CURVE.gf(), CURVE.gf()] - const q = [CURVE.gf(), CURVE.gf(), CURVE.gf(), CURVE.gf()] - - if (signature.length !== 64) { - return false - } - if (publicKey.length !== 32) { - return false - } - if (CURVE.unpackNeg(q, publicKey)) { - return false - } - - const ctx = blake2bInit(64, undefined) - blake2bUpdate(ctx, signature.subarray(0, 32)) - blake2bUpdate(ctx, publicKey) - blake2bUpdate(ctx, msg) - let k = blake2bFinal(ctx) - this.reduce(k) - this.scalarmult(p, q, k) - - let t = new Uint8Array(32) - this.scalarbase(q, signature.subarray(32)) - CURVE.add(p, q) - this.pack(t, p) - - return Util.compare(signature.subarray(0, 32), t) - } - - private naclSign(msg: Uint8Array, secretKey: Uint8Array): Uint8Array { - if (secretKey.length !== 32) { - throw new Error('bad secret key size') - } - - const signedMsg = new Uint8Array(64 + msg.length) - this.cryptoSign(signedMsg, msg, msg.length, secretKey) - - return signedMsg - } - - private cryptoSign(sm: Uint8Array, m: Uint8Array, n: number, sk: Uint8Array): number { - const CURVE = this.curve - const d = new Uint8Array(64) - const h = new Uint8Array(64) - const r = new Uint8Array(64) - const x = new Float64Array(64) - const p = [CURVE.gf(), CURVE.gf(), CURVE.gf(), CURVE.gf()] - - let i - let j - - const pk = Convert.hex2ab(this.generateKeys(Convert.ab2hex(sk)).publicKey) - - this.curve.cryptoHash(d, sk, 32) - d[0] &= 248 - d[31] &= 127 - d[31] |= 64 - - const smlen = n + 64 - for (i = 0; i < n; i++) { - sm[64 + i] = m[i] - } - - for (i = 0; i < 32; i++) { - sm[32 + i] = d[32 + i] - } - - this.curve.cryptoHash(r, sm.subarray(32), n + 32) - this.reduce(r) - this.scalarbase(p, r) - this.pack(sm, p) - - for (i = 32; i < 64; i++) { - sm[i] = pk[i - 32] - } - - this.curve.cryptoHash(h, sm, n + 64) - this.reduce(h) - - for (i = 0; i < 64; i++) { - x[i] = 0 - } - - for (i = 0; i < 32; i++) { - x[i] = r[i] - } - - for (i = 0; i < 32; i++) { - for (j = 0; j < 32; j++) { - x[i + j] += h[i] * d[j] - } - } - - this.modL(sm.subarray(32), x) - - return smlen - } - -} - -export interface KeyPair { - privateKey: string - publicKey: string -} diff --git a/lib/nano-address.ts b/lib/nano-address.ts deleted file mode 100644 index 030a2ab..0000000 --- a/lib/nano-address.ts +++ /dev/null @@ -1,150 +0,0 @@ -//@ts-ignore -import { blake2b } from 'blakejs' - -import Convert from './util/convert' - -export default class NanoAddress { - - static readonly alphabet = '13456789abcdefghijkmnopqrstuwxyz' - static readonly prefix = 'nano_' - - static deriveAddress = (publicKey: string): string => { - const publicKeyBytes = Convert.hex2ab(publicKey) - const checksum = blake2b(publicKeyBytes, undefined, 5).reverse() - const encoded = this.encodeNanoBase32(publicKeyBytes) - const encodedChecksum = this.encodeNanoBase32(checksum) - - return this.prefix + encoded + encodedChecksum - } - - static encodeNanoBase32 = (publicKey: Uint8Array): string => { - const length = publicKey.length - const leftover = (length * 8) % 5 - const offset = leftover === 0 ? 0 : 5 - leftover - - let value = 0 - let output = '' - let bits = 0 - - for (let i = 0; i < length; i++) { - value = (value << 8) | publicKey[i] - bits += 8 - - while (bits >= 5) { - output += this.alphabet[(value >>> (bits + offset - 5)) & 31] - bits -= 5 - } - } - - if (bits > 0) { - output += this.alphabet[(value << (5 - (bits + offset))) & 31] - } - - return output - } - - static addressToPublicKey = (input: string): string => { - const cleaned = input - .replace('nano_', '') - .replace('xrb_', '') - const publicKeyBytes = NanoAddress.decodeNanoBase32(cleaned) - return Convert.ab2hex(publicKeyBytes).slice(0, 64) - } - - static decodeNanoBase32 = (input: string): Uint8Array => { - const length = input.length - const leftover = (length * 5) % 8 - const offset = leftover === 0 ? 0 : 8 - leftover - - let bits = 0 - let value = 0 - let index = 0 - let output = new Uint8Array(Math.ceil((length * 5) / 8)) - - for (let i = 0; i < length; i++) { - value = (value << 5) | this.readChar(input[i]) - bits += 5 - - if (bits >= 8) { - output[index++] = (value >>> (bits + offset - 8)) & 255 - bits -= 8 - } - } - - if (bits > 0) { - output[index++] = (value << (bits + offset - 8)) & 255 - } - - if (leftover !== 0) { - output = output.slice(1) - } - - return output - } - - /** - * Validates a Nano address with 'nano' and 'xrb' prefixes - * - * Derived from https://github.com/alecrios/nano-address-validator - * - * @param {string} address Nano address - */ - static validateNanoAddress = (address: string): boolean => { - if (address === undefined) { - throw Error('Address must be defined.') - } - - if (typeof address !== 'string') { - throw TypeError('Address must be a string.') - } - - const allowedPrefixes: string[] = ['nano', 'xrb'] - const pattern = new RegExp( - `^(${allowedPrefixes.join('|')})_[13]{1}[13456789abcdefghijkmnopqrstuwxyz]{59}$`, - ) - - if (!pattern.test(address)) { - return false - } - - const expectedChecksum = address.slice(-8) - const publicKey = this.stripAddress(address) - const publicKeyBuffer = this.decodeNanoBase32(publicKey) - const actualChecksumBuffer = blake2b(publicKeyBuffer, null, 5).reverse() - const actualChecksum = this.encodeNanoBase32(actualChecksumBuffer) - - return expectedChecksum === actualChecksum - } - - static nanoAddressToHexString = (addr: string): string => { - addr = addr.slice(-60) - const isValid = /^[13456789abcdefghijkmnopqrstuwxyz]+$/.test(addr) - if (isValid) { - const keyBytes = this.decodeNanoBase32(addr.substring(0, 52)) - const hashBytes = this.decodeNanoBase32(addr.substring(52, 60)) - const blakeHash = blake2b(keyBytes, undefined, 5).reverse() - if (Convert.ab2hex(hashBytes) == Convert.ab2hex(blakeHash)) { - const key = Convert.ab2hex(keyBytes).toUpperCase() - return key - } - throw new Error('Checksum mismatch in address') - } else { - throw new Error('Illegal characters in address') - } - } - - static stripAddress(address: string): string { - return address.slice(address.indexOf('_') + 1, -8) - } - - private static readChar(char: string): number { - const idx = this.alphabet.indexOf(char) - - if (idx === -1) { - throw new Error(`Invalid character found: ${char}`) - } - - return idx - } - -} diff --git a/lib/nano-converter.ts b/lib/nano-converter.ts deleted file mode 100644 index fe60eb2..0000000 --- a/lib/nano-converter.ts +++ /dev/null @@ -1,48 +0,0 @@ -import BigNumber from 'bignumber.js' - -export default class NanoConverter { - - /** - * Converts the input value to the wanted unit - * - * @param input {string | BigNumber} value - * @param inputUnit {string} the unit to convert from - * @param outputUnit {string} the unit to convert to - */ - static convert = (input: string | BigNumber, inputUnit: string, outputUnit: string): string => { - let value = new BigNumber(input.toString()) - - switch (inputUnit) { - case 'RAW': - value = value - break - case 'NANO': - case 'MRAI': - value = value.shiftedBy(30) - break - case 'KRAI': - value = value.shiftedBy(27) - break - case 'RAI': - value = value.shiftedBy(24) - break - default: - throw new Error(`Unkown input unit ${inputUnit}, expected one of the following: RAW, NANO, MRAI, KRAI, RAI`) - } - - switch (outputUnit) { - case 'RAW': - return value.toFixed(0) - case 'NANO': - case 'MRAI': - return value.shiftedBy(-30).toFixed(30, 1) - case 'KRAI': - return value.shiftedBy(-27).toFixed(27, 1) - case 'RAI': - return value.shiftedBy(-24).toFixed(24, 1) - default: - throw new Error(`Unknown output unit ${outputUnit}, expected one of the following: RAW, NANO, MRAI, KRAI, RAI`) - } - } - -} diff --git a/lib/signer.ts b/lib/signer.ts deleted file mode 100644 index 2548b3e..0000000 --- a/lib/signer.ts +++ /dev/null @@ -1,48 +0,0 @@ -//@ts-ignore - -import { blake2bFinal, blake2bInit, blake2bUpdate } from 'blakejs' - -import Ed25519 from './ed25519' -import Convert from './util/convert' - -export default class Signer { - - /** - * Signs any data using the ed25519 signature system - * - * @param privateKey Private key to sign the data with - * @param data Data to sign - */ - static sign = (privateKey: string, ...data: string[]): string => { - const signature = new Ed25519().sign( - this.generateHash(data), - Convert.hex2ab(privateKey)) - return Convert.ab2hex(signature) - } - - /** - * Verify the signature with a public key - * - * @param publicKey Public key to verify the data with - * @param signature Signature to verify - * @param data Data to verify - */ - static verify = (publicKey: string, signature: string, ...data: string[]): boolean => { - return new Ed25519().verify( - this.generateHash(data), - Convert.hex2ab(publicKey), - Convert.hex2ab(signature)) - } - - /** - * Creates a blake2b hash of the input data - * - * @param data Data to hash - */ - static generateHash = (data: string[]): Uint8Array => { - const ctx = blake2bInit(32, undefined) - data.forEach(str => blake2bUpdate(ctx, Convert.hex2ab(str))) - return blake2bFinal(ctx) - } - -} \ No newline at end of file diff --git a/lib/util/convert.ts b/lib/util/convert.ts deleted file mode 100644 index 4bf8198..0000000 --- a/lib/util/convert.ts +++ /dev/null @@ -1,148 +0,0 @@ -export default class Convert { - - /** - * Convert a string (UTF-8 encoded) to a byte array - * - * @param {String} str UTF-8 encoded string - * @return {Uint8Array} Byte array - */ - static str2bin(str: string): Uint8Array { - str = str.replace(/\r\n/g, '\n') - const bin = new Uint8Array(str.length * 3) - let p = 0 - for (let i = 0, len = str.length; i < len; i++) { - const c = str.charCodeAt(i) - if (c < 128) { - bin[p++] = c - } else if (c < 2048) { - bin[p++] = (c >>> 6) | 192 - bin[p++] = (c & 63) | 128 - } else { - bin[p++] = (c >>> 12) | 224 - bin[p++] = ((c >>> 6) & 63) | 128 - bin[p++] = (c & 63) | 128 - } - } - return bin.subarray(0, p) - } - - /** - * Convert a byte array to a UTF-8 encoded string - * - * @param {Uint8Array} arr Byte array - * @return {string} UTF-8 encoded string - */ - static encodeUTF8 = (arr: Uint8Array): string => { - const s = [] - for (let i = 0; i < arr.length; i++) { - s.push(String.fromCharCode(arr[i])) - } - return decodeURIComponent(escape(s.join(''))) - } - - /** - * Convert a UTF-8 encoded string to a byte array - * - * @param {string} str UTF-8 encoded string - * @return {Uint8Array} Byte array - */ - static decodeUTF8 = (str: string): Uint8Array => { - if (typeof str !== 'string') { - throw new TypeError('expected string') - } - const d = unescape(encodeURIComponent(str)) - const b = new Uint8Array(d.length) - for (let i = 0; i < d.length; i++) { - b[i] = d.charCodeAt(i) - } - return b - } - - /** - * Convert Array of 8 bytes (int64) to hex string - * - * @param {Uint8Array} bin Array of bytes - * @return {String} Hex encoded string - */ - static ab2hex = (buf: ArrayBuffer): string => { - return Array.prototype.map.call(new Uint8Array(buf), x => ('00' + x.toString(16)).slice(-2)).join('') - } - - /** - * Convert hex string to array of 8 bytes (int64) - * - * @param {String} bin Array of bytes - * @return {Uint8Array} Array of 8 bytes (int64) - */ - static hex2ab = (hex: string): Uint8Array => { - const ab = [] - for (let i = 0; i < hex.length; i += 2) { - ab.push(parseInt(hex.substr(i, 2), 16)) - } - return new Uint8Array(ab) - } - - /** - * Convert a decimal number to hex string - * - * @param {String} str Decimal to be converted - * @param {Number} bytes Length of the output to be padded - * @returns Hexadecimal representation of the inputed decimal - */ - static dec2hex = (str: number | string, bytes: number): string => { - const decimals = str.toString().split('') - const sum = [] - let hex = [] - let i: number - let s: number - - while (decimals.length) { - s = 1 * +decimals.shift() - for (i = 0; s || i < sum.length; i++) { - s += (sum[i] || 0) * 10 - sum[i] = s % 16 - s = (s - sum[i]) / 16 - } - } - - while (sum.length) { - hex.push(sum.pop().toString(16)) - } - - let joined = hex.join('') - - if (joined.length % 2 != 0) { - joined = '0' + joined - } - - if (bytes > joined.length / 2) { - const diff = bytes - joined.length / 2 - for (let i = 0; i < diff; i++) { - joined = '00' + joined - } - } - - return joined - } - - static dec2bin = (dec: number): string => { - return (dec >>> 0).toString(2) - } - - static bytesToHexString = (bytes: number[]): string => { - return [...bytes].map(b => b.toString(16).padStart(2, '0')).join('') - } - - static hexStringToBinary = (hex: string): string => { - return [...hex].map(c => (Convert.dec2bin(parseInt(c, 16))).padStart(4, '0')).join('') - } - - static binaryToHexString = (bin: string): string => { - return parseInt(bin, 2).toString(16) - } - - static stringToHex = (str: string): string => { - return [...str].map(c => c.charCodeAt(0).toString(16)).join('') - } - -} diff --git a/lib/util/curve25519.ts b/lib/util/curve25519.ts deleted file mode 100644 index 026ef99..0000000 --- a/lib/util/curve25519.ts +++ /dev/null @@ -1,1414 +0,0 @@ -//@ts-ignore -import { blake2b } from 'blakejs' - -import Util from './util' - -/** - * Derived from: - * - mipher - * - tweetnacl - * - ed2curve-js - * - * With added types etc - */ -export default class Curve25519 { - - gf0: Int32Array - gf1: Int32Array - D: Int32Array - D2: Int32Array - I: Int32Array - _9: Uint8Array - _121665: Int32Array - _0: Uint8Array - sigma: Uint8Array - minusp: Uint32Array - - constructor() { - this.gf0 = this.gf() - this.gf1 = this.gf([1]) - this._9 = new Uint8Array(32) - this._9[0] = 9 - this._121665 = this.gf([0xdb41, 1]) - this.D = this.gf([0x78a3, 0x1359, 0x4dca, 0x75eb, 0xd8ab, 0x4141, 0x0a4d, 0x0070, 0xe898, 0x7779, 0x4079, 0x8cc7, 0xfe73, 0x2b6f, 0x6cee, 0x5203]) - this.D2 = this.gf([0xf159, 0x26b2, 0x9b94, 0xebd6, 0xb156, 0x8283, 0x149a, 0x00e0, 0xd130, 0xeef3, 0x80f2, 0x198e, 0xfce7, 0x56df, 0xd9dc, 0x2406]) - this.I = this.gf([0xa0b0, 0x4a0e, 0x1b27, 0xc4ee, 0xe478, 0xad2f, 0x1806, 0x2f43, 0xd7a7, 0x3dfb, 0x0099, 0x2b4d, 0xdf0b, 0x4fc1, 0x2480, 0x2b83]) - this._0 = new Uint8Array(16) - this.sigma = new Uint8Array([101, 120, 112, 97, 110, 100, 32, 51, 50, 45, 98, 121, 116, 101, 32, 107]) - this.minusp = new Uint32Array([5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 252]) - } - - gf(init?: number[]): Int32Array { - const r = new Int32Array(16) - if (init) { - for (let i = 0; i < init.length; i++) { - r[i] = init[i] - } - } - - return r - } - - A(o: Int32Array, a: Int32Array, b: Int32Array): void { - for (let i = 0; i < 16; i++) { - o[i] = a[i] + b[i] - } - } - - Z(o: Int32Array, a: Int32Array, b: Int32Array): void { - for (let i = 0; i < 16; i++) { - o[i] = a[i] - b[i] - } - } - - // Avoid loops for better performance - M(o: Int32Array, a: Int32Array, b: Int32Array): void { - let v, c, - t0 = 0, t1 = 0, t2 = 0, t3 = 0, t4 = 0, t5 = 0, t6 = 0, t7 = 0, - t8 = 0, t9 = 0, t10 = 0, t11 = 0, t12 = 0, t13 = 0, t14 = 0, t15 = 0, - t16 = 0, t17 = 0, t18 = 0, t19 = 0, t20 = 0, t21 = 0, t22 = 0, t23 = 0, - t24 = 0, t25 = 0, t26 = 0, t27 = 0, t28 = 0, t29 = 0, t30 = 0 - const b0 = b[0], - b1 = b[1], - b2 = b[2], - b3 = b[3], - b4 = b[4], - b5 = b[5], - b6 = b[6], - b7 = b[7], - b8 = b[8], - b9 = b[9], - b10 = b[10], - b11 = b[11], - b12 = b[12], - b13 = b[13], - b14 = b[14], - b15 = b[15] - - v = a[0] - t0 += v * b0 - t1 += v * b1 - t2 += v * b2 - t3 += v * b3 - t4 += v * b4 - t5 += v * b5 - t6 += v * b6 - t7 += v * b7 - t8 += v * b8 - t9 += v * b9 - t10 += v * b10 - t11 += v * b11 - t12 += v * b12 - t13 += v * b13 - t14 += v * b14 - t15 += v * b15 - v = a[1] - t1 += v * b0 - t2 += v * b1 - t3 += v * b2 - t4 += v * b3 - t5 += v * b4 - t6 += v * b5 - t7 += v * b6 - t8 += v * b7 - t9 += v * b8 - t10 += v * b9 - t11 += v * b10 - t12 += v * b11 - t13 += v * b12 - t14 += v * b13 - t15 += v * b14 - t16 += v * b15 - v = a[2] - t2 += v * b0 - t3 += v * b1 - t4 += v * b2 - t5 += v * b3 - t6 += v * b4 - t7 += v * b5 - t8 += v * b6 - t9 += v * b7 - t10 += v * b8 - t11 += v * b9 - t12 += v * b10 - t13 += v * b11 - t14 += v * b12 - t15 += v * b13 - t16 += v * b14 - t17 += v * b15 - v = a[3] - t3 += v * b0 - t4 += v * b1 - t5 += v * b2 - t6 += v * b3 - t7 += v * b4 - t8 += v * b5 - t9 += v * b6 - t10 += v * b7 - t11 += v * b8 - t12 += v * b9 - t13 += v * b10 - t14 += v * b11 - t15 += v * b12 - t16 += v * b13 - t17 += v * b14 - t18 += v * b15 - v = a[4] - t4 += v * b0 - t5 += v * b1 - t6 += v * b2 - t7 += v * b3 - t8 += v * b4 - t9 += v * b5 - t10 += v * b6 - t11 += v * b7 - t12 += v * b8 - t13 += v * b9 - t14 += v * b10 - t15 += v * b11 - t16 += v * b12 - t17 += v * b13 - t18 += v * b14 - t19 += v * b15 - v = a[5] - t5 += v * b0 - t6 += v * b1 - t7 += v * b2 - t8 += v * b3 - t9 += v * b4 - t10 += v * b5 - t11 += v * b6 - t12 += v * b7 - t13 += v * b8 - t14 += v * b9 - t15 += v * b10 - t16 += v * b11 - t17 += v * b12 - t18 += v * b13 - t19 += v * b14 - t20 += v * b15 - v = a[6] - t6 += v * b0 - t7 += v * b1 - t8 += v * b2 - t9 += v * b3 - t10 += v * b4 - t11 += v * b5 - t12 += v * b6 - t13 += v * b7 - t14 += v * b8 - t15 += v * b9 - t16 += v * b10 - t17 += v * b11 - t18 += v * b12 - t19 += v * b13 - t20 += v * b14 - t21 += v * b15 - v = a[7] - t7 += v * b0 - t8 += v * b1 - t9 += v * b2 - t10 += v * b3 - t11 += v * b4 - t12 += v * b5 - t13 += v * b6 - t14 += v * b7 - t15 += v * b8 - t16 += v * b9 - t17 += v * b10 - t18 += v * b11 - t19 += v * b12 - t20 += v * b13 - t21 += v * b14 - t22 += v * b15 - v = a[8] - t8 += v * b0 - t9 += v * b1 - t10 += v * b2 - t11 += v * b3 - t12 += v * b4 - t13 += v * b5 - t14 += v * b6 - t15 += v * b7 - t16 += v * b8 - t17 += v * b9 - t18 += v * b10 - t19 += v * b11 - t20 += v * b12 - t21 += v * b13 - t22 += v * b14 - t23 += v * b15 - v = a[9] - t9 += v * b0 - t10 += v * b1 - t11 += v * b2 - t12 += v * b3 - t13 += v * b4 - t14 += v * b5 - t15 += v * b6 - t16 += v * b7 - t17 += v * b8 - t18 += v * b9 - t19 += v * b10 - t20 += v * b11 - t21 += v * b12 - t22 += v * b13 - t23 += v * b14 - t24 += v * b15 - v = a[10] - t10 += v * b0 - t11 += v * b1 - t12 += v * b2 - t13 += v * b3 - t14 += v * b4 - t15 += v * b5 - t16 += v * b6 - t17 += v * b7 - t18 += v * b8 - t19 += v * b9 - t20 += v * b10 - t21 += v * b11 - t22 += v * b12 - t23 += v * b13 - t24 += v * b14 - t25 += v * b15 - v = a[11] - t11 += v * b0 - t12 += v * b1 - t13 += v * b2 - t14 += v * b3 - t15 += v * b4 - t16 += v * b5 - t17 += v * b6 - t18 += v * b7 - t19 += v * b8 - t20 += v * b9 - t21 += v * b10 - t22 += v * b11 - t23 += v * b12 - t24 += v * b13 - t25 += v * b14 - t26 += v * b15 - v = a[12] - t12 += v * b0 - t13 += v * b1 - t14 += v * b2 - t15 += v * b3 - t16 += v * b4 - t17 += v * b5 - t18 += v * b6 - t19 += v * b7 - t20 += v * b8 - t21 += v * b9 - t22 += v * b10 - t23 += v * b11 - t24 += v * b12 - t25 += v * b13 - t26 += v * b14 - t27 += v * b15 - v = a[13] - t13 += v * b0 - t14 += v * b1 - t15 += v * b2 - t16 += v * b3 - t17 += v * b4 - t18 += v * b5 - t19 += v * b6 - t20 += v * b7 - t21 += v * b8 - t22 += v * b9 - t23 += v * b10 - t24 += v * b11 - t25 += v * b12 - t26 += v * b13 - t27 += v * b14 - t28 += v * b15 - v = a[14] - t14 += v * b0 - t15 += v * b1 - t16 += v * b2 - t17 += v * b3 - t18 += v * b4 - t19 += v * b5 - t20 += v * b6 - t21 += v * b7 - t22 += v * b8 - t23 += v * b9 - t24 += v * b10 - t25 += v * b11 - t26 += v * b12 - t27 += v * b13 - t28 += v * b14 - t29 += v * b15 - v = a[15] - t15 += v * b0 - t16 += v * b1 - t17 += v * b2 - t18 += v * b3 - t19 += v * b4 - t20 += v * b5 - t21 += v * b6 - t22 += v * b7 - t23 += v * b8 - t24 += v * b9 - t25 += v * b10 - t26 += v * b11 - t27 += v * b12 - t28 += v * b13 - t29 += v * b14 - t30 += v * b15 - - t0 += 38 * t16 - t1 += 38 * t17 - t2 += 38 * t18 - t3 += 38 * t19 - t4 += 38 * t20 - t5 += 38 * t21 - t6 += 38 * t22 - t7 += 38 * t23 - t8 += 38 * t24 - t9 += 38 * t25 - t10 += 38 * t26 - t11 += 38 * t27 - t12 += 38 * t28 - t13 += 38 * t29 - t14 += 38 * t30 - - c = 1 - v = t0 + c + 65535; c = Math.floor(v / 65536); t0 = v - c * 65536 - v = t1 + c + 65535; c = Math.floor(v / 65536); t1 = v - c * 65536 - v = t2 + c + 65535; c = Math.floor(v / 65536); t2 = v - c * 65536 - v = t3 + c + 65535; c = Math.floor(v / 65536); t3 = v - c * 65536 - v = t4 + c + 65535; c = Math.floor(v / 65536); t4 = v - c * 65536 - v = t5 + c + 65535; c = Math.floor(v / 65536); t5 = v - c * 65536 - v = t6 + c + 65535; c = Math.floor(v / 65536); t6 = v - c * 65536 - v = t7 + c + 65535; c = Math.floor(v / 65536); t7 = v - c * 65536 - v = t8 + c + 65535; c = Math.floor(v / 65536); t8 = v - c * 65536 - v = t9 + c + 65535; c = Math.floor(v / 65536); t9 = v - c * 65536 - v = t10 + c + 65535; c = Math.floor(v / 65536); t10 = v - c * 65536 - v = t11 + c + 65535; c = Math.floor(v / 65536); t11 = v - c * 65536 - v = t12 + c + 65535; c = Math.floor(v / 65536); t12 = v - c * 65536 - v = t13 + c + 65535; c = Math.floor(v / 65536); t13 = v - c * 65536 - v = t14 + c + 65535; c = Math.floor(v / 65536); t14 = v - c * 65536 - v = t15 + c + 65535; c = Math.floor(v / 65536); t15 = v - c * 65536 - t0 += c - 1 + 37 * (c - 1) - - c = 1 - v = t0 + c + 65535; c = Math.floor(v / 65536); t0 = v - c * 65536 - v = t1 + c + 65535; c = Math.floor(v / 65536); t1 = v - c * 65536 - v = t2 + c + 65535; c = Math.floor(v / 65536); t2 = v - c * 65536 - v = t3 + c + 65535; c = Math.floor(v / 65536); t3 = v - c * 65536 - v = t4 + c + 65535; c = Math.floor(v / 65536); t4 = v - c * 65536 - v = t5 + c + 65535; c = Math.floor(v / 65536); t5 = v - c * 65536 - v = t6 + c + 65535; c = Math.floor(v / 65536); t6 = v - c * 65536 - v = t7 + c + 65535; c = Math.floor(v / 65536); t7 = v - c * 65536 - v = t8 + c + 65535; c = Math.floor(v / 65536); t8 = v - c * 65536 - v = t9 + c + 65535; c = Math.floor(v / 65536); t9 = v - c * 65536 - v = t10 + c + 65535; c = Math.floor(v / 65536); t10 = v - c * 65536 - v = t11 + c + 65535; c = Math.floor(v / 65536); t11 = v - c * 65536 - v = t12 + c + 65535; c = Math.floor(v / 65536); t12 = v - c * 65536 - v = t13 + c + 65535; c = Math.floor(v / 65536); t13 = v - c * 65536 - v = t14 + c + 65535; c = Math.floor(v / 65536); t14 = v - c * 65536 - v = t15 + c + 65535; c = Math.floor(v / 65536); t15 = v - c * 65536 - t0 += c - 1 + 37 * (c - 1) - - o[0] = t0 - o[1] = t1 - o[2] = t2 - o[3] = t3 - o[4] = t4 - o[5] = t5 - o[6] = t6 - o[7] = t7 - o[8] = t8 - o[9] = t9 - o[10] = t10 - o[11] = t11 - o[12] = t12 - o[13] = t13 - o[14] = t14 - o[15] = t15 - } - - coreSalsa20(o: Uint8Array, p: Uint8Array, k: Uint8Array, c: Uint8Array) { - const j0 = c[ 0] & 0xff | (c[ 1] & 0xff)<<8 | (c[ 2] & 0xff)<<16 | (c[ 3] & 0xff)<<24, - j1 = k[ 0] & 0xff | (k[ 1] & 0xff)<<8 | (k[ 2] & 0xff)<<16 | (k[ 3] & 0xff)<<24, - j2 = k[ 4] & 0xff | (k[ 5] & 0xff)<<8 | (k[ 6] & 0xff)<<16 | (k[ 7] & 0xff)<<24, - j3 = k[ 8] & 0xff | (k[ 9] & 0xff)<<8 | (k[10] & 0xff)<<16 | (k[11] & 0xff)<<24, - j4 = k[12] & 0xff | (k[13] & 0xff)<<8 | (k[14] & 0xff)<<16 | (k[15] & 0xff)<<24, - j5 = c[ 4] & 0xff | (c[ 5] & 0xff)<<8 | (c[ 6] & 0xff)<<16 | (c[ 7] & 0xff)<<24, - j6 = p[ 0] & 0xff | (p[ 1] & 0xff)<<8 | (p[ 2] & 0xff)<<16 | (p[ 3] & 0xff)<<24, - j7 = p[ 4] & 0xff | (p[ 5] & 0xff)<<8 | (p[ 6] & 0xff)<<16 | (p[ 7] & 0xff)<<24, - j8 = p[ 8] & 0xff | (p[ 9] & 0xff)<<8 | (p[10] & 0xff)<<16 | (p[11] & 0xff)<<24, - j9 = p[12] & 0xff | (p[13] & 0xff)<<8 | (p[14] & 0xff)<<16 | (p[15] & 0xff)<<24, - j10 = c[ 8] & 0xff | (c[ 9] & 0xff)<<8 | (c[10] & 0xff)<<16 | (c[11] & 0xff)<<24, - j11 = k[16] & 0xff | (k[17] & 0xff)<<8 | (k[18] & 0xff)<<16 | (k[19] & 0xff)<<24, - j12 = k[20] & 0xff | (k[21] & 0xff)<<8 | (k[22] & 0xff)<<16 | (k[23] & 0xff)<<24, - j13 = k[24] & 0xff | (k[25] & 0xff)<<8 | (k[26] & 0xff)<<16 | (k[27] & 0xff)<<24, - j14 = k[28] & 0xff | (k[29] & 0xff)<<8 | (k[30] & 0xff)<<16 | (k[31] & 0xff)<<24, - j15 = c[12] & 0xff | (c[13] & 0xff)<<8 | (c[14] & 0xff)<<16 | (c[15] & 0xff)<<24 - - let x0 = j0, x1 = j1, x2 = j2, x3 = j3, x4 = j4, x5 = j5, x6 = j6, x7 = j7, - x8 = j8, x9 = j9, x10 = j10, x11 = j11, x12 = j12, x13 = j13, x14 = j14, - x15 = j15, u - - for (let i = 0; i < 20; i += 2) { - u = x0 + x12 | 0 - x4 ^= u<<7 | u>>>(32-7) - u = x4 + x0 | 0 - x8 ^= u<<9 | u>>>(32-9) - u = x8 + x4 | 0 - x12 ^= u<<13 | u>>>(32-13) - u = x12 + x8 | 0 - x0 ^= u<<18 | u>>>(32-18) - - u = x5 + x1 | 0 - x9 ^= u<<7 | u>>>(32-7) - u = x9 + x5 | 0 - x13 ^= u<<9 | u>>>(32-9) - u = x13 + x9 | 0 - x1 ^= u<<13 | u>>>(32-13) - u = x1 + x13 | 0 - x5 ^= u<<18 | u>>>(32-18) - - u = x10 + x6 | 0 - x14 ^= u<<7 | u>>>(32-7) - u = x14 + x10 | 0 - x2 ^= u<<9 | u>>>(32-9) - u = x2 + x14 | 0 - x6 ^= u<<13 | u>>>(32-13) - u = x6 + x2 | 0 - x10 ^= u<<18 | u>>>(32-18) - - u = x15 + x11 | 0 - x3 ^= u<<7 | u>>>(32-7) - u = x3 + x15 | 0 - x7 ^= u<<9 | u>>>(32-9) - u = x7 + x3 | 0 - x11 ^= u<<13 | u>>>(32-13) - u = x11 + x7 | 0 - x15 ^= u<<18 | u>>>(32-18) - - u = x0 + x3 | 0 - x1 ^= u<<7 | u>>>(32-7) - u = x1 + x0 | 0 - x2 ^= u<<9 | u>>>(32-9) - u = x2 + x1 | 0 - x3 ^= u<<13 | u>>>(32-13) - u = x3 + x2 | 0 - x0 ^= u<<18 | u>>>(32-18) - - u = x5 + x4 | 0 - x6 ^= u<<7 | u>>>(32-7) - u = x6 + x5 | 0 - x7 ^= u<<9 | u>>>(32-9) - u = x7 + x6 | 0 - x4 ^= u<<13 | u>>>(32-13) - u = x4 + x7 | 0 - x5 ^= u<<18 | u>>>(32-18) - - u = x10 + x9 | 0 - x11 ^= u<<7 | u>>>(32-7) - u = x11 + x10 | 0 - x8 ^= u<<9 | u>>>(32-9) - u = x8 + x11 | 0 - x9 ^= u<<13 | u>>>(32-13) - u = x9 + x8 | 0 - x10 ^= u<<18 | u>>>(32-18) - - u = x15 + x14 | 0 - x12 ^= u<<7 | u>>>(32-7) - u = x12 + x15 | 0 - x13 ^= u<<9 | u>>>(32-9) - u = x13 + x12 | 0 - x14 ^= u<<13 | u>>>(32-13) - u = x14 + x13 | 0 - x15 ^= u<<18 | u>>>(32-18) - } - x0 = x0 + j0 | 0 - x1 = x1 + j1 | 0 - x2 = x2 + j2 | 0 - x3 = x3 + j3 | 0 - x4 = x4 + j4 | 0 - x5 = x5 + j5 | 0 - x6 = x6 + j6 | 0 - x7 = x7 + j7 | 0 - x8 = x8 + j8 | 0 - x9 = x9 + j9 | 0 - x10 = x10 + j10 | 0 - x11 = x11 + j11 | 0 - x12 = x12 + j12 | 0 - x13 = x13 + j13 | 0 - x14 = x14 + j14 | 0 - x15 = x15 + j15 | 0 - - o[ 0] = x0 >>> 0 & 0xff - o[ 1] = x0 >>> 8 & 0xff - o[ 2] = x0 >>> 16 & 0xff - o[ 3] = x0 >>> 24 & 0xff - - o[ 4] = x1 >>> 0 & 0xff - o[ 5] = x1 >>> 8 & 0xff - o[ 6] = x1 >>> 16 & 0xff - o[ 7] = x1 >>> 24 & 0xff - - o[ 8] = x2 >>> 0 & 0xff - o[ 9] = x2 >>> 8 & 0xff - o[10] = x2 >>> 16 & 0xff - o[11] = x2 >>> 24 & 0xff - - o[12] = x3 >>> 0 & 0xff - o[13] = x3 >>> 8 & 0xff - o[14] = x3 >>> 16 & 0xff - o[15] = x3 >>> 24 & 0xff - - o[16] = x4 >>> 0 & 0xff - o[17] = x4 >>> 8 & 0xff - o[18] = x4 >>> 16 & 0xff - o[19] = x4 >>> 24 & 0xff - - o[20] = x5 >>> 0 & 0xff - o[21] = x5 >>> 8 & 0xff - o[22] = x5 >>> 16 & 0xff - o[23] = x5 >>> 24 & 0xff - - o[24] = x6 >>> 0 & 0xff - o[25] = x6 >>> 8 & 0xff - o[26] = x6 >>> 16 & 0xff - o[27] = x6 >>> 24 & 0xff - - o[28] = x7 >>> 0 & 0xff - o[29] = x7 >>> 8 & 0xff - o[30] = x7 >>> 16 & 0xff - o[31] = x7 >>> 24 & 0xff - - o[32] = x8 >>> 0 & 0xff - o[33] = x8 >>> 8 & 0xff - o[34] = x8 >>> 16 & 0xff - o[35] = x8 >>> 24 & 0xff - - o[36] = x9 >>> 0 & 0xff - o[37] = x9 >>> 8 & 0xff - o[38] = x9 >>> 16 & 0xff - o[39] = x9 >>> 24 & 0xff - - o[40] = x10 >>> 0 & 0xff - o[41] = x10 >>> 8 & 0xff - o[42] = x10 >>> 16 & 0xff - o[43] = x10 >>> 24 & 0xff - - o[44] = x11 >>> 0 & 0xff - o[45] = x11 >>> 8 & 0xff - o[46] = x11 >>> 16 & 0xff - o[47] = x11 >>> 24 & 0xff - - o[48] = x12 >>> 0 & 0xff - o[49] = x12 >>> 8 & 0xff - o[50] = x12 >>> 16 & 0xff - o[51] = x12 >>> 24 & 0xff - - o[52] = x13 >>> 0 & 0xff - o[53] = x13 >>> 8 & 0xff - o[54] = x13 >>> 16 & 0xff - o[55] = x13 >>> 24 & 0xff - - o[56] = x14 >>> 0 & 0xff - o[57] = x14 >>> 8 & 0xff - o[58] = x14 >>> 16 & 0xff - o[59] = x14 >>> 24 & 0xff - - o[60] = x15 >>> 0 & 0xff - o[61] = x15 >>> 8 & 0xff - o[62] = x15 >>> 16 & 0xff - o[63] = x15 >>> 24 & 0xff - } - - coreHsalsa20(o: Uint8Array, p: Uint8Array, k: Uint8Array, c: Uint8Array) { - const j0 = c[ 0] & 0xff | (c[ 1] & 0xff)<<8 | (c[ 2] & 0xff)<<16 | (c[ 3] & 0xff)<<24, - j1 = k[ 0] & 0xff | (k[ 1] & 0xff)<<8 | (k[ 2] & 0xff)<<16 | (k[ 3] & 0xff)<<24, - j2 = k[ 4] & 0xff | (k[ 5] & 0xff)<<8 | (k[ 6] & 0xff)<<16 | (k[ 7] & 0xff)<<24, - j3 = k[ 8] & 0xff | (k[ 9] & 0xff)<<8 | (k[10] & 0xff)<<16 | (k[11] & 0xff)<<24, - j4 = k[12] & 0xff | (k[13] & 0xff)<<8 | (k[14] & 0xff)<<16 | (k[15] & 0xff)<<24, - j5 = c[ 4] & 0xff | (c[ 5] & 0xff)<<8 | (c[ 6] & 0xff)<<16 | (c[ 7] & 0xff)<<24, - j6 = p[ 0] & 0xff | (p[ 1] & 0xff)<<8 | (p[ 2] & 0xff)<<16 | (p[ 3] & 0xff)<<24, - j7 = p[ 4] & 0xff | (p[ 5] & 0xff)<<8 | (p[ 6] & 0xff)<<16 | (p[ 7] & 0xff)<<24, - j8 = p[ 8] & 0xff | (p[ 9] & 0xff)<<8 | (p[10] & 0xff)<<16 | (p[11] & 0xff)<<24, - j9 = p[12] & 0xff | (p[13] & 0xff)<<8 | (p[14] & 0xff)<<16 | (p[15] & 0xff)<<24, - j10 = c[ 8] & 0xff | (c[ 9] & 0xff)<<8 | (c[10] & 0xff)<<16 | (c[11] & 0xff)<<24, - j11 = k[16] & 0xff | (k[17] & 0xff)<<8 | (k[18] & 0xff)<<16 | (k[19] & 0xff)<<24, - j12 = k[20] & 0xff | (k[21] & 0xff)<<8 | (k[22] & 0xff)<<16 | (k[23] & 0xff)<<24, - j13 = k[24] & 0xff | (k[25] & 0xff)<<8 | (k[26] & 0xff)<<16 | (k[27] & 0xff)<<24, - j14 = k[28] & 0xff | (k[29] & 0xff)<<8 | (k[30] & 0xff)<<16 | (k[31] & 0xff)<<24, - j15 = c[12] & 0xff | (c[13] & 0xff)<<8 | (c[14] & 0xff)<<16 | (c[15] & 0xff)<<24 - - let x0 = j0, x1 = j1, x2 = j2, x3 = j3, x4 = j4, x5 = j5, x6 = j6, x7 = j7, - x8 = j8, x9 = j9, x10 = j10, x11 = j11, x12 = j12, x13 = j13, x14 = j14, - x15 = j15, u - - for (let i = 0; i < 20; i += 2) { - u = x0 + x12 | 0 - x4 ^= u<<7 | u>>>(32-7) - u = x4 + x0 | 0 - x8 ^= u<<9 | u>>>(32-9) - u = x8 + x4 | 0 - x12 ^= u<<13 | u>>>(32-13) - u = x12 + x8 | 0 - x0 ^= u<<18 | u>>>(32-18) - - u = x5 + x1 | 0 - x9 ^= u<<7 | u>>>(32-7) - u = x9 + x5 | 0 - x13 ^= u<<9 | u>>>(32-9) - u = x13 + x9 | 0 - x1 ^= u<<13 | u>>>(32-13) - u = x1 + x13 | 0 - x5 ^= u<<18 | u>>>(32-18) - - u = x10 + x6 | 0 - x14 ^= u<<7 | u>>>(32-7) - u = x14 + x10 | 0 - x2 ^= u<<9 | u>>>(32-9) - u = x2 + x14 | 0 - x6 ^= u<<13 | u>>>(32-13) - u = x6 + x2 | 0 - x10 ^= u<<18 | u>>>(32-18) - - u = x15 + x11 | 0 - x3 ^= u<<7 | u>>>(32-7) - u = x3 + x15 | 0 - x7 ^= u<<9 | u>>>(32-9) - u = x7 + x3 | 0 - x11 ^= u<<13 | u>>>(32-13) - u = x11 + x7 | 0 - x15 ^= u<<18 | u>>>(32-18) - - u = x0 + x3 | 0 - x1 ^= u<<7 | u>>>(32-7) - u = x1 + x0 | 0 - x2 ^= u<<9 | u>>>(32-9) - u = x2 + x1 | 0 - x3 ^= u<<13 | u>>>(32-13) - u = x3 + x2 | 0 - x0 ^= u<<18 | u>>>(32-18) - - u = x5 + x4 | 0 - x6 ^= u<<7 | u>>>(32-7) - u = x6 + x5 | 0 - x7 ^= u<<9 | u>>>(32-9) - u = x7 + x6 | 0 - x4 ^= u<<13 | u>>>(32-13) - u = x4 + x7 | 0 - x5 ^= u<<18 | u>>>(32-18) - - u = x10 + x9 | 0 - x11 ^= u<<7 | u>>>(32-7) - u = x11 + x10 | 0 - x8 ^= u<<9 | u>>>(32-9) - u = x8 + x11 | 0 - x9 ^= u<<13 | u>>>(32-13) - u = x9 + x8 | 0 - x10 ^= u<<18 | u>>>(32-18) - - u = x15 + x14 | 0 - x12 ^= u<<7 | u>>>(32-7) - u = x12 + x15 | 0 - x13 ^= u<<9 | u>>>(32-9) - u = x13 + x12 | 0 - x14 ^= u<<13 | u>>>(32-13) - u = x14 + x13 | 0 - x15 ^= u<<18 | u>>>(32-18) - } - - o[ 0] = x0 >>> 0 & 0xff - o[ 1] = x0 >>> 8 & 0xff - o[ 2] = x0 >>> 16 & 0xff - o[ 3] = x0 >>> 24 & 0xff - - o[ 4] = x5 >>> 0 & 0xff - o[ 5] = x5 >>> 8 & 0xff - o[ 6] = x5 >>> 16 & 0xff - o[ 7] = x5 >>> 24 & 0xff - - o[ 8] = x10 >>> 0 & 0xff - o[ 9] = x10 >>> 8 & 0xff - o[10] = x10 >>> 16 & 0xff - o[11] = x10 >>> 24 & 0xff - - o[12] = x15 >>> 0 & 0xff - o[13] = x15 >>> 8 & 0xff - o[14] = x15 >>> 16 & 0xff - o[15] = x15 >>> 24 & 0xff - - o[16] = x6 >>> 0 & 0xff - o[17] = x6 >>> 8 & 0xff - o[18] = x6 >>> 16 & 0xff - o[19] = x6 >>> 24 & 0xff - - o[20] = x7 >>> 0 & 0xff - o[21] = x7 >>> 8 & 0xff - o[22] = x7 >>> 16 & 0xff - o[23] = x7 >>> 24 & 0xff - - o[24] = x8 >>> 0 & 0xff - o[25] = x8 >>> 8 & 0xff - o[26] = x8 >>> 16 & 0xff - o[27] = x8 >>> 24 & 0xff - - o[28] = x9 >>> 0 & 0xff - o[29] = x9 >>> 8 & 0xff - o[30] = x9 >>> 16 & 0xff - o[31] = x9 >>> 24 & 0xff - } - - S(o: Int32Array, a: Int32Array): void { - this.M(o, a, a) - } - - add(p: Int32Array[], q: Int32Array[]): void { - const a = this.gf(), b = this.gf(), c = this.gf(), - d = this.gf(), e = this.gf(), f = this.gf(), - g = this.gf(), h = this.gf(), t = this.gf() - - this.Z(a, p[1], p[0]) - this.Z(t, q[1], q[0]) - this.M(a, a, t) - this.A(b, p[0], p[1]) - this.A(t, q[0], q[1]) - this.M(b, b, t) - this.M(c, p[3], q[3]) - this.M(c, c, this.D2) - this.M(d, p[2], q[2]) - this.A(d, d, d) - this.Z(e, b, a) - this.Z(f, d, c) - this.A(g, d, c) - this.A(h, b, a) - this.M(p[0], e, f) - this.M(p[1], h, g) - this.M(p[2], g, f) - this.M(p[3], e, h) - } - - set25519(r: Int32Array, a: Int32Array): void { - for (let i = 0; i < 16; i++) { - r[i] = a[i] - } - } - - car25519(o: Int32Array): void { - let i, v, c = 1 - for (i = 0; i < 16; i++) { - v = o[i] + c + 65535 - c = Math.floor(v / 65536) - o[i] = v - c * 65536 - } - - o[0] += c - 1 + 37 * (c - 1) - } - - // b is 0 or 1 - sel25519(p: Int32Array, q: Int32Array, b: number): void { - let i, t - const c = ~(b - 1) - for (i = 0; i < 16; i++) { - t = c & (p[i] ^ q[i]) - p[i] ^= t - q[i] ^= t - } - } - - inv25519(o: Int32Array, i: Int32Array): void { - let a - const c = this.gf() - for (a = 0; a < 16; a++) { - c[a] = i[a] - } - - for (a = 253; a >= 0; a--) { - this.S(c, c) - if (a !== 2 && a !== 4) { - this.M(c, c, i) - } - } - - for (a = 0; a < 16; a++) { - o[a] = c[a] - } - } - - neq25519(a: Int32Array, b: Int32Array): boolean { - const c = new Uint8Array(32), d = new Uint8Array(32) - this.pack25519(c, a) - this.pack25519(d, b) - return !Util.compare(c, d) - } - - par25519(a: Int32Array): number { - const d = new Uint8Array(32) - this.pack25519(d, a) - return d[0] & 1 - } - - pow2523(o: Int32Array, i: Int32Array): void { - let a - const c = this.gf() - for (a = 0; a < 16; a++) { - c[a] = i[a] - } - - for (a = 250; a >= 0; a--) { - this.S(c, c) - if (a !== 1) this.M(c, c, i) - } - - for (a = 0; a < 16; a++) { - o[a] = c[a] - } - } - - cswap(p: Int32Array[], q: Int32Array[], b: number): void { - for (let i = 0; i < 4; i++) { - this.sel25519(p[i], q[i], b) - } - } - - pack25519(o: Uint8Array, n: Int32Array): void { - let i - const m = this.gf() - const t = this.gf() - for (i = 0; i < 16; i++) { - t[i] = n[i] - } - - this.car25519(t) - this.car25519(t) - this.car25519(t) - for (let j = 0; j < 2; j++) { - m[0] = t[0] - 0xffed - for (i = 1; i < 15; i++) { - m[i] = t[i] - 0xffff - ((m[i - 1] >>> 16) & 1) - m[i - 1] &= 0xffff - } - - m[15] = t[15] - 0x7fff - ((m[14] >>> 16) & 1) - const b = (m[15] >>> 16) & 1 - m[14] &= 0xffff - this.sel25519(t, m, 1 - b) - } - - for (i = 0; i < 16; i++) { - o[2 * i] = t[i] & 0xff - o[2 * i + 1] = t[i] >>> 8 - } - } - - unpack25519(o: Int32Array, n: Uint8Array): void { - for (let i = 0; i < 16; i++) { - o[i] = n[2 * i] + (n[2 * i + 1] << 8) - } - - o[15] &= 0x7fff - } - - unpackNeg(r: Int32Array[], p: Uint8Array): number { - const t = this.gf(), - chk = this.gf(), - num = this.gf(), - den = this.gf(), - den2 = this.gf(), - den4 = this.gf(), - den6 = this.gf() - - this.set25519(r[2], this.gf1) - this.unpack25519(r[1], p) - this.S(num, r[1]) - this.M(den, num, this.D) - this.Z(num, num, r[2]) - this.A(den, r[2], den) - - this.S(den2, den) - this.S(den4, den2) - this.M(den6, den4, den2) - this.M(t, den6, num) - this.M(t, t, den) - - this.pow2523(t, t) - this.M(t, t, num) - this.M(t, t, den) - this.M(t, t, den) - this.M(r[0], t, den) - - this.S(chk, r[0]) - this.M(chk, chk, den) - if (this.neq25519(chk, num)) { - this.M(r[0], r[0], this.I) - } - - this.S(chk, r[0]) - this.M(chk, chk, den) - if (this.neq25519(chk, num)) { - return -1 - } - - if (this.par25519(r[0]) === (p[31] >>> 7)) { - this.Z(r[0], this.gf0, r[0]) - } - - this.M(r[3], r[0], r[1]) - - return 0 - } - - vn(x: Uint8Array, xi: number, y: Uint8Array, yi: number, n: number) { - let i, d = 0 - for (i = 0; i < n; i++) { - d |= x[xi+i]^y[yi+i] - } - return (1 & ((d - 1) >>> 8)) - 1 - } - - /** - * Internal scalar mult function - * @param {Uint8Array} q Result - * @param {Uint8Array} s Secret key - * @param {Uint8Array} p Public key - */ - cryptoScalarmult(q: Uint8Array, s: Uint8Array, p: Uint8Array): void { - const x = new Int32Array(80) - let r, i - const a = this.gf(), b = this.gf(), c = this.gf(), - d = this.gf(), e = this.gf(), f = this.gf() - - this.unpack25519(x, p) - for (i = 0; i < 16; i++) { - b[i] = x[i] - d[i] = a[i] = c[i] = 0 - } - - a[0] = d[0] = 1 - for (i = 254; i >= 0; --i) { - r = (s[i >>> 3] >>> (i & 7)) & 1 - this.sel25519(a, b, r) - this.sel25519(c, d, r) - this.A(e, a, c) - this.Z(a, a, c) - this.A(c, b, d) - this.Z(b, b, d) - this.S(d, e) - this.S(f, a) - this.M(a, c, a) - this.M(c, b, e) - this.A(e, a, c) - this.Z(a, a, c) - this.S(b, a) - this.Z(c, d, f) - this.M(a, c, this._121665) - this.A(a, a, d) - this.M(c, c, a) - this.M(a, d, f) - this.M(d, b, x) - this.S(b, e) - this.sel25519(a, b, r) - this.sel25519(c, d, r) - } - - for (i = 0; i < 16; i++) { - x[i + 16] = a[i] - x[i + 32] = c[i] - x[i + 48] = b[i] - x[i + 64] = d[i] - } - - const x32 = x.subarray(32) - const x16 = x.subarray(16) - this.inv25519(x32, x32) - this.M(x16, x16, x32) - this.pack25519(q, x16) - } - - cryptoStreamSalsa20Xor(c: Uint8Array, cpos: number, m: Uint8Array, mpos: number, b: number, n: Uint8Array, k: Uint8Array) { - const z = new Uint8Array(16) - const x = new Uint8Array(64) - let u, i - for (i = 0; i < 16; i++) { - z[i] = 0 - } - for (i = 0; i < 8; i++) { - z[i] = n[i] - } - while (b >= 64) { - this.coreSalsa20(x, z, k, this.sigma) - for (i = 0; i < 64; i++) c[cpos+i] = m[mpos+i] ^ x[i] - u = 1 - for (i = 8; i < 16; i++) { - u = u + (z[i] & 0xff) | 0 - z[i] = u & 0xff - u >>>= 8 - } - b -= 64 - cpos += 64 - mpos += 64 - } - if (b > 0) { - this.coreSalsa20(x, z, k, this.sigma) - for (i = 0; i < b; i++) { - c[cpos+i] = m[mpos+i] ^ x[i] - } - } - return 0 - } - - cryptoStreamSalsa20(c: Uint8Array, cpos: number, b: number, n: Uint8Array, k: Uint8Array) { - const z = new Uint8Array(16), x = new Uint8Array(64) - let u, i - for (i = 0; i < 16; i++) z[i] = 0 - for (i = 0; i < 8; i++) z[i] = n[i] - while (b >= 64) { - this.coreSalsa20(x, z, k, this.sigma) - for (i = 0; i < 64; i++) { - c[cpos+i] = x[i] - } - u = 1 - for (i = 8; i < 16; i++) { - u = u + (z[i] & 0xff) | 0 - z[i] = u & 0xff - u >>>= 8 - } - b -= 64 - cpos += 64 - } - if (b > 0) { - this.coreSalsa20(x, z, k, this.sigma) - for (i = 0; i < b; i++) { - c[cpos+i] = x[i] - } - } - return 0 - } - - add1305(h: Uint32Array, c: Uint32Array) { - let j, u = 0 - for (j = 0; j < 17; j++) { - u = (u + ((h[j] + c[j]) | 0)) | 0 - h[j] = u & 255 - u >>>= 8 - } - } - - cryptoOnetimeauth(out: Uint8Array, outpos: number, m: Uint8Array, mpos: number, n: number, k: Uint8Array) { - let s, i, j, u - const x = new Uint32Array(17), r = new Uint32Array(17), - h = new Uint32Array(17), c = new Uint32Array(17), - g = new Uint32Array(17) - for (j = 0; j < 17; j++) { - r[j]=h[j]=0 - } - for (j = 0; j < 16; j++) { - r[j]=k[j] - } - - r[3]&=15 - r[4]&=252 - r[7]&=15 - r[8]&=252 - r[11]&=15 - r[12]&=252 - r[15]&=15 - - while (n > 0) { - for (j = 0; j < 17; j++) { - c[j] = 0 - } - for (j = 0; (j < 16) && (j < n); ++j) { - c[j] = m[mpos+j] - } - c[j] = 1 - mpos += j; n -= j - this.add1305(h, c) - for (i = 0; i < 17; i++) { - x[i] = 0 - for (j = 0; j < 17; j++) { - x[i] = (x[i] + (h[j] * ((j <= i) ? r[i - j] : ((320 * r[i + 17 - j])|0))) | 0) | 0 - } - } - for (i = 0; i < 17; i++) { - h[i] = x[i] - } - u = 0 - for (j = 0; j < 16; j++) { - u = (u + h[j]) | 0 - h[j] = u & 255 - u >>>= 8 - } - u = (u + h[16]) | 0; h[16] = u & 3 - u = (5 * (u >>> 2)) | 0 - for (j = 0; j < 16; j++) { - u = (u + h[j]) | 0 - h[j] = u & 255 - u >>>= 8 - } - u = (u + h[16]) | 0; h[16] = u - } - - for (j = 0; j < 17; j++) { - g[j] = h[j] - } - this.add1305(h, this.minusp) - s = (-(h[16] >>> 7) | 0) - for (j = 0; j < 17; j++) { - h[j] ^= s & (g[j] ^ h[j]) - } - - for (j = 0; j < 16; j++) { - c[j] = k[j + 16] - } - c[16] = 0 - this.add1305(h, c) - for (j = 0; j < 16; j++) { - out[outpos+j] = h[j] - } - return 0 - } - - cryptoOnetimeauthVerify(h: Uint8Array, hpos: number, m: Uint8Array, mpos: number, n: number, k: Uint8Array) { - const x = new Uint8Array(16) - this.cryptoOnetimeauth(x, 0, m, mpos, n, k) - return this.cryptoVerify16(h, hpos, x, 0) - } - - cryptoVerify16(x: Uint8Array, xi: number, y: Uint8Array, yi: number) { - return this.vn(x, xi, y, yi, 16) - } - - cryptoBoxBeforenm(k: Uint8Array, y: Uint8Array, x: Uint8Array) { - const s = new Uint8Array(32) - this.cryptoScalarmult(s, x, y) - return this.coreHsalsa20(k, this._0, s, this.sigma) - } - - cryptoSecretbox(c: Uint8Array, m: Uint8Array, d: number, n: Uint8Array, k: Uint8Array) { - let i - if (d < 32) { - return -1 - } - this.cryptoStreamXor(c, 0, m, 0, d, n, k) - this.cryptoOnetimeauth(c, 16, c, 32, d - 32, c) - for (i = 0; i < 16; i++) { - c[i] = 0 - } - return 0 - } - - cryptoSecretboxOpen(m: Uint8Array, c: Uint8Array, d: number, n: Uint8Array, k: Uint8Array) { - let i - const x = new Uint8Array(32) - if (d < 32) { - return -1 - } - this.cryptoStream(x, 0, 32, n, k) - if (this.cryptoOnetimeauthVerify(c, 16, c, 32, d - 32, x) !== 0) { - return -1 - } - this.cryptoStreamXor(m, 0, c, 0, d, n, k) - for (i = 0; i < 32; i++) { - m[i] = 0 - } - return 0 - } - - cryptoStream(c: Uint8Array, cpos: number, d: number, n: Uint8Array, k: Uint8Array) { - const s = new Uint8Array(32) - this.coreHsalsa20(s, n, k, this.sigma) - const sn = new Uint8Array(8) - for (var i = 0; i < 8; i++) { - sn[i] = n[i+16] - } - return this.cryptoStreamSalsa20(c, cpos, d, sn, s) - } - - cryptoStreamXor(c: Uint8Array, cpos: number, m: Uint8Array, mpos: number, d: number, n: Uint8Array, k: Uint8Array) { - const s = new Uint8Array(32) - this.coreHsalsa20(s, n, k, this.sigma) - const sn = new Uint8Array(8) - for (var i = 0; i < 8; i++) { - sn[i] = n[i+16] - } - return this.cryptoStreamSalsa20Xor(c, cpos, m, mpos, d, sn, s) - } - - checkLengths(k: Uint8Array, n: Uint8Array) { - if (k.length !== 32) { - throw new Error('bad key size') - } - if (n.length !== 24) { - throw new Error('bad nonce size') - } - } - - checkBoxLengths(pk: Uint8Array, sk: Uint8Array) { - if (pk.length !== 32) { - throw new Error('bad public key size') - } - if (sk.length !== 32) { - throw new Error('bad secret key size') - } - } - - checkArrayTypes(...params: any) { - for (let i = 0; i < params.length; i++) { - if (!(params[i] instanceof Uint8Array)) { - throw new TypeError('unexpected type, use Uint8Array') - } - } - } - - secretbox(msg: Uint8Array, nonce: Uint8Array, key: Uint8Array) { - this.checkArrayTypes(msg, nonce, key) - this.checkLengths(key, nonce) - const m = new Uint8Array(32 + msg.length) - const c = new Uint8Array(m.length) - for (let i = 0; i < msg.length; i++) { - m[i + 32] = msg[i] - } - this.cryptoSecretbox(c, m, m.length, nonce, key) - return c.subarray(16) - } - - secretboxOpen(box: Uint8Array, nonce: Uint8Array, key: Uint8Array) { - this.checkArrayTypes(box, nonce, key) - this.checkLengths(key, nonce) - const c = new Uint8Array(16 + box.length) - const m = new Uint8Array(c.length) - for (let i = 0; i < box.length; i++) { - c[i+16] = box[i] - } - if (c.length < 32) { - return null - } - if (this.cryptoSecretboxOpen(m, c, c.length, nonce, key) !== 0) { - return null - } - return m.subarray(32) - } - - box(msg: Uint8Array, nonce: Uint8Array, publicKey: Uint8Array, secretKey: Uint8Array) { - const k = this.boxBefore(publicKey, secretKey) - return this.secretbox(msg, nonce, k) - } - - boxOpen(msg: Uint8Array, nonce: Uint8Array, publicKey: Uint8Array, secretKey: Uint8Array) { - const k = this.boxBefore(publicKey, secretKey) - return this.secretboxOpen(msg, nonce, k) - } - - boxBefore(publicKey: Uint8Array, secretKey: Uint8Array) { - this.checkArrayTypes(publicKey, secretKey) - this.checkBoxLengths(publicKey, secretKey) - const k = new Uint8Array(32) - this.cryptoBoxBeforenm(k, publicKey, secretKey) - return k - } - - /** - * Generate the common key as the produkt of sk1 * pk2 - * @param {Uint8Array} sk A 32 byte secret key of pair 1 - * @param {Uint8Array} pk A 32 byte public key of pair 2 - * @return {Uint8Array} sk * pk - */ - scalarMult(sk: Uint8Array, pk: Uint8Array) { - const q = new Uint8Array(32) - this.cryptoScalarmult(q, sk, pk) - - return q - } - - /** - * Generate a curve 25519 keypair - * @param {Uint8Array} seed A 32 byte cryptographic secure random array. This is basically the secret key - * @param {Object} Returns sk (Secret key) and pk (Public key) as 32 byte typed arrays - */ - generateKeys(seed: Uint8Array): { sk: Uint8Array, pk: Uint8Array } { - const sk = seed.slice() - const pk = new Uint8Array(32) - if (sk.length !== 32) { - throw new Error('Invalid secret key size, expected 32 bytes') - } - - sk[0] &= 0xf8 - sk[31] &= 0x7f - sk[31] |= 0x40 - - this.cryptoScalarmult(pk, sk, this._9) - - return { - sk, - pk, - } - } - - /** - * Converts a ed25519 public key to Curve25519 to be used in - * Diffie-Hellman key exchange - */ - convertEd25519PublicKeyToCurve25519(pk: Uint8Array) { - const z = new Uint8Array(32) - const q = [this.gf(), this.gf(), this.gf(), this.gf()] - const a = this.gf() - const b = this.gf() - - if (this.unpackNeg(q, pk)) { - return null - } - - const y = q[1] - - this.A(a, this.gf1, y) - this.Z(b, this.gf1, y) - this.inv25519(b, b) - this.M(a, a, b) - - this.pack25519(z, a) - - return z - } - - /** - * Converts a ed25519 secret key to Curve25519 to be used in - * Diffie-Hellman key exchange - */ - convertEd25519SecretKeyToCurve25519(sk: Uint8Array) { - const d = new Uint8Array(64) - const o = new Uint8Array(32) - let i - - this.cryptoHash(d, sk, 32) - d[0] &= 248 - d[31] &= 127 - d[31] |= 64 - - for (i = 0; i < 32; i++) { - o[i] = d[i] - } - - for (i = 0; i < 64; i++) { - d[i] = 0 - } - - return o - } - - cryptoHash(out: Uint8Array, m: Uint8Array, n: number): number { - const input = new Uint8Array(n) - for (let i = 0; i < n; ++i) { - input[i] = m[i] - } - - const hash = blake2b(input) - for (let i = 0; i < 64; ++i) { - out[i] = hash[i] - } - - return 0 - } - -} diff --git a/lib/util/util.ts b/lib/util/util.ts deleted file mode 100644 index 439e158..0000000 --- a/lib/util/util.ts +++ /dev/null @@ -1,30 +0,0 @@ -export default class Util { - - /** - * Time constant comparison of two arrays - * - * @param {Uint8Array} lh First array of bytes - * @param {Uint8Array} rh Second array of bytes - * @return {Boolean} True if the arrays are equal (length and content), false otherwise - */ - static compare = (lh: Uint8Array, rh: Uint8Array): boolean => { - if (lh.length !== rh.length) { - return false - } - - let i - let d = 0 - const len = lh.length - - for (i = 0; i < len; i++) { - d |= lh[i] ^ rh[i] - } - - return d === 0 - } - - static normalizeUTF8 = (str: string): string => { - return str ? str.normalize('NFKD') : '' - } - -} diff --git a/package-lock.json b/package-lock.json index c47972d..0b8d14f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,2477 +1,660 @@ { - "name": "nanocurrency-web", - "version": "1.4.3", + "name": "libnemo", + "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "nanocurrency-web", - "version": "1.4.3", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.1.2", - "blakejs": "^1.2.1", - "byte-base64": "^1.1.0", - "crypto-js": "^4.2.0" - }, - "devDependencies": { - "chai": "^5.1.1", - "mocha": "^10.7.0", - "ts-loader": "^9.5.1", - "typescript": "^5.5.4", - "webpack": "^5.93.0", - "webpack-cli": "^5.1.4" - } - }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@types/eslint": { - "version": "8.56.10", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", - "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.14.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", - "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", - "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", - "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.12.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", - "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-opt": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1", - "@webassemblyjs/wast-printer": "1.12.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", - "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", - "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", - "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", - "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webpack-cli/configtest": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", - "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - } - }, - "node_modules/@webpack-cli/info": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", - "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - } - }, - "node_modules/@webpack-cli/serve": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", - "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - }, - "peerDependenciesMeta": { - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/acorn": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", - "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^8" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/bignumber.js": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", - "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/blakejs": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", - "integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==", - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true, - "license": "ISC" - }, - "node_modules/browserslist": { - "version": "4.23.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", - "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001629", - "electron-to-chromium": "^1.4.796", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.16" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/byte-base64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/byte-base64/-/byte-base64-1.1.0.tgz", - "integrity": "sha512-56cXelkJrVMdCY9V/3RfDxTh4VfMFCQ5km7B7GkIGfo4bcPL9aACyJLB0Ms3Ezu5rsHmLB2suis96z4fLM03DA==", - "license": "MIT" - }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001636", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz", - "integrity": "sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chai": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", - "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/check-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, - "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, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "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, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "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, - "license": "MIT" - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/crypto-js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", - "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/debug/node_modules/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, - "license": "MIT" - }, - "node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.4.803", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.803.tgz", - "integrity": "sha512-61H9mLzGOCLLVsnLiRzCbc63uldP0AniRYPV3hbGVtONA1pI7qSGILdbofR7A8TMbOypDocEAjH/e+9k1QIe3g==", - "dev": true, - "license": "ISC" - }, - "node_modules/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, - "license": "MIT" - }, - "node_modules/enhanced-resolve": { - "version": "5.17.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", - "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/envinfo": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz", - "integrity": "sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==", - "dev": true, - "license": "MIT", - "bin": { - "envinfo": "dist/cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/es-module-lexer": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.3.tgz", - "integrity": "sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg==", - "dev": true, - "license": "MIT" - }, - "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/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, - "license": "MIT" - }, - "node_modules/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, - "license": "MIT" - }, - "node_modules/fastest-levenshtein": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", - "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.9.1" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "license": "BSD-3-Clause", - "bin": { - "flat": "cli.js" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/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, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, - "node_modules/import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/interpret": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/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, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/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, - "license": "MIT" - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.11.5" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/loupe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", - "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.1" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mocha": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.7.0.tgz", - "integrity": "sha512-v8/rBWr2VO5YkspYINnvu81inSz2y3ODJrhO175/Exzor1RcEZZkizgE2A+w/CAXXoESS8Kys5E62dOHGHzULA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-colors": "^4.1.3", - "browser-stdout": "^1.3.1", - "chokidar": "^3.5.3", - "debug": "^4.3.5", - "diff": "^5.2.0", - "escape-string-regexp": "^4.0.0", - "find-up": "^5.0.0", - "glob": "^8.1.0", - "he": "^1.2.0", - "js-yaml": "^4.1.0", - "log-symbols": "^4.1.0", - "minimatch": "^5.1.6", - "ms": "^2.1.3", - "serialize-javascript": "^6.0.2", - "strip-json-comments": "^3.1.1", - "supports-color": "^8.1.1", - "workerpool": "^6.5.1", - "yargs": "^16.2.0", - "yargs-parser": "^20.2.9", - "yargs-unparser": "^2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha.js" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathval": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", - "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, - "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", + "name": "libnemo", + "version": "0.0.1", + "license": "(GPL-3.0-or-later AND MIT)", "dependencies": { - "p-try": "^2.0.0" + "@ledgerhq/hw-transport-webusb": "6.29.4", + "blakejs": "^1.2.1" }, - "engines": { - "node": ">=6" + "devDependencies": { + "@types/node": "^22.7.4", + "@types/w3c-web-hid": "^1.0.6", + "@types/w3c-web-usb": "^1.0.10", + "@types/web-bluetooth": "^0.0.20", + "esbuild": "^0.24.0", + "typescript": "^5.6.2" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" + "type": "nano", + "url": "nano:nano_1zosoqs47yt47bnfg7sdf46kj7asn58b7uzm9ek95jw7ccatq37898u1zoso" }, - "engines": { - "node": ">=8" + "optionalDependencies": { + "@ledgerhq/hw-transport-web-ble": "^6.29.4", + "@ledgerhq/hw-transport-webhid": "^6.29.4", + "@ledgerhq/hw-transport-webusb": "^6.29.4" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">=6" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" + "node": ">=18" } }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "node_modules/@esbuild/android-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=8.10.0" + "node": ">=18" } }, - "node_modules/rechoir": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "node_modules/@esbuild/android-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "resolve": "^1.20.0" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">= 10.13.0" + "node": ">=18" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "node_modules/@esbuild/android-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=18" } }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/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==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", + "cpu": [ + "arm64" ], - "license": "MIT" - }, - "node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=18" } }, - "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=10" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" + "node": ">=18" } }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "node_modules/@esbuild/linux-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "kind-of": "^6.0.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", + "cpu": [ + "loong64" + ], "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", + "cpu": [ + "mips64el" + ], "dev": true, "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", + "cpu": [ + "riscv64" + ], "dev": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/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==", + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", + "cpu": [ + "s390x" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" } }, - "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "node_modules/@esbuild/linux-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "node": ">=18" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=18" } }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=6" + "node": ">=18" } }, - "node_modules/terser": { - "version": "5.31.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.1.tgz", - "integrity": "sha512-37upzU1+viGvuFtBo9NPufCb9dwM0+l9hMxYyWfBA+fbwrPqNJAhbZ6W47bBFnZHKHTUBnMvi87434qq+qnxOg==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", + "cpu": [ + "x64" + ], "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" - }, + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } + "node": ">=18" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=8.0" + "node": ">=18" } }, - "node_modules/ts-loader": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", - "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==", + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "enhanced-resolve": "^5.0.0", - "micromatch": "^4.0.0", - "semver": "^7.3.4", - "source-map": "^0.7.4" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "typescript": "*", - "webpack": "^5.0.0" + "node": ">=18" } }, - "node_modules/ts-loader/node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "node_modules/@esbuild/win32-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 8" + "node": ">=18" } }, - "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", - "dev": true, + "node_modules/@ledgerhq/devices": { + "version": "8.4.4", + "resolved": "https://registry.npmjs.org/@ledgerhq/devices/-/devices-8.4.4.tgz", + "integrity": "sha512-sz/ryhe/R687RHtevIE9RlKaV8kkKykUV4k29e7GAVwzHX1gqG+O75cu1NCJUHLbp3eABV5FdvZejqRUlLis9A==", "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true, - "license": "MIT" - }, - "node_modules/update-browserslist-db": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", - "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", + "optional": true, "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" + "@ledgerhq/errors": "^6.19.1", + "@ledgerhq/logs": "^6.12.0", + "rxjs": "^7.8.1", + "semver": "^7.3.5" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", + "node_modules/@ledgerhq/errors": { + "version": "6.19.1", + "resolved": "https://registry.npmjs.org/@ledgerhq/errors/-/errors-6.19.1.tgz", + "integrity": "sha512-75yK7Nnit/Gp7gdrJAz0ipp31CCgncRp+evWt6QawQEtQKYEDfGo10QywgrrBBixeRxwnMy1DP6g2oCWRf1bjw==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/@ledgerhq/hw-transport": { + "version": "6.31.4", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport/-/hw-transport-6.31.4.tgz", + "integrity": "sha512-6c1ir/cXWJm5dCWdq55NPgCJ3UuKuuxRvf//Xs36Bq9BwkV2YaRQhZITAkads83l07NAdR16hkTWqqpwFMaI6A==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "punycode": "^2.1.0" + "@ledgerhq/devices": "^8.4.4", + "@ledgerhq/errors": "^6.19.1", + "@ledgerhq/logs": "^6.12.0", + "events": "^3.3.0" } }, - "node_modules/watchpack": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", - "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", - "dev": true, - "license": "MIT", + "node_modules/@ledgerhq/hw-transport-web-ble": { + "version": "6.29.4", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport-web-ble/-/hw-transport-web-ble-6.29.4.tgz", + "integrity": "sha512-OJyp6CryvyFlg1L9uifo5hYYdDt+WPw8/0ijBixYhYmGvlRz2W6/F2c5rG/zBQWcNnNydPOLjLJM0vR070RfCw==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" + "@ledgerhq/devices": "^8.4.4", + "@ledgerhq/errors": "^6.19.1", + "@ledgerhq/hw-transport": "^6.31.4", + "@ledgerhq/logs": "^6.12.0", + "rxjs": "^7.8.1" } }, - "node_modules/webpack": { - "version": "5.93.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", - "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", - "dev": true, - "license": "MIT", + "node_modules/@ledgerhq/hw-transport-webhid": { + "version": "6.29.4", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport-webhid/-/hw-transport-webhid-6.29.4.tgz", + "integrity": "sha512-XkF37lcuyg9zVExMyfDQathWly8rRcGac13wgZATBa3nZ+hUzzWr5QVKg1pKCw10izVHGErW/9a4tbb72rUEmQ==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.7.1", - "acorn-import-attributes": "^1.9.5", - "browserslist": "^4.21.10", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.0", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } + "@ledgerhq/devices": "^8.4.4", + "@ledgerhq/errors": "^6.19.1", + "@ledgerhq/hw-transport": "^6.31.4", + "@ledgerhq/logs": "^6.12.0" } }, - "node_modules/webpack-cli": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", - "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", - "dev": true, - "license": "MIT", + "node_modules/@ledgerhq/hw-transport-webusb": { + "version": "6.29.4", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport-webusb/-/hw-transport-webusb-6.29.4.tgz", + "integrity": "sha512-HoGF1LlBT9HEGBQy2XeCHrFdv/FEOZU0+J+yfKcgAQIAiASr2MLvdzwoJbUS8h6Gn+vc+/BjzBSO3JNn7Loqbg==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^2.1.1", - "@webpack-cli/info": "^2.0.2", - "@webpack-cli/serve": "^2.0.5", - "colorette": "^2.0.14", - "commander": "^10.0.1", - "cross-spawn": "^7.0.3", - "envinfo": "^7.7.3", - "fastest-levenshtein": "^1.0.12", - "import-local": "^3.0.2", - "interpret": "^3.1.1", - "rechoir": "^0.8.0", - "webpack-merge": "^5.7.3" - }, - "bin": { - "webpack-cli": "bin/cli.js" - }, - "engines": { - "node": ">=14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "5.x.x" - }, - "peerDependenciesMeta": { - "@webpack-cli/generators": { - "optional": true - }, - "webpack-bundle-analyzer": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - } + "@ledgerhq/devices": "^8.4.4", + "@ledgerhq/errors": "^6.19.1", + "@ledgerhq/hw-transport": "^6.31.4", + "@ledgerhq/logs": "^6.12.0" } }, - "node_modules/webpack-cli/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - } + "node_modules/@ledgerhq/logs": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/logs/-/logs-6.12.0.tgz", + "integrity": "sha512-ExDoj1QV5eC6TEbMdLUMMk9cfvNKhhv5gXol4SmULRVCx/3iyCPhJ74nsb3S0Vb+/f+XujBEj3vQn5+cwS0fNA==", + "license": "Apache-2.0", + "optional": true }, - "node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "node_modules/@types/node": { + "version": "22.7.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz", + "integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==", "dev": true, "license": "MIT", "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" + "undici-types": "~6.19.2" } }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "node_modules/@types/w3c-web-hid": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/w3c-web-hid/-/w3c-web-hid-1.0.6.tgz", + "integrity": "sha512-IWyssXmRDo6K7s31dxf+U+x/XUWuVsl9qUIYbJmpUHPcTv/COfBCKw/F0smI45+gPV34brjyP30BFcIsHgYWLA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } + "license": "MIT" }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "node_modules/@types/w3c-web-usb": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.10.tgz", + "integrity": "sha512-CHgUI5kTc/QLMP8hODUHhge0D4vx+9UiAwIGiT0sTy/B2XpdX1U5rJt6JSISgr6ikRT7vxV9EVAFeYZqUnl1gQ==", "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } + "license": "MIT" }, - "node_modules/wildcard": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", - "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", "dev": true, "license": "MIT" }, - "node_modules/workerpool": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", - "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", - "dev": true, - "license": "Apache-2.0" + "node_modules/blakejs": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", + "integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==", + "license": "MIT" }, - "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==", + "node_modules/esbuild": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "bin": { + "esbuild": "bin/esbuild" }, "engines": { - "node": ">=10" + "node": ">=18" }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.0", + "@esbuild/android-arm": "0.24.0", + "@esbuild/android-arm64": "0.24.0", + "@esbuild/android-x64": "0.24.0", + "@esbuild/darwin-arm64": "0.24.0", + "@esbuild/darwin-x64": "0.24.0", + "@esbuild/freebsd-arm64": "0.24.0", + "@esbuild/freebsd-x64": "0.24.0", + "@esbuild/linux-arm": "0.24.0", + "@esbuild/linux-arm64": "0.24.0", + "@esbuild/linux-ia32": "0.24.0", + "@esbuild/linux-loong64": "0.24.0", + "@esbuild/linux-mips64el": "0.24.0", + "@esbuild/linux-ppc64": "0.24.0", + "@esbuild/linux-riscv64": "0.24.0", + "@esbuild/linux-s390x": "0.24.0", + "@esbuild/linux-x64": "0.24.0", + "@esbuild/netbsd-x64": "0.24.0", + "@esbuild/openbsd-arm64": "0.24.0", + "@esbuild/openbsd-x64": "0.24.0", + "@esbuild/sunos-x64": "0.24.0", + "@esbuild/win32-arm64": "0.24.0", + "@esbuild/win32-ia32": "0.24.0", + "@esbuild/win32-x64": "0.24.0" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "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, - "license": "ISC", + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "optional": true, "engines": { - "node": ">=10" + "node": ">=0.8.x" } }, - "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, - "license": "MIT", + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "license": "Apache-2.0", + "optional": 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" + "tslib": "^2.1.0" } }, - "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, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, "engines": { "node": ">=10" } }, - "node_modules/yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", "dev": true, - "license": "MIT", - "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, "engines": { - "node": ">=10" + "node": ">=14.17" } }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "license": "MIT" } } } diff --git a/package-lock.json.license b/package-lock.json.license new file mode 100644 index 0000000..36ee55c --- /dev/null +++ b/package-lock.json.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2024 Chris Duncan +SPDX-License-Identifier: GPL-3.0-or-later diff --git a/package.json b/package.json index b867c2b..f2acf18 100644 --- a/package.json +++ b/package.json @@ -1,48 +1,66 @@ { - "name": "nanocurrency-web", - "version": "1.4.3", - "description": "Toolkit for Nano cryptocurrency client side offline integrations", - "author": "Miro Metsänheimo ", - "license": "MIT", - "homepage": "https://github.com/numsu/nanocurrency-web-js#readme", - "repository": { - "type": "git", - "url": "git+https://github.com/numsu/nanocurrency-web-js.git" - }, - "bugs": { - "url": "https://github.com/numsu/nanocurrency-web-js/issues" - }, + "name": "libnemo", + "version": "0.0.1", + "description": "Asynchronous, non-blocking Nano cryptocurrency integration toolkit.", "keywords": [ + "nemo", "nano", - "currency", - "mnemonic", "crypto", + "currency", + "coin", "wallet", + "mnemonic", + "seed", "block", "sign", "encrypt", - "decrypt" + "decrypt", + "keys" ], - "main": "dist/index.js", - "types": "dist/index.d.ts", - "unpkg": "dist/index.min.js", + "homepage": "https://zoso.dev", + "bugs": "https://zoso.dev", + "license": "(GPL-3.0-or-later AND MIT)", + "author": "Chris Duncan ", + "funding": { + "type": "nano", + "url": "nano:nano_1zosoqs47yt47bnfg7sdf46kj7asn58b7uzm9ek95jw7ccatq37898u1zoso" + }, + "files": [ + "/LICENSES", + "AUTHORS.md", + "package.json.license" + ], + "main": "dist/main.js", + "browser": "dist/main.min.js", + "repository": { + "type": "git", + "url": "git+https://zoso.dev/libnemo.git" + }, "scripts": { - "build": "rm -rf dist && tsc && npm run build:webpack", - "build:webpack": "webpack", - "test": "mocha --reporter spec" + "build": "rm -rf dist && tsc && esbuild dist/main.js --outfile=dist/main.min.js --target=es2022 --format=esm --platform=node --bundle --minify", + "test": "npm run build && node --test --env-file .env", + "test:coverage": "npm run test -- --experimental-test-coverage", + "test:coverage:report": "npm run test:coverage -- --test-reporter=lcov --test-reporter-destination=coverage.info && genhtml coverage.info --output-directory test/coverage && rm coverage.info && xdg-open test/coverage/index.html" }, "dependencies": { - "bignumber.js": "^9.1.2", - "blakejs": "^1.2.1", - "byte-base64": "^1.1.0", - "crypto-js": "^4.2.0" + "blakejs": "^1.2.1" + }, + "optionalDependencies": { + "@ledgerhq/hw-transport-web-ble": "^6.29.4", + "@ledgerhq/hw-transport-webhid": "^6.29.4", + "@ledgerhq/hw-transport-webusb": "^6.29.4" }, "devDependencies": { - "chai": "^5.1.1", - "mocha": "^10.7.0", - "ts-loader": "^9.5.1", - "typescript": "^5.5.4", - "webpack": "^5.93.0", - "webpack-cli": "^5.1.4" - } + "@types/node": "^22.7.4", + "@types/w3c-web-hid": "^1.0.6", + "@types/w3c-web-usb": "^1.0.10", + "@types/web-bluetooth": "^0.0.20", + "esbuild": "^0.24.0", + "typescript": "^5.6.2" + }, + "type": "module", + "private": true, + "exports": "./dist/main.js", + "types": "dist/main.d.ts", + "unpkg": "dist/main.min.js" } diff --git a/package.json.license b/package.json.license new file mode 100644 index 0000000..36ee55c --- /dev/null +++ b/package.json.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2024 Chris Duncan +SPDX-License-Identifier: GPL-3.0-or-later diff --git a/src/lib/account.ts b/src/lib/account.ts new file mode 100644 index 0000000..b783d69 --- /dev/null +++ b/src/lib/account.ts @@ -0,0 +1,208 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later + +import { blake2b } from 'blakejs' +import { ACCOUNT_KEY_LENGTH, ALPHABET, PREFIX, PREFIX_LEGACY } from './constants.js' +import { base32, bytes, hex } from './convert.js' +import Ed25519 from './ed25519.js' +import { Node } from './node.js' +import { Safe } from './safe.js' + +/** +* Represents a single Nano address and the associated public key. To include the +* matching private key, it must be known at the time of object instantiation. +* The frontier, balance, and representative for the account can also be set or +* be fetched from the network. +*/ +export class Account { + #a: string + #pub: string + #prv: string | null + #i?: number + #f?: string + #b?: bigint + #r?: Account + #s: Safe + + get address () { return `${PREFIX}${this.#a}` } + get publicKey () { return this.#pub } + get privateKey () { return this.#prv } + get index () { return this.#i } + get frontier () { return this.#f } + get balance () { return this.#b?.toString() } + get representative () { return this.#r } + + set frontier (v) { this.#f = v } + set balance (v) { this.#b = v ? BigInt(v) : undefined } + set representative (v) { + if (v?.constructor === Account) { + this.#r = v + } else if (typeof v === 'string') { + this.#r = new Account(v) + } else { + throw new TypeError(`Invalid argument for account representative: ${v}`) + } + } + + constructor (address: string, index?: number) { + Account.validate(address) + if (index !== undefined && typeof index !== 'number') { + throw new TypeError(`Invalid index ${index} when creating Account ${address}`) + } + this.#a = address + .replace(PREFIX, '') + .replace(PREFIX_LEGACY, '') + this.#pub = Account.#addressToKey(this.#a) + this.#prv = null + this.#i = index + this.#s = new Safe() + } + + /** + * Asynchronously instantiates an Account object from its public key. + * + * @param {string} key - Public key of the account + * @param {number} [index] - Account number used when deriving the key + * @returns {Promise} The instantiated Account object + */ + static async fromPublicKey (key: string, index?: number): Promise { + Account.#validateKey(key) + const address = await Account.#keyToAddress(key) + const account = new this(address, index) + return account + } + + /** + * Asynchronously instantiates an Account object from its private key. + * + * @param {string} key - Private key of the account + * @param {number} [index] - Account number used when deriving the key + * @returns {Promise} A new Account object + */ + static async fromPrivateKey (key: string, index?: number): Promise { + Account.#validateKey(key) + const publicKey = Ed25519.getPublicKey(key) + const account = await Account.fromPublicKey(publicKey, index) + account.#prv = key.toUpperCase() + return account + } + + async lock (password: string): Promise + async lock (key: CryptoKey): Promise + async lock (passkey: string | CryptoKey): Promise { + try { + if (this.#prv != null) { + await this.#s.put(this.#pub, passkey as string, this.#prv) + } + } catch (err) { + console.error(`Failed to lock account ${this.address}`, err) + return false + } + this.#prv = null + return true + } + + async unlock (password: string): Promise + async unlock (key: CryptoKey): Promise + async unlock (passkey: string | CryptoKey): Promise { + try { + this.#prv = await this.#s.get(this.#pub, passkey as string) + } catch (err) { + console.error(`Failed to unlock account ${this.address}`, err) + return false + } + return true + } + + /** + * Validates a Nano address with 'nano' and 'xrb' prefixes + * Derived from https://github.com/alecrios/nano-address-validator + * + * @param {string} address - Nano address to validate + * @throws Error if address is undefined, not a string, or an invalid format + */ + static validate (address: string): void { + if (address === undefined) { + throw new ReferenceError('Address is undefined.') + } + if (typeof address !== 'string') { + throw new TypeError('Address must be a string.') + } + const pattern = new RegExp(`^(${PREFIX}|${PREFIX_LEGACY})[13]{1}[${ALPHABET}]{59}$`, + ) + if (!pattern.test(address)) { + throw new RangeError('Invalid address format') + } + + const expectedChecksum = address.slice(-8) + const keyBase32 = address.slice(address.indexOf('_') + 1, -8) + const keyBuf = base32.toBytes(keyBase32) + const actualChecksumBuf = blake2b(keyBuf, undefined, 5).reverse() + const actualChecksum = bytes.toBase32(actualChecksumBuf) + + if (expectedChecksum !== actualChecksum) { + throw new Error('Incorrect address checksum') + } + } + + /** + * Refreshes the account from its current state on the network. + * + * A successful response sets the balance, frontier, and representative + * properties. + * + * @param {Node|string|URL} node - Node information required to call `account_info` + */ + async refresh (node: Node | string | URL): Promise { + if (typeof node === 'string' || node.constructor === URL) { + node = new Node(node) + } + if (node.constructor !== Node) { + throw new TypeError('RPC must be a valid node') + } + const data = { + "representative": "true", + "account": this.address + } + const { balance, frontier, representative } = await node.call('account_info', data) + if (frontier == null) { + throw new Error('Account not found') + } + this.#b = BigInt(balance) + this.#f = frontier + this.#r = new Account(representative) + } + + static #addressToKey (v: string): string { + const keyBytes = base32.toBytes(v.substring(0, 52)) + const checksumBytes = base32.toBytes(v.substring(52, 60)) + const blakeHash = blake2b(keyBytes, undefined, 5).reverse() + if (bytes.toHex(checksumBytes) !== bytes.toHex(blakeHash)) { + throw new Error('Checksum mismatch in address') + } + return bytes.toHex(keyBytes) + } + + static async #keyToAddress (key: string): Promise { + const publicKeyBytes = hex.toBytes(key) + const checksum = blake2b(publicKeyBytes, undefined, 5).reverse() + const encoded = bytes.toBase32(publicKeyBytes) + const encodedChecksum = bytes.toBase32(checksum) + return `${PREFIX}${encoded}${encodedChecksum}` + } + + static #validateKey (key: string): void { + if (key === undefined) { + throw new TypeError(`Key is undefined`) + } + if (typeof key !== 'string') { + throw new TypeError(`Key must be a string`) + } + if (key.length !== ACCOUNT_KEY_LENGTH) { + throw new TypeError(`Key must be ${ACCOUNT_KEY_LENGTH} characters`) + } + if (!/^[0-9a-fA-F]+$/i.test(key)) { + throw new RangeError('Key is not a valid hexadecimal value') + } + } +} diff --git a/src/lib/bip32-key-derivation.ts b/src/lib/bip32-key-derivation.ts new file mode 100644 index 0000000..8580ad0 --- /dev/null +++ b/src/lib/bip32-key-derivation.ts @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later + +import { BIP44_COIN_NANO, BIP44_PURPOSE, HARDENED_OFFSET, SLIP10_ED25519 } from './constants.js' +import { bytes, dec, hex, utf8 } from './convert.js' + +type ExtendedKey = { + privateKey: string + chainCode: string +} + +/** +* Derives a private child key following the BIP-32 and BIP-44 derivation path +* registered to the Nano block lattice. Only hardened child keys are defined. +* +* @param {string} seed - Hexadecimal seed derived from mnemonic phrase +* @param {number} index - Account number between 0 and 2^31-1 +* @returns Private child key for the account +*/ +export async function nanoCKD (seed: string, index: number): Promise { + if (!Number.isSafeInteger(index) || index < 0 || index > 0x7fffffff) { + throw new RangeError(`Invalid child key index 0x${index.toString(16)}`) + } + const masterKey = await slip10(SLIP10_ED25519, seed) + const purposeKey = await CKDpriv(masterKey, BIP44_PURPOSE + HARDENED_OFFSET) + const coinKey = await CKDpriv(purposeKey, BIP44_COIN_NANO + HARDENED_OFFSET) + const accountKey = await CKDpriv(coinKey, index + HARDENED_OFFSET) + return accountKey.privateKey +} + +async function slip10 (curve: string, S: string): Promise { + const key = utf8.toBytes(curve) + const data = hex.toBytes(S) + const I = await hmac(key, data) + const IL = I.slice(0, I.length / 2) + const IR = I.slice(I.length / 2) + return ({ privateKey: IL, chainCode: IR }) +} + +async function CKDpriv ({ privateKey, chainCode }: ExtendedKey, index: number): Promise { + const key = hex.toBytes(chainCode) + const data = hex.toBytes(`00${bytes.toHex(ser256(privateKey))}${bytes.toHex(ser32(index))}`) + const I = await hmac(key, data) + const IL = I.slice(0, I.length / 2) + const IR = I.slice(I.length / 2) + return ({ privateKey: IL, chainCode: IR }) +} + +function ser32 (integer: number): Uint8Array { + const bits = integer.toString(2) + if (bits.length > 32) { + throw new RangeError(`Expected 32-bit integer, received ${bits.length} bits: ${bits}`) + } + const bytes = dec.toBytes(integer) + const result = new Uint8Array(4) + result.set(bytes, 4 - bytes.length) + return result +} + +function ser256 (integer: string): Uint8Array { + const bits = hex.toBin(integer) + if (bits.length > 256) { + throw new RangeError(`Expected 256-bit integer, received ${bits.length} bits: ${bits}`) + } + const bytes = hex.toBytes(integer) + const result = new Uint8Array(32) + result.set(bytes, 32 - bytes.length) + return result +} + +async function hmac (key: Uint8Array, data: Uint8Array): Promise { + const { subtle } = globalThis.crypto + const pk = await subtle.importKey('raw', key, { name: 'HMAC', hash: 'SHA-512' }, false, ['sign']) + const signature = await subtle.sign('HMAC', pk, data) + return bytes.toHex(new Uint8Array(signature)) +} diff --git a/src/lib/bip39-mnemonic.ts b/src/lib/bip39-mnemonic.ts new file mode 100644 index 0000000..70d3996 --- /dev/null +++ b/src/lib/bip39-mnemonic.ts @@ -0,0 +1,199 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later + +import { Bip39Words } from './bip39-wordlist.js' +import { bin, bytes, dec, utf8 } from './convert.js' +import { BIP39_ITERATIONS } from './constants.js' +import { Entropy } from './entropy.js' + +const { subtle } = globalThis.crypto + +/** +* Represents a mnemonic phrase that identifies a wallet as defined by BIP-39. +*/ +export class Bip39Mnemonic { + static #isInternal: boolean = false + #bip44Seed: string = '' + #blake2bSeed: string = '' + #phrase: string = '' + get phrase (): string { return this.#phrase.normalize('NFKD') } + + constructor () { + if (!Bip39Mnemonic.#isInternal) { + throw new Error(`Bip39Mnemonic cannot be instantiated directly. Use 'await Bip39Mnemonic.fromPhrase()' or 'await Bip39Mnemonic.fromEntropy() instead.`) + } + Bip39Mnemonic.#isInternal = false + } + + /** + * Imports and validates an existing mnemonic phrase. + * + * The phrase must be valid according to the BIP-39 specification. Typically + * wallets use the maximum of 24 words. + * + * @param {string} phrase - String of 12, 15, 18, 21, or 24 words + * @returns {string} Mnemonic phrase validated using the BIP-39 wordlist + */ + static async fromPhrase (phrase: string): Promise { + Bip39Mnemonic.#isInternal = true + const self = new this() + const isValid = await Bip39Mnemonic.validate(phrase) + if (isValid) { + self.#phrase = phrase.normalize('NFKD') + return self + } else { + throw new Error('Invalid mnemonic phrase.') + } + } + + /** + * Derives a mnemonic phrase from source of entropy or seed. + * + * The entropy must be between 32-64 characters to stay within the defined + * limit of 128-256 bits. Typically wallets use the maximum entropy allowed. + * + * @param {string} entropy - Hexadecimal string + * @returns {string} Mnemonic phrase created using the BIP-39 wordlist + */ + static async fromEntropy (entropy: string): Promise { + const e = new Entropy(entropy) + const checksum = await this.checksum(e) + let concatenation = `${e.bits}${checksum}` + const words = [] + while (concatenation.length > 0) { + const wordBits = concatenation.substring(0, 11) + const wordIndex = parseInt(wordBits, 2) + words.push(Bip39Words[wordIndex]) + concatenation = concatenation.substring(11) + } + const sentence = words.join(' ') + return Bip39Mnemonic.fromPhrase(sentence) + } + + /** + * SHA-256 hash of entropy that is appended to the entropy and subsequently + * used to generate the mnemonic phrase. + * + * @param {Entropy} entropy - Cryptographically strong pseudorandom data of length N bits + * @returns First N/32 bits of the hash as a hexadecimal string + */ + static async checksum (entropy: Entropy): Promise { + const hashBuffer = await subtle.digest('SHA-256', entropy.bytes) + const hashBytes = new Uint8Array(hashBuffer) + const hashBits = bytes.toBin(hashBytes) + const checksumLength = entropy.bits.length / 32 + const checksum = hashBits.substring(0, checksumLength) + return checksum + } + + /** + * Validates a mnemonic phrase. + * + * @param {string} mnemonic - Mnemonic phrase to validate + * @returns {boolean} True if the mnemonic phrase is valid + */ + static async validate (mnemonic: string): Promise { + const words = mnemonic.normalize('NFKD').split(' ') + if (words.length % 3 !== 0) { + return false + } + + const wordBits = words.map(word => { + const wordIndex = Bip39Words.indexOf(word) + if (wordIndex === -1) { + return false + } + return dec.toBin(wordIndex, 11) + }).join('') + const checksumLength = wordBits.length / 33 + const entropyLength = wordBits.length - checksumLength + const entropyBits = wordBits.substring(0, entropyLength) + const checksumBits = wordBits.substring(entropyLength) + + if ( + entropyBits == null + || entropyBits.length < 128 + || entropyBits.length > 256 + || entropyBits.length % 32 !== 0 + ) { + return false + } + + const entropy = new Entropy(bin.toBytes(entropyBits)) + const expectedChecksum = await this.checksum(entropy) + + if (expectedChecksum !== checksumBits) { + return false + } + + return true + } + + /** + * Converts the mnemonic phrase to a BIP-39 seed. + * + * A passphrase string can be specified. If the passphrase is undefined, null, + * or not a string, the empty string ("") is used instead. + * + * @param {string} [passphrase=''] - Used as the PBKDF2 salt. Default: "" + * @returns {string} Hexadecimal seed + */ + async toBip39Seed (passphrase: string): Promise { + if (this.#blake2bSeed === '') { + if (passphrase == null || typeof passphrase !== 'string') { + passphrase = '' + } + passphrase = `mnemonic${passphrase.normalize('NFKD')}` + + const derivationAlgorithm: Pbkdf2Params = { + name: 'PBKDF2', + hash: 'SHA-512', + salt: utf8.toBytes(passphrase), + iterations: BIP39_ITERATIONS + } + const phraseKey = await subtle.importKey('raw', utf8.toBytes(this.phrase), 'PBKDF2', false, ['deriveBits', 'deriveKey']) + const derivedKeyType: HmacImportParams = { + name: 'HMAC', + hash: 'SHA-512', + length: 512 + } + const isSeedKeyExtractable: boolean = true + const seedKeyUsages: KeyUsage[] = ['sign'] + + const seedKey = await subtle.deriveKey(derivationAlgorithm, phraseKey, derivedKeyType, isSeedKeyExtractable, seedKeyUsages) + const seedBuffer = await subtle.exportKey('raw', seedKey) + const seedBytes = new Uint8Array(seedBuffer) + const seed = bytes.toHex(seedBytes) + this.#bip44Seed = seed + } + return this.#bip44Seed + } + + /** + * Converts the mnemonic phrase to a BLAKE2b seed. + * + * @returns {string} Hexadecimal seed + */ + async toBlake2bSeed (): Promise { + if (this.#blake2bSeed === '') { + const wordArray = this.phrase.split(' ') + const bits = wordArray.map((w: string) => { + const wordIndex = Bip39Words.indexOf(w) + if (wordIndex === -1) { + return false + } + return dec.toBin(wordIndex, 11) + }).join('') + + const dividerIndex = Math.floor(bits.length / 33) * 32 + const entropyBits = bits.slice(0, dividerIndex) + const entropyBytes = entropyBits.match(/(.{1,8})/g)?.map((bin: string) => parseInt(bin, 2)) + if (entropyBytes == null) { + throw new Error('Invalid mnemonic phrase') + } + this.#blake2bSeed = bytes.toHex(Uint8Array.from(entropyBytes)) + } + return this.#blake2bSeed + } +} + diff --git a/lib/words.ts b/src/lib/bip39-wordlist.ts similarity index 98% rename from lib/words.ts rename to src/lib/bip39-wordlist.ts index 2708a2a..2b873c5 100644 --- a/lib/words.ts +++ b/src/lib/bip39-wordlist.ts @@ -1,5 +1,10 @@ -const words = [ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later +/** +* https://github.com/bitcoin/bips/blob/master/bip-0039/english.txt +*/ +export const Bip39Words: readonly string[] = Object.freeze([ 'abandon', 'ability', 'able', @@ -2047,8 +2052,5 @@ const words = [ 'zebra', 'zero', 'zone', - 'zoo', - -] - -export default words + 'zoo' +]) diff --git a/src/lib/block.ts b/src/lib/block.ts new file mode 100644 index 0000000..f94cb16 --- /dev/null +++ b/src/lib/block.ts @@ -0,0 +1,333 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later + +import { BURN_ADDRESS, PREAMBLE } from './constants.js' +import { Account } from './account.js' +import { bytes, dec, hex } from './convert.js' +import Ed25519 from './ed25519.js' +import { Node } from './node.js' +import Tools from './tools.js' + +/** +* Represents a block as defined by the Nano cryptocurrency protocol. The Block +* class is abstract and cannot be directly instantiated. Every block must be one +* of three derived classes: SendBlock, ReceiveBlock, ChangeBlock. +*/ +abstract class Block { + account: Account + type: string = 'state' + abstract subtype: 'send' | 'receive' | 'change' + abstract previous: string + abstract representative: Account + abstract balance: bigint + abstract link: string + abstract signature?: string + abstract work?: string + + constructor (account: Account | string) { + if (this.constructor === Block) { + throw new Error('Block is an abstract class and cannot be instantiated directly.') + } + if (account.constructor === Account) { + this.account = account + } else if (typeof account === 'string') { + this.account = new Account(account) + } else { + throw new TypeError('Invalid account') + } + } + + /** + * Converts the block to JSON format as expected by the `process` RPC. + * + * @returns JSON representation of the block + */ + json (): { [key: string]: string } { + return { + "type": this.type, + "account": this.account.address, + "previous": this.previous, + "representative": this.representative.address, + "balance": this.balance.toString(), + "link": this.link, + "signature": this.signature ?? '', + "work": this.work ?? '' + } + } + + /** + * Hashes the block using Blake2b. + * + * @returns {Promise} Block data hashed to a byte array + */ + async hash (): Promise { + const data = [ + PREAMBLE, + this.account.publicKey, + this.previous, + this.representative.publicKey, + dec.toHex(this.balance, 32), + this.link + ] + return Tools.blake2b(data) + } + + /** + * Sends the block to a node for calculating proof-of-work on the network. + * + * A successful response sets the `work` property. + * + * @param {Node|string|URL} node - Node information required to call `work_generate` + */ + async pow (node: Node | string | URL): Promise { + if (typeof node === 'string' || node.constructor === URL) { + node = new Node(node) + } + if (node.constructor !== Node) { + throw new TypeError('RPC must be a valid node') + } + const data = { + "hash": this.previous + } + const { work } = await node.call('work_generate', data) + this.work = work + } + + /** + * Signs the block using a private key. If a key is not provided, the private + * key of the block's account will be used if it exists. If that fails, an + * error is thrown. + * + * If successful, the result is stored in the object's `signature` + * property. + * + * @param {string} [key] - Hexadecimal-formatted private key to use for signing + */ + async sign (key?: string): Promise + /** + * Signs the block using a Ledger hardware wallet. If that fails, an error is + * thrown with the status code from the device. + * + * If successful, the result is stored in the object's `signature` + * property. + * + * @param {number} index - Account index between 0x0 and 0x7fffffff + * @param {object} [block] - JSON of previous block for offline signing + */ + async sign (index?: number, block?: { [key: string]: string }): Promise + async sign (input?: number | string, block?: { [key: string]: string }): Promise { + if (typeof input === 'number') { + const index = input + const { Ledger } = await import('./ledger.js') + const ledger = await Ledger.init() + await ledger.open() + if (block) { + try { + await ledger.updateCache(index, block) + } catch (err) { + console.warn('Error updating Ledger cache of previous block, attempting signature anyway', err) + } + } + const result = await ledger.sign(index, this as SendBlock | ReceiveBlock | ChangeBlock) + if (result.status !== 'OK') { + throw new Error(result.status) + } + this.signature = result.signature + } else { + const key = input ?? this.account.privateKey + if (!key) { + throw new Error('No valid key found to sign block') + } + const signature = Ed25519.sign( + await this.hash(), + hex.toBytes(key) + ) + this.signature = bytes.toHex(signature) + } + } + + /** + * Sends the block to a node for processing on the network. + * + * The block must already be signed (see `sign()` for more information). + * The block must also have a `work` value. + * + * @param {Node|string|URL} node - Node information required to call `process` + * @returns {Promise} Hash of the processed block + */ + async process (node: Node): Promise { + if (!this.signature) { + throw new Error('Block is missing signature. Use sign() and try again.') + } + if (!this.work == null) { + throw new Error('Block is missing proof-of-work. Generate PoW and try again.') + } + const data = { + "subtype": this.subtype, + "json_block": "true", + "block": this.json() + } + const res = await node.call('process', data) + if (res.hash == null) { + throw new Error('Block could not be processed', res) + } + return res.hash + } + + /** + * Verifies the signature of the block. If a key is not provided, the public + * key of the block's account will be used if it exists. + * + * @param {string} [key] - Hexadecimal-formatted public key to use for verification + * @returns {boolean} True if block was signed by the matching private key + */ + async verify (key?: string): Promise { + key ??= this.account.publicKey + if (!key) { + throw new Error('Provide a key for block signature verification.') + } + const data = [ + PREAMBLE, + this.account.publicKey, + this.previous, + this.representative.publicKey, + dec.toHex(this.balance, 32), + this.link + ] + return Ed25519.verify( + await Tools.blake2b(data), + hex.toBytes(key), + hex.toBytes(this.signature ?? '') + ) + } +} + +/** +* Represents a block that sends funds from one address to another as defined by +* the Nano cryptocurrency protocol. +*/ +export class SendBlock extends Block { + type: 'state' = 'state' + subtype: 'send' = 'send' + previous: string + representative: Account + balance: bigint + link: string + signature?: string + work?: string + + constructor (sender: Account | string, balance: string, recipient: string, amount: string, representative: string, frontier: string, work?: string) { + super(sender) + this.previous = frontier + this.representative = new Account(representative) + this.link = new Account(recipient).publicKey + this.work = work ?? '' + + const bigBalance = BigInt(balance) + const bigAmount = BigInt(amount) + this.balance = bigBalance - bigAmount + + validate(this) + } +} + +/** +* Represents a block that receives funds sent to one address from another as +* defined by the Nano cryptocurrency protocol. +*/ +export class ReceiveBlock extends Block { + type: 'state' = 'state' + subtype: 'receive' = 'receive' + previous: string + representative: Account + balance: bigint + link: string + signature?: string + work?: string + + constructor (recipient: string, balance: string, origin: string, amount: string, representative: string, frontier?: string, work?: string) { + super(recipient) + this.previous = frontier ?? new Account(recipient).publicKey + this.representative = new Account(representative) + this.link = origin + this.work = work ?? '' + + const bigBalance = BigInt(balance) + const bigAmount = BigInt(amount) + this.balance = bigBalance + bigAmount + + validate(this) + } +} + +/** +* Represents a block that changes the representative account to which the user +* account delegates their vote weight using the the Open Representative Voting +* specification as defined by the Nano cryptocurrency protocol. +*/ +export class ChangeBlock extends Block { + type: 'state' = 'state' + subtype: 'change' = 'change' + previous: string + representative: Account + balance: bigint + link: string = new Account(BURN_ADDRESS).publicKey + signature?: string + work?: string + + constructor (account: string, balance: string, representative: string, frontier: string, work?: string) { + super(account) + this.previous = frontier + this.representative = new Account(representative) + this.balance = BigInt(balance) + this.work = work ?? '' + + validate(this) + } +} + +/** + * Validates block data. + * + * @param {Block} block - SendBlock, ReceiveBlock, or ChangeBlock to validate + */ +function validate (block: Block): void { + if (block.account == null) { + throw new Error('Account missing') + } + if (block.previous == null || block.previous === '') { + throw new Error('Frontier missing') + } + if (block.representative == null) { + throw new Error('Representative missing') + } + if (block.balance == null) { + throw new Error('Balance missing') + } + if (block.balance < 0) { + throw new Error('Negative balance') + } + switch (block.constructor) { + case SendBlock: { + if (block.link == null || block.link === '') { + throw new Error('Recipient missing') + } + break + } + case ReceiveBlock: { + if (block.link == null) { + throw new Error('Origin send block hash missing') + } + break + } + case ChangeBlock: { + if (block.link == null) { + throw new Error('Change block link missing') + } + if (+block.link !== 0) { + throw new Error('Invalid change block link') + } + break + } + } +} diff --git a/src/lib/constants.ts b/src/lib/constants.ts new file mode 100644 index 0000000..f746364 --- /dev/null +++ b/src/lib/constants.ts @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later + +export const ACCOUNT_KEY_LENGTH = 64 +export const ADDRESS_GAP = 20 +export const ALPHABET = '13456789abcdefghijkmnopqrstuwxyz' +export const BIP39_ITERATIONS = 2048 +export const BIP44_PURPOSE = 44 +export const BIP44_COIN_NANO = 165 +export const BURN_ADDRESS = 'nano_1111111111111111111111111111111111111111111111111111hifc8npp' +export const HARDENED_OFFSET = 0x80000000 +export const NONCE_LENGTH = 24 +export const PREAMBLE = '0000000000000000000000000000000000000000000000000000000000000006' +export const PREFIX = 'nano_' +export const PREFIX_LEGACY = 'xrb_' +export const SEED_LENGTH_BIP44 = 128 +export const SEED_LENGTH_BLAKE2B = 64 +export const SLIP10_ED25519 = 'ed25519 seed' +export const XNO = 'Ӿ' + +export const LEDGER_STATUS_CODES: { [key: number]: string } = Object.freeze({ + 0x6700: 'INCORRECT_LENGTH', + 0x670a: 'NO_APPLICATION_SPECIFIED', + 0x6807: 'APPLICATION_NOT_INSTALLED', + 0x6d00: 'APPLICATION_ALREADY_LAUNCHED', + 0x6982: 'SECURITY_STATUS_NOT_SATISFIED', + 0x6985: 'CONDITIONS_OF_USE_NOT_SATISFIED', + 0x6a81: 'INVALID_SIGNATURE', + 0x6a82: 'CACHE_MISS', + 0x6b00: 'INCORRECT_PARAMETER', + 0x6e01: 'TRANSPORT_STATUS_ERROR', + 0x9000: 'OK' +}) + +export const UNITS: { [key: string]: number } = Object.freeze({ + RAW: 0, + RAI: 24, + NYANO: 24, + KRAI: 27, + PICO: 27, + MRAI: 30, + NANO: 30, + KNANO: 33, + MNANO: 36 +}) diff --git a/src/lib/convert.ts b/src/lib/convert.ts new file mode 100644 index 0000000..630b894 --- /dev/null +++ b/src/lib/convert.ts @@ -0,0 +1,312 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later + +import { ALPHABET } from "./constants.js" + +export const base32 = { + /** + * Convert a base32 string to a Uint8Array of bytes. + * + * @param {string} base32 - String to convert + * @returns {Uint8Array} Byte array representation of the input string + */ + toBytes (base32: string): Uint8Array { + const leftover = (base32.length * 5) % 8 + const offset = leftover === 0 + ? 0 + : 8 - leftover + let bits = 0 + let value = 0 + let index = 0 + let output = new Uint8Array(Math.ceil((base32.length * 5) / 8)) + for (let i = 0; i < base32.length; i++) { + value = (value << 5) | ALPHABET.indexOf(base32[i]) + bits += 5 + if (bits >= 8) { + output[index++] = (value >>> (bits + offset - 8)) & 255 + bits -= 8 + } + } + if (bits > 0) { + output[index++] = (value << (bits + offset - 8)) & 255 + } + if (leftover !== 0) { + output = output.slice(1) + } + return output + } +} + +export const bin = { + /** + * Convert a binary string to a Uint8Array of bytes. + * + * @param {string} bin - String to convert + * @returns {Uint8Array} Byte array representation of the input string + */ + toBytes (bin: string): Uint8Array { + const bytes = [] + while (bin.length > 0) { + const bits = bin.substring(0, 8) + bytes.push(parseInt(bits, 2)) + bin = bin.substring(8) + } + return new Uint8Array(bytes) + }, + /** + * Convert a binary string to a hexadecimal representation + * + * @param {string} bin - String to convert + * @returns {string} Hexadecimal representation of the input string + */ + toHex (bin: string): string { + return parseInt(bin, 2).toString(16) + } +} + +export const buffer = { + /** + * Converts an ArrayBuffer to a base32 string. + * + * @param {ArrayBuffer} buffer - Buffer to convert + * @returns {string} Base32 string representation of the input buffer + */ + toBase32 (buffer: ArrayBuffer): string { + return bytes.toBase32(new Uint8Array(buffer)) + }, + /** + * Converts an ArrayBuffer to a binary string. + * + * @param {ArrayBuffer} buffer - Buffer to convert + * @returns {string} Binary string representation of the input buffer + */ + toBin (buffer: ArrayBuffer): string { + return bytes.toBin(new Uint8Array(buffer)) + }, + /** + * Sums an ArrayBuffer to a decimal integer. If the result is larger than + * Number.MAX_SAFE_INTEGER, it will be returned as a bigint. + * + * @param {ArrayBuffer} buffer - Buffer to convert + * @returns {bigint|number} Decimal sum of the literal buffer values + */ + toDec (buffer: ArrayBuffer): bigint | number { + return bytes.toDec(new Uint8Array(buffer)) + }, + /** + * Converts an ArrayBuffer to a hexadecimal string. + * + * @param {ArrayBuffer} buffer - Buffer to convert + * @returns {string} Hexadecimal string representation of the input buffer + */ + toHex (buffer: ArrayBuffer): string { + return bytes.toHex(new Uint8Array(buffer)) + }, + /** + * Converts an ArrayBuffer to a UTF-8 text string. + * + * @param {ArrayBuffer} buffer - Buffer to convert + * @returns {string} UTF-8 encoded text string + */ + toUtf8 (buffer: ArrayBuffer): string { + return bytes.toUtf8(new Uint8Array(buffer)) + } +} + +export const bytes = { + /** + * Converts a Uint8Aarray of bytes to a base32 string. + * + * @param {Uint8Array} bytes - Byte array to convert + * @returns {string} Base32 string representation of the input bytes + */ + toBase32 (bytes: Uint8Array): string { + const leftover = (bytes.length * 8) % 5 + const offset = leftover === 0 + ? 0 + : 5 - leftover + let value = 0 + let output = '' + let bits = 0 + for (let i = 0; i < bytes.length; i++) { + value = (value << 8) | bytes[i] + bits += 8 + while (bits >= 5) { + output += ALPHABET[(value >>> (bits + offset - 5)) & 31] + bits -= 5 + } + } + if (bits > 0) { + output += ALPHABET[(value << (5 - (bits + offset))) & 31] + } + return output + }, + /** + * Convert a Uint8Array of bytes to a binary string. + * + * @param {Uint8Array} bytes - Byte array to convert + * @returns {string} Binary string representation of the input value + */ + toBin (bytes: Uint8Array): string { + return [...bytes].map(b => b.toString(2).padStart(8, '0')).join('') + }, + /** + * Sums an array of bytes to a decimal integer. If the result is larger than + * Number.MAX_SAFE_INTEGER, it will be returned as a bigint. + * + * @param {Uint8Array} bytes - Byte array to convert + * @returns {bigint|number} Decimal sum of the literal byte values + */ + toDec (bytes: Uint8Array): bigint | number { + const integers: bigint[] = [] + bytes.reverse().forEach(b => integers.push(BigInt(b))) + const decimal = integers.reduce((sum, byte, index) => { + return sum + (byte << BigInt(index * 8)) + }) + if (decimal > 9007199254740991n) { + return decimal + } else { + return Number(decimal) + } + }, + /** + * Converts a Uint8Array of bytes to a hexadecimal string. + * + * @param {Uint8Array} bytes - Byte array to convert + * @returns {string} Hexadecimal string representation of the input bytes + */ + toHex (bytes: Uint8Array): string { + const byteArray = [...bytes].map(byte => byte.toString(16).padStart(2, '0')) + return byteArray.join('').toUpperCase() + }, + /** + * Converts a Uint8Array of bytes to a UTF-8 text string. + * + * @param {Uint8Array} bytes - Byte array to convert + * @returns {string} UTF-8 encoded text string + */ + toUtf8 (bytes: Uint8Array): string { + return new TextDecoder().decode(bytes) + } +} + +export const dec = { + /** + * Convert a decimal integer to a binary string. + * + * @param {bigint|number|string} decimal - Integer to convert + * @param {number} [padding=0] - Minimum length of the resulting string which will be padded as necessary with starting zeroes + * @returns {string} Binary string representation of the input decimal + */ + toBin (decimal: bigint | number | string, padding: number = 0): string { + if (typeof padding !== 'number') { + throw new TypeError('Invalid padding') + } + try { + return BigInt(decimal) + .toString(2) + .padStart(padding, '0') + } catch (err) { + throw new RangeError('Invalid decimal integer') + } + }, + /** + * Convert a decimal integer to a Uint8Array of bytes. Fractional part is truncated. + * + * @param {bigint|number|string} decimal - Integer to convert + * @param {number} [padding=0] - Minimum length of the resulting array which will be padded as necessary with starting 0x00 bytes + * @returns {Uint8Array} Byte array representation of the input decimal + */ + toBytes (decimal: bigint | number | string, padding: number = 0): Uint8Array { + if (typeof padding !== 'number') { + throw new TypeError('Invalid padding') + } + let integer = BigInt(decimal) + const bytes = [] + while (integer > 0) { + const lsb = BigInt.asUintN(8, integer) + bytes.push(Number(lsb)) + integer >>= 8n + } + const result = new Uint8Array(Math.max(padding, bytes.length)) + result.set(bytes) + return (result.reverse()) + }, + /** + * Convert a decimal integer to a hexadecimal string. + * + * @param {(bigint|number|string)} decimal - Integer to convert + * @param {number} [padding=0] - Minimum length of the resulting string which will be padded as necessary with starting zeroes + * @returns {string} Hexadecimal string representation of the input decimal + */ + toHex (decimal: bigint | number | string, padding: number = 0): string { + if (typeof padding !== 'number') { + throw new TypeError('Invalid padding') + } + try { + return BigInt(decimal) + .toString(16) + .padStart(padding, '0') + .toUpperCase() + } catch (err) { + throw new RangeError('Invalid decimal integer') + } + } +} + +export const hex = { + /** + * Convert a hexadecimal string to a binary string. + * + * @param {string} hex - Hexadecimal number string to convert + * @returns {string} Binary string representation of the input value + */ + toBin (hex: string): string { + return [...hex].map(c => dec.toBin(parseInt(c, 16), 4)).join('') + }, + /** + * Convert a hexadecimal string to a Uint8Array of bytes. + * + * @param {string} hex - Hexadecimal number string to convert + * @param {number} [padding=0] - Minimum length of the resulting array which will be padded as necessary with starting 0x00 bytes + * @returns {Uint8Array} Byte array representation of the input value + */ + toBytes (hex: string, padding: number = 0): Uint8Array { + if (typeof padding !== 'number') { + throw new TypeError('Invalid padding when converting hex to bytes') + } + const hexArray = hex.match(/.{1,2}/g) + if (!/^[0-9a-f]+$/i.test(hex) || hexArray == null) { + console.warn('Invalid hex string when converting to bytes') + return new Uint8Array() + } else { + const bytes = Uint8Array.from(hexArray.map(byte => parseInt(byte, 16))) + const result = new Uint8Array(Math.max(padding, bytes.length)) + result.set(bytes.reverse()) + return result.reverse() + } + } +} + +export const utf8 = { + /** + * Convert a UTF-8 text string to a Uint8Array of bytes. + * + * @param {string} utf8 - String to convert + * @returns {Uint8Array} Byte array representation of the input string + */ + toBytes (utf8: string): Uint8Array { + return new TextEncoder().encode(utf8) + }, + /** + * Convert a string to a hexadecimal representation + * + * @param {string} utf8 - String to convert + * @returns {string} Hexadecimal representation of the input string + */ + toHex (utf8: string): string { + return bytes.toHex(this.toBytes(utf8)) + } +} + +export default { base32, bin, bytes, dec, hex, utf8 } diff --git a/src/lib/curve25519.ts b/src/lib/curve25519.ts new file mode 100644 index 0000000..14ba60f --- /dev/null +++ b/src/lib/curve25519.ts @@ -0,0 +1,695 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later + +import { blake2b } from 'blakejs' + +/** +* Derived from: +* - mipher +* - tweetnacl +* - ed2curve-js +* +* With added types etc +*/ +export default class Curve25519 { + gf0: Int32Array + gf1: Int32Array + D: Int32Array + D2: Int32Array + I: Int32Array + _9: Uint8Array + _121665: Int32Array + _0: Uint8Array + sigma: Uint8Array + minusp: Uint32Array + + constructor () { + this.gf0 = this.gf() + this.gf1 = this.gf([1]) + this._9 = new Uint8Array(32) + this._9[0] = 9 + this._121665 = this.gf([0xdb41, 1]) + this.D = this.gf([0x78a3, 0x1359, 0x4dca, 0x75eb, 0xd8ab, 0x4141, 0x0a4d, 0x0070, 0xe898, 0x7779, 0x4079, 0x8cc7, 0xfe73, 0x2b6f, 0x6cee, 0x5203]) + this.D2 = this.gf([0xf159, 0x26b2, 0x9b94, 0xebd6, 0xb156, 0x8283, 0x149a, 0x00e0, 0xd130, 0xeef3, 0x80f2, 0x198e, 0xfce7, 0x56df, 0xd9dc, 0x2406]) + this.I = this.gf([0xa0b0, 0x4a0e, 0x1b27, 0xc4ee, 0xe478, 0xad2f, 0x1806, 0x2f43, 0xd7a7, 0x3dfb, 0x0099, 0x2b4d, 0xdf0b, 0x4fc1, 0x2480, 0x2b83]) + this._0 = new Uint8Array(16) + this.sigma = new Uint8Array([101, 120, 112, 97, 110, 100, 32, 51, 50, 45, 98, 121, 116, 101, 32, 107]) + this.minusp = new Uint32Array([5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 252]) + } + + gf (init?: number[]): Int32Array { + const r = new Int32Array(16) + if (init) { + for (let i = 0; i < init.length; i++) { + r[i] = init[i] + } + } + + return r + } + + A (o: Int32Array, a: Int32Array, b: Int32Array): void { + for (let i = 0; i < 16; i++) { + o[i] = a[i] + b[i] + } + } + + Z (o: Int32Array, a: Int32Array, b: Int32Array): void { + for (let i = 0; i < 16; i++) { + o[i] = a[i] - b[i] + } + } + + // Avoid loops for better performance + M (o: Int32Array, a: Int32Array, b: Int32Array): void { + let v, c, + t0 = 0, t1 = 0, t2 = 0, t3 = 0, t4 = 0, t5 = 0, t6 = 0, t7 = 0, + t8 = 0, t9 = 0, t10 = 0, t11 = 0, t12 = 0, t13 = 0, t14 = 0, t15 = 0, + t16 = 0, t17 = 0, t18 = 0, t19 = 0, t20 = 0, t21 = 0, t22 = 0, t23 = 0, + t24 = 0, t25 = 0, t26 = 0, t27 = 0, t28 = 0, t29 = 0, t30 = 0 + const b0 = b[0], + b1 = b[1], + b2 = b[2], + b3 = b[3], + b4 = b[4], + b5 = b[5], + b6 = b[6], + b7 = b[7], + b8 = b[8], + b9 = b[9], + b10 = b[10], + b11 = b[11], + b12 = b[12], + b13 = b[13], + b14 = b[14], + b15 = b[15] + + v = a[0] + t0 += v * b0 + t1 += v * b1 + t2 += v * b2 + t3 += v * b3 + t4 += v * b4 + t5 += v * b5 + t6 += v * b6 + t7 += v * b7 + t8 += v * b8 + t9 += v * b9 + t10 += v * b10 + t11 += v * b11 + t12 += v * b12 + t13 += v * b13 + t14 += v * b14 + t15 += v * b15 + v = a[1] + t1 += v * b0 + t2 += v * b1 + t3 += v * b2 + t4 += v * b3 + t5 += v * b4 + t6 += v * b5 + t7 += v * b6 + t8 += v * b7 + t9 += v * b8 + t10 += v * b9 + t11 += v * b10 + t12 += v * b11 + t13 += v * b12 + t14 += v * b13 + t15 += v * b14 + t16 += v * b15 + v = a[2] + t2 += v * b0 + t3 += v * b1 + t4 += v * b2 + t5 += v * b3 + t6 += v * b4 + t7 += v * b5 + t8 += v * b6 + t9 += v * b7 + t10 += v * b8 + t11 += v * b9 + t12 += v * b10 + t13 += v * b11 + t14 += v * b12 + t15 += v * b13 + t16 += v * b14 + t17 += v * b15 + v = a[3] + t3 += v * b0 + t4 += v * b1 + t5 += v * b2 + t6 += v * b3 + t7 += v * b4 + t8 += v * b5 + t9 += v * b6 + t10 += v * b7 + t11 += v * b8 + t12 += v * b9 + t13 += v * b10 + t14 += v * b11 + t15 += v * b12 + t16 += v * b13 + t17 += v * b14 + t18 += v * b15 + v = a[4] + t4 += v * b0 + t5 += v * b1 + t6 += v * b2 + t7 += v * b3 + t8 += v * b4 + t9 += v * b5 + t10 += v * b6 + t11 += v * b7 + t12 += v * b8 + t13 += v * b9 + t14 += v * b10 + t15 += v * b11 + t16 += v * b12 + t17 += v * b13 + t18 += v * b14 + t19 += v * b15 + v = a[5] + t5 += v * b0 + t6 += v * b1 + t7 += v * b2 + t8 += v * b3 + t9 += v * b4 + t10 += v * b5 + t11 += v * b6 + t12 += v * b7 + t13 += v * b8 + t14 += v * b9 + t15 += v * b10 + t16 += v * b11 + t17 += v * b12 + t18 += v * b13 + t19 += v * b14 + t20 += v * b15 + v = a[6] + t6 += v * b0 + t7 += v * b1 + t8 += v * b2 + t9 += v * b3 + t10 += v * b4 + t11 += v * b5 + t12 += v * b6 + t13 += v * b7 + t14 += v * b8 + t15 += v * b9 + t16 += v * b10 + t17 += v * b11 + t18 += v * b12 + t19 += v * b13 + t20 += v * b14 + t21 += v * b15 + v = a[7] + t7 += v * b0 + t8 += v * b1 + t9 += v * b2 + t10 += v * b3 + t11 += v * b4 + t12 += v * b5 + t13 += v * b6 + t14 += v * b7 + t15 += v * b8 + t16 += v * b9 + t17 += v * b10 + t18 += v * b11 + t19 += v * b12 + t20 += v * b13 + t21 += v * b14 + t22 += v * b15 + v = a[8] + t8 += v * b0 + t9 += v * b1 + t10 += v * b2 + t11 += v * b3 + t12 += v * b4 + t13 += v * b5 + t14 += v * b6 + t15 += v * b7 + t16 += v * b8 + t17 += v * b9 + t18 += v * b10 + t19 += v * b11 + t20 += v * b12 + t21 += v * b13 + t22 += v * b14 + t23 += v * b15 + v = a[9] + t9 += v * b0 + t10 += v * b1 + t11 += v * b2 + t12 += v * b3 + t13 += v * b4 + t14 += v * b5 + t15 += v * b6 + t16 += v * b7 + t17 += v * b8 + t18 += v * b9 + t19 += v * b10 + t20 += v * b11 + t21 += v * b12 + t22 += v * b13 + t23 += v * b14 + t24 += v * b15 + v = a[10] + t10 += v * b0 + t11 += v * b1 + t12 += v * b2 + t13 += v * b3 + t14 += v * b4 + t15 += v * b5 + t16 += v * b6 + t17 += v * b7 + t18 += v * b8 + t19 += v * b9 + t20 += v * b10 + t21 += v * b11 + t22 += v * b12 + t23 += v * b13 + t24 += v * b14 + t25 += v * b15 + v = a[11] + t11 += v * b0 + t12 += v * b1 + t13 += v * b2 + t14 += v * b3 + t15 += v * b4 + t16 += v * b5 + t17 += v * b6 + t18 += v * b7 + t19 += v * b8 + t20 += v * b9 + t21 += v * b10 + t22 += v * b11 + t23 += v * b12 + t24 += v * b13 + t25 += v * b14 + t26 += v * b15 + v = a[12] + t12 += v * b0 + t13 += v * b1 + t14 += v * b2 + t15 += v * b3 + t16 += v * b4 + t17 += v * b5 + t18 += v * b6 + t19 += v * b7 + t20 += v * b8 + t21 += v * b9 + t22 += v * b10 + t23 += v * b11 + t24 += v * b12 + t25 += v * b13 + t26 += v * b14 + t27 += v * b15 + v = a[13] + t13 += v * b0 + t14 += v * b1 + t15 += v * b2 + t16 += v * b3 + t17 += v * b4 + t18 += v * b5 + t19 += v * b6 + t20 += v * b7 + t21 += v * b8 + t22 += v * b9 + t23 += v * b10 + t24 += v * b11 + t25 += v * b12 + t26 += v * b13 + t27 += v * b14 + t28 += v * b15 + v = a[14] + t14 += v * b0 + t15 += v * b1 + t16 += v * b2 + t17 += v * b3 + t18 += v * b4 + t19 += v * b5 + t20 += v * b6 + t21 += v * b7 + t22 += v * b8 + t23 += v * b9 + t24 += v * b10 + t25 += v * b11 + t26 += v * b12 + t27 += v * b13 + t28 += v * b14 + t29 += v * b15 + v = a[15] + t15 += v * b0 + t16 += v * b1 + t17 += v * b2 + t18 += v * b3 + t19 += v * b4 + t20 += v * b5 + t21 += v * b6 + t22 += v * b7 + t23 += v * b8 + t24 += v * b9 + t25 += v * b10 + t26 += v * b11 + t27 += v * b12 + t28 += v * b13 + t29 += v * b14 + t30 += v * b15 + + t0 += 38 * t16 + t1 += 38 * t17 + t2 += 38 * t18 + t3 += 38 * t19 + t4 += 38 * t20 + t5 += 38 * t21 + t6 += 38 * t22 + t7 += 38 * t23 + t8 += 38 * t24 + t9 += 38 * t25 + t10 += 38 * t26 + t11 += 38 * t27 + t12 += 38 * t28 + t13 += 38 * t29 + t14 += 38 * t30 + + c = 1 + v = t0 + c + 65535; c = Math.floor(v / 65536); t0 = v - c * 65536 + v = t1 + c + 65535; c = Math.floor(v / 65536); t1 = v - c * 65536 + v = t2 + c + 65535; c = Math.floor(v / 65536); t2 = v - c * 65536 + v = t3 + c + 65535; c = Math.floor(v / 65536); t3 = v - c * 65536 + v = t4 + c + 65535; c = Math.floor(v / 65536); t4 = v - c * 65536 + v = t5 + c + 65535; c = Math.floor(v / 65536); t5 = v - c * 65536 + v = t6 + c + 65535; c = Math.floor(v / 65536); t6 = v - c * 65536 + v = t7 + c + 65535; c = Math.floor(v / 65536); t7 = v - c * 65536 + v = t8 + c + 65535; c = Math.floor(v / 65536); t8 = v - c * 65536 + v = t9 + c + 65535; c = Math.floor(v / 65536); t9 = v - c * 65536 + v = t10 + c + 65535; c = Math.floor(v / 65536); t10 = v - c * 65536 + v = t11 + c + 65535; c = Math.floor(v / 65536); t11 = v - c * 65536 + v = t12 + c + 65535; c = Math.floor(v / 65536); t12 = v - c * 65536 + v = t13 + c + 65535; c = Math.floor(v / 65536); t13 = v - c * 65536 + v = t14 + c + 65535; c = Math.floor(v / 65536); t14 = v - c * 65536 + v = t15 + c + 65535; c = Math.floor(v / 65536); t15 = v - c * 65536 + t0 += c - 1 + 37 * (c - 1) + + c = 1 + v = t0 + c + 65535; c = Math.floor(v / 65536); t0 = v - c * 65536 + v = t1 + c + 65535; c = Math.floor(v / 65536); t1 = v - c * 65536 + v = t2 + c + 65535; c = Math.floor(v / 65536); t2 = v - c * 65536 + v = t3 + c + 65535; c = Math.floor(v / 65536); t3 = v - c * 65536 + v = t4 + c + 65535; c = Math.floor(v / 65536); t4 = v - c * 65536 + v = t5 + c + 65535; c = Math.floor(v / 65536); t5 = v - c * 65536 + v = t6 + c + 65535; c = Math.floor(v / 65536); t6 = v - c * 65536 + v = t7 + c + 65535; c = Math.floor(v / 65536); t7 = v - c * 65536 + v = t8 + c + 65535; c = Math.floor(v / 65536); t8 = v - c * 65536 + v = t9 + c + 65535; c = Math.floor(v / 65536); t9 = v - c * 65536 + v = t10 + c + 65535; c = Math.floor(v / 65536); t10 = v - c * 65536 + v = t11 + c + 65535; c = Math.floor(v / 65536); t11 = v - c * 65536 + v = t12 + c + 65535; c = Math.floor(v / 65536); t12 = v - c * 65536 + v = t13 + c + 65535; c = Math.floor(v / 65536); t13 = v - c * 65536 + v = t14 + c + 65535; c = Math.floor(v / 65536); t14 = v - c * 65536 + v = t15 + c + 65535; c = Math.floor(v / 65536); t15 = v - c * 65536 + t0 += c - 1 + 37 * (c - 1) + + o[0] = t0 + o[1] = t1 + o[2] = t2 + o[3] = t3 + o[4] = t4 + o[5] = t5 + o[6] = t6 + o[7] = t7 + o[8] = t8 + o[9] = t9 + o[10] = t10 + o[11] = t11 + o[12] = t12 + o[13] = t13 + o[14] = t14 + o[15] = t15 + } + + S (o: Int32Array, a: Int32Array): void { + this.M(o, a, a) + } + + add (p: Int32Array[], q: Int32Array[]): void { + const a = this.gf(), b = this.gf(), c = this.gf(), + d = this.gf(), e = this.gf(), f = this.gf(), + g = this.gf(), h = this.gf(), t = this.gf() + + this.Z(a, p[1], p[0]) + this.Z(t, q[1], q[0]) + this.M(a, a, t) + this.A(b, p[0], p[1]) + this.A(t, q[0], q[1]) + this.M(b, b, t) + this.M(c, p[3], q[3]) + this.M(c, c, this.D2) + this.M(d, p[2], q[2]) + this.A(d, d, d) + this.Z(e, b, a) + this.Z(f, d, c) + this.A(g, d, c) + this.A(h, b, a) + this.M(p[0], e, f) + this.M(p[1], h, g) + this.M(p[2], g, f) + this.M(p[3], e, h) + } + + set25519 (r: Int32Array, a: Int32Array): void { + for (let i = 0; i < 16; i++) { + r[i] = a[i] + } + } + + car25519 (o: Int32Array): void { + let i, v, c = 1 + for (i = 0; i < 16; i++) { + v = o[i] + c + 65535 + c = Math.floor(v / 65536) + o[i] = v - c * 65536 + } + + o[0] += c - 1 + 37 * (c - 1) + } + + // b is 0 or 1 + sel25519 (p: Int32Array, q: Int32Array, b: number): void { + let i, t + const c = ~(b - 1) + for (i = 0; i < 16; i++) { + t = c & (p[i] ^ q[i]) + p[i] ^= t + q[i] ^= t + } + } + + inv25519 (o: Int32Array, i: Int32Array): void { + let a + const c = this.gf() + for (a = 0; a < 16; a++) { + c[a] = i[a] + } + + for (a = 253; a >= 0; a--) { + this.S(c, c) + if (a !== 2 && a !== 4) { + this.M(c, c, i) + } + } + + for (a = 0; a < 16; a++) { + o[a] = c[a] + } + } + + neq25519 (a: Int32Array, b: Int32Array): boolean { + const c = new Uint8Array(32), d = new Uint8Array(32) + this.pack25519(c, a) + this.pack25519(d, b) + if (c.length !== d.length) return true + for (let i = 0; i < c.length; i++) { + if (c[i] !== d[i]) return true + } + return false + } + + par25519 (a: Int32Array): number { + const d = new Uint8Array(32) + this.pack25519(d, a) + return d[0] & 1 + } + + pow2523 (o: Int32Array, i: Int32Array): void { + let a + const c = this.gf() + for (a = 0; a < 16; a++) { + c[a] = i[a] + } + + for (a = 250; a >= 0; a--) { + this.S(c, c) + if (a !== 1) this.M(c, c, i) + } + + for (a = 0; a < 16; a++) { + o[a] = c[a] + } + } + + cswap (p: Int32Array[], q: Int32Array[], b: number): void { + for (let i = 0; i < 4; i++) { + this.sel25519(p[i], q[i], b) + } + } + + pack25519 (o: Uint8Array, n: Int32Array): void { + let i + const m = this.gf() + const t = this.gf() + for (i = 0; i < 16; i++) { + t[i] = n[i] + } + + this.car25519(t) + this.car25519(t) + this.car25519(t) + for (let j = 0; j < 2; j++) { + m[0] = t[0] - 0xffed + for (i = 1; i < 15; i++) { + m[i] = t[i] - 0xffff - ((m[i - 1] >>> 16) & 1) + m[i - 1] &= 0xffff + } + + m[15] = t[15] - 0x7fff - ((m[14] >>> 16) & 1) + const b = (m[15] >>> 16) & 1 + m[14] &= 0xffff + this.sel25519(t, m, 1 - b) + } + + for (i = 0; i < 16; i++) { + o[2 * i] = t[i] & 0xff + o[2 * i + 1] = t[i] >>> 8 + } + } + + unpack25519 (o: Int32Array, n: Uint8Array): void { + for (let i = 0; i < 16; i++) { + o[i] = n[2 * i] + (n[2 * i + 1] << 8) + } + + o[15] &= 0x7fff + } + + unpackNeg (r: Int32Array[], p: Uint8Array): number { + const t = this.gf(), + chk = this.gf(), + num = this.gf(), + den = this.gf(), + den2 = this.gf(), + den4 = this.gf(), + den6 = this.gf() + + this.set25519(r[2], this.gf1) + this.unpack25519(r[1], p) + this.S(num, r[1]) + this.M(den, num, this.D) + this.Z(num, num, r[2]) + this.A(den, r[2], den) + + this.S(den2, den) + this.S(den4, den2) + this.M(den6, den4, den2) + this.M(t, den6, num) + this.M(t, t, den) + + this.pow2523(t, t) + this.M(t, t, num) + this.M(t, t, den) + this.M(t, t, den) + this.M(r[0], t, den) + + this.S(chk, r[0]) + this.M(chk, chk, den) + if (this.neq25519(chk, num)) { + this.M(r[0], r[0], this.I) + } + + this.S(chk, r[0]) + this.M(chk, chk, den) + if (this.neq25519(chk, num)) { + return -1 + } + + if (this.par25519(r[0]) === (p[31] >>> 7)) { + this.Z(r[0], this.gf0, r[0]) + } + + this.M(r[3], r[0], r[1]) + + return 0 + } + + /** + * Converts a ed25519 public key to Curve25519 to be used in + * Diffie-Hellman key exchange + */ + convertEd25519PublicKeyToCurve25519 (pk: Uint8Array) { + const z = new Uint8Array(32) + const q = [this.gf(), this.gf(), this.gf(), this.gf()] + const a = this.gf() + const b = this.gf() + + if (this.unpackNeg(q, pk)) { + return null + } + + const y = q[1] + + this.A(a, this.gf1, y) + this.Z(b, this.gf1, y) + this.inv25519(b, b) + this.M(a, a, b) + + this.pack25519(z, a) + + return z + } + + /** + * Converts a ed25519 private key to Curve25519 to be used in + * Diffie-Hellman key exchange + */ + convertEd25519PrivateKeyToCurve25519 (sk: Uint8Array) { + const d = new Uint8Array(64) + const o = new Uint8Array(32) + let i + + this.cryptoHash(d, sk, 32) + d[0] &= 248 + d[31] &= 127 + d[31] |= 64 + + for (i = 0; i < 32; i++) { + o[i] = d[i] + } + for (i = 0; i < 64; i++) { + d[i] = 0 + } + return o + } + + cryptoHash (out: Uint8Array, m: Uint8Array, n: number): number { + const input = new Uint8Array(n) + for (let i = 0; i < n; ++i) { + input[i] = m[i] + } + const hash = blake2b(input) + for (let i = 0; i < 64; ++i) { + out[i] = hash[i] + } + return 0 + } +} diff --git a/src/lib/ed25519.ts b/src/lib/ed25519.ts new file mode 100644 index 0000000..395a409 --- /dev/null +++ b/src/lib/ed25519.ts @@ -0,0 +1,255 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later + +import blakejs from 'blakejs' +const { blake2b, blake2bInit, blake2bUpdate, blake2bFinal } = blakejs +import { bytes, hex } from './convert.js' +import Curve25519 from './curve25519.js' + +type KeyPair = { + privateKey: string + publicKey: string +} + +const curve = new Curve25519() +const X: Int32Array = curve.gf([0xd51a, 0x8f25, 0x2d60, 0xc956, 0xa7b2, 0x9525, 0xc760, 0x692c, 0xdc5c, 0xfdd6, 0xe231, 0xc0a4, 0x53fe, 0xcd6e, 0x36d3, 0x2169]) +const Y: Int32Array = curve.gf([0x6658, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666]) +const L: Uint8Array = new Uint8Array([0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10]) + +/** +* Generate a public key from a private key using the Ed25519 algorithm. The key +* should be a cryptographically strong random value. +* +* @param {string} privateKey - 32-byte hexadecimal private key +* @returns {string} 32-byte hexadecimal public key +*/ +function getPublicKey (privateKey: string): string { + const pk = new Uint8Array(32) + const p = [curve.gf(), curve.gf(), curve.gf(), curve.gf()] + const h = blake2b(hex.toBytes(privateKey), undefined, 64).slice(0, 32) + + h[0] &= 0xf8 + h[31] &= 0x7f + h[31] |= 0x40 + + scalarbase(p, h) + pack(pk, p) + + return bytes.toHex(pk) +} + +/** +* Convert Ed25519 keypair to Curve25519 keypair suitable for Diffie-Hellman key exchange +* +* @param {KeyPair} keyPair - Ed25519 keypair +* @returns {KeyPair} Curve25519 keypair +*/ +function convertKeys (keyPair: KeyPair): KeyPair { + const pubKeyBuf = hex.toBytes(keyPair.publicKey) + const ab: (Uint8Array | null) = curve.convertEd25519PublicKeyToCurve25519(pubKeyBuf) + if (ab == null) { + throw new Error('Invalid key pair') + } + const publicKey = bytes.toHex(ab) ?? '' + if (publicKey === '') { + throw new Error('Invalid key pair') + } + const privKeyBuf = hex.toBytes(keyPair.privateKey) + const privateKey = bytes.toHex(curve.convertEd25519PrivateKeyToCurve25519(privKeyBuf)) + return { + publicKey, + privateKey, + } +} + +/** +* Generate a message signature +* @param {Uint8Array} msg - Message to be signed +* @param {Uint8Array} privateKey - Private key to use for signing +* @returns {Uint8Array} 64-byte signature +*/ +function sign (msg: Uint8Array, privateKey: Uint8Array): Uint8Array { + return naclSign(msg, privateKey).slice(0, 64) +} + +/** +* Verify a message signature +* @param {Uint8Array} msg - Message to be signed as byte array +* @param {Uint8Array} publicKey - Public key as byte array +* @param {Uint8Array} signature - Signature as byte array +* @returns {boolean} True if `msg` was signed by `publicKey`, else false +*/ +function verify (msg: Uint8Array, publicKey: Uint8Array, signature: Uint8Array): boolean { + const CURVE = curve + const p = [CURVE.gf(), CURVE.gf(), CURVE.gf(), CURVE.gf()] + const q = [CURVE.gf(), CURVE.gf(), CURVE.gf(), CURVE.gf()] + + if (signature.length !== 64) { + return false + } + if (publicKey.length !== 32) { + return false + } + if (CURVE.unpackNeg(q, publicKey)) { + return false + } + + const ctx = blake2bInit(64, undefined) + blake2bUpdate(ctx, signature.subarray(0, 32)) + blake2bUpdate(ctx, publicKey) + blake2bUpdate(ctx, msg) + let k = blake2bFinal(ctx) + reduce(k) + scalarmult(p, q, k) + + let t = new Uint8Array(32) + scalarbase(q, signature.subarray(32)) + CURVE.add(p, q) + pack(t, p) + + if (signature.subarray(0, 32).length !== t.length) return false + for (let i = 0; i < t.length; i++) { + if (signature.subarray(0, 32)[i] !== t[i]) return false + } + return true +} + +function pack (r: Uint8Array, p: Int32Array[]): void { + const CURVE = curve + const tx = CURVE.gf(), + ty = CURVE.gf(), + zi = CURVE.gf() + CURVE.inv25519(zi, p[2]) + CURVE.M(tx, p[0], zi) + CURVE.M(ty, p[1], zi) + CURVE.pack25519(r, ty) + r[31] ^= CURVE.par25519(tx) << 7 +} + +function modL (r: Uint8Array, x: Uint32Array | Float64Array): void { + let carry, i, j, k + for (i = 63; i >= 32; --i) { + carry = 0 + for (j = i - 32, k = i - 12; j < k; ++j) { + x[j] += carry - 16 * x[i] * L[j - (i - 32)] + carry = (x[j] + 128) >> 8 + x[j] -= carry * 256 + } + x[j] += carry + x[i] = 0 + } + + carry = 0 + for (j = 0; j < 32; j++) { + x[j] += carry - (x[31] >> 4) * L[j] + carry = x[j] >> 8 + x[j] &= 255 + } + for (j = 0; j < 32; j++) { + x[j] -= carry * L[j] + } + for (i = 0; i < 32; i++) { + x[i + 1] += x[i] >>> 8 + r[i] = x[i] & 0xff + } +} + +function reduce (r: Uint8Array): void { + const x = new Uint32Array(64) + for (let i = 0; i < 64; i++) { + x[i] = r[i] + } + modL(r, x) +} + +function scalarmult (p: Int32Array[], q: Int32Array[], s: Uint8Array): void { + const CURVE = curve + CURVE.set25519(p[0], CURVE.gf0) + CURVE.set25519(p[1], CURVE.gf1) + CURVE.set25519(p[2], CURVE.gf1) + CURVE.set25519(p[3], CURVE.gf0) + for (let i = 255; i >= 0; --i) { + const b = (s[(i / 8) | 0] >>> (i & 7)) & 1 + CURVE.cswap(p, q, b) + CURVE.add(q, p) + CURVE.add(p, p) + CURVE.cswap(p, q, b) + } +} + +function scalarbase (p: Int32Array[], s: Uint8Array): void { + const CURVE = curve + const q = [CURVE.gf(), CURVE.gf(), CURVE.gf(), CURVE.gf()] + CURVE.set25519(q[0], X) + CURVE.set25519(q[1], Y) + CURVE.set25519(q[2], CURVE.gf1) + CURVE.M(q[3], X, Y) + scalarmult(p, q, s) +} + +function naclSign (msg: Uint8Array, privateKey: Uint8Array): Uint8Array { + if (privateKey.length !== 32) { + throw new Error('bad private key size') + } + const signedMsg = new Uint8Array(64 + msg.length) + cryptoSign(signedMsg, msg, msg.length, privateKey) + return signedMsg +} + +function cryptoSign (sm: Uint8Array, m: Uint8Array, n: number, sk: Uint8Array): number { + const CURVE = curve + const d = new Uint8Array(64) + const h = new Uint8Array(64) + const r = new Uint8Array(64) + const x = new Float64Array(64) + const p = [CURVE.gf(), CURVE.gf(), CURVE.gf(), CURVE.gf()] + + let i + let j + + const pubKey = getPublicKey(bytes.toHex(sk)) + const pk = hex.toBytes(pubKey) + + curve.cryptoHash(d, sk, 32) + d[0] &= 248 + d[31] &= 127 + d[31] |= 64 + + const smlen = n + 64 + for (i = 0; i < n; i++) { + sm[64 + i] = m[i] + } + for (i = 0; i < 32; i++) { + sm[32 + i] = d[32 + i] + } + + curve.cryptoHash(r, sm.subarray(32), n + 32) + reduce(r) + scalarbase(p, r) + pack(sm, p) + + for (i = 32; i < 64; i++) { + sm[i] = pk[i - 32] + } + + curve.cryptoHash(h, sm, n + 64) + reduce(h) + + for (i = 0; i < 64; i++) { + x[i] = 0 + } + for (i = 0; i < 32; i++) { + x[i] = r[i] + } + for (i = 0; i < 32; i++) { + for (j = 0; j < 32; j++) { + x[i + j] += h[i] * d[j] + } + } + + modL(sm.subarray(32), x) + + return smlen +} + +export default { convertKeys, getPublicKey, sign, verify } diff --git a/src/lib/entropy.ts b/src/lib/entropy.ts new file mode 100644 index 0000000..3437fbd --- /dev/null +++ b/src/lib/entropy.ts @@ -0,0 +1,120 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later + +import { bytes, hex } from './convert.js' + +const { crypto } = globalThis +const MIN = 16 +const MAX = 32 +const MOD = 4 + +/** +* Represents a cryptographically strong source of entropy suitable for use in +* BIP-39 mnemonic phrase generation and consequently BIP-44 key derivation. +* +* The constructor will accept one of several different data types under certain +* constraints. If the constraints are not met, an error will be thrown. If no +* value, or the equivalent of no value, is passed to the constructor, then a +* brand new source of entropy will be generated at the maximum size of 256 bits. +*/ +export class Entropy { + #bits: string + #buffer: ArrayBuffer + #bytes: Uint8Array + #hex: string + + get bits () { return this.#bits } + get buffer () { return this.#buffer } + get bytes () { return this.#bytes } + get hex () { return this.#hex } + + /** + * Generate 256 bits of entropy. + */ + constructor () + /** + * Generate between 16-32 bytes of entropy. + * @param {number} size - Number of bytes to generate + */ + constructor (size: number) + /** + * Import existing entropy and validate it. + * @param {string} hex - Hexadecimal string + */ + constructor (hex: string) + /** + * Import existing entropy and validate it. + * @param {ArrayBuffer} buffer - Byte buffer + */ + constructor (buffer: ArrayBuffer) + /** + * Import existing entropy and validate it. + * @param {Uint8Array} bytes - Byte array + */ + constructor (bytes: Uint8Array) + constructor (input?: number | string | ArrayBuffer | Uint8Array) { + if (typeof input === 'number' && input > 0) { + if (input < MIN || input > MAX) { + throw new RangeError(`Entropy must be ${MIN}-${MAX} bytes`) + } + if (input % MOD !== 0) { + throw new RangeError(`Entropy must be a multiple of ${MOD} bytes`) + } + this.#bytes = crypto.getRandomValues(new Uint8Array(input)) + this.#hex = bytes.toHex(this.#bytes) + this.#bits = hex.toBin(this.#hex) + this.#buffer = this.#bytes.buffer + return + } + + if (typeof input === 'string' && input.length > 0) { + if (input.length < MIN * 2 || input.length > MAX * 2) { + throw new RangeError(`Entropy must be ${MIN * 2}-${MAX * 2} characters`) + } + if (input.length % MOD * 2 !== 0) { + throw new RangeError(`Entropy must be a multiple of ${MOD * 2} characters`) + } + this.#hex = input + if (!/^[0-9a-fA-F]+$/i.test(this.#hex)) { + throw new RangeError('Entropy contains invalid hexadecimal characters') + } + this.#bytes = hex.toBytes(this.#hex) + this.#bits = hex.toBin(this.#hex) + this.#buffer = this.#bytes.buffer + return + } + + if (input instanceof ArrayBuffer && input.byteLength > 0) { + if (input.byteLength < MIN || input.byteLength > MAX) { + throw new Error(`Entropy must be ${MIN}-${MAX} bytes`) + } + if (input.byteLength % MOD !== 0) { + throw new RangeError(`Entropy must be a multiple of ${MOD} bytes`) + } + this.#buffer = input + this.#bytes = new Uint8Array(this.#buffer) + this.#bits = bytes.toBin(this.#bytes) + this.#hex = bytes.toHex(this.#bytes) + return + } + + if (input instanceof Uint8Array && input.length > 0) { + if (input.length < MIN || input.length > MAX) { + throw new Error(`Entropy must be ${MIN}-${MAX} bytes`) + } + if (input.length % MOD !== 0) { + throw new RangeError(`Entropy must be a multiple of ${MOD} bytes`) + } + this.#bytes = input + this.#bits = bytes.toBin(this.#bytes) + this.#buffer = this.#bytes.buffer + this.#hex = bytes.toHex(this.#bytes) + return + } + + this.#bytes = crypto.getRandomValues(new Uint8Array(MAX)) + this.#hex = bytes.toHex(this.#bytes) + this.#bits = hex.toBin(this.#hex) + this.#buffer = this.#bytes.buffer + } +} diff --git a/src/lib/ledger.ts b/src/lib/ledger.ts new file mode 100644 index 0000000..5cf5bf9 --- /dev/null +++ b/src/lib/ledger.ts @@ -0,0 +1,360 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later + +/** +* Ledger ADPU commands: https://github.com/roosmaa/ledger-app-nano/blob/master/doc/nano.md +*/ + +import Transport from '@ledgerhq/hw-transport' +import { default as TransportBLE } from '@ledgerhq/hw-transport-web-ble' +import { default as TransportUSB } from '@ledgerhq/hw-transport-webusb' +import { default as TransportHID } from '@ledgerhq/hw-transport-webhid' +import { BIP44_COIN_NANO, BIP44_PURPOSE, HARDENED_OFFSET, LEDGER_STATUS_CODES } from './constants.js' +import { bytes, dec, hex, utf8 } from './convert.js' +import { Node } from './node.js' +import { ChangeBlock, ReceiveBlock, SendBlock } from './block.js' + +export class Ledger { + static #isInternal: boolean = false + #status: 'DISCONNECTED' | 'LOCKED' | 'BUSY' | 'CONNECTED' = 'DISCONNECTED' + get status () { return this.#status } + openTimeout = 3000 + listenTimeout = 30000; + transport: Transport | null = null + DynamicTransport: typeof TransportBLE | typeof TransportUSB | typeof TransportHID = TransportHID + + constructor () { + if (!Ledger.#isInternal) { + throw new Error('Ledger cannot be instantiated directly. Use Ledger.init()') + } + Ledger.#isInternal = false + } + + static async init (): Promise { + Ledger.#isInternal = true + const self = new this() + await self.checkBrowserSupport() + await self.listen() + return self + } + + /** + * Check which transport protocols are supported by the browser and set the + * transport type according to the following priorities: Bluetooth, USB, HID. + */ + async checkBrowserSupport (): Promise { + console.log('Checking browser Ledger support...') + const supports = { + ble: await TransportBLE.isSupported(), + usb: await TransportUSB.isSupported(), + hid: await TransportHID.isSupported() + } + console.log(`ble: ${supports.ble}; usb: ${supports.usb}; hid: ${supports.hid}`) + if (supports.ble) { + this.DynamicTransport = TransportBLE + } else if (supports.usb) { + this.DynamicTransport = TransportUSB + } else if (supports.hid) { + this.DynamicTransport = TransportHID + } else { + throw new Error('Unsupported browser') + } + } + + async listen (): Promise { + const { usb } = globalThis.navigator + if (usb) { + usb.addEventListener('connect', console.log.bind(console)) + usb.addEventListener('disconnect', console.log.bind(console)) + } + } + + async connect (): Promise { + const { usb } = globalThis.navigator + if (usb) { + usb.removeEventListener('disconnect', this.onDisconnectUsb.bind(this)) + usb.addEventListener('disconnect', this.onDisconnectUsb.bind(this)) + } + const version = await this.version() + if (version.status === 'OK') { + if (version.name === 'Nano') { + const account = await this.account() + if (account.status === 'OK') { + this.#status = 'CONNECTED' + } else if (account.status === 'SECURITY_STATUS_NOT_SATISFIED') { + this.#status = 'LOCKED' + } else { + this.#status = 'DISCONNECTED' + } + } else if (version.name === 'BOLOS') { + const open = await this.open() + this.#status = (open.status === 'OK') + ? 'CONNECTED' + : 'DISCONNECTED' + } else { + this.#status = 'BUSY' + } + } else { + this.#status = 'DISCONNECTED' + } + return this.status + } + + async onDisconnectUsb (e: USBConnectionEvent) { + if (e.device?.manufacturerName === 'Ledger') { + const { usb } = globalThis.navigator + usb.removeEventListener('disconnect', this.onDisconnectUsb) + this.#status = 'DISCONNECTED' + } + } + + /** + * Open Nano app by launching user flow. + * + * https://developers.ledger.com/docs/connectivity/ledgerJS/open-close-info-on-apps#open-application + * + * This command resets the internal USB connection of the device which can + * cause subsequent commands to fail if called too quickly. A one-second delay + * is implemented in this method to mitigate the issue. + * + * https://github.com/LedgerHQ/ledger-live/issues/4964#issuecomment-1878361157 + * + * @returns Status of command + */ + async open (): Promise<{ status: string }> { + const name = new TextEncoder().encode('Nano') + const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout) + const response = await transport.send(0xe0, 0xd8, 0x00, 0x00, name as Buffer) + .then(res => bytes.toDec(res)) + .catch(err => err.statusCode) as number + return new Promise(resolve => setTimeout(resolve, 1000, { status: LEDGER_STATUS_CODES[response] })) + } + + /** + * Close the currently running app. + * + * https://developers.ledger.com/docs/connectivity/ledgerJS/open-close-info-on-apps#quit-application + * + * This command resets the internal USB connection of the device which can + * cause subsequent commands to fail if called too quickly. A one-second delay + * is implemented in this method to mitigate the issue. + * + * https://github.com/LedgerHQ/ledger-live/issues/4964#issuecomment-1878361157 + * + * @returns Status of command + */ + async close (): Promise { + const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout) + const response = await transport.send(0xb0, 0xa7, 0x00, 0x00) + .then(res => bytes.toDec(res)) + .catch(err => err.statusCode) as number + return new Promise(resolve => setTimeout(resolve, 1000, { status: LEDGER_STATUS_CODES[response] })) + } + + /** + * Get the version of the current process. If a specific app is running, get + * the app version. Otherwise, get the Ledger BOLOS version instead. + * + * https://developers.ledger.com/docs/connectivity/ledgerJS/open-close-info-on-apps#get-information + * + * @returns Status, process name, and version + */ + async version (): Promise<{ status: string, name: string | null, version: string | null }> { + const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout) + const response = await transport.send(0xb0, 0x01, 0x00, 0x00) + .catch(err => dec.toBytes(err.statusCode)) as Uint8Array + await transport.close() + + if (response.length === 2) { + const statusCode = bytes.toDec(response) as number + const status = LEDGER_STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR' + return { status, name: null, version: null } + } + + const nameLength = response[1] + const name = response.slice(2, 2 + nameLength).toString() + const versionLength = response[2 + nameLength] + const version = response.slice(2 + nameLength + 1, 2 + nameLength + 1 + versionLength).toString() + const statusCode = bytes.toDec(response.slice(-2)) as number + + const status = LEDGER_STATUS_CODES[statusCode] + return { status, name, version } + } + + /** + * Get an account at a specific BIP-44 index. + * + * @returns Response object containing command status, public key, and address + */ + async account (index: number = 0, show: boolean = false): Promise<{ status: string, publicKey: string | null, address: string | null }> { + if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) { + throw new TypeError('Invalid account index') + } + const purpose = dec.toBytes(BIP44_PURPOSE + HARDENED_OFFSET, 4) + const coin = dec.toBytes(BIP44_COIN_NANO + HARDENED_OFFSET, 4) + const account = dec.toBytes(index + HARDENED_OFFSET, 4) + const data = new Uint8Array([0x03, ...purpose, ...coin, ...account]) + + const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout) + const response = await transport.send(0xa1, 0x02, show ? 0x01 : 0x00, 0x00, data as Buffer) + .catch(err => dec.toBytes(err.statusCode)) as Uint8Array + await transport.close() + + if (response.length === 2) { + const statusCode = bytes.toDec(response) as number + const status = LEDGER_STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR' + return { status, publicKey: null, address: null } + } + + try { + const publicKey = bytes.toHex(response.slice(0, 32)) + const addressLength = response[32] + const address = response.slice(33, 33 + addressLength).toString() + const statusCode = bytes.toDec(response.slice(33 + addressLength)) as number + const status = LEDGER_STATUS_CODES[statusCode] + return { status, publicKey, address } + } catch (err) { + return { status: 'ERROR_PARSING_ACCOUNT', publicKey: null, address: null } + } + } + + /** + * Cache frontier block in device memory. + * + * @param {number} index - Account number + * @param {any} block - Block data to cache + * @returns Status of command + */ + async cacheBlock (index: number = 0, block: SendBlock | ReceiveBlock | ChangeBlock): Promise<{ status: string }> { + if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) { + throw new TypeError('Invalid account index') + } + if (!(block instanceof SendBlock) && !(block instanceof ReceiveBlock) && !(block instanceof ChangeBlock)) { + throw new TypeError('Invalid block format') + } + if (!block.signature) { + throw new ReferenceError('Cannot cache unsigned block') + } + + const purpose = dec.toBytes(BIP44_PURPOSE + HARDENED_OFFSET, 4) + const coin = dec.toBytes(BIP44_COIN_NANO + HARDENED_OFFSET, 4) + const account = dec.toBytes(index + HARDENED_OFFSET, 4) + const previous = hex.toBytes(block.previous) + const link = hex.toBytes(block.link) + const representative = hex.toBytes(block.representative.publicKey) + const balance = hex.toBytes(BigInt(block.balance).toString(16), 16) + const signature = hex.toBytes(block.signature) + const data = new Uint8Array([0x03, ...purpose, ...coin, ...account, ...previous, ...link, ...representative, ...balance, ...signature]) + + const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout) + const response = await transport.send(0xa1, 0x03, 0x00, 0x00, data as Buffer) + .then(res => bytes.toDec(res)) + .catch(err => err.statusCode) as number + await transport.close() + + return { status: LEDGER_STATUS_CODES[response] } + } + + /** + * Sign a block with the Ledger device. + * + * @param {number} index - Account number + * @param {object} block - Block data to sign + * @returns {Promise} Status, signature, and block hash + */ + async sign (index: number, block: SendBlock | ReceiveBlock | ChangeBlock): Promise<{ status: string, signature: string, hash: string }> + /** + * Sign a nonce with the Ledger device. + * + * @param {number} index - Account number + * @param {string} nonce - 128-bit string to sign + * @returns {Promise} Status and signature + */ + async sign (index: number, nonce: string): Promise<{ status: string, signature: string }> + async sign (index: number = 0, input: string | SendBlock | ReceiveBlock | ChangeBlock): Promise<{ status: string, signature: string | null, hash?: string | null }> { + if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) { + throw new TypeError('Invalid account index') + } + const purpose = dec.toBytes(BIP44_PURPOSE + HARDENED_OFFSET, 4) + const coin = dec.toBytes(BIP44_COIN_NANO + HARDENED_OFFSET, 4) + const account = dec.toBytes(index + HARDENED_OFFSET, 4) + + const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout) + if (typeof input === 'string') { + // input is a nonce + const nonce = utf8.toBytes(input) + if (nonce.length !== 16) { + throw new RangeError('Nonce must be 16-byte string') + } + const data = new Uint8Array([0x03, ...purpose, ...coin, ...account, ...nonce]) + const response = await transport.send(0xa1, 0x05, 0x00, 0x00, data as Buffer) + .catch(err => dec.toBytes(err.statusCode)) as Uint8Array + await transport.close() + + if (response.length === 2) { + const statusCode = bytes.toDec(response) as number + const status = LEDGER_STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR' + return { status, signature: null } + } + + const signature = bytes.toHex(response.slice(0, 64)) + const statusCode = bytes.toDec(response.slice(-2)) as number + const status = LEDGER_STATUS_CODES[statusCode] + return { status, signature } + } else { + // input is a block + const previous = hex.toBytes(input.previous) + const link = hex.toBytes(input.link) + const representative = hex.toBytes(input.representative.publicKey) + const balance = hex.toBytes(BigInt(input.balance).toString(16), 16) + const data = new Uint8Array([0x03, ...purpose, ...coin, ...account, ...previous, ...link, ...representative, ...balance]) + const response = await transport.send(0xa1, 0x04, 0x00, 0x00, data as Buffer) + .catch(err => dec.toBytes(err.statusCode)) as Uint8Array + await transport.close() + + if (response.length === 2) { + const statusCode = bytes.toDec(response) as number + const status = LEDGER_STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR' + return { status, signature: null, hash: null } + } + + const hash = bytes.toHex(response.slice(0, 32)) + const signature = bytes.toHex(response.slice(32, 96)) + const statusCode = bytes.toDec(response.slice(-2)) as number + const status = LEDGER_STATUS_CODES[statusCode] + return { status, signature, hash } + } + } + + /** + * Update cache from raw block data. Suitable for offline use. + * + * @param {number} index - Account number + * @param {object} block - JSON-formatted block data + */ + async updateCache (index: number, block: { [key: string]: string }): Promise<{ status: string }> + /** + * Update cache from a block hash by calling out to a node. Suitable for online + * use only. + * + * @param {number} index - Account number + * @param {string} hash - Hexadecimal block hash + * @param {Node} node - Node class object with a node URL + */ + async updateCache (index: number, hash: string, node: Node): Promise<{ status: string }> + async updateCache (index: number, input: any, node?: Node): Promise<{ status: string }> { + if (typeof input === 'string' && node?.constructor === Node) { + const data = { + 'json_block': 'true', + 'hash': input + } + const res = await node.call('block_info', data) + if (!res || res.ok === false) { + throw new Error(`Unable to fetch block info`, res) + } + input = res.contents + } + return this.cacheBlock(index, input) + } +} + diff --git a/src/lib/node.ts b/src/lib/node.ts new file mode 100644 index 0000000..8bae1b7 --- /dev/null +++ b/src/lib/node.ts @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later + +import { Safe } from './safe.js' + +/** +* Represents a Nano network node. It primarily consists of a URL which will +* accept RPC calls, and an optional API key header construction can be passed if +* required by the node. Once instantiated, the Node object can be used to call +* any action supported by the Nano protocol. The URL protocol must be HTTPS; any +* other value will be changed automatically. +*/ +export class Node { + #u: URL + #n?: string + + constructor (url: string | URL, apiKeyName?: string) { + this.#u = new URL(url) + this.#u.protocol = 'https:' + this.#n = apiKeyName + } + + /** + * + * @param {string} action - Nano protocol RPC call to execute + * @param {object} [data] - JSON to send to the node as defined by the action + * @returns JSON-formatted RPC results from the node + */ + async call (action: string, data?: { [key: string]: any }): Promise { + this.#validate(action) + const headers: { [key: string]: string } = {} + headers['Content-Type'] = 'application/json' + if (this.#n && process.env.LIBNEMO_RPC_API_KEY) { + headers[this.#n] = process.env.LIBNEMO_RPC_API_KEY + } + + data ??= {} + data.action = action.toLowerCase() + const body = JSON.stringify(data) + .replaceAll('/', '\\u002f') + .replaceAll('<', '\\u003c') + .replaceAll('>', '\\u003d') + .replaceAll('\\', '\\u005c') + + const req = new Request(this.#u, { + method: 'POST', + headers, + body + }) + try { + const res = await fetch(req) + return await res.json() + } catch (err) { + console.error(err) + return JSON.stringify(err) + } + } + + #validate (action: string): void { + if (!action) { + throw new ReferenceError('Action is required for RPCs') + } + if (typeof action !== 'string') { + throw new TypeError('RPC action must be a string') + } + if (!/^[A-Za-z]+(_[A-Za-z]+)*$/.test(action)) { + throw new TypeError('RPC action contains invalid characters') + } + } +} diff --git a/src/lib/rolodex.ts b/src/lib/rolodex.ts new file mode 100644 index 0000000..3efffe5 --- /dev/null +++ b/src/lib/rolodex.ts @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later + +import { Account } from './account.js' + +type RolodexEntry = { + name: string + account: Account +} + +/** +* Represents a basic address book of Nano accounts. Multiple addresses can be +* saved under one nickname. +*/ +export class Rolodex { + #entries: RolodexEntry[] = [] + + /** + * Adds an address to the rolodex under a specific nickname. + * + * If the name exists, add the address as a new association to that name. If + * the account exists under a different name, update the name. If no matches + * are found at all, add a completely new entry. + * + * @param {string} name - Alias for the address + * @param {string} address - Nano account address + */ + async add (name: string, address: string): Promise { + if (name == null || name === '') { + throw new Error('Name is required for rolodex entries') + } + if (address == null || address === '') { + throw new Error('address is required for rolodex entries') + } + const account = new Account(address) + const nameResult = this.#entries.find(e => e.name === name) + const accountResult = this.#entries.find(e => e.account.address === address) + if (!accountResult) { + this.#entries.push({ name, account }) + } else if (!nameResult) { + accountResult.name = name + } + } + + /** + * Gets the name associated with a specific Nano address from the rolodex. + * + * @param {string} address - Nano account address + * @returns {string|null} Name associated with the address, or null if not found + */ + getName (address: string): string | null { + const result = this.#entries.find(e => e.account.address === address) + return result?.name ?? null + } + + /** + * Gets all Nano addresses associated with a name from the rolodex. + * + * @param {string} name - Alias to look up + * @returns {string[]} List of Nano addresses associated with the name + */ + getAddresses (name: string): string[] { + const entries = this.#entries.filter(e => e.name === name) + return entries.map(a => a.account.address) + } + + /** + * Verifies whether the public key of any Nano address saved under a specific + * name in the rolodex was used to sign a specific set of data. + * + * @param {string} name - Alias to look up + * @param {string} signature - Signature to use for verification + * @param {...string} data - Signed data to verify + * @returns {boolean} True if the signature was used to sign the data, else false + */ + async verify (name: string, signature: string, ...data: string[]): Promise { + const Tools = await import('./tools.js') + const entries = this.#entries.filter(e => e.name === name) + for (const entry of entries) { + const key = entry.account.publicKey + const verified = await Tools.verify(key, signature, ...data) + if (verified) { + return true + } + } + return false + } +} diff --git a/src/lib/safe.ts b/src/lib/safe.ts new file mode 100644 index 0000000..bd0b689 --- /dev/null +++ b/src/lib/safe.ts @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later + +import { buffer, utf8 } from './convert.js' +import { Entropy } from './entropy.js' +const { subtle } = globalThis.crypto +const ERR_MSG = 'Failed to store item in Safe' +export class Safe { + #items: Map = new Map() + + /** + * Encrypts data with a password and stores it in the Safe. + */ + async put (name: string, password: string, data: any): Promise + /** + * Encrypts data with a CryptoKey and stores it in the Safe. + */ + async put (name: string, key: CryptoKey, data: any): Promise + async put (name: string, passkey: string | CryptoKey, data: any): Promise { + if (this.#items.get(name)) { + throw new Error(ERR_MSG) + } + return this.overwrite(name, passkey as string, data) + } + + /** + * Encrypts data with a password and stores it in the Safe. + */ + async overwrite (name: string, password: string, data: any): Promise + /** + * Encrypts data with a CryptoKey and stores it in the Safe. + */ + async overwrite (name: string, key: CryptoKey, data: any): Promise + async overwrite (name: string, passkey: string | CryptoKey, data: any): Promise { + if (this.#isNotValid(name, passkey, data)) { + throw new Error(ERR_MSG) + } + + const iv = new Entropy() + if (typeof passkey === 'string') { + try { + passkey = await subtle.importKey('raw', utf8.toBytes(passkey), 'PBKDF2', false, ['deriveBits', 'deriveKey']) + passkey = await subtle.deriveKey({ name: 'PBKDF2', hash: 'SHA-512', salt: iv.bytes, iterations: 210000 }, passkey, { name: 'AES-GCM', length: 256 }, false, ['encrypt']) + } catch (err) { + throw new Error(ERR_MSG) + } + } + + try { + if (typeof data === 'bigint') { + data = data.toString() + } + data = JSON.stringify(data) + const encoded = utf8.toBytes(data) + const encrypted = await subtle.encrypt({ name: 'AES-GCM', iv: iv.buffer }, passkey, encoded) + this.#items.set(name, { encrypted, iv }) + passkey = '' + } catch (err) { + throw new Error(ERR_MSG) + } + return this.#items.has(name) + } + + /** + * Retrieves data from the Safe and decrypts data with a password. + */ + async get (name: string, password: string): Promise + /** + * Retrieves data from the Safe and decrypts data with a CryptoKey. + */ + async get (name: string, key: CryptoKey): Promise + async get (name: string, passkey: string | CryptoKey): Promise { + if (this.#isNotValid(name, passkey)) { + return null + } + + const item = this.#items.get(name) + if (item == null) { + return null + } + const { encrypted, iv } = item + + try { + if (typeof passkey === 'string') { + passkey = await subtle.importKey('raw', utf8.toBytes(passkey), 'PBKDF2', false, ['deriveBits', 'deriveKey']) + passkey = await subtle.deriveKey({ name: 'PBKDF2', hash: 'SHA-512', salt: iv.bytes, iterations: 210000 }, passkey, { name: 'AES-GCM', length: 256 }, false, ['decrypt']) + } + } catch (err) { + return null + } + + try { + const decrypted = await subtle.decrypt({ name: 'AES-GCM', iv: iv.buffer }, passkey, encrypted) + const decoded = buffer.toUtf8(decrypted) + const data = JSON.parse(decoded) + passkey = '' + this.#items.delete(name) + return data + } catch (err) { + return null + } + } + + #isNotValid (name: string, passkey: string | CryptoKey, data?: any): boolean { + if (typeof name !== 'string' || name === '') { + return true + } + if (typeof passkey !== 'string' || passkey === '') { + if (!(passkey instanceof CryptoKey)) { + return true + } + } + if (typeof data === 'object') { + try { + JSON.stringify(data) + } catch (err) { + return true + } + } + return false + } +} diff --git a/src/lib/tools.ts b/src/lib/tools.ts new file mode 100644 index 0000000..13b79b7 --- /dev/null +++ b/src/lib/tools.ts @@ -0,0 +1,175 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later + +import blakejs from 'blakejs' +const { blake2bInit, blake2bUpdate, blake2bFinal } = blakejs +import { Account } from './account.js' +import { UNITS } from './constants.js' +import { bytes, hex } from './convert.js' +import Ed25519 from './ed25519.js' +import { Node } from './node.js' +import { Bip44Wallet, Blake2bWallet, LedgerWallet } from './wallet.js' +import { SendBlock } from './block.js' + +/** +* Converts a decimal amount from one unit divider to another. +* +* @param {string} amount - Decimal amount to convert +* @param {string} inputUnit - Current denomination +* @param {string} outputUnit - Desired denomination +*/ +export async function convert (amount: string, inputUnit: string, outputUnit: string): Promise { + if (!amount || !/^[0-9]*\.?[0-9]*$/.test(amount)) { + throw new Error('Invalid amount') + } + let [i = '', f = ''] = amount.toString().split('.') + i = i.replace(/^0*/g, '') + f = f.replace(/0*$/g, '') + + inputUnit = inputUnit.toUpperCase() + outputUnit = outputUnit.toUpperCase() + if (!(inputUnit in UNITS)) { + throw new Error(`Unknown denomination ${inputUnit}, expected one of the following: ${Object.keys(UNITS)}`) + } + if (!(outputUnit in UNITS)) { + throw new Error(`Unknown denomination ${outputUnit}, expected one of the following: ${Object.keys(UNITS)}`) + } + + // convert to raw + i = `${i}${f.slice(0, UNITS[inputUnit])}`.padEnd(i.length + UNITS[inputUnit], '0') + f = f.slice(UNITS[inputUnit]) + if (i.length > 39) { + throw new Error('Amount exceeds Nano limits') + } + if (i.length < 1) { + throw new Error('Amount must be at least 1 raw') + } + if (f.length > 0) { + throw new Error('Amount contains fractional raw') + } + + // convert to desired denomination + if (UNITS[outputUnit] !== 0) { + f = i.slice(-UNITS[outputUnit]) + f + i = i.slice(0, -UNITS[outputUnit]) + } + i = i.replace(/^0*/g, '') ?? '0' + f = f.replace(/0*$/g, '') + return `${i}${f ? '.' : ''}${f}` +} + +/** +* Hashes data using BLAKE2b. +* +* @param {string|string[]} data - Hexadecimal-formatted data to hash +* @returns {Promise} Array of bytes representing hashed input data +*/ +export async function blake2b (data: string | string[]): Promise { + if (!Array.isArray(data)) data = [data] + const ctx = blake2bInit(32, undefined) + data.forEach(str => blake2bUpdate(ctx, hex.toBytes(str))) + return blake2bFinal(ctx) +} + +/** +* Converts one or more strings to hexadecimal and hashes them with BLAKE2b. +* +* @param {string|string[]} data - Input to hash +* @returns {Promise} Hash of the input +*/ +export async function hash (data: string | string[]): Promise { + if (!Array.isArray(data)) data = [data] + const enc = new TextEncoder() + data = data.map(str => { return bytes.toHex(enc.encode(str)) }) + const hash = await blake2b(data) + return bytes.toHex(hash) +} + +/** +* Signs arbitrary strings with a private key using the Ed25519 signature scheme. +* +* @param {string} key - Hexadecimal-formatted private key to use for signing +* @param {...string} input - Data to be signed +* @returns {Promise} Hexadecimal-formatted signature +*/ +export async function sign (key: string, ...input: string[]): Promise { + const enc = new TextEncoder() + const data = input.map(str => { return bytes.toHex(enc.encode(str)) }) + const signature = Ed25519.sign( + await blake2b(data), + hex.toBytes(key)) + return bytes.toHex(signature) +} + +/** +* Collects the funds from a specified range of accounts in a wallet and sends +* them all to a single recipient address. +* +* @param {Node|string|URL} node - Node information required to refresh accounts, calculate PoW, and process blocks +* @param {Blake2bWallet|Bip44Wallet|LedgerWallet} wallet - Wallet from which to sweep funds +* @param {string} recipient - Destination address for all swept funds +* @param {number} from - Starting account index to sweep +* @param {number} to - Ending account index to sweep +* @returns An array of results including both successes and failures + */ +export async function sweep (node: Node | string | URL, wallet: Blake2bWallet | Bip44Wallet | LedgerWallet, recipient: string, from: number = 0, to: number = from) { + if (node == null || wallet == null || recipient == null) { + throw new ReferenceError('Missing required sweep arguments') + } + if (typeof node === 'string' || node.constructor === URL) { + node = new Node(node) + } + if (node.constructor !== Node) { + throw new TypeError('RPC must be a valid node') + } + const blockQueue = [] + const results: { status: 'success' | 'error', address: string, message: string }[] = [] + + const recipientAccount = new Account(recipient) + const accounts = await wallet.refresh(node, from, to) + for (const account of accounts) { + if (account.representative?.address && account.frontier) { + const block = new SendBlock( + account, + account.balance ?? '0', + recipientAccount.address, + account.balance ?? '0', + account.representative.address, + account.frontier + ) + blockQueue.push(new Promise(async resolve => { + try { + await block.pow(node) + await block.sign(account.index) + const hash = await block.process(node) + results.push({ status: 'success', address: block.account.address, message: hash }) + } catch (err: any) { + results.push({ status: 'error', address: block.account.address, message: err.message }) + } finally { + resolve(null) + } + })) + } + } + await Promise.allSettled(blockQueue) + return results +} + +/** +* Verifies the signature of arbitrary strings using a public key. +* +* @param {string} key - Hexadecimal-formatted public key to use for verification +* @param {string} signature - Hexadcimal-formatted signature +* @param {...string} input - Data to be verified +* @returns {boolean} True if the data was signed by the public key's matching private key +*/ +export async function verify (key: string, signature: string, ...input: string[]): Promise { + const enc = new TextEncoder() + const data = input.map(str => { return bytes.toHex(enc.encode(str)) }) + return Ed25519.verify( + await blake2b(data), + hex.toBytes(key), + hex.toBytes(signature)) +} + +export default { blake2b, convert, hash, sign, sweep, verify } diff --git a/src/lib/wallet.ts b/src/lib/wallet.ts new file mode 100644 index 0000000..f998a5a --- /dev/null +++ b/src/lib/wallet.ts @@ -0,0 +1,577 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later + +import { Account } from './account.js' +import { Bip39Mnemonic } from './bip39-mnemonic.js' +import { nanoCKD } from './bip32-key-derivation.js' +import { ADDRESS_GAP, SEED_LENGTH_BIP44, SEED_LENGTH_BLAKE2B } from './constants.js' +import { bytes, dec } from './convert.js' +import { Entropy } from './entropy.js' +import { Node } from './node.js' +import { Safe } from './safe.js' +import Tools from './tools.js' +import type { Ledger } from './ledger.js' + +/** +* Represents a wallet containing numerous Nano accounts derived from a single +* source, the form of which can vary based on the type of wallet. The Wallet +* class itself is abstract and cannot be directly instantiated. Currently, three +* types of wallets are supported, each as a derived class: Bip44Wallet, +* Blake2bWallet, LedgerWallet. +*/ +abstract class Wallet { + #accounts: Account[] + #id: Entropy + #mnemonic: Bip39Mnemonic | null + #safe: Safe + #seed: string | null + get mnemonic () { + if (this.#mnemonic instanceof Bip39Mnemonic) { + return this.#mnemonic.phrase + } + return '' + } + get seed () { + if (typeof this.#seed === 'string') { + return this.#seed + } + return '' + } + + abstract ckd (index: number): Promise + + constructor (seed?: string, mnemonic?: Bip39Mnemonic) { + if (this.constructor === Wallet) { + throw new Error('Wallet is an abstract class and cannot be instantiated directly.') + } + this.#accounts = [] + this.#id = new Entropy(16) + this.#mnemonic = mnemonic ?? null + this.#safe = new Safe() + this.#seed = seed ?? null + } + + /** + * Retrieves accounts from a wallet using its child key derivation function. + * + * @param {number} from - Start index of secret keys. Default: 0 + * @param {number} to - End index of secret keys. Default: `from` + */ + async accounts (from: number = 0, to: number = from): Promise { + if (from > to) { + const swap = from + from = to + to = swap + } + const accounts: Account[] = [] + for (let i = from; i <= to; i++) { + if (this.#accounts[i]) { + accounts.push(this.#accounts[i]) + } else { + const account = await this.ckd(i) + if (account != null) { + this.#accounts[i] = account + accounts.push(account) + } + } + } + return accounts + } + + /** + * Fetches the lowest-indexed unopened account from a wallet in sequential + * order. An account is unopened if it has no frontier block. + * + * @param {Node|string|URL} node - Node information required to refresh accounts, calculate PoW, and process blocks + * @param {number} batchSize - Number of accounts to fetch and check per RPC callout + * @param {number} from - Account index from which to start the search + * @returns {Promise} The lowest-indexed unopened account belonging to the wallet + */ + + async getNextNewAccount (node: Node, batchSize: number = ADDRESS_GAP, from: number = 0): Promise { + if (!Number.isSafeInteger(batchSize) || batchSize < 1) { + throw new RangeError(`Invalid batch size ${batchSize}`) + } + const accounts = await this.accounts(from, from + batchSize - 1) + const addresses = accounts.map(({ address }) => address) + const data = { + "accounts": addresses + } + const { errors } = await node.call('accounts_frontiers', data) + for (const key of Object.keys(errors ?? {})) { + const value = errors[key] + if (value === 'Account not found') { + return new Account(key) + } + } + return await this.getNextNewAccount(node, batchSize, from + batchSize) + } + + /** + * Refreshes wallet account balances, frontiers, and representatives from the current state on the network. + * + * A successful response will set these properties on each account. + * + * @param {Node|string|URL} node - Node information required to refresh accounts, calculate PoW, and process blocks + * @returns {Promise} Accounts with updated balances, frontiers, and representatives + */ + async refresh (node: Node | string | URL, from: number = 0, to: number = from): Promise { + if (typeof node === 'string' || node.constructor === URL) { + node = new Node(node) + } + if (node.constructor !== Node) { + throw new TypeError('RPC must be a valid node') + } + const accounts = await this.accounts(from, to) + for (let i = 0; i < accounts.length; i++) { + try { + await accounts[i].refresh(node) + } catch (err) { + accounts.splice(i--, 1) + } + } + return accounts + } + + /** + * Locks the wallet with a password that will be needed to unlock it later. + * + * @param {string} password Used to lock the wallet + * @returns True if successfully locked + */ + async lock (password: string): Promise + /** + * Locks the wallet with a CryptoKey that will be needed to unlock it later. + * + * @param {CryptoKey} key Used to lock the wallet + * @returns True if successfully locked + */ + async lock (key: CryptoKey): Promise + async lock (passkey: string | CryptoKey): Promise { + let success = true + try { + success &&= await this.#safe.overwrite(this.#id.hex, passkey as string, this.#id.hex) + if (!success) { + throw null + } + if (this.#mnemonic instanceof Bip39Mnemonic) { + success &&= await this.#safe.put('mnemonic', passkey as string, this.#mnemonic.phrase) + } + if (typeof this.#seed === 'string') { + success &&= await this.#safe.put('seed', passkey as string, this.#seed) + } + for (const a of this.#accounts) { + success &&= await a.lock(passkey as string) + } + if (!success) { + throw success + } + } catch (err) { + throw new Error('Failed to lock wallet') + } + this.#mnemonic = null + this.#seed = null + return true + } + + /** + * Unlocks the wallet using the same password as used prior to lock it. + * + * @param {string} password Used previously to lock the wallet + * @returns True if successfully unlocked + */ + async unlock (password: string): Promise + /** + * Unlocks the wallet using the same CryptoKey as used prior to lock it. + * + * @param {CryptoKey} key Used previously to lock the wallet + * @returns True if successfully unlocked + */ + async unlock (key: CryptoKey): Promise + async unlock (passkey: string | CryptoKey): Promise { + try { + const id = await this.#safe.get(this.#id.hex, passkey as string) + if (id !== this.#id.hex) { + throw null + } + const mnemonic = await this.#safe.get('mnemonic', passkey as string) + const seed = await this.#safe.get('seed', passkey as string) + for (const a of this.#accounts) { + await a.unlock(passkey as string) + } + if (mnemonic != null) { + this.#mnemonic = await Bip39Mnemonic.fromPhrase(mnemonic) + } + if (seed != null) { + this.#seed = seed + } + } catch (err) { + throw new Error('Failed to unlock wallet') + } + return true + } +} + +/** +* Hierarchical deterministic (HD) wallet created by using a source of entropy to +* derive a mnemonic phrase. The mnemonic phrase, in combination with an optional +* salt, is used to generate a seed. A value can be provided as a parameter for +* entropy, mnemonic + salt, or seed; if no argument is passed, a new entropy +* value will be generated using a cryptographically strong pseudorandom number +* generator. +* +* Importantly, the salt is not stored in the instantiated Wallet object. If a +* salt is used, then losing it means losing the ability to regenerate the seed +* from the mnemonic. +* +* Accounts are derived from the seed. Private keys are derived using a BIP-44 +* derivation path. The public key is derived from the private key using the +* Ed25519 key algorithm. Account addresses are derived as described in the nano +* documentation (https://docs.nano.org) +* +* A password must be provided when creating or importing the wallet and is used +* to lock and unlock the wallet. The wallet will be initialized as locked. When +* the wallet is unlocked, a new password can be specified using the lock() +* method. +*/ +export class Bip44Wallet extends Wallet { + static #isInternal: boolean = false + + constructor (seed: string, mnemonic?: Bip39Mnemonic) { + if (!Bip44Wallet.#isInternal) { + throw new Error(`Bip44Wallet cannot be instantiated directly. Use 'await Bip44Wallet.create()' instead.`) + } + super(seed, mnemonic) + Bip44Wallet.#isInternal = false + } + + /** + * Creates a new HD wallet by using an entropy value generated using a + * cryptographically strong pseudorandom number generator. + * + * @param {string} password - Encrypts the wallet to lock and unlock it + * @param {string} [salt=''] - Used when generating the final seed + * @returns {Bip44Wallet} A newly instantiated Bip44Wallet + */ + static async create (password: string, salt: string): Promise + /** + * Creates a new HD wallet by using an entropy value generated using a + * cryptographically strong pseudorandom number generator. + * + * @param {CryptoKey} key - Encrypts the wallet to lock and unlock it + * @param {string} [salt=''] - Used when generating the final seed + * @returns {Bip44Wallet} A newly instantiated Bip44Wallet + */ + static async create (key: CryptoKey, salt: string): Promise + static async create (passkey: string | CryptoKey, salt: string = ''): Promise { + Bip44Wallet.#isInternal = true + try { + const e = new Entropy() + return await Bip44Wallet.fromEntropy(passkey as string, e.hex, salt) + } catch (err) { + throw new Error(`Error creating new Bip44Wallet: ${err}`) + } + } + + /** + * Creates a new HD wallet by using a pregenerated entropy value. The user + * must ensure that it is cryptographically strongly random. + * + * @param {string} password - Used to lock and unlock the wallet + * @param {string} entropy - Used when generating the initial mnemonic phrase + * @param {string} [salt=''] - Used when generating the final seed + * @returns {Bip44Wallet} A newly instantiated Bip44Wallet + */ + static async fromEntropy (password: string, entropy: string, salt: string): Promise + /** + * Creates a new HD wallet by using a pregenerated entropy value. The user + * must ensure that it is cryptographically strongly random. + * + * @param {CryptoKey} key - Used to lock and unlock the wallet + * @param {string} entropy - Used when generating the initial mnemonic phrase + * @param {string} [salt=''] - Used when generating the final seed + * @returns {Bip44Wallet} A newly instantiated Bip44Wallet + */ + static async fromEntropy (key: CryptoKey, entropy: string, salt: string): Promise + static async fromEntropy (passkey: string | CryptoKey, entropy: string, salt: string = ''): Promise { + Bip44Wallet.#isInternal = true + try { + const e = new Entropy(entropy) + const m = await Bip39Mnemonic.fromEntropy(e.hex) + const s = await m.toBip39Seed(salt) + const wallet = new this(s, m) + await wallet.lock(passkey as string) + return wallet + } catch (err) { + throw new Error(`Error importing Bip44Wallet from entropy: ${err}`) + } + } + + /** + * Creates a new HD wallet by using a pregenerated mnemonic phrase. + * + * @param {string} password - Used to lock and unlock the wallet + * @param {string} mnemonic - Used when generating the final seed + * @param {string} [salt=''] - Used when generating the final seed + * @returns {Bip44Wallet} A newly instantiated Bip44Wallet + */ + static async fromMnemonic (password: string, mnemonic: string, salt: string): Promise + /** + * Creates a new HD wallet by using a pregenerated mnemonic phrase. + * + * @param {CryptoKey} key - Used to lock and unlock the wallet + * @param {string} mnemonic - Used when generating the final seed + * @param {string} [salt=''] - Used when generating the final seed + * @returns {Bip44Wallet} A newly instantiated Bip44Wallet + */ + static async fromMnemonic (key: CryptoKey, mnemonic: string, salt: string): Promise + static async fromMnemonic (passkey: string | CryptoKey, mnemonic: string, salt: string = ''): Promise { + Bip44Wallet.#isInternal = true + try { + const m = await Bip39Mnemonic.fromPhrase(mnemonic) + const s = await m.toBip39Seed(salt) + const wallet = new this(s, m) + await wallet.lock(passkey as string) + return wallet + } catch (err) { + throw new Error(`Error importing Bip44Wallet from mnemonic: ${err}`) + } + } + + /** + * Creates a new HD wallet by using a pregenerated seed value. This seed cannot + * be used to regenerate any higher level randomness which includes entropy, + * mnemonic phrase, and salt. + * + * @param {string} password - Used to lock and unlock the wallet + * @param {string} seed - Hexadecimal 128-character string used to derive private-public key pairs + * @returns {Bip44Wallet} A newly instantiated Bip44Wallet + */ + static async fromSeed (password: string, seed: string): Promise + /** + * Creates a new HD wallet by using a pregenerated seed value. This seed cannot + * be used to regenerate any higher level randomness which includes entropy, + * mnemonic phrase, and salt. + * + * @param {CryptoKey} key - Used to lock and unlock the wallet + * @param {string} seed - Hexadecimal 128-character string used to derive private-public key pairs + * @returns {Bip44Wallet} A newly instantiated Bip44Wallet + */ + static async fromSeed (key: CryptoKey, seed: string): Promise + static async fromSeed (passkey: string | CryptoKey, seed: string): Promise { + Bip44Wallet.#isInternal = true + if (seed.length !== SEED_LENGTH_BIP44) { + throw new Error(`Expected a ${SEED_LENGTH_BIP44}-character seed, but received ${seed.length}-character string.`) + } + if (!/^[0-9a-fA-F]+$/i.test(seed)) { + throw new Error('Seed contains invalid hexadecimal characters.') + } + const wallet = new this(seed) + await wallet.lock(passkey as string) + return wallet + } + + /** + * Derives BIP-44 Nano account private keys. + * + * @param {number} from - Start index of private keys. Default: 0 + * @param {number} to - End index of private keys. Default: `from` + * @returns {Promise} + */ + async ckd (index: number): Promise { + const key = await nanoCKD(this.seed, index) + return await Account.fromPrivateKey(key, index) + } +} + +/** +* BLAKE2b wallet created by deriving a mnemonic phrase from a seed or vice +* versa. If no value is provided for either, a new BIP-39 seed and mnemonic will +* be generated using a cryptographically strong pseudorandom number generator. +* +* Account private keys are derived on an ad hoc basis using the Blake2b hashing +* function. Account public key are derived from the private key using the +* Ed25519 key algorithm. Account addresses are derived from the public key as +* described in the Nano documentation. +* https://docs.nano.org/integration-guides/the-basics/ +* +* A password must be provided when creating or importing the wallet and is used +* to lock and unlock the wallet. The wallet will be initialized as locked. When +* the wallet is unlocked, a new password can be specified using the lock() +* method. +*/ +export class Blake2bWallet extends Wallet { + static #isInternal: boolean = false + + constructor (seed: string, mnemonic: Bip39Mnemonic) { + if (!Blake2bWallet.#isInternal) { + throw new Error(`Blake2bWallet cannot be instantiated directly. Use 'await Blake2bWallet.create()' instead.`) + } + super(seed, mnemonic) + Blake2bWallet.#isInternal = false + } + + /** + * Creates a new BLAKE2b wallet by using a seed generated using a + * cryptographically strong pseudorandom number generator. + * + * @param {string} password - Encrypts the wallet to lock and unlock it + * @returns {Blake2bWallet} A newly instantiated Blake2bWallet + */ + static async create (password: string): Promise + /** + * Creates a new BLAKE2b wallet by using a seed generated using a + * cryptographically strong pseudorandom number generator. + * + * @param {CryptoKey} key - Encrypts the wallet to lock and unlock it + * @returns {Blake2bWallet} A newly instantiated Blake2bWallet + */ + static async create (key: CryptoKey): Promise + static async create (passkey: string | CryptoKey): Promise { + Blake2bWallet.#isInternal = true + try { + const seed = new Entropy() + return await Blake2bWallet.fromSeed(passkey as string, seed.hex) + } catch (err) { + throw new Error(`Error creating new Blake2bWallet: ${err}`) + } + } + + /** + * Creates a new BLAKE2b wallet by using a pregenerated seed. The user must + * ensure that it is cryptographically strongly random. + * + * @param {string} password - Used to lock and unlock the wallet + * @param {string} seed - Hexadecimal 64-character string used to derive private-public key pairs + * @returns {Blake2bWallet} A newly instantiated Blake2bWallet + */ + static async fromSeed (password: string, seed: string): Promise + /** + * Creates a new BLAKE2b wallet by using a pregenerated seed. The user must + * ensure that it is cryptographically strongly random. + * + * @param {CryptoKey} key - Used to lock and unlock the wallet + * @param {string} seed - Hexadecimal 64-character string used to derive private-public key pairs + * @returns {Blake2bWallet} A newly instantiated Blake2bWallet + */ + static async fromSeed (key: CryptoKey, seed: string): Promise + static async fromSeed (passkey: string | CryptoKey, seed: string): Promise { + Blake2bWallet.#isInternal = true + if (seed.length !== SEED_LENGTH_BLAKE2B) { + throw new Error(`Expected a ${SEED_LENGTH_BLAKE2B}-character seed, but received ${seed.length}-character string.`) + } + if (!/^[0-9a-fA-F]+$/i.test(seed)) { + throw new Error('Seed contains invalid hexadecimal characters.') + } + const s = seed + const m = await Bip39Mnemonic.fromEntropy(seed) + const wallet = new this(s, m) + await wallet.lock(passkey as string) + return wallet + } + + /** + * Creates a new BLAKE2b wallet by using a pregenerated mnemonic phrase. + * + * @param {string} password - Used to lock and unlock the wallet + * @param {string} mnemonic - Used when generating the final seed + * @returns {Blake2bWallet} A newly instantiated Blake2bWallet + */ + static async fromMnemonic (password: string, mnemonic: string): Promise + /** + * Creates a new BLAKE2b wallet by using a pregenerated mnemonic phrase. + * + * @param {CryptoKey} key - Used to lock and unlock the wallet + * @param {string} mnemonic - Used when generating the final seed + * @returns {Blake2bWallet} A newly instantiated Blake2bWallet + */ + static async fromMnemonic (key: CryptoKey, mnemonic: string): Promise + static async fromMnemonic (password: string | CryptoKey, mnemonic: string): Promise { + Blake2bWallet.#isInternal = true + try { + const m = await Bip39Mnemonic.fromPhrase(mnemonic) + const s = await m.toBlake2bSeed() + const wallet = new this(s, m) + await wallet.lock(password as string) + return wallet + } catch (err) { + throw new Error(`Error importing Blake2bWallet from mnemonic: ${err}`) + } + } + + /** + * Derives BLAKE2b account private keys. + * + * @param {number} from - Start index of private keys. Default: 0 + * @param {number} to - End index of private keys. Default: `from` + */ + async ckd (index: number): Promise { + const hash = await Tools.blake2b([this.seed, dec.toHex(index, 4)]) + const key = bytes.toHex(hash) + return await Account.fromPrivateKey(key, index) + } +} + +/** +* Ledger hardware wallet created by communicating with a Ledger device via ADPU +* calls. This wallet does not feature any seed nor mnemonic phrase as all +* private keys are held in the secure chip of the device. As such, the user +* is responsible for using Ledger technology to back up these pieces of data. +* +* Usage of this wallet is generally controlled by calling functions of the +* `ledger` object. For example, the wallet interface should have a button to +* initiate a device connection by calling `wallet.ledger.connect()`. For more +* information, refer to the ledger.js service file. +*/ +export class LedgerWallet extends Wallet { + static #isInternal: boolean = false + #ledger: Ledger + + get ledger () { return this.#ledger } + + constructor (ledger: Ledger) { + if (!LedgerWallet.#isInternal) { + throw new Error(`LedgerWallet cannot be instantiated directly. Use 'await LedgerWallet.create()' instead.`) + } + super() + this.#ledger = ledger + LedgerWallet.#isInternal = false + } + + /** + * Creates a new Ledger hardware wallet communication layer by dynamically + * importing the ledger.js service. + * + * @returns {LedgerWallet} A wallet containing accounts and a Ledger device communication object + */ + static async create (): Promise { + LedgerWallet.#isInternal = true + const { Ledger } = await import('./ledger.js') + const l = await Ledger.init() + return new this(l) + } + + async ckd (index: number): Promise { + const { status, publicKey } = await this.ledger.account(index) + if (status === 'OK' && publicKey != null) { + return await Account.fromPublicKey(publicKey, index) + } + return null + } + + async lock (): Promise { + if (this.#ledger == null) { + return false + } + const result = await this.#ledger.close() + return result === 'OK' + } + + async unlock (): Promise { + if (this.#ledger == null) { + return false + } + const result = await this.#ledger.connect() + return result === 'OK' + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..ea71ab9 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later + +import { Account } from './lib/account.js' +import { SendBlock, ReceiveBlock, ChangeBlock } from './lib/block.js' +import { Node } from './lib/node.js' +import { Rolodex } from './lib/rolodex.js' +import Tools from './lib/tools.js' +import { Bip44Wallet, Blake2bWallet, LedgerWallet } from './lib/wallet.js' + +export { Account, SendBlock, ReceiveBlock, ChangeBlock, Node, Rolodex, Tools, Bip44Wallet, Blake2bWallet, LedgerWallet } diff --git a/test/TEST_VECTORS.js b/test/TEST_VECTORS.js new file mode 100644 index 0000000..d0a458d --- /dev/null +++ b/test/TEST_VECTORS.js @@ -0,0 +1,151 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later + +export const GENESIS_ADDRESS = 'nano_3t6k35gi95xu6tergt6p69ck76ogmitsa8mnijtpxm9fkcm736xtoncuohr3' +export const RAW_MAX = '340282366920938463463374607431768211455' +export const SUPPLY_MAX = '133248297920938463463374607431768211455' + +export const NANO_TEST_VECTORS = Object.freeze({ + MNEMONIC: 'edge defense waste choose enrich upon flee junk siren film clown finish luggage leader kid quick brick print evidence swap drill paddle truly occur', + PASSWORD: 'some password', + BIP39_SEED: '0DC285FDE768F7FF29B66CE7252D56ED92FE003B605907F7A4F683C3DC8586D34A914D3C71FC099BB38EE4A59E5B081A3497B7A323E90CC68F67B5837690310C', + + PRIVATE_0: '3BE4FC2EF3F3B7374E6FC4FB6E7BB153F8A2998B3B3DAB50853EABE128024143', + PUBLIC_0: '5B65B0E8173EE0802C2C3E6C9080D1A16B06DE1176C938A924F58670904E82C4', + ADDRESS_0: 'nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d', + + PRIVATE_1: 'CE7E429E683D652446261C17A96DA9ED1897AEA96C8046F2B8036F6B05CB1A83', + PUBLIC_1: 'D9F7762E9CD4E7ED632481308CDB8F54ABF0241332C0A8641F61E92E2FB03C12', + ADDRESS_1: 'nano_3phqgrqbso99xojkb1bijmfryo7dy1k38ep1o3k3yrhb7rqu1h1k47yu78gz', + + PRIVATE_2: '1257DF74609B9C6461A3F4E7FD6E3278F2DDCF2562694F2C3AA0515AF4F09E38', + PUBLIC_2: 'A46DA51986E25A14D82E32D765DCEE69B9EECCD4405411430D91DDB61B717566', + ADDRESS_2: 'nano_3b5fnnerfrkt4me4wepqeqggwtfsxu8fai4n473iu6gxprfq4xd8pk9gh1dg' +}) + +export const TREZOR_TEST_VECTORS = Object.freeze({ + PASSWORD: 'TREZOR', + + ENTROPY_0: "0000000000000000000000000000000000000000000000000000000000000000", + MNEMONIC_0: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art", + SEED_0: "bda85446c68413707090a52022edd26a1c9462295029f2e60cd7c4f2bbd3097170af7a4d73245cafa9c3cca8d561a7c3de6f5d4a10be8ed2a5e608d68f92fcc8", + BIP32_KEY_0: "xprv9s21ZrQH143K32qBagUJAMU2LsHg3ka7jqMcV98Y7gVeVyNStwYS3U7yVVoDZ4btbRNf4h6ibWpY22iRmXq35qgLs79f312g2kj5539ebPM", + + ENTROPY_1: "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f", + MNEMONIC_1: "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title", + SEED_1: "bc09fca1804f7e69da93c2f2028eb238c227f2e9dda30cd63699232578480a4021b146ad717fbb7e451ce9eb835f43620bf5c514db0f8add49f5d121449d3e87", + BIP32_KEY_1: "xprv9s21ZrQH143K3Y1sd2XVu9wtqxJRvybCfAetjUrMMco6r3v9qZTBeXiBZkS8JxWbcGJZyio8TrZtm6pkbzG8SYt1sxwNLh3Wx7to5pgiVFU", + + ENTROPY_2: "8080808080808080808080808080808080808080808080808080808080808080", + MNEMONIC_2: "letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless", + SEED_2: "c0c519bd0e91a2ed54357d9d1ebef6f5af218a153624cf4f2da911a0ed8f7a09e2ef61af0aca007096df430022f7a2b6fb91661a9589097069720d015e4e982f", + BIP32_KEY_2: "xprv9s21ZrQH143K3CSnQNYC3MqAAqHwxeTLhDbhF43A4ss4ciWNmCY9zQGvAKUSqVUf2vPHBTSE1rB2pg4avopqSiLVzXEU8KziNnVPauTqLRo", + + ENTROPY_3: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + MNEMONIC_3: "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote", + SEED_3: "dd48c104698c30cfe2b6142103248622fb7bb0ff692eebb00089b32d22484e1613912f0a5b694407be899ffd31ed3992c456cdf60f5d4564b8ba3f05a69890ad", + BIP32_KEY_3: "xprv9s21ZrQH143K2WFF16X85T2QCpndrGwx6GueB72Zf3AHwHJaknRXNF37ZmDrtHrrLSHvbuRejXcnYxoZKvRquTPyp2JiNG3XcjQyzSEgqCB" +}) + +export const BIP32_TEST_VECTORS = Object.freeze({ + SEED_0: '000102030405060708090a0b0c0d0e0f', + m_PUB_0: 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8', + m_PRV_0: 'xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi', + m_0H_PUB_0: 'xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw', + m_0H_PRV_0: 'xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7', + m_0H_1_PUB_0: 'xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ', + m_0H_1_PRV_0: 'xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs', + m_0H_1_2H_PUB_0: 'xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5', + m_0H_1_2H_PRV_0: 'xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM', + m_0H_1_2H_2_PUB_0: 'xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV', + m_0H_1_2H_2_PRV_0: 'xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334', + m_0H_1_2H_2_1000000000_PUB_0: 'xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy', + m_0H_1_2H_2_1000000000_PRV_0: 'xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76', + + SEED_1: 'fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542', + m_PUB_1: 'xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB', + m_PRV_1: 'xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U', + m_0_PUB_1: 'xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH', + m_0_PRV_1: 'xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt', + m_0_2147483647H_PUB_1: 'xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a', + m_0_2147483647H_PRV_1: 'xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9', + m_0_2147483647H_1_PUB_1: 'xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon', + m_0_2147483647H_1_PRV_1: 'xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef', + m_0_2147483647H_1_2147483646H_PUB_1: 'xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL', + m_0_2147483647H_1_2147483646H_PRV_1: 'xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc', + m_0_2147483647H_1_2147483646H_2_PUB_1: 'xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt', + m_0_2147483647H_1_2147483646H_2_PRV_1: 'xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j', + + /** + * These vectors test for the retention of leading zeros. + * See bitpay / bitcore - lib#47 and iancoleman / bip39#58 for more information. + */ + SEED_2: '4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be', + m_PUB_2: 'xpub661MyMwAqRbcEZVB4dScxMAdx6d4nFc9nvyvH3v4gJL378CSRZiYmhRoP7mBy6gSPSCYk6SzXPTf3ND1cZAceL7SfJ1Z3GC8vBgp2epUt13', + m_PRV_2: 'xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6', + m_0H_PUB_2: 'xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y', + m_0H_PRV_2: 'xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L', + + /** + * These vectors test for the retention of leading zeros. + * See btcsuite / btcutil#172 for more information. + */ + + SEED_3: '3ddd5602285899a946114506157c7997e5444528f3003f6134712147db19b678', + m_PUB_3: 'xpub661MyMwAqRbcGczjuMoRm6dXaLDEhW1u34gKenbeYqAix21mdUKJyuyu5F1rzYGVxyL6tmgBUAEPrEz92mBXjByMRiJdba9wpnN37RLLAXa', + m_PRV_3: 'xprv9s21ZrQH143K48vGoLGRPxgo2JNkJ3J3fqkirQC2zVdk5Dgd5w14S7fRDyHH4dWNHUgkvsvNDCkvAwcSHNAQwhwgNMgZhLtQC63zxwhQmRv', + m_0H_PUB_3: 'xpub69AUMk3qDBi3uW1sXgjCmVjJ2G6WQoYSnNHyzkmdCHEhSZ4tBok37xfFEqHd2AddP56Tqp4o56AePAgCjYdvpW2PU2jbUPFKsav5ut6Ch1m', + m_0H_PRV_3: 'xprv9vB7xEWwNp9kh1wQRfCCQMnZUEG21LpbR9NPCNN1dwhiZkjjeGRnaALmPXCX7SgjFTiCTT6bXes17boXtjq3xLpcDjzEuGLQBM5ohqkao9G', + m_0H_1H_PUB_3: 'xpub6BJA1jSqiukeaesWfxe6sNK9CCGaujFFSJLomWHprUL9DePQ4JDkM5d88n49sMGJxrhpjazuXYWdMf17C9T5XnxkopaeS7jGk1GyyVziaMt', + m_0H_1H_PRV_3: 'xprv9xJocDuwtYCMNAo3Zw76WENQeAS6WGXQ55RCy7tDJ8oALr4FWkuVoHJeHVAcAqiZLE7Je3vZJHxspZdFHfnBEjHqU5hG1Jaj32dVoS6XLT1', + + /** + * These vectors test that invalid extended keys are recognized as invalid. + */ + INVALID_0: 'xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6LBpB85b3D2yc8sfvZU521AAwdZafEz7mnzBBsz4wKY5fTtTQBm', // (pubkey version / prvkey mismatch) + INVALID_1: 'xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFGTQQD3dC4H2D5GBj7vWvSQaaBv5cxi9gafk7NF3pnBju6dwKvH', // (prvkey version / pubkey mismatch) + INVALID_2: 'xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6Txnt3siSujt9RCVYsx4qHZGc62TG4McvMGcAUjeuwZdduYEvFn', // (invalid pubkey prefix 04) + INVALID_3: 'xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFGpWnsj83BHtEy5Zt8CcDr1UiRXuWCmTQLxEK9vbz5gPstX92JQ', // (invalid prvkey prefix 04) + INVALID_4: 'xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6N8ZMMXctdiCjxTNq964yKkwrkBJJwpzZS4HS2fxvyYUA4q2Xe4', // (invalid pubkey prefix 01) + INVALID_5: 'xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFAzHGBP2UuGCqWLTAPLcMtD9y5gkZ6Eq3Rjuahrv17fEQ3Qen6J', // (invalid prvkey prefix 01) + INVALID_6: 'xprv9s2SPatNQ9Vc6GTbVMFPFo7jsaZySyzk7L8n2uqKXJen3KUmvQNTuLh3fhZMBoG3G4ZW1N2kZuHEPY53qmbZzCHshoQnNf4GvELZfqTUrcv', // (zero depth with non - zero parent fingerprint) + INVALID_7: 'xpub661no6RGEX3uJkY4bNnPcw4URcQTrSibUZ4NqJEw5eBkv7ovTwgiT91XX27VbEXGENhYRCf7hyEbWrR3FewATdCEebj6znwMfQkhRYHRLpJ', // (zero depth with non - zero parent fingerprint) + INVALID_8: 'xprv9s21ZrQH4r4TsiLvyLXqM9P7k1K3EYhA1kkD6xuquB5i39AU8KF42acDyL3qsDbU9NmZn6MsGSUYZEsuoePmjzsB3eFKSUEh3Gu1N3cqVUN', // (zero depth with non - zero index) + INVALID_9: 'xpub661MyMwAuDcm6CRQ5N4qiHKrJ39Xe1R1NyfouMKTTWcguwVcfrZJaNvhpebzGerh7gucBvzEQWRugZDuDXjNDRmXzSZe4c7mnTK97pTvGS8', // (zero depth with non - zero index) + INVALID_10: 'DMwo58pR1QLEFihHiXPVykYB6fJmsTeHvyTp7hRThAtCX8CvYzgPcn8XnmdfHGMQzT7ayAmfo4z3gY5KfbrZWZ6St24UVf2Qgo6oujFktLHdHY4', // (unknown extended key version) + INVALID_11: 'DMwo58pR1QLEFihHiXPVykYB6fJmsTeHvyTp7hRThAtCX8CvYzgPcn8XnmdfHPmHJiEDXkTiJTVV9rHEBUem2mwVbbNfvT2MTcAqj3nesx8uBf9', // (unknown extended key version) + INVALID_12: 'xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzF93Y5wvzdUayhgkkFoicQZcP3y52uPPxFnfoLZB21Teqt1VvEHx', // (private key 0 not in 1..n - 1) + INVALID_13: 'xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFAzHGBP2UuGCqWLTAPLcMtD5SDKr24z3aiUvKr9bJpdrcLg1y3G', // (private key n not in 1..n - 1) + INVALID_14: 'xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6Q5JXayek4PRsn35jii4veMimro1xefsM58PgBMrvdYre8QyULY', // (invalid pubkey 020000000000000000000000000000000000000000000000000000000000000007) + INVALID_15: 'xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHL' // (invalid checksum) +}) + +export const CUSTOM_TEST_VECTORS = Object.freeze({ + ENTROPY_0: "00000000000000000000000000000000", + MNEMONIC_0: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + SEED_0: "5EB00BBDDCF069084889A8AB9155568165F5C453CCB85E70811AAED6F6DA5FC19A5AC40B389CD370D086206DEC8AA6C43DAEA6690F20AD3D8D48B2D2CE9E38E4", + PRIVATE_0: "7F72C7D17BEAC5CDC249D3AEBA8BF76D640129F69DB17E584A4A98E635855D7C", + PUBLIC_0: "588FAABCE802DF8C1700BDF50F2861DE1C0FA48B38B27ECE910D7C696759BAF5", + ADDRESS_0: "nano_1p6hocygi1pzjidi3hho3wn85qiw3ykapg7khu9b45dwf7momgqoytn1c1jz", + + ENTROPY_1: "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f", + MNEMONIC_1: "legal winner thank year wave sausage worth useful legal winner thank year wave sausage wise", + SEED_1: "D95F1FAA0F8AEA406101A81510690D781DA04C86678FC5A13A09C251B7505BB6D17EC0BB408F9D2D6BC9434ADBD79491F09F91186B1E445A392D682D8DB586AD", + PRIVATE_1: "8971838804CE4D715A9250D673EACCAEA7AF26349B6E43B50CCC5554DF0BD342", + PUBLIC_1: "D1DBB524EF0A1A56C91E2EDCE4D7E1A5814CCC45C1B18EDF657E2859BB96A1E9", + ADDRESS_1: "nano_3ngupnkgy4itcu6jwdpwwmdy5be3bm86difjjuhpczjad8xsfahbg61c7ipy", + + ENTROPY_2: "808080808080808080808080808080808080808080808080", + MNEMONIC_2: "letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter always", + SEED_2: "04D5F77103510C41D610F7F5FB3F0BADC77C377090815CEE808EA5D2F264FDFABF7C7DED4BE6D4C6D7CDB021BA4C777B0B7E57CA8AA6DE15AEB9905DBA674D66", + PRIVATE_2: "50F76F0211E8F18D3554C6C0DD8131E75BDBB2AC392BF6FBE23698E900EFCA13", + PUBLIC_2: "9C93BD3C0796381F7562E3E14D2E3CF0F4056226149D7CAC891B241434B4285A", + ADDRESS_2: "nano_396mqny1h7jr5xtp7rz3bnq5sw9n1oj4e76xhkpak8s64itdac4t95ux7xn4", + + ENTROPY_3: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + MNEMONIC_3: "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo veteran", + SEED_3: "DB4BFE5911205C0B2E048CEEF790D0433A902A070D0744AF9B5E88ED5E0AEF548246102DD6BA2313E418FD799360E2DBD8EB93EE40FE0942517555B66E89D488", + PRIVATE_3: "133F351F1EDF5B4FE86EA7849AEC2C7E4D3F5F16C0CBAB4A236ADF17ACCB98B0", + PUBLIC_3: "2D6E50265036DE634FC71A2F9BE8A336AB991B8751C15D883ACCD90BFD3BEDB6", + ADDRESS_3: "nano_1ddgc1m71fpyef9wg8jhmhnc8fodm6frgng3dp65om8s3hymqufp8jefijxu", +}) diff --git a/test/create-wallet.test.mjs b/test/create-wallet.test.mjs new file mode 100644 index 0000000..256eb81 --- /dev/null +++ b/test/create-wallet.test.mjs @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later + +'use strict' + +import { describe, it } from 'node:test' +import { strict as assert } from 'assert' +import { Account, Bip44Wallet, Blake2bWallet, LedgerWallet } from '../dist/main.js' +import { NANO_TEST_VECTORS, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js' + +// WARNING: Do not send any funds to the test vectors below +// Test vectors from https://docs.nano.org/integration-guides/key-management/ and elsewhere +describe('generate wallet test', async () => { + it('should fail to create a wallet when using new', () => { + assert.throws(() => new Bip44Wallet()) + assert.throws(() => new Blake2bWallet()) + assert.throws(() => new LedgerWallet()) + }) + + it('should fail to create a software wallet without a password', async () => { + await assert.rejects(Bip44Wallet.create()) + await assert.rejects(Blake2bWallet.create()) + }) + + it('should replace invalid salt with empty string', async () => { + const invalidArgs = [null, true, false, 0, 1, 2, { "foo": "bar" }] + for (const arg of invalidArgs) { + await assert.doesNotReject(Bip44Wallet.create(NANO_TEST_VECTORS.PASSWORD, arg), `Rejected ${arg}`) + } + }) + + it('should generate a BIP-44 wallet with random entropy', async () => { + const wallet = await Bip44Wallet.create(NANO_TEST_VECTORS.PASSWORD) + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + const accounts = await wallet.accounts() + + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.ok(accounts[0] instanceof Account) + }) + + it('should generate a BLAKE2b wallet with random entropy', async () => { + const wallet = await Blake2bWallet.create(NANO_TEST_VECTORS.PASSWORD) + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + const accounts = await wallet.accounts() + + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.ok(accounts[0] instanceof Account) + }) + + it('should generate the correct wallet with the given test vector', async () => { + const wallet = await Bip44Wallet.fromEntropy(NANO_TEST_VECTORS.PASSWORD, '6CAF5A42BB8074314AAE20295975ECE663BE7AAD945A73613D193B0CC41C7970') + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + const accounts = await wallet.accounts() + + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.ok(accounts[0] instanceof Account) + assert.equal(wallet.mnemonic, 'hole kiss mouse jacket also board click series citizen slight kite smoke desk diary rent mercy inflict antique edge invite slush athlete total brain') + assert.equal(wallet.seed, '1ACCDD4C25E06E47310D0C62C290EC166071D024352E003E5366E8BA6BA523F2A0CB34116AC55A238A886778880A9B2A547112FD7CFFADE81D8D8D084CCB7D36') + assert.equal(accounts[0].privateKey, 'EB18B748BCC48F824CF8A1FE92F7FC93BFC6F2A1EB9C1D40FA26D335D8A0C30F') + assert.equal(accounts[0].publicKey, 'A9EF7BBC004813CF75C5FC5C582066182D5C9CFFD42EB7EB81CEFEA8E78C47C5') + assert.equal(accounts[0].address, 'nano_3chhhgy11k1msxtwdz4wd1i8e83fdkghzo3gpzor5mqyo5mrrjy79zpw1g34') + }) + + it('should generate the correct wallet with the given test vector and a seed password', async () => { + const wallet = await Bip44Wallet.fromEntropy(NANO_TEST_VECTORS.PASSWORD, '6CAF5A42BB8074314AAE20295975ECE663BE7AAD945A73613D193B0CC41C7970', NANO_TEST_VECTORS.PASSWORD) + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + const accounts = await wallet.accounts() + + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.ok(accounts[0] instanceof Account) + + assert.equal(wallet.mnemonic, 'hole kiss mouse jacket also board click series citizen slight kite smoke desk diary rent mercy inflict antique edge invite slush athlete total brain') + assert.notEqual(wallet.seed, '1ACCDD4C25E06E47310D0C62C290EC166071D024352E003E5366E8BA6BA523F2A0CB34116AC55A238A886778880A9B2A547112FD7CFFADE81D8D8D084CCB7D36') + assert.notEqual(accounts[0].privateKey, 'EB18B748BCC48F824CF8A1FE92F7FC93BFC6F2A1EB9C1D40FA26D335D8A0C30F') + assert.notEqual(accounts[0].publicKey, 'A9EF7BBC004813CF75C5FC5C582066182D5C9CFFD42EB7EB81CEFEA8E78C47C5') + assert.notEqual(accounts[0].address, 'nano_3chhhgy11k1msxtwdz4wd1i8e83fdkghzo3gpzor5mqyo5mrrjy79zpw1g34') + + assert.equal(wallet.seed, '146E3E2A0530848C9174D45ECEC8C3F74A7BE3F1EE832F92EB6227284121EB2E48A6B8FC469403984CD5E8F0D1ED05777C78F458D0E98C911841590E5D645DC3') + assert.equal(accounts[0].privateKey, '2D5851BD5A89B8C943078BE6AD5BBEE8AEAB77D6A4744C20D1B87D78E3286B93') + assert.equal(accounts[0].publicKey, '923B6C7E281C1C5529FD2DC848117781216A1753CFD487FC34009F3591E636D7') + assert.equal(accounts[0].address, 'nano_36jufjz4i91wcnnztdgab1aqh1b3fado9mynizy5a16z8payefpqo81zsshc') + }) + + it('should throw when given invalid entropy with an invalid length', async () => { + assert.rejects(async () => await Bip44Wallet.fromEntropy(NANO_TEST_VECTORS.PASSWORD, '6CAF5A42BB8074314AAE20295975ECE663BE7AAD945A73613D193B0CC41C797')) + assert.rejects(async () => await Bip44Wallet.fromEntropy(NANO_TEST_VECTORS.PASSWORD, '6CAF5A42BB8074314AAE20295975ECE663BE7AAD945A73613D193B0CC41C79701')) + assert.rejects(async () => await Bip44Wallet.fromEntropy(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_0.replaceAll(/./g, 'x'))) + }) +}) + +describe('ledger wallet', { skip: true }, async () => { + it('should connect to ledger', async () => { + const wallet = await LedgerWallet.create() + assert.ok(wallet) + }) +}) diff --git a/test/derive-accounts.test.mjs b/test/derive-accounts.test.mjs new file mode 100644 index 0000000..55697fe --- /dev/null +++ b/test/derive-accounts.test.mjs @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later + +'use strict' + +import { describe, it } from 'node:test' +import { strict as assert } from 'assert' +import { Bip44Wallet, Blake2bWallet, LedgerWallet } from '../dist/main.js' +import { NANO_TEST_VECTORS } from './TEST_VECTORS.js' + +// WARNING: Do not send any funds to the test vectors below +// Test vectors from https://docs.nano.org/integration-guides/key-management/ and elsewhere +describe('derive child accounts from the same seed', async () => { + const wallet = await Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED) + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + + it('should derive the first account from the given BIP-44 seed', async function () { + const accounts = await wallet.accounts() + + assert.equal(accounts.length, 1) + assert.equal(accounts[0].privateKey, NANO_TEST_VECTORS.PRIVATE_0) + assert.equal(accounts[0].publicKey, NANO_TEST_VECTORS.PUBLIC_0) + assert.equal(accounts[0].address, NANO_TEST_VECTORS.ADDRESS_0) + }) + + it('should derive low indexed accounts from the given BIP-44 seed', async function () { + const accounts = await wallet.accounts(0, 2) + + assert.equal(accounts.length, 3) + assert.equal(accounts[0].privateKey, NANO_TEST_VECTORS.PRIVATE_0) + assert.equal(accounts[0].publicKey, NANO_TEST_VECTORS.PUBLIC_0) + assert.equal(accounts[0].address, NANO_TEST_VECTORS.ADDRESS_0) + assert.equal(accounts[1].privateKey, NANO_TEST_VECTORS.PRIVATE_1) + assert.equal(accounts[1].publicKey, NANO_TEST_VECTORS.PUBLIC_1) + assert.equal(accounts[1].address, NANO_TEST_VECTORS.ADDRESS_1) + assert.equal(accounts[2].privateKey, NANO_TEST_VECTORS.PRIVATE_2) + assert.equal(accounts[2].publicKey, NANO_TEST_VECTORS.PUBLIC_2) + assert.equal(accounts[2].address, NANO_TEST_VECTORS.ADDRESS_2) + }) + + it('should derive high indexed accounts from the given seed', async function () { + const accounts = await wallet.accounts(0x70000000, 0x700000ff) + + assert.equal(accounts.length, 0x100) + for (const a of accounts) { + assert.ok(a) + assert.ok(a.address) + assert.ok(a.publicKey) + assert.ok(a.privateKey) + assert.ok(a.index != null) + } + }) + + it('should derive accounts for a BLAKE2b wallet', async function () { + const bwallet = await Blake2bWallet.create(NANO_TEST_VECTORS.PASSWORD) + await bwallet.unlock(NANO_TEST_VECTORS.PASSWORD) + const lowAccounts = await bwallet.accounts(0, 2) + + assert.equal(lowAccounts.length, 3) + for (const a of lowAccounts) { + assert.ok(a) + assert.ok(a.address) + assert.ok(a.publicKey) + assert.ok(a.privateKey) + assert.ok(a.index != null) + } + + const highAccounts = await bwallet.accounts(0x70000000, 0x700000ff) + + assert.equal(highAccounts.length, 0x100) + for (const a of highAccounts) { + assert.ok(a) + assert.ok(a.address) + assert.ok(a.publicKey) + assert.ok(a.privateKey) + assert.ok(a.index != null) + } + }) +}) + +describe('Ledger device accounts', { skip: true }, async () => { + const wallet = await LedgerWallet.create() + + it('should fetch the first account from a Ledger device', async function () { + const accounts = await wallet.accounts() + + assert.equal(accounts.length, 1) + assert.ok(accounts[0].publicKey) + assert.ok(accounts[0].address) + }) +}) diff --git a/test/import-wallet.test.mjs b/test/import-wallet.test.mjs new file mode 100644 index 0000000..032db9f --- /dev/null +++ b/test/import-wallet.test.mjs @@ -0,0 +1,175 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later + +'use strict' + +import { describe, it } from 'node:test' +import { strict as assert } from 'assert' +import { Account, Bip44Wallet, Blake2bWallet } from '../dist/main.js' +import { BIP32_TEST_VECTORS, CUSTOM_TEST_VECTORS, NANO_TEST_VECTORS, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js' + +// WARNING: Do not send any funds to the test vectors below +// Test vectors from https://docs.nano.org/integration-guides/key-management/ and elsewhere +describe('import wallet with test vectors test', () => { + it('should successfully import a wallet with the official Nano test vectors mnemonic', async () => { + const wallet = await Bip44Wallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD) + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + const accounts = await wallet.accounts() + + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.ok(accounts[0] instanceof Account) + assert.equal(wallet.mnemonic, NANO_TEST_VECTORS.MNEMONIC) + assert.equal(wallet.seed, NANO_TEST_VECTORS.BIP39_SEED) + assert.equal(accounts[0].privateKey, NANO_TEST_VECTORS.PRIVATE_0) + assert.equal(accounts[0].publicKey, NANO_TEST_VECTORS.PUBLIC_0) + assert.equal(accounts[0].address, NANO_TEST_VECTORS.ADDRESS_0) + }) + + it('should successfully import a wallet with the checksum starting with a zero', async () => { + const wallet = await Bip44Wallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, 'food define cancel major spoon trash cigar basic aim bless wolf win ability seek paddle bench seed century group they mercy address monkey cake') + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + assert.equal(wallet.seed, 'F665F804E5907985455D1E5A7AD344843A2ED4179A7E06EEF263DE925FF6F4C0991B0A9344FCEE939FE0F1B1841B8C9B20FEACF6B954B74B2D26A01906B758E2') + }) + + it('should successfully import a wallet with a 12-word phrase', async () => { + const wallet = await Bip44Wallet.fromEntropy(NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.ENTROPY_0) + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + const accounts = await wallet.accounts() + const account = accounts[0] + + assert.equal(wallet.mnemonic, CUSTOM_TEST_VECTORS.MNEMONIC_0) + assert.equal(wallet.seed, CUSTOM_TEST_VECTORS.SEED_0) + assert.equal(account.privateKey, CUSTOM_TEST_VECTORS.PRIVATE_0) + assert.equal(account.publicKey, CUSTOM_TEST_VECTORS.PUBLIC_0) + assert.equal(account.address, CUSTOM_TEST_VECTORS.ADDRESS_0) + }) + + it('should successfully import a wallet with a 15-word phrase', async () => { + const wallet = await Bip44Wallet.fromEntropy(NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.ENTROPY_1) + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + const accounts = await wallet.accounts() + const account = accounts[0] + + assert.equal(wallet.mnemonic, CUSTOM_TEST_VECTORS.MNEMONIC_1) + assert.equal(wallet.seed, CUSTOM_TEST_VECTORS.SEED_1) + assert.equal(account.privateKey, CUSTOM_TEST_VECTORS.PRIVATE_1) + assert.equal(account.publicKey, CUSTOM_TEST_VECTORS.PUBLIC_1) + assert.equal(account.address, CUSTOM_TEST_VECTORS.ADDRESS_1) + }) + + it('should successfully import a wallet with a 18-word phrase', async () => { + const wallet = await Bip44Wallet.fromEntropy(NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.ENTROPY_2) + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + const accounts = await wallet.accounts() + const account = accounts[0] + + assert.equal(wallet.mnemonic, CUSTOM_TEST_VECTORS.MNEMONIC_2) + assert.equal(wallet.seed, CUSTOM_TEST_VECTORS.SEED_2) + assert.equal(account.privateKey, CUSTOM_TEST_VECTORS.PRIVATE_2) + assert.equal(account.publicKey, CUSTOM_TEST_VECTORS.PUBLIC_2) + assert.equal(account.address, CUSTOM_TEST_VECTORS.ADDRESS_2) + }) + + it('should successfully import a wallet with a 21-word phrase', async () => { + const wallet = await Bip44Wallet.fromEntropy(NANO_TEST_VECTORS.PASSWORD, CUSTOM_TEST_VECTORS.ENTROPY_3) + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + const accounts = await wallet.accounts() + const account = accounts[0] + + assert.equal(wallet.mnemonic, CUSTOM_TEST_VECTORS.MNEMONIC_3) + assert.equal(wallet.seed, CUSTOM_TEST_VECTORS.SEED_3) + assert.equal(account.privateKey, CUSTOM_TEST_VECTORS.PRIVATE_3) + assert.equal(account.publicKey, CUSTOM_TEST_VECTORS.PUBLIC_3) + assert.equal(account.address, CUSTOM_TEST_VECTORS.ADDRESS_3) + }) + + it('should successfully import a wallet with the official Nano test vectors seed', async () => { + const wallet = await Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED) + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + const accounts = await wallet.accounts() + + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.ok(accounts[0] instanceof Account) + assert.equal(wallet.mnemonic, '') + assert.equal(wallet.seed, NANO_TEST_VECTORS.BIP39_SEED) + assert.equal(accounts[0].privateKey, NANO_TEST_VECTORS.PRIVATE_0) + assert.equal(accounts[0].publicKey, NANO_TEST_VECTORS.PUBLIC_0) + assert.equal(accounts[0].address, NANO_TEST_VECTORS.ADDRESS_0) + }) + + it('should successfully import a BIP-44 wallet with the zero seed', async () => { + const wallet = await Bip44Wallet.fromEntropy(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_0, TREZOR_TEST_VECTORS.PASSWORD) + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + const accounts = await wallet.accounts(0, 3) + + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.equal(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_0) + assert.equal(wallet.seed, TREZOR_TEST_VECTORS.SEED_0.toUpperCase()) + assert.equal(accounts.length, 4) + for (let i = 0; i < accounts.length; i++) { + assert.ok(accounts[i]) + assert.ok(accounts[i].address) + assert.ok(accounts[i].publicKey) + assert.ok(accounts[i].privateKey) + assert.equal(accounts[i].index, i) + } + }) + + it('should successfully import a BLAKE2b wallet with the zero seed', async () => { + const wallet = await Blake2bWallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_0) + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + const accounts = await wallet.accounts(0, 3) + + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.equal(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_0) + assert.equal(wallet.seed, TREZOR_TEST_VECTORS.ENTROPY_0) + assert.equal(accounts.length, 4) + for (let i = 0; i < accounts.length; i++) { + assert.ok(accounts[i]) + assert.ok(accounts[i].address) + assert.ok(accounts[i].publicKey) + assert.ok(accounts[i].privateKey) + assert.equal(accounts[i].index, i) + } + }) + + it('should get identical BLAKE2b wallets when created with a seed versus with its derived mnemonic', async () => { + const wallet = await Blake2bWallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_0) + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + const walletAccounts = await wallet.accounts() + + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.ok(walletAccounts[0]) + assert.equal(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_0) + + const imported = await Blake2bWallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_0) + await imported.unlock(NANO_TEST_VECTORS.PASSWORD) + const importedAccounts = await imported.accounts() + + assert.equal(imported.mnemonic, wallet.mnemonic) + assert.equal(imported.seed, wallet.seed) + assert.equal(importedAccounts[0].privateKey, walletAccounts[0].privateKey) + assert.equal(importedAccounts[0].publicKey, walletAccounts[0].publicKey) + }) +}) + +describe('invalid wallet', async () => { + it('should throw when given a seed with an invalid length', async () => { + await assert.rejects(Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED + 'f'), + { message: `Expected a ${NANO_TEST_VECTORS.BIP39_SEED.length}-character seed, but received ${NANO_TEST_VECTORS.BIP39_SEED.length + 1}-character string.` }) + await assert.rejects(Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED.slice(0, -1)), + { message: `Expected a ${NANO_TEST_VECTORS.BIP39_SEED.length}-character seed, but received ${NANO_TEST_VECTORS.BIP39_SEED.length - 1}-character string.` }) + }) + + it('should throw when given a seed containing non-hex characters', async () => { + await assert.rejects(Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.SEED_0.replace(/./, 'g')), + { message: 'Seed contains invalid hexadecimal characters.' }) + await assert.rejects(Blake2bWallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_1.replace(/./, 'g')), + { message: 'Seed contains invalid hexadecimal characters.' }) + }) +}) diff --git a/test/lock-unlock-wallet.mjs b/test/lock-unlock-wallet.mjs new file mode 100644 index 0000000..b4a324d --- /dev/null +++ b/test/lock-unlock-wallet.mjs @@ -0,0 +1,245 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later + +'use strict' + +import { describe, it } from 'node:test' +import { strict as assert } from 'assert' +import { Bip44Wallet, Blake2bWallet } from '../dist/main.js' +import { NANO_TEST_VECTORS, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js' + +const skip = false + +// WARNING: Do not send any funds to the test vectors below +// Test vectors from https://docs.nano.org/integration-guides/key-management/ and elsewhere +describe('locking and unlocking a Bip44Wallet', { skip }, async () => { + it('should succeed with a password', async () => { + const wallet = await Bip44Wallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD) + + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.equal(wallet.mnemonic, '') + assert.equal(wallet.seed, '') + + const unlockResult = await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + + assert.equal(unlockResult, true) + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.equal(wallet.mnemonic, NANO_TEST_VECTORS.MNEMONIC) + assert.equal(wallet.seed, NANO_TEST_VECTORS.BIP39_SEED) + }) + + it('should succeed with a random CryptoKey', async () => { + const wallet = await Bip44Wallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD) + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + const key = await globalThis.crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']) + const lockResult = await wallet.lock(key) + + assert.ok(lockResult) + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.equal(wallet.mnemonic, '') + assert.equal(wallet.seed, '') + + const unlockResult = await wallet.unlock(key) + + assert.equal(unlockResult, true) + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.equal(wallet.mnemonic, NANO_TEST_VECTORS.MNEMONIC) + assert.equal(wallet.seed, NANO_TEST_VECTORS.BIP39_SEED) + }) + + it('should fail to unlock with different passwords', async () => { + const wallet = await Bip44Wallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD) + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + const lockResult = await wallet.lock(TREZOR_TEST_VECTORS.PASSWORD) + + await assert.rejects(wallet.unlock(NANO_TEST_VECTORS.PASSWORD), { message: 'Failed to unlock wallet' }) + assert.equal(lockResult, true) + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.notEqual(wallet.mnemonic, NANO_TEST_VECTORS.MNEMONIC) + assert.notEqual(wallet.seed, NANO_TEST_VECTORS.BIP39_SEED) + }) + + it('should fail to unlock with different keys', async () => { + const wallet = await Bip44Wallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD) + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + const rightKey = await globalThis.crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']) + const wrongKey = await globalThis.crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']) + const lockResult = await wallet.lock(rightKey) + + await assert.rejects(wallet.unlock(wrongKey), { message: 'Failed to unlock wallet' }) + assert.equal(lockResult, true) + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.notEqual(wallet.mnemonic, NANO_TEST_VECTORS.MNEMONIC) + assert.notEqual(wallet.seed, NANO_TEST_VECTORS.BIP39_SEED) + }) + + it('should fail to unlock with different valid inputs', async () => { + const wallet = await Bip44Wallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD) + const key = await globalThis.crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']) + + await assert.rejects(wallet.unlock(key), { message: 'Failed to unlock wallet' }) + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.notEqual(wallet.mnemonic, NANO_TEST_VECTORS.MNEMONIC) + assert.notEqual(wallet.seed, NANO_TEST_VECTORS.BIP39_SEED) + }) + + it('should fail with no input', async () => { + const wallet = await Bip44Wallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD) + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + + await assert.rejects(wallet.lock(), { message: 'Failed to lock wallet' }) + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.equal(wallet.mnemonic, NANO_TEST_VECTORS.MNEMONIC) + assert.equal(wallet.seed, NANO_TEST_VECTORS.BIP39_SEED) + + await wallet.lock('password') + + await assert.rejects(wallet.unlock(), { message: 'Failed to unlock wallet' }) + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.notEqual(wallet.mnemonic, NANO_TEST_VECTORS.MNEMONIC) + assert.notEqual(wallet.seed, NANO_TEST_VECTORS.BIP39_SEED) + }) + + it('should fail with invalid input', async () => { + const wallet = await Bip44Wallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.MNEMONIC, NANO_TEST_VECTORS.PASSWORD) + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + + await assert.rejects(wallet.lock(1), { message: 'Failed to lock wallet' }) + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.equal(wallet.mnemonic, NANO_TEST_VECTORS.MNEMONIC) + assert.equal(wallet.seed, NANO_TEST_VECTORS.BIP39_SEED) + + await wallet.lock(NANO_TEST_VECTORS.PASSWORD) + + await assert.rejects(wallet.unlock(1), { message: 'Failed to unlock wallet' }) + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.notEqual(wallet.mnemonic, NANO_TEST_VECTORS.MNEMONIC) + assert.notEqual(wallet.seed, NANO_TEST_VECTORS.BIP39_SEED) + }) +}) + +describe('locking and unlocking a Blake2bWallet', { skip }, async () => { + it('should succeed with a password', async () => { + const wallet = await Blake2bWallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_0) + + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.equal(wallet.mnemonic, '') + assert.equal(wallet.seed, '') + + const unlockResult = await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + + assert.equal(unlockResult, true) + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.equal(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_0) + assert.equal(wallet.seed, TREZOR_TEST_VECTORS.ENTROPY_0) + }) + + it('should succeed with a random CryptoKey', async () => { + const wallet = await Blake2bWallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_1) + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + const key = await globalThis.crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']) + const lockResult = await wallet.lock(key) + + assert.equal(lockResult, true) + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.equal(wallet.mnemonic, '') + assert.equal(wallet.seed, '') + + const unlockResult = await wallet.unlock(key) + + assert.equal(lockResult, true) + assert.equal(unlockResult, true) + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.equal(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_1) + assert.equal(wallet.seed, TREZOR_TEST_VECTORS.ENTROPY_1) + }) + + it('should fail to unlock with different passwords', async () => { + const wallet = await Blake2bWallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_1) + + await assert.rejects(wallet.unlock(TREZOR_TEST_VECTORS.PASSWORD), { message: 'Failed to unlock wallet' }) + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.notEqual(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_1) + assert.notEqual(wallet.seed, TREZOR_TEST_VECTORS.ENTROPY_1) + }) + + it('should fail to unlock with different keys', async () => { + const wallet = await Blake2bWallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_1) + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + const rightKey = await globalThis.crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']) + const wrongKey = await globalThis.crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']) + const lockResult = await wallet.lock(rightKey) + + await assert.rejects(wallet.unlock(wrongKey), { message: 'Failed to unlock wallet' }) + assert.equal(lockResult, true) + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.notEqual(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_1) + assert.notEqual(wallet.seed, TREZOR_TEST_VECTORS.ENTROPY_1) + }) + + it('should fail to unlock with different valid inputs', async () => { + const wallet = await Blake2bWallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_1) + const key = await globalThis.crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']) + + await assert.rejects(wallet.unlock(key), { message: 'Failed to unlock wallet' }) + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.notEqual(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_1) + assert.notEqual(wallet.seed, TREZOR_TEST_VECTORS.ENTROPY_1) + }) + + it('should fail with no input', async () => { + const wallet = await Blake2bWallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_1) + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + + await assert.rejects(wallet.lock(), { message: 'Failed to lock wallet' }) + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.equal(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_1) + assert.equal(wallet.seed, TREZOR_TEST_VECTORS.ENTROPY_1) + + await wallet.lock(NANO_TEST_VECTORS.PASSWORD) + + await assert.rejects(wallet.unlock(), { message: 'Failed to unlock wallet' }) + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.notEqual(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_1) + assert.notEqual(wallet.seed, TREZOR_TEST_VECTORS.ENTROPY_1) + }) + + it('should fail with invalid input', async () => { + const wallet = await Blake2bWallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_1) + await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) + + await assert.rejects(wallet.lock(1), { message: 'Failed to lock wallet' }) + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.equal(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_1) + assert.equal(wallet.seed, TREZOR_TEST_VECTORS.ENTROPY_1) + + await wallet.lock(NANO_TEST_VECTORS.PASSWORD) + + await assert.rejects(wallet.unlock(1), { message: 'Failed to unlock wallet' }) + assert.ok('mnemonic' in wallet) + assert.ok('seed' in wallet) + assert.notEqual(wallet.mnemonic, TREZOR_TEST_VECTORS.MNEMONIC_1) + assert.notEqual(wallet.seed, TREZOR_TEST_VECTORS.ENTROPY_1) + }) +}) diff --git a/test/manage-rolodex.mjs b/test/manage-rolodex.mjs new file mode 100644 index 0000000..93fde29 --- /dev/null +++ b/test/manage-rolodex.mjs @@ -0,0 +1,120 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later + +'use strict' + +import { describe, it } from 'node:test' +import { strict as assert } from 'assert' +import { Rolodex, Tools } from '../dist/main.js' +import { NANO_TEST_VECTORS } from './TEST_VECTORS.js' + +describe('rolodex valid contact management', async () => { + it('should create a rolodex and add a contact', async () => { + const rolodex = new Rolodex() + assert.equal(rolodex.constructor, Rolodex) + await rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_0) + assert.equal(rolodex.getAddresses('JohnDoe').length, 1) + assert.equal(rolodex.getAddresses('JohnDoe')[0], NANO_TEST_VECTORS.ADDRESS_0) + }) + + it('should get a name from an address', async () => { + const rolodex = new Rolodex() + await rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_0) + assert.equal(rolodex.getName(NANO_TEST_VECTORS.ADDRESS_0), 'JohnDoe') + }) + + it('should add three addresses to the same contact', async () => { + const rolodex = new Rolodex() + await rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_1) + await rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_2) + await rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_0) + assert.equal(rolodex.getAddresses('JohnDoe').length, 3) + assert.equal(rolodex.getAddresses('JohnDoe')[0], NANO_TEST_VECTORS.ADDRESS_1) + assert.equal(rolodex.getAddresses('JohnDoe')[1], NANO_TEST_VECTORS.ADDRESS_2) + assert.equal(rolodex.getAddresses('JohnDoe')[2], NANO_TEST_VECTORS.ADDRESS_0) + }) + + it('should update the name on an existing entry', async () => { + const rolodex = new Rolodex() + await rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_0) + await rolodex.add('JaneSmith', NANO_TEST_VECTORS.ADDRESS_0) + assert.equal(rolodex.getAddresses('JohnDoe').length, 0) + assert.equal(rolodex.getAddresses('JaneSmith').length, 1) + assert.equal(rolodex.getAddresses('JaneSmith')[0], NANO_TEST_VECTORS.ADDRESS_0) + }) + + it('should return empty address array for an unknown contact', async () => { + const rolodex = new Rolodex() + await rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_0) + assert.equal(Array.isArray(rolodex.getAddresses('JaneSmith')), true) + assert.equal(rolodex.getAddresses('JaneSmith').length, 0) + }) + + it('should return empty address array for blank contact names', () => { + const rolodex = new Rolodex() + assert.equal(Array.isArray(rolodex.getAddresses(undefined)), true) + assert.equal(rolodex.getAddresses(undefined).length, 0) + assert.equal(Array.isArray(rolodex.getAddresses(null)), true) + assert.equal(rolodex.getAddresses(null).length, 0) + assert.equal(Array.isArray(rolodex.getAddresses('')), true) + assert.equal(rolodex.getAddresses('').length, 0) + }) + + it('should return null for an unknown address', async () => { + const rolodex = new Rolodex() + await rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_0) + assert.equal(rolodex.getName(NANO_TEST_VECTORS.ADDRESS_1), null) + assert.notEqual(rolodex.getName(NANO_TEST_VECTORS.ADDRESS_1), undefined) + }) + + it('should return null for a blank address', async () => { + const rolodex = new Rolodex() + await rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_0) + assert.equal(rolodex.getName(undefined), null) + assert.notEqual(rolodex.getName(undefined), undefined) + assert.equal(rolodex.getName(null), null) + assert.notEqual(rolodex.getName(null), undefined) + assert.equal(rolodex.getName(''), null) + assert.notEqual(rolodex.getName(''), undefined) + }) +}) + +describe('rolodex exceptions', async () => { + it('should throw if adding no data', async () => { + const rolodex = new Rolodex() + await assert.rejects(rolodex.add()) + }) + + it('should throw if passed no address', async () => { + const rolodex = new Rolodex() + await assert.rejects(rolodex.add('JohnDoe')) + await assert.rejects(rolodex.add('JohnDoe', undefined)) + await assert.rejects(rolodex.add('JohnDoe', null)) + await assert.rejects(rolodex.add('JohnDoe', '')) + }) + + it('should throw if name is blank', async () => { + const rolodex = new Rolodex() + await assert.rejects(rolodex.add(undefined, NANO_TEST_VECTORS.ADDRESS_0)) + await assert.rejects(rolodex.add(null, NANO_TEST_VECTORS.ADDRESS_0)) + await assert.rejects(rolodex.add('', NANO_TEST_VECTORS.ADDRESS_0)) + }) +}) + +describe('rolodex data signature verification', async () => { + const data = 'Test data' + const signature = await Tools.sign(NANO_TEST_VECTORS.PRIVATE_0, data) + const rolodex = new Rolodex() + + it('should verify valid data and signature', async () => { + await rolodex.add('JohnDoe', NANO_TEST_VECTORS.ADDRESS_0) + const result = await rolodex.verify('JohnDoe', signature, data) + assert.equal(result, true) + }) + + it('should reject incorrect contact for signature', async () => { + await rolodex.add('JaneSmith', NANO_TEST_VECTORS.ADDRESS_1) + const result = await rolodex.verify('JaneSmith', signature, data) + assert.equal(result, false) + }) +}) diff --git a/test/refresh-accounts.test.mjs b/test/refresh-accounts.test.mjs new file mode 100644 index 0000000..1226e03 --- /dev/null +++ b/test/refresh-accounts.test.mjs @@ -0,0 +1,126 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later + +'use strict' + +import { describe, it } from 'node:test' +import { strict as assert } from 'assert' +import { Account, Bip44Wallet, Node } from '../dist/main.js' +import { NANO_TEST_VECTORS } from './TEST_VECTORS.js' + +// WARNING: Do not send any funds to the test vectors below +// Test vectors from https://docs.nano.org/integration-guides/key-management/ and elsewhere + +const wallet = await Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED) +const node = new Node(process.env.NODE_URL, process.env.API_KEY_NAME, process.env.API_KEY_VALUE) + +const skip = true + +describe('refreshing account info', { skip }, async () => { + it('should fetch balance, frontier, and representative', async () => { + const accounts = await wallet.accounts() + const account = accounts[0] + await account.refresh(node) + + assert.equal(typeof account.balance, 'string') + assert.notEqual(account.balance, undefined) + assert.notEqual(account.balance, null) + assert.notEqual(account.balance, '') + assert.equal(isNaN(parseInt(account.balance)), false) + assert.equal(parseInt(account.balance) < 0, false) + + assert.equal(typeof account.frontier, 'string') + assert.notEqual(account.frontier, undefined) + assert.notEqual(account.frontier, null) + assert.notEqual(account.frontier, '') + assert.match(account.frontier, /^[0-9A-F]{64}$/i) + + assert.equal(account.representative.constructor, Account) + assert.notEqual(account.representative, undefined) + assert.notEqual(account.representative, null) + assert.notEqual(account.representative, '') + assert.notEqual(account.representative.address, undefined) + assert.notEqual(account.representative.address, null) + assert.notEqual(account.representative.address, '') + }) + + it('should throw when refreshing unopened account', async () => { + const accounts = await wallet.accounts(0x7fffffff) + const account = accounts[0] + await assert.rejects(account.refresh(node), + { message: 'Account not found' }) + }) + + it('should throw when referencing invalid account index', async () => { + await assert.rejects(wallet.accounts(0x80000000), + { message: 'Invalid child key index 0x80000000' }) + }) + + it('should throw with invalid node', async () => { + const invalidNode = new Node('http://invalid.com') + const accounts = await wallet.accounts() + const account = accounts[0] + await assert.rejects(account.refresh(invalidNode), + { message: 'Account not found' }) + }) +}) + +describe('finding next unopened account', { skip }, async () => { + it('should return correct account from test vector', async () => { + const account = await wallet.getNextNewAccount(node) + assert.ok(account) + assert.equal(account.address, NANO_TEST_VECTORS.ADDRESS_1) + assert.equal(account.publicKey, NANO_TEST_VECTORS.PUBLIC_1) + }) + + it('should return successfully for small batch size', async () => { + const account = await wallet.getNextNewAccount(node, 1) + assert.ok(account) + assert.equal(account.address, NANO_TEST_VECTORS.ADDRESS_1) + assert.equal(account.publicKey, NANO_TEST_VECTORS.PUBLIC_1) + }) + + it('should return successfully for large batch size', async () => { + const account = await wallet.getNextNewAccount(node, 100) + assert.ok(account) + assert.equal(account.address, NANO_TEST_VECTORS.ADDRESS_1) + assert.equal(account.publicKey, NANO_TEST_VECTORS.PUBLIC_1) + }) + + it('should throw on invalid node URL', async () => { + await assert.rejects(wallet.getNextNewAccount()) + await assert.rejects(wallet.getNextNewAccount(null)) + await assert.rejects(wallet.getNextNewAccount(1)) + await assert.rejects(wallet.getNextNewAccount('')) + await assert.rejects(wallet.getNextNewAccount('foo')) + }) + + it('should throw on invalid batch size', async () => { + await assert.rejects(wallet.getNextNewAccount(node, null)) + await assert.rejects(wallet.getNextNewAccount(node, -1)) + await assert.rejects(wallet.getNextNewAccount(node, '')) + await assert.rejects(wallet.getNextNewAccount(node, 'foo')) + await assert.rejects(wallet.getNextNewAccount(node, { 'foo': 'bar' })) + }) +}) + +describe('refreshing wallet accounts', { skip }, async () => { + it('should get balance, frontier, and representative for one account', async () => { + const accounts = await wallet.refresh(node) + assert.ok(accounts[0] instanceof Account) + assert.equal(typeof accounts[0].balance, 'string') + assert.notEqual(accounts[0].frontier, undefined) + assert.notEqual(accounts[0].frontier, null) + assert.equal(typeof accounts[0].frontier, 'string') + }) + + it('should get balance, frontier, and representative for multiple accounts', async () => { + const accounts = await wallet.refresh(node, 0, 2) + assert.equal(accounts.length, 1) + assert.ok(accounts[0] instanceof Account) + }) + + it('should handle failure gracefully', async () => { + await assert.doesNotReject(wallet.refresh(node, 0, 20)) + }) +}) diff --git a/test/sign-blocks.test.mjs b/test/sign-blocks.test.mjs new file mode 100644 index 0000000..a02a9c2 --- /dev/null +++ b/test/sign-blocks.test.mjs @@ -0,0 +1,151 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later + +'use strict' + +import { describe, it } from 'node:test' +import { strict as assert } from 'assert' +import { SendBlock, ReceiveBlock, ChangeBlock } from '../dist/main.js' +import { NANO_TEST_VECTORS } from './TEST_VECTORS.js' + +// WARNING: Do not send any funds to the test vectors below +// Test vectors from https://docs.nano.org/integration-guides/key-management/ +describe('valid blocks', async () => { + it('should not allow negative balances', async () => { + assert.throws(() => { + const block = new SendBlock( + NANO_TEST_VECTORS.ADDRESS_0, + '7000000000000000000000000000000', + NANO_TEST_VECTORS.ADDRESS_1, + '12000000000000000000000000000000', + NANO_TEST_VECTORS.ADDRESS_2, + '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D' + ) + }, { message: 'Negative balance' }) + }) + + it('should allow zero balances', async () => { + const block = new SendBlock( + NANO_TEST_VECTORS.ADDRESS_0, + '9007199254740991', + NANO_TEST_VECTORS.ADDRESS_1, + '9007199254740991', + NANO_TEST_VECTORS.ADDRESS_2, + '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D' + ) + assert.notEqual(block.balance, 0) + assert.equal(block.balance, BigInt(0)) + }) + + it('should subtract balance from SendBlock correctly', async () => { + const block = new SendBlock( + NANO_TEST_VECTORS.ADDRESS_0, + '3000000000000000000000000000000', + NANO_TEST_VECTORS.ADDRESS_1, + '2000000000000000000000000000000', + NANO_TEST_VECTORS.ADDRESS_2, + '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D' + ) + assert.equal(block.balance, 1000000000000000000000000000000n) + }) + + it('should add balance from ReceiveBlock correctly', async () => { + const block = new ReceiveBlock( + NANO_TEST_VECTORS.ADDRESS_0, + '2000000000000000000000000000000', + NANO_TEST_VECTORS.ADDRESS_1, + '1000000000000000000000000000000', + NANO_TEST_VECTORS.ADDRESS_2, + '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D' + ) + assert.equal(block.balance, 3000000000000000000000000000000n) + }) +}) + +describe('block signing tests using official test vectors', async () => { + it('should create a valid signature for a receive block', async () => { + const work = 'c5cf86de24b24419' + const block = new ReceiveBlock( + 'nano_1e5aqegc1jb7qe964u4adzmcezyo6o146zb8hm6dft8tkp79za3sxwjym5rx', + '18618869000000000000000000000000', + 'CBC911F57B6827649423C92C88C0C56637A4274FF019E77E24D61D12B5338783', + '7000000000000000000000000000000', + 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou', + '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', + work + ) + await block.sign('781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3') + assert.equal(block.signature, 'F25D751AD0379A5718E08F3773DA6061A9E18842EF5615163C7F207B804CC2C5DD2720CFCE5FE6A78E4CC108DD9CAB65051526403FA2C24A1ED943BB4EA7880B') + assert.equal(block.work, work) + }) + + it('should create a valid signature for a receive block without work', async () => { + const block = new ReceiveBlock( + 'nano_1e5aqegc1jb7qe964u4adzmcezyo6o146zb8hm6dft8tkp79za3sxwjym5rx', + '18618869000000000000000000000000', + 'CBC911F57B6827649423C92C88C0C56637A4274FF019E77E24D61D12B5338783', + '7000000000000000000000000000000', + 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou', + '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', + ) + await block.sign('781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3') + assert.equal(block.signature, 'F25D751AD0379A5718E08F3773DA6061A9E18842EF5615163C7F207B804CC2C5DD2720CFCE5FE6A78E4CC108DD9CAB65051526403FA2C24A1ED943BB4EA7880B') + assert.equal(block.work, '') + }) + + it('should create a valid signature for a send block', async () => { + const work = 'fbffed7c73b61367' + const block = new SendBlock( + 'nano_1e5aqegc1jb7qe964u4adzmcezyo6o146zb8hm6dft8tkp79za3sxwjym5rx', + '5618869000000000000000000000000', + 'nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p', + '2000000000000000000000000000000', + 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou', + '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', + work, + ) + await block.sign('781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3') + assert.equal(block.signature.toUpperCase(), '79240D56231EF1885F354473733AF158DC6DA50E53836179565A20C0BE89D473ED3FF8CD11545FF0ED162A0B2C4626FD6BF84518568F8BB965A4884C7C32C205') + assert.equal(block.work, work) + }) + + it('should create a valid signature for a send block without work', async () => { + const block = new SendBlock( + 'nano_1e5aqegc1jb7qe964u4adzmcezyo6o146zb8hm6dft8tkp79za3sxwjym5rx', + '5618869000000000000000000000000', + 'nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p', + '2000000000000000000000000000000', + 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou', + '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', + ) + await block.sign('781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3') + assert.equal(block.signature.toUpperCase(), '79240D56231EF1885F354473733AF158DC6DA50E53836179565A20C0BE89D473ED3FF8CD11545FF0ED162A0B2C4626FD6BF84518568F8BB965A4884C7C32C205') + assert.equal(block.work, '') + }) + + it('should create a valid signature for a change rep block', async () => { + const work = '0000000000000000' + const block = new ChangeBlock( + 'nano_3igf8hd4sjshoibbbkeitmgkp1o6ug4xads43j6e4gqkj5xk5o83j8ja9php', + '3000000000000000000000000000000', + 'nano_1anrzcuwe64rwxzcco8dkhpyxpi8kd7zsjc1oeimpc3ppca4mrjtwnqposrs', + '128106287002E595F479ACD615C818117FCB3860EC112670557A2467386249D4', + work, + ) + await block.sign('781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3') // Did not find a private key at nano docs for this address + assert.equal(block.signature.toUpperCase(), 'A3C3C66D6519CBC0A198E56855942DEACC6EF741021A1B11279269ADC587DE1DA53CD478B8A47553231104CF24D742E1BB852B0546B87038C19BAE20F9082B0D') + assert.equal(block.work, work) + }) + + it('should create a valid signature for a change rep block without work', async () => { + const block = new ChangeBlock( + NANO_TEST_VECTORS.ADDRESS_0, + '0', + 'nano_34amtofxstsfyqcgphp8piij9u33widykq9wbz6ysjpxhbgmqu8btu1eexer', + 'F3C1D7B6EE97DA09D4C00538CEA93CBA5F74D78FD3FBE71347D2DFE7E53DF327' + ) + await block.sign(NANO_TEST_VECTORS.PRIVATE_0) + assert.equal(block.signature.toUpperCase(), '2BD2F905E74B5BEE3E2277CED1D1E3F7535E5286B6E22F7B08A814AA9E5C4E1FEA69B61D60B435ADC2CE756E6EE5F5BE7EC691FE87E024A0B22A3D980CA5B305') + assert.equal(block.work, '') + }) +}) diff --git a/test/test.mjs b/test/test.mjs deleted file mode 100644 index 4ec829d..0000000 --- a/test/test.mjs +++ /dev/null @@ -1,415 +0,0 @@ -'use strict' - -const { expect } = await import('chai') -const { wallet, block, tools, box } = await import('../dist/index.js') - -// WARNING: Do not send any funds to the test vectors below -describe('generate wallet test', function () { - this.slow(0) - - it('should generate wallet with random entropy', () => { - const result = wallet.generate() - expect(result).to.have.own.property('mnemonic') - expect(result).to.have.own.property('seed') - expect(result).to.have.own.property('accounts') - }) - - it('should generate the correct wallet with the given test vector', () => { - const result = wallet.generate('6caf5a42bb8074314aae20295975ece663be7aad945a73613d193b0cc41c7970') - expect(result).to.have.own.property('mnemonic') - expect(result).to.have.own.property('seed') - expect(result).to.have.own.property('accounts') - expect(result.mnemonic).to.equal('hole kiss mouse jacket also board click series citizen slight kite smoke desk diary rent mercy inflict antique edge invite slush athlete total brain') - expect(result.seed).to.equal('1accdd4c25e06e47310d0c62c290ec166071d024352e003e5366e8ba6ba523f2a0cb34116ac55a238a886778880a9b2a547112fd7cffade81d8d8d084ccb7d36') - expect(result.accounts[0].privateKey).to.equal('eb18b748bcc48f824cf8a1fe92f7fc93bfc6f2a1eb9c1d40fa26d335d8a0c30f') - expect(result.accounts[0].publicKey).to.equal('a9ef7bbc004813cf75c5fc5c582066182d5c9cffd42eb7eb81cefea8e78c47c5') - expect(result.accounts[0].address).to.equal('nano_3chhhgy11k1msxtwdz4wd1i8e83fdkghzo3gpzor5mqyo5mrrjy79zpw1g34') - }) - - it('should generate the correct wallet with the given test vector and a seed password', () => { - // Using the same entropy as before, but a different password - const result = wallet.generate('6caf5a42bb8074314aae20295975ece663be7aad945a73613d193b0cc41c7970', 'some password') - expect(result).to.have.own.property('mnemonic') - expect(result).to.have.own.property('seed') - expect(result).to.have.own.property('accounts') - - // Should result in the same mnemonic, but different seed and account - expect(result.mnemonic).to.equal('hole kiss mouse jacket also board click series citizen slight kite smoke desk diary rent mercy inflict antique edge invite slush athlete total brain') - expect(result.seed).to.not.equal('1accdd4c25e06e47310d0c62c290ec166071d024352e003e5366e8ba6ba523f2a0cb34116ac55a238a886778880a9b2a547112fd7cffade81d8d8d084ccb7d36') - expect(result.accounts[0].privateKey).to.not.equal('eb18b748bcc48f824cf8a1fe92f7fc93bfc6f2a1eb9c1d40fa26d335d8a0c30f') - expect(result.accounts[0].publicKey).to.not.equal('a9ef7bbc004813cf75c5fc5c582066182d5c9cffd42eb7eb81cefea8e78c47c5') - expect(result.accounts[0].address).to.not.equal('nano_3chhhgy11k1msxtwdz4wd1i8e83fdkghzo3gpzor5mqyo5mrrjy79zpw1g34') - - expect(result.seed).to.equal('146e3e2a0530848c9174d45ecec8c3f74a7be3f1ee832f92eb6227284121eb2e48a6b8fc469403984cd5e8f0d1ed05777c78f458d0e98c911841590e5d645dc3') - expect(result.accounts[0].privateKey).to.equal('2d5851bd5a89b8c943078be6ad5bbee8aeab77d6a4744c20d1b87d78e3286b93') - expect(result.accounts[0].publicKey).to.equal('923b6c7e281c1c5529fd2dc848117781216a1753cfd487fc34009f3591e636d7') - expect(result.accounts[0].address).to.equal('nano_36jufjz4i91wcnnztdgab1aqh1b3fado9mynizy5a16z8payefpqo81zsshc') - }) - - it('should throw when given an entropy with an invalid length', () => { - expect(() => wallet.generate('6caf5a42bb8074314aae20295975ece663be7aad945a73613d193b0cc41c797')).to.throw(Error) - expect(() => wallet.generate('6caf5a42bb8074314aae20295975ece663be7aad945a73613d193b0cc41c79701')).to.throw(Error) - }) - - it('should throw when given an entropy containing non-hex characters', () => { - expect(() => wallet.generate('6gaf5a42bb8074314aae20295975ece663be7aad945a73613d193b0cc41c7970')).to.throw(Error) - }) - -}) - -// Test vectors from https://docs.nano.org/integration-guides/key-management/ and elsewhere -describe('import wallet with test vectors test', function () { - this.slow(0) - - it('should successfully import a wallet with the official Nano test vectors mnemonic', () => { - const result = wallet.fromMnemonic( - 'edge defense waste choose enrich upon flee junk siren film clown finish luggage leader kid quick brick print evidence swap drill paddle truly occur', - 'some password') - expect(result).to.have.own.property('mnemonic') - expect(result).to.have.own.property('seed') - expect(result).to.have.own.property('accounts') - expect(result.mnemonic).to.equal('edge defense waste choose enrich upon flee junk siren film clown finish luggage leader kid quick brick print evidence swap drill paddle truly occur') - expect(result.seed).to.equal('0dc285fde768f7ff29b66ce7252d56ed92fe003b605907f7a4f683c3dc8586d34a914d3c71fc099bb38ee4a59e5b081a3497b7a323e90cc68f67b5837690310c') - expect(result.accounts[0].privateKey).to.equal('3be4fc2ef3f3b7374e6fc4fb6e7bb153f8a2998b3b3dab50853eabe128024143') - expect(result.accounts[0].publicKey).to.equal('5b65b0e8173ee0802c2c3e6c9080d1a16b06de1176c938a924f58670904e82c4') - expect(result.accounts[0].address).to.equal('nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d') - }) - - it('should successfully import a wallet with the checksum starting with a zero', () => { - wallet.fromMnemonic('food define cancel major spoon trash cigar basic aim bless wolf win ability seek paddle bench seed century group they mercy address monkey cake') - }) - - it('should successfully import a wallet with the official Nano test vectors seed', () => { - const result = wallet.fromSeed('0dc285fde768f7ff29b66ce7252d56ed92fe003b605907f7a4f683c3dc8586d34a914d3c71fc099bb38ee4a59e5b081a3497b7a323e90cc68f67b5837690310c') - expect(result).to.have.own.property('mnemonic') - expect(result).to.have.own.property('seed') - expect(result).to.have.own.property('accounts') - expect(result.mnemonic).to.be.undefined - expect(result.seed).to.equal('0dc285fde768f7ff29b66ce7252d56ed92fe003b605907f7a4f683c3dc8586d34a914d3c71fc099bb38ee4a59e5b081a3497b7a323e90cc68f67b5837690310c') - expect(result.accounts[0].privateKey).to.equal('3be4fc2ef3f3b7374e6fc4fb6e7bb153f8a2998b3b3dab50853eabe128024143') - expect(result.accounts[0].publicKey).to.equal('5b65b0e8173ee0802c2c3e6c9080d1a16b06de1176c938a924f58670904e82c4') - expect(result.accounts[0].address).to.equal('nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d') - }) - - it('should successfully import a legacy hex wallet with the a seed', () => { - const result = wallet.fromLegacySeed('0000000000000000000000000000000000000000000000000000000000000000') - expect(result).to.have.own.property('mnemonic') - expect(result).to.have.own.property('seed') - expect(result).to.have.own.property('accounts') - expect(result.mnemonic).to.equal('abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art') - expect(result.seed).to.equal('0000000000000000000000000000000000000000000000000000000000000000') - expect(result.accounts[0].privateKey).to.equal('9f0e444c69f77a49bd0be89db92c38fe713e0963165cca12faf5712d7657120f') - expect(result.accounts[0].publicKey).to.equal('c008b814a7d269a1fa3c6528b19201a24d797912db9996ff02a1ff356e45552b') - expect(result.accounts[0].address).to.equal('nano_3i1aq1cchnmbn9x5rsbap8b15akfh7wj7pwskuzi7ahz8oq6cobd99d4r3b7') - }) - - it('should successfully import legacy hex accounts with the a seed', () => { - const accounts = wallet.legacyAccounts('0000000000000000000000000000000000000000000000000000000000000000', 0, 3) - expect(accounts[0]).to.have.own.property('accountIndex') - expect(accounts[0]).to.have.own.property('privateKey') - expect(accounts[0]).to.have.own.property('publicKey') - expect(accounts[0]).to.have.own.property('address') - expect(accounts).to.have.lengthOf(4) - expect(accounts[2].accountIndex).to.equal(2) - expect(accounts[2].privateKey).to.equal('6a1804198020b080996ba45b5891f8227d7a4f41c8479824423780d234939d58') - expect(accounts[2].publicKey).to.equal('2fea520fe54f5d0dca79d553d9c7f5af7db6ac17586dbca6905794caadc639df') - expect(accounts[2].address).to.equal('nano_1dzcca9ycmtx3q79mocmu95zdduxptp3gp5fqkmb1ownscpweggzah8cb4rb') - }) - - it('should throw when given a seed with an invalid length', () => { - expect(() => wallet.generate('0dc285fde768f7ff29b66ce7252d56ed92fe003b605907f7a4f683c3dc8586d34a914d3c71fc099bb38ee4a59e5b081a3497b7a323e90cc68f67b5837690310')).to.throw(Error) - expect(() => wallet.generate('0dc285fde768f7ff29b66ce7252d56ed92fe003b605907f7a4f683c3dc8586d34a914d3c71fc099bb38ee4a59e5b081a3497b7a323e90cc68f67b5837690310cd')).to.throw(Error) - }) - - it('should throw when given a seed containing non-hex characters', () => { - expect(() => wallet.generate('0gc285fde768f7ff29b66ce7252d56ed92fe003b605907f7a4f683c3dc8586d34a914d3c71fc099bb38ee4a59e5b081a3497b7a323e90cc68f67b5837690310c')).to.throw(Error) - }) - - it('should successfully create a new legacy wallet and get the same result from importing one from the mnemonic', () => { - const result = wallet.generateLegacy('BE3E51EE51BAB11950B2495013512FEB110D9898B4137DA268709621CE2862F4') - expect(result).to.have.own.property('mnemonic') - expect(result).to.have.own.property('seed') - expect(result).to.have.own.property('accounts') - expect(result.mnemonic).to.equal('sail verb knee pet prison million drift empty exotic once episode stomach awkward slush glare list laundry battle bring clump brother before mesh pair') - - const imported = wallet.fromLegacyMnemonic(result.mnemonic) - expect(imported.mnemonic).to.equal(result.mnemonic) - expect(imported.seed.toUpperCase()).to.equal(result.seed) - expect(imported.accounts[0].privateKey).to.equal(result.accounts[0].privateKey) - }) - -}) - -describe('derive more accounts from the same seed test', function () { - this.slow(0) - - it('should derive accounts from the given seed', () => { - const result = wallet.accounts( - '0dc285fde768f7ff29b66ce7252d56ed92fe003b605907f7a4f683c3dc8586d34a914d3c71fc099bb38ee4a59e5b081a3497b7a323e90cc68f67b5837690310c', - 0, 14) - expect(result.length).to.equal(15) - expect(result[0].privateKey).to.equal('3be4fc2ef3f3b7374e6fc4fb6e7bb153f8a2998b3b3dab50853eabe128024143') - expect(result[0].publicKey).to.equal('5b65b0e8173ee0802c2c3e6c9080d1a16b06de1176c938a924f58670904e82c4') - expect(result[0].address).to.equal('nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d') - expect(result[14].privateKey).to.equal('5f12e37c64daf2501c6a6a20614fd8d977fed65b5b5f0b045ec997f2ed2f53ca') - expect(result[14].publicKey).to.equal('f93a61018e07825a095e8cf7bdce9242e9c12c5c41a55de597a2be93fa41306b') - expect(result[14].address).to.equal('nano_3ybte61rw3w4da6ox59qqq9b6iqbr6p7rif7dqkshaoykhx64e5dbp4o1ua1') - - const result2 = wallet.accounts( - '0dc285fde768f7ff29b66ce7252d56ed92fe003b605907f7a4f683c3dc8586d34a914d3c71fc099bb38ee4a59e5b081a3497b7a323e90cc68f67b5837690310c', - 0x80000000, 0x800000ff) - expect(result2.length).to.equal(0x100) - }) - -}) - -describe('derive more accounts from the same seed test', function () { - this.slow(0) - - it('should derive accounts from the given seed', () => { - const result = wallet.legacyAccounts( - 'BE3E51EE51BAB11950B2495013512FEB110D9898B4137DA268709621CE2862F4', - 0x0, 0xff) - expect(result.length).to.equal(0x100) - expect(result[0].privateKey).to.exist - expect(result[0].publicKey).to.exist - expect(result[0].address).to.exist - - const result2 = wallet.legacyAccounts( - 'BE3E51EE51BAB11950B2495013512FEB110D9898B4137DA268709621CE2862F4', - 0x80000000, 0x800000ff) - expect(result2.length).to.equal(0x100) - }) - -}) - -// Test vectors from https://docs.nano.org/integration-guides/key-management/ -describe('block signing tests using official test vectors', function () { - this.slow(0) - - it('should create a valid signature for a receive block', () => { - const work = 'c5cf86de24b24419' - const result = block.receive({ - walletBalanceRaw: '18618869000000000000000000000000', - transactionHash: 'CBC911F57B6827649423C92C88C0C56637A4274FF019E77E24D61D12B5338783', - toAddress: 'nano_1e5aqegc1jb7qe964u4adzmcezyo6o146zb8hm6dft8tkp79za3sxwjym5rx', - representativeAddress: 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou', - frontier: '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', - amountRaw: '7000000000000000000000000000000', - work, - }, '781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3') - expect(result.signature.toUpperCase()).to.equal('F25D751AD0379A5718E08F3773DA6061A9E18842EF5615163C7F207B804CC2C5DD2720CFCE5FE6A78E4CC108DD9CAB65051526403FA2C24A1ED943BB4EA7880B') - expect(result.work).to.equal(work) - }) - - it('should create a valid signature for a receive block without work', () => { - const result = block.receive({ - walletBalanceRaw: '18618869000000000000000000000000', - transactionHash: 'CBC911F57B6827649423C92C88C0C56637A4274FF019E77E24D61D12B5338783', - toAddress: 'nano_1e5aqegc1jb7qe964u4adzmcezyo6o146zb8hm6dft8tkp79za3sxwjym5rx', - representativeAddress: 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou', - frontier: '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', - amountRaw: '7000000000000000000000000000000', - }, '781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3') - expect(result.signature.toUpperCase()).to.equal('F25D751AD0379A5718E08F3773DA6061A9E18842EF5615163C7F207B804CC2C5DD2720CFCE5FE6A78E4CC108DD9CAB65051526403FA2C24A1ED943BB4EA7880B') - expect(result.work).to.equal('') - }) - - it('should create a valid signature for a send block', () => { - const work = 'fbffed7c73b61367' - const result = block.send({ - walletBalanceRaw: '5618869000000000000000000000000', - fromAddress: 'nano_1e5aqegc1jb7qe964u4adzmcezyo6o146zb8hm6dft8tkp79za3sxwjym5rx', - toAddress: 'nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p', - representativeAddress: 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou', - frontier: '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', - amountRaw: '2000000000000000000000000000000', - work, - }, '781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3') - expect(result.signature.toUpperCase()).to.equal('79240D56231EF1885F354473733AF158DC6DA50E53836179565A20C0BE89D473ED3FF8CD11545FF0ED162A0B2C4626FD6BF84518568F8BB965A4884C7C32C205') - expect(result.work).to.equal(work) - }) - - it('should create a valid signature for a send block without work', () => { - const result = block.send({ - walletBalanceRaw: '5618869000000000000000000000000', - fromAddress: 'nano_1e5aqegc1jb7qe964u4adzmcezyo6o146zb8hm6dft8tkp79za3sxwjym5rx', - toAddress: 'nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p', - representativeAddress: 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou', - frontier: '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', - amountRaw: '2000000000000000000000000000000', - }, '781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3') - expect(result.signature.toUpperCase()).to.equal('79240D56231EF1885F354473733AF158DC6DA50E53836179565A20C0BE89D473ED3FF8CD11545FF0ED162A0B2C4626FD6BF84518568F8BB965A4884C7C32C205') - expect(result.work).to.equal('') - }) - - it('should create a valid signature for a change rep block', () => { - const work = '0000000000000000' - const result = block.representative({ - walletBalanceRaw: '3000000000000000000000000000000', - address: 'nano_3igf8hd4sjshoibbbkeitmgkp1o6ug4xads43j6e4gqkj5xk5o83j8ja9php', - representativeAddress: 'nano_1anrzcuwe64rwxzcco8dkhpyxpi8kd7zsjc1oeimpc3ppca4mrjtwnqposrs', - frontier: '128106287002E595F479ACD615C818117FCB3860EC112670557A2467386249D4', - work, - }, '781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3') // Did not find a private key at nano docs for this address - expect(result.signature.toUpperCase()).to.equal('A3C3C66D6519CBC0A198E56855942DEACC6EF741021A1B11279269ADC587DE1DA53CD478B8A47553231104CF24D742E1BB852B0546B87038C19BAE20F9082B0D') - expect(result.work).to.equal(work) - }) - - it('should create a valid signature for a change rep block without work', () => { - const result = block.representative({ - walletBalanceRaw: '3000000000000000000000000000000', - address: 'nano_3igf8hd4sjshoibbbkeitmgkp1o6ug4xads43j6e4gqkj5xk5o83j8ja9php', - representativeAddress: 'nano_1anrzcuwe64rwxzcco8dkhpyxpi8kd7zsjc1oeimpc3ppca4mrjtwnqposrs', - frontier: '128106287002E595F479ACD615C818117FCB3860EC112670557A2467386249D4', - }, '781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3') // Did not find a private key at nano docs for this address - expect(result.signature.toUpperCase()).to.equal('A3C3C66D6519CBC0A198E56855942DEACC6EF741021A1B11279269ADC587DE1DA53CD478B8A47553231104CF24D742E1BB852B0546B87038C19BAE20F9082B0D') - expect(result.work).to.equal('') - }) - -}) - -describe('unit conversion tests', function () { - this.slow(0) - - it('should convert nano to raw', () => { - const result = tools.convert('1', 'NANO', 'RAW') - expect(result).to.equal('1000000000000000000000000000000') - }) - - it('should convert raw to nano', () => { - const result = tools.convert('1000000000000000000000000000000', 'RAW', 'NANO') - expect(result).to.equal('1.000000000000000000000000000000') - }) - -}) - -describe('Signer tests', function () { - this.slow(0) - - before(() => { - this.testWallet = wallet.generate() - }) - - // Private key: 3be4fc2ef3f3b7374e6fc4fb6e7bb153f8a2998b3b3dab50853eabe128024143 - // Public key: 5b65b0e8173ee0802c2c3e6c9080d1a16b06de1176c938a924f58670904e82c4 - - it('should sign data with a single parameter', () => { - const result = tools.sign('3be4fc2ef3f3b7374e6fc4fb6e7bb153f8a2998b3b3dab50853eabe128024143', 'miro@metsanheimo.fi') - expect(result).to.equal('fecb9b084065adc969904b55a0099c63746b68df41fecb713244d387eed83a80b9d4907278c5ebc0998a5fc8ba597fbaaabbfce0abd2ca2212acfe788637040c') - }) - - it('should sign data with multiple parameters', () => { - const result = tools.sign('3be4fc2ef3f3b7374e6fc4fb6e7bb153f8a2998b3b3dab50853eabe128024143', 'miro@metsanheimo.fi', 'somePassword') - expect(result).to.equal('bb534f9b469af451b1941ffef8ee461fc5d284b5d393140900c6e13a65ef08d0ae2bc77131ee182922f66c250c7237a83878160457d5c39a70e55f7fce925804') - }) - - it('should verify a signature using the public key', () => { - const result = tools.verify('5b65b0e8173ee0802c2c3e6c9080d1a16b06de1176c938a924f58670904e82c4', 'fecb9b084065adc969904b55a0099c63746b68df41fecb713244d387eed83a80b9d4907278c5ebc0998a5fc8ba597fbaaabbfce0abd2ca2212acfe788637040c', 'miro@metsanheimo.fi') - expect(result).to.be.true - - const result2 = tools.verify('5b65b0e8173ee0802c2c3e6c9080d1a16b06de1176c938a924f58670904e82c4', 'fecb9b084065adc969904b55a0099c63746b68df41fecb713244d387eed83a80b9d4907278c5ebc0998a5fc8ba597fbaaabbfce0abd2ca2212acfe788637040c', 'mir@metsanheimo.fi') - expect(result2).to.be.false - - const result3 = tools.verify('5b65b0e8173ee0802c2c3e6c9080d1a16b06de1176c938a924f58670904e82c4', 'aecb9b084065adc969904b55a0099c63746b68df41fecb713244d387eed83a80b9d4907278c5ebc0998a5fc8ba597fbaaabbfce0abd2ca2212acfe788637040c', 'miro@metsanheimo.fi') - expect(result3).to.be.false - }) - - it('should verify a block using the public key', () => { - const sendBlock = block.send({ - walletBalanceRaw: '5618869000000000000000000000000', - fromAddress: this.testWallet.accounts[0].address, - toAddress: 'nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p', - representativeAddress: 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou', - frontier: '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', - amountRaw: '2000000000000000000000000000000', - }, this.testWallet.accounts[0].privateKey) - - const publicKey = tools.addressToPublicKey(this.testWallet.accounts[0].address) - - const valid = tools.verifyBlock(publicKey, sendBlock) - expect(valid).to.be.true - - sendBlock.account = 'nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p' - const valid2 = tools.verifyBlock(this.testWallet.accounts[0].publicKey, sendBlock) - expect(valid2).to.be.false - }) - - it('should convert a Nano address to public key', () => { - const publicKey = tools.addressToPublicKey('nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d') - expect(publicKey).to.equal('5b65b0e8173ee0802c2c3e6c9080d1a16b06de1176c938a924f58670904e82c4') - }) - - it('should convert a public key to a Nano address', () => { - const address = tools.publicKeyToAddress('5b65b0e8173ee0802c2c3e6c9080d1a16b06de1176c938a924f58670904e82c4') - expect(address).to.equal('nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d') - }) - - it('should create a blake2b hash', () => { - let hash = tools.blake2b('asd') - expect(hash).to.equal('f787fbcdd2b4c6f6447921d6f163e8fddfb83d08432430cacaaab1bbedd723fe') - - hash = tools.blake2b(['asd']) - expect(hash).to.equal('f787fbcdd2b4c6f6447921d6f163e8fddfb83d08432430cacaaab1bbedd723fe') - }) - -}) - -describe('Box tests', function () { - this.slow(0) - - before(() => { - this.message = 'The quick brown fox jumps over the lazy dog 🔥' - this.bob = wallet.generate() - this.alice = wallet.generateLegacy() - }) - - it('should encrypt and decrypt a message from bob to alice', () => { - const encrypted = box.encrypt(this.message, this.alice.accounts[0].address, this.bob.accounts[0].privateKey) - const encrypted2 = box.encrypt(this.message, this.alice.accounts[0].address, this.bob.accounts[0].privateKey) - const encrypted3 = box.encrypt(this.message + 'asd', this.alice.accounts[0].address, this.bob.accounts[0].privateKey) - - // Just to be safe - expect(this.message).to.not.equal(encrypted) - expect(encrypted).to.not.equal(encrypted2) - expect(encrypted).to.not.equal(encrypted3) - - const decrypted = box.decrypt(encrypted, this.bob.accounts[0].address, this.alice.accounts[0].privateKey) - expect(this.message).to.equal(decrypted) - }) - - it('should encrypt and decrypt a message from alice to bob', () => { - const encrypted = box.encrypt(this.message, this.bob.accounts[0].address, this.alice.accounts[0].privateKey) - const decrypted = box.decrypt(encrypted, this.alice.accounts[0].address, this.bob.accounts[0].privateKey) - expect(this.message).to.equal(decrypted) - }) - - it('should fail to decrypt with wrong public key in encryption', () => { - // Encrypt with wrong public key - const aliceAccounts = wallet.legacyAccounts(this.alice.seed, 1, 2) - const encrypted = box.encrypt(this.message, aliceAccounts[0].address, this.bob.accounts[0].privateKey) - expect(() => box.decrypt(encrypted, this.bob.accounts[0].address, this.alice.accounts[0].privateKey)).to.throw() - }) - - it('should fail to decrypt with wrong public key in decryption', () => { - // Decrypt with wrong public key - const bobAccounts = wallet.accounts(this.bob.seed, 1, 2) - const encrypted = box.encrypt(this.message, this.alice.accounts[0].address, this.bob.accounts[0].privateKey) - expect(() => box.decrypt(encrypted, bobAccounts[0].address, this.alice.accounts[0].privateKey)).to.throw() - }) - - it('should fail to decrypt with wrong private key in encryption', () => { - // Encrypt with wrong public key - const bobAccounts = wallet.accounts(this.bob.seed, 1, 2) - const encrypted = box.encrypt(this.message, this.alice.accounts[0].address, bobAccounts[0].privateKey) - expect(() => box.decrypt(encrypted, this.bob.accounts[0].address, this.alice.accounts[0].privateKey)).to.throw() - }) - - it('should fail to decrypt with wrong private key in decryption', () => { - // Encrypt with wrong public key - const aliceAccounts = wallet.legacyAccounts(this.alice.seed, 1, 2) - const encrypted = box.encrypt(this.message, this.alice.accounts[0].address, this.bob.accounts[0].privateKey) - expect(() => box.decrypt(encrypted, this.bob.accounts[0].address, aliceAccounts[0].privateKey)).to.throw() - }) - -}) diff --git a/test/tools.test.mjs b/test/tools.test.mjs new file mode 100644 index 0000000..d2c1b6f --- /dev/null +++ b/test/tools.test.mjs @@ -0,0 +1,148 @@ +// SPDX-FileCopyrightText: 2024 Chris Duncan +// SPDX-License-Identifier: GPL-3.0-or-later + +'use strict' + +import { describe, it } from 'node:test' +import { strict as assert } from 'assert' +import { Bip44Wallet, Account, SendBlock, Node, Tools } from '../dist/main.js' +import { RAW_MAX, NANO_TEST_VECTORS } from './TEST_VECTORS.js' + +const wallet = await Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED) +await wallet.unlock(NANO_TEST_VECTORS.PASSWORD) +const rpc = new Node(process.env.NODE_URL, process.env.API_KEY_NAME, process.env.API_KEY_VALUE) + +describe('unit conversion tests', async () => { + it('should convert nano to raw', async () => { + const result = await Tools.convert('1', 'NANO', 'RAW') + assert.equal(result, '1000000000000000000000000000000') + }) + + it('should convert raw to nano', async () => { + const result = await Tools.convert('1000000000000000000000000000000', 'RAW', 'NANO') + assert.equal(result, '1') + }) + + it('should ignore leading and trailing zeros', async () => { + const result = await Tools.convert('0011002200.0033004400', 'nano', 'nano') + assert.equal(result, '11002200.00330044') + }) + + it('should convert raw to nyano', async () => { + const result = await Tools.convert(RAW_MAX, 'RAW', 'NYANO') + assert.equal(result, '340282366920938.463463374607431768211455') + }) + + it('should convert case-insensitive nyano to raw', async () => { + const result = await Tools.convert('0.000000000000000123456789', 'nYaNo', 'rAw') + assert.equal(result, '123456789') + }) + + it('should convert nano to pico', async () => { + const result = await Tools.convert('123.456', 'nano', 'pico') + assert.equal(result, '123456') + }) + + it('should convert knano to pico', async () => { + const result = await Tools.convert('123.456', 'nano', 'pico') + assert.equal(result, '123456') + }) + + it('should throw if amount exceeds raw max', async () => { + await assert.rejects(Tools.convert(RAW_MAX, 'NANO', 'RAW'), + { message: 'Amount exceeds Nano limits' }) + }) + + it('should throw if amount exceeds raw min', async () => { + await assert.rejects(Tools.convert('0.1', 'RAW', 'NANO'), + { message: 'Amount must be at least 1 raw' }) + }) + + it('should throw if amount is blank', async () => { + await assert.rejects(Tools.convert('', 'RAW', 'NANO'), + { message: 'Invalid amount' }) + }) + + it('should throw if amount has non-digit characters', async () => { + await assert.rejects(Tools.convert('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 'RAW', 'NANO'), + { message: 'Invalid amount' }) + }) +}) + +describe('signature tests', async () => { + it('should sign data with a single parameter', async () => { + const result = await Tools.sign(NANO_TEST_VECTORS.PRIVATE_0, 'miro@metsanheimo.fi') + assert.equal(result, 'FECB9B084065ADC969904B55A0099C63746B68DF41FECB713244D387EED83A80B9D4907278C5EBC0998A5FC8BA597FBAAABBFCE0ABD2CA2212ACFE788637040C') + }) + + it('should sign data with multiple parameters', async () => { + const result = await Tools.sign(NANO_TEST_VECTORS.PRIVATE_0, 'miro@metsanheimo.fi', 'somePassword') + assert.equal(result, 'BB534F9B469AF451B1941FFEF8EE461FC5D284B5D393140900C6E13A65EF08D0AE2BC77131EE182922F66C250C7237A83878160457D5C39A70E55F7FCE925804') + }) + + it('should verify a signature using the public key', async () => { + const result = await Tools.verify(NANO_TEST_VECTORS.PUBLIC_0, 'FECB9B084065ADC969904B55A0099C63746B68DF41FECB713244D387EED83A80B9D4907278C5EBC0998A5FC8BA597FBAAABBFCE0ABD2CA2212ACFE788637040C', 'miro@metsanheimo.fi') + assert.equal(result, true) + + const result2 = await Tools.verify(NANO_TEST_VECTORS.PUBLIC_0, 'FECB9B084065ADC969904B55A0099C63746B68DF41FECB713244D387EED83A80B9D4907278C5EBC0998A5FC8BA597FBAAABBFCE0ABD2CA2212ACFE788637040C', 'mir@metsanheimo.fi') + assert.equal(result2, false) + + const result3 = await Tools.verify(NANO_TEST_VECTORS.PUBLIC_0, 'AECB9B084065ADC969904B55A0099C63746B68DF41FECB713244D387EED83A80B9D4907278C5EBC0998A5FC8BA597FBAAABBFCE0ABD2CA2212ACFE788637040C', 'miro@metsanheimo.fi') + assert.equal(result3, false) + }) + + it('should verify a block using the public key', async () => { + const accounts = await wallet.accounts() + const sendBlock = new SendBlock( + accounts[0].address, + '5618869000000000000000000000000', + 'nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p', + '2000000000000000000000000000000', + 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou', + '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', + ) + await sendBlock.sign(accounts[0].privateKey) + const valid = await sendBlock.verify(accounts[0].publicKey) + assert.equal(valid, true) + }) + + it('should reject a block using the wrong public key', async () => { + const accounts = await wallet.accounts() + const sendBlock = new SendBlock( + accounts[0].address, + '5618869000000000000000000000000', + 'nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p', + '2000000000000000000000000000000', + 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou', + '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', + ) + sendBlock.sign(accounts[0].privateKey) + + sendBlock.account = new Account('nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p') + const valid = await sendBlock.verify(accounts[0].publicKey) + assert.equal(valid, false) + }) + + it('should create a BLAKE2b hash of a single string', async () => { + const hash = await Tools.hash('asd') + assert.equal(hash, 'F787FBCDD2B4C6F6447921D6F163E8FDDFB83D08432430CACAAAB1BBEDD723FE') + }) + + it('should create a BLAKE2b hash of a string array', async () => { + const hash = await Tools.hash(['asd']) + assert.equal(hash, 'F787FBCDD2B4C6F6447921D6F163E8FDDFB83D08432430CACAAAB1BBEDD723FE') + }) +}) + +describe('sweeper', async () => { + it('throws without required parameters', async () => { + await assert.rejects(Tools.sweep(), + { message: 'Missing required sweep arguments' }) + }) + + it('fails gracefully for ineligible accounts', async () => { + const results = await Tools.sweep(rpc, wallet, NANO_TEST_VECTORS.ADDRESS_1) + assert.ok(results) + assert.equal(results.length, 1) + }) +}) diff --git a/tsconfig.json b/tsconfig.json index 021ed89..b544969 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,19 +1,22 @@ { "compilerOptions": { - "target": "es5", - "module": "commonjs", + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", "declaration": true, + "noEmit": false, "outDir": "./dist", - "strict": true, + "alwaysStrict": true, + "downlevelIteration": false, "esModuleInterop": true, - "downlevelIteration": true, - "strictNullChecks": false, "forceConsistentCasingInFileNames": true, - "types": [ - "node" - ], - "lib": [ - "es2017" - ] - } + "noErrorTruncation": true, + "noFallthroughCasesInSwitch": true, + "strict": true + }, + "include": [ + "src/main.ts", + "src/*", + "src/**/*" + ] } diff --git a/tsconfig.json.license b/tsconfig.json.license new file mode 100644 index 0000000..36ee55c --- /dev/null +++ b/tsconfig.json.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2024 Chris Duncan +SPDX-License-Identifier: GPL-3.0-or-later diff --git a/webpack.config.js b/webpack.config.js deleted file mode 100644 index 4c5890d..0000000 --- a/webpack.config.js +++ /dev/null @@ -1,24 +0,0 @@ -const path = require('path') - -module.exports = { - entry: './index.ts', - mode: 'production', - module: { - rules: [ - { - test: /\.ts?$/, - use: 'ts-loader', - exclude: /node_modules/, - }, - ], - }, - resolve: { - extensions: ['.ts', '.js', '.json'], - }, - output: { - filename: 'index.min.js', - path: path.resolve(__dirname, 'dist'), - libraryTarget: 'var', - library: 'NanocurrencyWeb', - }, -} -- 2.34.1