diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..071940d --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +**/node_modules/** +**/dist \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..34ddb5f --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,9 @@ +{ + "parserOptions": { + "ecmaVersion": 8, + "sourceType": "module" + }, + "rules": { + "semi": "error" + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cf70988 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +**/node_modules diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..571b617 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,7 @@ +Copyright (c) 2020 DigiByte Foundation NZ Limited + +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. \ No newline at end of file diff --git a/README.md b/README.md index 6739d05..a387db0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,106 @@ -# digibyte-rosetta-nodeapi -Coinbase Rosetta Node API Implementation +

+ + Rosetta + +

+

+ Rosetta SDK +

+

+NodeJS Rosetta SDK to create and interact with Rosetta API implementations +

+

+ + + +

+ +# OpenAPI Generated JavaScript/Express Server + +## Overview +This server was generated using the [OpenAPI Generator](https://openapi-generator.tech) project. The code generator, and it's generated code allows you to develop your system with an API-First attitude, where the API contract is the anchor and definer of your project, and your code and business-logic aims to complete and comply to the terms in the API contract. + +### prerequisites +- NodeJS >= 10.6 +- NPM >= 6.10.0 + +The code was written on a mac, so assuming all should work smoothly on Linux-based computers. However, there is no reason not to run this library on Windows-based machines. If you find an OS-related problem, please open an issue and it will be resolved. + +### Running the server +#### This is a long read, but there's a lot to understand. Please take the time to go through this. +1. Use the OpenAPI Generator to generate your application: +Assuming you have Java (1.8+), and [have the jar](https://github.com/openapitools/openapi-generator#13---download-jar) to generate the application, run: +```java -jar {path_to_jar_file} generate -g nodejs-express-server -i {openapi yaml/json file} -o {target_directory_where_the_app_will_be_installed} ``` +If you do not have the jar, or do not want to run Java from your local machine, follow instructions on the [OpenAPITools page](https://github.com/openapitools/openapi-generator). You can run the script online, on docker, and various other ways. +2. Go to the generated directory you defined. There's a fully working NodeJS-ExpressJs server waiting for you. This is important - the code is yours to change and update! Look at config.js and see that the settings there are ok with you - the server will run on port 3000, and files will be uploaded to a new directory 'uploaded_files'. +3. The server will base itself on an openapi.yaml file which is located under /api/openapi.yaml. This is not exactly the same file that you used to generate the app: +I. If you have `application/json` contentBody that was defined inside the path object - the generate will have moved it to the components/schemas section of the openapi document. +II. Every process has a new element added to it - `x-eov-operation-handler: controllers/PetController` which directs the call to that file. +III. We have a Java application that translates the operationId to a method, and a nodeJS script that does the same process to call that method. Both are converting the method to `camelCase`, but might have discrepancy. Please pay attention to the operationID names, and see that they are represented in the `controllers` and `services` directories. +4. Take the time to understand the structure of the application. There might be bugs, and there might be settings and business-logic that does not meet your expectation. Instead of dumping this solution and looking for something else - see if you can make the generated code work for you. +To keep the explanation short (a more detailed explanation will follow): Application starts with a call to index.js (this is where you will plug in the db later). It calls expressServer.js which is where the express.js and openapi-validator kick in. This is an important file. Learn it. All calls to endpoints that were configured in the openapi.yaml document go to `controllers/{name_of_tag_which_the_operation_was_associated_with}.js`, which is a very small method. All the business-logic lies in `controllers/Controller.js`, and from there - to `services/{name_of_tag_which_the_operation_was_associated_with}.js`. + +5. Once you've understood what is *going* to happen, launch the app and ensure everything is working as expected: +``` +npm start +``` +### Tests +Unfortunately, I have not written any unit-tests. Those will come in the future. However, the package does come with all that is needed to write and run tests - mocha and sinon and the related libraries are included in the package.js and will be installed upon npm install command + +### View and test the API +(Assuming no changes were made to config.js) + +1. API documentation, and to check the available endpoints: +http://localhost:3000/api-docs/. To +2. Download the oepnapi.yaml document: http://localhost:3000/openapi. +3. Every call to an endpoint that was defined in the openapi document will return a 200 and a list of all the parameters and objects that were sent in the request. +4. Endpoints that require security need to have security handlers configured before they can return a successful response. At this point they will return [ a response code of 401](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401). +5. ##### At this stage the server does not support document body sent in xml format. + +### Node version and guidelines +The code was written using Node version 10.6, and complies to the [Airbnb .eslint guiding rules](https://github.com/airbnb/javascript). + +### Project Files +#### Root Directory: +In the root directory we have (besides package.json, config.js, and log files): +- **logger.js** - where we define the logger for the project. The project uses winston, but the purpose of this file is to enable users to change and modify their own logger behavior. +- **index.js** - This is the project's 'main' file, and from here we launch the application. This is a very short and concise file, and the idea behind launching from this short file is to allow use-cases of launching the server with different parameters (changing config and/or logger) without affecting the rest of the code. +- **expressServer.js** - The core of the Express.js server. This is where the express server is initialized, together with the OpenAPI validator, OpenAPI UI, and other libraries needed to start our server. If we want to add external links, that's where they would go. Our project uses the [express-openapi-validator](https://www.npmjs.com/package/express-openapi-validator) library that acts as a first step in the routing process - requests that are directed to paths defined in the `openapi.yaml` file are caught by this process, and it's parameters and bodyContent are validated against the schema. A successful result of this validation will be a new 'openapi' object added to the request. If the path requested is not part of the openapi.yaml file, the validator ignores the request and passes it on, as is, down the flow of the Express server. + +#### api/ +- **openapi.yaml** - This is the OpenAPI contract to which this server will comply. The file was generated using the codegen, and should contain everything needed to run the API Gateway - no references to external models/schemas. + +#### utils/ +Currently a single file: + +- **openapiRouter.js** - This is where the routing to our back-end code happens. If the request object includes an ```openapi``` object, it picks up the following values (that are part of the ```openapi.yaml``` file): 'x-openapi-router-controller', and 'x-openapi-router-service'. These variables are names of files/classes in the controllers and services directories respectively. The operationId of the request is also extracted. The operationId is a method in the controller and the service that was generated as part of the codegen process. The routing process sends the request and response objects to the controller, which will extract the expected variables from the request, and send it to be processed by the service, returning the response from the service to the caller. + +#### controllers/ +After validating the request, and ensuring this belongs to our API gateway, we send the request to a `controller`, where the variables and parameters are extracted from the request and sent to the relevant `service` for processing. The `controller` handles the response from the `service` and builds the appropriate HTTP response to be sent back to the user. + +- **index.js** - load all the controllers that were generated for this project, and export them to be used dynamically by the `openapiRouter.js`. If you would like to customize your controller, it is advised that you link to your controller here, and ensure that the codegen does not rewrite this file. + +- **Controller.js** - The core processor of the generated controllers. The generated controllers are designed to be as slim and generic as possible, referencing to the `Controller.js` for the business logic of parsing the needed variables and arguments from the request, and for building the HTTP response which will be sent back. The `Controller.js` is a class with static methods. + +- **.js** - auto-generated code, processing all the operations. The Controller is a class that is constructed with the service class it will be sending the request to. Every request defined by the `openapi.yaml` has an operationId. The operationId is the name of the method that will be called. Every method receives the request and response, and calls the `Controller.js` to process the request and response, adding the service method that should be called for the actual business-logic processing. + +#### services/ +This is where the API Gateway ends, and the unique business-logic of your application kicks in. Every endpoint in the `openapi.yaml` has a variable 'x-openapi-router-service', which is the name of the service class that is generated. The operationID of the endpoint is the name of the method that will be called. The generated code provides a simple promise with a try/catch clause. A successful operation ends with a call to the generic `Service.js` to build a successful response (payload and response code), and a failure will call the generic `Service.js` to build a response with an error object and the relevant response code. It is recommended to have the services be generated automatically once, and after the initial build add methods manually. + +- **index.js** - load all the services that were generated for this project, and export them to be used dynamically by the `openapiRouter.js`. If you would like to customize your service, it is advised that you link to your controller here, and ensure that the codegen does not rewrite this file. + +- **Service.js** - A utility class, very simple and thin at this point, with two static methods for building a response object for successful and failed results in the service operation. The default response code is 200 for success and 500 for failure. It is recommended to send more accurate response codes and override these defaults when relevant. + +- **.js** - auto-generated code, providing a stub Promise for each operationId defined in the `openapi.yaml`. Each method receives the variables that were defined in the `openapi.yaml` file, and wraps a Promise in a try/catch clause. The Promise resolves both success and failure in a call to the `Service.js` utility class for building the appropriate response that will be sent back to the Controller and then to the caller of this endpoint. + +#### tests/ +- **serverTests.js** - basic server validation tests, checking that the server is up, that a call to an endpoint within the scope of the `openapi.yaml` file returns 200, that a call to a path outside that scope returns 200 if it exists and a 404 if not. +- **routingTests.js** - Runs through all the endpoints defined in the `openapi.yaml`, and constructs a dummy request to send to the server. Confirms that the response code is 200. At this point requests containing xml or formData fail - currently they are not supported in the router. +- **additionalEndpointsTests.js** - A test file for all the endpoints that are defined outside the openapi.yaml scope. Confirms that these endpoints return a successful 200 response. + + +Future tests should be written to ensure that the response of every request sent should conform to the structure defined in the `openapi.yaml`. This test will fail 100% initially, and the job of the development team will be to clear these tests. + + +#### models/ +Currently a concept awaiting feedback. The idea is to have the objects defined in the openapi.yaml act as models which are passed between the different modules. This will conform the programmers to interact using defined objects, rather than loosley-defined JSON objects. Given the nature of JavaScript progrmmers, who want to work with their own bootstrapped parameters, this concept might not work. Keeping this here for future discussion and feedback. diff --git a/api/openapi.yaml b/api/openapi.yaml new file mode 100644 index 0000000..6f529dc --- /dev/null +++ b/api/openapi.yaml @@ -0,0 +1,1725 @@ +openapi: 3.0.2 +info: + description: A Standard for Blockchain Interaction + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + title: Rosetta + version: 1.3.1 +servers: +- url: / +paths: + /network/list: + post: + description: This endpoint returns a list of NetworkIdentifiers that the Rosetta + server can handle. + operationId: networkList + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MetadataRequest' + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/NetworkListResponse' + description: Expected response to a valid request + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: unexpected error + summary: Get List of Available Networks + tags: + - Network + x-eov-operation-handler: controllers/NetworkController + /network/status: + post: + description: This endpoint returns the current status of the network requested. + Any NetworkIdentifier returned by /network/list should be accessible here. + operationId: networkStatus + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/NetworkRequest' + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/NetworkStatusResponse' + description: Expected response to a valid request + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: unexpected error + summary: Get Network Status + tags: + - Network + x-eov-operation-handler: controllers/NetworkController + /network/options: + post: + description: This endpoint returns the version information and allowed network-specific + types for a NetworkIdentifier. Any NetworkIdentifier returned by /network/list + should be accessible here. Because options are retrievable in the context + of a NetworkIdentifier, it is possible to define unique options for each network. + operationId: networkOptions + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/NetworkRequest' + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/NetworkOptionsResponse' + description: Expected response to a valid request + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: unexpected error + summary: Get Network Options + tags: + - Network + x-eov-operation-handler: controllers/NetworkController + /block: + post: + description: Get a block by its Block Identifier. If transactions are returned + in the same call to the node as fetching the block, the response should include + these transactions in the Block object. If not, an array of Transaction Identifiers + should be returned so /block/transaction fetches can be done to get all transaction + information. + operationId: block + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/BlockRequest' + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/BlockResponse' + description: Expected response to a valid request + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: unexpected error + summary: Get a Block + tags: + - Block + x-eov-operation-handler: controllers/BlockController + /block/transaction: + post: + description: 'Get a transaction in a block by its Transaction Identifier. This + endpoint should only be used when querying a node for a block does not return + all transactions contained within it. All transactions returned by this endpoint + must be appended to any transactions returned by the /block method by consumers + of this data. Fetching a transaction by hash is considered an Explorer Method + (which is classified under the Future Work section). Calling this endpoint + requires reference to a BlockIdentifier because transaction parsing can change + depending on which block contains the transaction. For example, in Bitcoin + it is necessary to know which block contains a transaction to determine the + destination of fee payments. Without specifying a block identifier, the node + would have to infer which block to use (which could change during a re-org). Implementations + that require fetching previous transactions to populate the response (ex: + Previous UTXOs in Bitcoin) may find it useful to run a cache within the Rosetta + server in the /data directory (on a path that does not conflict with the node).' + operationId: blockTransaction + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/BlockTransactionRequest' + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/BlockTransactionResponse' + description: Expected response to a valid request + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: unexpected error + summary: Get a Block Transaction + tags: + - Block + x-eov-operation-handler: controllers/BlockController + /mempool: + post: + description: Get all Transaction Identifiers in the mempool + operationId: mempool + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MempoolRequest' + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/MempoolResponse' + description: Expected response to a valid request + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: unexpected error + summary: Get All Mempool Transactions + tags: + - Mempool + x-eov-operation-handler: controllers/MempoolController + /mempool/transaction: + post: + description: 'Get a transaction in the mempool by its Transaction Identifier. + This is a separate request than fetching a block transaction (/block/transaction) + because some blockchain nodes need to know that a transaction query is for + something in the mempool instead of a transaction in a block. Transactions + may not be fully parsable until they are in a block (ex: may not be possible + to determine the fee to pay before a transaction is executed). On this endpoint, + it is ok that returned transactions are only estimates of what may actually + be included in a block.' + operationId: mempoolTransaction + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MempoolTransactionRequest' + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/MempoolTransactionResponse' + description: Expected response to a valid request + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: unexpected error + summary: Get a Mempool Transaction + tags: + - Mempool + x-eov-operation-handler: controllers/MempoolController + /account/balance: + post: + description: Get an array of all Account Balances for an Account Identifier + and the Block Identifier at which the balance lookup was performed. Some + consumers of account balance data need to know at which block the balance + was calculated to reconcile account balance changes. To get all balances + associated with an account, it may be necessary to perform multiple balance + requests with unique Account Identifiers. If the client supports it, passing + nil AccountIdentifier metadata to the request should fetch all balances (if + applicable). It is also possible to perform a historical balance lookup (if + the server supports it) by passing in an optional BlockIdentifier. + operationId: accountBalance + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AccountBalanceRequest' + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/AccountBalanceResponse' + description: Expected response to a valid request + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: unexpected error + summary: Get an Account Balance + tags: + - Account + x-eov-operation-handler: controllers/AccountController + /construction/metadata: + post: + description: Get any information required to construct a transaction for a specific + network. Metadata returned here could be a recent hash to use, an account + sequence number, or even arbitrary chain state. It is up to the client to + correctly populate the options object with any network-specific details to + ensure the correct metadata is retrieved. It is important to clarify that + this endpoint should not pre-construct any transactions for the client (this + should happen in the SDK). This endpoint is left purposely unstructured because + of the wide scope of metadata that could be required. In a future version + of the spec, we plan to pass an array of Rosetta Operations to specify which + metadata should be received and to create a transaction in an accompanying + SDK. This will help to insulate the client from chain-specific details that + are currently required here. + operationId: constructionMetadata + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ConstructionMetadataRequest' + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ConstructionMetadataResponse' + description: Expected response to a valid request + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: unexpected error + summary: Get Transaction Construction Metadata + tags: + - Construction + x-eov-operation-handler: controllers/ConstructionController + /construction/submit: + post: + description: Submit a pre-signed transaction to the node. This call should not + block on the transaction being included in a block. Rather, it should return + immediately with an indication of whether or not the transaction was included + in the mempool. The transaction submission response should only return a + 200 status if the submitted transaction could be included in the mempool. + Otherwise, it should return an error. + operationId: constructionSubmit + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ConstructionSubmitRequest' + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ConstructionSubmitResponse' + description: Expected response to a valid request + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: unexpected error + summary: Submit a Signed Transaction + tags: + - Construction + x-eov-operation-handler: controllers/ConstructionController +components: + schemas: + NetworkIdentifier: + description: The network_identifier specifies which network a particular object + is associated with. + example: + blockchain: bitcoin + sub_network_identifier: + metadata: + producer: 0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5 + network: shard 1 + network: mainnet + properties: + blockchain: + example: bitcoin + type: string + network: + description: If a blockchain has a specific chain-id or network identifier, + it should go in this field. It is up to the client to determine which + network-specific identifier is mainnet or testnet. + example: mainnet + type: string + sub_network_identifier: + $ref: '#/components/schemas/SubNetworkIdentifier' + required: + - blockchain + - network + type: object + SubNetworkIdentifier: + description: In blockchains with sharded state, the SubNetworkIdentifier is + required to query some object on a specific shard. This identifier is optional + for all non-sharded blockchains. + example: + metadata: + producer: 0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5 + network: shard 1 + properties: + network: + example: shard 1 + type: string + metadata: + example: + producer: 0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5 + type: object + required: + - network + type: object + BlockIdentifier: + description: The block_identifier uniquely identifies a block in a particular + network. + example: + index: 1123941 + hash: 0x1f2cc6c5027d2f201a5453ad1119574d2aed23a392654742ac3c78783c071f85 + properties: + index: + description: This is also known as the block height. + example: 1123941 + format: int64 + type: integer + hash: + example: 0x1f2cc6c5027d2f201a5453ad1119574d2aed23a392654742ac3c78783c071f85 + type: string + required: + - hash + - index + type: object + PartialBlockIdentifier: + description: When fetching data by BlockIdentifier, it may be possible to only + specify the index or hash. If neither property is specified, it is assumed + that the client is making a request at the current block. + example: + index: 1123941 + hash: 0x1f2cc6c5027d2f201a5453ad1119574d2aed23a392654742ac3c78783c071f85 + properties: + index: + example: 1123941 + format: int64 + type: integer + hash: + example: 0x1f2cc6c5027d2f201a5453ad1119574d2aed23a392654742ac3c78783c071f85 + type: string + type: object + TransactionIdentifier: + description: The transaction_identifier uniquely identifies a transaction in + a particular network and block or in the mempool. + example: + hash: 0x2f23fd8cca835af21f3ac375bac601f97ead75f2e79143bdf71fe2c4be043e8f + properties: + hash: + description: 'Any transactions that are attributable only to a block (ex: + a block event) should use the hash of the block as the identifier.' + example: 0x2f23fd8cca835af21f3ac375bac601f97ead75f2e79143bdf71fe2c4be043e8f + type: string + required: + - hash + type: object + OperationIdentifier: + description: The operation_identifier uniquely identifies an operation within + a transaction. + example: + index: 1 + network_index: 0 + properties: + index: + description: The operation index is used to ensure each operation has a + unique identifier within a transaction. To clarify, there may not be + any notion of an operation index in the blockchain being described. + example: 1 + format: int64 + minimum: 0 + type: integer + network_index: + description: Some blockchains specify an operation index that is essential + for client use. For example, Bitcoin uses a network_index to identify + which UTXO was used in a transaction. network_index should not be populated + if there is no notion of an operation index in a blockchain (typically + most account-based blockchains). + example: 0 + format: int64 + minimum: 0 + type: integer + required: + - index + type: object + AccountIdentifier: + description: The account_identifier uniquely identifies an account within a + network. All fields in the account_identifier are utilized to determine this + uniqueness (including the metadata field, if populated). + example: + metadata: '{}' + address: 0x3a065000ab4183c6bf581dc1e55a605455fc6d61 + sub_account: + metadata: '{}' + address: 0x6b175474e89094c44da98b954eedeac495271d0f + properties: + address: + description: The address may be a cryptographic public key (or some encoding + of it) or a provided username. + example: 0x3a065000ab4183c6bf581dc1e55a605455fc6d61 + type: string + sub_account: + $ref: '#/components/schemas/SubAccountIdentifier' + metadata: + description: Blockchains that utilize a username model (where the address + is not a derivative of a cryptographic public key) should specify the + public key(s) owned by the address in metadata. + type: object + required: + - address + type: object + SubAccountIdentifier: + description: An account may have state specific to a contract address (ERC-20 + token) and/or a stake (delegated balance). The sub_account_identifier should + specify which state (if applicable) an account instantiation refers to. + example: + metadata: '{}' + address: 0x6b175474e89094c44da98b954eedeac495271d0f + properties: + address: + description: 'The SubAccount address may be a cryptographic value or some + other identifier (ex: bonded) that uniquely specifies a SubAccount.' + example: 0x6b175474e89094c44da98b954eedeac495271d0f + type: string + metadata: + description: If the SubAccount address is not sufficient to uniquely specify + a SubAccount, any other identifying information can be stored here. It + is important to note that two SubAccounts with identical addresses but + differing metadata will not be considered equal by clients. + type: object + required: + - address + type: object + Block: + description: Blocks contain an array of Transactions that occurred at a particular + BlockIdentifier. + example: + metadata: + transactions_root: 0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347 + difficulty: "123891724987128947" + parent_block_identifier: + index: 1123941 + hash: 0x1f2cc6c5027d2f201a5453ad1119574d2aed23a392654742ac3c78783c071f85 + transactions: + - metadata: + size: 12378 + lockTime: 1582272577 + operations: + - amount: + metadata: '{}' + currency: + symbol: BTC + metadata: + Issuer: Satoshi + decimals: 8 + value: "1238089899992" + metadata: + asm: 304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd01 + 03301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + hex: 48304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd012103301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + related_operations: + - index: 0 + operation_identifier: + index: 0 + type: Transfer + account: + metadata: '{}' + address: 0x3a065000ab4183c6bf581dc1e55a605455fc6d61 + sub_account: + metadata: '{}' + address: 0x6b175474e89094c44da98b954eedeac495271d0f + operation_identifier: + index: 1 + network_index: 0 + status: Reverted + - amount: + metadata: '{}' + currency: + symbol: BTC + metadata: + Issuer: Satoshi + decimals: 8 + value: "1238089899992" + metadata: + asm: 304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd01 + 03301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + hex: 48304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd012103301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + related_operations: + - index: 0 + operation_identifier: + index: 0 + type: Transfer + account: + metadata: '{}' + address: 0x3a065000ab4183c6bf581dc1e55a605455fc6d61 + sub_account: + metadata: '{}' + address: 0x6b175474e89094c44da98b954eedeac495271d0f + operation_identifier: + index: 1 + network_index: 0 + status: Reverted + transaction_identifier: + hash: 0x2f23fd8cca835af21f3ac375bac601f97ead75f2e79143bdf71fe2c4be043e8f + - metadata: + size: 12378 + lockTime: 1582272577 + operations: + - amount: + metadata: '{}' + currency: + symbol: BTC + metadata: + Issuer: Satoshi + decimals: 8 + value: "1238089899992" + metadata: + asm: 304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd01 + 03301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + hex: 48304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd012103301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + related_operations: + - index: 0 + operation_identifier: + index: 0 + type: Transfer + account: + metadata: '{}' + address: 0x3a065000ab4183c6bf581dc1e55a605455fc6d61 + sub_account: + metadata: '{}' + address: 0x6b175474e89094c44da98b954eedeac495271d0f + operation_identifier: + index: 1 + network_index: 0 + status: Reverted + - amount: + metadata: '{}' + currency: + symbol: BTC + metadata: + Issuer: Satoshi + decimals: 8 + value: "1238089899992" + metadata: + asm: 304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd01 + 03301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + hex: 48304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd012103301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + related_operations: + - index: 0 + operation_identifier: + index: 0 + type: Transfer + account: + metadata: '{}' + address: 0x3a065000ab4183c6bf581dc1e55a605455fc6d61 + sub_account: + metadata: '{}' + address: 0x6b175474e89094c44da98b954eedeac495271d0f + operation_identifier: + index: 1 + network_index: 0 + status: Reverted + transaction_identifier: + hash: 0x2f23fd8cca835af21f3ac375bac601f97ead75f2e79143bdf71fe2c4be043e8f + block_identifier: + index: 1123941 + hash: 0x1f2cc6c5027d2f201a5453ad1119574d2aed23a392654742ac3c78783c071f85 + timestamp: 1582833600000 + properties: + block_identifier: + $ref: '#/components/schemas/BlockIdentifier' + parent_block_identifier: + $ref: '#/components/schemas/BlockIdentifier' + timestamp: + $ref: '#/components/schemas/Timestamp' + transactions: + items: + $ref: '#/components/schemas/Transaction' + type: array + metadata: + example: + transactions_root: 0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347 + difficulty: "123891724987128947" + type: object + required: + - block_identifier + - parent_block_identifier + - timestamp + - transactions + type: object + Transaction: + description: Transactions contain an array of Operations that are attributable + to the same TransactionIdentifier. + example: + metadata: + size: 12378 + lockTime: 1582272577 + operations: + - amount: + metadata: '{}' + currency: + symbol: BTC + metadata: + Issuer: Satoshi + decimals: 8 + value: "1238089899992" + metadata: + asm: 304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd01 + 03301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + hex: 48304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd012103301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + related_operations: + - index: 0 + operation_identifier: + index: 0 + type: Transfer + account: + metadata: '{}' + address: 0x3a065000ab4183c6bf581dc1e55a605455fc6d61 + sub_account: + metadata: '{}' + address: 0x6b175474e89094c44da98b954eedeac495271d0f + operation_identifier: + index: 1 + network_index: 0 + status: Reverted + - amount: + metadata: '{}' + currency: + symbol: BTC + metadata: + Issuer: Satoshi + decimals: 8 + value: "1238089899992" + metadata: + asm: 304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd01 + 03301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + hex: 48304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd012103301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + related_operations: + - index: 0 + operation_identifier: + index: 0 + type: Transfer + account: + metadata: '{}' + address: 0x3a065000ab4183c6bf581dc1e55a605455fc6d61 + sub_account: + metadata: '{}' + address: 0x6b175474e89094c44da98b954eedeac495271d0f + operation_identifier: + index: 1 + network_index: 0 + status: Reverted + transaction_identifier: + hash: 0x2f23fd8cca835af21f3ac375bac601f97ead75f2e79143bdf71fe2c4be043e8f + properties: + transaction_identifier: + $ref: '#/components/schemas/TransactionIdentifier' + operations: + items: + $ref: '#/components/schemas/Operation' + type: array + metadata: + description: Transactions that are related to other transactions (like a + cross-shard transactioin) should include the tranaction_identifier of + these transactions in the metadata. + example: + size: 12378 + lockTime: 1582272577 + type: object + required: + - operations + - transaction_identifier + type: object + Operation: + description: Operations contain all balance-changing information within a transaction. + They are always one-sided (only affect 1 AccountIdentifier) and can succeed + or fail independently from a Transaction. + example: + amount: + metadata: '{}' + currency: + symbol: BTC + metadata: + Issuer: Satoshi + decimals: 8 + value: "1238089899992" + metadata: + asm: 304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd01 + 03301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + hex: 48304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd012103301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + related_operations: + - index: 0 + operation_identifier: + index: 0 + type: Transfer + account: + metadata: '{}' + address: 0x3a065000ab4183c6bf581dc1e55a605455fc6d61 + sub_account: + metadata: '{}' + address: 0x6b175474e89094c44da98b954eedeac495271d0f + operation_identifier: + index: 1 + network_index: 0 + status: Reverted + properties: + operation_identifier: + $ref: '#/components/schemas/OperationIdentifier' + related_operations: + description: Restrict referenced related_operations to identifier indexes + < the current operation_identifier.index. This ensures there exists a + clear DAG-structure of relations. Since operations are one-sided, one + could imagine relating operations in a single transfer or linking operations + in a call tree. + example: + - index: 0 + operation_identifier: + index: 0 + items: + $ref: '#/components/schemas/OperationIdentifier' + type: array + type: + description: The network-specific type of the operation. Ensure that any + type that can be returned here is also specified in the NetowrkStatus. + This can be very useful to downstream consumers that parse all block data. + example: Transfer + type: string + status: + description: The network-specific status of the operation. Status is not + defined on the transaction object because blockchains with smart contracts + may have transactions that partially apply. Blockchains with atomic transactions + (all operations succeed or all operations fail) will have the same status + for each operation. + example: Reverted + type: string + account: + $ref: '#/components/schemas/AccountIdentifier' + amount: + $ref: '#/components/schemas/Amount' + metadata: + example: + asm: 304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd01 + 03301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + hex: 48304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd012103301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + type: object + required: + - operation_identifier + - status + - type + type: object + Amount: + description: Amount is some Value of a Currency. It is considered invalid to + specify a Value without a Currency. + example: + metadata: '{}' + currency: + symbol: BTC + metadata: + Issuer: Satoshi + decimals: 8 + value: "1238089899992" + properties: + value: + description: Value of the transaction in atomic units represented as an + arbitrary-sized signed integer. For example, 1 BTC would be represented + by a value of 100000000. + example: "1238089899992" + type: string + currency: + $ref: '#/components/schemas/Currency' + metadata: + type: object + required: + - currency + - value + type: object + Currency: + description: Currency is composed of a canonical Symbol and Decimals. This Decimals + value is used to convert an Amount.Value from atomic units (Satoshis) to standard + units (Bitcoins). + example: + symbol: BTC + metadata: + Issuer: Satoshi + decimals: 8 + properties: + symbol: + description: Canonical symbol associated with a currency. + example: BTC + type: string + decimals: + description: Number of decimal places in the standard unit representation + of the amount. For example, BTC has 8 decimals. Note that it is not possible + to represent the value of some currency in atomic units that is not base + 10. + example: 8 + format: int32 + minimum: 0 + type: integer + metadata: + description: Any additional information related to the currency itself. For + example, it would be useful to populate this object with the contract + address of an ERC-20 token. + example: + Issuer: Satoshi + type: object + required: + - decimals + - symbol + type: object + Peer: + description: A Peer is a representation of a node's peer. + example: + metadata: '{}' + peer_id: 0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5 + properties: + peer_id: + example: 0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5 + type: string + metadata: + type: object + required: + - peer_id + type: object + Version: + description: The Version object is utilized to inform the client of the versions + of different components of the Rosetta implementation. + example: + metadata: '{}' + rosetta_version: 1.2.5 + node_version: 1.0.2 + middleware_version: 0.2.7 + properties: + rosetta_version: + description: The rosetta_version is the version of the Rosetta interface + the implementation adheres to. This can be useful for clients looking + to reliably parse responses. + example: 1.2.5 + type: string + node_version: + description: The node_version is the canonical version of the node runtime. + This can help clients manage deployments. + example: 1.0.2 + type: string + middleware_version: + description: When a middleware server is used to adhere to the Rosetta interface, + it should return its version here. This can help clients manage deployments. + example: 0.2.7 + type: string + metadata: + description: Any other information that may be useful about versioning of + dependent services should be returned here. + type: object + required: + - node_version + - rosetta_version + type: object + Allow: + description: Allow specifies supported Operation status, Operation types, and + all possible error statuses. This Allow object is used by clients to validate + the correctness of a Rosetta Server implementation. It is expected that these + clients will error if they receive some response that contains any of the + above information that is not specified here. + example: + operation_types: + - TRANSFER + - TRANSFER + operation_statuses: + - status: SUCCESS + successful: true + - status: SUCCESS + successful: true + errors: + - retriable: true + code: 0 + message: message + - retriable: true + code: 0 + message: message + properties: + operation_statuses: + description: All Operation.Status this implementation supports. Any status + that is returned during parsing that is not listed here will cause client + validation to error. + items: + $ref: '#/components/schemas/OperationStatus' + type: array + operation_types: + description: All Operation.Type this implementation supports. Any type that + is returned during parsing that is not listed here will cause client validation + to error. + items: + example: TRANSFER + type: string + type: array + errors: + description: All Errors that this implementation could return. Any error + that is returned during parsing that is not listed here will cause client + validation to error. + items: + $ref: '#/components/schemas/Error' + type: array + required: + - errors + - operation_statuses + - operation_types + type: object + OperationStatus: + description: OperationStatus is utilized to indicate which Operation status + are considered successful. + example: + status: SUCCESS + successful: true + properties: + status: + description: The status is the network-specific status of the operation. + type: string + successful: + description: An Operation is considered successful if the Operation.Amount + should affect the Operation.Account. Some blockchains (like Bitcoin) only + include successful operations in blocks but other blockchains (like Ethereum) + include unsuccessful operations that incur a fee. To reconcile the computed + balance from the stream of Operations, it is critical to understand which + Operation.Status indicate an Operation is successful and should affect + an Account. + type: boolean + required: + - status + - successful + type: object + Timestamp: + description: The timestamp of the block in milliseconds since the Unix Epoch. + The timestamp is stored in milliseconds because some blockchains produce blocks + more often than once a second. + example: 1582833600000 + format: int64 + minimum: 0 + type: integer + AccountBalanceRequest: + description: An AccountBalanceRequest is utilized to make a balance request + on the /account/balance endpoint. If the block_identifier is populated, a + historical balance query should be performed. + example: + account_identifier: + metadata: '{}' + address: 0x3a065000ab4183c6bf581dc1e55a605455fc6d61 + sub_account: + metadata: '{}' + address: 0x6b175474e89094c44da98b954eedeac495271d0f + network_identifier: + blockchain: bitcoin + sub_network_identifier: + metadata: + producer: 0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5 + network: shard 1 + network: mainnet + block_identifier: + index: 1123941 + hash: 0x1f2cc6c5027d2f201a5453ad1119574d2aed23a392654742ac3c78783c071f85 + properties: + network_identifier: + $ref: '#/components/schemas/NetworkIdentifier' + account_identifier: + $ref: '#/components/schemas/AccountIdentifier' + block_identifier: + $ref: '#/components/schemas/PartialBlockIdentifier' + required: + - account_identifier + - network_identifier + type: object + AccountBalanceResponse: + description: 'An AccountBalanceResponse is returned on the /account/balance + endpoint. If an account has a balance for each AccountIdentifier describing + it (ex: an ERC-20 token balance on a few smart contracts), an account balance + request must be made with each AccountIdentifier.' + example: + balances: + - metadata: '{}' + currency: + symbol: BTC + metadata: + Issuer: Satoshi + decimals: 8 + value: "1238089899992" + - metadata: '{}' + currency: + symbol: BTC + metadata: + Issuer: Satoshi + decimals: 8 + value: "1238089899992" + metadata: + sequence_number: 23 + block_identifier: + index: 1123941 + hash: 0x1f2cc6c5027d2f201a5453ad1119574d2aed23a392654742ac3c78783c071f85 + properties: + block_identifier: + $ref: '#/components/schemas/BlockIdentifier' + balances: + description: A single account may have a balance in multiple currencies. + items: + $ref: '#/components/schemas/Amount' + type: array + metadata: + description: Account-based blockchains that utilize a nonce or sequence + number should include that number in the metadata. This number could be + unique to the identifier or global across the account address. + example: + sequence_number: 23 + type: object + required: + - balances + - block_identifier + type: object + BlockRequest: + description: A BlockRequest is utilized to make a block request on the /block + endpoint. + example: + network_identifier: + blockchain: bitcoin + sub_network_identifier: + metadata: + producer: 0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5 + network: shard 1 + network: mainnet + block_identifier: + index: 1123941 + hash: 0x1f2cc6c5027d2f201a5453ad1119574d2aed23a392654742ac3c78783c071f85 + properties: + network_identifier: + $ref: '#/components/schemas/NetworkIdentifier' + block_identifier: + $ref: '#/components/schemas/PartialBlockIdentifier' + required: + - block_identifier + - network_identifier + type: object + BlockResponse: + description: A BlockResponse includes a fully-populated block or a partially-populated + block with a list of other transactions to fetch (other_transactions). + example: + block: + metadata: + transactions_root: 0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347 + difficulty: "123891724987128947" + parent_block_identifier: + index: 1123941 + hash: 0x1f2cc6c5027d2f201a5453ad1119574d2aed23a392654742ac3c78783c071f85 + transactions: + - metadata: + size: 12378 + lockTime: 1582272577 + operations: + - amount: + metadata: '{}' + currency: + symbol: BTC + metadata: + Issuer: Satoshi + decimals: 8 + value: "1238089899992" + metadata: + asm: 304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd01 + 03301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + hex: 48304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd012103301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + related_operations: + - index: 0 + operation_identifier: + index: 0 + type: Transfer + account: + metadata: '{}' + address: 0x3a065000ab4183c6bf581dc1e55a605455fc6d61 + sub_account: + metadata: '{}' + address: 0x6b175474e89094c44da98b954eedeac495271d0f + operation_identifier: + index: 1 + network_index: 0 + status: Reverted + - amount: + metadata: '{}' + currency: + symbol: BTC + metadata: + Issuer: Satoshi + decimals: 8 + value: "1238089899992" + metadata: + asm: 304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd01 + 03301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + hex: 48304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd012103301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + related_operations: + - index: 0 + operation_identifier: + index: 0 + type: Transfer + account: + metadata: '{}' + address: 0x3a065000ab4183c6bf581dc1e55a605455fc6d61 + sub_account: + metadata: '{}' + address: 0x6b175474e89094c44da98b954eedeac495271d0f + operation_identifier: + index: 1 + network_index: 0 + status: Reverted + transaction_identifier: + hash: 0x2f23fd8cca835af21f3ac375bac601f97ead75f2e79143bdf71fe2c4be043e8f + - metadata: + size: 12378 + lockTime: 1582272577 + operations: + - amount: + metadata: '{}' + currency: + symbol: BTC + metadata: + Issuer: Satoshi + decimals: 8 + value: "1238089899992" + metadata: + asm: 304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd01 + 03301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + hex: 48304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd012103301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + related_operations: + - index: 0 + operation_identifier: + index: 0 + type: Transfer + account: + metadata: '{}' + address: 0x3a065000ab4183c6bf581dc1e55a605455fc6d61 + sub_account: + metadata: '{}' + address: 0x6b175474e89094c44da98b954eedeac495271d0f + operation_identifier: + index: 1 + network_index: 0 + status: Reverted + - amount: + metadata: '{}' + currency: + symbol: BTC + metadata: + Issuer: Satoshi + decimals: 8 + value: "1238089899992" + metadata: + asm: 304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd01 + 03301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + hex: 48304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd012103301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + related_operations: + - index: 0 + operation_identifier: + index: 0 + type: Transfer + account: + metadata: '{}' + address: 0x3a065000ab4183c6bf581dc1e55a605455fc6d61 + sub_account: + metadata: '{}' + address: 0x6b175474e89094c44da98b954eedeac495271d0f + operation_identifier: + index: 1 + network_index: 0 + status: Reverted + transaction_identifier: + hash: 0x2f23fd8cca835af21f3ac375bac601f97ead75f2e79143bdf71fe2c4be043e8f + block_identifier: + index: 1123941 + hash: 0x1f2cc6c5027d2f201a5453ad1119574d2aed23a392654742ac3c78783c071f85 + timestamp: 1582833600000 + other_transactions: + - hash: 0x2f23fd8cca835af21f3ac375bac601f97ead75f2e79143bdf71fe2c4be043e8f + - hash: 0x2f23fd8cca835af21f3ac375bac601f97ead75f2e79143bdf71fe2c4be043e8f + properties: + block: + $ref: '#/components/schemas/Block' + other_transactions: + description: 'Some blockchains may require additional transactions to be + fetched that weren''t returned in the block response (ex: block only returns + transaction hashes). For blockchains with a lot of transactions in each + block, this can be very useful as consumers can concurrently fetch all + transactions returned.' + items: + $ref: '#/components/schemas/TransactionIdentifier' + type: array + required: + - block + type: object + BlockTransactionRequest: + description: A BlockTransactionRequest is used to fetch a Transaction included + in a block that is not returned in a BlockResponse. + example: + transaction_identifier: + hash: 0x2f23fd8cca835af21f3ac375bac601f97ead75f2e79143bdf71fe2c4be043e8f + network_identifier: + blockchain: bitcoin + sub_network_identifier: + metadata: + producer: 0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5 + network: shard 1 + network: mainnet + block_identifier: + index: 1123941 + hash: 0x1f2cc6c5027d2f201a5453ad1119574d2aed23a392654742ac3c78783c071f85 + properties: + network_identifier: + $ref: '#/components/schemas/NetworkIdentifier' + block_identifier: + $ref: '#/components/schemas/BlockIdentifier' + transaction_identifier: + $ref: '#/components/schemas/TransactionIdentifier' + required: + - block_identifier + - network_identifier + - transaction_identifier + type: object + BlockTransactionResponse: + description: A BlockTransactionResponse contains information about a block transaction. + example: + transaction: + metadata: + size: 12378 + lockTime: 1582272577 + operations: + - amount: + metadata: '{}' + currency: + symbol: BTC + metadata: + Issuer: Satoshi + decimals: 8 + value: "1238089899992" + metadata: + asm: 304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd01 + 03301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + hex: 48304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd012103301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + related_operations: + - index: 0 + operation_identifier: + index: 0 + type: Transfer + account: + metadata: '{}' + address: 0x3a065000ab4183c6bf581dc1e55a605455fc6d61 + sub_account: + metadata: '{}' + address: 0x6b175474e89094c44da98b954eedeac495271d0f + operation_identifier: + index: 1 + network_index: 0 + status: Reverted + - amount: + metadata: '{}' + currency: + symbol: BTC + metadata: + Issuer: Satoshi + decimals: 8 + value: "1238089899992" + metadata: + asm: 304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd01 + 03301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + hex: 48304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd012103301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + related_operations: + - index: 0 + operation_identifier: + index: 0 + type: Transfer + account: + metadata: '{}' + address: 0x3a065000ab4183c6bf581dc1e55a605455fc6d61 + sub_account: + metadata: '{}' + address: 0x6b175474e89094c44da98b954eedeac495271d0f + operation_identifier: + index: 1 + network_index: 0 + status: Reverted + transaction_identifier: + hash: 0x2f23fd8cca835af21f3ac375bac601f97ead75f2e79143bdf71fe2c4be043e8f + properties: + transaction: + $ref: '#/components/schemas/Transaction' + required: + - transaction + type: object + MempoolRequest: + description: A MempoolRequest is utilized to retrieve all transaction identifiers + in the mempool for a particular network_identifier. + example: + network_identifier: + blockchain: bitcoin + sub_network_identifier: + metadata: + producer: 0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5 + network: shard 1 + network: mainnet + properties: + network_identifier: + $ref: '#/components/schemas/NetworkIdentifier' + required: + - network_identifier + type: object + MempoolResponse: + description: A MempoolResponse contains all transaction identifiers in the mempool + for a particular network_identifier. + example: + transaction_identifiers: + - hash: 0x2f23fd8cca835af21f3ac375bac601f97ead75f2e79143bdf71fe2c4be043e8f + - hash: 0x2f23fd8cca835af21f3ac375bac601f97ead75f2e79143bdf71fe2c4be043e8f + properties: + transaction_identifiers: + items: + $ref: '#/components/schemas/TransactionIdentifier' + type: array + required: + - transaction_identifiers + type: object + MempoolTransactionRequest: + description: A MempoolTransactionRequest is utilized to retrieve a transaction + from the mempool. + example: + transaction_identifier: + hash: 0x2f23fd8cca835af21f3ac375bac601f97ead75f2e79143bdf71fe2c4be043e8f + network_identifier: + blockchain: bitcoin + sub_network_identifier: + metadata: + producer: 0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5 + network: shard 1 + network: mainnet + properties: + network_identifier: + $ref: '#/components/schemas/NetworkIdentifier' + transaction_identifier: + $ref: '#/components/schemas/TransactionIdentifier' + required: + - network_identifier + - transaction_identifier + type: object + MempoolTransactionResponse: + description: 'A MempoolTransactionResponse contains an estimate of a mempool + transaction. It may not be possible to know the full impact of a transaction + in the mempool (ex: fee paid).' + example: + metadata: + descendant_fees: 123923 + ancestor_count: 2 + transaction: + metadata: + size: 12378 + lockTime: 1582272577 + operations: + - amount: + metadata: '{}' + currency: + symbol: BTC + metadata: + Issuer: Satoshi + decimals: 8 + value: "1238089899992" + metadata: + asm: 304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd01 + 03301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + hex: 48304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd012103301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + related_operations: + - index: 0 + operation_identifier: + index: 0 + type: Transfer + account: + metadata: '{}' + address: 0x3a065000ab4183c6bf581dc1e55a605455fc6d61 + sub_account: + metadata: '{}' + address: 0x6b175474e89094c44da98b954eedeac495271d0f + operation_identifier: + index: 1 + network_index: 0 + status: Reverted + - amount: + metadata: '{}' + currency: + symbol: BTC + metadata: + Issuer: Satoshi + decimals: 8 + value: "1238089899992" + metadata: + asm: 304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd01 + 03301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + hex: 48304502201fd8abb11443f8b1b9a04e0495e0543d05611473a790c8939f089d073f90509a022100f4677825136605d732e2126d09a2d38c20c75946cd9fc239c0497e84c634e3dd012103301a8259a12e35694cc22ebc45fee635f4993064190f6ce96e7fb19a03bb6be2 + related_operations: + - index: 0 + operation_identifier: + index: 0 + type: Transfer + account: + metadata: '{}' + address: 0x3a065000ab4183c6bf581dc1e55a605455fc6d61 + sub_account: + metadata: '{}' + address: 0x6b175474e89094c44da98b954eedeac495271d0f + operation_identifier: + index: 1 + network_index: 0 + status: Reverted + transaction_identifier: + hash: 0x2f23fd8cca835af21f3ac375bac601f97ead75f2e79143bdf71fe2c4be043e8f + properties: + transaction: + $ref: '#/components/schemas/Transaction' + metadata: + example: + descendant_fees: 123923 + ancestor_count: 2 + type: object + required: + - transaction + type: object + MetadataRequest: + description: A MetadataRequest is utilized in any request where the only argument + is optional metadata. + example: + metadata: '{}' + properties: + metadata: + type: object + type: object + NetworkListResponse: + description: A NetworkListResponse contains all NetworkIdentifiers that the + node can serve information for. + example: + network_identifiers: + - blockchain: bitcoin + sub_network_identifier: + metadata: + producer: 0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5 + network: shard 1 + network: mainnet + - blockchain: bitcoin + sub_network_identifier: + metadata: + producer: 0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5 + network: shard 1 + network: mainnet + properties: + network_identifiers: + items: + $ref: '#/components/schemas/NetworkIdentifier' + type: array + required: + - network_identifiers + type: object + NetworkRequest: + description: A NetworkRequest is utilized to retrieve some data specific exclusively + to a NetworkIdentifier. + example: + metadata: '{}' + network_identifier: + blockchain: bitcoin + sub_network_identifier: + metadata: + producer: 0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5 + network: shard 1 + network: mainnet + properties: + network_identifier: + $ref: '#/components/schemas/NetworkIdentifier' + metadata: + type: object + required: + - network_identifier + type: object + NetworkStatusResponse: + description: NetworkStatusResponse contains basic information about the node's + view of a blockchain network. + example: + current_block_identifier: + index: 1123941 + hash: 0x1f2cc6c5027d2f201a5453ad1119574d2aed23a392654742ac3c78783c071f85 + peers: + - metadata: '{}' + peer_id: 0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5 + - metadata: '{}' + peer_id: 0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5 + current_block_timestamp: 1582833600000 + genesis_block_identifier: + index: 1123941 + hash: 0x1f2cc6c5027d2f201a5453ad1119574d2aed23a392654742ac3c78783c071f85 + properties: + current_block_identifier: + $ref: '#/components/schemas/BlockIdentifier' + current_block_timestamp: + $ref: '#/components/schemas/Timestamp' + genesis_block_identifier: + $ref: '#/components/schemas/BlockIdentifier' + peers: + items: + $ref: '#/components/schemas/Peer' + type: array + required: + - current_block_identifier + - current_block_timestamp + - genesis_block_identifier + - peers + type: object + NetworkOptionsResponse: + description: NetworkOptionsResponse contains information about the versioning + of the node and the allowed operation statuses, operation types, and errors. + example: + allow: + operation_types: + - TRANSFER + - TRANSFER + operation_statuses: + - status: SUCCESS + successful: true + - status: SUCCESS + successful: true + errors: + - retriable: true + code: 0 + message: message + - retriable: true + code: 0 + message: message + version: + metadata: '{}' + rosetta_version: 1.2.5 + node_version: 1.0.2 + middleware_version: 0.2.7 + properties: + version: + $ref: '#/components/schemas/Version' + allow: + $ref: '#/components/schemas/Allow' + required: + - allow + - version + type: object + ConstructionMetadataRequest: + description: A ConstructionMetadataRequest is utilized to get information required + to construct a transaction. The Options object used to specify which metadata + to return is left purposely unstructured to allow flexibility for implementers. + example: + options: '{}' + network_identifier: + blockchain: bitcoin + sub_network_identifier: + metadata: + producer: 0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5 + network: shard 1 + network: mainnet + properties: + network_identifier: + $ref: '#/components/schemas/NetworkIdentifier' + options: + description: 'Some blockchains require different metadata for different + types of transaction construction (ex: delegation versus a transfer). + Instead of requiring a blockchain node to return all possible types of + metadata for construction (which may require multiple node fetches), the + client can populate an options object to limit the metadata returned to + only the subset required.' + type: object + required: + - network_identifier + - options + type: object + ConstructionMetadataResponse: + description: The ConstructionMetadataResponse returns network-specific metadata + used for transaction construction. It is likely that the client will not inspect + this metadata before passing it to a client SDK that uses it for construction. + example: + metadata: + account_sequence: 23 + recent_block_hash: 0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5 + properties: + metadata: + example: + account_sequence: 23 + recent_block_hash: 0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5 + type: object + required: + - metadata + type: object + ConstructionSubmitRequest: + description: The transaction submission request includes a signed transaction. + example: + signed_transaction: signed_transaction + network_identifier: + blockchain: bitcoin + sub_network_identifier: + metadata: + producer: 0x52bc44d5378309ee2abf1539bf71de1b7d7be3b5 + network: shard 1 + network: mainnet + properties: + network_identifier: + $ref: '#/components/schemas/NetworkIdentifier' + signed_transaction: + type: string + required: + - network_identifier + - signed_transaction + type: object + ConstructionSubmitResponse: + description: A TransactionSubmitResponse contains the transaction_identifier + of a submitted transaction that was accepted into the mempool. + example: + metadata: '{}' + transaction_identifier: + hash: 0x2f23fd8cca835af21f3ac375bac601f97ead75f2e79143bdf71fe2c4be043e8f + properties: + transaction_identifier: + $ref: '#/components/schemas/TransactionIdentifier' + metadata: + type: object + required: + - transaction_identifier + type: object + Error: + description: Instead of utilizing HTTP status codes to describe node errors + (which often do not have a good analog), rich errors are returned using this + object. + example: + retriable: true + code: 0 + message: message + properties: + code: + description: Code is a network-specific error code. If desired, this code + can be equivalent to an HTTP status code. + format: int32 + minimum: 0 + type: integer + message: + description: Message is a network-specific error message. + type: string + retriable: + description: An error is retriable if the same request may succeed if submitted + again. + type: boolean + required: + - code + - message + - retriable + type: object diff --git a/config/default.js b/config/default.js new file mode 100644 index 0000000..5593365 --- /dev/null +++ b/config/default.js @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +const path = require('path'); + +const config = { + ROOT_DIR: path.join(__dirname, '..'), + URL_PORT: 8080, + URL_PATH: 'http://localhost', + BASE_VERSION: 'v2', + CONTROLLER_DIRECTORY: path.join(__dirname, 'controllers'), + PROJECT_DIR: path.join(__dirname, '..'), + BEAUTIFY_JSON: true, +}; + +config.OPENAPI_YAML = path.join(config.ROOT_DIR, 'api', 'openapi.yaml'); +config.FULL_PATH = `${config.URL_PATH}:${config.URL_PORT}/${config.BASE_VERSION}`; +config.FILE_UPLOAD_PATH = path.join(config.PROJECT_DIR, 'uploaded_files'); + +module.exports = config; diff --git a/examples/basic/README.md b/examples/basic/README.md new file mode 100644 index 0000000..d00b6a5 --- /dev/null +++ b/examples/basic/README.md @@ -0,0 +1,10 @@ +# Examples + +This folder demonstrates how to write a Rosetta Node server and how +to use either the Client package or Fetcher package to communicate +with that server. + +## Steps +1. Run `node server` +2. Run `node client` (in a new terminal window) +2. Run `node fetcher` (in a new terminal window) \ No newline at end of file diff --git a/examples/basic/client/index.js b/examples/basic/client/index.js new file mode 100644 index 0000000..a541e0d --- /dev/null +++ b/examples/basic/client/index.js @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +const RosettaSDK = require('../../..'); +const RosettaClient = RosettaSDK.Client; + +// Create an instance of APIClient and configure it. +const APIClient = new RosettaClient.ApiClient(); +APIClient.basePath = 'http://localhost:8080'; +APIClient.agent = 'rosetta-sdk-node'; +APIClient.timeout = 10 * 1000; // 10 seconds + +async function startClient() { + // Step1: Create an instance of RosettaSDK Client + const networkAPI = new RosettaClient.NetworkApi(APIClient).promises; + + // Step 2: Get all available networks + const metadataRequest = new RosettaClient.MetadataRequest(); + const networkList = await networkAPI.networkList(metadataRequest); + if (networkList.network_identifiers.length == 0) { + console.error('Server did not respond with any network identifier'); + return; + } + + // Step 3: Print the primary network + const primaryNetwork = networkList.network_identifiers[0]; + console.log(`Primary Network: ${primaryNetwork}`); + + // Step 4: Fetch the network status + const networkRequest = new RosettaClient.NetworkRequest(primaryNetwork); + const networkStatus = await networkAPI.networkStatus(networkRequest); + + // Step 5: Print Network Status + console.log(`Network Status: ${networkStatus}`); + + // Step 6: Asserter (ToDo?) + // Response Assertions are already handled by the server. + + // Step 7: Fetch the Network Options + const networkOptions = await networkAPI.networkOptions(networkRequest); + + // Step 8: Print Network Options + console.log(`Network Options: ${networkOptions}`); + + // Step 9: Asserter (ToDo?), refer to step 6. + // Step 10: ClientAsserter, refer to step 6. + // ... + + // Step 11: Fetch current block + const blockIdentifier = RosettaClient.PartialBlockIdentifier.constructFromObject({ + hash: networkStatus.current_block_identifier, + }); + const blockRequest = new RosettaClient.BlockRequest(primaryNetwork, blockIdentifier); + const block = await networkAPI.block(blockRequest); + + // Step 12: Print the block + console.log(`Current Block: ${block}`); + + // Step 13: Assert the block response is valid, refer to step 6 + + // Step 14: Print transactions in that block + block.OtherTransactions.forEach(tx => { + console.log(` Transaction: ${tx}`); + }); +}; + +startClient() + .catch(e => console.error(e)); diff --git a/examples/basic/fetcher/index.js b/examples/basic/fetcher/index.js new file mode 100644 index 0000000..92e3cbd --- /dev/null +++ b/examples/basic/fetcher/index.js @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +const RosettaSDK = require('../../..'); + +// Create an instance of Fetcher +const fetcher = new RosettaSDK.Fetcher({ + server: { + protocol: 'http', + host: 'localhost', + port: '8080', + }, +}); + +const main = (async function () { + const { primaryNetwork, networkStatus } = await fetcher.initializeAsserter(); + + console.log(`Primary Network: ${JSON.stringify(primaryNetwork)}`); + console.log(`Network Status: ${JSON.stringify(networkStatus)}`); + + const block = await fetcher.blockRetry( + primaryNetwork, + new RosettaSDK.Utils.constructPartialBlockIdentifier(networkStatus.current_block_identifier), + ); + + console.log(`Current Block: ${JSON.stringify(block)}`); + + const blockMap = fetcher.blockRange( + primaryNetwork, + networkStatus.genesis_block_identifier.index, + networkStatus.genesis_block_identifier.index + 10, + ); + + console.log(`Current Range: ${JSON.stringify(blockMap)}`); +}); + +main().catch(e => { + console.error(e); +}) diff --git a/examples/basic/server/index.js b/examples/basic/server/index.js new file mode 100644 index 0000000..4a1f2db --- /dev/null +++ b/examples/basic/server/index.js @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +const RosettaSDK = require('../../..'); + +const ServiceHandlers = require('./services'); +const networkIdentifier = require('./network'); + +const asserter = RosettaSDK.Asserter.NewServer( + ['Transfer', 'Reward'], + false, + [networkIdentifier], +); + +/* Create a server configuration */ +const Server = new RosettaSDK.Server({ + URL_PORT: 8080, +}); + +// Register global asserter +Server.useAsserter(asserter); + +/* Data API: Network */ +Server.register('/network/list', ServiceHandlers.Network.networkList); +Server.register('/network/options', ServiceHandlers.Network.networkOptions); +Server.register('/network/status', ServiceHandlers.Network.networkStatus); + +/* Data API: Block */ +Server.register('/block', ServiceHandlers.Block.block); +Server.register('/block/transaction', ServiceHandlers.Block.blockTransaction); diff --git a/examples/basic/server/network.js b/examples/basic/server/network.js new file mode 100644 index 0000000..16936c0 --- /dev/null +++ b/examples/basic/server/network.js @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +const RosettaSDK = require('../../..'); +const networkIdentifier = new RosettaSDK.Client.NetworkIdentifier('Rosetta', 'Testnet'); + +module.exports = networkIdentifier; \ No newline at end of file diff --git a/examples/basic/server/services/BlockService.js b/examples/basic/server/services/BlockService.js new file mode 100644 index 0000000..69921d8 --- /dev/null +++ b/examples/basic/server/services/BlockService.js @@ -0,0 +1,156 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +const RosettaSDK = require('../../../..'); +const Types = RosettaSDK.Client; + +/* Data API: Block */ + +/** +* Get a Block +* Get a block by its Block Identifier. If transactions are returned in the same call to the node as fetching the block, the response should include these transactions in the Block object. If not, an array of Transaction Identifiers should be returned so /block/transaction fetches can be done to get all transaction information. +* +* blockRequest BlockRequest +* returns BlockResponse +* */ +const block = async (params) => { + const { blockRequest } = params; + + if (blockRequest.block_identifier.index != 1000) { + const previousBlockIndex = Math.max(0, blockRequest.block_identifier.index - 1); + + const blockIdentifier = new Types.BlockIdentifier( + blockRequest.block_identifier.index, + `block ${blockRequest.block_identifier.index}`, + ); + + const parentBlockIdentifier = new Types.BlockIdentifier( + previousBlockIndex, + `block ${previousBlockIndex}`, + ); + + const timestamp = Date.now() - 500000; + const transactions = []; + + const block = new Types.Block( + blockIdentifier, + parentBlockIdentifier, + timestamp, + transactions, + ); + + return new Types.BlockResponse(block); + } + + const previousBlockIndex = Math.max(0, blockRequest.block_identifier.index - 1); + + const blockIdentifier = new Types.BlockIdentifier( + 1000, + 'block 1000', + ); + + const parentBlockIdentifier = new Types.BlockIdentifier( + 999, + 'block 999', + ); + + const timestamp = 1586483189000; + const transactionIdentifier = new Types.TransactionIdentifier('transaction 0'); + const operations = [ + Types.Operation.constructFromObject({ + 'operation_identifier': new Types.OperationIdentifier(0), + 'type': 'Transfer', + 'status': 'Success', + 'account': new Types.AccountIdentifier('account 0'), + 'amount': new Types.Amount( + '-1000', + new Types.Currency('ROS', 2) + ), + }), + + Types.Operation.constructFromObject({ + 'operation_identifier': new Types.OperationIdentifier(1), + 'related_operations': new Types.OperationIdentifier(0), + 'type': 'Transfer', + 'status': 'Reverted', + 'account': new Types.AccountIdentifier('account 1'), + 'amount': new Types.Amount( + '1000', + new Types.Currency('ROS', 2) + ), + }), + ]; + + const transactions = [ + new Types.Transaction(transactionIdentifier, operations), + ]; + + const block = new Types.Block( + blockIdentifier, + parentBlockIdentifier, + timestamp, + transactions, + ); + + const otherTransactions = [ + new Types.TransactionIdentifier('transaction 1'), + ]; + + return new Types.BlockResponse( + block, + otherTransactions, + ); +}; + +/** +* Get a Block Transaction +* Get a transaction in a block by its Transaction Identifier. This endpoint should only be used when querying a node for a block does not return all transactions contained within it. All transactions returned by this endpoint must be appended to any transactions returned by the /block method by consumers of this data. Fetching a transaction by hash is considered an Explorer Method (which is classified under the Future Work section). Calling this endpoint requires reference to a BlockIdentifier because transaction parsing can change depending on which block contains the transaction. For example, in Bitcoin it is necessary to know which block contains a transaction to determine the destination of fee payments. Without specifying a block identifier, the node would have to infer which block to use (which could change during a re-org). Implementations that require fetching previous transactions to populate the response (ex: Previous UTXOs in Bitcoin) may find it useful to run a cache within the Rosetta server in the /data directory (on a path that does not conflict with the node). +* +* blockTransactionRequest BlockTransactionRequest +* returns BlockTransactionResponse +* */ +const blockTransaction = async (params) => { + const { blockTransactionRequest } = params; + + const transactionIdentifier = new Types.TransactionIdentifier('transaction 1'); + const operations = [ + Types.Operation.constructFromObject({ + 'operation_identifier': new Types.OperationIdentifier(0), + 'type': 'Reward', + 'status': 'Success', + 'account': new Types.AccountIdentifier('account 2'), + 'amount': new Types.Amount( + '1000', + new Types.Currency('ROS', 2), + ), + }), + ]; + + return new Types.Transaction(transactionIdentifier, operations); +}; + +module.exports = { + /* /block */ + block, + + /* /block/transaction */ + blockTransaction, +}; diff --git a/examples/basic/server/services/NetworkService.js b/examples/basic/server/services/NetworkService.js new file mode 100644 index 0000000..b868616 --- /dev/null +++ b/examples/basic/server/services/NetworkService.js @@ -0,0 +1,115 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +const RosettaSDK = require('../../../..'); +const Types = RosettaSDK.Client; + +const networkIdentifier = require('../network'); + +/* Data API: Network */ + +/** +* Get List of Available Networks +* This endpoint returns a list of NetworkIdentifiers that the Rosetta server can handle. +* +* metadataRequest MetadataRequest +* returns NetworkListResponse +* */ +const networkList = async (params) => { + const { metadataRequest } = params; + + return new Types.NetworkListResponse( + [ networkIdentifier ], + ); +}; + +/** +* Get Network Options +* This endpoint returns the version information and allowed network-specific types for a NetworkIdentifier. Any NetworkIdentifier returned by /network/list should be accessible here. Because options are retrievable in the context of a NetworkIdentifier, it is possible to define unique options for each network. +* +* networkRequest NetworkRequest +* returns NetworkOptionsResponse +* */ +const networkOptions = async (params) => { + const { networkRequest } = params; + + const rosettaVersion = '1.4.0'; + const nodeVersion = '0.0.1'; + + const operationStatuses = [ + new Types.OperationStatus('Success', true), + new Types.OperationStatus('Reverted', false), + ]; + + const operationTypes = [ + 'Transfer', + 'Reward', + ]; + + const errors = [ + new Types.Error(1, 'not implemented', false), + ]; + + return new Types.NetworkOptionsResponse( + new Types.Version(rosettaVersion, nodeVersion), + new Types.Allow( + operationStatuses, + operationTypes, + errors, + ), + ); +}; + +/** +* Get Network Status +* This endpoint returns the current status of the network requested. Any NetworkIdentifier returned by /network/list should be accessible here. +* +* networkRequest NetworkRequest +* returns NetworkStatusResponse +* */ +const networkStatus = async (params) => { + const { networkRequest } = params; + + const currentBlockIdentifier = new Types.BlockIdentifier(1000, 'block 1000'); + const currentBlockTimestamp = 1586483189000; + const genesisBlockIdentifier = new Types.BlockIdentifier(0, 'block 0'); + const peers = [ + new Types.Peer('peer 1'), + ]; + + return new Types.NetworkStatusResponse( + currentBlockIdentifier, + currentBlockTimestamp, + genesisBlockIdentifier, + peers, + ); +}; + +module.exports = { + /* /network/list */ + networkList, + + /* /network/options */ + networkOptions, + + /* /network/status */ + networkStatus, +}; diff --git a/examples/basic/server/services/index.js b/examples/basic/server/services/index.js new file mode 100644 index 0000000..ff6b33a --- /dev/null +++ b/examples/basic/server/services/index.js @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +const Network = require('./NetworkService'); +const Block = require('./BlockService'); + +module.exports = { + Network, + Block, +}; diff --git a/examples/serverSkeleton/README.md b/examples/serverSkeleton/README.md new file mode 100644 index 0000000..df212f0 --- /dev/null +++ b/examples/serverSkeleton/README.md @@ -0,0 +1,6 @@ +# Rosetta Server Skeleton +This directory demonstrates provides a basic skeleton for Rosetta Servers. + +## Steps +1. Implement each response in `services/*Service.js` +2. Pack your implementation with a Dockerfile providing your backend \ No newline at end of file diff --git a/examples/serverSkeleton/index.js b/examples/serverSkeleton/index.js new file mode 100644 index 0000000..810814c --- /dev/null +++ b/examples/serverSkeleton/index.js @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +const RosettaSDK = require('../../..'); + +const ServiceHandlers = require('./services'); + +/* Create a server configuration */ +const Server = new RosettaSDK.Server({ + URL_PORT: 8080, +}); + +/* Data API: Network */ +Server.register('/network/list', ServiceHandlers.Network.networkList); +Server.register('/network/options', ServiceHandlers.Network.networkOptions); +Server.register('/network/status', ServiceHandlers.Network.networkStatus); + +/* Data API: Block */ +Server.register('/block', ServiceHandlers.Block.block); +Server.register('/block/transaction', ServiceHandlers.Block.blockTransaction); + +/* Data API: Account */ +Server.register('/account/balance', ServiceHandlers.Account.balance); + +/* Data API: Mempool */ +Server.register('/mempool', ServiceHandlers.Mempool.mempool); +Server.register('/mempool/transaction', ServiceHandlers.Mempool.mempoolTransaction); + +/* Data API: Construction */ +Server.register('/construction/metadata', ServiceHandlers.Construction.constructioMetadata); +Server.register('/construction/submit', ServiceHandlers.Construction.constructionSubmit); diff --git a/examples/serverSkeleton/services/AccountService.js b/examples/serverSkeleton/services/AccountService.js new file mode 100644 index 0000000..5ebfeed --- /dev/null +++ b/examples/serverSkeleton/services/AccountService.js @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +const RosettaSDK = require('../../..'); + +/* Data API: Account */ + +/** +* Get an Account Balance +* Get an array of all Account Balances for an Account Identifier and the Block Identifier at which the balance lookup was performed. Some consumers of account balance data need to know at which block the balance was calculated to reconcile account balance changes. To get all balances associated with an account, it may be necessary to perform multiple balance requests with unique Account Identifiers. If the client supports it, passing nil AccountIdentifier metadata to the request should fetch all balances (if applicable). It is also possible to perform a historical balance lookup (if the server supports it) by passing in an optional BlockIdentifier. +* +* accountBalanceRequest AccountBalanceRequest +* returns AccountBalanceResponse +* */ +const balance = async (params) => { + const { accountBalanceRequest } = params; + return {}; +}; + +module.exports = { + /* /account/balance */ + balance, +}; diff --git a/examples/serverSkeleton/services/BlockService.js b/examples/serverSkeleton/services/BlockService.js new file mode 100644 index 0000000..0514bee --- /dev/null +++ b/examples/serverSkeleton/services/BlockService.js @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +const RosettaSDK = require('../../../..'); + +/* Data API: Block */ + +/** +* Get a Block +* Get a block by its Block Identifier. If transactions are returned in the same call to the node as fetching the block, the response should include these transactions in the Block object. If not, an array of Transaction Identifiers should be returned so /block/transaction fetches can be done to get all transaction information. +* +* blockRequest BlockRequest +* returns BlockResponse +* */ +const block = async (params) => { + const { blockRequest } = params; + return {}; +}; + +/** +* Get a Block Transaction +* Get a transaction in a block by its Transaction Identifier. This endpoint should only be used when querying a node for a block does not return all transactions contained within it. All transactions returned by this endpoint must be appended to any transactions returned by the /block method by consumers of this data. Fetching a transaction by hash is considered an Explorer Method (which is classified under the Future Work section). Calling this endpoint requires reference to a BlockIdentifier because transaction parsing can change depending on which block contains the transaction. For example, in Bitcoin it is necessary to know which block contains a transaction to determine the destination of fee payments. Without specifying a block identifier, the node would have to infer which block to use (which could change during a re-org). Implementations that require fetching previous transactions to populate the response (ex: Previous UTXOs in Bitcoin) may find it useful to run a cache within the Rosetta server in the /data directory (on a path that does not conflict with the node). +* +* blockTransactionRequest BlockTransactionRequest +* returns BlockTransactionResponse +* */ +const blockTransaction = async (params) => { + const { blockTransactionRequest } = params; + return {}; +}; + +module.exports = { + /* /block */ + block, + + /* /block/transaction */ + blockTransaction, +}; diff --git a/examples/serverSkeleton/services/ConstructionService.js b/examples/serverSkeleton/services/ConstructionService.js new file mode 100644 index 0000000..b5f4618 --- /dev/null +++ b/examples/serverSkeleton/services/ConstructionService.js @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +const RosettaSDK = require('../../../..'); + +/* Data API: Construction */ + +/** +* Get Transaction Construction Metadata +* Get any information required to construct a transaction for a specific network. Metadata returned here could be a recent hash to use, an account sequence number, or even arbitrary chain state. It is up to the client to correctly populate the options object with any network-specific details to ensure the correct metadata is retrieved. It is important to clarify that this endpoint should not pre-construct any transactions for the client (this should happen in the SDK). This endpoint is left purposely unstructured because of the wide scope of metadata that could be required. In a future version of the spec, we plan to pass an array of Rosetta Operations to specify which metadata should be received and to create a transaction in an accompanying SDK. This will help to insulate the client from chain-specific details that are currently required here. +* +* constructionMetadataRequest ConstructionMetadataRequest +* returns ConstructionMetadataResponse +* */ +const constructionMetadata = async (params) => { + const { constructionMetadataRequest } = params; + return {}; +}; + +/** +* Submit a Signed Transaction +* Submit a pre-signed transaction to the node. This call should not block on the transaction being included in a block. Rather, it should return immediately with an indication of whether or not the transaction was included in the mempool. The transaction submission response should only return a 200 status if the submitted transaction could be included in the mempool. Otherwise, it should return an error. +* +* constructionSubmitRequest ConstructionSubmitRequest +* returns ConstructionSubmitResponse +* */ +const constructionSubmit = async (params) => { + const { constructionSubmitRequest } = params; + return {}; +}; + +module.exports = { + /* /construction/metadata */ + constructionMetadata, + + /* /construction/submit */ + constructionSubmit, +}; diff --git a/examples/serverSkeleton/services/MempoolService.js b/examples/serverSkeleton/services/MempoolService.js new file mode 100644 index 0000000..176cc57 --- /dev/null +++ b/examples/serverSkeleton/services/MempoolService.js @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +const RosettaSDK = require('../../../..'); + +/* Data API: Mempool */ + +/** +* Get All Mempool Transactions +* Get all Transaction Identifiers in the mempool +* +* mempoolRequest MempoolRequest +* returns MempoolResponse +* */ +const mempool = async (params) => { + const { mempoolRequest } = params; + return {}; +}; + +/** +* Get a Mempool Transaction +* Get a transaction in the mempool by its Transaction Identifier. This is a separate request than fetching a block transaction (/block/transaction) because some blockchain nodes need to know that a transaction query is for something in the mempool instead of a transaction in a block. Transactions may not be fully parsable until they are in a block (ex: may not be possible to determine the fee to pay before a transaction is executed). On this endpoint, it is ok that returned transactions are only estimates of what may actually be included in a block. +* +* mempoolTransactionRequest MempoolTransactionRequest +* returns MempoolTransactionResponse +* */ +const mempoolTransaction = async (params) => { + const { mempoolTransactionRequest } = params; + return {}; +}; + +module.exports = { + /* /mempool */ + mempool, + + /* /mempool/transaction */ + mempoolTransaction, +}; diff --git a/examples/serverSkeleton/services/NetworkService.js b/examples/serverSkeleton/services/NetworkService.js new file mode 100644 index 0000000..9574817 --- /dev/null +++ b/examples/serverSkeleton/services/NetworkService.js @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +const RosettaSDK = require('../../../..'); + + +/* Data API: Network */ + +/** +* Get List of Available Networks +* This endpoint returns a list of NetworkIdentifiers that the Rosetta server can handle. +* +* metadataRequest MetadataRequest +* returns NetworkListResponse +* */ +const networkList = async (params) => { + const { metadataRequest } = params; + return {}; +}; + +/** +* Get Network Options +* This endpoint returns the version information and allowed network-specific types for a NetworkIdentifier. Any NetworkIdentifier returned by /network/list should be accessible here. Because options are retrievable in the context of a NetworkIdentifier, it is possible to define unique options for each network. +* +* networkRequest NetworkRequest +* returns NetworkOptionsResponse +* */ +const networkOptions = async (params) => { + const { networkRequest } = params; + return {}; +}; + +/** +* Get Network Status +* This endpoint returns the current status of the network requested. Any NetworkIdentifier returned by /network/list should be accessible here. +* +* networkRequest NetworkRequest +* returns NetworkStatusResponse +* */ +const networkStatus = async (params) => { + const { networkRequest } = params; + return {}; +}; + +module.exports = { + /* /network/list */ + networkList, + + /* /network/options */ + networkOptions, + + /* /network/status */ + networkStatus, +}; diff --git a/examples/serverSkeleton/services/index.js b/examples/serverSkeleton/services/index.js new file mode 100644 index 0000000..79ec9b7 --- /dev/null +++ b/examples/serverSkeleton/services/index.js @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +const Network = require('./NetworkService'); +const Block = require('./BlockService'); +const Account = require('./AccountService'); +const Construction = require('./ConstructionService'); +const Mempool = require('./MempoolService'); + +module.exports = { + Network, + Block, + Account, + Construction, + Mempool, +}; diff --git a/index.js b/index.js new file mode 100644 index 0000000..f5307e7 --- /dev/null +++ b/index.js @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +const logger = require('./lib/logger'); + +const RosettaClient = require('rosetta-node-sdk-client'); +const RosettaFetcher = require('./lib/fetcher'); +const RosettaServer = require('./lib/server'); +const RosettaReconciler = require('./lib/reconciler'); +const RosettaParser = require('./lib/parser'); +const RosettaAsserter = require('./lib/asserter'); +const RosettaControllers = require('./lib/controllers'); + +const RosettaUtils = require('./lib/utils'); +const RosettaInternalModels = require('./lib/models'); + +const Errors = require('./lib/errors'); +const RosettaSyncer = require('./lib/syncer'); +const RosettaSyncerEvents = require('./lib/syncer/events'); + +module.exports = { + Asserter: RosettaAsserter, + Server: RosettaServer, + Reconciler: RosettaReconciler, + Controller: RosettaControllers, + Syncer: RosettaSyncer, + Fetcher: RosettaFetcher, + Client: RosettaClient, + Parser: RosettaParser, + + Utils: RosettaUtils, + InternalModels: RosettaInternalModels, + logger: logger, + + version: '1.3.1', + + RosettaSyncerEvents, + Errors, +}; \ No newline at end of file diff --git a/lib/asserter/index.js b/lib/asserter/index.js new file mode 100644 index 0000000..fd9908f --- /dev/null +++ b/lib/asserter/index.js @@ -0,0 +1,780 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +const fs = require('fs'); + +const { AsserterError } = require('../errors'); +const RosettaClient = require('rosetta-node-sdk-client'); + +const { Hash } = require('../utils'); + +class RosettaAsserter { + constructor({networkIdentifier, operationTypes = [], operationStatuses = [], + errorTypes = [], genesisBlockIdentifier, supportedNetworks = [], historicalBalanceLookup = false} = {}) { + + this.networkIdentifier = networkIdentifier; + this.operationTypes = operationTypes; + this.genesisBlockIdentifier = genesisBlockIdentifier; + this.supportedNetworks = supportedNetworks; + this.historicalBalanceLookup = historicalBalanceLookup; + + this.operationStatusMap = {}; + this.errorTypeMap = {}; + + if (operationStatuses && typeof operationStatuses == 'object' && Array.isArray(operationStatuses)) { + for (const operationStatus of operationStatuses) { + this.operationStatusMap[operationStatus.status] = operationStatus.successful; + } + } + + if (errorTypes && typeof errorTypes == 'object' && Array.isArray(errorTypes)) { + for (const errorType of errorTypes) { + this.errorTypeMap[errorType.code] = errorType; + } + } + } + + SupportedNetworks(supportedNetworks) { + if (!Array.isArray(supportedNetworks)) { + throw new AsserterError('SupportedNetworks must be an array'); + } + + if (supportedNetworks.length == 0) { + throw new AsserterError('NetworkIdentifier Array contains no supported networks'); + } + + const parsedNetworks = []; + + for (let network of supportedNetworks) { + this.NetworkIdentifier(network); + if (parsedNetworks.includes(Hash(network))) { + throw new AsserterError(`SupportedNetwork has a duplicate: ${JSON.stringify(network)}`); + } + + parsedNetworks.push(Hash(network)); + } + } + + SupportedNetwork(networkIdentifier) { + const index = this.supportedNetworks.findIndex(network => + Hash(network) == Hash(networkIdentifier) + ); + + if (index == -1) { + throw new AsserterError(`Network ${JSON.stringify(networkIdentifier)} is not supported`); + } + } + + ValidSupportedNetwork(requestNetwork) { + this.NetworkIdentifier(requestNetwork); + this.SupportedNetwork(requestNetwork); + } + + AccountBalanceRequest(accountBalanceRequest) { + if (accountBalanceRequest == null) { + throw new AsserterError('AccountBalanceRequest is null'); + } + + this.ValidSupportedNetwork(accountBalanceRequest.network_identifier); + this.AccountIdentifier(accountBalanceRequest.account_identifier); + + if (accountBalanceRequest.block_identifier == null) { + return; + } + + if (!this.historicalBalanceLookup) { + throw new AsserterError(`historical balance loopup is not supported`); + } + + this.PartialBlockIdentifier(accountBalanceRequest.block_identifier); + } + + BlockRequest(blockRequest) { + if (blockRequest == null) { + throw new AsserterError('BlockRequest is null'); + } + + this.NetworkIdentifier(blockRequest.network_identifier); + this.SupportedNetwork(blockRequest.network_identifier); + + this.PartialBlockIdentifier(blockRequest.block_identifier); + } + + BlockTransactionRequest(blockTransactionRequest) { + if (blockTransactionRequest == null) { + throw new AsserterError('BlockTransactionRequest is null'); + } + + this.NetworkIdentifier(blockTransactionRequest.network_identifier); + this.SupportedNetwork(blockTransactionRequest.network_identifier); + this.BlockIdentifier(blockTransactionRequest.block_identifier); + this.TransactionIdentifier(blockTransactionRequest.transaction_identifier); + } + + ConstructionMetadataRequest(constructionMetadataRequest) { + if (constructionMetadataRequest == null) { + throw new AsserterError('ConstructionMetadataRequest is null'); + } + + this.NetworkIdentifier(constructionMetadataRequest.network_identifier); + this.SupportedNetwork(constructionMetadataRequest.network_identifier); + + if (constructionMetadataRequest.options == null) { + throw new AsserterError('ConstructionMetadataRequest.options is null'); + } + } + + ConstructionSubmitRequest(constructionSubmitRequest) { + if (constructionSubmitRequest == null) { + throw new AsserterError('ConstructionSubmitRequest.options is null'); + } + + this.NetworkIdentifier(constructionSubmitRequest.network_identifier); + this.SupportedNetwork(constructionSubmitRequest.network_identifier); + + if (!constructionSubmitRequest.signed_transaction) {xr + throw new AsserterError('ConstructionSubmitRequest.signed_transaction is empty'); + } + } + + MempoolRequest(mempoolRequest) { + if (mempoolRequest == null) { + throw new AsserterError('MempoolRequest is null'); + } + + this.NetworkIdentifier(mempoolRequest.network_identifier); + this.SupportedNetwork(mempoolRequest.network_identifier); + } + + MempoolTransactionRequest(mempoolTransactionRequest) { + if (mempoolTransactionRequest == null) { + throw new AsserterError('MempoolTransactionRequest is null'); + } + + this.ValidSupportedNetwork(mempoolTransactionRequest.network_identifier); + this.TransactionIdentifier(mempoolTransactionRequest.transaction_identifier); + } + + MetadataRequest(metadataRequest) { + if (metadataRequest == null) { + throw new AsserterError('MetadataRequest is null'); + } + } + + NetworkRequest(networkRequest) { + if (networkRequest == null) { + throw new AsserterError('NetworkRequest is null'); + } + + this.ValidSupportedNetwork(networkRequest.network_identifier); + } + + ConstructionMetadataResponse(constructionMetadataResponse) { + if (constructionMetadataResponse == null) { + throw new AsserterError('ConstructionMetadataResponse cannot be null'); + } + + if (constructionMetadataResponse.metadata == null) { + throw new AsserterError('ConstructionMetadataResponse.metadata is null'); + } + } + + ConstructionSubmitResponse(constructionSubmitResponse) { + if (constructionSubmitResponse == null) { + throw new AsserterError('ConstructionSubmitResponse cannot be null'); + } + + // Note, this is not in the reference implementation (Go) + this.TransactionIdentifier(constructionSubmitResponse.transaction_identifier); + } + + MempoolTransactions(transactionIdentifiers) { + for (let t of transactionIdentifiers) { + this.TransactionIdentifier(t); + } + } + + NetworkIdentifier(networkIdentifier) { + if (networkIdentifier == null) + throw new AsserterError('NetworkIdentifier is null'); + + if (!networkIdentifier.blockchain) + throw new AsserterError('NetworkIdentifier.blockchain is missing'); + + if (!networkIdentifier.network) + throw new AsserterError('NetworkIdentifier.network is missing'); + + return this.SubNetworkIdentifier(networkIdentifier.sub_network_identifier); + } + + SubNetworkIdentifier(subnetworkIdentifier) { + // Only check if specified in the response. + if (subnetworkIdentifier == null) return; + + if (!subnetworkIdentifier.network) { + throw new AsserterError('NetworkIdentifier.sub_network_identifier.network is missing'); + } + } + + Peer(peer) { + if (peer == null || !peer.peer_id) { + throw new AsserterError('Peer.peer_id is missing'); + } + } + + Version(version) { + if (version == null) { + throw new AsserterError('Version is null'); + } + + if (!version.node_version) { + throw new AsserterError('Version.node_version is missing'); + } + + if (version.middleware_version != null && !version.middleware_version) { + throw new AsserterError('Version.middleware_version is missing'); + } + } + + StringArray(name, array) { + if (!array || array.length == 0) { + throw new AsserterError(`No ${name} found`); + } + + const existing = []; + + for (let element of array) { + if (!element) { + throw new AsserterError(`${name} has an empty string`); + } + + if (existing.includes(element)) { + throw new AsserterError(`${name} contains a duplicate element: ${element}`); + } + + existing.push(element); + } + } + + Timestamp(timestamp = 0) { + if (timestamp < RosettaAsserter.MinUnixEpoch) { + throw new AsserterError(`Timestamp ${timestamp} is before 01/01/2000`); + } else if (timestamp > RosettaAsserter.MaxUnixEpoch) { + throw new AsserterError(`Timestamp ${timestamp} is after 01/01/2040`); + } else { + return null; + } + } + + NetworkStatusResponse(networkStatusResponse) { + if (networkStatusResponse == null) { + throw new AsserterError('networkStatusResponse is null'); + } + + this.BlockIdentifier(networkStatusResponse.current_block_identifier); + this.Timestamp(networkStatusResponse.current_block_timestamp); + this.BlockIdentifier(networkStatusResponse.genesis_block_identifier); + + for (let peer of networkStatusResponse.peers) { + this.Peer(peer); + } + } + + OperationStatuses(operationStatuses) { + if (operationStatuses == null || operationStatuses.length == 0) { + throw new AsserterError('No Allow.operation_statuses found'); + } + + const existingStatuses = []; + let foundSuccessful = false; + + for (let status of operationStatuses) { + if (!status.status) { + throw new AsserterError('Operation.status is missing'); + } + + if (status.successful) { + foundSuccessful = true; + } + + existingStatuses.push(status.status); + } + + if (!foundSuccessful) { + throw new AsserterError('No successful Allow.operation_statuses found'); + } + + return this.StringArray("Allow.operation_statuses", existingStatuses); + } + + OperationTypes(types) { + return this.StringArray('Allow.operation_statuses', types); + } + + Error(error) { + if (error == null) { + throw new AsserterError('Error is null'); + } + + if (error.code < 0) { + throw new AsserterError('Error.code is negative'); + } + + if (!error.message) { + throw new AsserterError('Error.message is missing'); + } + } + + Errors(rosettaErrors = []) { + const statusCodeMap = {}; + + for (let rosettaError of rosettaErrors) { + this.Error(rosettaError); + + if (statusCodeMap[rosettaError.code] != null) { + throw new AsserterError('Error code used multiple times'); + } + + statusCodeMap[rosettaError.code] = true; + } + } + + Allow(allowed) { + if (allowed == null) { + throw new AsserterError('Allow is null'); + } + + this.OperationStatuses(allowed.operation_statuses); + this.OperationTypes(allowed.operation_types); + this.Errors(allowed.errors); + } + + NetworkOptionsResponse(networkOptionsResponse) { + if (networkOptionsResponse == null) { + throw new AsserterError('NetworkOptions Response is null'); + } + + this.Version(networkOptionsResponse.version); + return this.Allow(networkOptionsResponse.allow); + } + + containsNetworkIdentifier(networks, network) { + const networkHash = Hash(network); + const index = networks.findIndex((n) => Hash(n) == networkHash); + return index >= 0; + } + + NetworkListResponse(networkListResponse) { + if (networkListResponse == null) { + throw new AsserterError('NetworkListResponse is null'); + } + + const existingNetworks = []; + + for (let network of networkListResponse.network_identifiers) { + this.NetworkIdentifier(network); + if (this.containsNetworkIdentifier(existingNetworks, network)) { + throw new AsserterError('NetworkListResponse.Network contains duplicated'); + } + + existingNetworks.push(network); + } + } + + containsCurrency(currencies, currency) { + let currencyIndex = currencies.findIndex((a) => + Hash(a) == Hash(currency)); + + return currencyIndex >= 0; + } + + assertBalanceAmounts(amountsArray) { + const currencies = []; + + for (let amount of amountsArray) { + let containsCurrency = this.containsCurrency(currencies, amount.currency); + + if (containsCurrency) { + throw new AsserterError(`Currency ${amount.currency.symbol} used in balance multiple times`); + } + + currencies.push(amount.currency); + this.Amount(amount); + } + } + + Amount(amount) { + if (amount == null || amount.value == '') { + throw new AsserterError(`Amount.value is missing`); + + } + + // Allow all numbers, except e notation, or negative numbers. + if (!/^-?[0-9]+$/.test(amount.value)) { + throw new AsserterError(`Amount.value is not an integer: ${amount.value}`); + } + + if (amount.currency == null) { + throw new AsserterError('Amount.currency is null'); + } + + if (!amount.currency.symbol) { + throw new AsserterError('Amount.currency does not have a symbol'); + } + + if (amount.currency.decimals < 0) { + throw new AsserterError(`Amount.currency.decimals must be positive. Found: ${amount.currency.decimals}`); + } + } + + AccountBalanceResponse(partialBlockIdentifier, blockIdentifier, amountArray) { + this.BlockIdentifier(blockIdentifier); + this.assertBalanceAmounts(amountArray); + + if (partialBlockIdentifier == null) { + return; + } + + if (partialBlockIdentifier.hash != null && partialBlockIdentifier.hash != blockIdentifier.hash) { + throw new AsserterError(`Request BlockHash ${partialBlockIdentifier.hash}` + + ` does not match Response block hash ${blockIdentifier.hash}`); + } + + if (partialBlockIdentifier.index != null && partialBlockIdentifier.index != blockIdentifier.index) { + throw new AsserterError(`Request Index ${partialBlockIdentifier.index}` + + ` does not match Response block index ${blockIdentifier.index}`); + } + } + + OperationIdentifier(operationIdentifier, index) { + if (typeof index !== 'number') { + throw new AsserterError('OperationIdentifier: index must be a number'); + } + + if (operationIdentifier == null) { + throw new AsserterError('OperationIdentifier is null'); + } + + if (operationIdentifier.index != index) { + throw new AsserterError(`OperationIdentifier.index ${operationIdentifier.index} is out of order, expected ${index}`); + } + + if (operationIdentifier.network_index != null && operationIdentifier.network_index < 0) { + throw new AsserterError('OperationIdentifier.network_index is invalid'); + } + } + + AccountIdentifier(accountIdentifier) { + if (accountIdentifier == null) { + throw new AsserterError('Account is null'); + } + + if (!accountIdentifier.address) { + throw new AsserterError('Account.address is missing'); + } + + if (accountIdentifier.sub_account == null) { + return; + } + + if (!accountIdentifier.sub_account.address) { + throw new AsserterError('Account.sub_account.address is missing'); + } + } + + OperationStatus(status) { + if (status == null) { + throw new AsserterError('Asserter not initialized'); + } + + if (typeof status !== 'string') { + throw new AsserterError('OperationStatus.status must be a string'); + } + + if (status == '') { + throw new AsserterError('OperationStatus.status is empty'); + } + + if (this.operationStatusMap[status] == null) { + throw new AsserterError(`OperationStatus.status ${status} is not valid within this Asserter`); + } + } + + OperationType(type) { + if (typeof type !== 'string') { + throw new AsserterError('OperationStatus.type must be a string'); + } + + if (type == '' || !this.operationTypes.includes(type)) { + throw new AsserterError(`Operation.type ${type} is invalid`); + } + } + + Operation(operation, index, construction = false) { + if (operation == null) { + throw new AsserterError('Operation is null'); + } + + this.OperationIdentifier(operation.operation_identifier, index); + this.OperationType(operation.type); + + if (construction) { + if (operation.status && operation.status.length > 0) { + throw new AsserterError('Operation.status must be empty for construction'); + } + } else { + this.OperationStatus(operation.status); + } + + if (operation.amount == null) { + return null; + } + + this.AccountIdentifier(operation.account); + this.Amount(operation.amount); + } + + BlockIdentifier(blockIdentifier) { + if (blockIdentifier == null) { + throw new AsserterError('BlockIdentifier is null'); + } + + if (!blockIdentifier.hash) { + throw new AsserterError('BlockIdentifier.hash is missing'); + } + + if (blockIdentifier.index < 0) { + throw new AsserterError('BlockIdentifier.index is negative'); + } + } + + PartialBlockIdentifier(partialBlockIdentifier) { + if (partialBlockIdentifier == null) { + throw new AsserterError('PartialBlockIdentifier is null'); + } + + if (!!partialBlockIdentifier.hash) { + return null; + } + + if (partialBlockIdentifier.index != null && partialBlockIdentifier.index >= 0) { + return null; + } + + throw new AsserterError('Neither PartialBlockIdentifier.hash nor PartialBlockIdentifier.index is set'); + } + + TransactionIdentifier(transactionIdentifier) { + if (transactionIdentifier == null) { + throw new AsserterError('TransactionIdentifier is null'); + } + + if (!transactionIdentifier.hash) { + throw new AsserterError('TransactionIdentifier.hash is missing'); + } + } + + Transaction(transaction) { + if (transaction == null) { + } + + this.TransactionIdentifier(transaction.transaction_identifier); + + if (!Array.isArray(transaction.operations)) { + throw new AsserterError('Transaction.operations must be an array'); + } + + for (let i = 0; i < transaction.operations.length; ++i) { + const operation = transaction.operations[i]; + this.Operation(operation, i); + + const relatedIndices = []; + + if (!operation.related_operations) continue; + + for (let relatedOperation of operation.related_operations) { + if (relatedOperation.index >= operation.operation_identifier.index) { + throw new AsserterError(`Related operation index ${relatedOperation.index}` + + ` >= operation index ${operation.operation_identifier.index}`); + } + + if (relatedIndices.includes(relatedOperation.index)) { + throw new AsserterError(`Found duplicate related operation index`+ + ` ${relatedOperation.index} for operation index ${operation.operation_identifier.index}`); + } + + relatedIndices.push(relatedOperation.index); + } + } + } + + Block(block) { + if (block == null) { + throw new AsserterError('Block is null'); + } + + this.BlockIdentifier(block.block_identifier); + this.BlockIdentifier(block.parent_block_identifier); + + if (this.genesisBlockIdentifier.index != block.block_identifier.index) { + if (block.block_identifier.hash == block.parent_block_identifier.hash) { + throw new AsserterError('BlockIdentifier.hash == ParentBlockIdentifier.hash'); + } + + if (block.block_identifier.index <= block.parent_block_identifier.index) { + throw new AsserterError('BlockIdentifier.index <= ParentBlockIdentifier.index'); + } + + this.Timestamp(block.timestamp); + } + + for (let transaction of block.transactions) { + this.Transaction(transaction); + } + } + + static NewServer(supportedOperationTypes, historicalBalanceLookup, supportedNetworks) { + const tmp = new RosettaAsserter(); // ToDo: alter methods to static + + tmp.OperationTypes(supportedOperationTypes); + tmp.SupportedNetworks(supportedNetworks); + + return new RosettaAsserter({ + supportedNetworks, + historicalBalanceLookup, + operationTypes: supportedOperationTypes, + }); + } + + static NewClientWithFile(filePath) { + const buffer = fs.readFileSync(filePath); + const contents = buffer.toString(); + const json = JSON.parse(contents); + + return RosettaAsserter.NewClientWithOptions( + json.network_identifier, + json.genesis_block_identifier, + json.allowed_operation_types, + json.allowed_operation_statuses, + json.allowed_errors, + ); + } + + static NewClientWithResponses(networkIdentifier, networkStatus, networkOptions) { + const tmp = new RosettaAsserter(); + + tmp.NetworkIdentifier(networkIdentifier); + tmp.NetworkStatusResponse(networkStatus); + tmp.NetworkOptionsResponse(networkOptions); + + return RosettaAsserter.NewClientWithOptions( + networkIdentifier, + networkStatus.genesis_block_identifier, + networkOptions.allow.operation_types, + networkOptions.allow.operation_statuses, + networkOptions.allow.errors, + ); + } + + OperationSuccessful(operation) { + const status = this.operationStatusMap[operation.status]; + + if (status == null) { + throw new AsserterError(`${operation.status} not found in possible statuses`); + } + + return status; + } + + getClientConfiguration() { + const operationStatuses = []; + const errors = []; + + for (let key of Object.keys(this.operationStatusMap)) { + const value = this.operationStatusMap[key]; + const operationStatus = new RosettaClient.OperationStatus(key, value); + + // Validate + // this.OperationStatus(operationStatus); + + operationStatuses.push(operationStatus); + } + + for (let key of Object.keys(this.errorTypeMap)) { + const value = this.errorTypeMap[key]; + errors.push(value); + } + + const ret = { + network_identifier: this.networkIdentifier, + genesis_block_identifier: this.genesisBlockIdentifier, + allowed_operation_types: this.operationTypes, + allowed_operation_statuses: operationStatuses, + allowed_errors: errors, + }; + + return ret; + } + + static NewClientWithOptions(networkIdentifier, genesisBlockIdentifier, + operationTypes, operationStatuses = [], errors = []) { + + const tmp = new RosettaAsserter(); + + tmp.NetworkIdentifier(networkIdentifier); + tmp.BlockIdentifier(genesisBlockIdentifier); + tmp.OperationStatuses(operationStatuses); + tmp.OperationTypes(operationTypes); + + const r = new RosettaAsserter({ + networkIdentifier, + operationTypes, + genesisBlockIdentifier: genesisBlockIdentifier, + }); + + r.errorTypeMap = (() => { + const ret = {}; + + for (let error of errors) { + ret[error.code] = error; + } + + return ret; + })(); + + r.operationStatusMap = (() => { + const ret = {}; + + for (let status of operationStatuses) { + ret[status.status] = status.successful; + } + + return ret; + })(); + + return r; + } +} + +RosettaAsserter.MinUnixEpoch = 946713600000; // 01/01/2000 at 12:00:00 AM. +RosettaAsserter.MaxUnixEpoch = 2209017600000; // 01/01/2040 at 12:00:00 AM. + +module.exports = RosettaAsserter; \ No newline at end of file diff --git a/lib/controllers/AccountController.js b/lib/controllers/AccountController.js new file mode 100644 index 0000000..ca95a25 --- /dev/null +++ b/lib/controllers/AccountController.js @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +/** + * The AccountController file is a very simple one, which does not need to be changed manually, + * unless there's a case where business logic reoutes the request to an entity which is not + * the service. + * The heavy lifting of the Controller item is done in Request.js - that is where request + * parameters are extracted and sent to the service, and where response is handled. + */ + +const Controller = require('./Controller'); + +const accountBalance = async (request, response) => { + await Controller.handleRequest(request, response); +}; + +module.exports = { + accountBalance, +}; diff --git a/lib/controllers/BlockController.js b/lib/controllers/BlockController.js new file mode 100644 index 0000000..d1a7f72 --- /dev/null +++ b/lib/controllers/BlockController.js @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +/** + * The BlockController file is a very simple one, which does not need to be changed manually, + * unless there's a case where business logic reoutes the request to an entity which is not + * the service. + * The heavy lifting of the Controller item is done in Request.js - that is where request + * parameters are extracted and sent to the service, and where response is handled. + */ + +const Controller = require('./Controller'); + +const block = async (request, response) => { + await Controller.handleRequest(request, response); +}; + +const blockTransaction = async (request, response) => { + await Controller.handleRequest(request, response); +}; + +module.exports = { + block, + blockTransaction, +}; diff --git a/lib/controllers/CallHandler.js b/lib/controllers/CallHandler.js new file mode 100644 index 0000000..dcbb914 --- /dev/null +++ b/lib/controllers/CallHandler.js @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +/* + CallHandler calls registered service handlers (if existing). + Must be called with .bind(expressApp) +*/ + +const CallAsserter = async function (asserter, className, params) { + if (!asserter || !className || !params) return; + + const validationFunc = asserter[className]; + if (!validationFunc) { + console.log(`No validation func ${className} found`); + return; + } + + return validationFunc.bind(asserter)(params); +} + +const CallHandler = async function (route, args) { + const app = this; + const routeHandlers = app.routeHandlers; + + const data = routeHandlers[route]; + if (!data || !data.handler) { + throw new Error(`Service for ${route} not implemented`); + } + + // Each route can have a specific asserter. + // If a specific asserter was not set, the global asserter will + // be used to validate requests, if set. + const asserter = data.asserter || app.asserter; + + // Retrieve the modelName that was set by the Controller (collectRequestParams). + // Also retrieve the request POST args using the modelName. + const modelName = args.params.class; + const requestParamsKey = args.params.requestParamsKey; + const requestParams = args.params[requestParamsKey]; + + // Try to call the asserter. + await CallAsserter(asserter, modelName, requestParams); + + return await data.handler(args.params); +}; + +module.exports = CallHandler; diff --git a/lib/controllers/ConstructionController.js b/lib/controllers/ConstructionController.js new file mode 100644 index 0000000..b33150d --- /dev/null +++ b/lib/controllers/ConstructionController.js @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +/** + * The ConstructionController file is a very simple one, which does not need to be changed manually, + * unless there's a case where business logic reoutes the request to an entity which is not + * the service. + * The heavy lifting of the Controller item is done in Request.js - that is where request + * parameters are extracted and sent to the service, and where response is handled. + */ + +const Controller = require('./Controller'); + +const constructionMetadata = async (request, response) => { + await Controller.handleRequest(request, response); +}; + +const constructionSubmit = async (request, response) => { + await Controller.handleRequest(request, response); +}; + +module.exports = { + constructionMetadata, + constructionSubmit, +}; diff --git a/lib/controllers/Controller.js b/lib/controllers/Controller.js new file mode 100644 index 0000000..59d7c09 --- /dev/null +++ b/lib/controllers/Controller.js @@ -0,0 +1,224 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +const fs = require('fs'); +const path = require('path'); +const config = require('../../config/default'); +const logger = require('../logger'); + +const CallHandler = require('./CallHandler'); + +class Controller { + static sendResponse(response, payload, beautify = false) { + /** + * The default response-code is 200. We want to allow to change that. in That case, + * payload will be an object consisting of a code and a payload. If not customized + * send 200 and the payload as received in this method. + */ + response.status(payload.code || 200); + const responsePayload = payload.payload !== undefined ? payload.payload : payload; + if (responsePayload instanceof Object) { + if (beautify) { + const json = JSON.stringify(responsePayload, null, 4); + response.end(json); + } else { + response.json(responsePayload, null, 4); + } + } else { + response.end(responsePayload); + } + } + + static sendError(response, error) { + console.error(error); + + const errResponse = { + code: error.code, + message: error.error || error.message, + retriable: error.retriable || false, + details: error.details, + }; + + const serialized = JSON.stringify(errResponse, null, 4); + + response.status(500); + response.send(serialized); + response.end(); + } + + /** + * Files have been uploaded to the directory defined by config.js as upload directory + * Files have a temporary name, that was saved as 'filename' of the file object that is + * referenced in reuquest.files array. + * This method finds the file and changes it to the file name that was originally called + * when it was uploaded. To prevent files from being overwritten, a timestamp is added between + * the filename and its extension + * @param request + * @param fieldName + * @returns {string} + */ + static collectFile(request, fieldName) { + let uploadedFileName = ''; + if (request.files && request.files.length > 0) { + const fileObject = request.files.find(file => file.fieldname === fieldName); + if (fileObject) { + const fileArray = fileObject.originalname.split('.'); + const extension = fileArray.pop(); + fileArray.push(`_${Date.now()}`); + uploadedFileName = `${fileArray.join('')}.${extension}`; + fs.renameSync(path.join(config.FILE_UPLOAD_PATH, fileObject.filename), + path.join(config.FILE_UPLOAD_PATH, uploadedFileName)); + } + } + return uploadedFileName; + } + + // static collectFiles(request) { + // logger.info('Checking if files are expected in schema'); + // const requestFiles = {}; + // if (request.openapi.schema.requestBody !== undefined) { + // const [contentType] = request.headers['content-type'].split(';'); + // if (contentType === 'multipart/form-data') { + // const contentSchema = request.openapi.schema.requestBody.content[contentType].schema; + // Object.entries(contentSchema.properties).forEach(([name, property]) => { + // if (property.type === 'string' && ['binary', 'base64'].indexOf(property.format) > -1) { + // const fileObject = request.files.find(file => file.fieldname === name); + // const fileArray = fileObject.originalname.split('.'); + // const extension = fileArray.pop(); + // fileArray.push(`_${Date.now()}`); + // const uploadedFileName = `${fileArray.join('')}.${extension}`; + // fs.renameSync(path.join(config.FILE_UPLOAD_PATH, fileObject.filename), + // path.join(config.FILE_UPLOAD_PATH, uploadedFileName)); + // requestFiles[name] = uploadedFileName; + // } + // }); + // } else if (request.openapi.schema.requestBody.content[contentType] !== undefined + // && request.files !== undefined) { + // [request.body] = request.files; + // } + // } + // return requestFiles; + // } + + /** + * Extracts the given schema model name. + * input: { $ref: '#components/scope/ModelName' } + * output: ModelName (if lcFirstChar == false) + * output: modelName (if lcFirstChar == true) + **/ + static extractModelName(schema, lcFirstChar = true) { + const index = schema.$ref.lastIndexOf('/'); + + if (index == -1) { + console.warn(`${schema.$ref} did not have the expected format.`); + return schema.$ref; + } + + const lastPart = schema.$ref.substr(index + 1); + if (!lcFirstChar) return lastPart; + + return lastPart.charAt(0).toLowerCase() + lastPart.slice(1); + } + + static collectRequestParams(request) { + const requestParams = {}; + if (request.openapi.schema.requestBody !== undefined) { + const { content } = request.openapi.schema.requestBody; + if (content['application/json'] !== undefined) { + const schema = request.openapi.schema.requestBody.content['application/json'].schema; + + if (schema.$ref) { + let modelName = Controller.extractModelName(schema); + requestParams[modelName] = request.body; + requestParams['class'] = Controller.extractModelName(schema, false); + requestParams['requestParamsKey'] = modelName; + + } else { + requestParams.body = request.body; + } + } else if (content['multipart/form-data'] !== undefined) { + Object.keys(content['multipart/form-data'].schema.properties).forEach( + (property) => { + const propertyObject = content['multipart/form-data'].schema.properties[property]; + if (propertyObject.format !== undefined && propertyObject.format === 'binary') { + requestParams[property] = this.collectFile(request, property); + } else { + requestParams[property] = request.body[property]; + } + }, + ); + } + } + // if (request.openapi.schema.requestBody.content['application/json'] !== undefined) { + // const schema = request.openapi.schema.requestBody.content['application/json']; + // if (schema.$ref) { + // requestParams[schema.$ref.substr(schema.$ref.lastIndexOf('.'))] = request.body; + // } else { + // requestParams.body = request.body; + // } + // } + request.openapi.schema.parameters.forEach((param) => { + if (param.in === 'path') { + requestParams[param.name] = request.openapi.pathParams[param.name]; + } else if (param.in === 'query') { + requestParams[param.name] = request.query[param.name]; + } else if (param.in === 'header') { + requestParams[param.name] = request.headers[param.name]; + } + }); + return requestParams; + } + + static rejectResponse(error, code = 500) { + return { error, code }; + } + + static successResponse(payload, code = 200) { + return { payload, code }; + } + + static async handleRequest(request, response) { + try { + const params = { + params: this.collectRequestParams(request), + request, + response, + }; + + const app = request.app; + const route = request.route.path; + + // Call the registered service handler + const content = await CallHandler.bind(app)(route, params); + + // Internally wrap the service handler response in a success message + const wrapped = Controller.successResponse(content); + + // And finalize the response (json) + Controller.sendResponse(response, wrapped, app.config.BEAUTIFY_JSON); + + } catch (error) { + Controller.sendError(response, error); + } + } +} + +module.exports = Controller; diff --git a/lib/controllers/MempoolController.js b/lib/controllers/MempoolController.js new file mode 100644 index 0000000..679e043 --- /dev/null +++ b/lib/controllers/MempoolController.js @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +/** + * The MempoolController file is a very simple one, which does not need to be changed manually, + * unless there's a case where business logic reoutes the request to an entity which is not + * the service. + * The heavy lifting of the Controller item is done in Request.js - that is where request + * parameters are extracted and sent to the service, and where response is handled. + */ + +const Controller = require('./Controller'); + +const mempool = async (request, response) => { + await Controller.handleRequest(request, response); +}; + +const mempoolTransaction = async (request, response) => { + await Controller.handleRequest(request, response); +}; + +module.exports = { + mempool, + mempoolTransaction, +}; diff --git a/lib/controllers/NetworkController.js b/lib/controllers/NetworkController.js new file mode 100644 index 0000000..7d8c1d6 --- /dev/null +++ b/lib/controllers/NetworkController.js @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +/** + * The NetworkController file is a very simple one, which does not need to be changed manually, + * unless there's a case where business logic reoutes the request to an entity which is not + * the service. + * The heavy lifting of the Controller item is done in Request.js - that is where request + * parameters are extracted and sent to the service, and where response is handled. + */ + +const Controller = require('./Controller'); + +const networkList = async (request, response) => { + await Controller.handleRequest(request, response); +}; + +const networkOptions = async (request, response) => { + await Controller.handleRequest(request, response); +}; + +const networkStatus = async (request, response) => { + await Controller.handleRequest(request, response); +}; + +module.exports = { + networkList, + networkOptions, + networkStatus, +}; diff --git a/lib/controllers/index.js b/lib/controllers/index.js new file mode 100644 index 0000000..1093422 --- /dev/null +++ b/lib/controllers/index.js @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +const AccountController = require('./AccountController'); +const BlockController = require('./BlockController'); +const ConstructionController = require('./ConstructionController'); +const MempoolController = require('./MempoolController'); +const NetworkController = require('./NetworkController'); + +module.exports = { + AccountController, + BlockController, + ConstructionController, + MempoolController, + NetworkController, +}; diff --git a/lib/errors/AsserterError.js b/lib/errors/AsserterError.js new file mode 100644 index 0000000..cceb5ec --- /dev/null +++ b/lib/errors/AsserterError.js @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +// AsserterError.js + +class AsserterError extends Error { + constructor(message) { + super(message); + this.name = 'AsserterError'; + } +} +module.exports = AsserterError; \ No newline at end of file diff --git a/lib/errors/FetcherError.js b/lib/errors/FetcherError.js new file mode 100644 index 0000000..2833615 --- /dev/null +++ b/lib/errors/FetcherError.js @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +// FetcherError.js +class FetcherError extends Error { + constructor(message) { + super(message); + this.name = 'FetcherError'; + } +}; +module.exports = FetcherError; \ No newline at end of file diff --git a/lib/errors/InputError.js b/lib/errors/InputError.js new file mode 100644 index 0000000..fbf6f98 --- /dev/null +++ b/lib/errors/InputError.js @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +// InputError.js + +class InputError extends Error { + constructor(message) { + super(message); + this.name = 'InputError'; + } +} +module.exports = InputError; \ No newline at end of file diff --git a/lib/errors/InternalError.js b/lib/errors/InternalError.js new file mode 100644 index 0000000..980ef09 --- /dev/null +++ b/lib/errors/InternalError.js @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +// AsserterError.js + +class InternalError extends Error { + constructor(message) { + super(message); + this.name = 'InternalError'; + } +} +module.exports = InternalError; \ No newline at end of file diff --git a/lib/errors/ParserError.js b/lib/errors/ParserError.js new file mode 100644 index 0000000..3dfb925 --- /dev/null +++ b/lib/errors/ParserError.js @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +// ParserError.js +class ParserError extends Error { + constructor(message) { + super(message); + this.name = 'ParserError'; + } +}; +module.exports = ParserError; \ No newline at end of file diff --git a/lib/errors/ReconcilerError.js b/lib/errors/ReconcilerError.js new file mode 100644 index 0000000..83d449c --- /dev/null +++ b/lib/errors/ReconcilerError.js @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +// ReconcilerError.js +class ReconcilerError extends Error { + constructor(message, type, filename, lineNumber) { + super(message, filename, lineNumber); + this.type = type; + this.name = 'ReconcilerError'; + } +} +module.exports = ReconcilerError; \ No newline at end of file diff --git a/lib/errors/SyncerError.js b/lib/errors/SyncerError.js new file mode 100644 index 0000000..f056af8 --- /dev/null +++ b/lib/errors/SyncerError.js @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +// SyncerError.js +class SyncerError extends Error { + constructor(message) { + super(message); + this.name = 'SyncerError'; + } +}; +module.exports = SyncerError; \ No newline at end of file diff --git a/lib/errors/index.js b/lib/errors/index.js new file mode 100644 index 0000000..262f54b --- /dev/null +++ b/lib/errors/index.js @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +const FetcherError = require('./FetcherError'); +const SyncerError = require('./SyncerError'); +const AsserterError = require('./AsserterError'); +const InputError = require('./InputError'); +const ParserError = require('./ParserError'); +const ReconcilerError = require('./ReconcilerError'); +const InternalError = require('./InternalError'); + +module.exports = { + FetcherError, + SyncerError, + AsserterError, + InputError, + ParserError, + ReconcilerError, + InternalError, +}; \ No newline at end of file diff --git a/lib/fetcher/README.md b/lib/fetcher/README.md new file mode 100644 index 0000000..fcb1221 --- /dev/null +++ b/lib/fetcher/README.md @@ -0,0 +1,70 @@ +# Fetcher + +## Description +The Fetcher package provides a simplified client interface to communicate with a Rosetta server. It also provides automatic retries and concurrent block fetches. All request methods return **Promises**. + +If you want a lower-level interface to communicate with a Rosetta server, check out the Client. + +## How to use? +Make sure to import the library: +``` +const RosettaSDK = require('rosetta-node-sdk'); +``` + +Here is how to create a basic fetcher with the default configuration: +```javascript +/** + * Default configuration: + * API Endpoint: http://localhost:8000 + * Timeout: 5000 + * Requesting resources with at most 10 requests, and no delay. + */ +const fetcher = new RosettaSDK.Fetcher(); +``` + +You may also want to configure the fetcher: +```javascript +const fetcher = new RosettaSDK.Fetcher({ + /* See https://github.com/coveo/exponential-backoff#readme */ + retryOptions: { + delayFirstAttempt: false, + jitter: 'none', + mayDelay: Infinity, + numOfAttempts: 10, + retry: () => true, + startingDelay: 100, + timeMultiple: 2, + }, + + /* You may either pass a custom instance of APIClient */ + apiClient: ApiClientInstance, + + /* Or configure the fetcher directly */ + server: { + protocol = 'https', + host = 'digibyte.one', + port = 8000, + timeout = 10000, + + /* See superagent documentation */ + requestAgent: requestAgentInstance + }, +}); +``` + +Example request using the fetcher instance: +```javascript +const networkRequest = { + blockchain: "blockchain", + network: "network", +}; + +const account = { + address: "address", +}; + +const response = await fetcher.accountBalanceRetry(networkRequest, account); +``` + +## More examples +See [tests](../../test/fetcher.test.js) for detailed examples. diff --git a/lib/fetcher/index.js b/lib/fetcher/index.js new file mode 100644 index 0000000..08844fd --- /dev/null +++ b/lib/fetcher/index.js @@ -0,0 +1,354 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +const RosettaClient = require('rosetta-node-sdk-client'); +const { backOff } = require('exponential-backoff'); + +const PromisePool = require('../utils/PromisePool'); +const { FetcherError } = require('../errors'); + +class RosettaFetcher { + constructor({apiClient, retryOptions = {}, options = {}, server = {}, asserter = null} = {}) { + this.apiClient = apiClient || this.defaultApiClient(server); + + this.backOffOptions = Object.assign({ + delayFirstAttempt: false, + jitter: 'none', + mayDelay: Infinity, + numOfAttempts: 10, + retry: () => true, + startingDelay: 100, + timeMultiple: 2, + }, retryOptions); + + this.options = Object.assign({ + promisePoolSize: 8, + }, options); + + this.asserter = asserter; + } + + async initializeAsserter() { + if (this.asserter) { + throw new FetcherError('Asserter already initialized'); + } + + const networkList = await this.networkListRetry(); + if (networkList.network_identifiers.length == 0) { + throw new FetcherError('No Networks available'); + } + + const primaryNetwork = networkList.network_identifiers[0]; + const networkStatus = await this.networkStatusRetry(primaryNetwork); + const networkOptions = await this.networkOptionsRetry(primaryNetwork); + + this.asserter = RosettaSDK.Asserter.NewClientWithResponses( + primaryNetwork, + networkStatus, + networkOptions, + ); + + return { + primaryNetwork, + networkStatus, + }; + } + + defaultApiClient(options) { + const apiClient = new RosettaClient.ApiClient(); + + const { + protocol = 'http', + host = 'localhost', + port = 8000, + timeout = 5000, + requestAgent, + } = options; + + apiClient.basePath = `${protocol}://${host}:${port}`; + apiClient.timeout = timeout; + apiClient.requestAgent = requestAgent; + + return apiClient; + } + + async accountBalance(networkIdentifier, accountIdentifier, partialBlockIdentifier) { + const accountApi = new RosettaClient.promises.AccountApi(this.apiClient); + + const accountBalanceRequest = new RosettaClient.AccountBalanceRequest( + networkIdentifier, + accountIdentifier, + partialBlockIdentifier + ); + + const response = await accountApi.accountBalance(accountBalanceRequest); + const block = response.block_identifier; + const balances = response.balances; + const metadata = response.metadata; + + // ToDo: assertion + + return { + block: block, + balances: balances, + metadata: metadata, + }; + } + + async accountBalanceRetry(networkIdentifier, accountIdentifier, partialBlockIdentifier, retryOptions = {}) { + const response = await backOff(() => + this.accountBalance(networkIdentifier, accountIdentifier, partialBlockIdentifier), + Object.assign({}, this.backOffOptions, retryOptions), + ); + + return response; + } + + async block(networkIdentifier, blockIdentifier) { + const blockApi = new RosettaClient.promises.BlockApi(this.apiClient); + + const blockRequest = new RosettaClient.BlockRequest(networkIdentifier, blockIdentifier); + const blockResponse = await blockApi.block(blockRequest); + + if (typeof blockResponse.block.transactions === 'undefined') { + delete blockResponse.block.transactions; + } + + if (blockResponse.other_transactions == null || blockResponse.other_transactions.length == 0) { + return blockResponse.block; + } + + const transactions = this.transactions( + networkIdentifier, + blockIdentifier, + blockResponse.other_transactions + ); + + blockResponse.block.transactions = [blockResponse.block.transactions, ...transactions]; + return blockResponse.block; + } + + async transactions(networkIdentifier, blockIdentifier, hashes) { + console.log('hashes', hashes); + + // Resolve other transactions + const promiseArguments = hashes.map((otherTx) => { + return [networkIdentifier, blockIdentifier, otherTx.hash]; + }); + + // Wait for all transactions to be fetched + const transactions = await new PromisePool.create( + this.options.promisePoolSize, + promiseArguments, + this.transaction, + PromisePool.arrayApplier, + ); + + return transactions; + } + + async transaction(networkIdentifier, blockIdentifier, hash) { + const blockApi = new RosettaClient.promises.BlockApi(this.apiClient); + + const transactionIdentifier = new RosettaClient.TransactionIdentifier(hash); + const blockTransactionRequest = new RosettaClient.BlockTransactionRequest( + networkIdentifier, + blockIdentifier, + transactionIdentifier + ); + const transactionResponse = await RosettaClient.blockApi.blockTransaction(blockRequest); + + // ToDo: Client-side type assertion + + return transactionResponse.transaction; + } + + async blockRetry(networkIdentifier, blockIdentifier, retryOptions = {}) { + const response = await backOff(() => + this.block(networkIdentifier, blockIdentifier), + Object.assign({}, this.backOffOptions, retryOptions), + ); + + return response; + } + + /** + * BlockRange fetches blocks from startIndex to endIndex, inclusive. + * A direct path from startIndex to endIndex may not exist in the response, + * if called during a re-org. This case must be handled by any callers. + * @param {NetworkIdentifier} networkIdentifier + * @param {number} startIndex - index from first block + * @param {number} endIndex - index from last block + */ + async blockRange(networkIdentifier, startIndex, endIndex) { + const ret = []; + const promiseArguments = []; + + for (let i = startIndex; i <= endIndex; ++i) { + const partialBlockIdentifier = new RosettaClient.PartialBlockIdentifier({ index: i }); + promiseArguments.push([networkIdentifier, blockIdentifier]); + } + + // Wait for all blocks to be fetched + const blocks = await new PromisePool.create( + this.options.promisePoolSize, + promiseArguments, + this.blockRetry, + PromisePool.arrayApplier, + ); + + return blocks; + } + + async mempool(networkIdentifier) { + const mempoolApi = new RosettaClient.promises.MempoolApi(this.apiClient); + + const mempoolRequest = new RosettaClient.MempoolRequest(networkIdentifier); + + const response = await mempoolApi.mempool(mempoolRequest); + if (response.transaction_identifiers == null || response.transaction_identifiers.length == 0) { + throw new FetcherError('Mempool is empty'); + } + + // ToDo: Assertion + + return response.transaction_identifiers; + } + + async mempoolTransaction(networkIdentifier, transactionIdentifier) { + const mempoolApi = new RosettaClient.promises.MempoolApi(this.apiClient); + + const mempoolTransactionRequest = new RosettaClient.MempoolTransactionRequest( + networkIdentifier, + transactionIdentifier + ); + + const response = new RosettaClient.MempoolTransaction(mempoolTransactionRequest); + + // ToDo: Type Assertion + + return response.transaction; + } + + async networkStatus(networkIdentifier, metadata = {}) { + const networkApi = new RosettaClient.promises.NetworkApi(this.apiClient); + + const networkRequest = new RosettaClient.NetworkRequest.constructFromObject({ + network_identifier: networkIdentifier, + metadata: metadata, + }); + + const networkStatus = await networkApi.networkStatus(networkRequest); + // ToDo: Type Assertion + + return networkStatus; + } + + async networkStatusRetry(networkIdentifier, metadata = {}, retryOptions = {}) { + const response = await backOff(() => + this.networkStatus(networkIdentifier, metadata), + Object.assign({}, this.backOffOptions, retryOptions), + ); + + return response; + } + + async networkList(metadata = {}) { + const networkApi = new RosettaClient.promises.NetworkApi(this.apiClient); + + const metadataRequest = RosettaClient.MetadataRequest.constructFromObject({ + metadata, + }); + + const networkList = await networkApi.networkList(metadataRequest); + + // ToDo: Type Assertion + + return networkList; + } + + async networkListRetry(metadata = {}, retryOptions = {}) { + const response = await backOff(() => + this.networkList(metadata), + Object.assign({}, this.backOffOptions, retryOptions), + ); + + return response; + } + + async networkOptions(networkIdentifier, metadata = {}) { + const networkApi = new RosettaClient.promises.NetworkApi(this.apiClient); + + const networkRequest = new RosettaClient.MetadataRequest.constructFromObject({ + metadata, + }); + + const networkOptions = await networkApi.networkOptions(networkRequest); + + // ToDo: Type Assertion + + return networkOptions; + } + + async networkOptionsRetry(networkIdentifier, metadata = {}, retryOptions = {}) { + const response = await backOff(() => + this.networkList(networkIdentifier, metadata), + Object.assign({}, this.backOffOptions, retryOptions), + ); + + return response; + } + + async constructionMetadata(networkIdentifier, options = {}) { + const constructionApi = new RosettaClient.promises.ConstructionApi(this.apiClient); + + const constructionMetadataRequest = new RosettaClient.ConstructionMetadataRequest( + networkIdentifier, + options, + ); + + const response = await constructionApi.constructionMetadata(constructionMetadataRequest); + + // ToDo: Client-side Type Assertion + + return response.metadata; + } + + async constructionSubmit(networkIdentifier, signedTransaction) { + const constructionApi = new RosettaClient.promises.ConstructionApi(this.apiClient); + + const constructionSubmitRequest = new RosettaClient.ConstructionSubmitRequest( + networkIdentifier, + signedTransaction + ); + + const response = await constructionApi.constructionSubmit(constructionSubmitRequest); + + // ToDo: Client-side Type Assertion + + return { + transactionIdentifier: response.transaction_identifier, + metadata: response.metadata, + }; + } +} + +module.exports = RosettaFetcher; \ No newline at end of file diff --git a/lib/logger.js b/lib/logger.js new file mode 100644 index 0000000..e3fb407 --- /dev/null +++ b/lib/logger.js @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +const { transports, createLogger, format } = require('winston'); + +const logger = createLogger({ + level: 'info', + format: format.combine( + format.timestamp(), + format.json(), + ), + defaultMeta: { service: 'user-service' }, + transports: [ + new transports.Console(), + new transports.File({ filename: 'error.log', level: 'error', timestamp: true }), + new transports.File({ filename: 'combined.log', timestamp: true }), + ], +}); + +if (process.env.NODE_ENV !== 'production') { + logger.add(new transports.Console({ format: format.simple() })); +} + +module.exports = logger; diff --git a/lib/models/AccountDescription.js b/lib/models/AccountDescription.js new file mode 100644 index 0000000..668392a --- /dev/null +++ b/lib/models/AccountDescription.js @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +// AccountDescription.js + +module.exports = class AccountDescription { + constructor({ + exists = false, + sub_account_exists = false, + sub_account_address = '', + sub_account_metadata_keys = [] + }) { + this.exists = exists; + this.sub_account_exists = sub_account_exists; + this.sub_account_address = sub_account_address; + this.sub_account_metadata_keys = sub_account_metadata_keys; + } +}; \ No newline at end of file diff --git a/lib/models/AmountDescription.js b/lib/models/AmountDescription.js new file mode 100644 index 0000000..a6945a3 --- /dev/null +++ b/lib/models/AmountDescription.js @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +// AmountDescription.js + +const Sign = require('./Sign'); + +module.exports = class AmountDescription { + constructor({ + exists = false, + sign = Sign.Any, + currency = null, + }) { + this.exists = exists; + this.sign = new Sign(sign); + this.currency = currency; + } +}; \ No newline at end of file diff --git a/lib/models/Descriptions.js b/lib/models/Descriptions.js new file mode 100644 index 0000000..e44b030 --- /dev/null +++ b/lib/models/Descriptions.js @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +// Descriptions.js + +// Contains a slice of operation descriptions + +module.exports = class Descriptions { + constructor({ + operation_descriptions = [], + equal_amounts = [], + opposite_amounts = [], + equal_addresses = [], + err_unmatched = false, + }) { + this.operation_descriptions = operation_descriptions; + this.equal_amounts = equal_amounts; + this.opposite_amounts = opposite_amounts; + this.equal_addresses = equal_addresses; + this.err_unmatched = err_unmatched; + } +}; \ No newline at end of file diff --git a/lib/models/NetworkIdentifier.js b/lib/models/NetworkIdentifier.js new file mode 100644 index 0000000..5116463 --- /dev/null +++ b/lib/models/NetworkIdentifier.js @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +// NetworkIdentifier.js + +module.exports = class NetworkIdentifier { + constructor() { + this.blockchain = ''; + this.network = ''; + this.sub_network_identifier = { + 'network': '', + 'metadata': { + 'producer': '', + }, + }; // Optional + } +} \ No newline at end of file diff --git a/lib/models/NetworkListResponse.js b/lib/models/NetworkListResponse.js new file mode 100644 index 0000000..65e5a56 --- /dev/null +++ b/lib/models/NetworkListResponse.js @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +// NetworkListResponse.js + +module.exports = class NetworkListResponse { + constructor() { + this.network_identifiers = []; + } +}; \ No newline at end of file diff --git a/lib/models/OperationDescription.js b/lib/models/OperationDescription.js new file mode 100644 index 0000000..26dcbca --- /dev/null +++ b/lib/models/OperationDescription.js @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +// OperationDescription.js + +// OperationDescription is used to describe an operation +module.exports = class OperationDescription { + constructor({ + account, + amount, + metadata = [], + type = '', + allow_repeats = false, + optional = false, + }) { + this.account = account; + this.amount = amount; + this.metadata = metadata; + this.type = type; + this.allow_repeats = allow_repeats; + this.optional = optional; + } +}; \ No newline at end of file diff --git a/lib/models/Sign.js b/lib/models/Sign.js new file mode 100644 index 0000000..ec9ca1c --- /dev/null +++ b/lib/models/Sign.js @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +// Sign.js + +const { InternalError } = require('../errors'); +const { AmountValue } = require('../utils'); + +const ANY = '*'; +const POSITIVE = '+'; +const NEGATIVE = '-'; + +class Sign { + constructor(input) { + if ([ANY, POSITIVE, NEGATIVE].includes(input)) { + this.type = input; + + } else if (typeof input == 'number') { + switch(this.sign(input)) { + case -1: this.type = NEGATIVE; break; + case +1: this.type = POSITIVE; break; + case 0: this.type = POSITIVE; break; + } + + } else { + throw new InternalError(`Sign's constructor doesn't allow '${input}'`); + } + } + + sign(number) { + if (typeof number !== 'number') { + throw new InternalError(`n in sign(n) must be a number`); + } + + if (number > 0) return 1; + if (number < 0) return -1; + return 0; + } + + match(amount) { + if (this.type == ANY) { + return true; + } + + try { + const numeric = AmountValue(amount); + + if (this.type == NEGATIVE && this.sign(numeric) == -1) { + return true; + } + + if (this.type == POSITIVE && this.sign(numeric) == 1) { + return true; + } + } catch (e) { + console.error(`ERROR`, e); + return false; + } + + } + + toString() { + return this.type; + } +} + +Sign.Positive = POSITIVE; +Sign.Negative = NEGATIVE; +Sign.Any = ANY; + +module.exports = Sign; diff --git a/lib/models/index.js b/lib/models/index.js new file mode 100644 index 0000000..9982dd0 --- /dev/null +++ b/lib/models/index.js @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +// models/index.js + +const Descriptions = require('./Descriptions'); +const NetworkIdentifier = require('./NetworkIdentifier'); +const NetworkListResponse = require('./NetworkListResponse'); +const OperationDescription = require('./OperationDescription'); +const AmountDescription = require('./AmountDescription'); +const AccountDescription = require('./AccountDescription'); +const Sign = require('./Sign'); + +module.exports = { + Descriptions, + NetworkIdentifier, + NetworkListResponse, + OperationDescription, + AmountDescription, + AccountDescription, + Sign, +}; \ No newline at end of file diff --git a/lib/parser/index.js b/lib/parser/index.js new file mode 100644 index 0000000..4970fbf --- /dev/null +++ b/lib/parser/index.js @@ -0,0 +1,561 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +// Parser: index.js + +const Logger = require('../logger'); +const RosettaClient = require('rosetta-node-sdk-client'); +const { ParserError } = require('../errors'); + +const { + AddValues, + Hash, + AmountValue, + NegateValue, +} = require('../utils'); + +const { + Sign, +} = require('../models'); + +const ExpectedOppositesLength = 2; + +const EMPTY_OPERATIONS_GROUP = { + type: '', + operations: [], + currencies: [], + nil_amount_present: false, +} + +class Match { + constructor({operations = [], amounts = []} = {}) { + this.operations = operations; + this.amounts = amounts; + } + + first() { + if (this.operations.length > 0) { + return { + operation: this.operations[0], + amount: this.amounts[0], + }; + } + + return { operation: null, amount: null }; + } +} + +class RosettaParser { + constructor({asserter, exemptFunc} = {}) { + this.asserter = asserter; + this.exemptFunc = exemptFunc; + } + + skipOperation(operation) { + const status = this.asserter.OperationSuccessful(operation); + + if (!status) { + return true; + } + + if (operation.account == null) { + return true; + } + + if (operation.amount == null) { + return true; + } + + if (this.exemptFunc && this.exemptFunc(operation)) { + Logger.verbose(`Skipping excempt operation: ${JSON.stringify(operation)}`); + return true; + } + + return false; + } + + balanceChanges(block, blockRemoved) { + const balanceChanges = {}; + + for (let tx of block.transactions) { + for (let op of tx.operations) { + const skip = this.skipOperation(op); + if (skip) { continue; } + + const amount = op.amount; + let blockIdentifier = block.block_identifier; + + if (blockRemoved) { + const negatedValue = NegateValue(amount.value); + amount.value = negatedValue; + blockIdentifier = block.parent_block_identifier; + } + + const serializedAccount = Hash(op.account); + const serializedCurrency = Hash(op.amount.currency); + const key = `${serializedAccount}/${serializedCurrency}`; + + if (balanceChanges[key] == null) { + balanceChanges[key] = { + account_identifier: op.account, + currency: op.amount.currency, + block_identifier: blockIdentifier, + difference: amount.value, + }; + continue; + } + + const val = balanceChanges[key]; + val.difference = AddValues(val.difference, amount.value); + } + } + + // Collect all balance changes and return them. + const changes = []; + for (let changeId of Object.keys(balanceChanges)) { + const change = balanceChanges[changeId]; + changes.push(change); + } + + return changes; + } + + /** + * addOperationToGroup adds an Operation to a OperationGroup. + * + * @param {OperationsGroup} operationsGroup: Holds operations of a certain type (= destination) + * @param {Number} destinationIndex + * @param {[Number]} assignmentsArray + * @param {Operation} operation: The operation that is going to be added. + */ + addOperationToGroup(operationsGroup = EMPTY_OPERATIONS_GROUP, destinationIndex, assignmentsArray = [], operation) { + if (operation.type != operationsGroup.type && operationsGroup.type != '') { + operationsGroup.type = ''; + } + + // Add the operation + operationsGroup.operations.push(operation); + assignmentsArray[operation.operation_identifier.index] = destinationIndex; + + if (operation.amount == null) { + operationsGroup.nil_amount_present = true; + return; + } + + operationsGroup.nil_amount_present = false; + + if (-1 == operationsGroup.currencies.findIndex(curr => + Hash(curr) == Hash(operation.amount.currency), + )) { + operationsGroup.currencies.push(operation.amount.currency); + } + } + + /** + * sortOperationsGroup sorts operations of an operationGroup and returns + * all sorted operations as an array. + * + * @param {Number} operationsCount: Iterate from 0 .. + * @param {Map} operationsGroup: Input OperationsGroup, that will be sorted. + */ + sortOperationsGroup(operationsCount, operationsGroup) { + const sliceGroups = []; + + for (let i = 0; i < operationsCount; ++i) { + const v = operationsGroup[i]; + + if (v == null) { + continue; + } + + v.operations.sort((a, b) => { + return a.operation_identifier.index - + b.operation_identifier.index; + }); + + sliceGroups.push(v); + } + + return sliceGroups; + } + + /** + * Derives Operations from an transaction. + * Must not be called, unless properly validated (asserted for correctness). + * + * @param {Transaction} transaction: Input Transaction + * @return {OperationsGroup} operationsGroup + */ + groupOperations(transaction) { + const ops = transaction.operations || []; + + const opGroups = {}; + const opAssignments = new Array(ops.length).fill(0); + let counter = 0; + + if (ops) { + for (let i = 0; i < ops.length; ++i) { + const op = ops[i]; + + // Create a new group + if (!op.related_operations || op.related_operations.length == 0) { + let key = counter++; + + opGroups[key] = { + type: op.type, + operations: [ + RosettaClient.Operation.constructFromObject(op), + ], + }; + + if (op.amount != null) { + opGroups[key].currencies = [op.amount.currency]; + opGroups[key].nil_amount_present = false; + } else { + opGroups[key].currencies = []; + opGroups[key].nil_amount_present = true; + } + + opAssignments[i] = key; + continue; + } + + // Find groups to merge + const groupsToMerge = []; + for (let relatedOp of (op.related_operations || [])) { + if (!groupsToMerge.includes(opAssignments[relatedOp.index])) { + groupsToMerge.push(opAssignments[relatedOp.index]); + } + } + + // Ensure that they are sorted, so we can merge other groups. + groupsToMerge.sort(); + + const mergedGroupIndex = groupsToMerge[0]; + const mergedGroup = opGroups[mergedGroupIndex]; + + this.addOperationToGroup(mergedGroup, mergedGroupIndex, opAssignments, op); + + for (let otherGroupIndex of groupsToMerge.slice(1)) { + const otherGroup = opGroups[otherGroupIndex]; + + // Add otherGroup ops to mergedGroup + for (let otherOp of otherGroup.operations) { + this.addOperationToGroup(mergedGroup, mergedGroupIndex, opAssignments, otherOp); + } + + delete opGroups[otherGroupIndex]; + } + } + + return this.sortOperationsGroup(ops.length, opGroups); + } + + return this.sortOperationsGroup(0, opGroups); + } + + metadataMatch(metadataDescriptionArray, metadataMap) { + if (metadataDescriptionArray.length == 0) { + return; + } + + for (let req of metadataDescriptionArray) { + const val = metadataMap[req.key]; + + if (!val) { + throw new ParserError(`${req.key} not present in metadata`); + } + + if (typeof val != req.value_kind) { + throw new ParserError(`${req.key} value is not of type ${req.value_kind}`); + } + } + } + + accountMatch(accountDescription, accountIdentifier) { + if (accountDescription == null) { + return; + } + + if (accountIdentifier == null) { + if (accountDescription.exists) { + throw new ParserError(`Account is missing`); + } + return; + } + + if (accountIdentifier.sub_account == null) { + if (accountDescription.sub_account_exists) { + throw new ParserError(`sub_account_identifier is missing`); + } + return; + } + + if (!accountDescription.sub_account_exists) { + throw new ParserError(`sub_account is populated`); + } + + if (accountDescription.sub_account_address.length > 0 && + accountIdentifier.sub_account.address != accountDescription.sub_account_address) { + throw new ParserError(`sub_account_identifier.address is ${accountIdentifier.sub_account.address}` + + ` not ${accountDescription.sub_account_address}`); + } + + try { + this.metadataMatch(accountDescription.sub_account_metadata_keys, accountIdentifier.sub_account.metadata); + } catch (e) { + throw new ParserError(`${e.message}: account metadata keys mismatch`); + } + } + + amountMatch(amountDescription, amount) { + if (amountDescription == null) { + return; + } + + if (amount == null) { + if (amountDescription.exists) { + throw new ParserError(`amount is missing`); + } + + return; + } + + if (!amountDescription.exists) { + throw new ParserError(`amount is populated`); + } + + if (!amountDescription.sign.match(amount)) { + throw new ParserError(`amount sign of ${amount.value} was not ${amountDescription.sign.toString()}`); + } + + if (amountDescription.currency == null) { + return; + } + + if (amount.currency == null || Hash(amount.currency) != Hash(amountDescription.currency)) { + throw new ParserError(`Currency ${amountDescription.currency} is not ${amount.currency}`); + } + } + + operationMatch(operation, operationsDescriptionArray, matchesArray) { + for (let i = 0; i < operationsDescriptionArray.length; ++i) { + const des = operationsDescriptionArray[i]; + + if (matchesArray[i] != null && !des.allow_repeats) continue; + if (des.type.length > 0 && des.type != operation.type) continue; + + try { + this.accountMatch(des.account, operation.account); + this.amountMatch(des.amount, operation.amount); + this.metadataMatch(des.metadata, operation.metadata); + + } catch (e) { + continue; + } + + if (matchesArray[i] == null) { + matchesArray[i] = new Match(); + } + + if (operation.amount != null) { + const val = AmountValue(operation.amount); + + matchesArray[i].amounts.push(val); + } else { + matchesArray[i].amounts.push(null); + } + + matchesArray[i].operations.push(operation); + return true; + } + + return false; + } + + equalAmounts(operationsArray) { + if (operationsArray.length == 0) { + throw new ParserError(`cannot check equality of 0 operations`); + } + + const val = AmountValue(operationsArray[0].amount); + + for (let op of operationsArray) { + const otherVal = AmountValue(op.amount); + + if (val !== otherVal) { + throw new ParserError(`${op.amount.value} is not equal to ${operationsArray[0].amount.value}`); + } + } + } + + oppositeAmounts(operationA, operationB) { + const valA = AmountValue(operationA.amount); + const valB = AmountValue(operationB.amount); + + if (new Sign(valA).toString() == new Sign(valB).toString()) { + throw new ParserError(`${valA} and ${valB} have the same sign`); + } + + if (Math.abs(valA) !== Math.abs(valB)) { + throw new ParserError(`${valA} and ${valB} are not equal`); + } + } + + equalAddresses(operations) { + if (operations.length <= 1) { + throw new ParserError(`Cannot check address equality of ${operations.length} operations`); + } + + let base; + + for (let op of operations) { + if (op.account == null) { + throw new ParserError(`account is null`); + } + + if (!base) { + base = op.account.address; + continue; + } + + if (base != op.account.address) { + throw new ParserError(`${base} is not equal to ${op.account.address}`); + } + } + } + + matchIndexValid(matchesArray, index) { + if (typeof index != 'number') { + throw new ParserError(`Index must be a number`); + } + + if (index >= matchesArray.length) { + throw new ParserError(`Match index ${index} out of range`); + } + + if (matchesArray[index] == null) { + throw new ParserError(`Match index ${index} is null`); + } + } + + checkOps(requests2dArray, matchesArray, validCallback) { + for (let batch of requests2dArray) { + const ops = []; + + for (let reqIndex of batch) { + try { + this.matchIndexValid(matchesArray, reqIndex); + } catch (e) { + throw new ParserError(`${e.message}: index ${reqIndex} not valid`); + } + ops.push(...matchesArray[reqIndex].operations); + } + + if (typeof validCallback !== 'function') { + throw new ParserError(`validCallback must be a function`); + } + + validCallback(ops); + } + } + + comparisonMatch(descriptions, matchesArray) { + try { + this.checkOps(descriptions.equal_amounts, matchesArray, this.equalAmounts); + } catch (e) { + throw new ParserError(`${e.message}: operation amounts are not equal`); + } + + try { + this.checkOps(descriptions.equal_addresses, matchesArray, this.equalAddresses); + } catch (e) { + throw new ParserError(`${e.message}: operation addresses are not equal`); + } + + for (let amountMatch of descriptions.opposite_amounts) { + if (amountMatch.length != ExpectedOppositesLength) { + throw new ParserError(`Cannot check opposites of ${amountMatch.length} operations`); + } + + // Compare all possible pairs + try { + this.matchIndexValid(matchesArray, amountMatch[0]); + } catch (e) { + throw new ParserError(`${e.message}: opposite amounts comparison error`); + } + + try { + this.matchIndexValid(matchesArray, amountMatch[1]); + } catch (e) { + throw new ParserError(`${e.message}: opposite amounts comparison error`); + } + + const match0Ops = matchesArray[amountMatch[0]].operations; + const match1Ops = matchesArray[amountMatch[1]].operations; + + this.equalAmounts(match0Ops); + this.equalAmounts(match1Ops); + + this.oppositeAmounts(match0Ops[0], match1Ops[0]); + } + } + + MatchOperations(descriptions, operationsArray) { + if (operationsArray.length == 0) { + throw new ParserError(`Unable to match anything to zero operations`); + } + + const operationDescriptions = descriptions.operation_descriptions; + const matches = new Array(operationDescriptions.length).fill(null); + + if (operationDescriptions.length == 0) { + throw new ParserError(`No descriptions to match`); + } + + for (let i = 0; i < operationsArray.length; ++i) { + const op = operationsArray[i]; + const matchFound = this.operationMatch(op, operationDescriptions, matches); + + if (!matchFound && descriptions.err_unmatched) { + throw new ParserError(`Unable to find match for operation at index ${i}`); + } + } + + for (let i = 0; i < matches.length; ++i) { + if (matches[i] === null && !descriptions.operation_descriptions[i].optional) { + throw new ParserError(`Could not find match for description ${i}`); + } + } + + try { + this.comparisonMatch(descriptions, matches); + } catch (e) { + throw new ParserError(`${e.message}: group descriptions not met`); + } + + return matches; + } +} + +RosettaParser.Match = Match; + +module.exports = RosettaParser; \ No newline at end of file diff --git a/lib/reconciler/index.js b/lib/reconciler/index.js new file mode 100644 index 0000000..cb623f2 --- /dev/null +++ b/lib/reconciler/index.js @@ -0,0 +1,409 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +// Reconciler: index.js + +const Logger = require('../logger'); + +const { + ReconcilerError, +} = require('../errors'); + +const { + SubtractValues, + constructPartialBlockIdentifier, + Hash, +} = require('../utils'); + +const sleep = require('../utils/sleep'); + +const defaults = { + highWaterMark: -1, + lookupBalanceByBlock: true, + requiredDepthInactive: 500, + waitToCheckDiff: 10 * 1000, + waitToCheckDiffSleep: 5000, + withSeenAccounts: [], +}; + +const RECONCILIATION_INACTIVE_SLEEP_MS = 5000; + +const RECONCILIATION_ACTIVE = 'ACTIVE'; +const RECONCILIATION_INACTIVE = 'INACTIVE'; + +const RECONCILIATION_ERROR_HEAD_BEHIND_LIVE = 'ERROR_HEAD_BEHIND_LIVE'; +const RECONCILIATION_ERROR_ACCOUNT_UPDATED = 'ACCOUNT_UPDATED'; +const RECONCILIATION_ERROR_BLOCK_GONE = 'BLOCK_GONE'; + +class RosettaReconciler { + constructor(args = {}) { + const { networkIdentifier, helper, handler, fetcher } = args; + const configuration = Object.assign({}, defaults, args); + + this.network = networkIdentifier; + this.helper = helper; + this.handler = handler; + this.fetcher = fetcher; + + this.highWaterMark = configuration.lookupBalanceByBlock; + this.lookupBalanceByBlock = configuration.lookupBalanceByBlock; + + this.interestingAccounts = configuration.interestingAccounts || []; + this.inactiveQueue = []; + this.seenAccounts = this.handleSeenAccounts(configuration.withSeenAccounts); + + this.requiredDepthInactive = configuration.requiredDepthInactive; + this.waitToCheckDiff = configuration.waitToCheckDiff; + this.waitToCheckDiffSleep = configuration.waitToCheckDiffSleep; + + this.changeQueue = []; + } + + handleSeenAccounts(seenAccounts) { + const seen = {}; + + seenAccounts.forEach(s => { + this.inactiveQueue.push({ entry: s }); + seen[Hash(s)] = {}; + }); + + return seen; + } + + queueChanges(blockIdentifier, balanceChangesArray) { + for (let account of this.interestingAccounts) { + let skipAccount = false; + + for (let change of balanceChangesArray) { + if (Hash(account) == Hash(change)) { + skipAccount = true; + break; + } + } + + if (skipAccount) continue; + + balanceChangesArray.push({ + account_identifier: account.account, + currency: account.currency, + block_identifier: "0", + difference: block, + }); + } + + // ToDo: Remove one of these. JS is synchronous anyway. + if (!this.lookupBalanceByBlock) { + if (blockIdentifier.index < this.highWaterMark) { + return null; + } + + for (let change of balanceChangesArray) { + this.changeQueue.push(change); + Logger.info('Skipping active enqueue because backlog'); + } + + } else { + for (let change of balanceChangesArray) { + this.changeQueue.push(change); + } + } + } + + async compareBalance(accountIdentifier, currency, amount, blockIdentifier) { + const head = await this.helper.currentBlock(); + + if (blockIdentifier.index > head.index) { + throw new ReconcilerError( + `Live block ${blockIdentifier.index} > head block ${head.index}`, + RECONCILIATION_ERROR_HEAD_BEHIND_LIVE, + ); + } + + const exists = await this.helper.blockExists(blockIdentifier); + if (!exists) { + throw new ReconcilerError( + `Block gone! Block hash = ${blockIdentifier.hash}`, + RECONCILIATION_ERROR_BLOCK_GONE, + ); + } + + const { cachedBalance, balanceBlock } = + await this.helper.accountBalance(accountIdentifier, currency, head); + + if (blockIdentifier.index < balanceBlock.index) { + throw new ReconcilerError( + `Account updated: ${JSON.stringify(accountIdentifier)} updated at blockheight ${balanceBlock.index}`, + RECONCILIATION_ERROR_ACCOUNT_UPDATED, + ); + } + + const difference = SubtractValues(cachedBalance.value, amount); + return { + difference, + cachedBalance: cachedBalance.value, + headIndex: head.index, + }; + } + + async bestBalance(accountIdentifier, currency, partialBlockIdentifier) { + if (this.lookupBalanceByBlock) { + partialBlockIdentifier = null; + } + + return await this.getCurrencyBalance( + this.fetcher, + this.network, + accountIdentifier, + currency, + partialBlockIdentifier, + ); + } + + async accountReconciliation(accountIdentifier, currency, amount, blockIdentifier, isInactive) { + const accountCurrency = { + account_identifier: accountIdentifier, + currency: currency, + }; + + while (true) { + let difference; + let cachedBalance; + let headIndex; + + try { + const result = await this.compareBalance( + accountIdentifier, + currency, + amount, + blockIdentifier, + ); + + ({ difference, cachedBalance, headIndex } = result); + } catch (e) { + if (e instanceof ReconcilerError) { + switch (e.type) { + case RECONCILIATION_ERROR_HEAD_BEHIND_LIVE: { + // This error will only occur when lookupBalanceByBlock + // is disabled and the syncer is behind the current block of + // the node. This error should never occur when + // lookupBalanceByBlock is enabled. + const diff = blockIdentifier.index - headIndex; + if (diff < this.waitToCheckDiff) { + await sleep(this.waitToCheckDiffSleep); + continue; + } + + // Don't wait to check if we are very far behind + Logger.info(`Skipping reconciliation for ${JSON.stringify(accountCurrency)}:` + + ` ${diff} blocks behind`); + + // Set a highWaterMark to not accept any new + // reconciliation requests unless they happened + // after this new highWaterMark. + this.highWaterMark = partialBlockIdentifier.index; + break; + } + + case RECONCILIATION_ERROR_ACCOUNT_UPDATED: { + // Either the block has not been processed in a re-org yet + // or the block was orphaned + break; + } + + case RECONCILIATION_ERROR_BLOCK_GONE: { + // If account was updated, it must be + // enqueued again + break; + } + + default: + break; + } + + } else { + throw e; + } + } + + let reconciliationType = RECONCILIATION_ACTIVE; + + if (isInactive) { + reconciliationType = RECONCILIATION_INACTIVE; + } + + if (difference != "0") { + const error = await this.handler.reconciliationFailed( + reconciliationType, + accountCurrency.account_identifier, + accountCurrency.currency, + cachedBalance, + amount, + blockIdentifier, + ); + + if (error) throw error; + } + + this.inactiveAccountQueue(isInactive, accountCurrency, blockIdentifier); + + return await this.handler.reconciliationSucceeded( + reconciliationType, + accountCurrency.account_identifier, + accountCurrency.currency, + amount, + blockIdentifier, + ); + } + } + + static ContainsAccountCurrency(accountCurrencyMap, accountCurrency) { + const element = accountCurrencyMap[Hash(accountCurrency)]; + return element != null; + } + + async inactiveAccountQueue(isInactive, accountCurrency, blockIdentifier) { + // Only enqueue the first time we see an account on an active reconciliation. + let shouldEnqueueInactive = false; + + if (!isInactive && !RosettaReconciler.ContainsAccountCurrency(this.seenAccounts, accountCurrency)) { + this.seenAccounts[Hash(accountCurrency)] = {}; + shouldEnqueueInactive = true; + } + + if (isInactive || shouldEnqueueInactive) { + this.inactiveQueue.push({ + entry: accountCurrency, + lastCheck: blockIdentifier, + }); + } + } + + async reconcileActiveAccounts() { + while (true) { + const balanceChange = this.changeQueue.shift(); + if (balanceChange.block.index < this.highWaterMark) continue; + + const { block, value } = await this.bestBalance( + balanceChange.account_identifier, + balanceChange.currency, + constructPartialBlockIdentifier(balanceChange.block), + ); + + await this.accountReconciliation( + balanceChange.account_identifier, + balanceChange.currency, + value, + block, + false, + ); + } + } + + async reconcileInactiveAccounts() { + while (true) { + let head = null; + + try { + head = this.helper.currentBlock(); + } catch(e) { + + Logger.info('Waiting to start inactive reconciliation until a block is synced...'); + await sleep(RECONCILIATION_INACTIVE_SLEEP_MS); + continue; + } + + if (this.inactiveQueue.length > 0 && ( + this.inactiveQueue[0].last_check == null || // block is set to nil when loaded from previous run + this.inactiveQueue[0].last_check.index + this.requiredDepthInactive < head.index + )) { + const randomAccount = this.inactiveQueue.shift(); + + const { block, amount } = await this.bestBalance( + randomAccount.entry.account_identifier, + randomAccount.entry.currency, + constructPartialBlockIdentifier(head), + ); + + await this.accountReconciliation( + randomAccount.entry.account_identifier, + randomAccount.entry.currency, + amount, + block, + true, + ); + + } else { + await sleep(RECONCILIATION_INACTIVE_SLEEP_MS); + } + } + } + + async reconcile() { + // ToDo: Multithreading support (worker?) + await Promise.all([ + this.reconcileActiveAccounts(), + this.reconcileInactiveAccounts(), + ]); + } + + static extractAmount(amountArray, currency) { + for (let b of amountArray) { + if (Hash(b.currency) != Hash(currency)) continue; + return b; + } + + throw new Error(`Could not extract amount for ${JSON.stringify(currency)}`); + } + + async getCurrencyBalance(fetcher, networkIdentifier, accountIdentifier, currency, partialBlockIdentifier) { + const { liveBlock, liveBalances } = await fetcher.AcountBalanceRetry( + networkIdentifier, + accountIdentifier, + partialBlockIdentifier + ); + + const liveAmount = this.extractAmount(liveBalances, currency); + + return { + block: liveBlock, + value: liveAmount.value, + }; + } +} + +RosettaReconciler.AccountCurrency = class AccountCurrency { + constructor(opts) { + if (typeof opts == 'object' && opts.accountIdentifier) { + const { accountIdentifier, currency } = opts; + this.account = accountIdentifier; + this.currency = currency; + + } else { + const [ accountIdentifier, currency ] = arguments; + this.account = arguments[0]; + this.currency = arguments[1]; + } + }; +} + +RosettaReconciler.defaults = defaults; + +module.exports = RosettaReconciler; \ No newline at end of file diff --git a/lib/server/expressServer.js b/lib/server/expressServer.js new file mode 100644 index 0000000..c98e500 --- /dev/null +++ b/lib/server/expressServer.js @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +// const { Middleware } = require('swagger-express-middleware'); +const http = require('http'); +const fs = require('fs'); +const path = require('path'); +const swaggerUI = require('swagger-ui-express'); +const jsYaml = require('js-yaml'); +const express = require('express'); +const cors = require('cors'); +const cookieParser = require('cookie-parser'); +const bodyParser = require('body-parser'); +const { OpenApiValidator } = require('express-openapi-validator'); + +const logger = require('../logger'); + +class ExpressServer { + constructor(port, openApiYaml) { + this.port = port; + this.app = express(); + this.openApiPath = openApiYaml; + + try { + this.schema = jsYaml.safeLoad(fs.readFileSync(openApiYaml)); + } catch (e) { + logger.error('failed to start Express Server', e.message); + process.exit(1); + } + + this.setupMiddleware(); + } + + setupMiddleware() { + // this.setupAllowedMedia(); + this.app.use(cors()); + this.app.use(bodyParser.json({ limit: '14MB' })); + this.app.use(express.json()); + this.app.use(express.urlencoded({ extended: false })); + this.app.use(cookieParser()); + + //Simple test to see that the server is up and responding + this.app.get('/hello', (req, res) => res.send(`Hello World. path: ${this.openApiPath}`)); + + //Send the openapi document *AS GENERATED BY THE GENERATOR* + this.app.get('/openapi', (req, res) => res.sendFile((path.join(__dirname, '..', '..', 'api', 'openapi.yaml')))); + + //View the openapi document in a visual interface. Should be able to test from this page + this.app.use('/api-doc', swaggerUI.serve, swaggerUI.setup(this.schema)); + + this.app.get('/login-redirect', (req, res) => { + res.status(200); + res.json(req.query); + }); + + this.app.get('/oauth2-redirect.html', (req, res) => { + res.status(200); + res.json(req.query); + }); + + this.app.routeHandlers = {}; + } + + configure(config) { + this.app.config = config; + } + + launch() { + new OpenApiValidator({ + apiSpec: this.openApiPath, + operationHandlers: path.join(__dirname, '..'), + validateRequests: true, + validateResponses: false, + + }).install(this.app) + .catch(e => console.log(e)) + .then(() => { + // eslint-disable-next-line no-unused-vars + this.app.use((err, req, res, next) => { + // format errors + res.status(err.status || 500).json({ + message: err.message || err, + errors: err.errors || '', + }); + }); + + http.createServer(this.app).listen(this.port); + console.log(`Listening on port ${this.port}`); + }); + } + + async close() { + if (this.server !== undefined) { + await this.server.close(); + console.log(`Server on port ${this.port} shut down`); + } + } +} + +module.exports = ExpressServer; diff --git a/lib/server/index.js b/lib/server/index.js new file mode 100644 index 0000000..6a2c06d --- /dev/null +++ b/lib/server/index.js @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +const defaultConfig = require('../../config/default'); +const ExpressServer = require('./expressServer'); +const logger = require('../logger'); + +class RosettaServer { + constructor(configuration = {}) { + this.config = Object.assign( + {}, + defaultConfig, + configuration + ); + + const port = this.config.URL_PORT; + const openAPIPath = this.config.OPENAPI_YAML; + + try { + this.expressServer = new ExpressServer(port, openAPIPath); + this.expressServer.configure(this.config); + this.expressServer.launch(); + + logger.info(`Express server running on port ${port} using OpenAPI Spec: ${openAPIPath}`); + + } catch (e) { + logger.error('Express Server failure', error.message); + this.close(); + } + } + + async launch() { + try { + const port = this.config.URL_PORT; + const openAPIPath = this.config.OPENAPI_YAML; + + this.expressServer = new ExpressServer(port, openAPIPath); + this.expressServer.launch(); + + } catch (error) { + logger.error('Express Server failure', error.message); + await this.close(); + } + } + + /** + * Register asserter to be used for all requests. + * Only one can be registered at a time. + */ + useAsserter(asserter) { + this.expressServer.app.asserter = asserter; + } + + /** + * Reigster a routehandler at {route}. + * Optionally, pass an asserter that only handles requests to this + * specific route. + */ + register(route, handler, asserter) { + this.expressServer.app.routeHandlers[route] = { handler, asserter }; + } + + async close() { + } +} + +module.exports = RosettaServer; \ No newline at end of file diff --git a/lib/services/AccountService.js b/lib/services/AccountService.js new file mode 100644 index 0000000..0002872 --- /dev/null +++ b/lib/services/AccountService.js @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +/* eslint-disable no-unused-vars */ +const Service = require('./Service'); +const CallHandler = require('./CallHandler'); + +/** +* Get an Account Balance +* Get an array of all Account Balances for an Account Identifier and the Block Identifier at which the balance lookup was performed. Some consumers of account balance data need to know at which block the balance was calculated to reconcile account balance changes. To get all balances associated with an account, it may be necessary to perform multiple balance requests with unique Account Identifiers. If the client supports it, passing nil AccountIdentifier metadata to the request should fetch all balances (if applicable). It is also possible to perform a historical balance lookup (if the server supports it) by passing in an optional BlockIdentifier. +* +* accountBalanceRequest AccountBalanceRequest +* returns AccountBalanceResponse +* */ +const accountBalance = ({ request, response, params }) => new Promise( + async (resolve, reject) => { + try { + const res = await callHandler.bind(request.app)(request.route.path, params); + resolve(Service.successResponse(res)); + + } catch (e) { + reject(Service.rejectResponse( + e.message || 'Invalid input', + e.status || 405, + )); + } + }, +); + +module.exports = { + accountBalance, +}; diff --git a/lib/services/BlockService.js b/lib/services/BlockService.js new file mode 100644 index 0000000..1b8a94f --- /dev/null +++ b/lib/services/BlockService.js @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +/* eslint-disable no-unused-vars */ +const Service = require('./Service'); +const CallHandler = require('./CallHandler'); + +/** +* Get a Block +* Get a block by its Block Identifier. If transactions are returned in the same call to the node as fetching the block, the response should include these transactions in the Block object. If not, an array of Transaction Identifiers should be returned so /block/transaction fetches can be done to get all transaction information. +* +* blockRequest BlockRequest +* returns BlockResponse +* */ +const block = ({ request, response, params }) => new Promise( + async (resolve, reject) => { + try { + const res = await callHandler.bind(request.app)(request.route.path, params); + resolve(Service.successResponse(res)); + + } catch (e) { + reject(Service.rejectResponse( + e.message || 'Invalid input', + e.status || 405, + )); + } + }, +); +/** +* Get a Block Transaction +* Get a transaction in a block by its Transaction Identifier. This endpoint should only be used when querying a node for a block does not return all transactions contained within it. All transactions returned by this endpoint must be appended to any transactions returned by the /block method by consumers of this data. Fetching a transaction by hash is considered an Explorer Method (which is classified under the Future Work section). Calling this endpoint requires reference to a BlockIdentifier because transaction parsing can change depending on which block contains the transaction. For example, in Bitcoin it is necessary to know which block contains a transaction to determine the destination of fee payments. Without specifying a block identifier, the node would have to infer which block to use (which could change during a re-org). Implementations that require fetching previous transactions to populate the response (ex: Previous UTXOs in Bitcoin) may find it useful to run a cache within the Rosetta server in the /data directory (on a path that does not conflict with the node). +* +* blockTransactionRequest BlockTransactionRequest +* returns BlockTransactionResponse +* */ +const blockTransaction = ({ request, response, params }) => new Promise( + async (resolve, reject) => { + try { + const res = await callHandler.bind(request.app)(request.route.path, params); + resolve(Service.successResponse(res)); + + } catch (e) { + reject(Service.rejectResponse( + e.message || 'Invalid input', + e.status || 405, + )); + } + }, +); + +module.exports = { + block, + blockTransaction, +}; diff --git a/lib/services/ConstructionService.js b/lib/services/ConstructionService.js new file mode 100644 index 0000000..8affbb6 --- /dev/null +++ b/lib/services/ConstructionService.js @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +/* eslint-disable no-unused-vars */ +const Service = require('./Service'); +const CallHandler = require('./CallHandler'); + +/** +* Get Transaction Construction Metadata +* Get any information required to construct a transaction for a specific network. Metadata returned here could be a recent hash to use, an account sequence number, or even arbitrary chain state. It is up to the client to correctly populate the options object with any network-specific details to ensure the correct metadata is retrieved. It is important to clarify that this endpoint should not pre-construct any transactions for the client (this should happen in the SDK). This endpoint is left purposely unstructured because of the wide scope of metadata that could be required. In a future version of the spec, we plan to pass an array of Rosetta Operations to specify which metadata should be received and to create a transaction in an accompanying SDK. This will help to insulate the client from chain-specific details that are currently required here. +* +* constructionMetadataRequest ConstructionMetadataRequest +* returns ConstructionMetadataResponse +* */ +const constructionMetadata = ({ request, response, params }) => new Promise( + async (resolve, reject) => { + try { + const res = await callHandler.bind(request.app)(request.route.path, params); + resolve(Service.successResponse(res)); + + } catch (e) { + reject(Service.rejectResponse( + e.message || 'Invalid input', + e.status || 405, + )); + } + }, +); +/** +* Submit a Signed Transaction +* Submit a pre-signed transaction to the node. This call should not block on the transaction being included in a block. Rather, it should return immediately with an indication of whether or not the transaction was included in the mempool. The transaction submission response should only return a 200 status if the submitted transaction could be included in the mempool. Otherwise, it should return an error. +* +* constructionSubmitRequest ConstructionSubmitRequest +* returns ConstructionSubmitResponse +* */ +const constructionSubmit = ({ request, response, params }) => new Promise( + async (resolve, reject) => { + try { + const res = await callHandler.bind(request.app)(request.route.path, params); + resolve(Service.successResponse(res)); + + } catch (e) { + reject(Service.rejectResponse( + e.message || 'Invalid input', + e.status || 405, + )); + } + }, +); + +module.exports = { + constructionMetadata, + constructionSubmit, +}; diff --git a/lib/services/MempoolService.js b/lib/services/MempoolService.js new file mode 100644 index 0000000..3dda5d8 --- /dev/null +++ b/lib/services/MempoolService.js @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +/* eslint-disable no-unused-vars */ +const Service = require('./Service'); +const CallHandler = require('./CallHandler'); + +/** +* Get All Mempool Transactions +* Get all Transaction Identifiers in the mempool +* +* mempoolRequest MempoolRequest +* returns MempoolResponse +* */ +const mempool = ({ mempoolRequest }) => new Promise( + async (resolve, reject) => { + try { + const res = await callHandler.bind(request.app)(request.route.path, params); + resolve(Service.successResponse(res)); + + } catch (e) { + reject(Service.rejectResponse( + e.message || 'Invalid input', + e.status || 405, + )); + } + }, +); +/** +* Get a Mempool Transaction +* Get a transaction in the mempool by its Transaction Identifier. This is a separate request than fetching a block transaction (/block/transaction) because some blockchain nodes need to know that a transaction query is for something in the mempool instead of a transaction in a block. Transactions may not be fully parsable until they are in a block (ex: may not be possible to determine the fee to pay before a transaction is executed). On this endpoint, it is ok that returned transactions are only estimates of what may actually be included in a block. +* +* mempoolTransactionRequest MempoolTransactionRequest +* returns MempoolTransactionResponse +* */ +const mempoolTransaction = ({ mempoolTransactionRequest }) => new Promise( + async (resolve, reject) => { + try { + const res = await callHandler.bind(request.app)(request.route.path, params); + resolve(Service.successResponse(res)); + + } catch (e) { + reject(Service.rejectResponse( + e.message || 'Invalid input', + e.status || 405, + )); + } + }, +); + +module.exports = { + mempool, + mempoolTransaction, +}; diff --git a/lib/services/NetworkService.js b/lib/services/NetworkService.js new file mode 100644 index 0000000..80939eb --- /dev/null +++ b/lib/services/NetworkService.js @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +/* eslint-disable no-unused-vars */ +const Service = require('./Service'); +const CallHandler = require('./CallHandler'); + +/** +* Get List of Available Networks +* This endpoint returns a list of NetworkIdentifiers that the Rosetta server can handle. +* +* metadataRequest MetadataRequest +* returns NetworkListResponse +* */ +const networkList = ({ params, request, response }) => new Promise( + async (resolve, reject) => { + try { + const res = await CallHandler.bind(request.app)(request.route.path, params); + resolve(Service.successResponse(res)); + } catch (e) { + reject(Service.rejectResponse( + e.message || 'Invalid input', + e.status || 405, + )); + } + }, +); +/** +* Get Network Options +* This endpoint returns the version information and allowed network-specific types for a NetworkIdentifier. Any NetworkIdentifier returned by /network/list should be accessible here. Because options are retrievable in the context of a NetworkIdentifier, it is possible to define unique options for each network. +* +* networkRequest NetworkRequest +* returns NetworkOptionsResponse +* */ +const networkOptions = ({ params, request, response }) => new Promise( + async (resolve, reject) => { + try { + const res = await callHandler.bind(request.app)(request.route.path, params); + resolve(Service.successResponse(res)); + } catch (e) { + reject(Service.rejectResponse( + e.message || 'Invalid input', + e.status || 405, + )); + } + }, +); +/** +* Get Network Status +* This endpoint returns the current status of the network requested. Any NetworkIdentifier returned by /network/list should be accessible here. +* +* networkRequest NetworkRequest +* returns NetworkStatusResponse +* */ +const networkStatus = ({ params, request, response }) => new Promise( + async (resolve, reject) => { + try { + const res = await callHandler.bind(request.app)(request.route.path, params); + resolve(Service.successResponse(res)); + } catch (e) { + reject(Service.rejectResponse( + e.message || 'Invalid input', + e.status || 405, + )); + } + }, +); + +module.exports = { + networkList, + networkOptions, + networkStatus, +}; diff --git a/lib/services/Service.js b/lib/services/Service.js new file mode 100644 index 0000000..2923939 --- /dev/null +++ b/lib/services/Service.js @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +class Service { + static rejectResponse(error, code = 500) { + return { error, code }; + } + + static successResponse(payload, code = 200) { + return { payload, code }; + } +} + +module.exports = Service; diff --git a/lib/services/index.js b/lib/services/index.js new file mode 100644 index 0000000..bf71201 --- /dev/null +++ b/lib/services/index.js @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +const AccountService = require('./AccountService'); +const BlockService = require('./BlockService'); +const ConstructionService = require('./ConstructionService'); +const MempoolService = require('./MempoolService'); +const NetworkService = require('./NetworkService'); + +module.exports = { + AccountService, + BlockService, + ConstructionService, + MempoolService, + NetworkService, +}; diff --git a/lib/syncer/events/index.js b/lib/syncer/events/index.js new file mode 100644 index 0000000..c07beec --- /dev/null +++ b/lib/syncer/events/index.js @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +const BLOCK_ADDED = 'BLOCK_ADDED'; +const BLOCK_REMOVED = 'BLOCK_REMOVED'; +const SYNC_CANCELLED = 'SYNC_CANCELLED'; + +module.exports = { + BLOCK_ADDED, + BLOCK_REMOVED, + SYNC_CANCELLED, +}; \ No newline at end of file diff --git a/lib/syncer/index.js b/lib/syncer/index.js new file mode 100644 index 0000000..faff7f0 --- /dev/null +++ b/lib/syncer/index.js @@ -0,0 +1,251 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +const RosettaClient = require('rosetta-node-sdk-client'); +const RosettaFetcher = require('../fetcher'); +const EventEmitter = require('events'); +const { SyncerError } = require('../errors'); +const { Hash } = require('../utils'); +const SyncEvents = require('./events'); +const sleep = require('../utils/sleep'); +const logger = require('../logger'); + +/** + * RosettaSyncer + * Emits blockAdded and blockRemoved Events during sync. + * Emits cancel if sync was cancelled. + */ +class RosettaSyncer extends EventEmitter { + constructor({ networkIdentifier, fetcher, pastBlocks = [], + maxSync = 999, pastBlockSize = 40, defaultSyncSleep = 5000, genesisBlock = null }) { + super(); + + this.networkIdentifier = networkIdentifier; + this.fetcher = fetcher; + this.pastBlocks = pastBlocks; + this.genesisBlock = genesisBlock; + + this.nextIndex = null; + + this.maxSync = maxSync; + this.pastBlockSize = pastBlockSize; + this.defaultSyncSleep = defaultSyncSleep; + + // ToDo: Type checks + } + + async setStart(startIndex = -1) { + const networkStatus = await this.fetcher.networkStatusRetry(this.networkIdentifier); + + if (startIndex != -1) { + this.nextIndex = startIndex; + return; + } + + this.gensesisBlock = networkStatus.genesis_block_identifier; + this.nextIndex = networkStatus.genesis_block_identifier.index; + return; + } + + async nextSyncableRange(endIndexIn) { + let endIndex; + + if (this.nextIndex == -1) { + throw new SyncerError('Unable to determine current head'); + } + + if (endIndex == -1) { + const networkStatus = await this.fetcher.networkStatusRetry(this.networkIdentifier); + endIndex = networkStatus.current_block_identifier.index; + } + + if (this.nextIndex >= endIndex) { + return { + halt: true, + rangeEnd: -1, + }; + } + + if (endIndex - this.nextIndex > maxSync) { + endIndex = this.nextIndex + maxSync; + } + + return { + halt: false, + rangeEnd: endIndex, + }; + } + + async checkRemove(block) { + // ToDo: Type check block + + if (this.pastBlocks.length == 0) { + return { + shouldRemove: false, + lastBlock: null, + }; + } + + // Ensure processing correct index + if (block.block_identifier.index != this.nextIndex) { + throw new SyncerError( + `Get block ${block.block_identifier.index} instead of ${this.nextIndex}` + ); + } + + // Check if block parent is head + const lastBlock = this.pastBlocks[this.pastBlocks.length - 1]; + + if (Hash(block.parent_block_identifier) != Hash(lastBlock)) { + if (Hash(this.genesisBlock) == Hash(lastBlock)) { + throw new SyncerError('Cannot remove genesis block'); + } + + // Block can be removed. + return { + shouldRemove: true, + lastBlock, + }; + } + + return { + shouldRemove: false, + lastBlock: lastBlock, + }; + } + + async processBlock(blockIn) { + // ToDo: Type check block + const block = Object.assign({}, blockIn); // clone + + const { shouldRemove, lastBlock } = await this.checkRemove(block); + + if (shouldRemove) { + // Notify observers that a block was removed + this.emit(SyncEvents.BLOCK_REMOVED, lastBlock); + + // Remove the block internally + this.pastBlocks.pop(); + this.nextIndex = lastBlock.index; + return; + } + + // Notify observers that a block was added + this.emit(SyncEvents.BLOCK_ADDED, block); + + // Add the block internally + this.pastBlocks.push(block.block_identifier); + if (this.pastBlocks.length > this.pastBlockSize) { + this.pastBlocks.shift(); + } + + this.nextIndex = block.block_identifier.index + 1; + } + + async syncRange(endIndex) { + const blockMap = this.fetcher.blockRange( + this.networkIdentifier, + this.nextIndex, + endIndex + ); + + while (this.nextIndex <= endIndex) { + // ToDo: Map? + let block = blockMap[this.nextIndex]; + + if (!block) { + // Re-org happened. Refetch the next block. + const partialBlockIdentifier = new RosettaClient.PartialBlockIdentifier({ + index: this.nextIndex, + }); + + block = this.fetcher.blockRetry( + this.networkIdentifier, + partialBlockIdentifier, + ); + + } else { + // We are going to refetch the block. + // Delete the current version of it. + delete blockMap[this.nextIndex]; + } + + await this.processBlock(block); + } + } + + /** Syncs the blockchain in the requested range. + * Endless cycle unless an error happens or the requested range was synced successfully. + * @param {number} startIndex - Index to start sync from + * @param {number} endIndex - Index to end sync at (inclusive). + */ + async sync(startIndex, endIndex) { + this.emit(SyncEvents.SYNC_CANCELLED); + + try { + await this.setStart(startIndex); + } catch (e) { + throw new SyncerError(`Unable to set sync start index: ${e.message}`); + } + + while (true) { + let rangeEnd; + let halt; + + try { + const result = await this.nextSyncableRange(endIndex); + rangeEnd = result.rangeEnd; + halt = result.halt; + } catch(e) { + throw new SyncerError(`Unable to get next syncable range: ${e.message}`); + } + + if (halt) { + if (endIndex != -1) { + // Quit Sync. + break; + } + + logger.verbose(`Syncer at tip ${this.nextIndex}... Sleeping...`); + await sleep(this.defaultSyncSleep); + continue; + } + + logger.verbose(`Syncing ${this.nextIndex}-${rangeEnd}`); + + try { + await this.syncRange(rangeEnd); + } catch (e) { + throw new SyncerError(`Unable to sync to ${rangeEnd}: ${e.message}`); + } + } + + if (startIndex == -1) { + startIndex = this.genesisBlock.index; + } + + logger.info(`Finished Syncing ${startIndex}-${endIndex}`); + } +} + +RosettaSyncer.Events = SyncEvents; + +module.exports = RosettaSyncer; \ No newline at end of file diff --git a/lib/utils/PromisePool.js b/lib/utils/PromisePool.js new file mode 100644 index 0000000..1c71821 --- /dev/null +++ b/lib/utils/PromisePool.js @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +/** + * PromisePool.js + * Author: Yoshi Jaeger + * + * Adapted the code from https://github.com/rxaviers/async-pool/blob/master/lib/es7.js + * to use an applier proxy. + */ + +const defaultApplier = (promiseBodyFn, arg) => { + const r = promiseBodyFn(arg); + return r; +}; + +const arrayApplier = (promiseBodyFn, args = []) => { + const r = promiseBodyFn(...args); + return r; +}; + +async function PromisePool(poolLimit = 8, argArray, promiseBodyFn, applierFn = defaultApplier) { + const ret = []; + const executing = []; + + for (const item of argArray) { + const p = Promise.resolve().then(() => applierFn(promiseBodyFn, item)); + ret.push(p); + + const e = p.then(() => executing.splice(executing.indexOf(e), 1)); + executing.push(e); + + if (executing.length >= poolLimit) { + await Promise.race(executing); + } + } + + return Promise.all(ret); +} + +module.exports = { + create: PromisePool, + defaultApplier: defaultApplier, + arrayApplier: arrayApplier, +}; \ No newline at end of file diff --git a/lib/utils/PromisePool.test.js b/lib/utils/PromisePool.test.js new file mode 100644 index 0000000..0e766a7 --- /dev/null +++ b/lib/utils/PromisePool.test.js @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +/** + * PromisePool.test.js + * Author: Yoshi Jaeger + */ + +const PromisePool = require('./PromisePool'); +const { expect } = require('chai'); + +const array = []; +const poolSize = 2; + +const testFunction = (timeout, text) => { + return new Promise((fulfill, reject) => { + setTimeout(() => { + array.push(text); + fulfill(text); + }, timeout); + }); +}; + +describe('PromisePool', function () { + it('output should have the correct order', function (done) { + PromisePool.create( + poolSize, + [ + [500, 'first'], + [500, 'second'], + [1000, 'fourth'], + [100, 'third'], + ], + testFunction, + PromisePool.arrayApplier, + ).then(data => { + console.log(`All promises finished! Promises: ${data}, Data: ${array}`); + + // Data should be in correct order + expect(array).to.deep.equal(['first', 'second', 'third', 'fourth']); + + // Promises should return their data in correct order + expect(data).to.deep.equal(['first', 'second', 'fourth', 'third']); + + // Test finished + done(); + }); + }); +}); + diff --git a/lib/utils/index.js b/lib/utils/index.js new file mode 100644 index 0000000..c132e29 --- /dev/null +++ b/lib/utils/index.js @@ -0,0 +1,137 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +// models: index.js + +const { InputError } = require('../errors'); +const RosettaClient = require('rosetta-node-sdk-client'); + +function AddValues(a, b) { + const parsedA = parseInt(a); + const parsedB = parseInt(b); + + if (isNaN(parsedA)) { + throw new AsserterError('SupportedNetworks must be an array'); + } + + if (isNaN(parsedB)) { + throw new AsserterError('SupportedNetworks must be an array'); + } + + return `${parsedA + parsedB}`; +} + +function SubtractValues(a, b) { + const parsedA = parseInt(a); + const parsedB = parseInt(b); + + if (isNaN(parsedA)) { + throw new AsserterError('SupportedNetworks must be an array'); + } + + if (isNaN(parsedB)) { + throw new AsserterError('SupportedNetworks must be an array'); + } + + return `${parsedA - parsedB}`; +} + +function constructPartialBlockIdentifier(blockIdentifier) { + return RosettaClient.PartialBlockIdentifier.constructFromObject({ + hash: blockIdentifier.hash, + index: blockIdentifier.index, + }); +} + +// http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/ +Object.defineProperty(String.prototype, 'hashCode', { + value: function() { + var hash = 0, i, chr; + for (i = 0; i < this.length; i++) { + chr = this.charCodeAt(i); + hash = ((hash << 5) - hash) + chr; + hash |= 0; // Convert to 32bit integer + } + return hash; + } +}); + +function Hash(input) { + if (typeof input == 'object') { + let values = []; + const keys = Object.keys(input).sort(); + + for (let key of keys) { + if (typeof input[key] == 'object') { + const subHash = Hash(input[key]); + values.push(`${key}:${subHash}`); + } else { + values.push(`${key}:${input[key]}`); + } + } + + return values.join('|').hashCode(); + } + + if (typeof input == 'number') { + return `${input}`; + } + + if (typeof input == 'string') { + return input.hashCode(); + } + + throw new Error(`Invalid type ${typeof input} for Hasher`); +} + +function AmountValue(amount) { + if (amount == null) { + throw new Error(`Amount value cannot be null`); + } + + if (typeof amount.value !== 'string') { + throw new Error('Amount must be a string'); + } + + return parseInt(amount.value); +} + +function NegateValue(amount) { + if (amount == null) { + throw new Error(`Amount value cannot be null`); + } + + if (typeof amount !== 'string') { + throw new Error('Amount must be a string'); + } + + const negated = 0 - parseInt(amount); + return `${negated}`; +} + +module.exports = { + AddValues, + SubtractValues, + constructPartialBlockIdentifier, + AmountValue, + NegateValue, + Hash, +}; \ No newline at end of file diff --git a/lib/utils/openapiRouter.js b/lib/utils/openapiRouter.js new file mode 100644 index 0000000..7cf7c12 --- /dev/null +++ b/lib/utils/openapiRouter.js @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +const logger = require('../logger'); +const controllers = require('../controllers'); +const Services = require('../services'); + +function handleError(err, request, response, next) { + logger.error(err); + const code = err.code || 400; + response.status(code); + response.error = err; + next(JSON.stringify({ + code, + error: err, + })); +} + +/** + * The purpose of this route is to collect the request variables as defined in the + * OpenAPI document and pass them to the handling controller as another Express + * middleware. All parameters are collected in the requet.swagger.values key-value object + * + * The assumption is that security handlers have already verified and allowed access + * to this path. If the business-logic of a particular path is dependant on authentication + * parameters (e.g. scope checking) - it is recommended to define the authentication header + * as one of the parameters expected in the OpenAPI/Swagger document. + * + * Requests made to paths that are not in the OpernAPI scope + * are passed on to the next middleware handler. + * @returns {Function} + */ +function openApiRouter() { + return async (request, response, next) => { + try { + /** + * This middleware runs after a previous process have applied an openapi object + * to the request. + * If none was applied This is because the path requested is not in the schema. + * If there's no openapi object, we have nothing to do, and pass on to next middleware. + */ + if (request.openapi === undefined + || request.openapi.schema === undefined + ) { + next(); + return; + } + // request.swagger.paramValues = {}; + // request.swagger.params.forEach((param) => { + // request.swagger.paramValues[param.name] = getValueFromRequest(request, param); + // }); + const controllerName = request.openapi.schema['x-openapi-router-controller']; + const serviceName = request.openapi.schema['x-openapi-router-service']; + if (!controllers[controllerName] || controllers[controllerName] === undefined) { + handleError(`request sent to controller '${controllerName}' which has not been defined`, + request, response, next); + } else { + const apiController = new controllers[controllerName](Services[serviceName]); + const controllerOperation = request.openapi.schema.operationId; + await apiController[controllerOperation](request, response, next); + } + } catch (error) { + console.error(error); + const err = { code: 500, error: error.message }; + handleError(err, request, response, next); + } + }; +} + +module.exports = openApiRouter; diff --git a/lib/utils/sleep.js b/lib/utils/sleep.js new file mode 100644 index 0000000..9565d4c --- /dev/null +++ b/lib/utils/sleep.js @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +// sleep.js +// Author: Yoshi Jaeger + +module.exports = function (timeoutMs) { + return new Promise((fulfill, _) => { + setTimeout(fulfill, timeoutMs); + }); +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..28bcf9a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4446 @@ +{ + "name": "rosetta-node-sdk", + "version": "1.3.1b", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@apidevtools/json-schema-ref-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-8.0.0.tgz", + "integrity": "sha512-n4YBtwQhdpLto1BaUCyAeflizmIbaloGShsPyRtFf5qdFJxfssj+GgLavczgKJFa3Bq+3St2CKcpRJdjtB4EBw==", + "requires": { + "@jsdevtools/ono": "^7.1.0", + "call-me-maybe": "^1.0.1", + "js-yaml": "^3.13.1" + } + }, + "@babel/cli": { + "version": "7.10.3", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.10.3.tgz", + "integrity": "sha512-lWB3yH5/fWY8pi2Kj5/fA+17guJ9feSBw5DNjTju3/nRi9sXnl1JPh7aKQOSvdNbiDbkzzoGYtsr46M8dGmXDQ==", + "requires": { + "chokidar": "^2.1.8", + "commander": "^4.0.1", + "convert-source-map": "^1.1.0", + "fs-readdir-recursive": "^1.1.0", + "glob": "^7.0.0", + "lodash": "^4.17.13", + "make-dir": "^2.1.0", + "slash": "^2.0.0", + "source-map": "^0.5.0" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "optional": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "optional": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "optional": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "optional": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + } + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "optional": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "optional": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + } + }, + "fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "optional": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "optional": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "optional": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "optional": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "optional": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "optional": true + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "optional": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "optional": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "optional": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "@babel/code-frame": { + "version": "7.10.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.3.tgz", + "integrity": "sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.3" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.3", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz", + "integrity": "sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.10.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.3.tgz", + "integrity": "sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.3", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@jsdevtools/ono": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.2.tgz", + "integrity": "sha512-qS/a24RA5FEoiJS9wiv6Pwg2c/kiUo3IVUQcfeM9JvsR6pM8Yx+yl/6xWYLckZCT5jpLNhslgjiA8p/XcGyMRQ==" + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "acorn": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "dev": true + }, + "acorn-jsx": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", + "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", + "dev": true + }, + "ajv": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", + "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-colors": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", + "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", + "dev": true + }, + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=" + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "optional": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "optional": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "optional": true + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "array-includes": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.1.tgz", + "integrity": "sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0", + "is-string": "^1.0.5" + } + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "optional": true + }, + "array.prototype.flat": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz", + "integrity": "sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "optional": true + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "async": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", + "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" + }, + "async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "optional": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "optional": true + }, + "axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "dev": true, + "requires": { + "follow-redirects": "1.5.10" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "optional": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "optional": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "optional": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "optional": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "optional": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "binary-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", + "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", + "dev": true + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "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 + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "busboy": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", + "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", + "requires": { + "dicer": "0.2.5", + "readable-stream": "1.1.x" + } + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "optional": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "call-me-maybe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", + "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=" + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "chokidar": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", + "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.1", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.2.0" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "optional": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "optional": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-width": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", + "dev": true + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "optional": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", + "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "color-string": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", + "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" + }, + "colorspace": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz", + "integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==", + "requires": { + "color": "3.0.x", + "text-hex": "1.0.x" + } + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==" + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "confusing-browser-globals": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.9.tgz", + "integrity": "sha512-KbS1Y0jMtyPgIxjO7ZzMAuUpAKMt1SzCL9fsrKsX6b0zJPTaT0SiSPmewwVZg9UAO83HVIlEhZF84LIjZ0lmAw==", + "dev": true + }, + "contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", + "dev": true + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-parser": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.5.tgz", + "integrity": "sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw==", + "requires": { + "cookie": "0.4.0", + "cookie-signature": "1.0.6" + } + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==" + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "deasync": { + "version": "0.1.20", + "resolved": "https://registry.npmjs.org/deasync/-/deasync-0.1.20.tgz", + "integrity": "sha512-E1GI7jMI57hL30OX6Ht/hfQU8DO4AuB9m72WFm4c38GNbUD4Q03//XZaOIHZiY+H1xUaomcot5yk2q/qIZQkGQ==", + "optional": true, + "requires": { + "bindings": "^1.5.0", + "node-addon-api": "^1.7.1" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "optional": true + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "optional": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "optional": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "optional": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "optional": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "diagnostics": { + "version": "github:DABH/diagnostics#1533851826eac88679124d41f87e45e1b196be56", + "from": "github:DABH/diagnostics#master", + "requires": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "dicer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", + "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", + "requires": { + "readable-stream": "1.1.x", + "streamsearch": "0.1.2" + } + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + } + } + }, + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz", + "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.9.1", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^4.0.3", + "eslint-utils": "^1.3.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^5.0.1", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob": "^7.1.2", + "globals": "^11.7.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^6.2.2", + "js-yaml": "^3.13.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.11", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.2", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^5.5.1", + "strip-ansi": "^4.0.0", + "strip-json-comments": "^2.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "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 + } + } + }, + "eslint-config-airbnb-base": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.0.tgz", + "integrity": "sha512-Snswd5oC6nJaevs3nZoLSTvGJBvzTfnBqOIArkf3cbyTyq9UD79wOk8s+RiL6bhca0p/eRO6veczhf6A/7Jy8Q==", + "dev": true, + "requires": { + "confusing-browser-globals": "^1.0.9", + "object.assign": "^4.1.0", + "object.entries": "^1.1.2" + } + }, + "eslint-import-resolver-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz", + "integrity": "sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "resolve": "^1.13.1" + } + }, + "eslint-module-utils": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz", + "integrity": "sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "pkg-dir": "^2.0.0" + } + }, + "eslint-plugin-import": { + "version": "2.21.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.21.2.tgz", + "integrity": "sha512-FEmxeGI6yaz+SnEB6YgNHlQK1Bs2DKLM+YF+vuTk5H8J9CLbJLtlPvRFgZZ2+sXiKAlN5dpdlrWOjK8ZoZJpQA==", + "dev": true, + "requires": { + "array-includes": "^3.1.1", + "array.prototype.flat": "^1.2.3", + "contains-path": "^0.1.0", + "debug": "^2.6.9", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "^0.3.3", + "eslint-module-utils": "^2.6.0", + "has": "^1.0.3", + "minimatch": "^3.0.4", + "object.values": "^1.1.1", + "read-pkg-up": "^2.0.0", + "resolve": "^1.17.0", + "tsconfig-paths": "^3.9.0" + }, + "dependencies": { + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + } + } + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + }, + "espree": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", + "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==", + "dev": true, + "requires": { + "acorn": "^6.0.7", + "acorn-jsx": "^5.0.0", + "eslint-visitor-keys": "^1.0.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "esquery": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.1.0.tgz", + "integrity": "sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "optional": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "optional": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "exponential-backoff": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.0.1.tgz", + "integrity": "sha512-YOpmVqDXqyLgYrfU2k/RFVvSjy3p0A32aGDmwbR+lbmhROVmeCg6WSGqBgr4HB5AZNElg7Oj4Cm/vIbodLu2Ig==" + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "express-openapi-validator": { + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/express-openapi-validator/-/express-openapi-validator-3.16.1.tgz", + "integrity": "sha512-rjd0lvjiJMgggv6zIw/3DwDBk2VHn0spwmUGrJ3Fhsy9NhbEeMXN7qWUWaWhni2hEE0o7OWuj/PtBhnA16u+qQ==", + "requires": { + "ajv": "^6.12.2", + "content-type": "^1.0.4", + "deasync": "^0.1.19", + "js-yaml": "^3.14.0", + "json-schema-ref-parser": "^8.0.0", + "lodash.merge": "^4.6.2", + "lodash.uniq": "^4.5.0", + "lodash.zipobject": "^4.1.3", + "media-typer": "^1.1.0", + "multer": "^1.4.2", + "ono": "^7.1.2", + "path-to-regexp": "^6.1.0" + }, + "dependencies": { + "media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==" + }, + "ono": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/ono/-/ono-7.1.2.tgz", + "integrity": "sha512-es7Gfr+OGNFwiYpyHCLgBF+p/RA0qYbWysQKlZbLvvUBis5BygEs8TVJ4r+SgHDfagOgONhaAl6Y4JLy++0MTw==", + "requires": { + "@jsdevtools/ono": "7.1.2" + } + }, + "path-to-regexp": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.1.0.tgz", + "integrity": "sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw==" + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "optional": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "optional": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "optional": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "optional": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "optional": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "optional": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "optional": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "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==" + }, + "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==" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fast-safe-stringify": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", + "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" + }, + "fecha": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.0.tgz", + "integrity": "sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg==" + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "optional": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "flat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", + "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", + "dev": true, + "requires": { + "is-buffer": "~2.0.3" + } + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "dev": true, + "requires": { + "debug": "=3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "optional": true + }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" + }, + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "formidable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", + "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==" + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "optional": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "fs-readdir-recursive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", + "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "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 + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "optional": true + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "optional": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "optional": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "optional": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "optional": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "optional": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "optional": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "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 + }, + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "dev": true + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "inquirer": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", + "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", + "dev": true, + "requires": { + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^2.0.0", + "lodash": "^4.17.12", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "optional": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "optional": true + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "optional": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, + "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, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-buffer": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", + "dev": true + }, + "is-callable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", + "dev": true + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "optional": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "optional": true + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "optional": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "optional": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "optional": true + } + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "optional": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "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 + }, + "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==", + "optional": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", + "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" + }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", + "dev": true + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "optional": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "optional": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "json-pointer": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.0.tgz", + "integrity": "sha1-jlAFUKaqxUZKRzN32leqbMIoKNc=", + "requires": { + "foreach": "^2.0.4" + } + }, + "json-schema-ref-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-8.0.0.tgz", + "integrity": "sha512-2P4icmNkZLrBr6oa5gSZaDSol/oaBHYkoP/8dsw63E54NnHGRhhiFuy9yFoxPuSm+uHKmeGxAAWMDF16SCHhcQ==", + "requires": { + "@apidevtools/json-schema-ref-parser": "8.0.0" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "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==", + "optional": true + }, + "kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" + }, + "lodash.zipobject": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/lodash.zipobject/-/lodash.zipobject-4.1.3.tgz", + "integrity": "sha1-s5n1q6j/YqdG9peb8gshT5ZNvvg=" + }, + "log-symbols": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", + "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2" + } + }, + "logform": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.2.0.tgz", + "integrity": "sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg==", + "requires": { + "colors": "^1.2.1", + "fast-safe-stringify": "^2.0.4", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "triple-beam": "^1.3.0" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "dependencies": { + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + } + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "optional": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "optional": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "optional": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "optional": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "optional": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "optional": true + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "optional": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "optional": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "optional": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "requires": { + "mime-db": "1.44.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "optional": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "optional": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "requires": { + "minimist": "^1.2.5" + } + }, + "mocha": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.2.0.tgz", + "integrity": "sha512-O9CIypScywTVpNaRrCAgoUnJgozpIofjKUYmJhiCIJMiuYnLI6otcb1/kpW9/n/tJODHGZ7i8aLQoDVsMtOKQQ==", + "dev": true, + "requires": { + "ansi-colors": "3.2.3", + "browser-stdout": "1.3.1", + "chokidar": "3.3.0", + "debug": "3.2.6", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "find-up": "3.0.0", + "glob": "7.1.3", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.13.1", + "log-symbols": "3.0.0", + "minimatch": "3.0.4", + "mkdirp": "0.5.5", + "ms": "2.1.1", + "node-environment-flags": "1.0.6", + "object.assign": "4.1.0", + "strip-json-comments": "2.0.1", + "supports-color": "6.0.0", + "which": "1.3.1", + "wide-align": "1.1.3", + "yargs": "13.3.2", + "yargs-parser": "13.1.2", + "yargs-unparser": "1.6.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "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, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "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 + }, + "supports-color": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", + "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "multer": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz", + "integrity": "sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==", + "requires": { + "append-field": "^1.0.0", + "busboy": "^0.2.11", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.1", + "object-assign": "^4.1.1", + "on-finished": "^2.3.0", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + } + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, + "nan": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", + "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", + "optional": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "optional": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "optional": true + }, + "node-environment-flags": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", + "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==", + "dev": true, + "requires": { + "object.getownpropertydescriptors": "^2.0.3", + "semver": "^5.7.0" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "optional": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "optional": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "optional": true + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "optional": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "optional": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "object.entries": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.2.tgz", + "integrity": "sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "has": "^1.0.3" + } + }, + "object.getownpropertydescriptors": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", + "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "optional": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "object.values": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz", + "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "requires": { + "fn.name": "1.x.x" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "ono": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ono/-/ono-5.1.0.tgz", + "integrity": "sha512-GgqRIUWErLX4l9Up0khRtbrlH8Fyj59A0nKv8V6pWEto38aUgnOGOOF7UmgFFLzFnDSc8REzaTXOc0hqEe7yIw==" + }, + "openapi-sampler": { + "version": "1.0.0-beta.15", + "resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.0.0-beta.15.tgz", + "integrity": "sha512-wUD/vD3iBHKik/sME3uwUu4X3HFA53rDrPcVvLzgEELjHLbnTpSYfm4Jo9qZT1dPfBRowAnrF/VRQfOjL5QRAw==", + "requires": { + "json-pointer": "^0.6.0" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "optional": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "optional": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "^2.0.0" + } + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "optional": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + } + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "readdirp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz", + "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==", + "dev": true, + "requires": { + "picomatch": "^2.0.4" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "optional": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "optional": true + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "optional": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "optional": true + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "optional": true + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "optional": true + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "rosetta-node-sdk-client": { + "version": "git+https://github.com/SmartArray/rosetta-node-sdk-client.git#15fae60f4820f3507a68d687acbc0fd9dc4dd35a", + "from": "git+https://github.com/SmartArray/rosetta-node-sdk-client.git", + "requires": { + "@babel/cli": "^7.0.0", + "superagent": "3.7.0" + } + }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, + "rxjs": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz", + "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "optional": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "optional": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "requires": { + "is-arrayish": "^0.3.1" + } + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==" + }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + } + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "optional": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "optional": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "optional": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "optional": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "optional": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "optional": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "optional": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "optional": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "optional": true + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "optional": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "optional": true, + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "optional": true + }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "dev": true + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "optional": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "optional": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "optional": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "superagent": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.7.0.tgz", + "integrity": "sha512-/8trxO6NbLx4YXb7IeeFTSmsQ35pQBiTBsLNvobZx7qBzBeHYvKCyIIhW2gNcWbLzYxPAjdgFbiepd8ypwC0Gw==", + "requires": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.1.1", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.0.5" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "swagger-ui-dist": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.27.0.tgz", + "integrity": "sha512-dlbH4L8+UslXVeYvCulicmJP2cnHLoabQGfeav5lx74fM+tMQW53M8iqpH5wbBqBbFkZwza+IIWoPrenqO/F2g==" + }, + "swagger-ui-express": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.1.4.tgz", + "integrity": "sha512-Ea96ecpC+Iq9GUqkeD/LFR32xSs8gYqmTW1gXCuKg81c26WV6ZC2FsBSPVExQP6WkyUuz5HEiR0sEv/HCC343g==", + "requires": { + "swagger-ui-dist": "^3.18.1" + } + }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "optional": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "optional": true + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "optional": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "optional": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "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==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" + }, + "tsconfig-paths": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", + "integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + } + }, + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", + "dev": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "optional": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "optional": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "optional": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "optional": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "optional": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "optional": true + } + } + }, + "upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "optional": true + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "optional": true + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "optional": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "winston": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.3.1.tgz", + "integrity": "sha512-ijjJtGl8tqQzftLysZn0jQBwa1VjyIrgysvk9tJJczk88oXmXZ5z6CvSFcQ69FfXONINxgIVfx3lqwjK57Hfsg==", + "requires": { + "async": "^3.1.0", + "diagnostics": "github:DABH/diagnostics#master", + "is-stream": "^2.0.0", + "logform": "^2.2.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.4.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "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==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, + "winston-transport": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz", + "integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==", + "requires": { + "readable-stream": "^2.3.7", + "triple-beam": "^1.2.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "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, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "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 + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "yargs-unparser": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", + "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", + "dev": true, + "requires": { + "flat": "^4.1.0", + "lodash": "^4.17.15", + "yargs": "^13.3.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c1f1acf --- /dev/null +++ b/package.json @@ -0,0 +1,49 @@ +{ + "name": "rosetta-node-sdk", + "version": "1.3.1b", + "description": "A Standard for Blockchain Interaction", + "main": "index.js", + "scripts": { + "prestart": "npm install", + "start": "node index.js", + "run-example": "node examples/server", + "eslint": "eslint .", + "test": "mocha --exit test" + }, + "keywords": [ + "openapi-generator", + "openapi" + ], + "license": "MIT", + "private": false, + "dependencies": { + "@babel/cli": "^7.0.0", + "body-parser": "^1.19.0", + "camelcase": "^5.3.1", + "cookie-parser": "^1.4.4", + "cors": "^2.8.5", + "exponential-backoff": "^3.0.1", + "express": "^4.16.4", + "express-openapi-validator": "^3.16.1", + "js-yaml": "^3.3.0", + "ono": "^5.0.1", + "openapi-sampler": "^1.0.0-beta.15", + "rosetta-node-sdk-client": "git+https://github.com/SmartArray/rosetta-node-sdk-client.git", + "superagent": "3.7.0", + "swagger-ui-express": "^4.0.2", + "winston": "^3.2.1" + }, + "devDependencies": { + "axios": "^0.19.0", + "chai": "^4.2.0", + "eslint": "^5.16.0", + "eslint-config-airbnb-base": "^14.0.0", + "eslint-plugin-import": "^2.17.2", + "mocha": "^7.1.1" + }, + "eslintConfig": { + "env": { + "node": true + } + } +} diff --git a/test/asserter.test.js b/test/asserter.test.js new file mode 100644 index 0000000..3f7c47b --- /dev/null +++ b/test/asserter.test.js @@ -0,0 +1,3149 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +// asserter.test.js + +// server.test.js +const { expect } = require('chai'); +const fs = require('fs'); +const path = require('path'); +const RosettaSDK = require('..'); + +const { + constructPartialBlockIdentifier, +} = require('../lib/utils'); + +const c = (j) => j == undefined ? undefined : JSON.parse(JSON.stringify(j)); + +const T = RosettaSDK.Client; + +const createTempDir = () => { + return new Promise((fulfill, reject) => { + fs.mkdtemp('rosetta-test', (err, dir) => { + if (err) return reject(); + return fulfill(dir); + }); + }); +}; + +describe('Asserter Tests', function () { + describe('Main', function () { + const validNetwork = T.NetworkIdentifier.constructFromObject({ + blockchain: 'hello', + network: 'world', + }); + + const validNetworkStatus = T.NetworkStatusResponse.constructFromObject({ + current_block_identifier: T.BlockIdentifier.constructFromObject({ + index: 0, + hash: 'block 0', + }), + genesis_block_identifier: T.BlockIdentifier.constructFromObject({ + index: 100, + hash: 'block 100', + }), + current_block_timestamp: RosettaSDK.Asserter.MinUnixEpoch + 1, + peers: [ + { peer_id: 'peer 1' }, + ], + }); + + const invalidNetworkStatus = T.NetworkStatusResponse.constructFromObject({ + current_block_identifier: T.BlockIdentifier.constructFromObject({ + index: 100, + hash: 'block 100', + }), + current_block_timestamp: RosettaSDK.Asserter.MinUnixEpoch + 1, + peers: [ + { peer_id: 'peer 1' }, + ], + }); + + const validNetworkOptions = T.NetworkOptionsResponse.constructFromObject({ + version: new T.Version('1.4.0', '1.0'), + allow: T.Allow.constructFromObject({ + operation_statuses: [ + new T.OperationStatus('Success', true), + ], + operation_types: ['Transfer'], + errors: [ + new T.Error(1, 'error', true), + ], + // historical_balance_lookup: true, + }), + }); + + const invalidNetworkOptions = T.NetworkOptionsResponse.constructFromObject({ + version: new T.Version('1.4.0', '1.0'), + allow: T.Allow.constructFromObject({ + operation_statuses: [], + operation_types: ['Transfer'], + errors: [ + new T.Error(1, 'error', true), + ], + }), + }); + + const duplicateStatuses = T.NetworkOptionsResponse.constructFromObject({ + version: new T.Version('1.4.0', '1.0'), + allow: T.Allow.constructFromObject({ + operation_statuses: [ + new T.OperationStatus('Success', true), + new T.OperationStatus('Success', true), + ], + operation_types: ['Transfer'], + errors: [ + new T.Error(1, 'error', true), + ], + }), + }); + + const duplicateTypes = T.NetworkOptionsResponse.constructFromObject({ + version: new T.Version('1.4.0', '1.0'), + allow: T.Allow.constructFromObject({ + operation_types: ['Transfer', 'Transfer'], + operation_statuses: [new T.OperationStatus('Success', true)], + errors: [ + new T.Error(1, 'error', true), + ], + }), + }); + + const tests = { + "valid responses": { + network: validNetwork, + networkStatus: validNetworkStatus, + networkOptions: validNetworkOptions, + + err: null, + }, + "invalid network status": { + network: validNetwork, + networkStatus: invalidNetworkStatus, + networkOptions: validNetworkOptions, + + err: "BlockIdentifier is null", + }, + "invalid network options": { + network: validNetwork, + networkStatus: validNetworkStatus, + networkOptions: invalidNetworkOptions, + + err: "No Allow.operation_statuses found", + }, + "duplicate operation statuses": { + network: validNetwork, + networkStatus: validNetworkStatus, + networkOptions: duplicateStatuses, + + err: "Allow.operation_statuses contains a duplicate element: Success", + }, + "duplicate operation types": { + network: validNetwork, + networkStatus: validNetworkStatus, + networkOptions: duplicateTypes, + + err: "Allow.operation_statuses contains a duplicate element: Transfer", + }, + }; + + for (const test of Object.keys(tests)) { + const testName = test; + const testParams = tests[test]; + + it(`should pass case '${testName}' with responses`, function () { + let client; + let configuration; + + try { + client = RosettaSDK.Asserter.NewClientWithResponses( + testParams.network, + testParams.networkStatus, + testParams.networkOptions, + ); + } catch (e) { + // console.error(e) + expect(e.message).to.equal(testParams.err); + return; + } + + expect(client).to.not.equal(null); + + let thrown = false; + + try { + configuration = client.getClientConfiguration(); + } catch (e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + expect(configuration.network_identifier).to.equal(testParams.network) + expect(configuration.genesis_block_identifier).to.equal(testParams.networkStatus.genesis_block_identifier); + expect(configuration.allowed_operation_types).to.deep.equal(testParams.networkOptions.allow.operation_types); + expect(configuration.allowed_operation_statuses).to.deep.equal(testParams.networkOptions.allow.operation_statuses); + }); + + it(`should pass case '${testName}' with file`, async function () { + let dirpath; + let filepath; + let configuration; + let client; + let thrown = false; + + dirpath = await createTempDir(); + + setTimeout(() => { + // cleanup + if (fs.existsSync(filepath)) + fs.unlinkSync(filepath); + + fs.rmdirSync(dirpath); + }, 100); + + configuration = { + network_identifier: testParams.network, + genesis_block_identifier: testParams.networkStatus.genesis_block_identifier, + allowed_operation_types: testParams.networkOptions.allow.operation_types, + allowed_operation_statuses: testParams.networkOptions.allow.operation_statuses, + allowed_errors: testParams.networkOptions.allow.errors, + }; + + filepath = path.join(dirpath, 'test.json'); + fs.writeFileSync(filepath, JSON.stringify(configuration)); + + try { + client = RosettaSDK.Asserter.NewClientWithFile(filepath); + } catch(f) { + // console.error(f) + expect(f.message).to.equal(testParams.err); + return; + } + + try { + configuration = client.getClientConfiguration(); + } catch (f) { + // console.error(f); + thrown = true; + } + + expect(thrown).to.equal(false); + + expect((configuration.network_identifier)) + .to.deep.equal(c(testParams.network)) + + expect((configuration.genesis_block_identifier)) + .to.deep.equal((testParams.networkStatus.genesis_block_identifier)); + + expect((configuration.allowed_operation_types)) + .to.deep.equal((testParams.networkOptions.allow.operation_types)); + + expect((configuration.allowed_operation_statuses)) + .to.deep.equal((testParams.networkOptions.allow.operation_statuses)); + }); + } + }); + + describe('Block Tests', function () { + const asserter = new RosettaSDK.Asserter(); + + it('should successfully validate a block', async function () { + let thrown = false; + + try { + const block = new RosettaSDK.Client.BlockIdentifier(1, 'block 1'); + asserter.BlockIdentifier(block); + } catch (e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should fail when blockidentifier is null', async function () { + let thrown = false; + + try { + const block = null; + asserter.BlockIdentifier(block); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('BlockIdentifier is null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should fail due to a negative index', async function () { + let thrown = false; + + try { + const block = new RosettaSDK.Client.BlockIdentifier(-1, 'block 1'); + asserter.BlockIdentifier(block); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('BlockIdentifier.index is negative'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should detect an invalid block hash', async function () { + let thrown = false; + + try { + const block = new RosettaSDK.Client.BlockIdentifier(1, ''); + asserter.BlockIdentifier(block); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('BlockIdentifier.hash is missing'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + }); + + describe('Test Amount', function () { + const asserter = new RosettaSDK.Asserter(); + const { Amount, Currency } = RosettaSDK.Client; + + it('should correctly handle a valid amount', async function () { + let thrown = false; + + try { + const amount = new Amount('100000', new Currency('BTC', 1)); + asserter.Amount(amount); + } catch (e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should correctly handle a valid amount with no decimals', async function () { + let thrown = false; + + try { + const amount = new Amount('100000', new Currency('BTC')); + asserter.Amount(amount); + } catch (e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should correctly handle a negative amount', async function () { + let thrown = false; + + try { + const amount = new Amount('-100000', new Currency('BTC', 1)); + asserter.Amount(amount); + } catch (e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should throw when having detecting no amount', async function () { + let thrown = false; + + try { + const amount = null; + asserter.Amount(amount); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('Amount.value is missing'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when currency is missing', async function () { + let thrown = false; + + try { + const amount = new Amount('100000', null); + asserter.Amount(amount); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('Amount.currency is null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw if amount.value is not a number', async function () { + let thrown = false; + + try { + const amount = new Amount('xxxx', new Currency('BTC', 1)); + asserter.Amount(amount); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('Amount.value is not an integer: xxxx'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when detecting a non-int', async function () { + let thrown = false; + + try { + const amount = new Amount('1.1', new Currency('BTC', 1)); + asserter.Amount(amount); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('Amount.value is not an integer: 1.1'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when passing an invalid symbol', async function () { + let thrown = false; + + try { + const amount = new Amount('11', new Currency(null, 1)); + asserter.Amount(amount); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('Amount.currency does not have a symbol'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when detecting invalid decimals', async function () { + let thrown = false; + + try { + const amount = new Amount('111', new Currency('BTC', -1)); + asserter.Amount(amount); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('Amount.currency.decimals must be positive. Found: -1'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + }); + + describe('Test OperationIdentifier', function () { + const asserter = new RosettaSDK.Asserter(); + const { OperationIdentifier } = RosettaSDK.Client; + + const validNetworkIndex = 1; + const invalidNetworkIndex = -1; + + it('should assert a valid identifier', async function () { + let thrown = false; + + try { + const opId = new OperationIdentifier(0); + const index = 0; + asserter.OperationIdentifier(opId, index); + } catch (e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should throw when passing null as identifier', async function () { + let thrown = false; + + try { + const opId = null; + const index = 0; + asserter.OperationIdentifier(opId, index); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('OperationIdentifier is null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when passing out of order index', async function () { + let thrown = false; + + try { + const opId = new OperationIdentifier(0); + const index = 1; + asserter.OperationIdentifier(opId, index); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('OperationIdentifier.index 0 is out of order, expected 1'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should assert a valid identifier with a networkIndex properly', async function () { + let thrown = false; + + try { + const opId = new OperationIdentifier(0); + opId.network_index = validNetworkIndex; + + const index = 0; + asserter.OperationIdentifier(opId, index); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('OperationIdentifier.index 0 is out of order, expected 1'); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should throw when passing a valid identifier with an invalid networkIndex', async function () { + let thrown = false; + + try { + const opId = new OperationIdentifier(0); + opId.network_index = invalidNetworkIndex; + + const index = 0; + asserter.OperationIdentifier(opId, index); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('OperationIdentifier.network_index is invalid'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + }); + + describe('Test AccountIdentifier', function () { + const asserter = new RosettaSDK.Asserter(); + const { AccountIdentifier, SubAccountIdentifier } = RosettaSDK.Client; + + it('should assert a valid identifier properly', async function () { + let thrown = false; + + try { + const accId = new AccountIdentifier('acct1'); + asserter.AccountIdentifier(accId); + } catch (e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should throw when passing an invalid address', async function () { + let thrown = false; + + try { + const accId = new AccountIdentifier(''); + asserter.AccountIdentifier(accId); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('Account.address is missing'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should assert a valid identifier with subaccount', async function () { + let thrown = false; + + try { + const accId = new AccountIdentifier('acct1'); + accId.sub_account = new SubAccountIdentifier('acct2'); + asserter.AccountIdentifier(accId); + } catch (e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('throw when passing an invalid identifier with subaccount', async function () { + let thrown = false; + + try { + const accId = new AccountIdentifier('acct1'); + accId.sub_account = new SubAccountIdentifier(''); + asserter.AccountIdentifier(accId); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('Account.sub_account.address is missing'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + }); + + describe('Test Operation', function () { + const asserter = new RosettaSDK.Asserter(); + const { + Amount, + Currency, + OperationIdentifier, + Operation, + AccountIdentifier, + NetworkIdentifier, + BlockIdentifier, + Peer, + NetworkOptionsResponse, + NetworkStatusResponse, + Version, + Allow, + OperationStatus, + } = RosettaSDK.Client; + + const validAmount = new Amount('1000', new Currency('BTC', 8)); + const validAccount = new AccountIdentifier('test'); + + const tests = { + 'valid operation': { + operation: Operation.constructFromObject({ + operation_identifier: new OperationIdentifier(1), + type: 'PAYMENT', + status: 'SUCCESS', + account: validAccount, + amount: validAmount, + }), + index: 1, + successful: true, + err: null, + }, + 'valid operation no account': { + operation: Operation.constructFromObject({ + operation_identifier: new OperationIdentifier(1), + type: 'PAYMENT', + status: 'SUCCESS', + }), + index: 1, + successful: true, + err: null, + }, + 'nil operation': { + operation: null, + index: 1, + err: 'Operation is null', + }, + 'invalid operation no account': { + operation: Operation.constructFromObject({ + operation_identifier: new OperationIdentifier(1), + type: 'PAYMENT', + status: 'SUCCESS', + amount: validAmount, + }), + index: 1, + err: 'Account is null', + }, + 'invalid operation empty account': { + operation: Operation.constructFromObject({ + operation_identifier: new OperationIdentifier(1), + type: 'PAYMENT', + status: 'SUCCESS', + account: new AccountIdentifier(), + amount: validAmount, + }), + index: 1, + err: 'Account.address is missing', + }, + 'invalid operation invalid index': { + operation: Operation.constructFromObject({ + operation_identifier: new OperationIdentifier(1), + type: 'PAYMENT', + status: 'SUCCESS', + }), + index: 2, + err: 'OperationIdentifier.index 1 is out of order, expected 2', + }, + 'invalid operation invalid type': { + operation: Operation.constructFromObject({ + operation_identifier: new OperationIdentifier(1), + type: 'STAKE', + status: 'SUCCESS', + }), + index: 1, + err: 'Operation.type STAKE is invalid', + }, + 'unsuccessful operation': { + operation: Operation.constructFromObject({ + operation_identifier: new OperationIdentifier(1), + type: 'PAYMENT', + status: 'FAILURE', + }), + index: 1, + successful: false, + err: null, + }, + 'invalid operation invalid status': { + operation: Operation.constructFromObject({ + operation_identifier: new OperationIdentifier(1), + type: 'PAYMENT', + status: 'DEFERRED', + }), + index: 1, + err: 'OperationStatus.status DEFERRED is not valid within this Asserter', + }, + 'valid construction operation': { + operation: Operation.constructFromObject({ + operation_identifier: new OperationIdentifier(1), + type: 'PAYMENT', + account: validAccount, + amount: validAmount, + }), + index: 1, + successful: false, + construction: true, + err: null, + }, + 'invalid construction operation': { + operation: Operation.constructFromObject({ + operation_identifier: new OperationIdentifier(1), + type: 'PAYMENT', + status: 'SUCCESS', + account: validAccount, + amount: validAmount, + }), + index: 1, + successful: false, + construction: true, + err: 'Operation.status must be empty for construction', + }, + }; + + for (let testName of Object.keys(tests)) { + const testParams = tests[testName]; + + const networkIdentifier = new NetworkIdentifier('hello', 'world'); + + const networkStatusResponse = NetworkStatusResponse.constructFromObject({ + genesis_block_identifier: new BlockIdentifier(0, 'block 0'), + current_block_identifier: new BlockIdentifier(100, 'block 100'), + current_block_timestamp: RosettaSDK.Asserter.MinUnixEpoch + 1, + peers: [ new Peer('peer 1') ], + }); + + const networkOptionsResponse = NetworkOptionsResponse.constructFromObject({ + version: new Version('1.4.0', '1.0'), + allow: new Allow([ + new OperationStatus('SUCCESS', true), + new OperationStatus('FAILURE', false), + ], ['PAYMENT']), + }); + + let asserter; + + try { + asserter = RosettaSDK.Asserter.NewClientWithResponses( + networkIdentifier, + networkStatusResponse, + networkOptionsResponse, + ); + } catch (e) { + console.error(e); + } + + it(`should pass test case '${testName}'`, async function () { + expect(asserter).to.not.equal(undefined); + + let thrown = false; + + try { + asserter.Operation(testParams.operation, testParams.index, testParams.construction); + } catch (e) { + // console.error(e); + expect(e.message).to.equal(testParams.err); + thrown = true; + } + + expect(thrown).to.equal(testParams.err != null); + + if (!thrown && !testParams.construction) { + let success; + + try { + success = asserter.OperationSuccessful(testParams.operation); + } catch (e) { + console.error(e); + thrown = true; + } finally { + expect(thrown).to.equal(false); + expect(success).to.equal(testParams.successful); + } + } + }); + } + }); + + describe('ConstructionMetadataResponse Tests', function () { + const asserter = new RosettaSDK.Asserter(); + + const { + ConstructionMetadataResponse, + } = RosettaSDK.Client; + + it('should assert a valid response', async function () { + let thrown = false; + + let metadata = { + }; + + let response = new ConstructionMetadataResponse(metadata); + + try { + asserter.ConstructionMetadataResponse(response) + } catch (e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should throw on a null response', async function () { + let thrown = false; + + let metadata = { + }; + + let response = null; + + try { + asserter.ConstructionMetadataResponse(response) + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('ConstructionMetadataResponse cannot be null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw on invalid metadata', async function () { + let thrown = false; + + let metadata = null; + let response = new ConstructionMetadataResponse(metadata); + + try { + asserter.ConstructionMetadataResponse(response) + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('ConstructionMetadataResponse.metadata is null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + }); + + describe('ConstructionSubmitResponse Tests', function () { + const asserter = new RosettaSDK.Asserter(); + + const { + ConstructionSubmitResponse, + TransactionIdentifier, + } = RosettaSDK.Client; + + it('should assert a valid response', async function () { + let thrown = false; + + let txId = new TransactionIdentifier('tx1'); + let response = new ConstructionSubmitResponse(txId); + + try { + asserter.ConstructionSubmitResponse(response); + } catch (e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should throw on a null response', async function () { + let thrown = false; + + let response = null; + + try { + asserter.ConstructionSubmitResponse(response); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('ConstructionSubmitResponse cannot be null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw on invalid transaction identifier', async function () { + let thrown = false; + + let txId = null; + let response = new ConstructionSubmitResponse(txId); + + try { + asserter.ConstructionSubmitResponse(response); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('TransactionIdentifier is null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + }); + + describe('Network Tests', function () { + const asserter = new RosettaSDK.Asserter(); + + const { + NetworkIdentifier, + SubNetworkIdentifier, + } = RosettaSDK.Client; + + it('should assert a valid network properly', async function () { + let thrown = false; + + const network = new NetworkIdentifier('bitcoin', 'mainnet'); + + try { + asserter.NetworkIdentifier(network); + } catch (e) { + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should throw when network is null', async function () { + let thrown = false; + + const network = null; + + try { + asserter.NetworkIdentifier(network); + } catch (e) { + // console.error(e) + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('NetworkIdentifier is null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when asserting an invalid network (blockchain missing)', async function () { + let thrown = false; + + const network = new NetworkIdentifier('', 'mainnet'); + + try { + asserter.NetworkIdentifier(network); + } catch (e) { + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('NetworkIdentifier.blockchain is missing'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when asserting an invalid network (network missing)', async function () { + let thrown = false; + + const network = new NetworkIdentifier('bitcoin', ''); + + try { + asserter.NetworkIdentifier(network); + } catch (e) { + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('NetworkIdentifier.network is missing'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should correctly assert a valid subnetwork', async function () { + let thrown = false; + + const network = new NetworkIdentifier('bitcoin', 'mainnet'); + network.sub_network_identifier = new SubNetworkIdentifier('shard 1'); + + try { + asserter.NetworkIdentifier(network); + } catch (e) { + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should throw when passing an invalid subnetwork', async function () { + let thrown = false; + + const network = new NetworkIdentifier('bitcoin', 'mainnet'); + network.sub_network_identifier = new SubNetworkIdentifier(); + + try { + asserter.NetworkIdentifier(network); + } catch (e) { + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('NetworkIdentifier.sub_network_identifier.network is missing'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + }); + + describe('Test Version', function () { + const asserter = new RosettaSDK.Asserter(); + + const { + Version, + } = RosettaSDK.Client; + + const middlewareVersion = '1.2'; + const invalidMiddlewareVersion = ''; + const validRosettaVersion = '1.4.0'; + + it('should assert a valid version correctly', async function () { + let thrown = false; + + let version = Version.constructFromObject({ + rosetta_version: validRosettaVersion, + node_version: '1.0', + }); + + try { + asserter.Version(version); + } catch(e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should assert a valid version with middleware correctly', async function () { + let thrown = false; + + let version = Version.constructFromObject({ + rosetta_version: validRosettaVersion, + node_version: '1.0', + middleware_version: middlewareVersion, + }); + + try { + asserter.Version(version); + } catch(e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should throw on too old rosetta version', async function () { + let thrown = false; + + let version = Version.constructFromObject({ + rosetta_version: '1.2.0', + node_version: '1.0', + }); + + try { + asserter.Version(version); + } catch(e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should throw on null version', async function () { + let thrown = false; + + let version = null; + + try { + asserter.Version(version); + } catch(e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('Version is null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw on invalid node version', async function () { + let thrown = false; + + let version = Version.constructFromObject({ + rosetta_version: '1.2.0', + }); + + try { + asserter.Version(version); + } catch(e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('Version.node_version is missing'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw on invalid middleware version', async function () { + let thrown = false; + + let version = Version.constructFromObject({ + rosetta_version: validRosettaVersion, + node_version: '1.0', + middleware_version: invalidMiddlewareVersion, + }); + + try { + asserter.Version(version); + } catch(e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('Version.middleware_version is missing'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + }); + + describe('Test Allow', function () { + const asserter = new RosettaSDK.Asserter(); + + const { + OperationStatus, + Allow, + } = RosettaSDK.Client; + + const operationStatuses = [ + new OperationStatus('SUCCESS', true), + new OperationStatus('FAILURE', false), + ]; + + const operationTypes = ['PAYMENT']; + + it('should assert a valid allow correctly', async function () { + let thrown = false; + + let allow = new Allow(operationStatuses, operationTypes); + + try { + asserter.Allow(allow); + + } catch(e) { + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should throw when null is passed', async function () { + let thrown = false; + + let allow = null; + + try { + asserter.Allow(allow); + + } catch(e) { + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('Allow is null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when no operationStatuses are found', async function () { + let thrown = false; + + let allow = new Allow(null, operationTypes); + + try { + asserter.Allow(allow); + + } catch(e) { + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('No Allow.operation_statuses found'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when no successful statuses are found', async function () { + let thrown = false; + + let allow = new Allow([operationStatuses[1]], operationTypes); + + try { + asserter.Allow(allow); + + } catch(e) { + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('No successful Allow.operation_statuses found'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when no operation types exist', async function () { + let thrown = false; + + let allow = new Allow(operationStatuses, null); + + try { + asserter.Allow(allow); + + } catch(e) { + // console.error(e) + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('No Allow.operation_statuses found'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + }); + + describe('Test Error', function () { + const asserter = new RosettaSDK.Asserter(); + + it('should assert a valid error correctly', async function () { + let thrown = false; + + const error = new RosettaSDK.Client.Error(12, 'signature invalid'); + + try { + asserter.Error(error); + } catch (e) { + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should throw when passing nil', async function () { + let thrown = false; + + const error = null; + + try { + asserter.Error(error); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('Error is null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw on negative error codes', async function () { + let thrown = false; + + const error = new RosettaSDK.Client.Error(-1, 'signature invalid'); + + try { + asserter.Error(error); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('Error.code is negative'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw on empty error message', async function () { + let thrown = false; + + const error = new RosettaSDK.Client.Error(0); + + try { + asserter.Error(error); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('Error.message is missing'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + }); + + describe('Test Errors', function () { + const asserter = new RosettaSDK.Asserter(); + + it('should assert valid errors correctly', async function () { + let thrown = false; + + const errors = [ + new RosettaSDK.Client.Error(0, 'error 1'), + new RosettaSDK.Client.Error(1, 'error 2'), + ]; + + try { + asserter.Errors(errors); + } catch (e) { + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should throw on duplicate error codes', async function () { + let thrown = false; + + const errors = [ + new RosettaSDK.Client.Error(0, 'error 1'), + new RosettaSDK.Client.Error(0, 'error 2'), + ]; + + try { + asserter.Errors(errors); + } catch (e) { + // console.error(e) + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('Error code used multiple times'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + }); + + describe('Test Valid Network List Response', function () { + const asserter = new RosettaSDK.Asserter(); + + const { + NetworkIdentifier, + SubNetworkIdentifier, + NetworkListResponse, + } = RosettaSDK.Client; + + const network1 = new NetworkIdentifier('blockchain 1', 'network 1'); + + const network1Sub = new NetworkIdentifier('blockchain 1', 'network 1'); + network1Sub.sub_network_identifier = new SubNetworkIdentifier('subnetwork'); + + const network2 = new NetworkIdentifier('blockchain 2', 'network 2'); + + const network3 = new NetworkIdentifier(null, 'network 2'); + + it('should assert a valid network list correctly', async function () { + let thrown = false; + + const networkListResponse = new NetworkListResponse([ + network1, + network1Sub, + network2, + ]); + + try { + asserter.NetworkListResponse(networkListResponse); + } catch(e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should throw when passing null', async function () { + let thrown = false; + + const networkListResponse = null; + + try { + asserter.NetworkListResponse(networkListResponse); + } catch(e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('NetworkListResponse is null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when passing a duplicate network', async function () { + let thrown = false; + + const networkListResponse = new NetworkListResponse([ + network1Sub, + network1Sub, + ]); + + try { + asserter.NetworkListResponse(networkListResponse); + } catch(e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('NetworkListResponse.Network contains duplicated'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when passing an invalid network', async function () { + let thrown = false; + + const networkListResponse = new NetworkListResponse([ + network3, + ]); + + try { + asserter.NetworkListResponse(networkListResponse); + } catch(e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('NetworkIdentifier.blockchain is missing'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + }); + + describe('Test Block', function () { + const asserter = new RosettaSDK.Asserter(); + const { + Amount, + Currency, + OperationIdentifier, + Operation, + AccountIdentifier, + NetworkIdentifier, + BlockIdentifier, + Block, + Transaction, + TransactionIdentifier, + Peer, + NetworkOptionsResponse, + NetworkStatusResponse, + Version, + Allow, + OperationStatus, + } = RosettaSDK.Client; + + const validBlockIdentifier = new BlockIdentifier(100, 'blah'); + const validParentBlockIdentifier = new BlockIdentifier(99, 'blah parent'); + + const validAmount = new Amount('1000', new Currency('BTC', 8)); + const validAccount = new AccountIdentifier('test'); + + const validTransaction = new Transaction( + new TransactionIdentifier('blah'), + [ + Operation.constructFromObject({ + operation_identifier: new OperationIdentifier(0), + type: 'PAYMENT', + status: 'SUCCESS', + account: validAccount, + amount: validAmount, + }), + + Operation.constructFromObject({ + operation_identifier: new OperationIdentifier(1), + related_operations: [ + new OperationIdentifier(0), + ], + type: 'PAYMENT', + status: 'SUCCESS', + account: validAccount, + amount: validAmount, + }), + ], + ); + + const relatedToSelfTransaction = new Transaction( + new TransactionIdentifier('blah'), + [ + Operation.constructFromObject({ + operation_identifier: new OperationIdentifier(0), + related_operations: [ + new OperationIdentifier(0), + ], + type: 'PAYMENT', + status: 'SUCCESS', + account: validAccount, + amount: validAmount, + }), + ], + ); + + const outOfOrderTransaction = new Transaction( + new TransactionIdentifier('blah'), + [ + Operation.constructFromObject({ + operation_identifier: new OperationIdentifier(1), + related_operations: [ + new OperationIdentifier(0), + ], + type: 'PAYMENT', + status: 'SUCCESS', + account: validAccount, + amount: validAmount, + }), + + Operation.constructFromObject({ + operation_identifier: new OperationIdentifier(0), + type: 'PAYMENT', + status: 'SUCCESS', + account: validAccount, + amount: validAmount, + }), + ], + ); + + const relatedToLaterTransaction = new Transaction( + new TransactionIdentifier('blah'), + [ + Operation.constructFromObject({ + operation_identifier: new OperationIdentifier(0), + related_operations: [ + new OperationIdentifier(1), + ], + type: 'PAYMENT', + status: 'SUCCESS', + account: validAccount, + amount: validAmount, + }), + + Operation.constructFromObject({ + operation_identifier: new OperationIdentifier(1), + related_operations: [ + new OperationIdentifier(0), + ], + type: 'PAYMENT', + status: 'SUCCESS', + account: validAccount, + amount: validAmount, + }), + ], + ); + + const relatedDuplicateTransaction = new Transaction( + new TransactionIdentifier('blah'), + [ + Operation.constructFromObject({ + operation_identifier: new OperationIdentifier(0), + type: 'PAYMENT', + status: 'SUCCESS', + account: validAccount, + amount: validAmount, + }), + + Operation.constructFromObject({ + operation_identifier: new OperationIdentifier(1), + related_operations: [ + new OperationIdentifier(0), + new OperationIdentifier(0), + ], + type: 'PAYMENT', + status: 'SUCCESS', + account: validAccount, + amount: validAmount, + }), + ], + ); + + const tests = { + 'valid block': { + block: Block.constructFromObject({ + block_identifier: validBlockIdentifier, + parent_block_identifier: validParentBlockIdentifier, + timestamp: RosettaSDK.Asserter.MinUnixEpoch + 1, + transactions: [validTransaction], + }), + err: null, + }, + 'genesis block': { + block: Block.constructFromObject({ + block_identifier: validBlockIdentifier, + parent_block_identifier: validBlockIdentifier, + transactions: [validTransaction], + }), + genesisIndex: validBlockIdentifier.index, + err: null, + }, + 'out of order transaction operations': { + block: Block.constructFromObject({ + block_identifier: validBlockIdentifier, + parent_block_identifier: validParentBlockIdentifier, + timestamp: RosettaSDK.Asserter.MinUnixEpoch + 1, + transactions: [outOfOrderTransaction], + }), + err: 'OperationIdentifier.index 1 is out of order, expected 0', + }, + 'related to self transaction operations': { + block: Block.constructFromObject({ + block_identifier: validBlockIdentifier, + parent_block_identifier: validParentBlockIdentifier, + timestamp: RosettaSDK.Asserter.MinUnixEpoch + 1, + transactions: [relatedToSelfTransaction], + }), + err: 'Related operation index 0 >= operation index 0', + }, + 'related to later transaction operations': { + block: Block.constructFromObject({ + block_identifier: validBlockIdentifier, + parent_block_identifier: validParentBlockIdentifier, + timestamp: RosettaSDK.Asserter.MinUnixEpoch + 1, + transactions: [relatedToLaterTransaction], + }), + err: 'Related operation index 1 >= operation index 0', + }, + 'duplicate related transaction operations': { + block: Block.constructFromObject({ + block_identifier: validBlockIdentifier, + parent_block_identifier: validParentBlockIdentifier, + timestamp: RosettaSDK.Asserter.MinUnixEpoch + 1, + transactions: [relatedDuplicateTransaction], + }), + err: 'Found duplicate related operation index 0 for operation index 1', + }, + 'nil block': { + block: null, + err: 'Block is null', + }, + 'nil block hash': { + block: Block.constructFromObject({ + block_identifier: null, + parent_block_identifier: validParentBlockIdentifier, + timestamp: RosettaSDK.Asserter.MinUnixEpoch + 1, + transactions: [validTransaction], + }), + err: 'BlockIdentifier is null', + }, + 'invalid block hash': { + block: Block.constructFromObject({ + block_identifier: new BlockIdentifier(), + parent_block_identifier: validParentBlockIdentifier, + timestamp: RosettaSDK.Asserter.MinUnixEpoch + 1, + transactions: [validTransaction], + }), + err: 'BlockIdentifier.hash is missing', + }, + 'block previous hash missing': { + block: Block.constructFromObject({ + block_identifier: validBlockIdentifier, + parent_block_identifier: new BlockIdentifier(), + timestamp: RosettaSDK.Asserter.MinUnixEpoch + 1, + transactions: [validTransaction], + }), + err: 'BlockIdentifier.hash is missing', + }, + 'invalid parent block index': { + block: Block.constructFromObject({ + block_identifier: validBlockIdentifier, + parent_block_identifier: new BlockIdentifier( + validBlockIdentifier.index, + validParentBlockIdentifier.hash, + ), + timestamp: RosettaSDK.Asserter.MinUnixEpoch + 1, + transactions: [validTransaction], + }), + err: 'BlockIdentifier.index <= ParentBlockIdentifier.index', + }, + 'invalid parent block hash': { + block: Block.constructFromObject({ + block_identifier: validBlockIdentifier, + parent_block_identifier: new BlockIdentifier( + validParentBlockIdentifier.index, + validBlockIdentifier.hash, + ), + timestamp: RosettaSDK.Asserter.MinUnixEpoch + 1, + transactions: [validTransaction], + }), + err: 'BlockIdentifier.hash == ParentBlockIdentifier.hash', + }, + 'invalid block timestamp less than RosettaSDK.Asserter.MinUnixEpoch': { + block: Block.constructFromObject({ + block_identifier: validBlockIdentifier, + parent_block_identifier: validParentBlockIdentifier, + transactions: [validTransaction], + }), + err: 'Timestamp 0 is before 01/01/2000', + }, + 'invalid block timestamp greater than MaxUnixEpoch': { + block: Block.constructFromObject({ + block_identifier: validBlockIdentifier, + parent_block_identifier: validParentBlockIdentifier, + transactions: [validTransaction], + timestamp: RosettaSDK.Asserter.MaxUnixEpoch + 1, + }), + err: 'Timestamp 2209017600001 is after 01/01/2040', + }, + 'invalid block transaction': { + block: Block.constructFromObject({ + block_identifier: validBlockIdentifier, + parent_block_identifier: validParentBlockIdentifier, + timestamp: RosettaSDK.Asserter.MinUnixEpoch + 1, + transactions: [ + new Transaction(), + ], + }), + err: 'TransactionIdentifier is null', + }, + }; + + for (let testName of Object.keys(tests)) { + const testParams = tests[testName]; + + const networkIdentifier = new NetworkIdentifier('hello', 'world'); + + const genesisIndex = testParams.genesisIndex != null ? testParams.genesisIndex : 0; + + const networkStatusResponse = NetworkStatusResponse.constructFromObject({ + genesis_block_identifier: new BlockIdentifier(genesisIndex, `block ${genesisIndex}`), + current_block_identifier: new BlockIdentifier(100, 'block 100'), + current_block_timestamp: RosettaSDK.Asserter.MinUnixEpoch + 1, + peers: [ new Peer('peer 1') ], + }); + + const networkOptionsResponse = NetworkOptionsResponse.constructFromObject({ + version: new Version('1.4.0', '1.0'), + allow: new Allow([ + new OperationStatus('SUCCESS', true), + new OperationStatus('FAILURE', false), + ], ['PAYMENT']), + }); + + let asserter; + + try { + asserter = RosettaSDK.Asserter.NewClientWithResponses( + networkIdentifier, + networkStatusResponse, + networkOptionsResponse, + ); + } catch (e) { + console.error(e); + } + + it(`should pass test case '${testName}'`, async function () { + expect(asserter).to.not.equal(undefined); + + let thrown = false; + + try { + asserter.Block(testParams.block); + } catch (e) { + // console.error(e); + expect(e.message).to.equal(testParams.err); + thrown = true; + } + + expect(thrown).to.equal(testParams.err != null); + }); + + } + }); + + describe('Test Server', function () { + const { + NetworkIdentifier, + AccountBalanceRequest, + PartialBlockIdentifier, + TransactionIdentifier, + AccountIdentifier, + Currency, + OperationIdentifier, + Operation, + BlockIdentifier, + BlockTransactionRequest, + ConstructionMetadataRequest, + ConstructionSubmitRequest, + MempoolTransactionRequest, + BlockRequest, + MetadataRequest, + NetworkRequest, + Amount, + } = RosettaSDK.Client; + + const validNetworkIdentifier = NetworkIdentifier.constructFromObject({ + blockchain: 'Bitcoin', + network: 'Mainnet', + }); + + const wrongNetworkIdentifier = NetworkIdentifier.constructFromObject({ + blockchain: 'Bitcoin', + network: 'Testnet', + }); + + const validAccountIdentifier = new AccountIdentifier('acct1'); + + const genesisBlockIndex = 0; + const validBlockIndex = 1000; + const validPartialBlockIdentifier = PartialBlockIdentifier.constructFromObject({ + index: validBlockIndex, + }); + + const validBlockIdentifier = BlockIdentifier.constructFromObject({ + index: validBlockIndex, + hash: 'block 1', + }) + + const validTransactionIdentifier = new TransactionIdentifier('tx1'); + + /* + const validPublicKey = PublicKey.constructFromObject({ + bytes: []byte('hello'), + curve_type: Secp256k1, + }) + */ + + const validAmount = Amount.constructFromObject({ + value: '1000', + currency: Currency.constructFromObject({ + symbol: 'BTC', + decimals: 8, + }), + }); + + const validAccount = new AccountIdentifier('test'); + + const validOps = [ + Operation.constructFromObject({ + operation_identifier: new OperationIdentifier(0), + type: 'PAYMENT', + account: validAccount, + amount: validAmount, + }), + Operation.constructFromObject({ + operation_identifier: new OperationIdentifier(1), + related_operations: [ + new OperationIdentifier(0), + ], + type: 'PAYMENT', + account: validAccount, + amount: validAmount, + }), + ]; + + const unsupportedTypeOps = [ + Operation.constructFromObject({ + operation_identifier: new OperationIdentifier(0), + type: 'STAKE', + account: validAccount, + amount: validAmount, + }), + Operation.constructFromObject({ + operation_identifier: new OperationIdentifier(1), + related_operations: [ + new OperationIdentifier(0), + ], + type: 'PAYMENT', + account: validAccount, + amount: validAmount, + }), + ]; + + const invalidOps = [ + Operation.constructFromObject({ + operation_identifier: new OperationIdentifier(0), + type: 'PAYMENT', + status: 'SUCCESS', + account: validAccount, + amount: validAmount, + }), + Operation.constructFromObject({ + operation_identifier: new OperationIdentifier(1), + related_operations: [ + new OperationIdentifier(0), + ], + type: 'PAYMENT', + status: 'SUCCESS', + account: validAccount, + amount: validAmount, + }), + ]; + + /* + const validSignatures = [ + { + signing_payload: &types.SigningPayload{ + address: validAccount.Address, + bytes: []byte('blah'), + }, + public_key: validPublicKey, + signature_type: types.Ed25519, + bytes: []byte('hello'), + }, + ]; + + const signatureTypeMismatch = [ + { + signing_payload: &types.SigningPayload{ + address: validAccount.Address, + bytes: []byte('blah'), + signature_type: types.EcdsaRecovery, + }, + public_key: validPublicKey, + signature_type: types.Ed25519, + bytes: []byte('hello'), + }, + ]; + + const signatureTypeMatch = [ + { + signing_payload: &types.SigningPayload{ + address: validAccount.Address, + bytes: []byte('blah'), + signature_type: types.Ed25519, + }, + public_key: validPublicKey, + signature_type: types.Ed25519, + bytes: []byte('hello'), + }, + ]; + + const emptySignature = [ + { + signing_payload: &types.SigningPayload{ + address: validAccount.Address, + bytes: []byte('blah'), + signature_type: types.Ed25519, + }, + public_key: validPublicKey, + signature_type: types.Ed25519, + }, + ]; + */ + + const asserter = RosettaSDK.Asserter.NewServer( + ['PAYMENT'], + true, // allowHistorical + [validNetworkIdentifier], + ); + + describe('Test SupportedNetworks', function () { + it('should assert valid network identifiers correctly', async function () { + let thrown = false; + + const networks = [ + validNetworkIdentifier, + wrongNetworkIdentifier, + ]; + + try { + asserter.SupportedNetworks(networks); + } catch (e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should throw when passing no networks', async function () { + let thrown = false; + + const networks = [ + ]; + + try { + asserter.SupportedNetworks(networks); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('NetworkIdentifier Array contains no supported networks'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when returning an invalid network', async function () { + let thrown = false; + + const networks = [ + new NetworkIdentifier('blah'), + ]; + + try { + asserter.SupportedNetworks(networks); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('NetworkIdentifier.network is missing'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when returning duplicate networks', async function () { + let thrown = false; + + const networks = [ + validNetworkIdentifier, + validNetworkIdentifier, + ]; + + try { + asserter.SupportedNetworks(networks); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('SupportedNetwork has a duplicate: {"blockchain":"Bitcoin","network":"Mainnet"}'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + }); + + describe('Test AccountBalanceRequest', function () { + const createServer = (allowHistorical) => { + const server = RosettaSDK.Asserter.NewServer( + ['PAYMENT'], + allowHistorical, + [validNetworkIdentifier], + ); + + return server; + } + + it('should assert valid balance request correctly', async function () { + let thrown = false; + + const server = createServer(false); + const request = new AccountBalanceRequest(validNetworkIdentifier, validAccountIdentifier); + + try { + server.AccountBalanceRequest(request); + } catch (e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should throw when requesting account with invalid network', async function () { + let thrown = false; + + const server = createServer(false); + const request = new AccountBalanceRequest(wrongNetworkIdentifier, validAccountIdentifier); + + try { + server.AccountBalanceRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('Network {"blockchain":"Bitcoin","network":"Testnet"} is not supported'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when passing null as a request', async function () { + let thrown = false; + + const server = createServer(false); + const request = null; + + try { + server.AccountBalanceRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('AccountBalanceRequest is null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when passing a request without a network specifier', async function () { + let thrown = false; + + const server = createServer(false); + const request = new AccountBalanceRequest(null, validAccountIdentifier); + + try { + server.AccountBalanceRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('NetworkIdentifier is null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when passing a request without an account specifier', async function () { + let thrown = false; + + const server = createServer(false); + const request = new AccountBalanceRequest(validNetworkIdentifier, null); + + try { + server.AccountBalanceRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('Account is null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should handle a valid historical request properly', async function () { + let thrown = false; + + const server = createServer(true); + const request = new AccountBalanceRequest(validNetworkIdentifier, validAccountIdentifier); + + try { + server.AccountBalanceRequest(request); + } catch (e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should throw when passing an invalid historical request', async function () { + let thrown = false; + + const server = createServer(true); + const request = new AccountBalanceRequest(validNetworkIdentifier, validAccountIdentifier); + request.block_identifier = new PartialBlockIdentifier(); + + try { + server.AccountBalanceRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('Neither PartialBlockIdentifier.hash nor PartialBlockIdentifier.index is set'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when historical request is not available', async function () { + let thrown = false; + + const server = createServer(false); + const request = new AccountBalanceRequest(validNetworkIdentifier, validAccountIdentifier); + request.block_identifier = validPartialBlockIdentifier; + + try { + server.AccountBalanceRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('historical balance loopup is not supported'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + }); + + describe('Test BlockRequest', function () { + it('should assert a valid request properly', async function () { + let thrown = false; + + const request = new BlockRequest(validNetworkIdentifier, validPartialBlockIdentifier); + + try { + asserter.BlockRequest(request); + } catch (e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should assert a valid request for block 0 properly', async function () { + let thrown = false; + + const request = new BlockRequest(validNetworkIdentifier, PartialBlockIdentifier.constructFromObject({ + index: genesisBlockIndex, + })); + + try { + asserter.BlockRequest(request); + } catch (e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should throw when requesting an invalid network', async function () { + let thrown = false; + + const request = new BlockRequest(wrongNetworkIdentifier, validPartialBlockIdentifier); + + try { + asserter.BlockRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('Network {"blockchain":"Bitcoin","network":"Testnet"} is not supported'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when passing null', async function () { + let thrown = false; + + const request = null; + + try { + asserter.BlockRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('BlockRequest is null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when requesting a block without a network specifier', async function () { + let thrown = false; + + const request = new BlockRequest(); + request.block_identifier = validPartialBlockIdentifier; + + try { + asserter.BlockRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('NetworkIdentifier is null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when requesting a block without a block identifier', async function () { + let thrown = false; + + const request = new BlockRequest(validNetworkIdentifier); + + try { + asserter.BlockRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('PartialBlockIdentifier is null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when requesting an invalid partialBlockIdentifier', async function () { + let thrown = false; + + const request = new BlockRequest(validNetworkIdentifier, new PartialBlockIdentifier()); + + try { + asserter.BlockRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('Neither PartialBlockIdentifier.hash nor PartialBlockIdentifier.index is set'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + }); + + describe('Test BlockTransactionRequest', function () { + it('should assert a valid request properly', async function () { + let thrown = false; + + const request = new BlockTransactionRequest( + validNetworkIdentifier, + validBlockIdentifier, + validTransactionIdentifier + ); + + try { + asserter.BlockTransactionRequest(request); + } catch (e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should throw when passing an invalid network', async function () { + let thrown = false; + + const request = new BlockTransactionRequest( + wrongNetworkIdentifier, + validBlockIdentifier, + validTransactionIdentifier + ); + + try { + asserter.BlockTransactionRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('Network {"blockchain":"Bitcoin","network":"Testnet"} is not supported'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when passing null', async function () { + let thrown = false; + + const request = null; + + try { + asserter.BlockTransactionRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('BlockTransactionRequest is null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when the request is missing a network', async function () { + let thrown = false; + + const request = new BlockTransactionRequest( + null, + validBlockIdentifier, + validTransactionIdentifier + ); + + try { + asserter.BlockTransactionRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('NetworkIdentifier is null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when the request is missing a block identifier', async function () { + let thrown = false; + + const request = new BlockTransactionRequest( + validNetworkIdentifier, + null, + validTransactionIdentifier + ); + + try { + asserter.BlockTransactionRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('BlockIdentifier is null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when the request\'s blockIdentifier is invalid', async function () { + let thrown = false; + + const request = new BlockTransactionRequest( + validNetworkIdentifier, + new BlockIdentifier(), + ); + + try { + asserter.BlockTransactionRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('BlockIdentifier.hash is missing'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + }); + + describe('Test ConstructionMetadataRequest', function () { + it('should assert a valid request properly', async function () { + let thrown = false; + + const request = new ConstructionMetadataRequest( + validNetworkIdentifier, + {}, // options + ); + + try { + asserter.ConstructionMetadataRequest(request); + } catch (e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should throw when a wrong network was specified', async function () { + let thrown = false; + + const request = new ConstructionMetadataRequest( + wrongNetworkIdentifier, + {}, // options + ); + + try { + asserter.ConstructionMetadataRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('Network {"blockchain":"Bitcoin","network":"Testnet"} is not supported'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when the request is null', async function () { + let thrown = false; + + const request = null; + + try { + asserter.ConstructionMetadataRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('ConstructionMetadataRequest is null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when the request is missing a network', async function () { + let thrown = false; + + const request = new ConstructionMetadataRequest( + null, + {}, // options + ); + + try { + asserter.ConstructionMetadataRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('NetworkIdentifier is null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when the request is missing options', async function () { + let thrown = false; + + const request = new ConstructionMetadataRequest( + validNetworkIdentifier, + null, + ); + + try { + asserter.ConstructionMetadataRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('ConstructionMetadataRequest.options is null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + }); + + describe('Test ConstructionSubmitRequest', function () { + it('should assert the request properly', async function () { + let thrown = false; + + const request = new ConstructionSubmitRequest( + validNetworkIdentifier, + 'tx', + ); + + try { + asserter.ConstructionSubmitRequest(request); + } catch (e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should throw when the request is missing options', async function () { + let thrown = false; + + const request = new ConstructionSubmitRequest( + wrongNetworkIdentifier, + 'tx', + ); + + try { + asserter.ConstructionSubmitRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('Network {"blockchain":"Bitcoin","network":"Testnet"} is not supported'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw the request is null', async function () { + let thrown = false; + + const request = null; + + try { + asserter.ConstructionSubmitRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('ConstructionSubmitRequest.options is null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when the request has no transaction', async function () { + let thrown = false; + + const request = new ConstructionSubmitRequest( + ); + + try { + asserter.ConstructionSubmitRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('NetworkIdentifier is null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + }); + + describe('Test MempoolTransactionRequest', function () { + it('should assert a valid request properly', async function () { + let thrown = false; + + const request = new MempoolTransactionRequest( + validNetworkIdentifier, + validTransactionIdentifier, + ); + + try { + asserter.MempoolTransactionRequest(request); + } catch (e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should throw when the specified network not supported', async function () { + let thrown = false; + + const request = new MempoolTransactionRequest( + wrongNetworkIdentifier, + validTransactionIdentifier, + ); + + try { + asserter.MempoolTransactionRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('Network {"blockchain":"Bitcoin","network":"Testnet"} is not supported'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw then the request is null', async function () { + let thrown = false; + + const request = null; + + try { + asserter.MempoolTransactionRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('MempoolTransactionRequest is null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw then the request is missing a network', async function () { + let thrown = false; + + const request = new MempoolTransactionRequest( + null, + validTransactionIdentifier, + );; + + try { + asserter.MempoolTransactionRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('NetworkIdentifier is null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when the TransactionIdentifier is invalid', async function () { + let thrown = false; + + const request = new MempoolTransactionRequest( + validNetworkIdentifier, + new TransactionIdentifier(), + ); + + try { + asserter.MempoolTransactionRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('TransactionIdentifier.hash is missing'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + }); + + describe('Test MetadataRequest', function () { + it('should assert a valid request properly', async function () { + let thrown = false; + + const request = new MetadataRequest(); + + try { + asserter.MetadataRequest(request); + } catch (e) { + // console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should throw when the request is null', async function () { + let thrown = false; + + const request = null; + + try { + asserter.MempoolTransactionRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('MempoolTransactionRequest is null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + }); + + describe('Test NetworkRequest', function () { + it('should assert a valid request properly', async function () { + let thrown = false; + + const request = new NetworkRequest(validNetworkIdentifier); + + try { + asserter.NetworkRequest(request); + } catch (e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should throw when the request has a unsupported network identifier', async function () { + let thrown = false; + + const request = new NetworkRequest(wrongNetworkIdentifier); + + try { + asserter.NetworkRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('Network {"blockchain":"Bitcoin","network":"Testnet"} is not supported'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when the request is null', async function () { + let thrown = false; + + const request = null; + + try { + asserter.NetworkRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('NetworkRequest is null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should throw when the request is missing a network', async function () { + let thrown = false; + + const request = new NetworkRequest(); + + try { + asserter.NetworkRequest(request); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('AsserterError'); + expect(e.message).to.equal('NetworkIdentifier is null'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + }); + + describe('Test ConstructionDeriveRequest', function () { + // ToDo + }); + + describe('Test ConstructionPreprocessRequest', function () { + // ToDo + }); + + describe('Test ConstructionPayloadsRequest', function () { + // ToDo + }); + + describe('Test ConstructionCombineRequest', function () { + // ToDo + }); + + describe('Test ConstructionHashRequest', function () { + // ToDo + }); + + describe('Test ConstructionParseRequest', function () { + // ToDo + }); + }); + + describe('Contains Currency', function () { + const asserter = new RosettaSDK.Asserter(); + + it('should properly check if a currency is contained', async function () { + const toFind = new T.Currency('BTC', 8); + + const currencies = [ + new T.Currency('BTC', 8), + ]; + + const result = asserter.containsCurrency(currencies, toFind); + + expect(result).to.equal(true); + }); + + it('should handle complex contains', async function () { + const toFind = T.Currency.constructFromObject({ + symbol: 'BTC', + decimals: 8, + metadata: { + 'blah': 'hello', + }, + }); + + const currencies = [ + T.Currency.constructFromObject({ + symbol: 'BTC', + decimals: 8, + metadata: { + 'blah': 'hello', + }, + }), + ]; + + const result = asserter.containsCurrency(currencies, toFind); + + expect(result).to.equal(true); + }); + + it('should handle more complex contains', async function () { + const toFind = T.Currency.constructFromObject({ + symbol: 'BTC', + decimals: 8, + metadata: { + 'blah': 'hello', + 'blah2': 'bye', + }, + }); + + const currencies = [ + T.Currency.constructFromObject({ + symbol: 'BTC', + decimals: 8, + metadata: { + 'blah': 'hello', + 'blah2': 'bye', + }, + }), + ]; + + const result = asserter.containsCurrency(currencies, toFind); + + expect(result).to.equal(true); + }); + + it('should not find a currency in an empty currency array', async function () { + const toFind = T.Currency.constructFromObject({ + symbol: 'BTC', + decimals: 8, + }); + + const currencies = []; + + const result = asserter.containsCurrency(currencies, toFind); + + expect(result).to.equal(false); + }); + + it('should not find a currency with a different symbol', async function () { + const toFind = T.Currency.constructFromObject({ + symbol: 'BTC', + decimals: 8, + }); + + const currencies = [ + T.Currency.constructFromObject({ + symbol: 'ERX', + decimals: 8, + }), + ]; + + const result = asserter.containsCurrency(currencies, toFind); + + expect(result).to.equal(false); + }); + + it('should not find a currency with different decimals', async function () { + const toFind = T.Currency.constructFromObject({ + symbol: 'BTC', + decimals: 8, + }); + + const currencies = [ + T.Currency.constructFromObject({ + symbol: 'BTC', + decimals: 6, + }), + ]; + + const result = asserter.containsCurrency(currencies, toFind); + + expect(result).to.equal(false); + }); + + it('should not find a currency with different metadata', async function () { + const toFind = T.Currency.constructFromObject({ + symbol: 'BTC', + decimals: 8, + metadata: { + 'blah': 'hello', + }, + }); + + const currencies = [ + T.Currency.constructFromObject({ + symbol: 'BTC', + decimals: 8, + metadata: { + 'blah': 'bye', + }, + }), + ]; + + const result = asserter.containsCurrency(currencies, toFind); + + expect(result).to.equal(false); + }); + }); + + describe('Account Balance', function () { + const asserter = new RosettaSDK.Asserter(); + + const validBlock = T.BlockIdentifier.constructFromObject({ + index: 1000, + hash: 'jsakdl', + }); + + const invalidBlock = T.BlockIdentifier.constructFromObject({ + index: 1, + hash: '', + }); + + const invalidIndex = 1001; + const invalidHash = 'ajsdk'; + const validAmount = T.Amount.constructFromObject({ + value: '100', + currency: T.Currency.constructFromObject({ + symbol: 'BTC', + decimals: 8, + }), + }); + + it('should properly encode a simple balance', () => { + let thrown = false; + try { + asserter.AccountBalanceResponse(null, validBlock, [ validAmount ]); + } catch (e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should detect an invalid block', () => { + let thrown = false; + try { + asserter.AccountBalanceResponse(null, invalidBlock, [ validAmount ]); + } catch (e) { + // console.error(e); + expect(e.message).to.equal('BlockIdentifier.hash is missing'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should detect duplicate currencies', () => { + let thrown = false; + try { + asserter.AccountBalanceResponse(null, validBlock, [ validAmount, validAmount ]); + } catch (e) { + // console.error(e); + expect(e.message).to.equal('Currency BTC used in balance multiple times'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should assert history request index as valid', () => { + let thrown = false; + try { + const req = T.PartialBlockIdentifier.constructFromObject({ + index: validBlock.index, + }); + asserter.AccountBalanceResponse(req, validBlock, [ validAmount ]); + } catch (e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should assert valid history request hash as valid', () => { + let thrown = false; + try { + const req = T.PartialBlockIdentifier.constructFromObject({ + hash: validBlock.hash, + }); + asserter.AccountBalanceResponse(req, validBlock, [ validAmount ]); + } catch (e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should assert valid history request as valid', () => { + let thrown = false; + try { + const req = constructPartialBlockIdentifier(validBlock); + asserter.AccountBalanceResponse(req, validBlock, [ validAmount ]); + } catch (e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should assert valid history request index as invalid', () => { + let thrown = false; + try { + const req = T.PartialBlockIdentifier.constructFromObject({ + hash: validBlock.hash, + index: invalidIndex, + }); + asserter.AccountBalanceResponse(req, validBlock, [ validAmount ]); + } catch (e) { + // console.error(e); + expect(e.message).to.equal('Request Index 1001 does not match Response block index 1000'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should assert invalid historical request hash as invalid', () => { + let thrown = false; + try { + const req = T.PartialBlockIdentifier.constructFromObject({ + hash: invalidHash, + index: validBlock.index, + }); + asserter.AccountBalanceResponse(req, validBlock, [ validAmount ]); + } catch (e) { + // console.error(e); + expect(e.message).to.equal('Request BlockHash ajsdk does not match Response block hash jsakdl'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + }); +}); \ No newline at end of file diff --git a/test/fetcher.test.js b/test/fetcher.test.js new file mode 100644 index 0000000..aa2442f --- /dev/null +++ b/test/fetcher.test.js @@ -0,0 +1,541 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +// fetcher.test.js +const { expect } = require('chai'); +const Rosetta = require('..'); +const { constructPartialBlockIdentifier } = require('../lib/utils'); +const bodyParser = require('body-parser'); + +let START_PORT = 8000; + +function getPort() { + return START_PORT++; +} + +const c = (arg) => JSON.parse(JSON.stringify(arg)); + +const basicNetwork = { + blockchain: "blockchain", + network: "network", +}; + +const basicAccount = { + address: "address", +}; + +const basicBlock = { + index: 10, + hash: "block 10", +}; + +const basicAmounts = [{ + value: "1000", + currency: { + symbol: "BTC", + decimals: 8, + }, +}]; + +const basicFullBlock = { + block_identifier: basicBlock, + parent_block_identifier: { + index: 9, + hash: "block 9", + }, + timestamp: 1582833600000, +}; + +const basicNetworkStatus = { + current_block_identifier: basicBlock, + current_block_timestamp: 1582833600000, + genesis_block_identifier: { + index: 0, + hash: "block 0", + }, +}; + +const basicNetworkList = [ + basicNetwork, +]; + + +const basicNetworkOptions = { + version: { + rosetta_version: "1.4.0", + node_version: "0.0.1", + }, + allow: { + operation_statuses: [ + { + status: "SUCCESS", + successful: true, + }, + ], + operation_types: ["transfer"], + }, +}; + +async function createServer(params) { + const app = require('express')(); + var tries = 0; + + app.use(bodyParser.json()); + + app.post('/account/balance', (req, res) => { + const expected = { + network_identifier: basicNetwork, + account_identifier: basicAccount, + }; + + expect(req.body).to.deep.equal(expected); + + if (tries < params.errorsBeforeSuccess) { + tries++; + res.status(500); + return res.json({}); + } + + const response = new Rosetta.Client.AccountBalanceResponse(basicBlock, basicAmounts); + res.json((response)); + }); + + app.post('/block', (req, res) => { + const expected = { + network_identifier: basicNetwork, + block_identifier: constructPartialBlockIdentifier(basicBlock), + }; + + expect(req.body).to.deep.equal(expected); + + if (tries < params.errorsBeforeSuccess) { + tries++; + res.status(500); + return res.json({}); + } + + const response = new Rosetta.Client.BlockResponse(basicFullBlock); + res.json((response)); + }); + + app.post('/network/status', (req, res) => { + const expected = { + network_identifier: basicNetwork, + metadata: {}, + }; + + const networkRequest = new Rosetta.Client.NetworkRequest(req.body); + expect(req.body).to.deep.equal(expected); + + if (tries < params.errorsBeforeSuccess) { + tries++; + res.status(500); + return res.json({}); + } + + const response = Rosetta.Client.NetworkStatusResponse.constructFromObject(basicNetworkStatus); + res.json(response); + }); + + app.post('/network/list', (req, res) => { + const metadataRequest = new Rosetta.Client.MetadataRequest.constructFromObject({ metadata: {} }); + const expected = metadataRequest; + + expect(req.body).to.deep.equal(expected); + + if (tries < params.errorsBeforeSuccess) { + tries++; + res.status(500); + return res.json({}); + } + + const response = new Rosetta.Client.NetworkListResponse(basicNetworkList); + res.json(response); + }); + + const server = app.listen(params.port || 8000, null); + + return server; +}; + +const launchServer = (options) => { + return new Promise((fulfill, reject) => { + let server; + + const cb = () => { + createServer(options).then(s => { + server = s; + fulfill(); + }); + }; + + if (server) + server.close(cb); + else + cb(); + }); +}; + +describe('Fetcher', function () { + describe('Test AccountBalanceRetry', function () { + this.timeout(5000); + + it('no failures', async function () { + const port = getPort(); + + const server = await launchServer({ + errorsBeforeSuccess: 0, + port, + }); + + const fetcher = new Rosetta.Fetcher({ + retryOptions: { + numOfAttempts: 5, + }, + server: { + port, + }, + }); + + const { block, balances, metadata } = + await fetcher.accountBalanceRetry(basicNetwork, basicAccount, null); + + expect(basicBlock).to.deep.equal(block); + expect(basicAmounts.map((amount) => { + return Rosetta.Client.Amount.constructFromObject(amount); + })).to.deep.equal(balances); + expect(metadata).to.deep.equal(metadata); + return true; + }); + + it('retry failures', async function () { + const port = getPort(); + + const server = await launchServer({ + errorsBeforeSuccess: 2, + port, + }); + + const fetcher = new Rosetta.Fetcher({ + retryOptions: { + numOfAttempts: 5, + }, + server: { + port, + }, + }); + + const { block, balances, metadata } = + await fetcher.accountBalanceRetry(basicNetwork, basicAccount, null); + + expect(basicBlock).to.deep.equal(block); + expect(basicAmounts.map((amount) => { + return Rosetta.Client.Amount.constructFromObject(amount); + })).to.deep.equal(balances); + expect(metadata).to.deep.equal(metadata); + return true; + }); + + it('exhausted failures', async function () { + const port = getPort(); + + const server = await launchServer({ + errorsBeforeSuccess: 2, + port, + }); + + const fetcher = new Rosetta.Fetcher({ + retryOptions: { + numOfAttempts: 1, + }, + server: { + port, + }, + }); + + try { + const { block, balances, metadata } = + await fetcher.accountBalanceRetry(basicNetwork, basicAccount, null); + + } catch(e) { + expect(e.status).to.equal(500); + return true; + } + + throw new Error('Fetcher did exceed its max number of allowed retries'); + }); + }); + + /** + * BlockRetry + */ + + describe('Test BlockRetry', function () { + this.timeout(5000); + + it('no failures', async function () { + const port = getPort(); + + const server = await launchServer({ + errorsBeforeSuccess: 0, + port, + }); + + const fetcher = new Rosetta.Fetcher({ + retryOptions: { + numOfAttempts: 5, + }, + server: { + port, + }, + }); + + const block = + await fetcher.blockRetry(basicNetwork, constructPartialBlockIdentifier(basicBlock)); + + expect(c(block)).to.deep.equal(basicFullBlock); + return true; + }); + + it('retry failures', async function () { + const port = getPort(); + + const server = await launchServer({ + errorsBeforeSuccess: 2, + port, + }); + + const fetcher = new Rosetta.Fetcher({ + retryOptions: { + numOfAttempts: 5, + }, + server: { + port, + }, + }); + + const block = + await fetcher.blockRetry(basicNetwork, constructPartialBlockIdentifier(basicBlock)); + + expect(c(block)).to.deep.equal(basicFullBlock); + return true; + }); + + it('exhausted failures', async function () { + const port = getPort(); + + const server = await launchServer({ + errorsBeforeSuccess: 2, + port, + }); + + const fetcher = new Rosetta.Fetcher({ + retryOptions: { + numOfAttempts: 1, + }, + server: { + port, + }, + }); + + try { + const block = + await fetcher.blockRetry(basicNetwork, constructPartialBlockIdentifier(basicBlock)); + + } catch(e) { + expect(e.status).to.equal(500); + return true; + } + + throw new Error('Fetcher did exceed its max number of allowed retries'); + }); + }); + + /* + * NETWORK + */ + + describe('Test NetworkListRetry', function () { + this.timeout(5000); + + it('no failures', async function () { + const port = getPort(); + + const server = await launchServer({ + errorsBeforeSuccess: 0, + port, + }); + + const fetcher = new Rosetta.Fetcher({ + retryOptions: { + numOfAttempts: 5, + }, + server: { + port, + }, + }); + + const networkList = + await fetcher.networkListRetry({}); + + const expectedResponse = new Rosetta.Client.NetworkListResponse(basicNetworkList); + + expect(c(networkList)).to.deep.equal(expectedResponse); + return true; + }); + + it('retry failures', async function () { + const port = getPort(); + + const server = await launchServer({ + errorsBeforeSuccess: 2, + port, + }); + + const fetcher = new Rosetta.Fetcher({ + retryOptions: { + numOfAttempts: 5, + }, + server: { + port, + }, + }); + + const networkList = + await fetcher.networkListRetry({}); + + const expectedResponse = new Rosetta.Client.NetworkListResponse(basicNetworkList); + + expect(c(networkList)).to.deep.equal(expectedResponse); + return true; + }); + + it('exhausted retries', async function () { + const port = getPort(); + + const server = await launchServer({ + errorsBeforeSuccess: 2, + port, + }); + + const fetcher = new Rosetta.Fetcher({ + retryOptions: { + numOfAttempts: 1, + }, + server: { + port, + }, + }); + + try { + const networkList = + await fetcher.networkListRetry({}); + + } catch(e) { + expect(e.status).to.equal(500); + return true; + } + + throw new Error('Fetcher did exceed its max number of allowed retries'); + }); + }); + + describe('Test NetworkStatusRetry', function () { + this.timeout(5000); + + it('no failures', async function () { + const port = getPort(); + + const server = await launchServer({ + errorsBeforeSuccess: 0, + port, + }); + + const fetcher = new Rosetta.Fetcher({ + retryOptions: { + numOfAttempts: 5, + }, + server: { + port, + }, + }); + + const networkList = + await fetcher.networkStatusRetry(basicNetwork); + + expect(c(networkList)).to.deep.equal(basicNetworkStatus); + return true; + }); + + it('retry failures', async function () { + const port = getPort(); + + const server = await launchServer({ + errorsBeforeSuccess: 2, + port, + }); + + const fetcher = new Rosetta.Fetcher({ + retryOptions: { + numOfAttempts: 5, + }, + server: { + port, + }, + }); + + const networkList = + await fetcher.networkStatusRetry(basicNetwork); + + expect(c(networkList)).to.deep.equal(basicNetworkStatus); + return true; + }); + + it('exhausted retries', async function () { + const port = getPort(); + + const server = await launchServer({ + errorsBeforeSuccess: 2, + port, + }); + + const fetcher = new Rosetta.Fetcher({ + retryOptions: { + numOfAttempts: 1, + }, + server: { + port, + }, + }); + + try { + const networkList = + await fetcher.networkStatusRetry(basicNetwork); + + } catch(e) { + expect(e.status).to.equal(500); + return true; + } + + throw new Error('Fetcher did exceed its max number of allowed retries'); + }); + }); +}); \ No newline at end of file diff --git a/test/parser.test.js.js b/test/parser.test.js.js new file mode 100644 index 0000000..5048dfb --- /dev/null +++ b/test/parser.test.js.js @@ -0,0 +1,2527 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +// parser.test.js +const { expect } = require('chai'); +const RosettaSDK = require('..'); + +const { Hash } = RosettaSDK.Utils; +const { + Descriptions, + OperationDescription, + AccountDescription, + AmountDescription, + Sign, +} = RosettaSDK.InternalModels; + +const currency = { /* Currency */ + symbol: "Blah", + decimals: 2, +}; + +const recipient = { /* AccountIdentifier */ + address: "acct1", +}; + +const recipientAmount = { /* Amount */ + value: "100", + currency: currency, +}; + +const emptyAccountAndAmount = { /* Operation */ + operation_identifier: { + index: 0, + }, + type: "Transfer", + status: "Success", +}; + +const emptyAmount = { /* Operation */ + operation_identifier: { + index: 0, + }, + type: "Transfer", + status: "Success", + account: recipient, +}; + +const recipientOperation = { /* Operation */ + operation_identifier: { + index: 0, + }, + type: "Transfer", + status: "Success", + account: recipient, + amount: recipientAmount, +}; + +const recipientFailureOperation = { /* Operation */ + operation_identifier: { + index: 1, + }, + type: "Transfer", + status: "Failure", + account: recipient, + amount: recipientAmount, +}; + +const recipientTransaction = { /* Transaction */ + transaction_identifier: { + hash: "tx1", + }, + operations: [ + emptyAccountAndAmount, + emptyAmount, + recipientOperation, + recipientFailureOperation, + ], +}; + +const defaultStatus = [ /* OperationStatus */ + { + status: "Success", + successful: true, + }, { + status: "Failure", + successful: false, + }, +]; + +const c = (arg) => arg == undefined ? arg : JSON.parse(JSON.stringify(arg)); + +const createTransaction = (hash, address, value, currency) => { + return { /* Transaction */ + transaction_identifier: { + hash: hash, + }, + + operations: [{ /* [Operation] */ + operation_identifier: { + index: 0, + }, + + type: 'Transfer', + + status: 'Success', + + account: { + address: address, + }, + + amount: { + value: value, + currency: currency, + }, + }], + }; +}; + +const createAsserter = (allowedStatuses) => { + const asserter = new RosettaSDK.Asserter({ + networkIdentifier: { + blockchain: 'bitcoin', + network: 'mainnet', + }, + + genesisBlock: { + hash: 'block 0', + index: 0, + }, + + operationTypes: ['Transfer'], + + operationStatuses: allowedStatuses, + + errorTypes: [], + }); + + return asserter; +}; + +describe('Parser', function () { + describe('Test Balance Changes', function () { + it('should be able to parse a block', async function () { + const asserter = createAsserter(defaultStatus); + + const parser = new RosettaSDK.Parser({ + asserter, + }); + + const block = { + block_identifier: { + hash: '1', + index: 1, + }, + + parent_block_identifier: { + hash: '0', + index: 0, + }, + + transactions: [ + recipientTransaction, + ], + + timestamp: asserter.minUnixEpoch + 1, + }; + + const expectedChanges = [ + { + account_identifier: recipient, + currency: currency, + block_identifier: { + hash: '1', + index: 1, + }, + difference: '100', + }, + ]; + + const isOrphan = false; + + const changes = parser.balanceChanges(block, isOrphan); + expect(changes).to.deep.equal(expectedChanges); + }); + + it('should work with an excempt function', async function () { + const asserter = createAsserter(defaultStatus); + + const parser = new RosettaSDK.Parser({ + asserter, + exemptFunc: (op) => + Hash(op.account) == Hash(recipientOperation.account), + }); + + const block = { + block_identifier: { + hash: '1', + index: 1, + }, + + parent_block_identifier: { + hash: '0', + index: 0, + }, + + transactions: [ + recipientTransaction, + ], + + timestamp: asserter.minUnixEpoch + 1, + }; + + const expectedChanges = []; + const isOrphan = false; + + const changes = parser.balanceChanges(block, isOrphan); + expect(changes).to.deep.equal(expectedChanges); + }); + + it('should group balanceChanges if an address receives multiple utxos', async function () { + const asserter = createAsserter(defaultStatus); + + const parser = new RosettaSDK.Parser({ + asserter, + }); + + const block = { + block_identifier: { + hash: '1', + index: 1, + }, + + parent_block_identifier: { + hash: '0', + index: 0, + }, + + transactions: [ + createTransaction('tx1', 'addr1', '100', currency), + createTransaction('tx2', 'addr1', '150', currency), + createTransaction('tx3', 'addr2', '150', currency), + ], + + timestamp: asserter.minUnixEpoch + 1, + }; + + const expectedChanges = [ + { + account_identifier: { address: 'addr1' }, + currency: currency, + block_identifier: { + hash: '1', + index: 1, + }, + difference: '250', + }, + + { + account_identifier: { address: 'addr2' }, + currency: currency, + block_identifier: { + hash: '1', + index: 1, + }, + difference: '150', + }, + ]; + + const isOrphan = false; + + const changes = parser.balanceChanges(block, isOrphan); + expect(changes).to.deep.equal(expectedChanges); + }); + + it('should reduce balance again if an orphan block appears', async function () { + const asserter = createAsserter(defaultStatus); + + const parser = new RosettaSDK.Parser({ + asserter, + }); + + const block = { + block_identifier: { + hash: '1', + index: 1, + }, + + parent_block_identifier: { + hash: '0', + index: 0, + }, + + transactions: [ + createTransaction('tx1', 'addr1', '100', currency), + createTransaction('tx2', 'addr1', '150', currency), + createTransaction('tx3', 'addr2', '150', currency), + ], + + timestamp: asserter.minUnixEpoch + 1, + }; + + const expectedChanges = [ + { + account_identifier: { address: 'addr1' }, + currency: currency, + block_identifier: { + hash: '0', + index: 0, + }, + difference: '-250', + }, + + { + account_identifier: { address: 'addr2' }, + currency: currency, + block_identifier: { + hash: '0', + index: 0, + }, + difference: '-150', + }, + ]; + + const isOrphan = true; + + const changes = parser.balanceChanges(block, isOrphan); + expect(changes).to.deep.equal(expectedChanges); + }); + }); + + describe('Test Sort Operations', function () { + it('should sort operations correctly', function () { + const operationsMap = { + 2: { + operations: [ + { operation_identifier: { index: 2 } }, + ], + }, + + 4: { + operations: [ + { operation_identifier: { index: 4 } }, + ], + }, + + 0: { + operations: [ + { operation_identifier: { index: 1 }, related_operations: [{ index: 0 }] }, + { operation_identifier: { index: 3 }, related_operations: [{ index: 1 }] }, + { operation_identifier: { index: 0 } }, + ], + }, + + 5: { + operations: [ + { operation_identifier: { index: 5 } }, + ], + }, + }; + + const parser = new RosettaSDK.Parser(); + + const expectedResult = [ + { + operations: [ + { operation_identifier: { index: 0 } }, + { operation_identifier: { index: 1 }, related_operations: [{ index: 0 }] }, + { operation_identifier: { index: 3 }, related_operations: [{ index: 1 }] }, + ], + }, + + { + operations: [ + { operation_identifier: { index: 2 } }, + ], + }, + + { + operations: [ + { operation_identifier: { index: 4 } }, + ], + }, + + { + operations: [ + { operation_identifier: { index: 5 } }, + ], + }, + ]; + + const result = parser.sortOperationsGroup(6, operationsMap); + + expect(result).to.deep.equal(expectedResult); + }); + }); + + describe('Test Group Operations', function () { + it('should return nothing if there is no transaction', async function () { + const parser = new RosettaSDK.Parser(); + const transaction = new RosettaSDK.Client.Transaction(); + const result = parser.groupOperations(transaction); + + expect(result).to.deep.equal([]); + }); + + it('should not group unrelated operations', async function () { + const parser = new RosettaSDK.Parser(); + const transaction = { + operations: [ + { + operation_identifier: { index: 0 }, + type: 'op 0', + amount: { + currency: { symbol: 'BTC' }, + }, + }, + + { + operation_identifier: { index: 1 }, + type: 'op 1', + }, + + { + operation_identifier: { index: 2 }, + type: 'op 2', + }, + ], + }; + + const expectedResult = [ + { + type: 'op 0', + operations: [ + { + operation_identifier: { index: 0 }, + type: 'op 0', + amount: { + currency: { symbol: 'BTC', }, + }, + }, + ], + + nil_amount_present: false, + currencies: [ + { symbol: 'BTC' } + ], + }, + + { + type: 'op 1', + operations: [ + { + operation_identifier: { index: 1 }, + type: 'op 1', + }, + ], + + nil_amount_present: true, + currencies: [], + }, + + { + type: 'op 2', + operations: [ + { + operation_identifier: { index: 2 }, + type: 'op 2', + }, + ], + + nil_amount_present: true, + currencies: [], + }, + ]; + + const result = parser.groupOperations(transaction); + + expect(c(result)).to.deep.equal(expectedResult); + }); + + it('should group related operations', async function () { + const parser = new RosettaSDK.Parser(); + const transaction = { + operations: [ + { + operation_identifier: { index: 0 }, + type: 'type 0', + amount: { + currency: { symbol: 'BTC' }, + }, + }, + + { + operation_identifier: { index: 1 }, + type: 'type 1', + }, + + { + operation_identifier: { index: 2 }, + type: 'type 2', + amount: { + currency: { symbol: 'BTC' }, + } + }, + + { + operation_identifier: { index: 3 }, + related_operations: [ + { index: 2 }, + ], + type: 'type 2', + amount: { + currency: { symbol: 'ETH' }, + } + }, + + { + operation_identifier: { index: 4 }, + related_operations: [{ index: 2 }], + type: 'type 4', + }, + + { + operation_identifier: { index: 5 }, + related_operations: [{ index: 0 }], + type: 'type 0', + amount: { + currency: { symbol: 'BTC' }, + }, + }, + ], + }; + + const expectedResult = [ + { + type: 'type 0', + nil_amount_present: false, + operations: [ + { + operation_identifier: { index: 0 }, + type: 'type 0', + amount: { + currency: { symbol: 'BTC' }, + } + }, + + { + operation_identifier: { index: 5 }, + related_operations: [ + { index: 0 }, + ], + type: 'type 0', + amount: { + currency: { symbol: 'BTC' }, + } + }, + ], + + currencies: [ + { symbol: 'BTC' }, + ], + }, + + { + type: 'type 1', + nil_amount_present: true, + operations: [ + { + operation_identifier: { index: 1 }, + type: 'type 1', + }, + ], + + currencies: [], + }, + + { + type: '', + nil_amount_present: true, + currencies: [ + { symbol: 'BTC' }, + { symbol: 'ETH' }, + ], + operations: [ + { + operation_identifier: { index: 2 }, + type: 'type 2', + amount: { + currency: { symbol: 'BTC' }, + } + }, + + { + operation_identifier: { index: 3 }, + related_operations: [ + { index: 2 }, + ], + type: 'type 2', + amount: { + currency: { symbol: 'ETH' }, + } + }, + + { + operation_identifier: { index: 4 }, + related_operations: [ + { index: 2 }, + ], + type: 'type 4', + }, + ], + }, + ]; + + const result = parser.groupOperations(transaction); + expect(c(result)).to.deep.equal(expectedResult); + }); + }); + + describe('Test Match Operations', function () { + it('should detect a simple transfer (with extra empty op)', async function () { + const parser = new RosettaSDK.Parser(); + + const descriptions = new Descriptions({ + opposite_amounts: [[0, 1]], + operation_descriptions: [ + new OperationDescription({ + account: new AccountDescription({ exists: true }), + amount: new AmountDescription({ + exists: true, + sign: Sign.Negative + }), + }), + + new OperationDescription({ + account: new AccountDescription({ exists: true }), + amount: new AmountDescription({ + exists: true, + sign: Sign.Positive + }), + }), + ], + }); + + const operations = [ + { + account: { address: 'addr2' }, + amount: { value: '100' }, + }, + + {}, // should be ignored + + { + account: { address: 'addr1' }, + amount: { value: '-100' }, + }, + ]; + + const expectedMatches = [ + { + operations: [{ + account: { address: 'addr1' }, + amount: { value: '-100' }, + }], + + amounts: [-100] + }, + + { + operations: [{ + account: { address: 'addr2' }, + amount: { value: '100' }, + }], + + amounts: [100], + } + ]; + + let matches; + + try { + matches = parser.MatchOperations(descriptions, operations); + } catch (e) { + console.error(e); + } + + expect(c(matches)).to.deep.equal(expectedMatches); + }); + + it('should throw an error when parsing a simple transfer without an account', async function () { + const parser = new RosettaSDK.Parser(); + + const descriptions = new Descriptions({ + opposite_amounts: [[0, 1]], + equal_addresses: [[0, 1]], + operation_descriptions: [ + new OperationDescription({ + account: new AccountDescription({ exists: false }), + amount: new AmountDescription({ + exists: true, + sign: Sign.Negative + }), + }), + + new OperationDescription({ + account: new AccountDescription({ exists: true }), + amount: new AmountDescription({ + exists: true, + sign: Sign.Positive + }), + }), + ], + }); + + const operations = [ + { + account: { address: 'addr2' }, + amount: { value: '100' }, + }, + + { + amount: { value: '-100' }, + }, + ]; + + const expectedMatches = undefined; + + let matches; + + try { + matches = parser.MatchOperations(descriptions, operations); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('ParserError'); + expect(e.message).to.equal('account is null: operation addresses are not equal: group descriptions not met'); + } + + expect(matches).to.deep.equal(expectedMatches); + }); + + it('should match a simple transfer specifiying a type', async function () { + const parser = new RosettaSDK.Parser(); + + const descriptions = new Descriptions({ + opposite_amounts: [[0, 1]], + operation_descriptions: [ + new OperationDescription({ + account: new AccountDescription({ exists: true }), + type: 'input', + }), + + new OperationDescription({ + account: new AccountDescription({ exists: true }), + type: 'output', + }), + ], + }); + + const operations = [ + { + account: { address: 'addr2' }, + amount: { value: '100' }, + type: 'output', + }, + + {}, // should be ignored + + { + account: { address: 'addr1' }, + amount: { value: '-100' }, + type: 'input', + }, + ]; + + const expectedMatches = [ + { + operations: [{ + account: { address: 'addr1' }, + amount: { value: '-100' }, + type: 'input', + }], + + amounts: [-100] + }, + + { + operations: [{ + account: { address: 'addr2' }, + amount: { value: '100' }, + type: 'output', + }], + + amounts: [100], + } + ]; + + let matches; + + try { + matches = parser.MatchOperations(descriptions, operations); + } catch (e) { + console.error(e); + } + + expect(c(matches)).to.deep.equal(expectedMatches); + }); + + it('should reject a simple transfer that has an unmatched description', async function () { + const parser = new RosettaSDK.Parser(); + + const descriptions = new Descriptions({ + err_unmatched: true, + opposite_amounts: [[0, 1]], + operation_descriptions: [ + new OperationDescription({ + account: new AccountDescription({ exists: true }), + amount: new AmountDescription({ + exists: true, + sign: Sign.Negative, + }), + }), + + new OperationDescription({ + account: new AccountDescription({ exists: true }), + amount: new AmountDescription({ + exists: true, + sign: Sign.Positive, + }), + }), + ], + }); + + const operations = [ + { + account: { address: 'addr2' }, + amount: { value: '100' }, + }, + + {}, // should be ignored + + { + account: { address: 'addr1' }, + amount: { value: '-100' }, + }, + ]; + + const expectedMatches = undefined; + + let matches; + + try { + matches = parser.MatchOperations(descriptions, operations); + } catch (e) { + //console.error(e); + expect(e.name).to.equal('ParserError'); + expect(e.message).to.equal('Unable to find match for operation at index 1'); + } + + expect(matches).to.deep.equal(expectedMatches); + }); + + it('should reject a simple transfer with unequal amounts', async function () { + const parser = new RosettaSDK.Parser(); + + const descriptions = new Descriptions({ + equal_amounts: [[0, 1]], + operation_descriptions: [ + new OperationDescription({ + account: new AccountDescription({ exists: true }), + amount: new AmountDescription({ + exists: true, + sign: Sign.Negative, + }), + }), + + new OperationDescription({ + account: new AccountDescription({ exists: true }), + amount: new AmountDescription({ + exists: true, + sign: Sign.Positive, + }), + }), + ], + }); + + const operations = [ + { + account: { address: 'addr2' }, + amount: { value: '100' }, // Yoshi: unequal? + }, + + {}, // should be ignored + + { + account: { address: 'addr1' }, + amount: { value: '-100' }, + }, + ]; + + const expectedMatches = undefined; + + let matches; + let thrown = false; + + try { + matches = parser.MatchOperations(descriptions, operations); + } catch (e) { + //console.error(e); + expect(e.name).to.equal('ParserError'); + expect(e.message).to.equal('100 is not equal to -100: operation amounts are not equal: group descriptions not met'); + thrown = true; + } + + expect(thrown).to.equal(true); + expect(matches).to.deep.equal(expectedMatches); + }); + + it('should reject a simple transfer with invalid opposite amounts', async function () { + const parser = new RosettaSDK.Parser(); + + const descriptions = new Descriptions({ + opposite_amounts: [[0, 1]], + operation_descriptions: [ + new OperationDescription({ + account: new AccountDescription({ exists: true }), + amount: new AmountDescription({ exists: true }), + }), + + new OperationDescription({ + account: new AccountDescription({ exists: true }), + amount: new AmountDescription({ exists: true }), + }), + ], + }); + + const operations = [ + { + account: { address: 'addr2' }, + amount: { value: '100' }, + }, + + {}, // should be ignored + + { + account: { address: 'addr1' }, + amount: { value: '100' }, + }, + ]; + + const expectedMatches = undefined; + + let matches; + + try { + matches = parser.MatchOperations(descriptions, operations); + } catch (e) { + //console.error(e); + expect(e.name).to.equal('ParserError'); + expect(e.message).to.equal('100 and 100 have the same sign: group descriptions not met'); + } + + expect(matches).to.deep.equal(expectedMatches); + }); + + it('should match a simple transfer using currency', async function () { + const parser = new RosettaSDK.Parser(); + + const descriptions = new Descriptions({ + opposite_amounts: [[0, 1]], + operation_descriptions: [ + new OperationDescription({ + account: new AccountDescription({ exists: true }), + amount: new AmountDescription({ + exists: true, + sign: Sign.Negative, + currency: { + symbol: 'ETH', + decimals: 18, + }, + }), + }), + + new OperationDescription({ + account: new AccountDescription({ exists: true }), + amount: new AmountDescription({ + exists: true, + sign: Sign.Positive, + currency: { + symbol: 'BTC', + decimals: 8, + }, + }), + }), + ], + }); + + const operations = [ + { + account: { address: 'addr2' }, + amount: { + value: '100', + currency: { + symbol: 'BTC', + decimals: 8, + }, + }, + }, + + {}, + + { + account: { address: 'addr1' }, + amount: { + value: '-100', + currency: { + symbol: 'ETH', + decimals: 18, + }, + }, + }, + ]; + + const expectedMatches = [ + { + operations: [{ + account: { address: 'addr1' }, + amount: { + value: '-100', + currency: { + symbol: 'ETH', + decimals: 18, + }, + }, + }], + + amounts: [-100], + }, + + { + operations: [{ + account: { address: 'addr2' }, + amount: { + value: '100', + currency: { + symbol: 'BTC', + decimals: 8, + }, + }, + }], + + amounts: [100], + }, + ]; + + let matches; + + try { + matches = parser.MatchOperations(descriptions, operations); + } catch (e) { + console.error(e); + } + + expect(c(matches)).to.deep.equal(expectedMatches); + }); + + it('should reject to match a simple transfer if it can\'t match the currency', async function () { + const parser = new RosettaSDK.Parser(); + + const descriptions = new Descriptions({ + opposite_amounts: [[0, 1]], + operation_descriptions: [ + new OperationDescription({ + account: new AccountDescription({ exists: true }), + amount: new AmountDescription({ + exists: true, + sign: Sign.Negative, + currency: { + symbol: 'ETH', + decimals: 18, + }, + }), + }), + + new OperationDescription({ + account: new AccountDescription({ exists: true }), + amount: new AmountDescription({ + exists: true, + sign: Sign.Positive, + currency: { + symbol: 'BTC', + decimals: 8, + }, + }), + }), + ], + }); + + const operations = [ + { + account: { address: 'addr2' }, + amount: { + value: '100', + currency: { + symbol: 'ETH', + decimals: 18, + }, + }, + }, + + {}, + + { + account: { address: 'addr1' }, + amount: { + value: '-100', + currency: { + symbol: 'ETH', + decimals: 18, + }, + }, + }, + ]; + + const expectedMatches = undefined; + + let matches; + let thrown = false; + + try { + matches = parser.MatchOperations(descriptions, operations); + } catch (e) { + expect(e.name).to.equal('ParserError'); + expect(e.message).to.equal('Could not find match for description 1'); + thrown = true; + } + + expect(thrown).to.equal(true); + expect(c(matches)).to.deep.equal(expectedMatches); + }); + + it('should reject a simple transfer (with sender metadata) and non-equal addresses', async function () { + const parser = new RosettaSDK.Parser(); + + const descriptions = new Descriptions({ + opposite_amounts: [[0, 1]], + equal_addresses: [[0, 1]], + operation_descriptions: [ + new OperationDescription({ + account: new AccountDescription({ + exists: true, + sub_account_exists: true, + sub_account_address: 'sub', + sub_account_metadata_keys: [ + { key: 'validator', value_kind: 'string' }, + ], + }), + amount: new AmountDescription({ + exists: true, + sign: Sign.Negative, + }), + }), + + new OperationDescription({ + account: new AccountDescription({ exists: true }), + amount: new AmountDescription({ + exists: true, + sign: Sign.Positive, + }), + }), + ], + }); + + const operations = [ + { + account: { address: 'addr2' }, + amount: { value: '100' }, + }, + + {}, + + { + account: { + address: 'addr1', + sub_account: { + address: 'sub', + metadata: { + 'validator': '10', + }, + }, + }, + + amount: { + value: '-100', + }, + }, + ]; + + const expectedMatches = undefined; + + let matches; + let thrown = false; + + try { + matches = parser.MatchOperations(descriptions, operations); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('ParserError'); + expect(e.message).to.equal('addr1 is not equal to addr2: operation addresses are not equal: group descriptions not met'); + thrown = true; + } + + expect(thrown).to.equal(true); + expect(c(matches)).to.deep.equal(expectedMatches); + }); + + it('should match a simple transfer (using sender metadata)', async function () { + const parser = new RosettaSDK.Parser(); + + const descriptions = new Descriptions({ + opposite_amounts: [[0, 1]], + equal_addresses: [[0, 1]], + operation_descriptions: [ + new OperationDescription({ + account: new AccountDescription({ + exists: true, + sub_account_exists: true, + sub_account_address: 'sub', + sub_account_metadata_keys: [{ + key: 'validator', + value_kind: 'string', + }], + }), + amount: new AmountDescription({ + exists: true, + sign: Sign.Negative, + }), + }), + + new OperationDescription({ + account: new AccountDescription({ + exists: true, + }), + + amount: new AmountDescription({ + exists: true, + sign: Sign.Positive, + }), + }), + ], + }); + + const operations = [ + { + account: { address: 'addr1' }, + amount: { value: '100' }, + }, + + {}, + + { + account: { + address: 'addr1', + sub_account: { + address: 'sub', + metadata: { + 'validator': '10', + } + }, + }, + amount: { value: '-100' }, + }, + ]; + + const expectedMatches = [ + { + operations: [{ + account: { + address: 'addr1', + sub_account: { + address: 'sub', + metadata: { + 'validator': '10', + } + }, + }, + + amount: { + value: '-100', + }, + }], + + amounts: [-100], + }, + + { + operations: [{ + account: { address: 'addr1' }, + amount: { + value: '100', + }, + }], + + amounts: [100], + }, + ]; + + let matches; + + try { + matches = parser.MatchOperations(descriptions, operations); + } catch (e) { + console.error(e); + } + + expect(c(matches)).to.deep.equal(expectedMatches); + }); + + it('should reject to match a simple transfer with missing sender address metadata', async function () { + const parser = new RosettaSDK.Parser(); + + const descriptions = new Descriptions({ + opposite_amounts: [[0, 1]], + operation_descriptions: [ + new OperationDescription({ + account: new AccountDescription({ + exists: true, + sub_account_exists: true, + sub_account_address: 'sub', + sub_account_metadata_keys: [{ + key: 'validator', + value_kind: 'string', + }], + }), + amount: new AmountDescription({ + exists: true, + sign: Sign.Negative, + }), + }), + + new OperationDescription({ + account: new AccountDescription({ + exists: true, + }), + + amount: new AmountDescription({ + exists: true, + sign: Sign.Positive, + }), + }), + ], + }); + + const operations = [ + { + account: { address: 'addr2' }, + amount: { value: '100' }, + }, + + {}, + + { + account: { address: 'addr1' }, + amount: { value: '-100' }, + }, + ]; + + const expectedMatches = undefined; + + let matches; + let thrown = false; + + try { + matches = parser.MatchOperations(descriptions, operations); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('ParserError'); + expect(e.message).to.equal('Could not find match for description 0'); + thrown = true; + } + + expect(thrown).to.equal(true); + expect(c(matches)).to.deep.equal(expectedMatches); + }); + + it('should match nil amount ops', async function () { + const parser = new RosettaSDK.Parser(); + + const descriptions = new Descriptions({ + operation_descriptions: [ + new OperationDescription({ + account: new AccountDescription({ + exists: true, + sub_account_exists: true, + sub_account_address: 'sub 2', + }), + }), + + new OperationDescription({ + account: new AccountDescription({ + exists: true, + sub_account_exists: true, + sub_account_address: 'sub 1', + }), + }), + ], + }); + + const operations = [ + { + account: { + address: 'addr1', + sub_account: { + address: 'sub 1', + }, + }, + }, + + { + account: { + address: 'addr2', + sub_account: { + address: 'sub 2', + }, + }, + + amount: { value: '100' }, // allowed because no amount requirement provided + }, + ]; + + const expectedMatches = [ + { + operations: [{ + account: { + address: 'addr2', + sub_account: { + address: 'sub 2', + }, + }, + + amount: { + value: '100', + }, + }], + + amounts: [100], + }, + + { + operations: [{ + account: { + address: 'addr1', + sub_account: { + address: 'sub 1', + }, + }, + }], + + amounts: [null], + }, + ]; + + let matches; + + try { + matches = parser.MatchOperations(descriptions, operations); + } catch (e) { + console.error(e); + } + + expect(c(matches)).to.deep.equal(expectedMatches); + }); + + it('should reject to match nil amount ops (force false amount)', async function () { + const parser = new RosettaSDK.Parser(); + + const descriptions = new Descriptions({ + operation_descriptions: [ + new OperationDescription({ + account: new AccountDescription({ + exists: true, + sub_account_exists: true, + sub_account_address: 'sub 2', + }), + + amount: new AmountDescription({ + exists: false, + }), + }), + + new OperationDescription({ + account: new AccountDescription({ + exists: true, + sub_account_exists: true, + sub_account_address: 'sub 1', + }), + }), + ], + }); + + const operations = [ + { + account: { + address: 'addr1', + sub_account: { + address: 'sub 1', + }, + }, + }, + + { + account: { + address: 'addr2', + sub_account: { + address: 'sub 2', + }, + }, + + amount: {}, // allowed because no amount requirement provided + }, + ]; + + const expectedMatches = undefined; + + let matches; + let thrown; + + try { + matches = parser.MatchOperations(descriptions, operations); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('ParserError'); + expect(e.message).to.equal('Could not find match for description 0'); + thrown = true; + } + + expect(thrown).to.equal(true); + expect(c(matches)).to.deep.equal(expectedMatches); + }); + + it('should match nil amount ops (only requiring metadata keys)', async function () { + const parser = new RosettaSDK.Parser(); + + const descriptions = new Descriptions({ + operation_descriptions: [ + new OperationDescription({ + account: new AccountDescription({ + exists: true, + sub_account_exists: true, + sub_account_address: 'sub 2', + }), + }), + + new OperationDescription({ + account: new AccountDescription({ + exists: true, + sub_account_exists: true, + sub_account_metadata_keys: [{ + key: 'validator', + value_kind: 'number', + }], + }), + }), + ], + }); + + const operations = [ + { + account: { + address: 'addr1', + sub_account: { + address: 'sub 1', + metadata: { + validator: -1000, + } + }, + }, + }, + + { + account: { + address: 'addr2', + sub_account: { + address: 'sub 2', + }, + }, + }, + ]; + + const expectedMatches = [ + { + operations: [{ + account: { + address: 'addr2', + sub_account: { + address: 'sub 2', + }, + }, + }], + + amounts: [null], + }, + + { + operations: [{ + account: { + address: 'addr1', + sub_account: { + address: 'sub 1', + metadata: { + validator: -1000, + }, + }, + }, + }], + + amounts: [null], + }, + ]; + + let matches; + + try { + matches = parser.MatchOperations(descriptions, operations); + } catch (e) { + console.error(e); + } + + expect(c(matches)).to.deep.equal(expectedMatches); + }); + + it('should reject to match nil amount ops when sub account addresses mismatch', async function () { + const parser = new RosettaSDK.Parser(); + + const descriptions = new Descriptions({ + operation_descriptions: [ + new OperationDescription({ + account: new AccountDescription({ + exists: true, + sub_account_exists: true, + sub_account_address: 'sub 2', + }), + }), + + new OperationDescription({ + account: new AccountDescription({ + exists: true, + sub_account_exists: true, + sub_account_address: 'sub 1', + }), + }), + ], + }); + + const operations = [ + { + account: { + address: 'addr1', + sub_account: { + address: 'sub 3', + }, + }, + }, + + { + account: { + address: 'addr2', + sub_account: { + address: 'sub 2', + }, + }, + }, + ]; + + const expectedMatches = undefined; + + let matches; + let thrown = false; + + try { + matches = parser.MatchOperations(descriptions, operations); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('ParserError'); + expect(e.message).to.equal('Could not find match for description 1'); + thrown = true; + } + + expect(thrown).to.equal(true); + expect(c(matches)).to.deep.equal(expectedMatches); + }); + + it('should reject to match nil descriptions', async function () { + const parser = new RosettaSDK.Parser(); + + const descriptions = new Descriptions({ + }); + + const operations = [ + { + account: { + address: 'addr1', + sub_account: { + address: 'sub 3', + }, + }, + }, + + { + account: { + address: 'addr2', + sub_account: { + address: 'sub 2', + }, + }, + }, + ]; + + const expectedMatches = undefined; + + let matches; + let thrown = false; + + try { + matches = parser.MatchOperations(descriptions, operations); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('ParserError'); + expect(e.message).to.equal('No descriptions to match'); + thrown = true; + } + + expect(thrown).to.equal(true); + expect(c(matches)).to.deep.equal(expectedMatches); + }); + + it('should match two empty descriptions', async function () { + const parser = new RosettaSDK.Parser(); + + const descriptions = new Descriptions({ + operation_descriptions: [ + new OperationDescription({}), + new OperationDescription({}), + ], + }); + + const operations = [ + { + account: { + address: 'addr1', + sub_account: { + address: 'sub 3', + }, + }, + }, + + { + account: { + address: 'addr2', + sub_account: { + address: 'sub 2', + }, + }, + }, + ]; + + const expectedMatches = [ + { + operations: [{ + account: { + address: 'addr1', + sub_account: { + address: 'sub 3', + }, + }, + }], + amounts: [null], + }, + + { + operations: [{ + account: { + address: 'addr2', + sub_account: { + address: 'sub 2', + }, + } + }], + amounts: [null], + }, + ];; + + let matches; + + try { + matches = parser.MatchOperations(descriptions, operations); + } catch (e) { + console.error(e); + } + + expect(c(matches)).to.deep.equal(expectedMatches); + }); + + it('should reject to match empty operations', async function () { + const parser = new RosettaSDK.Parser(); + + const descriptions = new Descriptions({ + operation_descriptions: [ + new OperationDescription({}), + new OperationDescription({}), + ], + }); + + const operations = []; + + const expectedMatches = undefined; + + let matches; + let thrown = false; + + try { + matches = parser.MatchOperations(descriptions, operations); + } catch (e) { + // console.error(e); + thrown = true; + expect(e.name).to.equal('ParserError'); + expect(e.message).to.equal('Unable to match anything to zero operations'); + } + + expect(thrown).to.equal(true); + expect(c(matches)).to.deep.equal(expectedMatches); + }); + + it('should match simple repeated op', async function () { + const parser = new RosettaSDK.Parser(); + + const descriptions = new Descriptions({ + operation_descriptions: [ + new OperationDescription({ + account: new AccountDescription({ + exists: true, + }), + + amount: new AmountDescription({ + exists: true, + sign: Sign.Positive, + }), + + allow_repeats: true, + }), + ], + }); + + const operations = [ + { + account: { + address: 'addr2', + }, + + amount: { + value: '200', + }, + }, + {}, + { + account: { + address: 'addr1', + }, + amount: { + value: '100', + }, + }, + ]; + + const expectedMatches = [ + { + operations: [{ + account: { + address: 'addr2', + }, + + amount: { + value: '200', + }, + }, + + { + account: { + address: 'addr1', + }, + + amount: { + value: '100', + }, + }], + + amounts: [200, 100], + }, + ]; + + let matches; + + try { + matches = parser.MatchOperations(descriptions, operations); + } catch (e) { + console.error(e); + } + + expect(c(matches)).to.deep.equal(expectedMatches); + }); + + it('should reject to match simple repeated op, when unmatched are not allowed', async function () { + const parser = new RosettaSDK.Parser(); + + const descriptions = new Descriptions({ + operation_descriptions: [ + new OperationDescription({ + account: new AccountDescription({ + exists: true, + }), + + amount: new AmountDescription({ + exists: true, + sign: Sign.Positive, + }), + + allow_repeats: true, + }), + ], + + err_unmatched: true, + }); + + const operations = [ + { + account: { + address: 'addr2', + }, + + amount: { + value: '200', + }, + }, + {}, + { + account: { + address: 'addr1', + }, + amount: { + value: '100', + }, + }, + ]; + + const expectedMatches = undefined; + + let matches; + let thrown = false; + + try { + matches = parser.MatchOperations(descriptions, operations); + + } catch (e) { + // console.error(e); + expect(e.name).to.equal('ParserError'); + expect(e.message).to.equal('Unable to find match for operation at index 1'); + thrown = true; + } + + expect(thrown).to.equal(true); + expect(c(matches)).to.deep.equal(expectedMatches); + }); + + it('should reject to match simple repeated op with invalid comparison indexes', async function () { + const parser = new RosettaSDK.Parser(); + + const descriptions = new Descriptions({ + opposite_amounts: [[0, 1]], + operation_descriptions: [ + new OperationDescription({ + account: new AccountDescription({ + exists: true, + }), + + amount: new AmountDescription({ + exists: true, + sign: Sign.Positive, + }), + + allow_repeats: true, + }), + ], + }); + + const operations = [ + { + account: { + address: 'addr2', + }, + + amount: { + value: '200', + }, + }, + {}, + { + account: { + address: 'addr1', + }, + amount: { + value: '100', + }, + }, + ]; + + const expectedMatches = undefined; + + let matches; + let thrown = false; + + try { + matches = parser.MatchOperations(descriptions, operations); + + } catch (e) { + expect(e.name).to.equal('ParserError'); + expect(e.message).to.equal('Match index 1 out of range: opposite amounts comparison error: group descriptions not met'); + thrown = true; + } + + expect(thrown).to.equal(true); + expect(c(matches)).to.deep.equal(expectedMatches); + }); + + it('should reject to match simple repeated op with overlapping, repeated descriptions', async function () { + const parser = new RosettaSDK.Parser(); + + const descriptions = new Descriptions({ + operation_descriptions: [ + new OperationDescription({ + account: new AccountDescription({ + exists: true, + }), + + amount: new AmountDescription({ + exists: true, + sign: Sign.Positive, + }), + + allow_repeats: true, + }), + + new OperationDescription({ // this description should never be matched. + account: new AccountDescription({ + exists: true, + }), + + amount: new AmountDescription({ + exists: true, + sign: Sign.Positive, + }), + + allow_repeats: true, + }), + ], + }); + + const operations = [ + { + account: { + address: 'addr2', + }, + + amount: { + value: '200', + }, + }, + {}, + { + account: { + address: 'addr1', + }, + amount: { + value: '100', + }, + }, + ]; + + const expectedMatches = undefined; + + let matches; + let thrown = false; + + try { + matches = parser.MatchOperations(descriptions, operations); + + } catch (e) { + // console.error(e); + expect(e.name).to.equal('ParserError'); + expect(e.message).to.equal('Could not find match for description 1'); + thrown = true; + } + + expect(thrown).to.equal(true); + expect(c(matches)).to.deep.equal(expectedMatches); + }); + + it('should match complex repeated ops', async function () { + const parser = new RosettaSDK.Parser(); + + const descriptions = new Descriptions({ + operation_descriptions: [ + new OperationDescription({ + account: new AccountDescription({ + exists: true, + }), + + amount: new AmountDescription({ + exists: true, + sign: Sign.Positive, + }), + + allow_repeats: true, + type: 'output', + }), + + new OperationDescription({ // this description should never be matched. + account: new AccountDescription({ + exists: true, + }), + + amount: new AmountDescription({ + exists: true, + sign: Sign.Negative, + }), + + allow_repeats: true, + type: 'input', + }), + + new OperationDescription({ // this description should never be matched. + account: new AccountDescription({ + exists: true, + }), + + amount: new AmountDescription({ + exists: true, + sign: Sign.Negative, + }), + + allow_repeats: true, + }), + ], + }); + + const operations = [ + { + account: { address: 'addr2' }, + amount: { value: '200' }, + type: 'output', + }, + { + account: { address: 'addr3' }, + amount: { value: '200' }, + type: 'output', + }, + { + account: { address: 'addr1' }, + amount: { value: '-200' }, + type: 'input', + }, + + { + account: { address: 'addr4' }, + amount: { value: '-200' }, + type: 'input', + }, + + { + account: { address: 'addr5' }, + amount: { value: '-1000' }, + type: 'runoff', + }, + ]; + + const expectedMatches = [ + { + operations: [{ + account: { address: 'addr2' }, + amount: { value: '200' }, + type: 'output', + }, + { + account: { address: 'addr3' }, + amount: { value: '200' }, + type: 'output', + }], + + amounts: [200, 200] + }, + + { + operations: [{ + account: { address: 'addr1' }, + amount: { value: '-200' }, + type: 'input', + }, + { + account: { address: 'addr4' }, + amount: { value: '-200' }, + type: 'input', + }], + + amounts: [-200, -200] + }, + + { + operations: [{ + account: { address: 'addr5' }, + amount: { value: '-1000' }, + type: 'runoff', + }], + + amounts: [-1000] + }, + ]; + + let matches; + + try { + matches = parser.MatchOperations(descriptions, operations); + } catch (e) { + console.error(e); + } + + expect(c(matches)).to.deep.equal(expectedMatches); + }); + + it('should match an optional description, that is not met', async function () { + const parser = new RosettaSDK.Parser(); + + const descriptions = new Descriptions({ + operation_descriptions: [ + new OperationDescription({ + account: new AccountDescription({ + exists: true, + }), + + amount: new AmountDescription({ + exists: true, + sign: Sign.Positive, + }), + + allow_repeats: true, + }), + + new OperationDescription({ // this description should never be matched. + account: new AccountDescription({ + exists: true, + }), + + amount: new AmountDescription({ + exists: true, + sign: Sign.Negative, + }), + + optional: true, + }), + ], + }); + + const operations = [ + { + account: { address: 'addr2' }, + amount: { value: '200' }, + }, + {}, + { + account: { address: 'addr1' }, + amount: { value: '100' }, + }, + ]; + + const expectedMatches = [ + { + operations: [{ + account: { address: 'addr2' }, + amount: { value: '200' }, + }, + { + account: { address: 'addr1' }, + amount: { value: '100' }, + }], + + amounts: [200, 100] + }, + + null, // optional not met, must not throw + ]; + + let matches; + + try { + matches = parser.MatchOperations(descriptions, operations); + } catch (e) { + console.error(e); + } + + expect(c(matches)).to.deep.equal(expectedMatches); + }); + + it('should reject to match an optional description when equal amounts were not found', async function () { + const parser = new RosettaSDK.Parser(); + + const descriptions = new Descriptions({ + equal_amounts: [[0, 1]], + operation_descriptions: [ + new OperationDescription({ + account: new AccountDescription({ + exists: true, + }), + + amount: new AmountDescription({ + exists: true, + sign: Sign.Positive, + }), + + allow_repeats: true, + }), + + new OperationDescription({ // this description should never be matched. + account: new AccountDescription({ + exists: true, + }), + + amount: new AmountDescription({ + exists: true, + sign: Sign.Negative, + }), + + optional: true, + }), + ], + }); + + const operations = [ + { + account: { address: 'addr2' }, + amount: { value: '200' }, + }, + {}, + { + account: { address: 'addr1' }, + amount: { value: '100' }, + }, + ]; + + const expectedMatches = undefined; + + let matches; + let thrown = false; + + try { + matches = parser.MatchOperations(descriptions, operations); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('ParserError'); + expect(e.message).to.equal('Match index 1 is null: index 1 not valid: operation amounts are not equal: group descriptions not met'); + thrown = true; + } + + expect(thrown).to.equal(true); + expect(c(matches)).to.deep.equal(expectedMatches); + }); + + it('should reject to match an optional description when opposite amounts were not found', async function () { + const parser = new RosettaSDK.Parser(); + + const descriptions = new Descriptions({ + opposite_amounts: [[0, 1]], + operation_descriptions: [ + new OperationDescription({ + account: new AccountDescription({ + exists: true, + }), + + amount: new AmountDescription({ + exists: true, + sign: Sign.Positive, + }), + + allow_repeats: true, + }), + + new OperationDescription({ // this description should never be matched. + account: new AccountDescription({ + exists: true, + }), + + amount: new AmountDescription({ + exists: true, + sign: Sign.Negative, + }), + + optional: true, + }), + ], + }); + + const operations = [ + { + account: { address: 'addr2' }, + amount: { value: '200' }, + }, + {}, + { + account: { address: 'addr1' }, + amount: { value: '100' }, + }, + ]; + + const expectedMatches = undefined; + + let matches; + let thrown = false; + + try { + matches = parser.MatchOperations(descriptions, operations); + } catch (e) { + // console.error(e); + expect(e.name).to.equal('ParserError'); + expect(e.message).to.equal('Match index 1 is null: opposite amounts comparison error: group descriptions not met'); + thrown = true; + } + + expect(thrown).to.equal(true); + expect(c(matches)).to.deep.equal(expectedMatches); + }); + }); + + describe('Test Match', function () { + it('should handle an empty match correctly', async function () { + const op = null; + const amount = null; + const match = new RosettaSDK.Parser.Match({}).first(); + + expect(match.operation).to.deep.equal(op); + expect(match.amount).to.deep.equal(amount); + }); + + it('should handle a single op match', async function () { + const op = { + operation_identifier: { index: 1 }, + }; + + const amount = 100; + + const match = new RosettaSDK.Parser.Match({ + operations: [{ + operation_identifier: { index: 1 }, + }], + + amounts: [100], + }).first(); + + expect(match.operation).to.deep.equal(op); + expect(match.amount).to.deep.equal(amount); + }); + + it('should handle multi op match properly', async function () { + const op = { + operation_identifier: { index: 1 }, + }; + + const amount = 100; + + const match = new RosettaSDK.Parser.Match({ + operations: [{ + operation_identifier: { index: 1 }, + }, { + operation_identifier: { index: 2 }, + }], + + amounts: [100, 200], + }).first(); + + expect(match.operation).to.deep.equal(op); + expect(match.amount).to.deep.equal(amount); + }); + + it('should handle multi op match with null amount correctly', async function () { + const op = { + operation_identifier: { index: 1 }, + }; + + const amount = null; + + const match = new RosettaSDK.Parser.Match({ + operations: [{ + operation_identifier: { index: 1 }, + }], + + amounts: [null], + }).first(); + + expect(match.operation).to.deep.equal(op); + expect(match.amount).to.deep.equal(amount); + }); + + }); +}); \ No newline at end of file diff --git a/test/reconciler.test.js b/test/reconciler.test.js new file mode 100644 index 0000000..9fab5b0 --- /dev/null +++ b/test/reconciler.test.js @@ -0,0 +1,469 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +// reconciler.test.js + +const { expect } = require('chai'); +const RosettaSDK = require('..'); + +const { Hash } = require('../lib/utils'); + +const { + AccountCurrency +} = RosettaSDK.Reconciler; + +const { + AccountIdentifier, + Currency, + Amount, + Block, + SubAccountIdentifier, + BlockIdentifier, +} = RosettaSDK.Client; + +const templateReconciler = () => { + return new RosettaSDK.Reconciler(); +}; + +describe('Reconciler Tests', function () { + describe('Test Reconciler Constructor', function () { + const accountCurrency = new AccountCurrency( + new AccountIdentifier('acct 1'), + new Currency('BTC', 8), + ); + + it('should have default options set', async function () { + const options = {}; + const reconciler = new RosettaSDK.Reconciler(options); + + expect(reconciler.inactiveQueue).to.deep.equal([]); + expect(reconciler.seenAccounts).to.deep.equal({}); + expect(reconciler.interestingAccounts).to.deep.equal([]); + expect(reconciler.lookupBalanceByBlock).to.deep.equal(RosettaSDK.Reconciler.defaults.lookupBalanceByBlock); + expect(reconciler.changeQueue).to.deep.equal([]); + }); + + it('should have interesting accounts set', async function () { + const options = { + interestingAccounts: [accountCurrency], + }; + const reconciler = new RosettaSDK.Reconciler(options); + + expect(reconciler.inactiveQueue).to.deep.equal([]); + expect(reconciler.seenAccounts).to.deep.equal({}); + expect(reconciler.interestingAccounts).to.deep.equal([accountCurrency]); + expect(reconciler.lookupBalanceByBlock).to.deep.equal(RosettaSDK.Reconciler.defaults.lookupBalanceByBlock); + expect(reconciler.changeQueue).to.deep.equal([]); + }); + + it('should have seen accounts set', async function () { + const options = { + withSeenAccounts: [accountCurrency], + }; + const reconciler = new RosettaSDK.Reconciler(options); + + expect(reconciler.inactiveQueue).to.deep.equal([{ + entry: accountCurrency, + }]); + + expect(reconciler.seenAccounts).to.deep.equal({ + [Hash(accountCurrency)]: {}, + }); + + expect(reconciler.interestingAccounts).to.deep.equal([]); + expect(reconciler.lookupBalanceByBlock).to.deep.equal(RosettaSDK.Reconciler.defaults.lookupBalanceByBlock); + expect(reconciler.changeQueue).to.deep.equal([]); + }); + + it('should have the correct setting for lookupBalanceByBlock', async function () { + const options = { + lookupBalanceByBlock: false, + }; + const reconciler = new RosettaSDK.Reconciler(options); + + expect(reconciler.inactiveQueue).to.deep.equal([]); + expect(reconciler.seenAccounts).to.deep.equal({}); + expect(reconciler.interestingAccounts).to.deep.equal([]); + expect(reconciler.lookupBalanceByBlock).to.deep.equal(false); + expect(reconciler.changeQueue).to.deep.equal([]); + }); + }); + + describe('Contains AccountCurrency', function () { + const currency1 = new Currency('blah', 2); + const currency2 = new Currency('blah2', 2); + + const accountsArray = [ + new AccountCurrency({ // test using object as args + accountIdentifier: new AccountIdentifier('test'), + currency: currency1, + }), + + new AccountCurrency( // test using params as args + AccountIdentifier.constructFromObject({ + address: 'cool', + sub_account: new SubAccountIdentifier('test2'), + }), + currency1, + ), + + new AccountCurrency( + AccountIdentifier.constructFromObject({ + address: 'cool', + sub_account: SubAccountIdentifier.constructFromObject({ + address: 'test2', + metadata: { 'neat': 'stuff' }, + }), + }), + currency1, + ), + ]; + + const accounts = accountsArray.reduce((a, c, i) => { + a[Hash(c)] = {}; + return a; + }, {}); + + it('should not find a non-existing account', async function () { + const found = RosettaSDK.Reconciler.ContainsAccountCurrency(accounts, new AccountCurrency( + new AccountIdentifier('blah'), + currency1, + )); + + expect(found).to.equal(false); + }); + + it('should find a basic account', async function () { + const found = RosettaSDK.Reconciler.ContainsAccountCurrency(accounts, new AccountCurrency( + new AccountIdentifier('test'), + currency1, + )); + + expect(found).to.equal(true); + }); + + it('should not find a basic account with a bad currency', async function () { + const found = RosettaSDK.Reconciler.ContainsAccountCurrency(accounts, new AccountCurrency( + new AccountIdentifier('test'), + currency2, + )); + + expect(found).to.equal(false); + }); + + it('should find an account with subaccount', async function () { + const found = RosettaSDK.Reconciler.ContainsAccountCurrency(accounts, new AccountCurrency( + AccountIdentifier.constructFromObject({ + address: 'cool', + sub_account: SubAccountIdentifier.constructFromObject({ + address: 'test2', + }), + }), + currency1, + )); + + expect(found).to.equal(true); + }); + + it('should find an account with subaccount and metadata', async function () { + const found = RosettaSDK.Reconciler.ContainsAccountCurrency(accounts, new AccountCurrency( + AccountIdentifier.constructFromObject({ + address: 'cool', + sub_account: SubAccountIdentifier.constructFromObject({ + address: 'test2', + metadata: { 'neat': 'stuff' }, + }), + }), + currency1, + )); + + expect(found).to.equal(true); + }); + + it('should not find an account with subaccount and metadata', async function () { + const found = RosettaSDK.Reconciler.ContainsAccountCurrency(accounts, new AccountCurrency( + AccountIdentifier.constructFromObject({ + address: 'cool', + sub_account: SubAccountIdentifier.constructFromObject({ + address: 'test2', + metadata: { 'neater': 'stuff' }, + }), + }), + currency1, + )); + + expect(found).to.equal(false); + }); + }); + + describe('Test ExtractAmount', function () { + const currency1 = new Currency('curr1', 4); + const currency2 = new Currency('curr2', 7); + const amount1 = new Amount('100', currency1); + const amount2 = new Amount('200', currency2); + const balances = [ amount1, amount2 ]; + const badCurr = new Currency('no curr', 100); + + it('should not be able to extract balance of a bad currency', async function () { + let thrown = false; + + try { + RosettaSDK.Reconciler.extractAmount(balances, badCurr); + } catch (e) { + expect(e.message).to.equal('Could not extract amount for {"symbol":"no curr","decimals":100}'); + thrown = true; + } + + expect(thrown).to.equal(true); + }); + + it('should find a simple account', async function () { + let thrown = false; + + try { + const result = RosettaSDK.Reconciler.extractAmount(balances, currency1); + expect(result).to.deep.equal(amount1); + } catch (e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + + it('should find another simple account', async function () { + let thrown = false; + + try { + const result = RosettaSDK.Reconciler.extractAmount(balances, currency2); + expect(result).to.deep.equal(amount2); + } catch (e) { + console.error(e); + thrown = true; + } + + expect(thrown).to.equal(false); + }); + }); + + describe('Test CompareBalance', async function () { + const account1 = new AccountIdentifier('blah'); + const account2 = AccountIdentifier.constructFromObject({ + address: 'blah', + sub_account: new SubAccountIdentifier('sub blah'), + }); + + const currency1 = new Currency('curr1', 4); + const currency2 = new Currency('curr2', 7); + + const amount1 = new Amount('100', currency1); + const amount2 = new Amount('200', currency2); + + const block0 = new BlockIdentifier(0, 'block0'); + const block1 = new BlockIdentifier(1, 'block1'); + const block2 = new BlockIdentifier(2, 'block2'); + + const helper = { + headBlock: null, + storedBlocks: {}, + + balanceAccount: null, + balanceAmount: null, + balanceBlock: null, + + blockExists: function (blockIdentifier) { + return this.storedBlocks[blockIdentifier.hash] != null; + }, + + currentBlock: function () { + if (!this.headBlock) + throw new Error('Head Block is null'); + + return this.headBlock; + }, + + accountBalance: function (account, currency, headBlock) { + if (!this.balanceAccount || Hash(this.balanceAccount) != Hash(account)) { + throw new Error('Account does not exist'); + } + + return { + cachedBalance: this.balanceAmount, + balanceBlock: this.balanceBlock, + }; + }, + }; + + const reconciler = new RosettaSDK.Reconciler({ helper }); + + it('should have no headblock set yet', async function () { + let thrown = false; + try { + const { difference, cachedBalance, headIndex } = await reconciler.compareBalance( + account1, + currency1, + amount1.value, + block1, + ); + } catch(e) { + thrown = true; + expect(e.message).to.equal('Head Block is null'); + } + + expect(thrown).to.equal(true); + }); + + it('should set the head block', function () { + helper.headBlock = block0; + }); + + it('should throw that live block is ahead of head block', async function () { + let thrown = false; + try { + const { difference, cachedBalance, headIndex } = await reconciler.compareBalance( + account1, + currency1, + amount1.value, + block1, + ); + } catch(e) { + thrown = true; + // console.error(e); + expect(e.message).to.equal('Live block 1 > head block 0'); + } + + expect(thrown).to.equal(true); + }); + + it('should set another head block', function () { + helper.headBlock = new BlockIdentifier(2, 'hash2'); + }); + + it('should throw that a block is missing', async function () { + let thrown = false; + try { + const { difference, cachedBalance, headIndex } = await reconciler.compareBalance( + account1, + currency1, + amount1.value, + block1, + ); + } catch(e) { + thrown = true; + // console.error(e); + expect(e.message).to.equal('Block gone! Block hash = block1'); + } + + expect(thrown).to.equal(true); + }); + + it('should reconfigure helper', function () { + helper.storedBlocks[block0.hash] = new Block(block0, block0); + helper.storedBlocks[block1.hash] = new Block(block1, block0); + helper.storedBlocks[block2.hash] = new Block(block2, block1); + helper.balanceAccount = account1; + helper.balanceAmount = amount1; + helper.balanceBlock = block1; + }); + + it('should throw an error that the account was updated after live block', async function () { + let thrown = false; + try { + const { difference, cachedBalance, headIndex } = await reconciler.compareBalance( + account1, + currency1, + amount1.value, + block0, + ); + } catch(e) { + thrown = true; + // console.error(e); + expect(e.message).to.equal('Account updated: {"address":"blah"} updated at blockheight 1'); + } + + expect(thrown).to.equal(true); + }); + + it('should return the correct account balance', async function () { + let thrown = false; + try { + const { difference, cachedBalance, headIndex } = await reconciler.compareBalance( + account1, + currency1, + amount1.value, + block1, + ); + + expect(difference).to.equal('0'); + expect(cachedBalance).to.equal(amount1.value); + expect(headIndex).to.equal(2); + } catch(e) { + thrown = true; + console.error(e); + } + + expect(thrown).to.equal(false); + }); + + it('should return another balance', async function () { + let thrown = false; + try { + const { difference, cachedBalance, headIndex } = await reconciler.compareBalance( + account1, + currency1, + amount2.value, + block2, + ); + + expect(difference).to.equal('-100'); + expect(cachedBalance).to.equal(amount1.value); + expect(headIndex).to.equal(2); + } catch(e) { + thrown = true; + console.error(e); + } + + expect(thrown).to.equal(false); + }); + + it('should throw when comparing balance for a non-existing account', async function () { + let thrown = false; + try { + const { difference, cachedBalance, headIndex } = await reconciler.compareBalance( + account2, + currency1, + amount2.value, + block2, + ); + + expect(difference).to.equal('0'); + expect(cachedBalance).to.equal(""); + expect(headIndex).to.equal(2); + } catch(e) { + thrown = true; + expect(e.message).to.equal('Account does not exist'); + } + + expect(thrown).to.equal(true); + }); + }); +}); + diff --git a/test/server.test.js b/test/server.test.js new file mode 100644 index 0000000..039217b --- /dev/null +++ b/test/server.test.js @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +// server.test.js +const { expect } = require('chai'); +const RosettaSDK = require('..'); + diff --git a/test/syncer.test.js b/test/syncer.test.js new file mode 100644 index 0000000..6eaba33 --- /dev/null +++ b/test/syncer.test.js @@ -0,0 +1,281 @@ +/** + * Copyright (c) 2020 DigiByte Foundation NZ Limited + * + * 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. + */ + +// syncer.test.js +const { expect, should } = require('chai'); +const RosettaSDK = require('..'); + +const networkIdentifier = { + blockchain: "blah", + network: "testnet", +}; + +const currency = { + symbol: "Blah", + decimals: 2, +}; + +const recipient = { + address: "acct1", +}; + +const recipientAmount = { + value: "100", + currency: currency, +}; + +const recipientOperation = { + operation_identifier: { + index: 0, + }, + type: "Transfer", + status: "Success", + account: recipient, + amount: recipientAmount, +}; + +const recipientFailureOperation = { + operation_identifier: { + index: 1, + }, + type: "Transfer", + status: "Failure", + account: recipient, + amount: recipientAmount, +}; + +const recipientTransaction = { + transaction_identifier: { + hash: "tx1", + }, + operations: [ + recipientOperation, + recipientFailureOperation, + ], +}; + +const sender = { + address: "acct2", +}; + +const senderAmount = { + value: "-100", + currency: currency, +}; + +const senderOperation = { + operation_identifier: { + index: 0, + }, + type: "Transfer", + status: "Success", + account: sender, + amount: senderAmount, +}; + +const senderTransaction = { + transaction_identifier: { + hash: "tx2", + }, + operations: [ + senderOperation, + ], +}; + +const orphanGenesis = { + block_identifier: { + hash: "1", + index: 1, + }, + parent_block_identifier: { + hash: "0a", + index: 0, + }, + transactions: [], +}; + +const blockSequence = [ + { // genesis + block_identifier: { + hash: "0", + index: 0, + }, + parent_block_identifier: { + hash: "0", + index: 0, + }, + }, + { + block_identifier: { + hash: "1", + index: 1, + }, + parent_block_identifier: { + hash: "0", + index: 0, + }, + transactions: [ + recipientTransaction, + ], + }, + { // reorg + block_identifier: { + hash: "2", + index: 2, + }, + parent_block_identifier: { + hash: "1a", + index: 1, + }, + }, + { + block_identifier: { + hash: "1a", + index: 1, + }, + parent_block_identifier: { + hash: "0", + index: 0, + }, + }, + { + block_identifier: { + hash: "3", + index: 3, + }, + parent_block_identifier: { + hash: "2", + index: 2, + }, + transactions: [ + senderTransaction, + ], + }, + { // invalid block + block_identifier: { + hash: "5", + index: 5, + }, + parent_block_identifier: { + hash: "4", + index: 4, + }, + }, +]; + +describe('Syncer', function () { + should(); + + const syncer = new RosettaSDK.Syncer({ + networkIdentifier: networkIdentifier, + genesisBlock: blockSequence[0].block_identifier, + // fetcher + }); + + let lastEvent = null; + + syncer.on(RosettaSDK.Syncer.Events.BLOCK_ADDED, (block) => { + // console.log('Block added', block); + lastEvent = 'BLOCK_ADDED'; + }); + + syncer.on(RosettaSDK.Syncer.Events.BLOCK_REMOVED, (block) => { + // console.log('Block removed', block); + lastEvent = 'BLOCK_REMOVED'; + }); + + syncer.on(RosettaSDK.Syncer.Events.SYNC_CANCELLED, () => { + // console.log('CANCELLED'); + lastEvent = 'CANCELLED'; + }); + + it('should exist no block', async function () { + expect(syncer.pastBlocks).to.deep.equal([]); + + await syncer.processBlock(blockSequence[0]); + expect(lastEvent).to.equal('BLOCK_ADDED'); + expect(syncer.nextIndex).to.equal(1); + expect(blockSequence[0].block_identifier, syncer.pastBlocks[syncer.pastBlocks.length - 1]); + expect(syncer.pastBlocks).to.deep.equal([blockSequence[0].block_identifier]); + }); + + it('should not be able to remove the genesis block', async function () { + const err = await syncer.processBlock(orphanGenesis).catch(e => e); + expect(err instanceof RosettaSDK.Errors.SyncerError).to.be.true; + expect(err.name).to.equal('SyncerError'); + expect(err.message).to.equal('Cannot remove genesis block'); + }); + + it('should exist no block, no reorg should be required', async function () { + await syncer.processBlock(blockSequence[1]); + expect(lastEvent).to.equal('BLOCK_ADDED'); + expect(syncer.nextIndex).to.equal(2); + expect(blockSequence[1].block_identifier, syncer.pastBlocks[syncer.pastBlocks.length - 1]); + expect(syncer.pastBlocks).to.deep.equal([ + blockSequence[0].block_identifier, + blockSequence[1].block_identifier, + ]); + }); + + it('should handle orphan blocks', async function () { + await syncer.processBlock(blockSequence[2]); + expect(lastEvent).to.equal('BLOCK_REMOVED'); + expect(syncer.nextIndex).to.equal(1); + expect(blockSequence[0].block_identifier, syncer.pastBlocks[syncer.pastBlocks.length - 1]); + expect(syncer.pastBlocks).to.deep.equal([ + blockSequence[0].block_identifier, + ]); + + await syncer.processBlock(blockSequence[3]); + expect(lastEvent).to.equal('BLOCK_ADDED'); + expect(syncer.nextIndex).to.equal(2); + expect(blockSequence[3].block_identifier, syncer.pastBlocks[syncer.pastBlocks.length - 1]); + expect(syncer.pastBlocks).to.deep.equal([ + blockSequence[0].block_identifier, + blockSequence[3].block_identifier, + ]); + + await syncer.processBlock(blockSequence[2]); + expect(lastEvent).to.equal('BLOCK_ADDED'); + expect(syncer.nextIndex).to.equal(3); + expect(blockSequence[2].block_identifier, syncer.pastBlocks[syncer.pastBlocks.length - 1]); + expect(syncer.pastBlocks).to.deep.equal([ + blockSequence[0].block_identifier, + blockSequence[3].block_identifier, + blockSequence[2].block_identifier, + ]); + }); + + it('should handle out of order blocks', async function () { + const error = await syncer.processBlock(blockSequence[5]).catch(e => e); + expect(error instanceof RosettaSDK.Errors.SyncerError).to.be.true; + expect(error.name).to.equal('SyncerError'); + expect(error.message).to.equal(`Get block 5 instead of 3`); + + expect(syncer.nextIndex).to.equal(3); + expect(blockSequence[2].block_identifier, syncer.pastBlocks[syncer.pastBlocks.length - 1]); + expect(syncer.pastBlocks).to.deep.equal([ + blockSequence[0].block_identifier, + blockSequence[3].block_identifier, + blockSequence[2].block_identifier, + ]); + }); +});