diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..55f440f7b --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +node_modules +**/*.json \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 000000000..7fc0bff8e --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,42 @@ +{ + "env": { + "es6": true, + "node": true, + "jasmine": true + }, + "extends": [ + "standard" + ], + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 11, + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint" + ], + "rules": { + "dot-notation": ["off"], + "max-len": ["error", { "code": 160, "ignoreStrings": true }], + "no-multi-spaces": ["error", { "ignoreEOLComments": true }], + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + "vars": "all", + "args": "after-used", + "ignoreRestSiblings": true, + "argsIgnorePattern": "^_" + } + ], + "padded-blocks": ["off"], + "quotes": ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": true} ], + "semi": ["off"], + "@typescript-eslint/semi": ["error", "always"], + "sort-imports": "error" + } +} diff --git a/.gitignore b/.gitignore index bd06f486b..493a70c8c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,3 @@ nunitresults.xml # Istanbul code coverage folders .nyc_output/ coverage/ - -# sidetree-ipfs data folder -sidetree-ipfs/ diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 62d802f38..000000000 --- a/docker-compose.yml +++ /dev/null @@ -1,63 +0,0 @@ -# Docker Compose for single sidetree bitcoin node - -version: '3.4' -services: - sidetree-bitcore: - image: sidetree-bitcore:latest - build: - context: . - dockerfile: docker/sidetree-bitcore/Dockerfile - ports: - - 3001:3001 - volumes: - - bitcoin-data:/app/sidetree-bitcore/data - environment: - BITCOIN_NETWORK: testnet - - sidetree-bitcoin: - image: sidetree-bitcoin:latest - build: - context: . - dockerfile: docker/sidetree-bitcoin/Dockerfile - ports: - - 3002:3002 - environment: - MONGODB_CONNECTION_STRING: "mongodb://root:example@mongo:27017/" - depends_on: - - mongo - - - sidetree-core: - image: sidetree-core:latest - build: - context: . - dockerfile: docker/sidetree-core/Dockerfile - ports: - - 3000:3000 - environment: - MONGODB_CONNECTION_STRING: "mongodb://root:example@mongo:27017/" - BLOCKCHAIN_SERVICE_URI: "http://sidetree-bitcoin:3002" - depends_on: - - mongo - - sidetree-bitcoin - - mongo: - image: mongo - restart: always - ports: - - 27017:27017 - environment: - MONGO_INITDB_ROOT_USERNAME: root - MONGO_INITDB_ROOT_PASSWORD: example - - mongo-express: - image: mongo-express - restart: always - ports: - - 8081:8081 - environment: - ME_CONFIG_MONGODB_ADMINUSERNAME: root - ME_CONFIG_MONGODB_ADMINPASSWORD: example - -volumes: - bitcoin-data: diff --git a/docs/bitcoin.md b/docs/bitcoin.md new file mode 100644 index 000000000..d40d02283 --- /dev/null +++ b/docs/bitcoin.md @@ -0,0 +1,31 @@ +# Bitcoin Blockchain Service Reference Implementation + + +## Value Time Lock + +### Protocol parameters + +| Protocol parameters | Description | +| ------------------------------------ | ---------------------------------------- | +| minimumValueTimeLockDurationInBlocks | TODO | +| maximumValueTimeLockDurationInBlocks | TODO | + +### Configuration parameters +* valueTimeLockUpdateEnabled + +This parameter controls whether the value time lock is actively being renewed and if the funds will be returned to wallet in case of `valueTimeLockAmountInBitcoins` being set to zero. When this parameter is set to `false`, parameters `valueTimeLockAmountInBitcoins`, `valueTimeLockPollPeriodInSeconds` and `valueTimeLockTransactionFeesAmountInBitcoins` will be ignored. + +* valueTimeLockAmountInBitcoins + +The desired fund locked to write larger operation batches. Set to 0 will causes existing locked fund (if exists) to be released back to wallet upon lock expiry. + +* valueTimeLockPollPeriodInSeconds + +The polling duration between checks to see if the value time lock needs to be re-locked or released back to wallet. + +* valueTimeLockTransactionFeesAmountInBitcoins + +The fund allocated for transaction fees for subsequent re-locking of the initial value time lock. + +> Developer's note: +This allotted amount is locked together with value time lock for simplicity of re-lock implementation. If this allotted amount is depleted due to subsequent re-locks, the remaining locked amount will be released back to wallet, and a new lock will be created with this allotted amount added to it again. diff --git a/docs/implementation.md b/docs/core.md similarity index 88% rename from docs/implementation.md rename to docs/core.md index 09854ddc3..2a50a6c9b 100644 --- a/docs/implementation.md +++ b/docs/core.md @@ -1,4 +1,4 @@ -# Sidetree Node.js Implementation Document +# Sidetree Core Node.js Implementation Document This document focuses on the Node.js implementation of the Sidetree protocol. @@ -82,6 +82,7 @@ The orchestration layer requires implementation of following interfaces per prot - `IRequestHandler` - Handles REST API requests. +## Core Service REST API ### REST API HTTP Response status codes @@ -94,9 +95,7 @@ The orchestration layer requires implementation of following interfaces per prot | 500 | Server error. | - -## Core Serivce REST API -The Core Service REST API impliments the [Sidetree REST API](https://identity.foundation/sidetree/api/), in addition it also exposes the following version API. +The Core Service REST API implements the [Sidetree REST API](https://identity.foundation/sidetree/api/), in addition it also exposes the following version API. ### Fetch the current service versions. Fetches the current version of the core and the dependent services. The service implementation defines the versioning scheme and its interpretation. @@ -698,141 +697,6 @@ HTTP/1.1 200 OK } ``` - -## CAS REST API -The CAS (content addressable storage) REST API interface aims to abstract the underlying Sidetree storage away from the main protocol logic. This allows the CAS to be updated or even replaced if needed without affecting the core protocol logic. Conversely, the interface also allows the protocol logic to be implemented in an entirely different language while interfacing with the same CAS. - -All hashes used in the API are encoded multihash as specified by the Sidetree protocol. - -### Read content -Read the content of a given address and return it in the response body as octet-stream. - -#### Request path -``` -GET /?max-size= -``` - -#### Request query parameters -- `max-size` - - Required. - - If the content exceeds the specified maximum allowed size, `HTTP 400 Bad Request` with `content_exceeds_maximum_allowed_size` as the value for the `code` parameter in a JSON body is returned. - - -#### Request example -``` -GET /QmWd5PH6vyRH5kMdzZRPBnf952dbR4av3Bd7B2wBqMaAcf -``` -#### Response headers -| Name | Value | -| --------------------- | ---------------------- | -| ```Content-Type``` | ```application/octet-stream``` | - -#### Response example - Resoucre not found - -```http -HTTP/1.1 404 Not Found -``` - -#### Response example - Content exceeds maximum allowed size - -```http -HTTP/1.1 400 Bad Request - -{ - "code": "content_exceeds_maximum_allowed_size" -} -``` - -#### Response example - Content not a file - -```http -HTTP/1.1 400 Bad Request - -{ - "code": "content_not_a_file" -} -``` - -#### Response example - Content hash is invalid - -```http -HTTP/1.1 400 Bad Request - -{ - "code": "content_hash_invalid" -} -``` - -### Write content -Write content to CAS. - -#### Request path -``` -POST / -``` - -#### Request headers -| Name | Value | -| --------------------- | ---------------------- | -| ```Content-Type``` | ```application/octet-stream``` | - -#### Response headers -| Name | Value | -| --------------------- | ---------------------- | -| ```Content-Type``` | ```application/json``` | - -#### Response body schema -```json -{ - "hash": "Hash of data written to CAS" -} -``` - -#### Response body example -```json -{ - "hash": "QmWd5PH6vyRH5kMdzZRPBnf952dbR4av3Bd7B2wBqMaAcf" -} -``` - -### Fetch the current service version -Fetches the current version of the service. The service implementation defines the versioning scheme and its interpretation. - -Returns the service _name_ and _version_ of the CAS service. - -#### Request path -``` -GET /version -``` - -#### Request headers -None. - -#### Request example -``` -GET /version -``` - -#### Response body schema -```json -{ - "name": "A string representing the name of the service", - "version": "A string representing the version of currently running service." -} -``` - -#### Response example -```http -HTTP/1.1 200 OK - -{ - "name": "ipfs", - "version": "1.0.0" -} -``` - ## Frequently Asked Questions ### Why is the signature not verified before a request is queued and written to the blockchain? End users are expected to use a "user agent" for making requests which should almost always generate the right signature, diff --git a/docs/docker.md b/docs/docker.md deleted file mode 100644 index 29356bf51..000000000 --- a/docs/docker.md +++ /dev/null @@ -1,33 +0,0 @@ -# Sidetree Docker Images - -## Overview -The Sidetree components can be build an operated as dockerized containers. To run a sidetree installation, you'll need the following images: - -- *sidetree-bitcore* -- Bitcore Node with Sidetree Bitcore Extension -- *sidetree-bitcoin* -- Generic Blockchain Interface with Bitcoin Implementation -- *sidetree-core* -- Sidetree Core Interface -- *mongo* -- *ipfs* - -## Environment -You'll need to have Docker Environment setup. To interact with the `docker-compose.yaml` you'll also need `docker-compose`. - -## Build - -To build all containers locally run - - docker-compose build - -in the root directory of this repository. - -## Run via compose - - docker-compose up {"service"} - -## Container Configuration -Containers are (mostly) configured via environment variables. These are: - -### sidetree-bitcore -- BITCOIN_NETWORK: {testnet, livenet} -- BITCOIN_PRIVATE_KEY_WIF: "Private Key in XX Format" -- BITCOIN_FEE: "Tx Fee in Satoshi" diff --git a/lib/bitcoin/BitcoinBlockDataIterator.ts b/lib/bitcoin/BitcoinBlockDataIterator.ts index 35010eb82..4d1608704 100644 --- a/lib/bitcoin/BitcoinBlockDataIterator.ts +++ b/lib/bitcoin/BitcoinBlockDataIterator.ts @@ -1,5 +1,5 @@ -import BitcoinFileReader from './BitcoinFileReader'; import BitcoinBlockModel from './models/BitcoinBlockModel'; +import BitcoinFileReader from './BitcoinFileReader'; import BitcoinRawDataParser from './BitcoinRawDataParser'; /** diff --git a/lib/bitcoin/BitcoinClient.ts b/lib/bitcoin/BitcoinClient.ts index d9108b746..cc95cf4d7 100644 --- a/lib/bitcoin/BitcoinClient.ts +++ b/lib/bitcoin/BitcoinClient.ts @@ -1,16 +1,16 @@ import * as httpStatus from 'http-status'; +import { Address, Block, Networks, PrivateKey, Script, Transaction, Unit, crypto } from 'bitcore-lib'; +import nodeFetch, { FetchError, RequestInit, Response } from 'node-fetch'; import BitcoinBlockModel from './models/BitcoinBlockModel'; -import BitcoinSidetreeTransactionModel from './models/BitcoinSidetreeTransactionModel'; import BitcoinInputModel from './models/BitcoinInputModel'; +import BitcoinSidetreeTransactionModel from './models/BitcoinSidetreeTransactionModel'; import BitcoinLockTransactionModel from './models/BitcoinLockTransactionModel'; import BitcoinOutputModel from './models/BitcoinOutputModel'; import BitcoinTransactionModel from './models/BitcoinTransactionModel'; import BitcoinWallet from './BitcoinWallet'; import IBitcoinWallet from './interfaces/IBitcoinWallet'; -import nodeFetch, { FetchError, Response, RequestInit } from 'node-fetch'; -import ReadableStream from '../common/ReadableStream'; -import { Address, crypto, Networks, PrivateKey, Script, Transaction, Unit, Block } from 'bitcore-lib'; import { IBlockInfo } from './BitcoinProcessor'; +import ReadableStream from '../common/ReadableStream'; /** * Structure (internal to this class) to store the transaction information @@ -42,7 +42,8 @@ export default class BitcoinClient { bitcoinWalletOrImportString: IBitcoinWallet | string, private requestTimeout: number, private requestMaxRetries: number, - private sidetreeTransactionFeeMarkupPercentage: number) { + private sidetreeTransactionFeeMarkupPercentage: number, + private estimatedFeeSatoshiPerKB?: number) { if (typeof bitcoinWalletOrImportString === 'string') { console.info('Creating bitcoin wallet using the import string passed in.'); @@ -118,7 +119,7 @@ export default class BitcoinClient { * @param bitcoinLockTransaction The transaction object. */ public async broadcastLockTransaction (bitcoinLockTransaction: BitcoinLockTransactionModel): Promise { - const transactionHash = this.broadcastTransactionRpc(bitcoinLockTransaction.serializedTransactionObject); + const transactionHash = await this.broadcastTransactionRpc(bitcoinLockTransaction.serializedTransactionObject); console.info(`Broadcasted lock transaction: ${transactionHash}`); return transactionHash; @@ -336,14 +337,14 @@ export default class BitcoinClient { /** * Gets the transaction fee of a transaction in satoshis. * @param transactionId the id of the target transaction. - * @returns the transaction fee. + * @returns the transaction fee in satoshis. */ public async getTransactionFeeInSatoshis (transactionId: string): Promise { const transaction = await this.getRawTransaction(transactionId); let inputSatoshiSum = 0; - for (let i = 0 ; i < transaction.inputs.length ; i++) { + for (let i = 0; i < transaction.inputs.length; i++) { const currentInput = transaction.inputs[i]; const transactionOutValue = await this.getTransactionOutValueInSatoshi(currentInput.previousTransactionId, currentInput.outputIndexInPreviousTransaction); @@ -397,11 +398,11 @@ export default class BitcoinClient { return response.labels.length > 0 || response.iswatchonly; } - private async getCurrentEstimatedFeeInSatoshisPerKb (): Promise { + private async getCurrentEstimatedFeeInSatoshisPerKB (): Promise { const request = { method: 'estimatesmartfee', params: [ - 1 // Number of confirmation targtes + 1 // Number of confirmation targets ] }; @@ -418,6 +419,21 @@ export default class BitcoinClient { return BitcoinClient.convertBtcToSatoshis(feerateInBtc); } + /** Get the current estimated fee from RPC and update stored estimate */ + private async updateEstimatedFeeInSatoshisPerKB (): Promise { + let estimatedFeeSatoshiPerKB; + try { + estimatedFeeSatoshiPerKB = await this.getCurrentEstimatedFeeInSatoshisPerKB(); + this.estimatedFeeSatoshiPerKB = estimatedFeeSatoshiPerKB; + } catch (error) { + estimatedFeeSatoshiPerKB = this.estimatedFeeSatoshiPerKB; + if (!estimatedFeeSatoshiPerKB) { + throw error; + } + } + return estimatedFeeSatoshiPerKB; + } + /** Get the transaction out value in satoshi, for a specified output index */ private async getTransactionOutValueInSatoshi (transactionId: string, outputIndex: number) { const transaction = await this.getRawTransaction(transactionId); @@ -526,19 +542,20 @@ export default class BitcoinClient { * be already set to get the estimate more accurate. * * @param transaction The transaction for which the fee is to be calculated. + * @returns the transaction fee in satoshis. */ private async calculateTransactionFee (transaction: Transaction): Promise { - // Get esimtated fee from RPC - const estimatedFeeInKb = await this.getCurrentEstimatedFeeInSatoshisPerKb(); + // Get estimated fee from RPC + const estimatedFeePerKB = await this.updateEstimatedFeeInSatoshisPerKB(); // Estimate the size of the transaction const estimatedSizeInBytes = (transaction.inputs.length * 150) + (transaction.outputs.length * 50); - const estimatedSizeInKb = estimatedSizeInBytes / 1000; + const estimatedSizeInKB = estimatedSizeInBytes / 1000; - const estimatedFee = estimatedSizeInKb * estimatedFeeInKb; + const estimatedFee = estimatedSizeInKB * estimatedFeePerKB; // Add a percentage to the fee (trying to be on the higher end of the estimate) - const estimatedFeeWithPercentage = estimatedFee + (estimatedFee * .4); + const estimatedFeeWithPercentage = estimatedFee * 1.4; // Make sure that there are no decimals in the fee as it is not supported return Math.ceil(estimatedFeeWithPercentage); @@ -557,9 +574,9 @@ export default class BitcoinClient { const payToScriptAddress = new Address(payToScriptHashOutput); const freezeTransaction = new Transaction() - .from(unspentCoins) - .to(payToScriptAddress, freezeAmountInSatoshis) - .change(walletAddress); + .from(unspentCoins) + .to(payToScriptAddress, freezeAmountInSatoshis) + .change(walletAddress); const transactionFee = await this.calculateTransactionFee(freezeTransaction); @@ -573,8 +590,8 @@ export default class BitcoinClient { previousFreezeDurationInBlocks: number, newFreezeDurationInBlocks: number): Promise<[Transaction, Script]> { - // tslint:disable-next-line: max-line-length - console.info(`Creating a freeze transaction with freeze time in blocks: ${newFreezeDurationInBlocks} from previously frozen transaction with id: ${previousFreezeTransaction.id}`); + // eslint-disable-next-line max-len + console.info(`Creating a freeze transaction with freeze time of ${newFreezeDurationInBlocks} blocks, from previously frozen transaction with id: ${previousFreezeTransaction.id}`); const freezeScript = BitcoinClient.createFreezeScript(newFreezeDurationInBlocks, this.bitcoinWallet.getAddress()); const payToScriptHashOutput = Script.buildScriptHashOut(freezeScript); @@ -594,8 +611,8 @@ export default class BitcoinClient { previousFreezeTransaction: BitcoreTransactionWrapper, previousFreezeDurationInBlocks: number): Promise { - // tslint:disable-next-line: max-line-length - console.info(`Creating a transaction to return (to the wallet) the preivously frozen amount from transaction with id: ${previousFreezeTransaction.id} which was frozen for block duration: ${previousFreezeDurationInBlocks}`); + // eslint-disable-next-line max-len + console.info(`Creating a transaction to return (to the wallet) the previously frozen amount from transaction with id: ${previousFreezeTransaction.id} which was frozen for block duration: ${previousFreezeDurationInBlocks}`); return this.createSpendTransactionFromFrozenTransaction( previousFreezeTransaction, @@ -625,8 +642,8 @@ export default class BitcoinClient { // Now create a spend transaction using the frozen output. Create the transaction with all // inputs and outputs as they are needed to calculate the fee. const spendTransaction = new Transaction() - .from([frozenOutputAsInput]) - .to(paytoAddress, previousFreezeAmountInSatoshis); + .from([frozenOutputAsInput]) + .to(paytoAddress, previousFreezeAmountInSatoshis); // The check-sequence-verify lock requires transaction version 2 (spendTransaction as any).version = 2; @@ -644,7 +661,7 @@ export default class BitcoinClient { // and add another one with the correct amount. spendTransaction.outputs.shift(); spendTransaction.to(paytoAddress, previousFreezeAmountInSatoshis - transactionFee) - .fee(transactionFee); + .fee(transactionFee); return spendTransaction; } @@ -674,10 +691,10 @@ export default class BitcoinClient { const publicKeyHashOut = Script.buildPublicKeyHashOut(walletAddress); const redeemScript = Script.empty() - .add(lockBuffer) - .add(178) // OP_CSV (https://github.com/bitcoin/bips/blob/master/bip-0112.mediawiki) - .add(117) // OP_DROP - .add(publicKeyHashOut); + .add(lockBuffer) + .add(178) // OP_CSV (https://github.com/bitcoin/bips/blob/master/bip-0112.mediawiki) + .add(117) // OP_DROP + .add(publicKeyHashOut); return redeemScript; } @@ -748,11 +765,11 @@ export default class BitcoinClient { private async rpcCall (request: any, timeout: boolean): Promise { // append some standard jrpc parameters - request['jsonrpc'] = '1.0'; - request['id'] = Math.round(Math.random() * Number.MAX_SAFE_INTEGER).toString(32); + request.jsonrpc = '1.0'; + request.id = Math.round(Math.random() * Number.MAX_SAFE_INTEGER).toString(32); const requestString = JSON.stringify(request); - console.debug(`Sending jRPC request: id: ${request.id}, method: ${request['method']}`); + console.debug(`Sending jRPC request: id: ${request.id}, method: ${request.method}`); const requestOptions: RequestInit = { body: requestString, diff --git a/lib/bitcoin/BitcoinFileReader.ts b/lib/bitcoin/BitcoinFileReader.ts index 2b70e2193..193721c4d 100644 --- a/lib/bitcoin/BitcoinFileReader.ts +++ b/lib/bitcoin/BitcoinFileReader.ts @@ -1,7 +1,7 @@ +import * as fs from 'fs'; import ErrorCode from './ErrorCode'; import IBitcoinFileReader from './interfaces/IBitcoinFileReader'; import SidetreeError from '../common/SidetreeError'; -import * as fs from 'fs'; /** * concrete implementation of BitcoinFileReader diff --git a/lib/bitcoin/BitcoinProcessor.ts b/lib/bitcoin/BitcoinProcessor.ts index 78c9d945a..6d4eefab2 100644 --- a/lib/bitcoin/BitcoinProcessor.ts +++ b/lib/bitcoin/BitcoinProcessor.ts @@ -1,5 +1,6 @@ -import BitcoinBlockModel from './models/BitcoinBlockModel'; +import * as timeSpan from 'time-span'; import BitcoinBlockDataIterator from './BitcoinBlockDataIterator'; +import BitcoinBlockModel from './models/BitcoinBlockModel'; import BitcoinClient from './BitcoinClient'; import BitcoinServiceStateModel from './models/BitcoinServiceStateModel'; import BitcoinTransactionModel from './models/BitcoinTransactionModel'; @@ -18,8 +19,8 @@ import RequestError from './RequestError'; import ResponseStatus from '../common/enums/ResponseStatus'; import ServiceInfoProvider from '../common/ServiceInfoProvider'; import ServiceVersionModel from '../common/models/ServiceVersionModel'; -import SidetreeError from '../common/SidetreeError'; import SharedErrorCode from '../common/SharedErrorCode'; +import SidetreeError from '../common/SidetreeError'; import SidetreeTransactionParser from './SidetreeTransactionParser'; import SpendingMonitor from './SpendingMonitor'; import TransactionFeeModel from '../common/models/TransactionFeeModel'; @@ -29,8 +30,6 @@ import ValueTimeLockModel from '../common/models/ValueTimeLockModel'; import VersionManager from './VersionManager'; import VersionModel from '../common/models/VersionModel'; -import timeSpan = require('time-span'); - /** * Object representing a blockchain time and hash */ @@ -131,7 +130,8 @@ export default class BitcoinProcessor { config.bitcoinWalletOrImportString, config.requestTimeoutInMilliseconds || 300, config.requestMaxRetries || 3, - config.sidetreeTransactionFeeMarkupPercentage || 0); + config.sidetreeTransactionFeeMarkupPercentage || 0, + config.defaultTransactionFeeInSatoshisPerKB); this.sidetreeTransactionParser = new SidetreeTransactionParser(this.bitcoinClient, this.sidetreePrefix); @@ -145,17 +145,18 @@ export default class BitcoinProcessor { this.mongoDbLockTransactionStore = new MongoDbLockTransactionStore(config.mongoDbConnectionString, config.databaseName); const valueTimeLockTransactionFeesInBtc = config.valueTimeLockTransactionFeesAmountInBitcoins === 0 ? 0 - : config.valueTimeLockTransactionFeesAmountInBitcoins || 0.25; - - this.lockMonitor = - new LockMonitor( - this.bitcoinClient, - this.mongoDbLockTransactionStore, - this.lockResolver, - config.valueTimeLockPollPeriodInSeconds || 10 * 60, - BitcoinClient.convertBtcToSatoshis(config.valueTimeLockAmountInBitcoins), // Desired lock amount in satoshis - BitcoinClient.convertBtcToSatoshis(valueTimeLockTransactionFeesInBtc), // Txn Fees amount in satoshis - ProtocolParameters.maximumValueTimeLockDurationInBlocks); // Desired lock duration in blocks + : config.valueTimeLockTransactionFeesAmountInBitcoins || 0.25; + + this.lockMonitor = new LockMonitor( + this.bitcoinClient, + this.mongoDbLockTransactionStore, + this.lockResolver, + config.valueTimeLockPollPeriodInSeconds, + config.valueTimeLockUpdateEnabled, + BitcoinClient.convertBtcToSatoshis(config.valueTimeLockAmountInBitcoins), // Desired lock amount in satoshis + BitcoinClient.convertBtcToSatoshis(valueTimeLockTransactionFeesInBtc), // Txn Fees amount in satoshis + ProtocolParameters.maximumValueTimeLockDurationInBlocks // Desired lock duration in blocks + ); } /** @@ -193,7 +194,8 @@ export default class BitcoinProcessor { // NOTE: important to this initialization after we have processed all the blocks // this is because that the lock monitor needs the normalized fee calculator to // have all the data. - await this.lockMonitor.initialize(); + await this.lockMonitor.startPeriodicProcessing(); + void this.periodicPoll(); } @@ -272,7 +274,7 @@ export default class BitcoinProcessor { startingBlockHeight: number, heightOfEarliestKnownValidBlock: number) { - for (let block of blocks) { + for (const block of blocks) { if (block.height >= startingBlockHeight && block.height <= heightOfEarliestKnownValidBlock) { notYetValidatedBlocks.set( block.hash, @@ -281,8 +283,9 @@ export default class BitcoinProcessor { hash: block.hash, totalFee: BitcoinProcessor.getBitcoinBlockTotalFee(block), transactionCount: block.transactions.length, - previousHash: block.previousHash } - ); + previousHash: block.previousHash + } + ); await this.processSidetreeTransactionsInBlock(block); } } @@ -330,7 +333,7 @@ export default class BitcoinProcessor { // get the total fee including block reward const coinbaseTransaction = block.transactions[0]; let totalOutputSatoshi = 0; - for (let output of coinbaseTransaction.outputs) { + for (const output of coinbaseTransaction.outputs) { totalOutputSatoshi += output.satoshis; } @@ -370,8 +373,8 @@ export default class BitcoinProcessor { } catch (e) { const inputs = { blockHeight: block.height, blockHash: block.hash, transactionIndex: transactionIndex }; console.debug('An error happened when trying to add sidetree transaction to the store. Moving on to the next transaction. Inputs: %s\r\nFull error: %s', - JSON.stringify(inputs), - JSON.stringify(e, Object.getOwnPropertyNames(e))); + JSON.stringify(inputs), + JSON.stringify(e, Object.getOwnPropertyNames(e))); throw e; } @@ -426,7 +429,7 @@ export default class BitcoinProcessor { console.info(`Returning transactions since ${since ? 'block ' + TransactionNumber.getBlockNumber(since) : 'beginning'}...`); // deep copy last processed block const currentLastProcessedBlock = Object.assign({}, this.lastProcessedBlock!); - let [transactions, numOfBlocksAcquired] = await this.getTransactionsSince(since, currentLastProcessedBlock.height); + const [transactions, numOfBlocksAcquired] = await this.getTransactionsSince(since, currentLastProcessedBlock.height); // make sure the last processed block hasn't changed since before getting transactions // if changed, then a block reorg happened. @@ -455,7 +458,8 @@ export default class BitcoinProcessor { return block; } } - return; + + return undefined; } /** @@ -472,7 +476,8 @@ export default class BitcoinProcessor { return transaction; } } - return; + + return undefined; } /** @@ -490,7 +495,6 @@ export default class BitcoinProcessor { if (!feeWithinSpendingLimits) { throw new RequestError(ResponseStatus.BadRequest, SharedErrorCode.SpendingCapPerPeriodReached); - } // Write a warning if the balance is running low @@ -560,11 +564,11 @@ export default class BitcoinProcessor { /** * Gets the lock information which is currently held by this node. It throws an RequestError if none exist. */ - public getActiveValueTimeLockForThisNode (): ValueTimeLockModel { + public async getActiveValueTimeLockForThisNode (): Promise { let currentLock: ValueTimeLockModel | undefined; try { - currentLock = this.lockMonitor.getCurrentValueTimeLock(); + currentLock = await this.lockMonitor.getCurrentValueTimeLock(); } catch (e) { if (e instanceof SidetreeError && e.code === ErrorCode.LockMonitorCurrentValueTimeLockInPendingState) { diff --git a/lib/bitcoin/BitcoinRawDataParser.ts b/lib/bitcoin/BitcoinRawDataParser.ts index d0a5dc6a5..57228780c 100644 --- a/lib/bitcoin/BitcoinRawDataParser.ts +++ b/lib/bitcoin/BitcoinRawDataParser.ts @@ -1,8 +1,8 @@ import BitcoinBlockModel from './models/BitcoinBlockModel'; import BitcoinClient from './BitcoinClient'; +import { Block } from 'bitcore-lib'; import ErrorCode from './ErrorCode'; import SidetreeError from '../common/SidetreeError'; -import { Block } from 'bitcore-lib'; /** * Parser for raw bitcoin block data @@ -19,6 +19,7 @@ export default class BitcoinRawDataParser { regtest: Buffer.from('fabfb5da', 'hex'), skip: Buffer.from('00000000', 'hex') // this means to skip the rest of the file }; + private static magicBytesLength = 4; private static sizeBytesLength = 4; diff --git a/lib/bitcoin/BitcoinWallet.ts b/lib/bitcoin/BitcoinWallet.ts index 5d4acc6ab..c1610e1d4 100644 --- a/lib/bitcoin/BitcoinWallet.ts +++ b/lib/bitcoin/BitcoinWallet.ts @@ -1,7 +1,7 @@ -import IBitcoinWallet from './interfaces/IBitcoinWallet'; +import { Address, PrivateKey, Script, Transaction } from 'bitcore-lib'; import ErrorCode from './ErrorCode'; +import IBitcoinWallet from './interfaces/IBitcoinWallet'; import SidetreeError from '../common/SidetreeError'; -import { Address, PrivateKey, Script, Transaction } from 'bitcore-lib'; /** * Represents a bitcoin wallet. @@ -58,9 +58,9 @@ export default class BitcoinWallet implements IBitcoinWallet { // Create a script and add it to the input. const inputScript = Script.empty() - .add(signature.toTxFormat()) - .add(this.walletPublicKeyAsBuffer) - .add(inputRedeemScript.toBuffer()); + .add(signature.toTxFormat()) + .add(this.walletPublicKeyAsBuffer) + .add(inputRedeemScript.toBuffer()); (lockTransaction.inputs[0] as any).setScript(inputScript); diff --git a/lib/bitcoin/IBitcoinConfig.ts b/lib/bitcoin/IBitcoinConfig.ts index 590eac94b..05edbb8cd 100644 --- a/lib/bitcoin/IBitcoinConfig.ts +++ b/lib/bitcoin/IBitcoinConfig.ts @@ -11,16 +11,18 @@ export default interface IBitcoinConfig { bitcoinRpcUsername: string | undefined; bitcoinRpcPassword: string | undefined; bitcoinWalletOrImportString: IBitcoinWallet | string; - lowBalanceNoticeInDays: number | undefined; - sidetreeTransactionPrefix: string; + databaseName: string; + defaultTransactionFeeInSatoshisPerKB: number | undefined; genesisBlockNumber: number; + lowBalanceNoticeInDays: number | undefined; mongoDbConnectionString: string; - databaseName: string; requestTimeoutInMilliseconds: number | undefined; requestMaxRetries: number | undefined; - transactionPollPeriodInSeconds: number | undefined; sidetreeTransactionFeeMarkupPercentage: number; + sidetreeTransactionPrefix: string; + transactionPollPeriodInSeconds: number | undefined; + valueTimeLockUpdateEnabled: boolean; valueTimeLockAmountInBitcoins: number; - valueTimeLockPollPeriodInSeconds: number | undefined; + valueTimeLockPollPeriodInSeconds: number; valueTimeLockTransactionFeesAmountInBitcoins: number | undefined; } diff --git a/lib/bitcoin/MongoDbBlockMetadataStore.ts b/lib/bitcoin/MongoDbBlockMetadataStore.ts index 9c032b928..ae8623088 100644 --- a/lib/bitcoin/MongoDbBlockMetadataStore.ts +++ b/lib/bitcoin/MongoDbBlockMetadataStore.ts @@ -1,7 +1,7 @@ +import { Collection, Cursor } from 'mongodb'; import BlockMetadata from './models/BlockMetadata'; import IBlockMetadataStore from './interfaces/IBlockMetadataStore'; import MongoDbStore from '../common/MongoDbStore'; -import { Collection, Cursor } from 'mongodb'; /** * Implementation of IBlockMetadataStore using MongoDB database. @@ -50,10 +50,12 @@ export default class MongoDbBlockMetadataStore extends MongoDbStore implements I let dbCursor: Cursor; // Add filter to query. - dbCursor = this.collection!.find({ $and: [ - { height: { $gte: fromInclusiveHeight } }, - { height: { $lt: toExclusiveHeight } } - ] }); + dbCursor = this.collection!.find({ + $and: [ + { height: { $gte: fromInclusiveHeight } }, + { height: { $lt: toExclusiveHeight } } + ] + }); // Add sort to query. dbCursor = dbCursor.sort({ height: 1 }); @@ -106,7 +108,7 @@ export default class MongoDbBlockMetadataStore extends MongoDbStore implements I } const exponentiallySpacedBlocks = await this.collection!.find( - { height : { $in : heightOfBlocksToReturn } }, + { height: { $in: heightOfBlocksToReturn } }, MongoDbBlockMetadataStore.optionToExcludeIdField ).toArray(); exponentiallySpacedBlocks.sort((a, b) => b.height - a.height); // Sort in height descending order. diff --git a/lib/bitcoin/ProtocolParameters.ts b/lib/bitcoin/ProtocolParameters.ts index acb226206..5331f0c17 100644 --- a/lib/bitcoin/ProtocolParameters.ts +++ b/lib/bitcoin/ProtocolParameters.ts @@ -3,6 +3,6 @@ import ProtocolParameters from './models/ProtocolParameters'; /** * Defines the list of protocol parameters, intended ONLY to be used within each version of the protocol implementation. */ -let protocolParameters: ProtocolParameters = require('./protocol-parameters.json'); +const protocolParameters: ProtocolParameters = require('./protocol-parameters.json'); export default protocolParameters; diff --git a/lib/bitcoin/SidetreeTransactionParser.ts b/lib/bitcoin/SidetreeTransactionParser.ts index 03e9b1ed0..2d2b6aad4 100644 --- a/lib/bitcoin/SidetreeTransactionParser.ts +++ b/lib/bitcoin/SidetreeTransactionParser.ts @@ -132,11 +132,11 @@ export default class SidetreeTransactionParser { const scriptAsmParts = scriptAsm.split(' '); const isScriptValid = - scriptAsmParts.length === 5 - && scriptAsmParts[0] === 'OP_DUP' - && scriptAsmParts[1] === 'OP_HASH160' - && scriptAsmParts[3] === 'OP_EQUALVERIFY' - && scriptAsmParts[4] === 'OP_CHECKSIG'; + scriptAsmParts.length === 5 && + scriptAsmParts[0] === 'OP_DUP' && + scriptAsmParts[1] === 'OP_HASH160' && + scriptAsmParts[3] === 'OP_EQUALVERIFY' && + scriptAsmParts[4] === 'OP_CHECKSIG'; return isScriptValid ? scriptAsmParts[2] : undefined; } diff --git a/lib/bitcoin/SpendingMonitor.ts b/lib/bitcoin/SpendingMonitor.ts index 3d8f3fcc0..969be71d1 100644 --- a/lib/bitcoin/SpendingMonitor.ts +++ b/lib/bitcoin/SpendingMonitor.ts @@ -69,7 +69,7 @@ export default class SpendingMonitor { const allTxnsSinceStartingBlock = await this.transactionStore.getTransactionsLaterThan(startingBlockFirstTxnNumber - 1, undefined); - // tslint:disable-next-line: max-line-length + // eslint-disable-next-line max-len console.info(`SpendingMonitor: total number of transactions from the transaction store starting from block: ${startingBlockHeight} are: ${allTxnsSinceStartingBlock.length}`); // Since the transactions from the store include transactions written by ALL the nodes in the network, @@ -84,7 +84,7 @@ export default class SpendingMonitor { const totalFeePlusCurrentFee = totalFeeForRelatedTxns + currentFeeInSatoshis; if (totalFeePlusCurrentFee > this.bitcoinFeeSpendingCutoffInSatoshis) { - // tslint:disable-next-line: max-line-length + // eslint-disable-next-line max-len console.error(`Current fee (in satoshis): ${currentFeeInSatoshis} + total fees (${totalFeeForRelatedTxns}) since block number: ${startingBlockHeight} is greater than the spending cap: ${this.bitcoinFeeSpendingCutoffInSatoshis}`); return false; } diff --git a/lib/bitcoin/TransactionNumber.ts b/lib/bitcoin/TransactionNumber.ts index 010cb2ff7..396cb230f 100644 --- a/lib/bitcoin/TransactionNumber.ts +++ b/lib/bitcoin/TransactionNumber.ts @@ -11,8 +11,8 @@ export default class TransactionNumber { */ public static construct (blockNumber: number, position: number) { const transactionNumber = - blockNumber * (2 ** this.bitWidth) - + position; + blockNumber * (2 ** this.bitWidth) + + position; return transactionNumber; } diff --git a/lib/bitcoin/lock/LockIdentifierSerializer.ts b/lib/bitcoin/lock/LockIdentifierSerializer.ts index e1a6b9531..7313ea464 100644 --- a/lib/bitcoin/lock/LockIdentifierSerializer.ts +++ b/lib/bitcoin/lock/LockIdentifierSerializer.ts @@ -1,7 +1,7 @@ -import base64url from 'base64url'; import ErrorCode from '../ErrorCode'; import LockIdentifierModel from '../models/LockIdentifierModel'; import SidetreeError from '../../common/SidetreeError'; +import base64url from 'base64url'; /** * Encapsulates functionality to serialize and deserialize a lock identifier. diff --git a/lib/bitcoin/lock/LockMonitor.ts b/lib/bitcoin/lock/LockMonitor.ts index 5ec601363..cd64e32ac 100644 --- a/lib/bitcoin/lock/LockMonitor.ts +++ b/lib/bitcoin/lock/LockMonitor.ts @@ -4,6 +4,7 @@ import ErrorCode from '../ErrorCode'; import LockIdentifier from '../models/LockIdentifierModel'; import LockIdentifierSerializer from './LockIdentifierSerializer'; import LockResolver from './LockResolver'; +import LogColor from '../../common/LogColor'; import MongoDbLockTransactionStore from './MongoDbLockTransactionStore'; import SavedLockModel from '../models/SavedLockedModel'; import SavedLockType from '../enums/SavedLockType'; @@ -31,17 +32,19 @@ interface LockState { * Encapsulates functionality to monitor and create/remove amount locks on bitcoin. */ export default class LockMonitor { - private initialized: boolean; - private periodicPollTimeoutId: NodeJS.Timeout | undefined; - private currentLockState: LockState; - + /** + * Constructor for LockMonitor. + * @param valueTimeLockUpdateEnabled When this parameter is set to `false`, parameters `lockPeriodInBlocks`, + * `transactionFeesAmountInSatoshis` and `desiredLockAmountInSatoshis` will be ignored. + */ constructor ( private bitcoinClient: BitcoinClient, private lockTransactionStore: MongoDbLockTransactionStore, private lockResolver: LockResolver, private pollPeriodInSeconds: number, + private valueTimeLockUpdateEnabled: boolean, private desiredLockAmountInSatoshis: number, private transactionFeesAmountInSatoshis: number, private lockPeriodInBlocks: number) { @@ -53,34 +56,21 @@ export default class LockMonitor { if (!Number.isInteger(transactionFeesAmountInSatoshis)) { throw new SidetreeError(ErrorCode.LockMonitorTransactionFeesAmountIsNotWholeNumber, `${transactionFeesAmountInSatoshis}`); } - - this.currentLockState = { - activeValueTimeLock: undefined, - latestSavedLockInfo: undefined, - status: LockStatus.None - }; - - this.initialized = false; } /** - * Initializes this object by performing the periodic poll tasks. + * Starts the periodic reading and updating of lock status. */ - public async initialize (): Promise { - this.currentLockState = await this.getCurrentLockState(); - + public async startPeriodicProcessing (): Promise { await this.periodicPoll(); - this.initialized = true; } /** * Gets the current lock information if exist; undefined otherwise. Throws an error * if the lock information is not confirmed on the blockchain. */ - public getCurrentValueTimeLock (): ValueTimeLockModel | undefined { - - // Make a copy of the state so in case it gets changed between now and the function return - const currentLockState = Object.assign({}, this.currentLockState); + public async getCurrentValueTimeLock (): Promise { + const currentLockState = await this.getCurrentLockState(); // If there's no lock then return undefined if (currentLockState.status === LockStatus.None) { @@ -109,12 +99,6 @@ export default class LockMonitor { } catch (e) { const message = `An error occurred during periodic poll: ${SidetreeError.stringify(e)}`; console.error(message); - - // Rethrow if the error is in the initialization phase. We don't want to continue with the - // service during initialization. - if (!this.initialized) { - throw e; - } } finally { this.periodicPollTimeoutId = setTimeout(this.periodicPoll.bind(this), 1000 * this.pollPeriodInSeconds); } @@ -123,45 +107,46 @@ export default class LockMonitor { } private async handlePeriodicPolling (): Promise { + const currentLockState = await this.getCurrentLockState(); + console.info(`Refreshed the in-memory value time lock state.`); - // If the current lock is in pending state then we cannot do anything and need to just return. - if (this.currentLockState.status === LockStatus.Pending) { - console.info(`The current lock status is in pending state; going to skip rest of the routine.`); + // If lock update is disabled, then no further action needs to be taken. + if (this.valueTimeLockUpdateEnabled === false) { + console.info(`Value time lock update is disabled, will not attempt to update the value time lock.`); + return; + } - // But refresh the lock state before returning so that the next polling has the new value. - this.currentLockState = await this.getCurrentLockState(); + // If the current lock is in pending state then we cannot do anything other than rebroadcast the transaction again. + if (currentLockState.status === LockStatus.Pending) { + console.info(`The current lock status is in pending state, rebroadcast the transaction again in case the transaction is lost in the previous broadcast.`); + await this.rebroadcastTransaction(currentLockState.latestSavedLockInfo!); return; } // Now that we are not pending, check what do we have to do about the lock next. - const validCurrentLockExist = this.currentLockState.status === LockStatus.Confirmed; + const validCurrentLockExist = currentLockState.status === LockStatus.Confirmed; const lockRequired = this.desiredLockAmountInSatoshis > 0; - let currentLockUpdated = false; - if (lockRequired && !validCurrentLockExist) { await this.handleCreatingNewLock(this.desiredLockAmountInSatoshis); - currentLockUpdated = true; } if (lockRequired && validCurrentLockExist) { // The routine will true only if there were any changes made to the lock - currentLockUpdated = - await this.handleExistingLockRenewal( - this.currentLockState.activeValueTimeLock!, - this.currentLockState.latestSavedLockInfo!, - this.desiredLockAmountInSatoshis); + await this.handleExistingLockRenewal( + currentLockState.activeValueTimeLock!, + currentLockState.latestSavedLockInfo!, + this.desiredLockAmountInSatoshis + ); } if (!lockRequired && validCurrentLockExist) { - currentLockUpdated = - await this.handleReleaseExistingLock( - this.currentLockState.activeValueTimeLock!, - this.desiredLockAmountInSatoshis); - } + console.info(LogColor.lightBlue(`Value time lock no longer needed.`)); - if (currentLockUpdated) { - this.currentLockState = await this.getCurrentLockState(); + await this.handleReleaseExistingLock( + currentLockState.activeValueTimeLock!, + this.desiredLockAmountInSatoshis + ); } } @@ -184,9 +169,6 @@ export default class LockMonitor { // if it is not as we don't want to do anything until last lock information is at least // broadcasted. if (!(await this.isTransactionBroadcasted(lastSavedLock.transactionId))) { - - await this.rebroadcastTransaction(lastSavedLock); - return { activeValueTimeLock: undefined, latestSavedLockInfo: lastSavedLock, @@ -284,7 +266,8 @@ export default class LockMonitor { `Lock amount: ${totalLockAmount}; Wallet balance: ${walletBalance}`); } - console.info(`Going to create a new lock for amount: ${totalLockAmount} satoshis. Current wallet balance: ${walletBalance}`); + console.info(LogColor.lightBlue(`Current wallet balance: ${LogColor.green(walletBalance)}`)); + console.info(LogColor.lightBlue(`Creating a new lock for amount: ${LogColor.green(totalLockAmount)} satoshis.`)); const lockTransaction = await this.bitcoinClient.createLockTransaction(totalLockAmount, this.lockPeriodInBlocks); @@ -306,15 +289,15 @@ export default class LockMonitor { desiredLockAmountInSatoshis: number): Promise { // Just return if we haven't reached the unlock block yet - if (! (await this.isUnlockTimeReached(currentValueTimeLock.unlockTransactionTime))) { + if (!(await this.isUnlockTimeReached(currentValueTimeLock.unlockTransactionTime))) { return false; } - // If the desired lock amount is different from prevoius then just return the amount to + // If the desired lock amount is different from previous then just return the amount to // the wallet and let the next poll iteration start a new lock. if (latestSavedLockInfo.desiredLockAmountInSatoshis !== desiredLockAmountInSatoshis) { - // tslint:disable-next-line: max-line-length - console.info(`Current desired lock amount ${desiredLockAmountInSatoshis} satoshis is different from the previous desired lock amount ${latestSavedLockInfo.desiredLockAmountInSatoshis} satoshis. Going to release the lock.`); + console.info(LogColor.lightBlue(`Current desired lock amount ${LogColor.green(desiredLockAmountInSatoshis)} satoshis is different from the previous `) + + LogColor.lightBlue(`desired lock amount ${LogColor.green(latestSavedLockInfo.desiredLockAmountInSatoshis)} satoshis. Going to release the lock.`)); await this.releaseLock(currentValueTimeLock, desiredLockAmountInSatoshis); return true; @@ -322,14 +305,13 @@ export default class LockMonitor { // If we have gotten to here then we need to try renew. try { - await this.renewLock(currentValueTimeLock, desiredLockAmountInSatoshis); } catch (e) { // If there is not enough balance for the relock then just release the lock. Let the next // iteration of the polling to try and create a new lock. if (e instanceof SidetreeError && e.code === ErrorCode.LockMonitorNotEnoughBalanceForRelock) { - console.warn(`There is not enough balance for relocking so going to release the lock. Error: ${e.message}`); + console.warn(LogColor.yellow(`There is not enough balance for relocking so going to release the lock. Error: ${e.message}`)); await this.releaseLock(currentValueTimeLock, desiredLockAmountInSatoshis); } else { // This is an unexpected error at this point ... rethrow as this is needed to be investigated. @@ -350,13 +332,17 @@ export default class LockMonitor { */ private async handleReleaseExistingLock (currentValueTimeLock: ValueTimeLockModel, desiredLockAmountInSatoshis: number): Promise { - // Don't continue unless the current locktime model is actually reached - if (! (await this.isUnlockTimeReached(currentValueTimeLock.unlockTransactionTime))) { + // Don't continue unless the current lock time model is actually reached + if (!(await this.isUnlockTimeReached(currentValueTimeLock.unlockTransactionTime))) { return false; } + console.info(LogColor.lightBlue(`Value time lock no longer needed and unlock time reached, releasing lock...`)); + await this.releaseLock(currentValueTimeLock, desiredLockAmountInSatoshis); + console.info(LogColor.lightBlue(`Value time lock released.`)); + return true; } @@ -367,15 +353,15 @@ export default class LockMonitor { const relockTransaction = await this.bitcoinClient.createRelockTransaction( - currentLockIdentifier.transactionId, - currentLockDuration, - this.lockPeriodInBlocks); + currentLockIdentifier.transactionId, + currentLockDuration, + this.lockPeriodInBlocks); // If the transaction fee is making the relock amount less than the desired amount if (currentValueTimeLock.amountLocked - relockTransaction.transactionFee < desiredLockAmountInSatoshis) { throw new SidetreeError( ErrorCode.LockMonitorNotEnoughBalanceForRelock, - // tslint:disable-next-line: max-line-length + // eslint-disable-next-line max-len `The current locked amount (${currentValueTimeLock.amountLocked} satoshis) minus the relocking fee (${relockTransaction.transactionFee} satoshis) is causing the relock amount to go below the desired lock amount: ${desiredLockAmountInSatoshis}`); } diff --git a/lib/bitcoin/lock/LockResolver.ts b/lib/bitcoin/lock/LockResolver.ts index c1ac03c8f..cbbc705e3 100644 --- a/lib/bitcoin/lock/LockResolver.ts +++ b/lib/bitcoin/lock/LockResolver.ts @@ -1,13 +1,13 @@ import BitcoinClient from '../BitcoinClient'; -import BitcoinTransactionModel from '../models/BitcoinTransactionModel'; import BitcoinOutputModel from '../models/BitcoinOutputModel'; +import BitcoinTransactionModel from '../models/BitcoinTransactionModel'; import ErrorCode from '../ErrorCode'; import LockIdentifierModel from '../models/LockIdentifierModel'; import LockIdentifierSerializer from './LockIdentifierSerializer'; +import { Script } from 'bitcore-lib'; import SidetreeError from '../../common/SidetreeError'; import ValueTimeLockModel from '../../common/models/ValueTimeLockModel'; import VersionManager from '../VersionManager'; -import { Script } from 'bitcore-lib'; /** Structure (internal for this class) to hold the redeem script verification results */ interface LockScriptVerifyResult { @@ -88,9 +88,11 @@ export default class LockResolver { const unlockAtBlock = lockStartBlock + scriptVerifyResult.lockDurationInBlocks!; if (!this.isLockDurationValid(lockStartBlock, unlockAtBlock)) { - throw new SidetreeError(ErrorCode.LockResolverDurationIsInvalid, - // tslint:disable-next-line: max-line-length - `Lock start block: ${lockStartBlock}. Unlock block: ${unlockAtBlock}. Allowed range: [${this.minimumLockDurationInBlocks} - ${this.maximumLockDurationInBlocks}.]`); + throw new SidetreeError( + ErrorCode.LockResolverDurationIsInvalid, + // eslint-disable-next-line max-len + `Lock start block: ${lockStartBlock}. Unlock block: ${unlockAtBlock}. Allowed range: [${this.minimumLockDurationInBlocks} - ${this.maximumLockDurationInBlocks}.]` + ); } const normalizedFee = this.versionManager.getFeeCalculator(lockStartBlock).getNormalizedFee(lockStartBlock); diff --git a/lib/bitcoin/lock/MongoDbLockTransactionStore.ts b/lib/bitcoin/lock/MongoDbLockTransactionStore.ts index d97c79a98..8274114bb 100644 --- a/lib/bitcoin/lock/MongoDbLockTransactionStore.ts +++ b/lib/bitcoin/lock/MongoDbLockTransactionStore.ts @@ -1,5 +1,5 @@ -import SavedLockModel from './../models/SavedLockedModel'; import { Collection, Db, Long, MongoClient } from 'mongodb'; +import SavedLockModel from './../models/SavedLockedModel'; /** * Encapsulates functionality to store the bitcoin lock information to Db. @@ -39,7 +39,7 @@ export default class MongoDbLockTransactionStore { */ public async addLock (bitcoinLock: SavedLockModel): Promise { const lockInMongoDb = { - desiredLockAmountInSatoshis : bitcoinLock.desiredLockAmountInSatoshis, + desiredLockAmountInSatoshis: bitcoinLock.desiredLockAmountInSatoshis, transactionId: bitcoinLock.transactionId, rawTransaction: bitcoinLock.rawTransaction, redeemScriptAsHex: bitcoinLock.redeemScriptAsHex, @@ -64,10 +64,10 @@ export default class MongoDbLockTransactionStore { */ public async getLastLock (): Promise { const lastLocks = await this.lockCollection! - .find() - .limit(1) - .sort({ createTimestamp: -1 }) - .toArray(); + .find() + .limit(1) + .sort({ createTimestamp: -1 }) + .toArray(); if (!lastLocks || lastLocks.length <= 0) { return undefined; diff --git a/lib/common/MongoDbTransactionStore.ts b/lib/common/MongoDbTransactionStore.ts index 0d76cdaec..de955ccf4 100644 --- a/lib/common/MongoDbTransactionStore.ts +++ b/lib/common/MongoDbTransactionStore.ts @@ -1,6 +1,6 @@ +import { Collection, Cursor, Db, Long, MongoClient } from 'mongodb'; import ITransactionStore from '../core/interfaces/ITransactionStore'; import TransactionModel from './models/TransactionModel'; -import { Collection, Cursor, Db, Long, MongoClient } from 'mongodb'; /** * Implementation of ITransactionStore that stores the transaction data in a MongoDB database. @@ -153,7 +153,7 @@ export default class MongoDbTransactionStore implements ITransactionStore { * @param transactionTimeHash the transaction time hash which the transactions should be removed for */ public async removeTransactionByTransactionTimeHash (transactionTimeHash: string) { - await this.transactionCollection!.deleteMany({ transactionTimeHash: { $eq: transactionTimeHash } }); + await this.transactionCollection!.deleteMany({ transactionTimeHash: { $eq: transactionTimeHash } }); } /** @@ -176,10 +176,12 @@ export default class MongoDbTransactionStore implements ITransactionStore { // if begin === end, query for 1 transaction time cursor = this.transactionCollection!.find({ transactionTime: { $eq: Long.fromNumber(inclusiveBeginTransactionTime) } }); } else { - cursor = this.transactionCollection!.find({ $and: [ - { transactionTime: { $gte: Long.fromNumber(inclusiveBeginTransactionTime) } }, - { transactionTime: { $lt: Long.fromNumber(exclusiveEndTransactionTime) } } - ] }); + cursor = this.transactionCollection!.find({ + $and: [ + { transactionTime: { $gte: Long.fromNumber(inclusiveBeginTransactionTime) } }, + { transactionTime: { $lt: Long.fromNumber(exclusiveEndTransactionTime) } } + ] + }); } const transactions: TransactionModel[] = await cursor.sort({ transactionNumber: 1 }).toArray(); diff --git a/lib/common/ReadableStream.ts b/lib/common/ReadableStream.ts index 3bd8819ec..2b3030997 100644 --- a/lib/common/ReadableStream.ts +++ b/lib/common/ReadableStream.ts @@ -1,5 +1,5 @@ -import SidetreeError from './SidetreeError'; import ErrorCode from '../common/SharedErrorCode'; +import SidetreeError from './SidetreeError'; /** * ReadableStream utilities diff --git a/lib/core/BatchScheduler.ts b/lib/core/BatchScheduler.ts index e6072af5c..6eafbd75c 100644 --- a/lib/core/BatchScheduler.ts +++ b/lib/core/BatchScheduler.ts @@ -1,6 +1,6 @@ +import * as timeSpan from 'time-span'; import IBlockchain from './interfaces/IBlockchain'; import IVersionManager from './interfaces/IVersionManager'; -import timeSpan = require('time-span'); /** * Class that performs periodic writing of batches of Sidetree operations to CAS and blockchain. diff --git a/lib/core/Blockchain.ts b/lib/core/Blockchain.ts index f59602e6c..6e7f7997d 100644 --- a/lib/core/Blockchain.ts +++ b/lib/core/Blockchain.ts @@ -3,7 +3,6 @@ import BlockchainTimeModel from './models/BlockchainTimeModel'; import CoreErrorCode from './ErrorCode'; import IBlockchain from './interfaces/IBlockchain'; import JsonAsync from './versions/latest/util/JsonAsync'; -import nodeFetch from 'node-fetch'; import ReadableStream from '../common/ReadableStream'; import ServiceVersionFetcher from './ServiceVersionFetcher'; import ServiceVersionModel from '../common/models/ServiceVersionModel'; @@ -11,6 +10,7 @@ import SharedErrorCode from '../common/SharedErrorCode'; import SidetreeError from '../common/SidetreeError'; import TransactionModel from '../common/models/TransactionModel'; import ValueTimeLockModel from '../common/models/ValueTimeLockModel'; +import nodeFetch from 'node-fetch'; /** * Class that communicates with the underlying blockchain using REST API defined by the protocol document. diff --git a/lib/core/Core.ts b/lib/core/Core.ts index 3cb21e898..8923e6df0 100644 --- a/lib/core/Core.ts +++ b/lib/core/Core.ts @@ -3,6 +3,7 @@ import Blockchain from './Blockchain'; import Config from './models/Config'; import DownloadManager from './DownloadManager'; import ICas from './interfaces/ICas'; +import LogColor from '../common/LogColor'; import MongoDbOperationStore from './MongoDbOperationStore'; import MongoDbTransactionStore from '../common/MongoDbTransactionStore'; import MongoDbUnresolvableTransactionStore from './MongoDbUnresolvableTransactionStore'; @@ -32,16 +33,18 @@ export default class Core { /** * Core constructor. */ - public constructor (config: Config, versionModels: VersionModel[], private cas: ICas) { + public constructor (private config: Config, versionModels: VersionModel[], private cas: ICas) { // Component dependency construction & injection. - this.versionManager = new VersionManager(config, versionModels); // `VersionManager` is first constructed component. + this.versionManager = new VersionManager(config, versionModels); // `VersionManager` is first constructed component as multiple components depend on it. + this.serviceInfo = new ServiceInfo('core'); this.operationStore = new MongoDbOperationStore(config.mongoDbConnectionString, config.databaseName); this.blockchain = new Blockchain(config.blockchainServiceUri); this.downloadManager = new DownloadManager(config.maxConcurrentDownloads, this.cas); this.resolver = new Resolver(this.versionManager, this.operationStore); - this.batchScheduler = new BatchScheduler(this.versionManager, this.blockchain, config.batchingIntervalInSeconds); this.transactionStore = new MongoDbTransactionStore(config.mongoDbConnectionString, config.databaseName); this.unresolvableTransactionStore = new MongoDbUnresolvableTransactionStore(config.mongoDbConnectionString, config.databaseName); + + this.batchScheduler = new BatchScheduler(this.versionManager, this.blockchain, config.batchingIntervalInSeconds); this.observer = new Observer( this.versionManager, this.blockchain, @@ -51,8 +54,6 @@ export default class Core { this.unresolvableTransactionStore, config.observingIntervalInSeconds ); - - this.serviceInfo = new ServiceInfo('core'); } /** @@ -71,11 +72,20 @@ export default class Core { this.operationStore, this.resolver, this.transactionStore - ); // `VersionManager` is last initialized component. + ); // `VersionManager` is last initialized component as it needs many shared/common components to be ready first. + + if (this.config.observingIntervalInSeconds > 0) { + await this.observer.startPeriodicProcessing(); + } else { + console.warn(LogColor.yellow(`Transaction observer is disabled.`)); + } - await this.observer.startPeriodicProcessing(); + if (this.config.batchingIntervalInSeconds > 0) { + this.batchScheduler.startPeriodicBatchWriting(); + } else { + console.warn(LogColor.yellow(`Batch writing is disabled.`)); + } - this.batchScheduler.startPeriodicBatchWriting(); this.blockchain.startPeriodicCachedBlockchainTimeRefresh(); this.downloadManager.start(); } @@ -114,8 +124,8 @@ export default class Core { ]; return { - status : ResponseStatus.Succeeded, - body : JSON.stringify(responses) + status: ResponseStatus.Succeeded, + body: JSON.stringify(responses) }; } } diff --git a/lib/core/MongoDbOperationStore.ts b/lib/core/MongoDbOperationStore.ts index dfb866dd3..fda4f1929 100644 --- a/lib/core/MongoDbOperationStore.ts +++ b/lib/core/MongoDbOperationStore.ts @@ -1,7 +1,7 @@ +import { Binary, Collection, Long, MongoClient } from 'mongodb'; import AnchoredOperationModel from './models/AnchoredOperationModel'; import IOperationStore from './interfaces/IOperationStore'; import OperationType from './enums/OperationType'; -import { Binary, Collection, Long, MongoClient } from 'mongodb'; /** * Sidetree operation stored in MongoDb. @@ -68,7 +68,7 @@ export default class MongoDbOperationStore implements IOperationStore { * Implement OperationStore.put */ public async put (operations: AnchoredOperationModel[]): Promise { - let batch = this.collection!.initializeUnorderedBulkOp(); + const batch = this.collection!.initializeUnorderedBulkOp(); for (const operation of operations) { const mongoOperation = MongoDbOperationStore.convertToMongoOperation(operation); @@ -106,19 +106,21 @@ export default class MongoDbOperationStore implements IOperationStore { } public async deleteUpdatesEarlierThan (didUniqueSuffix: string, transactionNumber: number, operationIndex: number): Promise { - await this.collection!.deleteMany({ $or: [ - { - didSuffix: didUniqueSuffix, - txnNumber: { $lt: Long.fromNumber(transactionNumber) }, - type: OperationType.Update - }, - { - didSuffix: didUniqueSuffix, - txnNumber: Long.fromNumber(transactionNumber), - opIndex: { $lt: operationIndex }, - type: OperationType.Update - } - ]}); + await this.collection!.deleteMany({ + $or: [ + { + didSuffix: didUniqueSuffix, + txnNumber: { $lt: Long.fromNumber(transactionNumber) }, + type: OperationType.Update + }, + { + didSuffix: didUniqueSuffix, + txnNumber: Long.fromNumber(transactionNumber), + opIndex: { $lt: operationIndex }, + type: OperationType.Update + } + ] + }); } /** diff --git a/lib/core/MongoDbUnresolvableTransactionStore.ts b/lib/core/MongoDbUnresolvableTransactionStore.ts index 44c446669..87fbc5d5f 100644 --- a/lib/core/MongoDbUnresolvableTransactionStore.ts +++ b/lib/core/MongoDbUnresolvableTransactionStore.ts @@ -1,6 +1,6 @@ +import { Collection, Db, Long, MongoClient } from 'mongodb'; import IUnresolvableTransactionStore from './interfaces/IUnresolvableTransactionStore'; import TransactionModel from '../common/models/TransactionModel'; -import { Collection, Db, Long, MongoClient } from 'mongodb'; interface IUnresolvableTransaction extends TransactionModel { firstFetchTime: number; @@ -111,8 +111,8 @@ export default class MongoDbUnresolvableTransactionStore implements IUnresolvabl } const now = Date.now(); - const unresolvableTransactionsToRetry - = await this.unresolvableTransactionCollection!.find({ nextRetryTime: { $lte: now } }).sort({ nextRetryTime: 1 }).limit(returnCount).toArray(); + const unresolvableTransactionsToRetry = + await this.unresolvableTransactionCollection!.find({ nextRetryTime: { $lte: now } }).sort({ nextRetryTime: 1 }).limit(returnCount).toArray(); return unresolvableTransactionsToRetry; } diff --git a/lib/core/Observer.ts b/lib/core/Observer.ts index 2a6a4ef03..26240cc1c 100644 --- a/lib/core/Observer.ts +++ b/lib/core/Observer.ts @@ -1,3 +1,5 @@ +import * as timeSpan from 'time-span'; +import TransactionUnderProcessingModel, { TransactionProcessingStatus } from './models/TransactionUnderProcessingModel'; import IBlockchain from './interfaces/IBlockchain'; import IOperationStore from './interfaces/IOperationStore'; import ITransactionProcessor from './interfaces/ITransactionProcessor'; @@ -6,10 +8,8 @@ import IUnresolvableTransactionStore from './interfaces/IUnresolvableTransaction import IVersionManager from './interfaces/IVersionManager'; import SharedErrorCode from '../common/SharedErrorCode'; import SidetreeError from '../common/SidetreeError'; -import timeSpan = require('time-span'); import ThroughputLimiter from './ThroughputLimiter'; import TransactionModel from '../common/models/TransactionModel'; -import TransactionUnderProcessingModel, { TransactionProcessingStatus } from './models/TransactionUnderProcessingModel'; /** * Class that performs periodic processing of batches of Sidetree operations anchored to the blockchain. @@ -180,8 +180,6 @@ export default class Observer { // Wait a little before checking again. await new Promise(resolve => setTimeout(resolve, 1000)); } - - return; } /** @@ -276,8 +274,8 @@ export default class Observer { const exponentiallySpacedTransactions = await this.transactionStore.getExponentiallySpacedTransactions(); // Find a known valid Sidetree transaction that is prior to the block reorganization. - const bestKnownValidRecentTransaction - = await this.blockchain.getFirstValidTransaction(exponentiallySpacedTransactions); + const bestKnownValidRecentTransaction = + await this.blockchain.getFirstValidTransaction(exponentiallySpacedTransactions); const bestKnownValidRecentTransactionNumber = bestKnownValidRecentTransaction === undefined ? undefined : bestKnownValidRecentTransaction.transactionNumber; console.info(`Best known valid recent transaction: ${bestKnownValidRecentTransactionNumber}`); diff --git a/lib/core/Resolver.ts b/lib/core/Resolver.ts index 295ff1e09..58a469909 100644 --- a/lib/core/Resolver.ts +++ b/lib/core/Resolver.ts @@ -166,7 +166,7 @@ export default class Resolver { private async applyOperation ( operation: AnchoredOperationModel, didState: DidState | undefined - ): Promise { + ): Promise { let appliedDidState = didState; // NOTE: MUST NOT throw error, else a bad operation can be used to denial resolution for a DID. diff --git a/lib/core/ServiceVersionFetcher.ts b/lib/core/ServiceVersionFetcher.ts index 6d0d12317..301267947 100644 --- a/lib/core/ServiceVersionFetcher.ts +++ b/lib/core/ServiceVersionFetcher.ts @@ -1,6 +1,6 @@ -import nodeFetch from 'node-fetch'; import ReadableStream from '../common/ReadableStream'; import ServiceVersionModel from '../common/models/ServiceVersionModel'; +import nodeFetch from 'node-fetch'; /** * Encapsulates the functionality of getting the version information from the dependent services. diff --git a/lib/core/ThroughputLimiter.ts b/lib/core/ThroughputLimiter.ts index aafad324c..6fc5fae75 100644 --- a/lib/core/ThroughputLimiter.ts +++ b/lib/core/ThroughputLimiter.ts @@ -15,7 +15,7 @@ export default class ThroughputLimiter { * @param transactions array of transactions to filter for */ public async getQualifiedTransactions (transactions: TransactionModel[]) { - let currentTransactionTime: number | undefined = undefined; + let currentTransactionTime: number | undefined; const transactionsGroupedByTransactionTime: TransactionModel[][] = []; for (const transaction of transactions) { diff --git a/lib/core/models/Config.ts b/lib/core/models/Config.ts index 122641c44..497f6bcc1 100644 --- a/lib/core/models/Config.ts +++ b/lib/core/models/Config.ts @@ -4,9 +4,9 @@ export default interface Config { batchingIntervalInSeconds: number; blockchainServiceUri: string; + databaseName: string | undefined; didMethodName: string; maxConcurrentDownloads: number; - observingIntervalInSeconds: number; mongoDbConnectionString: string; - databaseName: string | undefined; + observingIntervalInSeconds: number; } diff --git a/lib/core/versions/0.11.0/AnchorFile.ts b/lib/core/versions/0.11.0/AnchorFile.ts index 69b204dd7..3039ffccf 100644 --- a/lib/core/versions/0.11.0/AnchorFile.ts +++ b/lib/core/versions/0.11.0/AnchorFile.ts @@ -50,7 +50,7 @@ export default class AnchorFile { } const allowedProperties = new Set(['map_file_uri', 'operations', 'writer_lock_id']); - for (let property in anchorFileModel) { + for (const property in anchorFileModel) { if (!allowedProperties.has(property)) { throw new SidetreeError(ErrorCode.AnchorFileHasUnknownProperty); } @@ -88,7 +88,7 @@ export default class AnchorFile { const allowedOperationsProperties = new Set(['create', 'recover', 'deactivate']); const operations = anchorFileModel.operations; - for (let property in operations) { + for (const property in operations) { if (!allowedOperationsProperties.has(property)) { throw new SidetreeError(ErrorCode.AnchorFileUnexpectedPropertyInOperations, `Unexpected property ${property} in 'operations' property in anchor file.`); } diff --git a/lib/core/versions/0.11.0/ChunkFile.ts b/lib/core/versions/0.11.0/ChunkFile.ts index 51c1ba207..de7a6b053 100644 --- a/lib/core/versions/0.11.0/ChunkFile.ts +++ b/lib/core/versions/0.11.0/ChunkFile.ts @@ -23,7 +23,7 @@ export default class ChunkFile { chunkFileBuffer: Buffer ): Promise { - let endTimer = timeSpan(); + const endTimer = timeSpan(); const maxAllowedDecompressedSizeInBytes = ProtocolParameters.maxChunkFileSizeInBytes * Compressor.estimatedDecompressionMultiplier; const decompressedChunkFileBuffer = await Compressor.decompress(chunkFileBuffer, maxAllowedDecompressedSizeInBytes); const chunkFileObject = await JsonAsync.parse(decompressedChunkFileBuffer); @@ -31,7 +31,7 @@ export default class ChunkFile { // Ensure only properties specified by Sidetree protocol are given. const allowedProperties = new Set(['deltas']); - for (let property in chunkFileObject) { + for (const property in chunkFileObject) { if (!allowedProperties.has(property)) { throw new SidetreeError(ErrorCode.ChunkFileUnexpectedProperty, `Unexpected property ${property} in chunk file.`); } diff --git a/lib/core/versions/0.11.0/DocumentComposer.ts b/lib/core/versions/0.11.0/DocumentComposer.ts index 370418fa3..b17c258f5 100644 --- a/lib/core/versions/0.11.0/DocumentComposer.ts +++ b/lib/core/versions/0.11.0/DocumentComposer.ts @@ -27,7 +27,7 @@ export default class DocumentComposer { const authentication: any[] = []; const publicKeys: any[] = []; if (Array.isArray(document.public_keys)) { - for (let publicKey of document.public_keys) { + for (const publicKey of document.public_keys) { const id = '#' + publicKey.id; const didDocumentPublicKey = { id: id, @@ -54,7 +54,7 @@ export default class DocumentComposer { let serviceEndpoints; if (Array.isArray(document.service_endpoints)) { serviceEndpoints = []; - for (let serviceEndpoint of document.service_endpoints) { + for (const serviceEndpoint of document.service_endpoints) { const didDocumentServiceEndpoint = { id: '#' + serviceEndpoint.id, type: serviceEndpoint.type, @@ -113,7 +113,7 @@ export default class DocumentComposer { } const allowedProperties = new Set(['public_keys', 'service_endpoints']); - for (let property in document) { + for (const property in document) { if (!allowedProperties.has(property)) { throw new SidetreeError(ErrorCode.DocumentComposerUnknownPropertyInDocument, `Unexpected property ${property} in document.`); } @@ -140,7 +140,7 @@ export default class DocumentComposer { throw new SidetreeError(ErrorCode.DocumentComposerUpdateOperationDocumentPatchesNotArray); } - for (let patch of patches) { + for (const patch of patches) { DocumentComposer.validatePatch(patch); } } @@ -183,7 +183,7 @@ export default class DocumentComposer { } const publicKeyIdSet: Set = new Set(); - for (let publicKey of publicKeys) { + for (const publicKey of publicKeys) { const publicKeyProperties = Object.keys(publicKey); // the expected fields are id, purpose, type and jwk if (publicKeyProperties.length !== 4) { @@ -234,7 +234,7 @@ export default class DocumentComposer { throw new SidetreeError(ErrorCode.DocumentComposerPatchPublicKeyIdsNotArray); } - for (let publicKeyId of patch.public_keys) { + for (const publicKeyId of patch.public_keys) { if (typeof publicKeyId !== 'string') { throw new SidetreeError(ErrorCode.DocumentComposerPatchPublicKeyIdNotString); } @@ -284,7 +284,7 @@ export default class DocumentComposer { throw new SidetreeError(ErrorCode.DocumentComposerPatchServiceEndpointsNotArray); } - for (let serviceEndpoint of serviceEndpoints) { + for (const serviceEndpoint of serviceEndpoints) { const serviceEndpointProperties = Object.keys(serviceEndpoint); if (serviceEndpointProperties.length !== 3) { // type, id, and endpoint throw new SidetreeError(ErrorCode.DocumentComposerServiceEndpointMissingOrUnknownProperty); @@ -336,7 +336,7 @@ export default class DocumentComposer { public static applyPatches (document: any, patches: any[]): any { // Loop through and apply all patches. let resultantDocument = document; - for (let patch of patches) { + for (const patch of patches) { resultantDocument = DocumentComposer.applyPatchToDidDocument(resultantDocument, patch); } @@ -367,7 +367,7 @@ export default class DocumentComposer { const publicKeyMap = new Map((document.public_keys || []).map(publicKey => [publicKey.id, publicKey])); // Loop through all given public keys and add them if they don't exist already. - for (let publicKey of patch.public_keys) { + for (const publicKey of patch.public_keys) { // NOTE: If a key ID already exists, we will just replace the existing key. // Not throwing error will minimize the need (thus risk) of reusing exposed update reveal value. publicKeyMap.set(publicKey.id, publicKey); @@ -385,7 +385,7 @@ export default class DocumentComposer { const publicKeyMap = new Map((document.public_keys || []).map(publicKey => [publicKey.id, publicKey])); // Loop through all given public key IDs and delete them from the existing public key only if it is not a recovery key. - for (let publicKey of patch.public_keys) { + for (const publicKey of patch.public_keys) { const existingKey = publicKeyMap.get(publicKey); if (existingKey !== undefined) { diff --git a/lib/core/versions/0.11.0/MapFile.ts b/lib/core/versions/0.11.0/MapFile.ts index aad8c6588..53da010db 100644 --- a/lib/core/versions/0.11.0/MapFile.ts +++ b/lib/core/versions/0.11.0/MapFile.ts @@ -44,7 +44,7 @@ export default class MapFile { } const allowedProperties = new Set(['chunks', 'operations']); - for (let property in mapFileModel) { + for (const property in mapFileModel) { if (!allowedProperties.has(property)) { throw new SidetreeError(ErrorCode.MapFileHasUnknownProperty); } diff --git a/lib/core/versions/0.11.0/MongoDbOperationQueue.ts b/lib/core/versions/0.11.0/MongoDbOperationQueue.ts index 607aa5eb5..2e94a84a1 100644 --- a/lib/core/versions/0.11.0/MongoDbOperationQueue.ts +++ b/lib/core/versions/0.11.0/MongoDbOperationQueue.ts @@ -1,7 +1,7 @@ import ErrorCode from './ErrorCode'; import IOperationQueue from './interfaces/IOperationQueue'; import SidetreeError from '../../../common/SidetreeError'; -import { Binary, Collection, MongoClient, Db } from 'mongodb'; +import { Binary, Collection, Db, MongoClient } from 'mongodb'; import QueuedOperationModel from './models/QueuedOperationModel'; /** diff --git a/lib/core/versions/0.11.0/ProtocolParameters.ts b/lib/core/versions/0.11.0/ProtocolParameters.ts index 93b91b185..4287dff98 100644 --- a/lib/core/versions/0.11.0/ProtocolParameters.ts +++ b/lib/core/versions/0.11.0/ProtocolParameters.ts @@ -3,6 +3,6 @@ /** * Defines the list of protocol parameters, intended ONLY to be used within each version of Sidetree. */ -let protocolParameters: ProtocolParameters = require('./protocol-parameters.json'); +const protocolParameters: ProtocolParameters = require('./protocol-parameters.json'); export default protocolParameters; diff --git a/lib/core/versions/0.11.0/TransactionProcessor.ts b/lib/core/versions/0.11.0/TransactionProcessor.ts index 3ceacdfaa..f096f6573 100644 --- a/lib/core/versions/0.11.0/TransactionProcessor.ts +++ b/lib/core/versions/0.11.0/TransactionProcessor.ts @@ -101,8 +101,8 @@ export default class TransactionProcessor implements ITransactionProcessor { // Verify required lock if one was needed. const valueTimeLock = anchorFile.model.writer_lock_id - ? await this.blockchain.getValueTimeLock(anchorFile.model.writer_lock_id) - : undefined; + ? await this.blockchain.getValueTimeLock(anchorFile.model.writer_lock_id) + : undefined; ValueTimeLockVerifier.verifyLockAmountAndThrowOnError( valueTimeLock, paidOperationCount, @@ -211,10 +211,10 @@ export default class TransactionProcessor implements ITransactionProcessor { chunkFile: ChunkFileModel | undefined ): Promise { - let createOperations = anchorFile.createOperations; - let recoverOperations = anchorFile.recoverOperations; - let deactivateOperations = anchorFile.deactivateOperations; - let updateOperations = (mapFile && mapFile.updateOperations) ? mapFile.updateOperations : []; + const createOperations = anchorFile.createOperations; + const recoverOperations = anchorFile.recoverOperations; + const deactivateOperations = anchorFile.deactivateOperations; + const updateOperations = (mapFile && mapFile.updateOperations) ? mapFile.updateOperations : []; // Add the operations in the following order of types: create, recover, update, deactivate. const operations = []; diff --git a/lib/core/versions/0.11.0/TransactionSelector.ts b/lib/core/versions/0.11.0/TransactionSelector.ts index 82cb543c4..b536a33d8 100644 --- a/lib/core/versions/0.11.0/TransactionSelector.ts +++ b/lib/core/versions/0.11.0/TransactionSelector.ts @@ -47,8 +47,8 @@ export default class TransactionSelector implements ITransactionSelector { TransactionSelector.enqueueFirstTransactionFromEachWriter(transactions, currentTransactionTime, transactionsPriorityQueue); const [numberOfOperations, numberOfTransactions] = await this.getNumberOfOperationsAndTransactionsAlreadyInTransactionTime(currentTransactionTime); - let numberOfOperationsToQualify = this.maxNumberOfOperationsPerBlock - numberOfOperations; - let numberOfTransactionsToQualify = this.maxNumberOfTransactionsPerBlock - numberOfTransactions; + const numberOfOperationsToQualify = this.maxNumberOfOperationsPerBlock - numberOfOperations; + const numberOfTransactionsToQualify = this.maxNumberOfTransactionsPerBlock - numberOfTransactions; const transactionsToReturn = TransactionSelector.getHighestFeeTransactionsFromCurrentTransactionTime( numberOfOperationsToQualify, @@ -112,9 +112,9 @@ export default class TransactionSelector implements ITransactionSelector { let numberOfOperationsSeen = 0; const transactionsToReturn = []; - while (transactionsToReturn.length < numberOfTransactionsToQualify - && numberOfOperationsSeen < numberOfOperationsToQualify - && transactionsPriorityQueue.length > 0) { + while (transactionsToReturn.length < numberOfTransactionsToQualify && + numberOfOperationsSeen < numberOfOperationsToQualify && + transactionsPriorityQueue.length > 0) { const currentTransaction = transactionsPriorityQueue.pop(); try { const numOfOperationsInCurrentTransaction = AnchoredDataSerializer.deserialize(currentTransaction.anchorString).numberOfOperations; diff --git a/lib/core/versions/0.11.0/util/Jwk.ts b/lib/core/versions/0.11.0/util/Jwk.ts index 100ea6be2..0a852714e 100644 --- a/lib/core/versions/0.11.0/util/Jwk.ts +++ b/lib/core/versions/0.11.0/util/Jwk.ts @@ -38,7 +38,7 @@ export default class Jwk { } const allowedProperties = new Set(['kty', 'crv', 'x', 'y']); - for (let property in jwk) { + for (const property in jwk) { if (!allowedProperties.has(property)) { throw new SidetreeError(ErrorCode.JwkEs256kHasUnknownProperty); } diff --git a/lib/core/versions/0.11.0/util/Jws.ts b/lib/core/versions/0.11.0/util/Jws.ts index 5e21e85d0..76e1e7134 100644 --- a/lib/core/versions/0.11.0/util/Jws.ts +++ b/lib/core/versions/0.11.0/util/Jws.ts @@ -37,7 +37,7 @@ export default class Jws { const decodedProtectedHeadJsonString = Encoder.decodeBase64UrlAsString(protectedHeader); const decodedProtectedHeader = JSON.parse(decodedProtectedHeadJsonString); - let expectedHeaderPropertyCount = 1; // By default we must have header property is `alg`. + const expectedHeaderPropertyCount = 1; // By default we must have header property is `alg`. const headerProperties = Object.keys(decodedProtectedHeader); if (headerProperties.length !== expectedHeaderPropertyCount) { diff --git a/lib/core/versions/latest/AnchorFile.ts b/lib/core/versions/latest/AnchorFile.ts index 69b204dd7..a832566e1 100644 --- a/lib/core/versions/latest/AnchorFile.ts +++ b/lib/core/versions/latest/AnchorFile.ts @@ -50,7 +50,7 @@ export default class AnchorFile { } const allowedProperties = new Set(['map_file_uri', 'operations', 'writer_lock_id']); - for (let property in anchorFileModel) { + for (const property in anchorFileModel) { if (!allowedProperties.has(property)) { throw new SidetreeError(ErrorCode.AnchorFileHasUnknownProperty); } @@ -88,7 +88,7 @@ export default class AnchorFile { const allowedOperationsProperties = new Set(['create', 'recover', 'deactivate']); const operations = anchorFileModel.operations; - for (let property in operations) { + for (const property in operations) { if (!allowedOperationsProperties.has(property)) { throw new SidetreeError(ErrorCode.AnchorFileUnexpectedPropertyInOperations, `Unexpected property ${property} in 'operations' property in anchor file.`); } @@ -167,7 +167,11 @@ export default class AnchorFile { const createOperations = createOperationArray.map(operation => { return { - suffix_data: operation.encodedSuffixData + suffix_data: { + delta_hash: operation.suffixData.deltaHash, + recovery_commitment: operation.suffixData.recoveryCommitment, + type: operation.suffixData.type + } }; }); diff --git a/lib/core/versions/latest/BatchWriter.ts b/lib/core/versions/latest/BatchWriter.ts index 55f0593a3..6376882c0 100644 --- a/lib/core/versions/latest/BatchWriter.ts +++ b/lib/core/versions/latest/BatchWriter.ts @@ -1,13 +1,13 @@ +import AnchorFile from './AnchorFile'; import AnchoredData from './models/AnchoredData'; import AnchoredDataSerializer from './AnchoredDataSerializer'; -import AnchorFile from './AnchorFile'; import ChunkFile from './ChunkFile'; import CreateOperation from './CreateOperation'; import DeactivateOperation from './DeactivateOperation'; import FeeManager from './FeeManager'; -import ICas from '../../interfaces/ICas'; import IBatchWriter from '../../interfaces/IBatchWriter'; import IBlockchain from '../../interfaces/IBlockchain'; +import ICas from '../../interfaces/ICas'; import IOperationQueue from './interfaces/IOperationQueue'; import IVersionMetadataFetcher from '../../interfaces/IVersionMetadataFetcher'; import LogColor from '../../../common/LogColor'; @@ -93,7 +93,7 @@ export default class BatchWriter implements IBatchWriter { const maxNumberOfOpsAllowedByLock = ValueTimeLockVerifier.calculateMaxNumberOfOperationsAllowed(valueTimeLock, this.versionMetadataFetcher); if (maxNumberOfOpsAllowedByLock > maxNumberOfOpsAllowedByProtocol) { - // tslint:disable-next-line: max-line-length + // eslint-disable-next-line max-len console.info(`Maximum number of operations allowed by value time lock: ${maxNumberOfOpsAllowedByLock}; Maximum number of operations allowed by protocol: ${maxNumberOfOpsAllowedByProtocol}`); } diff --git a/lib/core/versions/latest/ChunkFile.ts b/lib/core/versions/latest/ChunkFile.ts index 51c1ba207..bfa1d8169 100644 --- a/lib/core/versions/latest/ChunkFile.ts +++ b/lib/core/versions/latest/ChunkFile.ts @@ -1,3 +1,4 @@ +import * as timeSpan from 'time-span'; import ChunkFileModel from './models/ChunkFileModel'; import Compressor from './util/Compressor'; import CreateOperation from './CreateOperation'; @@ -7,7 +8,6 @@ import JsonAsync from './util/JsonAsync'; import ProtocolParameters from './ProtocolParameters'; import RecoverOperation from './RecoverOperation'; import SidetreeError from '../../../common/SidetreeError'; -import timeSpan = require('time-span'); import UpdateOperation from './UpdateOperation'; /** @@ -23,7 +23,7 @@ export default class ChunkFile { chunkFileBuffer: Buffer ): Promise { - let endTimer = timeSpan(); + const endTimer = timeSpan(); const maxAllowedDecompressedSizeInBytes = ProtocolParameters.maxChunkFileSizeInBytes * Compressor.estimatedDecompressionMultiplier; const decompressedChunkFileBuffer = await Compressor.decompress(chunkFileBuffer, maxAllowedDecompressedSizeInBytes); const chunkFileObject = await JsonAsync.parse(decompressedChunkFileBuffer); @@ -31,7 +31,7 @@ export default class ChunkFile { // Ensure only properties specified by Sidetree protocol are given. const allowedProperties = new Set(['deltas']); - for (let property in chunkFileObject) { + for (const property in chunkFileObject) { if (!allowedProperties.has(property)) { throw new SidetreeError(ErrorCode.ChunkFileUnexpectedProperty, `Unexpected property ${property} in chunk file.`); } @@ -48,14 +48,14 @@ export default class ChunkFile { throw new SidetreeError(ErrorCode.ChunkFileDeltasPropertyNotArray, 'Invalid chunk file, deltas property is not an array.'); } - // Validate every encoded delta string. - for (const encodedDelta of deltas) { - if (typeof encodedDelta !== 'string') { - throw new SidetreeError(ErrorCode.ChunkFileDeltasNotArrayOfStrings, 'Invalid chunk file, deltas property is not an array of strings.'); + // Validate every delta is an object + for (const delta of deltas) { + if (typeof delta !== 'object') { + throw new SidetreeError(ErrorCode.ChunkFileDeltasNotArrayOfObjects, 'Invalid chunk file, deltas property is not an array of objects.'); } // Verify size of each delta does not exceed the maximum allowed limit. - Delta.validateEncodedDeltaSize(encodedDelta); + Delta.validateDeltaSize(delta); } } @@ -64,9 +64,9 @@ export default class ChunkFile { */ public static async createBuffer (createOperations: CreateOperation[], recoverOperations: RecoverOperation[], updateOperations: UpdateOperation[]) { const deltas = []; - deltas.push(...createOperations.map(operation => operation.encodedDelta!)); - deltas.push(...recoverOperations.map(operation => operation.encodedDelta!)); - deltas.push(...updateOperations.map(operation => operation.encodedDelta!)); + deltas.push(...createOperations.map(operation => operation.delta!)); + deltas.push(...recoverOperations.map(operation => operation.delta!)); + deltas.push(...updateOperations.map(operation => operation.delta!)); const chunkFileModel = { deltas diff --git a/lib/core/versions/latest/CreateOperation.ts b/lib/core/versions/latest/CreateOperation.ts index 6196f76e1..6807a0bb8 100644 --- a/lib/core/versions/latest/CreateOperation.ts +++ b/lib/core/versions/latest/CreateOperation.ts @@ -2,6 +2,7 @@ import DeltaModel from './models/DeltaModel'; import Encoder from './Encoder'; import ErrorCode from './ErrorCode'; import JsonAsync from './util/JsonAsync'; +import JsonCanonicalizer from './util/JsonCanonicalizer'; import Multihash from './Multihash'; import Operation from './Operation'; import OperationModel from './models/OperationModel'; @@ -69,13 +70,24 @@ export default class CreateOperation implements OperationModel { return encodedMultihash; } + /** + * Computes the DID unique suffix given the encoded suffix data object. + * @param suffixData the suffix data object to calculate unique suffix from + */ + private static computeJcsDidUniqueSuffix (suffixData: object): string { + const suffixDataBuffer = JsonCanonicalizer.canonicalizeAsBuffer(suffixData); + const multihash = Multihash.hash(suffixDataBuffer); + const encodedMultihash = Encoder.encode(multihash); + return encodedMultihash; + } + /** * Parses the given input as a create operation entry in the anchor file. */ public static async parseOperationFromAnchorFile (input: any): Promise { // Issue #442 - Replace `operationBuffer` in `OperationModel` and `AnchoredOperationModel` with actual operation request const operationBuffer = Buffer.from(JSON.stringify(input)); - const operation = await CreateOperation.parseObject(input, operationBuffer, true); + const operation = await CreateOperation.parseJcsObject(input, operationBuffer, true); return operation; } @@ -85,10 +97,76 @@ export default class CreateOperation implements OperationModel { public static async parse (operationBuffer: Buffer): Promise { const operationJsonString = operationBuffer.toString(); const operationObject = await JsonAsync.parse(operationJsonString); - const createOperation = await CreateOperation.parseObject(operationObject, operationBuffer, false); + let createOperation; + if (typeof operationObject.suffix_data === 'string') { + // TODO: SIP 2 #781 deprecates this. Should be deleted when fully switched over + createOperation = await CreateOperation.parseObject(operationObject, operationBuffer, false); + } else { + createOperation = CreateOperation.parseJcsObject(operationObject, operationBuffer, false); + } return createOperation; } + /** + * Parses the given operation object as a `CreateOperation`. + * The `operationBuffer` given is assumed to be valid and is assigned to the `operationBuffer` directly. + * NOTE: This method is purely intended to be used as an optimization method over the `parse` method in that + * JSON parsing is not required to be performed more than once when an operation buffer of an unknown operation type is given. + * @param operationObject The operationObject is a json object with no encoding + * @param operationBuffer The buffer format of the operationObject + * @param anchorFileMode If set to true, then `delta` and `type` properties are expected to be absent. + */ + public static parseJcsObject (operationObject: any, operationBuffer: Buffer, anchorFileMode: boolean): CreateOperation { + let expectedPropertyCount = 3; + if (anchorFileMode) { + expectedPropertyCount = 1; + } + const properties = Object.keys(operationObject); + if (properties.length !== expectedPropertyCount) { + throw new SidetreeError(ErrorCode.CreateOperationMissingOrUnknownProperty); + } + + CreateOperation.validateSuffixData(operationObject.suffix_data); + const suffixData: SuffixDataModel = { + deltaHash: operationObject.suffix_data.delta_hash, + recoveryCommitment: operationObject.suffix_data.recovery_commitment + }; + + if (operationObject.suffix_data.type !== undefined) { + suffixData.type = operationObject.suffix_data.type; + } + + // For compatibility with data pruning, we have to assume that `delta` may be unavailable, + // thus an operation with invalid `delta` needs to be processed as an operation with unavailable `delta`, + // so here we let `delta` be `undefined`. + let delta; + let encodedDelta; + if (!anchorFileMode) { + if (operationObject.type !== OperationType.Create) { + throw new SidetreeError(ErrorCode.CreateOperationTypeIncorrect); + } + + try { + Operation.validateDelta(operationObject.delta); + delta = { + patches: operationObject.delta.patches, + updateCommitment: operationObject.delta.update_commitment + }; + } catch { + // For compatibility with data pruning, we have to assume that `delta` may be unavailable, + // thus an operation with invalid `delta` needs to be processed as an operation with unavailable `delta`, + // so here we let `delta` be `undefined`. + } + // TODO: SIP 2 #781 remove encoded delta and encoded suffix data when old long form is fully deprecated. + encodedDelta = Encoder.encode(JsonCanonicalizer.canonicalizeAsBuffer(operationObject.delta)); + } + + const didUniqueSuffix = CreateOperation.computeJcsDidUniqueSuffix(operationObject.suffix_data); + + const encodedSuffixData = Encoder.encode(JsonCanonicalizer.canonicalizeAsBuffer(operationObject.suffix_data)); + return new CreateOperation(operationBuffer, didUniqueSuffix, encodedSuffixData, suffixData, encodedDelta, delta); + } + /** * Parses the given operation object as a `CreateOperation`. * The `operationBuffer` given is assumed to be valid and is assigned to the `operationBuffer` directly. @@ -97,6 +175,7 @@ export default class CreateOperation implements OperationModel { * @param anchorFileMode If set to true, then `delta` and `type` properties are expected to be absent. */ public static async parseObject (operationObject: any, operationBuffer: Buffer, anchorFileMode: boolean): Promise { + // TODO: SIP 2 #781 deprecates this. Should be deleted when fully switched over let expectedPropertyCount = 3; if (anchorFileMode) { expectedPropertyCount = 1; @@ -132,14 +211,11 @@ export default class CreateOperation implements OperationModel { return new CreateOperation(operationBuffer, didUniqueSuffix, encodedSuffixData, suffixData, encodedDelta, delta); } - private static async parseSuffixData (suffixDataEncodedString: any): Promise { - if (typeof suffixDataEncodedString !== 'string') { - throw new SidetreeError(ErrorCode.CreateOperationSuffixDataMissingOrNotString); + private static validateSuffixData (suffixData: any): void { + if (typeof suffixData !== 'object') { + throw new SidetreeError(ErrorCode.CreateOperationSuffixDataIsNotObject); } - const suffixDataJsonString = Encoder.decodeAsString(suffixDataEncodedString); - const suffixData = await JsonAsync.parse(suffixDataJsonString); - const properties = Object.keys(suffixData); // will have 3 if has type if (properties.length !== 3 && properties.length !== 2) { @@ -166,6 +242,16 @@ export default class CreateOperation implements OperationModel { throw new SidetreeError(ErrorCode.CreateOperationSuffixDataTypeInvalidCharacter); } } + } + + private static async parseSuffixData (suffixDataEncodedString: any): Promise { + if (typeof suffixDataEncodedString !== 'string') { + throw new SidetreeError(ErrorCode.CreateOperationSuffixDataMissingOrNotString); + } + + const suffixDataJsonString = Encoder.decodeAsString(suffixDataEncodedString); + const suffixData = await JsonAsync.parse(suffixDataJsonString); + CreateOperation.validateSuffixData(suffixData); return { deltaHash: suffixData.delta_hash, diff --git a/lib/core/versions/latest/Delta.ts b/lib/core/versions/latest/Delta.ts index 726ef0451..84115669e 100644 --- a/lib/core/versions/latest/Delta.ts +++ b/lib/core/versions/latest/Delta.ts @@ -1,4 +1,5 @@ import ErrorCode from './ErrorCode'; +import JsonCanonicalizer from './util/JsonCanonicalizer'; import ProtocolParameters from './ProtocolParameters'; import SidetreeError from '../../../common/SidetreeError'; @@ -9,6 +10,7 @@ export default class Delta { /** * Validates size of the encoded delta string. + * TODO: SIP 2 #781 delete this when long form is fully switched over * @throws `SidetreeError` if fails validation. */ public static validateEncodedDeltaSize (encodedDelta: string) { @@ -19,4 +21,16 @@ export default class Delta { throw new SidetreeError(ErrorCode.DeltaExceedsMaximumSize, errorMessage); } } + + /** + * Validates size of the delta object + */ + public static validateDeltaSize (delta: object) { + const size = Buffer.byteLength(JsonCanonicalizer.canonicalizeAsBuffer(delta)); + if (size > ProtocolParameters.maxDeltaSizeInBytes) { + const errorMessage = `${size} bytes of 'delta' exceeded limit of ${ProtocolParameters.maxDeltaSizeInBytes} bytes.`; + console.info(errorMessage); + throw new SidetreeError(ErrorCode.DeltaExceedsMaximumSize, errorMessage); + } + } } diff --git a/lib/core/versions/latest/Did.ts b/lib/core/versions/latest/Did.ts index dcd6bc516..f3f3ad8b1 100644 --- a/lib/core/versions/latest/Did.ts +++ b/lib/core/versions/latest/Did.ts @@ -1,6 +1,8 @@ import CreateOperation from './CreateOperation'; import Delta from './Delta'; +import Encoder from './Encoder'; import ErrorCode from './ErrorCode'; +import JsonCanonicalizer from './util/JsonCanonicalizer'; import Multihash from './Multihash'; import OperationType from '../../enums/OperationType'; import SidetreeError from '../../../common/SidetreeError'; @@ -35,14 +37,18 @@ export default class Did { private constructor (did: string, didMethodName: string) { this.didMethodName = didMethodName; const didPrefix = `did:${didMethodName}:`; + // TODO https://github.com/decentralized-identity/sidetree/issues/470 add network prefix to the didPrefix string if (!did.startsWith(didPrefix)) { throw new SidetreeError(ErrorCode.DidIncorrectPrefix); } - const indexOfDotChar = did.indexOf('.'); - // If there is no 'dot', then DID can only be in short-form. - if (indexOfDotChar < 0) { + const didWithoutPrefix = did.split(didPrefix)[1]; + + // split by : and ?, if there is 1 element, then it's short form. Long form has 2 elements + // TODO: SIP 2 #781 when the ? format is deprecated, `:` will be the only seperator. + const didSplitLength = didWithoutPrefix.split(/:|\?/).length; + if (didSplitLength === 1) { this.isShortForm = true; } else { this.isShortForm = false; @@ -53,7 +59,7 @@ export default class Did { } else { // Long-form can be in the form of: // 'did::?--initial-state=.' or - // 'did:::.' + // 'did:::Base64url(JCS({suffix-data, delta}))' const indexOfQuestionMarkChar = did.indexOf('?'); if (indexOfQuestionMarkChar > 0) { @@ -83,18 +89,18 @@ export default class Did { if (!did.isShortForm) { // Long-form can be in the form of: // 'did::?--initial-state=.' or - // 'did:::.' + // 'did:::Base64url(JCS({suffix-data, delta}))' const indexOfQuestionMarkChar = didString.indexOf('?'); - let initialState; + let createOperation; if (indexOfQuestionMarkChar > 0) { - initialState = Did.getInitialStateFromDidStringWithQueryParameter(didString, didMethodName); + const initialState = Did.getInitialStateFromDidStringWithQueryParameter(didString, didMethodName); + createOperation = await Did.constructCreateOperationFromInitialState(initialState); } else { - initialState = Did.getInitialStateFromDidStringWithExtraColon(didString); + const initialStateEncodedJcs = Did.getInitialStateFromDidStringWithExtraColon(didString); + createOperation = Did.constructCreateOperationFromEncodedJcs(initialStateEncodedJcs); } - const createOperation = await Did.constructCreateOperationFromInitialState(initialState); - // NOTE: we cannot use the unique suffix directly from `createOperation.didUniqueSuffix` for comparison, // because a given long-form DID may have been created long ago, // thus this version of `CreateOperation.parse()` maybe using a different hashing algorithm than that of the unique DID suffix (short-form). @@ -152,7 +158,7 @@ export default class Did { } private static getInitialStateFromDidStringWithExtraColon (didString: string): string { - // DID example: 'did:::.' + // DID example: 'did:::Base64url(JCS({suffix-data, delta}))' const lastColonIndex = didString.lastIndexOf(':'); @@ -161,7 +167,41 @@ export default class Did { return initialStateValue; } + private static constructCreateOperationFromEncodedJcs (initialStateEncodedJcs: string): CreateOperation { + // Initial state should be in the format base64url(JCS(initialState)) + const initialStateDecodedJcs = Encoder.decodeAsString(initialStateEncodedJcs); + let initialStateObject; + try { + initialStateObject = JSON.parse(initialStateDecodedJcs); + } catch { + throw new SidetreeError(ErrorCode.DidInitialStateJcsIsNotJosn, 'Long form initial state should be encoded jcs.'); + } + + Did.validateInitialState(initialStateEncodedJcs, initialStateObject); + Delta.validateDeltaSize(initialStateObject.delta); + + const createOperationRequest = { + type: OperationType.Create, + suffix_data: initialStateObject.suffix_data, + delta: initialStateObject.delta + }; + const createOperationBuffer = Buffer.from(JSON.stringify(createOperationRequest)); + const createOperation = CreateOperation.parseJcsObject(createOperationRequest, createOperationBuffer, false); + return createOperation; + } + + /** + * Make sure initial state is JCS + */ + private static validateInitialState (initialStateEncodedJcs: string, initialStateObject: any): void { + const expectedInitialState = Encoder.encode(JsonCanonicalizer.canonicalizeAsBuffer(initialStateObject)); + if (expectedInitialState !== initialStateEncodedJcs) { + throw new SidetreeError(ErrorCode.DidInitialStateJcsIsNotJcs, 'Initial state object and JCS string mismatch.'); + } + } + private static async constructCreateOperationFromInitialState (initialState: string): Promise { + // TODO: SIP 2 #781 deprecates this. Should be deleted when fully switched over // Initial state should be in the format: . const firstIndexOfDot = initialState.indexOf('.'); if (firstIndexOfDot === -1) { diff --git a/lib/core/versions/latest/DocumentComposer.ts b/lib/core/versions/latest/DocumentComposer.ts index 370418fa3..055696c0a 100644 --- a/lib/core/versions/latest/DocumentComposer.ts +++ b/lib/core/versions/latest/DocumentComposer.ts @@ -27,7 +27,7 @@ export default class DocumentComposer { const authentication: any[] = []; const publicKeys: any[] = []; if (Array.isArray(document.public_keys)) { - for (let publicKey of document.public_keys) { + for (const publicKey of document.public_keys) { const id = '#' + publicKey.id; const didDocumentPublicKey = { id: id, @@ -54,7 +54,7 @@ export default class DocumentComposer { let serviceEndpoints; if (Array.isArray(document.service_endpoints)) { serviceEndpoints = []; - for (let serviceEndpoint of document.service_endpoints) { + for (const serviceEndpoint of document.service_endpoints) { const didDocumentServiceEndpoint = { id: '#' + serviceEndpoint.id, type: serviceEndpoint.type, @@ -113,7 +113,7 @@ export default class DocumentComposer { } const allowedProperties = new Set(['public_keys', 'service_endpoints']); - for (let property in document) { + for (const property in document) { if (!allowedProperties.has(property)) { throw new SidetreeError(ErrorCode.DocumentComposerUnknownPropertyInDocument, `Unexpected property ${property} in document.`); } @@ -140,7 +140,7 @@ export default class DocumentComposer { throw new SidetreeError(ErrorCode.DocumentComposerUpdateOperationDocumentPatchesNotArray); } - for (let patch of patches) { + for (const patch of patches) { DocumentComposer.validatePatch(patch); } } @@ -183,7 +183,7 @@ export default class DocumentComposer { } const publicKeyIdSet: Set = new Set(); - for (let publicKey of publicKeys) { + for (const publicKey of publicKeys) { const publicKeyProperties = Object.keys(publicKey); // the expected fields are id, purpose, type and jwk if (publicKeyProperties.length !== 4) { @@ -210,12 +210,12 @@ export default class DocumentComposer { throw new SidetreeError(ErrorCode.DocumentComposerPublicKeyPurposeMissingOrUnknown); } - if (publicKey.purpose.length > 3) { + if (publicKey.purpose.length > 2) { throw new SidetreeError(ErrorCode.DocumentComposerPublicKeyPurposeExceedsMaxLength); } const validPurposes = new Set(Object.values(PublicKeyPurpose)); - // Purpose must be one of the valid ones in KeyPurpose + // Purpose must be one of the valid ones in PublicKeyPurpose for (const purpose of publicKey.purpose) { if (!validPurposes.has(purpose)) { throw new SidetreeError(ErrorCode.DocumentComposerPublicKeyInvalidPurpose); @@ -234,7 +234,7 @@ export default class DocumentComposer { throw new SidetreeError(ErrorCode.DocumentComposerPatchPublicKeyIdsNotArray); } - for (let publicKeyId of patch.public_keys) { + for (const publicKeyId of patch.public_keys) { if (typeof publicKeyId !== 'string') { throw new SidetreeError(ErrorCode.DocumentComposerPatchPublicKeyIdNotString); } @@ -284,7 +284,7 @@ export default class DocumentComposer { throw new SidetreeError(ErrorCode.DocumentComposerPatchServiceEndpointsNotArray); } - for (let serviceEndpoint of serviceEndpoints) { + for (const serviceEndpoint of serviceEndpoints) { const serviceEndpointProperties = Object.keys(serviceEndpoint); if (serviceEndpointProperties.length !== 3) { // type, id, and endpoint throw new SidetreeError(ErrorCode.DocumentComposerServiceEndpointMissingOrUnknownProperty); @@ -295,22 +295,28 @@ export default class DocumentComposer { if (typeof serviceEndpoint.type !== 'string') { throw new SidetreeError(ErrorCode.DocumentComposerPatchServiceEndpointTypeNotString); } + if (serviceEndpoint.type.length > 30) { throw new SidetreeError(ErrorCode.DocumentComposerPatchServiceEndpointTypeTooLong); } - if (typeof serviceEndpoint.endpoint !== 'string') { - throw new SidetreeError(ErrorCode.DocumentComposerPatchServiceEndpointServiceEndpointNotString); - } - if (serviceEndpoint.endpoint.length > 100) { - throw new SidetreeError(ErrorCode.DocumentComposerPatchServiceEndpointServiceEndpointTooLong); - } - try { - // just want to validate url, no need to assign to variable, it will throw if not valid - // tslint:disable-next-line - new URL(serviceEndpoint.endpoint); - } catch { - throw new SidetreeError(ErrorCode.DocumentComposerPatchServiceEndpointServiceEndpointNotValidUrl); + // `endpoint` validation. + const endpoint = serviceEndpoint.endpoint; + if (typeof endpoint === 'string') { + try { + // just want to validate url, no need to assign to variable, it will throw if not valid + // tslint:disable-next-line + new URL(serviceEndpoint.endpoint); + } catch { + throw new SidetreeError(ErrorCode.DocumentComposerPatchServiceEndpointServiceEndpointNotValidUrl); + } + } else if (typeof endpoint === 'object') { + // Allow `object` type only if it is not an array. + if (Array.isArray(endpoint)) { + throw new SidetreeError(ErrorCode.DocumentComposerPatchServiceEndpointValueCannotBeAnArray); + } + } else { + throw new SidetreeError(ErrorCode.DocumentComposerPatchServiceEndpointMustBeStringOrNonArrayObject); } } } @@ -319,7 +325,7 @@ export default class DocumentComposer { if (typeof id !== 'string') { throw new SidetreeError(ErrorCode.DocumentComposerIdNotString, `ID not string: ${JSON.stringify(id)} is of type '${typeof id}'`); } - if (id.length > 20) { + if (id.length > 50) { throw new SidetreeError(ErrorCode.DocumentComposerIdTooLong); } @@ -336,7 +342,7 @@ export default class DocumentComposer { public static applyPatches (document: any, patches: any[]): any { // Loop through and apply all patches. let resultantDocument = document; - for (let patch of patches) { + for (const patch of patches) { resultantDocument = DocumentComposer.applyPatchToDidDocument(resultantDocument, patch); } @@ -367,7 +373,7 @@ export default class DocumentComposer { const publicKeyMap = new Map((document.public_keys || []).map(publicKey => [publicKey.id, publicKey])); // Loop through all given public keys and add them if they don't exist already. - for (let publicKey of patch.public_keys) { + for (const publicKey of patch.public_keys) { // NOTE: If a key ID already exists, we will just replace the existing key. // Not throwing error will minimize the need (thus risk) of reusing exposed update reveal value. publicKeyMap.set(publicKey.id, publicKey); @@ -385,7 +391,7 @@ export default class DocumentComposer { const publicKeyMap = new Map((document.public_keys || []).map(publicKey => [publicKey.id, publicKey])); // Loop through all given public key IDs and delete them from the existing public key only if it is not a recovery key. - for (let publicKey of patch.public_keys) { + for (const publicKey of patch.public_keys) { const existingKey = publicKeyMap.get(publicKey); if (existingKey !== undefined) { diff --git a/lib/core/versions/latest/Encoder.ts b/lib/core/versions/latest/Encoder.ts index 8a65a2092..e032fd109 100644 --- a/lib/core/versions/latest/Encoder.ts +++ b/lib/core/versions/latest/Encoder.ts @@ -1,6 +1,6 @@ -import base64url from 'base64url'; import ErrorCode from './ErrorCode'; import SidetreeError from '../../../common/SidetreeError'; +import base64url from 'base64url'; /** * Class that encodes binary blobs into strings. diff --git a/lib/core/versions/latest/ErrorCode.ts b/lib/core/versions/latest/ErrorCode.ts index 7f52eaa1c..54e7dff10 100644 --- a/lib/core/versions/latest/ErrorCode.ts +++ b/lib/core/versions/latest/ErrorCode.ts @@ -26,11 +26,12 @@ export default { CasFileNotFound: 'cas_file_not_found', CasFileTooLarge: 'cas_file_too_large', CasNotReachable: 'cas_not_reachable', - ChunkFileDeltasNotArrayOfStrings: 'chunk_file_deltas_not_array_of_string', + ChunkFileDeltasNotArrayOfObjects: 'chunk_file_deltas_not_array_of_objects', ChunkFileDeltasPropertyNotArray: 'chunk_file_deltas_property_not_array', ChunkFileUnexpectedProperty: 'chunk_file_unexpected_property', CompressorMaxAllowedDecompressedDataSizeExceeded: 'compressor_max_allowed_decompressed_data_size_exceeded', CreateOperationMissingOrUnknownProperty: 'create_operation_missing_or_unknown_property', + CreateOperationSuffixDataIsNotObject: 'create_operation_suffix_data_is_not_object', CreateOperationSuffixDataMissingOrNotString: 'create_operation_suffix_data_missing_or_not_string', CreateOperationSuffixDataMissingOrUnknownProperty: 'create_operation_suffix_data_missing_or_unknown_property', CreateOperationSuffixDataTypeInvalidCharacter: 'create_operation_suffix_data_type_invalid_character', @@ -42,10 +43,13 @@ export default { DeactivateOperationSignedDataMissingOrUnknownProperty: 'deactivate_operation_signed_data_missing_or_unknown_property', DeactivateOperationSignedDidUniqueSuffixMismatch: 'deactivate_operation_signed_did_unique_suffix_mismatch', DeactivateOperationTypeIncorrect: 'deactivate_operation_type_incorrect', + DeltaIsNotObject: 'delta_is_not_object', DeltaMissingOrNotString: 'delta_missing_or_not_string', DeltaExceedsMaximumSize: 'delta_exceeds_maximum_size', DeltaMissingOrUnknownProperty: 'delta_missing_or_unknown_property', DidIncorrectPrefix: 'did_incorrect_prefix', + DidInitialStateJcsIsNotJcs: 'did_initial_state_jcs_is_not_jcs', + DidInitialStateJcsIsNotJosn: 'did_initial_state_jcs_is_not_json', DidInitialStateValueContainsMoreThanOneDot: 'did_initial_state_value_contains_more_than_one_dot', DidInitialStateValueContainsNoDot: 'did_initial_state_value_contains_no_dot', DidInitialStateValueDoesNotContainTwoParts: 'did_initial_state_value_does_not_contain_two_parts', @@ -77,11 +81,11 @@ export default { DocumentComposerPublicKeyPurposeExceedsMaxLength: 'document_composer_public_key_purpose_exceeds_max_length', DocumentComposerPublicKeyInvalidPurpose: 'document_composer_public_key_invalid_purpose', DocumentComposerPatchServiceEndpointIdsNotArray: 'document_composer_service_endpoint_ids_not_array', - DocumentComposerPatchServiceEndpointServiceEndpointNotString: 'document_composer_service_endpoint_service_endpoint_not_string', + DocumentComposerPatchServiceEndpointMustBeStringOrNonArrayObject: 'document_composer_service_endpoint_must_be_string_or_non_array_object', DocumentComposerPatchServiceEndpointServiceEndpointNotValidUrl: 'document_composer_service_endpoint_service_endpoint_not_valid_url', - DocumentComposerPatchServiceEndpointServiceEndpointTooLong: 'document_composer_service_endpoint_service_endpoint_too_long', DocumentComposerPatchServiceEndpointTypeNotString: 'document_composer_service_endpoint_type_not_string', DocumentComposerPatchServiceEndpointTypeTooLong: 'document_composer_service_endpoint_type_too_long', + DocumentComposerPatchServiceEndpointValueCannotBeAnArray: 'document_composer_service_endpoint_service_endpoint_cannot_be_an_array', DocumentComposerServiceEndpointMissingOrUnknownProperty: 'document_composer_service_endpoint_missing_or_unknown_property', DocumentComposerServiceNotArray: 'document_composer_service_not_array', DocumentComposerUnknownPropertyInDocument: 'document_composer_unknown_property_in_document', @@ -90,6 +94,8 @@ export default { DocumentNotValidOriginalDocument: 'document_not_valid_original_document', EncoderValidateBase64UrlStringInputNotBase64UrlString: 'encoder_validate_base64url_string_input_not_base64url_string', EncoderValidateBase64UrlStringInputNotString: 'encoder_validate_base64url_string_input_not_string', + JwkEs256kHasIncorrectLengthOfX: 'jwk_es256k_has_incorrect_length_of_x', + JwkEs256kHasIncorrectLengthOfY: 'jwk_es256k_has_incorrect_length_of_y', JwkEs256kHasUnknownProperty: 'jwk_es256k_has_unknown_property', JwkEs256kMissingOrInvalidCrv: 'jwk_es256k_missing_or_invalid_crv', JwkEs256kMissingOrInvalidKty: 'jwk_es256k_missing_or_invalid_kty', diff --git a/lib/core/versions/latest/MapFile.ts b/lib/core/versions/latest/MapFile.ts index aad8c6588..53da010db 100644 --- a/lib/core/versions/latest/MapFile.ts +++ b/lib/core/versions/latest/MapFile.ts @@ -44,7 +44,7 @@ export default class MapFile { } const allowedProperties = new Set(['chunks', 'operations']); - for (let property in mapFileModel) { + for (const property in mapFileModel) { if (!allowedProperties.has(property)) { throw new SidetreeError(ErrorCode.MapFileHasUnknownProperty); } diff --git a/lib/core/versions/latest/MongoDbOperationQueue.ts b/lib/core/versions/latest/MongoDbOperationQueue.ts index 607aa5eb5..33617467a 100644 --- a/lib/core/versions/latest/MongoDbOperationQueue.ts +++ b/lib/core/versions/latest/MongoDbOperationQueue.ts @@ -1,8 +1,8 @@ +import { Binary, Collection, Db, MongoClient } from 'mongodb'; import ErrorCode from './ErrorCode'; import IOperationQueue from './interfaces/IOperationQueue'; -import SidetreeError from '../../../common/SidetreeError'; -import { Binary, Collection, MongoClient, Db } from 'mongodb'; import QueuedOperationModel from './models/QueuedOperationModel'; +import SidetreeError from '../../../common/SidetreeError'; /** * Sidetree operation stored in MongoDb. @@ -70,7 +70,7 @@ export default class MongoDbOperationQueue implements IOperationQueue { return []; } - const queuedOperations = await this.collection!.find().sort({ _id : 1 }).limit(count).toArray(); + const queuedOperations = await this.collection!.find().sort({ _id: 1 }).limit(count).toArray(); const lastOperation = queuedOperations[queuedOperations.length - 1]; await this.collection!.deleteMany({ _id: { $lte: lastOperation._id } }); @@ -83,7 +83,7 @@ export default class MongoDbOperationQueue implements IOperationQueue { } // NOTE: `_id` is the default index that is sorted based by create time. - const queuedOperations = await this.collection!.find().sort({ _id : 1 }).limit(count).toArray(); + const queuedOperations = await this.collection!.find().sort({ _id: 1 }).limit(count).toArray(); return queuedOperations.map((operation) => MongoDbOperationQueue.convertToQueuedOperationModel(operation)); } diff --git a/lib/core/versions/latest/Multihash.ts b/lib/core/versions/latest/Multihash.ts index 22faac99c..dde4a94be 100644 --- a/lib/core/versions/latest/Multihash.ts +++ b/lib/core/versions/latest/Multihash.ts @@ -46,6 +46,17 @@ export default class Multihash { return hash; } + /** + * Canonicalize the given content, then double hashes the result using the latest supported hash algorithm, then encodes the multihash. + * Mainly used for testing purposes. + */ + public static canonicalizeThenHashThenEncode (content: object) { + const canonicalizedStringBuffer = JsonCanonicalizer.canonicalizeAsBuffer(content); + + const multihashEncodedString = Multihash.hashThenEncode(canonicalizedStringBuffer, ProtocolParameters.hashAlgorithmInMultihashCode); + return multihashEncodedString; + } + /** * Canonicalize the given content, then double hashes the result using the latest supported hash algorithm, then encodes the multihash. * Mainly used for testing purposes. @@ -133,7 +144,7 @@ export default class Multihash { try { const contentBuffer = Encoder.decodeAsBuffer(encodedContent); - return Multihash.verify(contentBuffer, encodedMultihash); + return Multihash.verifyEncodedMultihashForContent(contentBuffer, encodedMultihash); } catch (error) { console.log(error); return false; @@ -182,7 +193,7 @@ export default class Multihash { /** * Verifies the multihash against the content `Buffer`. */ - private static verify (content: Buffer, encodedMultihash: string): boolean { + public static verifyEncodedMultihashForContent (content: Buffer, encodedMultihash: string): boolean { try { const expectedMultihashBuffer = Encoder.decodeAsBuffer(encodedMultihash); @@ -190,7 +201,11 @@ export default class Multihash { const actualMultihashBuffer = Multihash.hash(content, hashAlgorithmCode); - return Buffer.compare(actualMultihashBuffer, expectedMultihashBuffer) === 0; + // Compare the strings instead of buffers, because encoding schemes such as base64URL can allow two distinct strings to decode into the same buffer. + // e.g. 'EiAJID5-y7rbEs7I3PPiMtwVf28LTkPFD4BWIZPCtb6AMg' and + // 'EiAJID5-y7rbEs7I3PPiMtwVf28LTkPFD4BWIZPCtb6AMv' would decode into the same buffer. + const actualMultihashString = Encoder.encode(actualMultihashBuffer); + return actualMultihashString === encodedMultihash; } catch (error) { console.log(error); return false; diff --git a/lib/core/versions/latest/Operation.ts b/lib/core/versions/latest/Operation.ts index b413c60d3..012ad120f 100644 --- a/lib/core/versions/latest/Operation.ts +++ b/lib/core/versions/latest/Operation.ts @@ -30,7 +30,7 @@ export default class Operation { const isAnchorFileMode = false; if (operationType === OperationType.Create) { - return CreateOperation.parseObject(operationObject, operationBuffer, isAnchorFileMode); + return CreateOperation.parseJcsObject(operationObject, operationBuffer, isAnchorFileMode); } else if (operationType === OperationType.Update) { return UpdateOperation.parseObject(operationObject, operationBuffer, isAnchorFileMode); } else if (operationType === OperationType.Recover) { @@ -43,16 +43,14 @@ export default class Operation { } /** - * Parses the given encoded delta string into an internal `DeltaModel`. + * validate delta and throw if invalid + * @param delta the delta to validate */ - public static async parseDelta (deltaEncodedString: any): Promise { - if (typeof deltaEncodedString !== 'string') { - throw new SidetreeError(ErrorCode.DeltaMissingOrNotString); + public static validateDelta (delta: any): void { + if (typeof delta !== 'object') { + throw new SidetreeError(ErrorCode.DeltaIsNotObject); } - const deltaJsonString = Encoder.decodeAsString(deltaEncodedString); - const delta = await JsonAsync.parse(deltaJsonString); - const properties = Object.keys(delta); if (properties.length !== 2) { throw new SidetreeError(ErrorCode.DeltaMissingOrUnknownProperty); @@ -66,6 +64,20 @@ export default class Operation { DocumentComposer.validateDocumentPatches(delta.patches); const nextUpdateCommitment = Encoder.decodeAsBuffer(delta.update_commitment); Multihash.verifyHashComputedUsingLatestSupportedAlgorithm(nextUpdateCommitment); + } + + /** + * Parses the given encoded delta string into an internal `DeltaModel`. + */ + public static async parseDelta (deltaEncodedString: any): Promise { + if (typeof deltaEncodedString !== 'string') { + throw new SidetreeError(ErrorCode.DeltaMissingOrNotString); + } + + const deltaJsonString = Encoder.decodeAsString(deltaEncodedString); + const delta = await JsonAsync.parse(deltaJsonString); + + Operation.validateDelta(delta); return { patches: delta.patches, diff --git a/lib/core/versions/latest/OperationProcessor.ts b/lib/core/versions/latest/OperationProcessor.ts index a9526f338..ad43ab302 100644 --- a/lib/core/versions/latest/OperationProcessor.ts +++ b/lib/core/versions/latest/OperationProcessor.ts @@ -1,8 +1,8 @@ import AnchoredOperationModel from '../../models/AnchoredOperationModel'; import CreateOperation from './CreateOperation'; import DeactivateOperation from './DeactivateOperation'; -import DocumentComposer from './DocumentComposer'; import DidState from '../../models/DidState'; +import DocumentComposer from './DocumentComposer'; import ErrorCode from './ErrorCode'; import IOperationProcessor from '../../interfaces/IOperationProcessor'; import JsonCanonicalizer from './util/JsonCanonicalizer'; @@ -113,11 +113,19 @@ export default class OperationProcessor implements IOperationProcessor { lastOperationTransactionNumber: anchoredOperationModel.transactionNumber }; - // Ensure actual delta hash matches expected delta hash. - const isMatchingDelta = Multihash.isValidHash(operation.encodedDelta, operation.suffixData.deltaHash); + // Verify the delta hash against the expected delta hash. + const deltaPayload = operation.delta ? JsonCanonicalizer.canonicalizeAsBuffer({ + update_commitment: operation.delta.updateCommitment, + patches: operation.delta.patches + }) : undefined; + if (deltaPayload === undefined) { + return newDidState; + }; + + const isMatchingDelta = Multihash.verifyEncodedMultihashForContent(deltaPayload, operation.suffixData.deltaHash); if (!isMatchingDelta) { return newDidState; - } + }; // Apply the given patches against an empty object. const delta = operation.delta; @@ -163,10 +171,18 @@ export default class OperationProcessor implements IOperationProcessor { } // Verify the delta hash against the expected delta hash. - const isValidDelta = Multihash.isValidHash(operation.encodedDelta, operation.signedData.deltaHash); - if (!isValidDelta) { + const deltaPayload = operation.delta ? JsonCanonicalizer.canonicalizeAsBuffer({ + update_commitment: operation.delta.updateCommitment, + patches: operation.delta.patches + }) : undefined; + if (deltaPayload === undefined) { return didState; - } + }; + + const isMatchingDelta = Multihash.verifyEncodedMultihashForContent(deltaPayload, operation.signedData.deltaHash); + if (!isMatchingDelta) { + return didState; + }; let resultingDocument; try { @@ -221,11 +237,19 @@ export default class OperationProcessor implements IOperationProcessor { lastOperationTransactionNumber: anchoredOperationModel.transactionNumber }; - // Verify the actual delta hash against the expected delta hash. - const isMatchingDelta = Multihash.isValidHash(operation.encodedDelta, operation.signedData.deltaHash); + // Verify the delta hash against the expected delta hash. + const deltaPayload = operation.delta ? JsonCanonicalizer.canonicalizeAsBuffer({ + update_commitment: operation.delta.updateCommitment, + patches: operation.delta.patches + }) : undefined; + if (deltaPayload === undefined) { + return newDidState; + }; + + const isMatchingDelta = Multihash.verifyEncodedMultihashForContent(deltaPayload, operation.signedData.deltaHash); if (!isMatchingDelta) { return newDidState; - } + }; // Apply the given patches against an empty object. const delta = operation.delta; diff --git a/lib/core/versions/latest/ProtocolParameters.ts b/lib/core/versions/latest/ProtocolParameters.ts index 93b91b185..4287dff98 100644 --- a/lib/core/versions/latest/ProtocolParameters.ts +++ b/lib/core/versions/latest/ProtocolParameters.ts @@ -3,6 +3,6 @@ /** * Defines the list of protocol parameters, intended ONLY to be used within each version of Sidetree. */ -let protocolParameters: ProtocolParameters = require('./protocol-parameters.json'); +const protocolParameters: ProtocolParameters = require('./protocol-parameters.json'); export default protocolParameters; diff --git a/lib/core/versions/latest/RecoverOperation.ts b/lib/core/versions/latest/RecoverOperation.ts index e51d17cc8..b7e872eb1 100644 --- a/lib/core/versions/latest/RecoverOperation.ts +++ b/lib/core/versions/latest/RecoverOperation.ts @@ -34,9 +34,6 @@ export default class RecoverOperation implements OperationModel { /** Signed data. */ public readonly signedDataJws: Jws; - /** Encoded string of the delta. */ - public readonly encodedDelta: string | undefined; - /** Decoded signed data payload. */ public readonly signedData: SignedDataModel; @@ -51,7 +48,6 @@ export default class RecoverOperation implements OperationModel { didUniqueSuffix: string, signedDataJws: Jws, signedData: SignedDataModel, - encodedDelta: string | undefined, delta: DeltaModel | undefined ) { this.operationBuffer = operationBuffer; @@ -59,7 +55,6 @@ export default class RecoverOperation implements OperationModel { this.didUniqueSuffix = didUniqueSuffix; this.signedDataJws = signedDataJws; this.signedData = signedData; - this.encodedDelta = encodedDelta; this.delta = delta; } @@ -108,20 +103,22 @@ export default class RecoverOperation implements OperationModel { const signedData = await RecoverOperation.parseSignedDataPayload(signedDataJws.payload); // If not in anchor file mode, we need to validate `type` and `delta` properties. - let encodedDelta = undefined; let delta = undefined; if (!anchorFileMode) { if (operationObject.type !== OperationType.Recover) { throw new SidetreeError(ErrorCode.RecoverOperationTypeIncorrect); } - encodedDelta = operationObject.delta; try { - delta = await Operation.parseDelta(operationObject.delta); + Operation.validateDelta(operationObject.delta); + delta = { + patches: operationObject.delta.patches, + updateCommitment: operationObject.delta.update_commitment + }; } catch { - // For compatibility with data pruning, we have to assume that delta may be unavailable, - // thus an operation with invalid delta needs to be processed as an operation with unavailable delta, - // so here we let delta be `undefined`. + // For compatibility with data pruning, we have to assume that `delta` may be unavailable, + // thus an operation with invalid `delta` needs to be processed as an operation with unavailable `delta`, + // so here we let `delta` be `undefined`. } } @@ -130,7 +127,6 @@ export default class RecoverOperation implements OperationModel { operationObject.did_suffix, signedDataJws, signedData, - encodedDelta, delta ); } diff --git a/lib/core/versions/latest/RequestHandler.ts b/lib/core/versions/latest/RequestHandler.ts index be1aac05a..afe80db8e 100644 --- a/lib/core/versions/latest/RequestHandler.ts +++ b/lib/core/versions/latest/RequestHandler.ts @@ -44,7 +44,7 @@ export default class RequestHandler implements IRequestHandler { if (operationRequest.type === OperationType.Create || operationRequest.type === OperationType.Recover || operationRequest.type === OperationType.Update) { - Delta.validateEncodedDeltaSize(operationRequest.delta); + Delta.validateDeltaSize(operationRequest.delta); } operationModel = await Operation.parse(request); @@ -146,7 +146,7 @@ export default class RequestHandler implements IRequestHandler { * @param shortOrLongFormDid Can either be: * 1. A short-form DID. e.g. 'did::abc' or * 2. A long-form DID. e.g. 'did::?--initial-state=.' or - * 'did:::.' + * 'did:::Base64url(JCS({suffix-data, delta}))' */ public async handleResolveRequest (shortOrLongFormDid: string): Promise { try { @@ -167,6 +167,7 @@ export default class RequestHandler implements IRequestHandler { } if (didState === undefined) { + console.info(`DID not found for DID '${shortOrLongFormDid}'...`); return { status: ResponseStatus.NotFound, body: { code: ErrorCode.DidNotFound, message: 'DID Not Found' } @@ -183,6 +184,7 @@ export default class RequestHandler implements IRequestHandler { const didDeactivated = didState.nextRecoveryCommitmentHash === undefined; const status = didDeactivated ? ResponseStatus.Deactivated : ResponseStatus.Succeeded; + console.info(`DID Document found for DID '${shortOrLongFormDid}'...`); return { status, body: document @@ -190,6 +192,7 @@ export default class RequestHandler implements IRequestHandler { } catch (error) { // Give meaningful/specific error code and message when possible. if (error instanceof SidetreeError) { + console.info(`Bad request. Code: ${error.code}. Message: ${error.message}`); return { status: ResponseStatus.BadRequest, body: { code: error.code, message: error.message } @@ -210,6 +213,8 @@ export default class RequestHandler implements IRequestHandler { * @returns [DID state, published] */ private async resolveLongFormDid (did: Did): Promise<[DidState | undefined, boolean]> { + console.info(`Handling long-form DID resolution of DID '${did}'...`); + // Attempt to resolve the DID by using operations found from the network first. let didState = await this.resolver.resolve(did.uniqueSuffix); diff --git a/lib/core/versions/latest/TransactionProcessor.ts b/lib/core/versions/latest/TransactionProcessor.ts index 3ceacdfaa..3c5fbe966 100644 --- a/lib/core/versions/latest/TransactionProcessor.ts +++ b/lib/core/versions/latest/TransactionProcessor.ts @@ -1,6 +1,6 @@ +import AnchorFile from './AnchorFile'; import AnchoredDataSerializer from './AnchoredDataSerializer'; import AnchoredOperationModel from '../../models/AnchoredOperationModel'; -import AnchorFile from './AnchorFile'; import ArrayMethods from './util/ArrayMethods'; import ChunkFile from './ChunkFile'; import ChunkFileModel from './models/ChunkFileModel'; @@ -101,8 +101,8 @@ export default class TransactionProcessor implements ITransactionProcessor { // Verify required lock if one was needed. const valueTimeLock = anchorFile.model.writer_lock_id - ? await this.blockchain.getValueTimeLock(anchorFile.model.writer_lock_id) - : undefined; + ? await this.blockchain.getValueTimeLock(anchorFile.model.writer_lock_id) + : undefined; ValueTimeLockVerifier.verifyLockAmountAndThrowOnError( valueTimeLock, paidOperationCount, @@ -211,10 +211,10 @@ export default class TransactionProcessor implements ITransactionProcessor { chunkFile: ChunkFileModel | undefined ): Promise { - let createOperations = anchorFile.createOperations; - let recoverOperations = anchorFile.recoverOperations; - let deactivateOperations = anchorFile.deactivateOperations; - let updateOperations = (mapFile && mapFile.updateOperations) ? mapFile.updateOperations : []; + const createOperations = anchorFile.createOperations; + const recoverOperations = anchorFile.recoverOperations; + const deactivateOperations = anchorFile.deactivateOperations; + const updateOperations = (mapFile && mapFile.updateOperations) ? mapFile.updateOperations : []; // Add the operations in the following order of types: create, recover, update, deactivate. const operations = []; diff --git a/lib/core/versions/latest/TransactionSelector.ts b/lib/core/versions/latest/TransactionSelector.ts index 82cb543c4..bfb0861f4 100644 --- a/lib/core/versions/latest/TransactionSelector.ts +++ b/lib/core/versions/latest/TransactionSelector.ts @@ -47,8 +47,8 @@ export default class TransactionSelector implements ITransactionSelector { TransactionSelector.enqueueFirstTransactionFromEachWriter(transactions, currentTransactionTime, transactionsPriorityQueue); const [numberOfOperations, numberOfTransactions] = await this.getNumberOfOperationsAndTransactionsAlreadyInTransactionTime(currentTransactionTime); - let numberOfOperationsToQualify = this.maxNumberOfOperationsPerBlock - numberOfOperations; - let numberOfTransactionsToQualify = this.maxNumberOfTransactionsPerBlock - numberOfTransactions; + const numberOfOperationsToQualify = this.maxNumberOfOperationsPerBlock - numberOfOperations; + const numberOfTransactionsToQualify = this.maxNumberOfTransactionsPerBlock - numberOfTransactions; const transactionsToReturn = TransactionSelector.getHighestFeeTransactionsFromCurrentTransactionTime( numberOfOperationsToQualify, @@ -74,7 +74,7 @@ export default class TransactionSelector implements ITransactionSelector { // only 1 transaction is allowed per writer if (writerToTransactionNumberMap.has(transaction.writer)) { const acceptedTransactionNumber = writerToTransactionNumberMap.get(transaction.writer); - // tslint:disable-next-line:max-line-length + // eslint-disable-next-line max-len console.info(`Multiple transactions found in transaction time ${currentTransactionTime} from writer ${transaction.writer}, considering transaction ${acceptedTransactionNumber} and ignoring ${transaction.transactionNumber}`); } else { transactionsPriorityQueue.push(transaction); @@ -112,9 +112,9 @@ export default class TransactionSelector implements ITransactionSelector { let numberOfOperationsSeen = 0; const transactionsToReturn = []; - while (transactionsToReturn.length < numberOfTransactionsToQualify - && numberOfOperationsSeen < numberOfOperationsToQualify - && transactionsPriorityQueue.length > 0) { + while (transactionsToReturn.length < numberOfTransactionsToQualify && + numberOfOperationsSeen < numberOfOperationsToQualify && + transactionsPriorityQueue.length > 0) { const currentTransaction = transactionsPriorityQueue.pop(); try { const numOfOperationsInCurrentTransaction = AnchoredDataSerializer.deserialize(currentTransaction.anchorString).numberOfOperations; diff --git a/lib/core/versions/latest/UpdateOperation.ts b/lib/core/versions/latest/UpdateOperation.ts index 23a081110..515cb16e6 100644 --- a/lib/core/versions/latest/UpdateOperation.ts +++ b/lib/core/versions/latest/UpdateOperation.ts @@ -39,9 +39,6 @@ export default class UpdateOperation implements OperationModel { /** Patch data. */ public readonly delta: DeltaModel | undefined; - /** Encoded string of the delta. */ - public readonly encodedDelta: string | undefined; - /** * NOTE: should only be used by `parse()` and `parseObject()` else the constructed instance could be invalid. */ @@ -50,14 +47,12 @@ export default class UpdateOperation implements OperationModel { didUniqueSuffix: string, signedDataJws: Jws, signedData: SignedDataModel, - encodedDelta: string | undefined, delta: DeltaModel | undefined) { this.operationBuffer = operationBuffer; this.type = OperationType.Update; this.didUniqueSuffix = didUniqueSuffix; this.signedDataJws = signedDataJws; this.signedData = signedData; - this.encodedDelta = encodedDelta; this.delta = delta; } @@ -106,18 +101,19 @@ export default class UpdateOperation implements OperationModel { const signedDataModel = await UpdateOperation.parseSignedDataPayload(signedData.payload); // If not in map file mode, we need to validate `type` and `delta` properties. - let encodedDelta = undefined; let delta = undefined; if (!mapFileMode) { if (operationObject.type !== OperationType.Update) { throw new SidetreeError(ErrorCode.UpdateOperationTypeIncorrect); } - - encodedDelta = operationObject.delta; - delta = await Operation.parseDelta(encodedDelta); + Operation.validateDelta(operationObject.delta); + delta = { + patches: operationObject.delta.patches, + updateCommitment: operationObject.delta.update_commitment + }; } - return new UpdateOperation(operationBuffer, operationObject.did_suffix, signedData, signedDataModel, encodedDelta, delta); + return new UpdateOperation(operationBuffer, operationObject.did_suffix, signedData, signedDataModel, delta); } private static async parseSignedDataPayload (signedDataEncodedString: string): Promise { diff --git a/lib/core/versions/latest/ValueTimeLockVerifier.ts b/lib/core/versions/latest/ValueTimeLockVerifier.ts index fd2bd7ff1..759de758d 100644 --- a/lib/core/versions/latest/ValueTimeLockVerifier.ts +++ b/lib/core/versions/latest/ValueTimeLockVerifier.ts @@ -76,7 +76,7 @@ export default class ValueTimeLockVerifier { sidetreeTransactionTime >= valueTimeLock.unlockTransactionTime) { throw new SidetreeError( ErrorCode.ValueTimeLockVerifierTransactionTimeOutsideLockRange, - // tslint:disable-next-line: max-line-length + // eslint-disable-next-line max-len `Sidetree transaction block: ${sidetreeTransactionTime}; lock start time: ${valueTimeLock.lockTransactionTime}; unlock time: ${valueTimeLock.unlockTransactionTime}`); } } diff --git a/lib/core/versions/latest/models/AnchorFileModel.ts b/lib/core/versions/latest/models/AnchorFileModel.ts index a845211c1..a3dc6caf2 100644 --- a/lib/core/versions/latest/models/AnchorFileModel.ts +++ b/lib/core/versions/latest/models/AnchorFileModel.ts @@ -6,7 +6,11 @@ export default interface AnchorFileModel { map_file_uri: string; operations: { create?: { - suffix_data: string; + suffix_data: { + delta_hash: string; + recovery_commitment: string; + type?: string; + }; }[], recover?: { did_suffix: string; diff --git a/lib/core/versions/latest/models/ChunkFileModel.ts b/lib/core/versions/latest/models/ChunkFileModel.ts index 53f90fa36..f74f73a90 100644 --- a/lib/core/versions/latest/models/ChunkFileModel.ts +++ b/lib/core/versions/latest/models/ChunkFileModel.ts @@ -1,6 +1,7 @@ /** * Defines the external Chunk File structure. + * Deltas are intentionally objects because at observing time, we don't know if delta are valid or not yet. */ export default interface ChunkFileModel { - deltas: string[]; + deltas: object[]; } diff --git a/lib/core/versions/latest/models/ServiceEndpointModel.ts b/lib/core/versions/latest/models/ServiceEndpointModel.ts index b052ec8f2..d225f2326 100644 --- a/lib/core/versions/latest/models/ServiceEndpointModel.ts +++ b/lib/core/versions/latest/models/ServiceEndpointModel.ts @@ -2,8 +2,8 @@ * Defines the data structure of an element of `service` array within the DID Document used by Sidetree for basic type safety checks. * NOTE: The class intentionally contains "Endpoint" to disambiguate from overloaded term "Service". */ -export default interface DidServiceEndpointModel { +export default interface ServiceEndpointModel { id: string; type: string; - endpoint: string; + endpoint: string | object; } diff --git a/lib/core/versions/latest/protocol-parameters.json b/lib/core/versions/latest/protocol-parameters.json index 23259b019..789066d3b 100644 --- a/lib/core/versions/latest/protocol-parameters.json +++ b/lib/core/versions/latest/protocol-parameters.json @@ -2,7 +2,7 @@ "hashAlgorithmInMultihashCode": 18, "maxAnchorFileSizeInBytes": 1000000, "maxMapFileSizeInBytes": 1000000, - "maxChunkFileSizeInBytes": 20000000, + "maxChunkFileSizeInBytes": 10000000, "maxNumberOfOperationsPerTransactionTime": 600000, "maxNumberOfTransactionsPerTransactionTime": 300, "maxOperationsPerBatch": 10000, diff --git a/lib/core/versions/latest/util/Jwk.ts b/lib/core/versions/latest/util/Jwk.ts index 100ea6be2..53525ad1d 100644 --- a/lib/core/versions/latest/util/Jwk.ts +++ b/lib/core/versions/latest/util/Jwk.ts @@ -1,7 +1,7 @@ import ErrorCode from '../ErrorCode'; +import { JWK } from 'jose'; import JwkEs256k from '../../../models/JwkEs256k'; import SidetreeError from '../../../../common/SidetreeError'; -import { JWK } from 'jose'; /** * Class containing reusable JWK operations. @@ -38,7 +38,7 @@ export default class Jwk { } const allowedProperties = new Set(['kty', 'crv', 'x', 'y']); - for (let property in jwk) { + for (const property in jwk) { if (!allowedProperties.has(property)) { throw new SidetreeError(ErrorCode.JwkEs256kHasUnknownProperty); } @@ -59,6 +59,15 @@ export default class Jwk { if (typeof jwk.y !== 'string') { throw new SidetreeError(ErrorCode.JwkEs256kMissingOrInvalidTypeY); } + + // `x` and `y` need 43 Base64URL encoded bytes to contain 256 bits. + if (jwk.x.length !== 43) { + throw new SidetreeError(ErrorCode.JwkEs256kHasIncorrectLengthOfX, `SECP256K1 JWK 'x' property must be 43 bytes.`); + } + + if (jwk.y.length !== 43) { + throw new SidetreeError(ErrorCode.JwkEs256kHasIncorrectLengthOfY, `SECP256K1 JWK 'y' property must be 43 bytes.`); + } } /** diff --git a/lib/core/versions/latest/util/Jws.ts b/lib/core/versions/latest/util/Jws.ts index 5e21e85d0..12d997aab 100644 --- a/lib/core/versions/latest/util/Jws.ts +++ b/lib/core/versions/latest/util/Jws.ts @@ -1,9 +1,9 @@ import Encoder from '../Encoder'; import ErrorCode from '../ErrorCode'; +import { JWS } from 'jose'; import JwkEs256k from '../../../models/JwkEs256k'; import JwsModel from '../models/JwsModel'; import SidetreeError from '../../../../common/SidetreeError'; -import { JWS } from 'jose'; /** * Class containing reusable JWS operations. @@ -37,7 +37,7 @@ export default class Jws { const decodedProtectedHeadJsonString = Encoder.decodeBase64UrlAsString(protectedHeader); const decodedProtectedHeader = JSON.parse(decodedProtectedHeadJsonString); - let expectedHeaderPropertyCount = 1; // By default we must have header property is `alg`. + const expectedHeaderPropertyCount = 1; // By default we must have header property is `alg`. const headerProperties = Object.keys(decodedProtectedHeader); if (headerProperties.length !== expectedHeaderPropertyCount) { diff --git a/lib/index.ts b/lib/index.ts index bf7cb8428..727070e05 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,13 +1,16 @@ // NOTE: Aliases to classes and interfaces are used for external consumption. -// Core service exports. +import ISidetreeBitcoinConfig from './bitcoin/IBitcoinConfig'; +import ISidetreeBitcoinWallet from './bitcoin/interfaces/IBitcoinWallet'; import ISidetreeCas from './core/interfaces/ICas'; -import SidetreeCore from './core/Core'; +import SidetreeBitcoinProcessor from './bitcoin/BitcoinProcessor'; import SidetreeConfig from './core/models/Config'; +import SidetreeCore from './core/Core'; import SidetreeResponse from './common/Response'; import SidetreeResponseModel from './common/models/ResponseModel'; import SidetreeVersionModel from './common/models/VersionModel'; +// Core service exports. export { ISidetreeCas, SidetreeConfig, @@ -18,10 +21,6 @@ export { }; // Blockchain service exports. -import SidetreeBitcoinProcessor from './bitcoin/BitcoinProcessor'; -import ISidetreeBitcoinConfig from './bitcoin/IBitcoinConfig'; -import ISidetreeBitcoinWallet from './bitcoin/interfaces/IBitcoinWallet'; - export { ISidetreeBitcoinConfig, ISidetreeBitcoinWallet, diff --git a/lib/ipfs/Ipfs.ts b/lib/ipfs/Ipfs.ts index 7638643df..0f325dd38 100644 --- a/lib/ipfs/Ipfs.ts +++ b/lib/ipfs/Ipfs.ts @@ -1,16 +1,16 @@ -import * as crypto from 'crypto'; import * as HttpStatus from 'http-status'; +import * as crypto from 'crypto'; import * as url from 'url'; -import base64url from 'base64url'; import FetchResult from '../common/models/FetchResult'; import FetchResultCode from '../common/enums/FetchResultCode'; import ICas from '../core/interfaces/ICas'; import IpfsErrorCode from '../ipfs/IpfsErrorCode'; -import nodeFetch from 'node-fetch'; import ReadableStream from '../common/ReadableStream'; import SharedErrorCode from '../common/SharedErrorCode'; import SidetreeError from '../common/SidetreeError'; import Timeout from './Util/Timeout'; +import base64url from 'base64url'; +import nodeFetch from 'node-fetch'; const multihashes = require('multihashes'); diff --git a/package-lock.json b/package-lock.json index 8518543d6..3fb3efeb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -755,6 +755,63 @@ "dev": true, "optional": true }, + "@eslint/eslintrc": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.1.3.tgz", + "integrity": "sha512-4YVwPkANLeNtRjMekzux1ci8hIaH5eGKktGqR0d3LWsKNn5B2X/1Z6Trxy7jQXl9EBGE6Yj02O+t09FMeRllaA==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "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" + } + }, + "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 + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + } + } + }, "@istanbuljs/load-nyc-config": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz", @@ -911,6 +968,18 @@ "integrity": "sha1-vygstUDprXoKBLdCCCwHO2Veqzk=", "dev": true }, + "@types/json-schema": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", + "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==", + "dev": true + }, + "@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 + }, "@types/minimist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz", @@ -966,6 +1035,143 @@ "integrity": "sha1-IENZZuGHcBj8ie4kaxukF1BxsDs=", "dev": true }, + "@typescript-eslint/eslint-plugin": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.3.0.tgz", + "integrity": "sha512-RqEcaHuEKnn3oPFislZ6TNzsBLqpZjN93G69SS+laav/I8w/iGMuMq97P0D2/2/kW4SCebHggqhbcCfbDaaX+g==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "4.3.0", + "@typescript-eslint/scope-manager": "4.3.0", + "debug": "^4.1.1", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.0.0", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "dependencies": { + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } + } + }, + "@typescript-eslint/experimental-utils": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.3.0.tgz", + "integrity": "sha512-cmmIK8shn3mxmhpKfzMMywqiEheyfXLV/+yPDnOTvQX/ztngx7Lg/OD26J8gTZfkLKUmaEBxO2jYP3keV7h2OQ==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/scope-manager": "4.3.0", + "@typescript-eslint/types": "4.3.0", + "@typescript-eslint/typescript-estree": "4.3.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + } + }, + "@typescript-eslint/parser": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.3.0.tgz", + "integrity": "sha512-JyfRnd72qRuUwItDZ00JNowsSlpQGeKfl9jxwO0FHK1qQ7FbYdoy5S7P+5wh1ISkT2QyAvr2pc9dAemDxzt75g==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "4.3.0", + "@typescript-eslint/types": "4.3.0", + "@typescript-eslint/typescript-estree": "4.3.0", + "debug": "^4.1.1" + } + }, + "@typescript-eslint/scope-manager": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.3.0.tgz", + "integrity": "sha512-cTeyP5SCNE8QBRfc+Lgh4Xpzje46kNUhXYfc3pQWmJif92sjrFuHT9hH4rtOkDTo/si9Klw53yIr+djqGZS1ig==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.3.0", + "@typescript-eslint/visitor-keys": "4.3.0" + } + }, + "@typescript-eslint/types": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.3.0.tgz", + "integrity": "sha512-Cx9TpRvlRjOppGsU6Y6KcJnUDOelja2NNCX6AZwtVHRzaJkdytJWMuYiqi8mS35MRNA3cJSwDzXePfmhU6TANw==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.3.0.tgz", + "integrity": "sha512-ZAI7xjkl+oFdLV/COEz2tAbQbR3XfgqHEGy0rlUXzfGQic6EBCR4s2+WS3cmTPG69aaZckEucBoTxW9PhzHxxw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.3.0", + "@typescript-eslint/visitor-keys": "4.3.0", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "dependencies": { + "globby": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz", + "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + } + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } + } + }, + "@typescript-eslint/visitor-keys": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.3.0.tgz", + "integrity": "sha512-xZxkuR7XLM6RhvLkgv9yYlTcBHnTULzfnw4i6+z2TGBLy9yljAypQaZl9c3zFvy7PNI7fYWyvKYtohyF8au3cw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.3.0", + "eslint-visitor-keys": "^2.0.0" + } + }, "JSONStream": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", @@ -985,6 +1191,18 @@ "negotiator": "0.6.2" } }, + "acorn": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", + "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", + "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", + "dev": true + }, "add-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/add-stream/-/add-stream-1.0.0.tgz", @@ -1001,6 +1219,18 @@ "indent-string": "^4.0.0" } }, + "ajv": { + "version": "6.12.5", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.5.tgz", + "integrity": "sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, "ansi-colors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", @@ -1252,6 +1482,17 @@ "integrity": "sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=", "dev": true }, + "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-initial": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz", @@ -1320,6 +1561,16 @@ "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" }, + "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" + } + }, "arrify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", @@ -1331,6 +1582,12 @@ "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" }, + "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-done": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", @@ -2280,6 +2537,12 @@ "resolved": "https://registry.npmjs.org/connect-livereload/-/connect-livereload-0.6.1.tgz", "integrity": "sha512-3R0kMOdL7CjJpU66fzAkCe6HNtd3AavCS4m+uW4KtJjrdGPT0SQEZieAYd+cm+lJoBznNQ4lqipYWkhBMgk00g==" }, + "contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", + "dev": true + }, "continuable-cache": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/continuable-cache/-/continuable-cache-0.3.1.tgz", @@ -3953,6 +4216,12 @@ "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", "dev": true }, + "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 + }, "default-compare": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", @@ -4219,6 +4488,23 @@ "once": "^1.4.0" } }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + }, + "dependencies": { + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + } + } + }, "entities": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", @@ -4241,22 +4527,74 @@ } }, "es-abstract": { - "version": "1.18.0-next.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", - "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", "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.2", - "is-negative-zero": "^2.0.0", "is-regex": "^1.1.1", "object-inspect": "^1.8.0", "object-keys": "^1.1.1", "object.assign": "^4.1.1", "string.prototype.trimend": "^1.0.1", "string.prototype.trimstart": "^1.0.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "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.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "object.assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz", + "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.0", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "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.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + } } }, "es-to-primitive": { @@ -4285,52 +4623,639 @@ "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "dev": true }, - "es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "requires": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, + "es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "requires": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "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=" + }, + "eslint": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.10.0.tgz", + "integrity": "sha512-BDVffmqWl7JJXqCjAK6lWtcQThZB/aP1HXSH1JKwGwv0LQEdvpR7qzNrUT487RM39B5goWuboFad5ovMBmD8yA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@eslint/eslintrc": "^0.1.3", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^1.3.0", + "espree": "^7.3.0", + "esquery": "^1.2.0", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "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" + } + }, + "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 + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "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" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "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 + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "eslint-config-standard": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-14.1.1.tgz", + "integrity": "sha512-Z9B+VR+JIXRxz21udPTL9HpFMyoMUEeX1G251EQ6e05WD9aPVtVBn09XUmZ259wCMlCDmYDSZG62Hhm+ZTJcUg==", + "dev": true + }, + "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" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "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" + } + } + } + }, + "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" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.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" + } + }, + "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" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "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 + }, + "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" + } + } + } + }, + "eslint-plugin-es": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", + "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", + "dev": true, + "requires": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + } + }, + "eslint-plugin-import": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz", + "integrity": "sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw==", + "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.4", + "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": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "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 + }, + "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" + } + }, + "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" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "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" + } + }, + "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" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } + } + }, + "eslint-plugin-node": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", + "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", + "dev": true, + "requires": { + "eslint-plugin-es": "^3.0.0", + "eslint-utils": "^2.0.0", + "ignore": "^5.1.1", + "minimatch": "^3.0.4", + "resolve": "^1.10.1", + "semver": "^6.1.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "eslint-plugin-prefer-arrow": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-prefer-arrow/-/eslint-plugin-prefer-arrow-1.2.2.tgz", + "integrity": "sha512-C8YMhL+r8RMeMdYAw/rQtE6xNdMulj+zGWud/qIGnlmomiPRaLDGLMeskZ3alN6uMBojmooRimtdrXebLN4svQ==", + "dev": true + }, + "eslint-plugin-promise": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz", + "integrity": "sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw==", + "dev": true + }, + "eslint-plugin-standard": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-4.0.1.tgz", + "integrity": "sha512-v/KBnfyaOMPmZc/dmc6ozOdWqekGp7bBGq4jLAecEfPGmfKiWS4sA8sC0LqiV9w5qmXAtXVn4M3p1jSyhY85SQ==", + "dev": true + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, "requires": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" } }, - "es6-symbol": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", - "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, "requires": { - "d": "^1.0.1", - "ext": "^1.1.2" + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "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 + } } }, - "es6-weak-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", - "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "eslint-visitor-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "dev": true + }, + "espree": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.0.tgz", + "integrity": "sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw==", + "dev": true, "requires": { - "d": "1", - "es5-ext": "^0.10.46", - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.1" + "acorn": "^7.4.0", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.3.0" + }, + "dependencies": { + "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 + } } }, - "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=" - }, "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha1-E7BM2z5sXRnfkatph6hpVhmwqnE=", "dev": true }, + "esquery": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "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.2", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", @@ -4530,6 +5455,12 @@ "time-stamp": "^1.0.0" } }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, "fast-glob": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", @@ -4543,6 +5474,12 @@ "picomatch": "^2.2.1" } }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, "fast-levenshtein": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.1.4.tgz", @@ -4573,6 +5510,15 @@ "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", @@ -4830,6 +5776,23 @@ "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==" }, + "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 + }, "flush-write-stream": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", @@ -5007,6 +5970,12 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "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 + }, "gensync": { "version": "1.0.0-beta.1", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", @@ -6480,6 +7449,12 @@ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", "dev": true }, + "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", @@ -6926,6 +7901,12 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -7027,6 +8008,16 @@ "flush-write-stream": "^1.0.2" } }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, "liftoff": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", @@ -7913,6 +8904,12 @@ "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", @@ -8195,6 +9192,18 @@ "make-iterator": "^1.0.0" } }, + "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", @@ -8220,6 +9229,28 @@ "mimic-fn": "^1.0.0" } }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "dependencies": { + "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 + } + } + }, "ordered-read-streams": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", @@ -8599,6 +9630,12 @@ "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, "pretty-hrtime": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", @@ -8631,6 +9668,12 @@ "fromentries": "^1.2.0" } }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -8662,6 +9705,12 @@ } } }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, "q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", @@ -8935,6 +9984,12 @@ "safe-regex": "^1.1.0" } }, + "regexpp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "dev": true + }, "release-zalgo": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", @@ -9107,6 +10162,15 @@ "integrity": "sha1-jKCMLLtbVedNr6lr9/0aJ9VoyNA=", "dev": 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" + } + }, "run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -9403,6 +10467,43 @@ "integrity": "sha1-3lUoUaF1nfOo8gZTVEL17E3eq0Q=", "dev": true }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "dependencies": { + "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" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + } + } + }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -9805,26 +10906,6 @@ "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", - "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", - "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.2", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } } }, "string.prototype.trimstart": { @@ -9834,26 +10915,6 @@ "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", - "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", - "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.2", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } } }, "string_decoder": { @@ -9927,6 +10988,37 @@ "es6-symbol": "^3.1.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": { + "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 + }, + "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" + } + } + } + }, "temp-dir": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", @@ -9977,6 +11069,12 @@ "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", "dev": true }, + "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", @@ -10166,6 +11264,35 @@ } } }, + "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" + }, + "dependencies": { + "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" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } + } + }, "tslib": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", @@ -10295,6 +11422,15 @@ "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, "type-fest": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", @@ -10436,6 +11572,15 @@ "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==" }, + "uri-js": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", + "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, "urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", @@ -10470,6 +11615,12 @@ "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", "dev": true }, + "v8-compile-cache": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", + "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", + "dev": true + }, "v8flags": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", @@ -10670,6 +11821,15 @@ "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" + } + }, "write-file-atomic": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", diff --git a/package.json b/package.json index 4a7f0e1fe..65b68d451 100644 --- a/package.json +++ b/package.json @@ -31,11 +31,20 @@ "@types/node": "11.13.4", "@types/node-fetch": "2.3.2", "@types/time-span": "2.0.0", + "@typescript-eslint/eslint-plugin": "^4.3.0", + "@typescript-eslint/parser": "^4.3.0", "async-retry": "1.2.3", "conventional-changelog": "3.1.18", "conventional-changelog-cli": "2.0.35", "copyfiles": "2.3.0", "cz-conventional-changelog": "3.1.0", + "eslint": "^7.10.0", + "eslint-config-standard": "^14.1.1", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prefer-arrow": "^1.2.2", + "eslint-plugin-promise": "^4.2.1", + "eslint-plugin-standard": "^4.0.1", "husky": "1.3.1", "jasmine": "^3.6.1", "jasmine-reporters": "2.3.2", @@ -53,15 +62,16 @@ "publish:unstable": "./scripts/publish-unstable.sh", "publish:release": "./scripts/publish.sh", "version:release": "npm version --message \"chore(ref-imp): publish release [skip ci]\"", - "lint": "tslint --fix --project .", + "lint": "eslint --ext ts lib/ tests/", + "lint:fix": "eslint --ext ts lib/ tests/ --fix", "spec": "node -e \"require('spec-up')({ nowatch: true })\"", "spec:edit": "node -e \"require('spec-up')()\"", "version": "conventional-changelog -p angular -i CHANGELOG.md -s && git add CHANGELOG.md" }, "husky": { "hooks": { - "pre-commit": "[[ -n $HUSKY_BYPASS ]] || npm run lint", - "commit-msg": "[[ -n $HUSKY_BYPASS ]] || commitlint -E HUSKY_GIT_PARAMS" + "pre-commit": "npm run lint", + "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" } }, "config": { @@ -80,6 +90,8 @@ "lib/**" ], "exclude": [ + "**/index.d.ts", + "**/index.ts", "lib/bitcoin/versions/[0-9]**/**", "lib/core/versions/[0-9]**/**", "lib/core/versions/**/VersionMetadata.ts", diff --git a/tests/bitcoin/BitcoinClient.spec.ts b/tests/bitcoin/BitcoinClient.spec.ts index 519190991..741a7a72b 100644 --- a/tests/bitcoin/BitcoinClient.spec.ts +++ b/tests/bitcoin/BitcoinClient.spec.ts @@ -1,13 +1,13 @@ import * as httpStatus from 'http-status'; import * as nodeFetchPackage from 'node-fetch'; -import BitcoinDataGenerator from './BitcoinDataGenerator'; +import { Address, PrivateKey, Script, Transaction } from 'bitcore-lib'; import BitcoinClient from '../../lib/bitcoin/BitcoinClient'; +import BitcoinDataGenerator from './BitcoinDataGenerator'; import BitcoinLockTransactionModel from '../../lib/bitcoin/models/BitcoinLockTransactionModel'; import BitcoinTransactionModel from '../../lib/bitcoin/models/BitcoinTransactionModel'; import BitcoinWallet from '../../lib/bitcoin/BitcoinWallet'; import IBitcoinWallet from '../../lib/bitcoin/interfaces/IBitcoinWallet'; import ReadableStream from '../../lib/common/ReadableStream'; -import { Address, PrivateKey, Script, Transaction } from 'bitcore-lib'; describe('BitcoinClient', async () => { @@ -79,6 +79,12 @@ describe('BitcoinClient', async () => { const actual = new BitcoinClient('uri:mock', 'u', 'p', bitcoinWalletImportString, 10, 10, 10); expect(actual['bitcoinWallet']).toEqual(expectedWallet); }); + + it('should use the estimated fee set by the estimatedFeeSatoshiPerKB parameter', () => { + const expectedEstimatedFee = 42; + const actual = new BitcoinClient('uri:mock', 'u', 'p', ctorWallet, 10, 10, 10, expectedEstimatedFee); + expect(actual['estimatedFeeSatoshiPerKB']).toEqual(expectedEstimatedFee); + }); }); describe('createSidetreeTransaction', () => { @@ -103,7 +109,8 @@ describe('BitcoinClient', async () => { expect(result).toEqual({ transactionId: mockSignedTxn.id, transactionFee: mockTxn.getFee(), - serializedTransactionObject: mockSerializedTxn}); + serializedTransactionObject: mockSerializedTxn + }); }); }); @@ -468,14 +475,14 @@ describe('BitcoinClient', async () => { }); }); - describe('getCurrentEstimatedFeeInSatoshisPerKb', () => { + describe('getCurrentEstimatedFeeInSatoshisPerKB', () => { it('should call the correct rpc and return the fee', async () => { const mockFeeInBitcoins = 155; const spy = mockRpcCall('estimatesmartfee', [1], { feerate: mockFeeInBitcoins }); const expectedFeeInSatoshis = mockFeeInBitcoins * 100000000; - const actual = await bitcoinClient['getCurrentEstimatedFeeInSatoshisPerKb'](); + const actual = await bitcoinClient['getCurrentEstimatedFeeInSatoshisPerKB'](); expect(actual).toEqual(expectedFeeInSatoshis); expect(spy).toHaveBeenCalled(); }); @@ -484,7 +491,7 @@ describe('BitcoinClient', async () => { const spy = mockRpcCall('estimatesmartfee', [1], { }); try { - await bitcoinClient['getCurrentEstimatedFeeInSatoshisPerKb'](); + await bitcoinClient['getCurrentEstimatedFeeInSatoshisPerKB'](); fail('should have thrown'); } catch (error) { expect(spy).toHaveBeenCalled(); @@ -496,7 +503,7 @@ describe('BitcoinClient', async () => { const spy = mockRpcCall('estimatesmartfee', [1], { feerate: 1, errors: ['some error'] }); try { - await bitcoinClient['getCurrentEstimatedFeeInSatoshisPerKb'](); + await bitcoinClient['getCurrentEstimatedFeeInSatoshisPerKB'](); fail('should have thrown'); } catch (error) { expect(spy).toHaveBeenCalled(); @@ -505,6 +512,47 @@ describe('BitcoinClient', async () => { }); }); + describe('updateEstimatedFeeInSatoshisPerKB', () => { + it('should always call the correct rpc and return the updated fee', async () => { + const mockFeeInBitcoins = 156; + const expectedFeeInSatoshis = mockFeeInBitcoins * 100000000; + const spy = spyOn(bitcoinClient as any, 'getCurrentEstimatedFeeInSatoshisPerKB').and.returnValue(expectedFeeInSatoshis); + + const actual = await bitcoinClient['updateEstimatedFeeInSatoshisPerKB'](); + expect(actual).toEqual(expectedFeeInSatoshis); + expect(bitcoinClient['estimatedFeeSatoshiPerKB']).toEqual(expectedFeeInSatoshis); + expect(spy).toHaveBeenCalled(); + }); + + it('should always call the correct rpc and return the stored fee on error', async () => { + const mockFeeInBitcoins = 157; + const spy = spyOn(bitcoinClient as any, 'getCurrentEstimatedFeeInSatoshisPerKB'); + spy.and.throwError('test'); + + const expectedFeeInSatoshis = mockFeeInBitcoins * 100000000; + bitcoinClient['estimatedFeeSatoshiPerKB'] = expectedFeeInSatoshis; + + const actual = await bitcoinClient['updateEstimatedFeeInSatoshisPerKB'](); + expect(actual).toEqual(expectedFeeInSatoshis); + expect(bitcoinClient['estimatedFeeSatoshiPerKB']).toEqual(expectedFeeInSatoshis); + expect(spy).toHaveBeenCalled(); + }); + + it('should rethrow RPC error when no fee was stored', async (done) => { + const spy = spyOn(bitcoinClient as any, 'getCurrentEstimatedFeeInSatoshisPerKB'); + spy.and.throwError('test'); + + try { + await bitcoinClient['updateEstimatedFeeInSatoshisPerKB'](); + fail('should have thrown'); + } catch { + expect(spy).toHaveBeenCalled(); + } finally { + done(); + } + }); + }); + describe('getTransactionOutValueInSatoshi', () => { it('should return the satoshis from the correct output index.', async () => { const mockTxnWithMultipleOutputs: BitcoinTransactionModel = { @@ -729,7 +777,7 @@ describe('BitcoinClient', async () => { describe('calculateTransactionFee', () => { it('should calculate the fee correctly', async () => { const estimatedFee = 1528; - spyOn(bitcoinClient as any, 'getCurrentEstimatedFeeInSatoshisPerKb').and.returnValue(estimatedFee); + spyOn(bitcoinClient as any, 'getCurrentEstimatedFeeInSatoshisPerKB').and.returnValue(estimatedFee); const mockTransaction = BitcoinDataGenerator.generateBitcoinTransaction(bitcoinWalletImportString, 10000); @@ -739,6 +787,38 @@ describe('BitcoinClient', async () => { expect(expectedFee).toEqual(actualFee); }); + + it('should throw if no stored estimate and the fee estimate throws', async (done) => { + const spy = spyOn(bitcoinClient as any, 'getCurrentEstimatedFeeInSatoshisPerKB'); + spy.and.throwError('test'); + + const mockTransaction = BitcoinDataGenerator.generateBitcoinTransaction(bitcoinWalletImportString, 10000); + + try { + await bitcoinClient['calculateTransactionFee'](mockTransaction); + fail('should have thrown'); + } catch { + expect(spy).toHaveBeenCalled(); + } finally { + done(); + } + }); + + it('should use stored estimate, if estimate fails', async () => { + const estimatedFee = 1529; + bitcoinClient['estimatedFeeSatoshiPerKB'] = estimatedFee; + const spy = spyOn(bitcoinClient as any, 'getCurrentEstimatedFeeInSatoshisPerKB'); + spy.and.throwError('test'); + + const mockTransaction = BitcoinDataGenerator.generateBitcoinTransaction(bitcoinWalletImportString, 10000); + + const expectedFee = 108; + + const actualFee = await bitcoinClient['calculateTransactionFee'](mockTransaction); + + expect(expectedFee).toEqual(actualFee); + expect(spy).toHaveBeenCalled(); + }); }); describe('createFreezeTransaction', () => { @@ -791,7 +871,6 @@ describe('BitcoinClient', async () => { const createScriptSpy = spyOn(BitcoinClient as any, 'createFreezeScript').and.returnValue(mockRedeemScript); const utilFuncSpy = spyOn(bitcoinClient as any, 'createSpendTransactionFromFrozenTransaction').and.returnValue(mockFreezeTxn2); - // tslint:disable-next-line: max-line-length const [actualTxn, redeemScript] = await bitcoinClient['createSpendToFreezeTransaction'](mockFreezeTxn1, mockFreezeUntilPreviousBlock, mockFreezeUntilBlock); expect(actualTxn).toEqual(mockFreezeTxn2); expect(redeemScript).toEqual(mockRedeemScript); @@ -1023,7 +1102,7 @@ describe('BitcoinClient', async () => { it('should throw if the request failed', async (done) => { const request: any = { - 'test': 'some random string' + test: 'some random string' }; const result = 'some result'; const statusCode = 7890; @@ -1058,7 +1137,7 @@ describe('BitcoinClient', async () => { it('should throw if the RPC call failed', async (done) => { const request: any = { - 'test': 'some request value' + test: 'some request value' }; const result = 'some result'; diff --git a/tests/bitcoin/BitcoinDataGenerator.ts b/tests/bitcoin/BitcoinDataGenerator.ts index 182d79d46..283c61090 100644 --- a/tests/bitcoin/BitcoinDataGenerator.ts +++ b/tests/bitcoin/BitcoinDataGenerator.ts @@ -1,6 +1,6 @@ +import { PrivateKey, Transaction } from 'bitcore-lib'; import BitcoinBlockModel from '../../lib/bitcoin/models/BitcoinBlockModel'; import BitcoinClient from '../../lib/bitcoin/BitcoinClient'; -import { PrivateKey, Transaction } from 'bitcore-lib'; /** * Encapsulates the functions that help with generating the test data for the Bitcoin blockchain. diff --git a/tests/bitcoin/BitcoinFileReader.spec.ts b/tests/bitcoin/BitcoinFileReader.spec.ts index 4a8524f01..99d3db2cd 100644 --- a/tests/bitcoin/BitcoinFileReader.spec.ts +++ b/tests/bitcoin/BitcoinFileReader.spec.ts @@ -1,7 +1,7 @@ +import * as fs from 'fs'; import BitcoinFileReader from '../../lib/bitcoin/BitcoinFileReader'; import ErrorCode from '../../lib/bitcoin/ErrorCode'; import SidetreeError from '../../lib/common/SidetreeError'; -import * as fs from 'fs'; describe('BitcoinFileReader', () => { let bitcoinFileReader: BitcoinFileReader; diff --git a/tests/bitcoin/BitcoinProcessor.spec.ts b/tests/bitcoin/BitcoinProcessor.spec.ts index bed047337..719b6333a 100644 --- a/tests/bitcoin/BitcoinProcessor.spec.ts +++ b/tests/bitcoin/BitcoinProcessor.spec.ts @@ -1,9 +1,9 @@ import * as fs from 'fs'; import * as httpStatus from 'http-status'; +import BitcoinProcessor, { IBlockInfo } from '../../lib/bitcoin/BitcoinProcessor'; import BitcoinBlockModel from '../../lib/bitcoin/models/BitcoinBlockModel'; import BitcoinClient from '../../lib/bitcoin/BitcoinClient'; import BitcoinDataGenerator from './BitcoinDataGenerator'; -import BitcoinProcessor, { IBlockInfo } from '../../lib/bitcoin/BitcoinProcessor'; import BitcoinRawDataParser from '../../lib/bitcoin/BitcoinRawDataParser'; import BitcoinTransactionModel from '../../lib/bitcoin/models/BitcoinTransactionModel'; import BlockMetadata from '../../lib/bitcoin/models/BlockMetadata'; @@ -14,9 +14,9 @@ import JasmineSidetreeErrorValidator from '../JasmineSidetreeErrorValidator'; import RequestError from '../../lib/bitcoin/RequestError'; import ResponseStatus from '../../lib/common/enums/ResponseStatus'; import ServiceVersionModel from '../../lib/common/models/ServiceVersionModel'; +import SharedErrorCode from '../../lib/common/SharedErrorCode'; import SidetreeError from '../../lib/common/SidetreeError'; import SidetreeTransactionModel from '../../lib/bitcoin/models/SidetreeTransactionModel'; -import SharedErrorCode from '../../lib/common/SharedErrorCode'; import TransactionFeeModel from '../../lib/common/models/TransactionFeeModel'; import TransactionModel from '../../lib/common/models/TransactionModel'; import TransactionNumber from '../../lib/bitcoin/TransactionNumber'; @@ -49,14 +49,16 @@ describe('BitcoinProcessor', () => { bitcoinRpcPassword: '123456789', bitcoinWalletOrImportString: BitcoinClient.generatePrivateKey('testnet'), databaseName: 'bitcoin-test', - requestTimeoutInMilliseconds: 300, + defaultTransactionFeeInSatoshisPerKB: undefined, genesisBlockNumber: 1480000, lowBalanceNoticeInDays: 28, - requestMaxRetries: 3, mongoDbConnectionString: 'mongodb://localhost:27017', + requestMaxRetries: 3, + requestTimeoutInMilliseconds: 300, sidetreeTransactionPrefix: 'sidetree:', - transactionPollPeriodInSeconds: 60, sidetreeTransactionFeeMarkupPercentage: 0, + transactionPollPeriodInSeconds: 60, + valueTimeLockUpdateEnabled: true, valueTimeLockPollPeriodInSeconds: 60, valueTimeLockAmountInBitcoins: 1, valueTimeLockTransactionFeesAmountInBitcoins: undefined @@ -89,7 +91,7 @@ describe('BitcoinProcessor', () => { transactionStoreInitializeSpy = spyOn(bitcoinProcessor['transactionStore'], 'initialize'); bitcoinClientInitializeSpy = spyOn(bitcoinProcessor['bitcoinClient'], 'initialize'); mongoLockTxnStoreSpy = spyOn(bitcoinProcessor['mongoDbLockTransactionStore'], 'initialize'); - lockMonitorSpy = spyOn(bitcoinProcessor['lockMonitor'], 'initialize'); + lockMonitorSpy = spyOn(bitcoinProcessor['lockMonitor']!, 'startPeriodicProcessing'); blockMetadataStoreAddSpy = spyOn(bitcoinProcessor['blockMetadataStore'], 'add'); blockMetadataStoreGetLastSpy = spyOn(bitcoinProcessor['blockMetadataStore'], 'getLast'); @@ -144,6 +146,7 @@ describe('BitcoinProcessor', () => { bitcoinRpcPassword: 'password123', bitcoinWalletOrImportString: BitcoinClient.generatePrivateKey('testnet'), databaseName: randomString(), + defaultTransactionFeeInSatoshisPerKB: 42, genesisBlockNumber: randomNumber(), mongoDbConnectionString: randomString(), sidetreeTransactionPrefix: randomString(4), @@ -152,6 +155,7 @@ describe('BitcoinProcessor', () => { requestMaxRetries: undefined, transactionPollPeriodInSeconds: undefined, sidetreeTransactionFeeMarkupPercentage: 0, + valueTimeLockUpdateEnabled: true, valueTimeLockPollPeriodInSeconds: 60, valueTimeLockAmountInBitcoins: 1, valueTimeLockTransactionFeesAmountInBitcoins: undefined @@ -165,6 +169,7 @@ describe('BitcoinProcessor', () => { expect(bitcoinProcessor['transactionStore'].databaseName).toEqual(config.databaseName); expect(bitcoinProcessor['transactionStore']['serverUrl']).toEqual(config.mongoDbConnectionString); expect(bitcoinProcessor['bitcoinClient']['sidetreeTransactionFeeMarkupPercentage']).toEqual(0); + expect(bitcoinProcessor['bitcoinClient']['estimatedFeeSatoshiPerKB']).toEqual(42); }); }); @@ -311,9 +316,9 @@ describe('BitcoinProcessor', () => { }; // return as many as page size const transactions: TransactionModel[] = createTransactions(BitcoinProcessor['pageSizeInBlocks'], bitcoinProcessor['genesisBlockNumber'], true); - const laterThanMock = spyOn(bitcoinProcessor['transactionStore'], 'getTransactionsStartingFrom').and.callFake((() => { + const laterThanMock = spyOn(bitcoinProcessor['transactionStore'], 'getTransactionsStartingFrom').and.callFake(() => { return Promise.resolve(transactions); - })); + }); const actual = await bitcoinProcessor.transactions(); expect(verifyMock).toHaveBeenCalledTimes(1); // called after data was retrieved @@ -333,9 +338,9 @@ describe('BitcoinProcessor', () => { hash: 'some hash', previousHash: 'previous hash' }; - const laterThanMock = spyOn(bitcoinProcessor['transactionStore'], 'getTransactionsStartingFrom').and.callFake((() => { + const laterThanMock = spyOn(bitcoinProcessor['transactionStore'], 'getTransactionsStartingFrom').and.callFake(() => { return Promise.resolve(transactions); - })); + }); const actual = await bitcoinProcessor.transactions(); expect(verifyMock).toHaveBeenCalledTimes(1); @@ -355,9 +360,9 @@ describe('BitcoinProcessor', () => { hash: 'some hash', previousHash: 'previous hash' }; - const laterThanMock = spyOn(bitcoinProcessor['transactionStore'], 'getTransactionsStartingFrom').and.callFake((() => { + const laterThanMock = spyOn(bitcoinProcessor['transactionStore'], 'getTransactionsStartingFrom').and.callFake(() => { return Promise.resolve(transactions); - })); + }); const actual = await bitcoinProcessor.transactions(); expect(verifyMock).toHaveBeenCalledTimes(1); @@ -377,9 +382,9 @@ describe('BitcoinProcessor', () => { hash: 'some hash', previousHash: 'previous hash' }; - const laterThanMock = spyOn(bitcoinProcessor['transactionStore'], 'getTransactionsStartingFrom').and.callFake((() => { + const laterThanMock = spyOn(bitcoinProcessor['transactionStore'], 'getTransactionsStartingFrom').and.callFake(() => { return Promise.resolve(transactions); - })); + }); const actual = await bitcoinProcessor.transactions(); expect(verifyMock).toHaveBeenCalledTimes(1); @@ -398,9 +403,9 @@ describe('BitcoinProcessor', () => { hash: 'some hash', previousHash: 'previous hash' }; - const laterThanMock = spyOn(bitcoinProcessor['transactionStore'], 'getTransactionsStartingFrom').and.callFake(((begin) => { + const laterThanMock = spyOn(bitcoinProcessor['transactionStore'], 'getTransactionsStartingFrom').and.callFake((begin) => { return Promise.resolve(createTransactions(10, begin, false)); - })); + }); const actual = await bitcoinProcessor.transactions(); expect(verifyMock).toHaveBeenCalledTimes(1); @@ -419,9 +424,9 @@ describe('BitcoinProcessor', () => { hash: 'some hash', previousHash: 'previous hash' }; - const laterThanMock = spyOn(bitcoinProcessor['transactionStore'], 'getTransactionsStartingFrom').and.callFake((() => { + const laterThanMock = spyOn(bitcoinProcessor['transactionStore'], 'getTransactionsStartingFrom').and.callFake(() => { return Promise.resolve([]); - })); + }); const actual = await bitcoinProcessor.transactions(); expect(verifyMock).toHaveBeenCalledTimes(1); @@ -448,9 +453,9 @@ describe('BitcoinProcessor', () => { return Promise.resolve(true); }); const transactions = createTransactions(BitcoinProcessor['pageSizeInBlocks'], expectedHeight, true); - const laterThanMock = spyOn(bitcoinProcessor['transactionStore'], 'getTransactionsStartingFrom').and.callFake((() => { + const laterThanMock = spyOn(bitcoinProcessor['transactionStore'], 'getTransactionsStartingFrom').and.callFake(() => { return Promise.resolve(transactions); - })); + }); const actual = await bitcoinProcessor.transactions(expectedTransactionNumber, expectedHash); expect(verifyMock).toHaveBeenCalledTimes(2); @@ -561,7 +566,7 @@ describe('BitcoinProcessor', () => { describe('firstValidTransaction', () => { it('should return the first of the valid transactions', async (done) => { const transactions: TransactionModel[] = []; - let heights: number[] = []; + const heights: number[] = []; const count = 10; for (let i = 0; i < count; i++) { const height = randomNumber(); @@ -628,7 +633,7 @@ describe('BitcoinProcessor', () => { it('should warn if the number of Satoshis are under the lowBalance calculation', async (done) => { const monitorAddSpy = spyOn(bitcoinProcessor['spendingMonitor'], 'addTransactionDataBeingWritten'); const getCoinsSpy = spyOn(bitcoinProcessor['bitcoinClient'], 'getBalanceInSatoshis').and.returnValue(Promise.resolve(lowLevelWarning - 1)); - spyOn(bitcoinProcessor['spendingMonitor'],'isCurrentFeeWithinSpendingLimit').and.returnValue(Promise.resolve(true)); + spyOn(bitcoinProcessor['spendingMonitor'], 'isCurrentFeeWithinSpendingLimit').and.returnValue(Promise.resolve(true)); const hash = randomString(); const broadcastSpy = spyOn(bitcoinProcessor['bitcoinClient'], 'broadcastSidetreeTransaction' as any).and.returnValue(Promise.resolve('someHash')); spyOn(bitcoinProcessor['bitcoinClient'], 'createSidetreeTransaction').and.returnValue(Promise.resolve({ @@ -860,9 +865,9 @@ describe('BitcoinProcessor', () => { it('should process as intended', async () => { const processSidetreeTransactionsInBlockSpy = spyOn(bitcoinProcessor, 'processSidetreeTransactionsInBlock' as any); const blockData: any[] = [ - { hash: 'abc', height: 2, previousHash: 'def', transactions: [ { outputs: [{ satoshis: 100 }, { satoshis: 5000000000 }, { satoshis: 50 }] }] }, - { hash: 'def', height: 1, previousHash: 'out of range', transactions: [ { outputs: [{ satoshis: 5000000000 }] }] }, - { hash: 'ghi', height: 4, previousHash: 'out of range', transactions: [ { outputs: [{ satoshis: 5000000000 }] }] } + { hash: 'abc', height: 2, previousHash: 'def', transactions: [{ outputs: [{ satoshis: 100 }, { satoshis: 5000000000 }, { satoshis: 50 }] }] }, + { hash: 'def', height: 1, previousHash: 'out of range', transactions: [{ outputs: [{ satoshis: 5000000000 }] }] }, + { hash: 'ghi', height: 4, previousHash: 'out of range', transactions: [{ outputs: [{ satoshis: 5000000000 }] }] } ]; const notYetValidatedBlocks: Map = new Map(); const startingHeight = 2; @@ -941,9 +946,9 @@ describe('BitcoinProcessor', () => { it('should process transactions as intended', async () => { // this is the end to end test const startBlock = randomBlock(testConfig.genesisBlockNumber); - const getCurrentHeightMock = spyOn(bitcoinProcessor['bitcoinClient'],'getCurrentBlockHeight').and.returnValue(Promise.resolve(startBlock.height + 1)); - const getCurrentHashMock = spyOn(bitcoinProcessor['bitcoinClient'],'getBlockInfoFromHeight') - .and.returnValue(Promise.resolve({ hash: 'hash2', height: startBlock.height + 1, previousHash: 'hash1' })); + const getCurrentHeightMock = spyOn(bitcoinProcessor['bitcoinClient'], 'getCurrentBlockHeight').and.returnValue(Promise.resolve(startBlock.height + 1)); + const getCurrentHashMock = spyOn(bitcoinProcessor['bitcoinClient'], 'getBlockInfoFromHeight') + .and.returnValue(Promise.resolve({ hash: 'hash2', height: startBlock.height + 1, previousHash: 'hash1' })); const fsReaddirSyncSpy = spyOn(fs, 'readdirSync').and.returnValue(['blk001.dat' as any]); const fsReadFileSyncSpy = spyOn(fs, 'readFileSync').and.returnValue(Buffer.from('someBuffer')); const processSidetreeTransactionsInBlockSpy = spyOn(bitcoinProcessor, 'processSidetreeTransactionsInBlock' as any); @@ -999,7 +1004,7 @@ describe('BitcoinProcessor', () => { const mockProcessedBlockMetadata = BlockMetadataGenerator.generate(1)[0]; const startBlock = randomBlock(testConfig.genesisBlockNumber); const processMock = spyOn(bitcoinProcessor, 'processBlock' as any).and.returnValue(Promise.resolve(mockProcessedBlockMetadata)); - const getCurrentHeightMock = spyOn(bitcoinProcessor['bitcoinClient'],'getCurrentBlockHeight').and.returnValue(Promise.resolve(startBlock.height + 1)); + const getCurrentHeightMock = spyOn(bitcoinProcessor['bitcoinClient'], 'getCurrentBlockHeight').and.returnValue(Promise.resolve(startBlock.height + 1)); await bitcoinProcessor['processTransactions'](startBlock); @@ -1014,7 +1019,7 @@ describe('BitcoinProcessor', () => { const mockProcessedBlockMetadata = BlockMetadataGenerator.generate(1)[0]; const startBlock = randomBlock(testConfig.genesisBlockNumber); const processMock = spyOn(bitcoinProcessor, 'processBlock' as any).and.returnValue(Promise.resolve(mockProcessedBlockMetadata)); - const getCurrentHeightMock = spyOn(bitcoinProcessor['bitcoinClient'],'getCurrentBlockHeight').and.returnValue(Promise.resolve(startBlock.height + 9)); + const getCurrentHeightMock = spyOn(bitcoinProcessor['bitcoinClient'], 'getCurrentBlockHeight').and.returnValue(Promise.resolve(startBlock.height + 9)); await bitcoinProcessor['processTransactions'](startBlock); expect(bitcoinProcessor['lastProcessedBlock']).toBeDefined(); @@ -1026,7 +1031,6 @@ describe('BitcoinProcessor', () => { }); it('should throw if asked to start processing before genesis', async (done) => { - // tslint:disable-next-line: max-line-length const tipSpy = spyOn(bitcoinProcessor['bitcoinClient'], 'getCurrentBlockHeight' as any).and.returnValue(Promise.resolve(testConfig.genesisBlockNumber + 1)); const processMock = spyOn(bitcoinProcessor, 'processBlock' as any); @@ -1133,8 +1137,8 @@ describe('BitcoinProcessor', () => { revertDatabaseSpy.and.returnValue(Promise.resolve(undefined)); // Simulate that current height in bitcoin core is ahead than the desired starting block. - const getCurrentBlockHeightSpy - = spyOn(bitcoinProcessor['bitcoinClient'], 'getCurrentBlockHeight').and.returnValue(Promise.resolve(Number.MAX_SAFE_INTEGER)); + const getCurrentBlockHeightSpy = + spyOn(bitcoinProcessor['bitcoinClient'], 'getCurrentBlockHeight').and.returnValue(Promise.resolve(Number.MAX_SAFE_INTEGER)); await bitcoinProcessor['getStartingBlockForPeriodicPoll'](); @@ -1281,17 +1285,15 @@ describe('BitcoinProcessor', () => { spyOn(bitcoinProcessor['bitcoinClient'], 'getBlock').and.returnValue(Promise.resolve(blockData)); const mockSidetreeTxnModels: TransactionModel[] = [ - // tslint:disable-next-line: max-line-length { anchorString: 'anchor1', transactionTimeHash: 'timehash1', transactionTime: 100, transactionNumber: 200, transactionFeePaid: 300, normalizedTransactionFee: 400, writer: 'writer1' }, - // tslint:disable-next-line: max-line-length { anchorString: 'anchor2', transactionTimeHash: 'timehash2', transactionTime: 150, transactionNumber: 250, transactionFeePaid: 350, normalizedTransactionFee: 450, writer: 'writer2' } ]; // Return the mock values one-by-one in order let getSidetreeTxnCallIndex = 0; - spyOn(bitcoinProcessor as any,'getSidetreeTransactionModelIfExist').and.callFake(() => { + spyOn(bitcoinProcessor as any, 'getSidetreeTransactionModelIfExist').and.callFake(() => { - let retValue: TransactionModel | undefined = undefined; + let retValue: TransactionModel | undefined; if (getSidetreeTxnCallIndex < mockSidetreeTxnModels.length) { retValue = mockSidetreeTxnModels[getSidetreeTxnCallIndex]; @@ -1333,7 +1335,7 @@ describe('BitcoinProcessor', () => { spyOn(bitcoinProcessor['bitcoinClient'], 'getBlockHash' as any).and.returnValue(blockHash); spyOn(bitcoinProcessor['bitcoinClient'], 'getBlock').and.returnValue(Promise.resolve(blockData)); - spyOn(bitcoinProcessor as any,'getSidetreeTransactionModelIfExist').and.returnValue(undefined); + spyOn(bitcoinProcessor as any, 'getSidetreeTransactionModelIfExist').and.returnValue(undefined); const addTransactionSpy = spyOn(bitcoinProcessor['transactionStore'], 'addTransaction'); @@ -1359,7 +1361,7 @@ describe('BitcoinProcessor', () => { spyOn(bitcoinProcessor['bitcoinClient'], 'getBlockHash' as any).and.returnValue(blockHash); spyOn(bitcoinProcessor['bitcoinClient'], 'getBlock').and.returnValue(Promise.resolve(blockData)); - spyOn(bitcoinProcessor as any,'getSidetreeTransactionModelIfExist').and.throwError('Test exception'); + spyOn(bitcoinProcessor as any, 'getSidetreeTransactionModelIfExist').and.throwError('Test exception'); const addTransaction = spyOn(bitcoinProcessor['transactionStore'], 'addTransaction'); @@ -1523,7 +1525,7 @@ describe('BitcoinProcessor', () => { }); describe('getActiveValueTimeLockForThisNode', () => { - it('should return the value-time-lock from the lockmonitor', () => { + it('should return the value-time-lock from the lock monitor', async () => { const mockValueTimeLock: ValueTimeLockModel = { amountLocked: 1000, identifier: 'lock identifier', @@ -1533,17 +1535,17 @@ describe('BitcoinProcessor', () => { lockTransactionTime: 1220 }; - spyOn(bitcoinProcessor['lockMonitor'], 'getCurrentValueTimeLock').and.returnValue(mockValueTimeLock); + spyOn(bitcoinProcessor['lockMonitor']!, 'getCurrentValueTimeLock').and.returnValue(Promise.resolve(mockValueTimeLock)); - const actual = bitcoinProcessor.getActiveValueTimeLockForThisNode(); + const actual = await bitcoinProcessor.getActiveValueTimeLockForThisNode(); expect(actual).toEqual(mockValueTimeLock); }); - it('should throw not-found error if the lock monitor returns undefined.', () => { - spyOn(bitcoinProcessor['lockMonitor'], 'getCurrentValueTimeLock').and.returnValue(undefined); + it('should throw not-found error if the lock monitor returns undefined.', async () => { + spyOn(bitcoinProcessor['lockMonitor']!, 'getCurrentValueTimeLock').and.returnValue(Promise.resolve(undefined)); try { - bitcoinProcessor.getActiveValueTimeLockForThisNode(); + await bitcoinProcessor.getActiveValueTimeLockForThisNode(); fail('Expected exception is not thrown'); } catch (e) { const expectedError = new RequestError(ResponseStatus.NotFound, SharedErrorCode.ValueTimeLockNotFound); @@ -1551,13 +1553,13 @@ describe('BitcoinProcessor', () => { } }); - it('should throw pending-state exception if the lock monitor throws pending-state error', () => { - spyOn(bitcoinProcessor['lockMonitor'], 'getCurrentValueTimeLock').and.callFake(() => { + it('should throw pending-state exception if the lock monitor throws pending-state error', async () => { + spyOn(bitcoinProcessor['lockMonitor']!, 'getCurrentValueTimeLock').and.callFake(() => { throw new SidetreeError(ErrorCode.LockMonitorCurrentValueTimeLockInPendingState); }); try { - bitcoinProcessor.getActiveValueTimeLockForThisNode(); + await bitcoinProcessor.getActiveValueTimeLockForThisNode(); fail('Expected exception is not thrown'); } catch (e) { const expectedError = new RequestError(ResponseStatus.NotFound, ErrorCode.ValueTimeLockInPendingState); @@ -1565,11 +1567,11 @@ describe('BitcoinProcessor', () => { } }); - it('should bubble up any other errors.', () => { - spyOn(bitcoinProcessor['lockMonitor'], 'getCurrentValueTimeLock').and.throwError('no lock found.'); + it('should bubble up any other errors.', async () => { + spyOn(bitcoinProcessor['lockMonitor']!, 'getCurrentValueTimeLock').and.throwError('no lock found.'); try { - bitcoinProcessor.getActiveValueTimeLockForThisNode(); + await bitcoinProcessor.getActiveValueTimeLockForThisNode(); fail('Expected exception is not thrown'); } catch (e) { const expectedError = new RequestError(ResponseStatus.ServerError); diff --git a/tests/bitcoin/BitcoinRawDataParser.spec.ts b/tests/bitcoin/BitcoinRawDataParser.spec.ts index 3dc254936..62b017b67 100644 --- a/tests/bitcoin/BitcoinRawDataParser.spec.ts +++ b/tests/bitcoin/BitcoinRawDataParser.spec.ts @@ -1,7 +1,7 @@ +import * as fs from 'fs'; import BitcoinRawDataParser from '../../lib/bitcoin/BitcoinRawDataParser'; import ErrorCode from '../../lib/bitcoin/ErrorCode'; import SidetreeError from '../../lib/common/SidetreeError'; -import * as fs from 'fs'; describe('BitcoinRawDataParser', () => { describe('parseRawDataFile', () => { diff --git a/tests/bitcoin/MongoDbBlockMetadataStore.spec.ts b/tests/bitcoin/MongoDbBlockMetadataStore.spec.ts index e21b8bbc4..b186e8423 100644 --- a/tests/bitcoin/MongoDbBlockMetadataStore.spec.ts +++ b/tests/bitcoin/MongoDbBlockMetadataStore.spec.ts @@ -1,9 +1,9 @@ import BlockMetadata from '../../lib/bitcoin/models/BlockMetadata'; import BlockMetadataGenerator from '../generators/BlockMetadataGenerator'; import Config from '../../lib/core/models/Config'; +import { MongoClient } from 'mongodb'; import MongoDb from '../common/MongoDb'; import MongoDbBlockMetadataStore from '../../lib/bitcoin/MongoDbBlockMetadataStore'; -import { MongoClient } from 'mongodb'; /** * Creates a MongoDbBlockMetadataStore and initializes it. @@ -15,7 +15,7 @@ async function createBlockMetadataStore (storeUri: string, databaseName: string) } describe('MongoDbBlockMetadataStore', async () => { - const config: Config = require('../json/bitcoin-config-test.json'); + const config: Config = require('../json/config-test.json'); const databaseName = 'sidetree-test'; let mongoServiceAvailable = false; diff --git a/tests/bitcoin/MongoDbServiceStateStore.spec.ts b/tests/bitcoin/MongoDbServiceStateStore.spec.ts index 1d9f4019f..1dc94ca95 100644 --- a/tests/bitcoin/MongoDbServiceStateStore.spec.ts +++ b/tests/bitcoin/MongoDbServiceStateStore.spec.ts @@ -14,7 +14,7 @@ async function createStore (storeUri: string, databaseName: string): Promise { - const config: Config = require('../json/bitcoin-config-test.json'); + const config: Config = require('../json/config-test.json'); const databaseName = 'sidetree-test'; let mongoServiceAvailable = false; diff --git a/tests/bitcoin/SidetreeTransactionParser.spec.ts b/tests/bitcoin/SidetreeTransactionParser.spec.ts index 3917330e2..bdcabfbd7 100644 --- a/tests/bitcoin/SidetreeTransactionParser.spec.ts +++ b/tests/bitcoin/SidetreeTransactionParser.spec.ts @@ -158,7 +158,7 @@ describe('SidetreeTransactionParser', () => { }); it('should return undefined if no valid sidetree transaction exist', async (done) => { - const mockOutput: BitcoinOutputModel = { satoshis: 0, scriptAsmAsString: 'some random data' }; + const mockOutput: BitcoinOutputModel = { satoshis: 0, scriptAsmAsString: 'some random data' }; const actual = sidetreeTxnParser['getSidetreeDataFromOutputIfExist'](mockOutput, sidetreeTransactionPrefix); expect(actual).not.toBeDefined(); diff --git a/tests/bitcoin/SpendingMonitor.spec.ts b/tests/bitcoin/SpendingMonitor.spec.ts index 6758e2cf2..a6f212f5a 100644 --- a/tests/bitcoin/SpendingMonitor.spec.ts +++ b/tests/bitcoin/SpendingMonitor.spec.ts @@ -11,9 +11,7 @@ describe('SpendingMonitor', () => { const bitcoinFeeSpendingCutoffPeriodInBlocks = 100; const bitcoinFeeSpendingCutoffInSatoshis = BitcoinClient.convertBtcToSatoshis(3); // 3 * BitcoinProcessor['satoshiPerBitcoin']; const mockTxns: TransactionModel[] = [ - // tslint:disable-next-line: max-line-length { transactionNumber: 12345, transactionTime: 10, transactionTimeHash: 'hash1', anchorString: 'anchor_string1', transactionFeePaid: 100, normalizedTransactionFee: 90, writer: 'writer1' }, - // tslint:disable-next-line: max-line-length { transactionNumber: 67890, transactionTime: 11, transactionTimeHash: 'hash2', anchorString: 'anchor_string2', transactionFeePaid: 110, normalizedTransactionFee: 95, writer: 'writer2' } ]; @@ -24,7 +22,7 @@ describe('SpendingMonitor', () => { describe('constructor', () => { it('should throw if the cutoff period is not in the correct range.', async (done) => { try { - // tslint:disable-next-line: no-unused-expression + // eslint-disable-next-line no-new new SpendingMonitor(0, bitcoinFeeSpendingCutoffInSatoshis, new MockTransactionStore()); fail('Expected exception not thrown'); } catch (e) { @@ -35,7 +33,7 @@ describe('SpendingMonitor', () => { it('should throw if the cutoff amount is not in the correct range.', async (done) => { try { - // tslint:disable-next-line: no-unused-expression + // eslint-disable-next-line no-new new SpendingMonitor(1, 0, new MockTransactionStore()); fail('Expected exception not thrown'); } catch (e) { diff --git a/tests/bitcoin/lock/LockIdentifierSerializer.spec.ts b/tests/bitcoin/lock/LockIdentifierSerializer.spec.ts index 595ab20d8..a56e56963 100644 --- a/tests/bitcoin/lock/LockIdentifierSerializer.spec.ts +++ b/tests/bitcoin/lock/LockIdentifierSerializer.spec.ts @@ -1,8 +1,8 @@ -import base64url from 'base64url'; import ErrorCode from '../../../lib/bitcoin/ErrorCode'; import JasmineSidetreeErrorValidator from '../../JasmineSidetreeErrorValidator'; import LockIdentifier from '../../../lib/bitcoin/models/LockIdentifierModel'; import LockIdentifierSerializer from '../../../lib/bitcoin/lock/LockIdentifierSerializer'; +import base64url from 'base64url'; describe('LockIdentifierSerializer', () => { diff --git a/tests/bitcoin/lock/LockMonitor.spec.ts b/tests/bitcoin/lock/LockMonitor.spec.ts index 3491308e0..6e12c4e4a 100644 --- a/tests/bitcoin/lock/LockMonitor.spec.ts +++ b/tests/bitcoin/lock/LockMonitor.spec.ts @@ -8,8 +8,8 @@ import LockIdentifierSerializer from '../../../lib/bitcoin/lock/LockIdentifierSe import LockMonitor from '../../../lib/bitcoin/lock/LockMonitor'; import LockResolver from '../../../lib/bitcoin/lock/LockResolver'; import MongoDbLockTransactionStore from '../../../lib/bitcoin/lock/MongoDbLockTransactionStore'; -import SavedLockedModel from '../../../lib/bitcoin/models/SavedLockedModel'; import SavedLockType from '../../../lib/bitcoin/enums/SavedLockType'; +import SavedLockedModel from '../../../lib/bitcoin/models/SavedLockedModel'; import SidetreeError from '../../../lib/common/SidetreeError'; import ValueTimeLockModel from '../../../lib/common/models/ValueTimeLockModel'; import VersionManager from '../../../lib/bitcoin/VersionManager'; @@ -38,41 +38,30 @@ describe('LockMonitor', () => { let lockMonitor: LockMonitor; beforeEach(() => { - lockMonitor = new LockMonitor(bitcoinClient, mongoDbLockStore, lockResolver, 60, 1200, 100, 2000); - lockMonitor['initialized'] = true; + lockMonitor = new LockMonitor(bitcoinClient, mongoDbLockStore, lockResolver, 60, true, 1200, 100, 2000); }); describe('constructor', () => { it('should throw if the desired lock amount is not a whole number', () => { JasmineSidetreeErrorValidator.expectSidetreeErrorToBeThrown( - () => new LockMonitor(bitcoinClient, mongoDbLockStore, lockResolver, 10, 1000.34, 25, 1234), + () => new LockMonitor(bitcoinClient, mongoDbLockStore, lockResolver, 10, true, 1000.34, 25, 1234), ErrorCode.LockMonitorDesiredLockAmountIsNotWholeNumber); }); it('should throw if the txn fees amount is not a whole number', () => { JasmineSidetreeErrorValidator.expectSidetreeErrorToBeThrown( - () => new LockMonitor(bitcoinClient, mongoDbLockStore, lockResolver, 10, 1000, 1234.56, 45), + () => new LockMonitor(bitcoinClient, mongoDbLockStore, lockResolver, 10, true, 1000, 1234.56, 45), ErrorCode.LockMonitorTransactionFeesAmountIsNotWholeNumber); }); - - it('should set the initialized flag to false', () => { - const monitor = new LockMonitor(bitcoinClient, mongoDbLockStore, lockResolver, 10, 1000, 1200, 45); - expect(monitor['initialized']).toBeFalsy(); - }); }); - describe('initialize', () => { + describe('startPeriodicProcessing', () => { it('should call the periodic poll function', async () => { - const mockLockInfo = createLockState(undefined, undefined, 'none'); - const resolveSpy = spyOn(lockMonitor as any, 'getCurrentLockState').and.returnValue(Promise.resolve(mockLockInfo)); const pollSpy = spyOn(lockMonitor as any, 'periodicPoll').and.returnValue(Promise.resolve()); - lockMonitor['initialized'] = false; - await lockMonitor.initialize(); + await lockMonitor.startPeriodicProcessing(); - expect(resolveSpy).toHaveBeenCalledBefore(pollSpy); expect(pollSpy).toHaveBeenCalled(); - expect(lockMonitor['initialized']).toBeTruthy(); }); }); @@ -81,10 +70,10 @@ describe('LockMonitor', () => { const clearTimeoutSpy = spyOn(global, 'clearTimeout').and.returnValue(); const handlePollingSpy = spyOn(lockMonitor as any, 'handlePeriodicPolling').and.returnValue(Promise.resolve()); - const setTimeoutOutput: NodeJS.Timeout = 12344 as any; + const setTimeoutOutput = 12344 as any; const setTimeoutSpy = spyOn(global, 'setTimeout').and.returnValue(setTimeoutOutput as any); - const mockPeriodicPollTimeoutId: NodeJS.Timeout = 98765 as any; + const mockPeriodicPollTimeoutId = 98765 as any; lockMonitor['periodicPollTimeoutId'] = mockPeriodicPollTimeoutId; await lockMonitor['periodicPoll'](); @@ -95,25 +84,6 @@ describe('LockMonitor', () => { expect(lockMonitor['periodicPollTimeoutId']).toEqual(setTimeoutOutput); }); - it('should rethrow if the initialize flag is not true', async (done) => { - const mockErrorCode = 'error during initialization'; - spyOn(lockMonitor as any, 'handlePeriodicPolling').and.callFake(() => { - throw new SidetreeError(mockErrorCode); - }); - - const setTimeoutSpy = spyOn(global, 'setTimeout').and.returnValue(123456 as any); - - lockMonitor['periodicPollTimeoutId'] = undefined; - lockMonitor['initialized'] = false; - - await JasmineSidetreeErrorValidator.expectSidetreeErrorToBeThrownAsync( - () => lockMonitor['periodicPoll'](), - mockErrorCode); - - expect(setTimeoutSpy).toHaveBeenCalled(); - done(); - }); - it('should call setTimeout() at the end of the execution even if an exception is thrown.', async () => { const handlePollingSpy = spyOn(lockMonitor as any, 'handlePeriodicPolling').and.throwError('unhandled exception'); @@ -121,7 +91,6 @@ describe('LockMonitor', () => { const setTimeoutSpy = spyOn(global, 'setTimeout').and.returnValue(setTimeoutOutput as any); lockMonitor['periodicPollTimeoutId'] = undefined; - lockMonitor['initialized'] = true; await lockMonitor['periodicPoll'](); expect(handlePollingSpy).toHaveBeenCalled(); @@ -130,22 +99,24 @@ describe('LockMonitor', () => { }); describe('getCurrentValueTimeLock', () => { - it('should return undefined if there is no current lock', () => { - lockMonitor['currentLockState'] = createLockState(undefined, undefined, 'none'); + it('should return undefined if there is no current lock', async () => { + const mockCurrentLockState = createLockState(undefined, undefined, 'none'); + spyOn(lockMonitor as any, 'getCurrentLockState').and.returnValue(Promise.resolve(mockCurrentLockState)); - const actual = lockMonitor.getCurrentValueTimeLock(); + const actual = await lockMonitor.getCurrentValueTimeLock(); expect(actual).toBeUndefined(); }); it('should throw if the current lock status is pending', () => { - lockMonitor['currentLockState'] = createLockState(undefined, undefined, 'pending'); + const mockCurrentLockState = createLockState(undefined, undefined, 'pending'); + spyOn(lockMonitor as any, 'getCurrentLockState').and.returnValue(Promise.resolve(mockCurrentLockState)); - JasmineSidetreeErrorValidator.expectSidetreeErrorToBeThrown( + JasmineSidetreeErrorValidator.expectSidetreeErrorToBeThrownAsync( () => lockMonitor.getCurrentValueTimeLock(), ErrorCode.LockMonitorCurrentValueTimeLockInPendingState); }); - it('should return the current value time lock', () => { + it('should return the current value time lock', async () => { const mockCurrentValueLock: ValueTimeLockModel = { amountLocked: 300, identifier: 'identifier', @@ -155,15 +126,16 @@ describe('LockMonitor', () => { lockTransactionTime: 1220 }; - lockMonitor['currentLockState'] = createLockState(undefined, mockCurrentValueLock, 'confirmed'); + const mockCurrentLockState = createLockState(undefined, mockCurrentValueLock, 'confirmed'); + spyOn(lockMonitor as any, 'getCurrentLockState').and.returnValue(mockCurrentLockState); - const actual = lockMonitor.getCurrentValueTimeLock(); + const actual = await lockMonitor.getCurrentValueTimeLock(); expect(actual).toEqual(mockCurrentValueLock); }); }); describe('handlePeriodicPolling', () => { - it('should only update the lock state if the current lock status is pending.', async () => { + it('should only refresh the lock state if lock update is disabled.', async () => { const mockCurrentValueLock: ValueTimeLockModel = { amountLocked: 300, identifier: 'identifier', @@ -173,15 +145,14 @@ describe('LockMonitor', () => { lockTransactionTime: 1220 }; - const mockCurrentLockInfo = createLockState(undefined, mockCurrentValueLock, 'pending'); - lockMonitor['currentLockState'] = mockCurrentLockInfo; + const mockCurrentLockInfo = createLockState(undefined, mockCurrentValueLock, 'confirmed'); - const resolveCurrentLockSpy = spyOn(lockMonitor as any, 'getCurrentLockState'); + const resolveCurrentLockSpy = spyOn(lockMonitor as any, 'getCurrentLockState').and.returnValue(mockCurrentLockInfo); const createNewLockSpy = spyOn(lockMonitor as any, 'handleCreatingNewLock'); const existingLockSpy = spyOn(lockMonitor as any, 'handleExistingLockRenewal'); const releaseLockSpy = spyOn(lockMonitor as any, 'handleReleaseExistingLock'); - lockMonitor['desiredLockAmountInSatoshis'] = 1000; + lockMonitor['valueTimeLockUpdateEnabled'] = false; await lockMonitor['handlePeriodicPolling'](); expect(resolveCurrentLockSpy).toHaveBeenCalled(); @@ -190,11 +161,31 @@ describe('LockMonitor', () => { expect(releaseLockSpy).not.toHaveBeenCalled(); }); + + it('should rebroadcast if the last lock transaction is still pending.', async () => { + const rebroadcastTransactionSpy = spyOn(lockMonitor as any, 'rebroadcastTransaction').and.returnValue(Promise.resolve()); + const mockCurrentValueLock: ValueTimeLockModel = { + amountLocked: 300, + identifier: 'identifier', + owner: 'owner', + unlockTransactionTime: 12323, + normalizedFee: 100, + lockTransactionTime: 1220 + }; + + const mockCurrentLockInfo = createLockState(undefined, mockCurrentValueLock, 'pending'); + const getCurrentLockStateSpy = spyOn(lockMonitor as any, 'getCurrentLockState').and.returnValue(mockCurrentLockInfo); + + await lockMonitor['handlePeriodicPolling'](); + + expect(getCurrentLockStateSpy).toHaveBeenCalled(); + expect(rebroadcastTransactionSpy).toHaveBeenCalled(); + }); + it('should not do anything if a lock is not required and none exist.', async () => { const mockCurrentLockInfo = createLockState(undefined, undefined, 'none'); - lockMonitor['currentLockState'] = mockCurrentLockInfo; + const resolveCurrentLockSpy = spyOn(lockMonitor as any, 'getCurrentLockState').and.returnValue(mockCurrentLockInfo); - const resolveCurrentLockSpy = spyOn(lockMonitor as any, 'getCurrentLockState'); const createNewLockSpy = spyOn(lockMonitor as any, 'handleCreatingNewLock'); const existingLockSpy = spyOn(lockMonitor as any, 'handleExistingLockRenewal'); const releaseLockSpy = spyOn(lockMonitor as any, 'handleReleaseExistingLock'); @@ -202,15 +193,14 @@ describe('LockMonitor', () => { lockMonitor['desiredLockAmountInSatoshis'] = 0; await lockMonitor['handlePeriodicPolling'](); + expect(resolveCurrentLockSpy).toHaveBeenCalled(); expect(createNewLockSpy).not.toHaveBeenCalled(); expect(existingLockSpy).not.toHaveBeenCalled(); expect(releaseLockSpy).not.toHaveBeenCalled(); - expect(resolveCurrentLockSpy).not.toHaveBeenCalled(); }); it('should call the new lock routine if a lock is required but does not exist.', async () => { const mockCurrentLockInfo = createLockState(undefined, undefined, 'none'); - lockMonitor['currentLockState'] = mockCurrentLockInfo; const resolveCurrentLockSpy = spyOn(lockMonitor as any, 'getCurrentLockState').and.returnValue(Promise.resolve(mockCurrentLockInfo)); @@ -257,7 +247,6 @@ describe('LockMonitor', () => { }; const mockCurrentLockInfo = createLockState(mockSavedLock, mockCurrentValueLock, 'confirmed'); - lockMonitor['currentLockState'] = mockCurrentLockInfo; const resolveCurrentLockSpy = spyOn(lockMonitor as any, 'getCurrentLockState').and.returnValue(Promise.resolve(mockCurrentLockInfo)); @@ -295,7 +284,6 @@ describe('LockMonitor', () => { }; const mockCurrentLockInfo = createLockState(mockSavedLock, mockCurrentValueLock, 'confirmed'); - lockMonitor['currentLockState'] = mockCurrentLockInfo; const resolveCurrentLockSpy = spyOn(lockMonitor as any, 'getCurrentLockState').and.returnValue(Promise.resolve(mockCurrentLockInfo)); @@ -306,10 +294,10 @@ describe('LockMonitor', () => { lockMonitor['desiredLockAmountInSatoshis'] = 50; await lockMonitor['handlePeriodicPolling'](); + expect(resolveCurrentLockSpy).toHaveBeenCalled(); expect(createNewLockSpy).not.toHaveBeenCalled(); expect(existingLockSpy).toHaveBeenCalled(); expect(releaseLockSpy).not.toHaveBeenCalled(); - expect(resolveCurrentLockSpy).not.toHaveBeenCalled(); }); it('should call the release lock routine if a lock is not required but one does exist.', async () => { @@ -333,7 +321,6 @@ describe('LockMonitor', () => { }; const mockCurrentLockInfo = createLockState(mockSavedLock, mockCurrentValueLock, 'confirmed'); - lockMonitor['currentLockState'] = mockCurrentLockInfo; const resolveCurrentLockSpy = spyOn(lockMonitor as any, 'getCurrentLockState').and.returnValue(Promise.resolve(mockCurrentLockInfo)); @@ -350,7 +337,7 @@ describe('LockMonitor', () => { expect(resolveCurrentLockSpy).toHaveBeenCalled(); }); - it('should not resolve current lock if the the release lock routine returns false.', async () => { + it('should always refresh the lock state even if the the release lock routine returns false.', async () => { const mockSavedLock: SavedLockedModel = { createTimestamp: 1212, @@ -371,9 +358,7 @@ describe('LockMonitor', () => { }; const mockCurrentLockInfo = createLockState(mockSavedLock, mockCurrentValueLock, 'confirmed'); - lockMonitor['currentLockState'] = mockCurrentLockInfo; - - const resolveCurrentLockSpy = spyOn(lockMonitor as any, 'getCurrentLockState').and.returnValue(Promise.resolve(mockCurrentLockInfo)); + const getCurrentLockStateSpy = spyOn(lockMonitor as any, 'getCurrentLockState').and.returnValue(Promise.resolve(mockCurrentLockInfo)); const createNewLockSpy = spyOn(lockMonitor as any, 'handleCreatingNewLock'); const existingLockSpy = spyOn(lockMonitor as any, 'handleExistingLockRenewal'); @@ -381,11 +366,11 @@ describe('LockMonitor', () => { lockMonitor['desiredLockAmountInSatoshis'] = 0; await lockMonitor['handlePeriodicPolling'](); - + + expect(getCurrentLockStateSpy).toHaveBeenCalled(); expect(createNewLockSpy).not.toHaveBeenCalled(); expect(existingLockSpy).not.toHaveBeenCalled(); expect(releaseLockSpy).toHaveBeenCalled(); - expect(resolveCurrentLockSpy).not.toHaveBeenCalled(); }); }); @@ -404,8 +389,7 @@ describe('LockMonitor', () => { expect(resolveLockSpy).not.toHaveBeenCalled(); }); - it('should rebroadcast if the last lock transaction is not yet broadcasted.', async () => { - const rebroadcastSpy = spyOn(lockMonitor as any, 'rebroadcastTransaction').and.returnValue(Promise.resolve()); + it('should return lock status as pending if the last lock transaction is not yet broadcasted.', async () => { const resolveLockSpy = spyOn(lockResolver, 'resolveLockIdentifierAndThrowOnError'); const mockLastLock: SavedLockedModel = { @@ -425,7 +409,6 @@ describe('LockMonitor', () => { const actual = await lockMonitor['getCurrentLockState'](); expect(actual).toEqual(expected); - expect(rebroadcastSpy).toHaveBeenCalled(); expect(resolveLockSpy).not.toHaveBeenCalled(); }); @@ -610,7 +593,7 @@ describe('LockMonitor', () => { type: SavedLockType.Create }; - const saveBroadcastSpy = spyOn(lockMonitor as any,'saveThenBroadcastTransaction').and.returnValue(Promise.resolve(mockLockInfoSaved)); + const saveBroadcastSpy = spyOn(lockMonitor as any, 'saveThenBroadcastTransaction').and.returnValue(Promise.resolve(mockLockInfoSaved)); const desiredLockAmount = mockWalletBalance - (mockWalletBalance * 0.5); const actual = await lockMonitor['handleCreatingNewLock'](desiredLockAmount); @@ -1006,7 +989,7 @@ describe('LockMonitor', () => { }; const mockDateValue = Date.now(); - spyOn(Date,'now').and.returnValue(mockDateValue); + spyOn(Date, 'now').and.returnValue(mockDateValue); const lockStoreSpy = spyOn(lockMonitor['lockTransactionStore'], 'addLock').and.returnValue(Promise.resolve()); const broadcastTxnSpy = spyOn(lockMonitor['bitcoinClient'], 'broadcastLockTransaction').and.returnValue(Promise.resolve('id')); diff --git a/tests/bitcoin/lock/LockResolver.spec.ts b/tests/bitcoin/lock/LockResolver.spec.ts index 0133a4885..fabfae9c8 100644 --- a/tests/bitcoin/lock/LockResolver.spec.ts +++ b/tests/bitcoin/lock/LockResolver.spec.ts @@ -1,7 +1,9 @@ +import { Address, Networks, PrivateKey, Script, crypto } from 'bitcore-lib'; import BitcoinClient from '../../../lib/bitcoin/BitcoinClient'; import BitcoinOutputModel from '../../../lib/bitcoin/models/BitcoinOutputModel'; import BitcoinTransactionModel from '../../../lib/bitcoin/models/BitcoinTransactionModel'; import ErrorCode from '../../../lib/bitcoin/ErrorCode'; +import { IBlockInfo } from '../../../lib/bitcoin/BitcoinProcessor'; import JasmineSidetreeErrorValidator from '../../JasmineSidetreeErrorValidator'; import LockIdentifierModel from '../../../lib/bitcoin/models/LockIdentifierModel'; import LockIdentifierSerializer from '../../../lib/bitcoin/lock/LockIdentifierSerializer'; @@ -9,18 +11,16 @@ import LockResolver from '../../../lib/bitcoin/lock/LockResolver'; import ValueTimeLockModel from '../../../lib/common/models/ValueTimeLockModel'; import VersionManager from '../../../lib/bitcoin/VersionManager'; import VersionModel from '../../../lib/common/models/VersionModel'; -import { Address, crypto, Networks, PrivateKey, Script } from 'bitcore-lib'; -import { IBlockInfo } from '../../../lib/bitcoin/BitcoinProcessor'; function createValidLockRedeemScript (lockDurationInBlocks: number, targetWalletAddress: Address): Script { const lockDurationInBlocksBuffer = Buffer.alloc(3); lockDurationInBlocksBuffer.writeIntLE(lockDurationInBlocks, 0, 3); return Script.empty() - .add(lockDurationInBlocksBuffer) - .add(178) // OP_CSV - .add(117) // OP_DROP - .add(Script.buildPublicKeyHashOut(targetWalletAddress)); + .add(lockDurationInBlocksBuffer) + .add(178) // OP_CSV + .add(117) // OP_DROP + .add(Script.buildPublicKeyHashOut(targetWalletAddress)); } function createLockScriptVerifyResult (isScriptValid: boolean, owner: string | undefined, lockDurationInBlocks: number | undefined): any { @@ -47,7 +47,7 @@ describe('LockResolver', () => { let lockResolver: LockResolver; beforeEach(() => { - let bitcoinClient = new BitcoinClient('uri:test', 'u', 'p', validTestWalletImportString, 10, 1, 0); + const bitcoinClient = new BitcoinClient('uri:test', 'u', 'p', validTestWalletImportString, 10, 1, 0); lockResolver = new LockResolver(versionManager, bitcoinClient, 200, 250); }); @@ -290,7 +290,7 @@ describe('LockResolver', () => { }); it('should throw if script creation throws.', async () => { - spyOn(Buffer,'from').and.throwError('som error'); + spyOn(Buffer, 'from').and.throwError('som error'); JasmineSidetreeErrorValidator.expectSidetreeErrorToBeThrown( () => LockResolver['createScript']('some input'), @@ -311,8 +311,8 @@ describe('LockResolver', () => { spyOn(lockResolver['bitcoinClient'], 'getRawTransaction').and.throwError('not found custom error.'); await JasmineSidetreeErrorValidator.expectSidetreeErrorToBeThrownAsync( - () => lockResolver['getTransaction']('input id'), - ErrorCode.LockResolverTransactionNotFound); + () => lockResolver['getTransaction']('input id'), + ErrorCode.LockResolverTransactionNotFound); }); }); diff --git a/tests/bitcoin/lock/MongoDbLockTransactionStore.spec.ts b/tests/bitcoin/lock/MongoDbLockTransactionStore.spec.ts index dacc2a163..3a8f6fea1 100644 --- a/tests/bitcoin/lock/MongoDbLockTransactionStore.spec.ts +++ b/tests/bitcoin/lock/MongoDbLockTransactionStore.spec.ts @@ -1,10 +1,10 @@ import Config from '../../../lib/core/models/Config'; +import { MongoClient } from 'mongodb'; import MongoDb from '../../common/MongoDb'; import MongoDbLockTransactionStore from '../../../lib/bitcoin/lock/MongoDbLockTransactionStore'; import SavedLockModel from '../../../lib/bitcoin/models/SavedLockedModel'; import SavedLockType from '../../../lib/bitcoin/enums/SavedLockType'; -import { MongoClient } from 'mongodb'; async function createLockStore (transactionStoreUri: string, databaseName: string): Promise { const lockStore = new MongoDbLockTransactionStore(transactionStoreUri, databaseName); @@ -34,9 +34,9 @@ async function generateAndStoreLocks (lockStore: MongoDbLockTransactionStore, co } function getLockTypeFromIndex (i: number): SavedLockType { - return (i % 3 === 0) ? SavedLockType.Create : - (i % 3 === 1) ? SavedLockType.Relock : - SavedLockType.ReturnToWallet; + return (i % 3 === 0) ? SavedLockType.Create + : (i % 3 === 1) ? SavedLockType.Relock + : SavedLockType.ReturnToWallet; } describe('MongoDbLockTransactionStore', async () => { diff --git a/tests/common/ServiceInfoProvider.spec.ts b/tests/common/ServiceInfoProvider.spec.ts index 66376a0d2..7fea18505 100644 --- a/tests/common/ServiceInfoProvider.spec.ts +++ b/tests/common/ServiceInfoProvider.spec.ts @@ -5,7 +5,7 @@ describe('ServiceInfoProvider', () => { it('should return the version from the package.json file.', async () => { const packageJson = require('../../package.json'); - let serviceInfo = new ServiceInfo('test-service'); + const serviceInfo = new ServiceInfo('test-service'); const serviceVersion = serviceInfo.getServiceVersion(); expect(serviceVersion.name).toEqual('test-service'); diff --git a/tests/core/AnchorFile.spec.ts b/tests/core/AnchorFile.spec.ts index 132d39021..8cf516a61 100644 --- a/tests/core/AnchorFile.spec.ts +++ b/tests/core/AnchorFile.spec.ts @@ -273,7 +273,9 @@ describe('AnchorFile', async () => { const anchorFileModel = await AnchorFile.createModel(undefined, mapFileUri, [createOperation], [recoverOperation], [deactivateOperation]); expect(anchorFileModel.map_file_uri).toEqual(mapFileUri); - expect(anchorFileModel.operations.create![0].suffix_data).toEqual(createOperation.encodedSuffixData); + expect(anchorFileModel.operations.create![0].suffix_data).toEqual({ + delta_hash: createOperation.suffixData.deltaHash, recovery_commitment: createOperation.suffixData.recoveryCommitment, type: undefined + }); // Verify recover operation. const recoveryOperationInAnchorFile = anchorFileModel.operations.recover![0]; @@ -298,7 +300,9 @@ describe('AnchorFile', async () => { const anchorFile = await AnchorFile.parse(anchorFileBuffer); expect(anchorFile.model.map_file_uri).toEqual(mapFileHash); - expect(anchorFile.model.operations.create![0].suffix_data).toEqual(createOperation.encodedSuffixData); + expect(anchorFile.model.operations.create![0].suffix_data).toEqual({ + delta_hash: createOperation.suffixData.deltaHash, recovery_commitment: createOperation.suffixData.recoveryCommitment + }); }); }); }); diff --git a/tests/core/Blockchain.spec.ts b/tests/core/Blockchain.spec.ts index c60ebb65a..e17637167 100644 --- a/tests/core/Blockchain.spec.ts +++ b/tests/core/Blockchain.spec.ts @@ -4,8 +4,8 @@ import ErrorCode from '../../lib/bitcoin/ErrorCode'; import JasmineSidetreeErrorValidator from '../JasmineSidetreeErrorValidator'; import ReadableStream from '../../lib/common/ReadableStream'; import ServiceVersionModel from '../../lib/common/models/ServiceVersionModel'; -import SidetreeError from '../../lib/common/SidetreeError'; import SharedErrorCode from '../../lib/common/SharedErrorCode'; +import SidetreeError from '../../lib/common/SidetreeError'; import TransactionModel from '../../lib/common/models/TransactionModel'; import ValueTimeLockModel from '../../lib/common/models/ValueTimeLockModel'; diff --git a/tests/core/ChunkFile.spec.ts b/tests/core/ChunkFile.spec.ts index a15d84c4e..333f1ac3c 100644 --- a/tests/core/ChunkFile.spec.ts +++ b/tests/core/ChunkFile.spec.ts @@ -1,10 +1,10 @@ import * as crypto from 'crypto'; import ChunkFile from '../../lib/core/versions/latest/ChunkFile'; +import Compressor from '../../lib/core/versions/latest/util/Compressor'; import Encoder from '../../lib/core/versions/latest/Encoder'; import ErrorCode from '../../lib/core/versions/latest/ErrorCode'; import JasmineSidetreeErrorValidator from '../JasmineSidetreeErrorValidator'; import Jwk from '../../lib/core/versions/latest/util/Jwk'; -import Compressor from '../../lib/core/versions/latest/util/Compressor'; import OperationGenerator from '../generators/OperationGenerator'; describe('ChunkFile', async () => { @@ -49,8 +49,8 @@ describe('ChunkFile', async () => { const decompressedChunkFileModel = await ChunkFile.parse(chunkFileBuffer); expect(decompressedChunkFileModel.deltas.length).toEqual(2); - expect(decompressedChunkFileModel.deltas[0]).toEqual(createOperation.encodedDelta!); - expect(decompressedChunkFileModel.deltas[1]).toEqual(recoverOperation.encodedDelta!); + expect(decompressedChunkFileModel.deltas[0]).toEqual(createOperation.delta!); + expect(decompressedChunkFileModel.deltas[1]).toEqual(recoverOperation.delta!); }); }); @@ -71,14 +71,16 @@ describe('ChunkFile', async () => { ]; JasmineSidetreeErrorValidator.expectSidetreeErrorToBeThrown( - () => (ChunkFile as any).validateDeltasProperty(deltas), ErrorCode.ChunkFileDeltasNotArrayOfStrings + () => (ChunkFile as any).validateDeltasProperty(deltas), ErrorCode.ChunkFileDeltasNotArrayOfObjects ); }); it('should throw if any `delta` element exceeds max size.', async () => { const randomBytes = crypto.randomBytes(2000); // Intentionally larger than maximum. const deltas = [ - Encoder.encode(randomBytes) + { + objectKey: Encoder.encode(randomBytes) + } ]; JasmineSidetreeErrorValidator.expectSidetreeErrorToBeThrown( diff --git a/tests/core/Core.spec.ts b/tests/core/Core.spec.ts index 6f82e6623..deb63d7d6 100644 --- a/tests/core/Core.spec.ts +++ b/tests/core/Core.spec.ts @@ -1,3 +1,4 @@ +import Config from '../../lib/core/models/Config'; import Core from '../../lib/core/Core'; import IRequestHandler from '../../lib/core/interfaces/IRequestHandler'; import MockCas from '../mocks/MockCas'; @@ -7,7 +8,7 @@ import ServiceVersionModel from '../../lib/common/models/ServiceVersionModel'; describe('Core', async () => { - const testConfig = require('../json/bitcoin-config-test.json'); + const testConfig = require('../json/config-test.json'); const testVersionConfig = require('../json/core-protocol-versioning-test.json'); const mockCas = new MockCas(); @@ -60,7 +61,28 @@ describe('Core', async () => { expect(batchSchedulerStartSpy).toHaveBeenCalled(); expect(blockchainStartSpy).toHaveBeenCalled(); expect(downloadManagerStartSpy).toHaveBeenCalled(); + }); + + it('should not start the Batch Writer and Observer if they are disabled.', async () => { + // Disable Batch Writer and observer in config. + const config = Object.assign({}, testConfig) as Config; + config.batchingIntervalInSeconds = 0; + config.observingIntervalInSeconds = 0; + + const core = new Core(config, testVersionConfig, mockCas); + const observerStartSpy = spyOn(core['observer'], 'startPeriodicProcessing'); + const batchSchedulerStartSpy = spyOn(core['batchScheduler'], 'startPeriodicBatchWriting'); + spyOn(core['transactionStore'], 'initialize'); + spyOn(core['unresolvableTransactionStore'], 'initialize'); + spyOn(core['operationStore'], 'initialize'); + spyOn(core['blockchain'], 'initialize'); + spyOn(core['versionManager'], 'initialize'); + spyOn(core['blockchain'], 'startPeriodicCachedBlockchainTimeRefresh'); + spyOn(core['downloadManager'], 'start'); + await core.initialize(); + expect(observerStartSpy).not.toHaveBeenCalled(); + expect(batchSchedulerStartSpy).not.toHaveBeenCalled(); }); }); @@ -84,7 +106,7 @@ describe('Core', async () => { expect(fetchedResponse.status).toEqual(ResponseStatus.Succeeded); // Sort the output to make it easier to validate - let fetchedVersions: ServiceVersionModel[] = JSON.parse(fetchedResponse.body); + const fetchedVersions: ServiceVersionModel[] = JSON.parse(fetchedResponse.body); fetchedVersions.sort((a, b) => a.name > b.name ? 1 : -1); expect(fetchedVersions[0]).toEqual(expectedCoreVersion); diff --git a/tests/core/CreateOperation.spec.ts b/tests/core/CreateOperation.spec.ts index 079750db0..bcd39f7be 100644 --- a/tests/core/CreateOperation.spec.ts +++ b/tests/core/CreateOperation.spec.ts @@ -1,3 +1,6 @@ +import * as fs from 'fs'; +import * as suffixData from '../fixtures/uniqueSuffix/suffixData.json'; + import CreateOperation from '../../lib/core/versions/latest/CreateOperation'; import Encoder from '../../lib/core/versions/latest/Encoder'; import ErrorCode from '../../lib/core/versions/latest/ErrorCode'; @@ -8,8 +11,114 @@ import OperationType from '../../lib/core/enums/OperationType'; import SidetreeError from '../../lib/common/SidetreeError'; describe('CreateOperation', async () => { + describe('parseJcsObject', () => { + it('should leave delta as empty if it is not valid', () => { + const operationObject = { + type: 'create', + suffix_data: { + delta_hash: 'something', + recovery_commitment: 'something', + type: 'type' + }, + delta: 'this is not a valid delta' + }; + + spyOn(CreateOperation as any, 'validateSuffixData').and.callFake(() => { + // do nothing + }); + + const result = CreateOperation.parseJcsObject(operationObject, Buffer.from('something'), false); + expect(result.delta).toBeUndefined(); + }); + + it('should process as anchor file mode when anchorFileMode is true', () => { + const operationObject = { + suffix_data: { + delta_hash: 'something', + recovery_commitment: 'something', + type: 'type' + } + }; + + spyOn(CreateOperation as any, 'validateSuffixData').and.callFake(() => { + // do nothing + }); + + const result = CreateOperation.parseJcsObject(operationObject, Buffer.from('something'), true); + expect(result.delta).toBeUndefined(); + expect(result.suffixData).toBeDefined(); + }); + + it('should throw sidetree error if object contains more or less than 3 properties', () => { + const twoProperties = { one: 1, two: 2 }; + const fourProperties = { one: 1, two: 2, three: 3, four: 4 }; + + try { + CreateOperation.parseJcsObject(twoProperties, Buffer.from(JSON.stringify(twoProperties)), false); + fail('expect to throw sidetree error but did not'); + } catch (e) { + expect(e).toEqual(new SidetreeError(ErrorCode.CreateOperationMissingOrUnknownProperty)); + } + + try { + CreateOperation.parseJcsObject(fourProperties, Buffer.from(JSON.stringify(fourProperties)), false); + fail('expect to throw sidetree error but did not'); + } catch (e) { + expect(e).toEqual(new SidetreeError(ErrorCode.CreateOperationMissingOrUnknownProperty)); + } + }); + + it('should throw sidetree error if type is not create', () => { + const testObject = { + type: 'notCreate', + suffix_data: { + delta_hash: 'something', + recovery_commitment: 'something', + type: 'type' + }, + delta: 'something' + }; + + spyOn(CreateOperation as any, 'validateSuffixData').and.callFake(() => { + // do nothing + }); + + try { + CreateOperation.parseJcsObject(testObject, Buffer.from(JSON.stringify(testObject)), false); + fail('expect to throw sidetree error but did not'); + } catch (e) { + expect(e).toEqual(new SidetreeError(ErrorCode.CreateOperationTypeIncorrect)); + } + }); + + it('should throw sidetree error if has more or less than 1 property when in anchor file mode', () => { + const testObject = { + type: 'this should not exist', + suffix_data: { + delta_hash: 'something', + recovery_commitment: 'something', + type: 'type' + } + }; + try { + CreateOperation.parseJcsObject(testObject, Buffer.from(JSON.stringify(testObject)), true); + fail('expect to throw sidetree error but did not'); + } catch (e) { + expect(e).toEqual(new SidetreeError(ErrorCode.CreateOperationMissingOrUnknownProperty)); + } + }); + }); + + describe('computeJcsDidUniqueSuffix', () => { + it('should return expected did unique suffix', () => { + const actual = Multihash.canonicalizeThenHashThenEncode(suffixData); + const expected = fs.readFileSync('tests/fixtures/uniqueSuffix/resultingSuffix.txt', 'utf8'); + expect(actual).toEqual(expected); + }); + }); + describe('computeDidUniqueSuffix()', async () => { - it('should pass test vector.', async (done) => { + it('should return expected did unique suffix', async (done) => { const suffixDataString = 'AStringActingAsTheSuffixData'; const encodedSuffixDataString = Encoder.encode(suffixDataString); const didUniqueSuffix = (CreateOperation as any).computeDidUniqueSuffix(encodedSuffixDataString); @@ -56,7 +165,19 @@ describe('CreateOperation', async () => { }); }); + describe('validateSuffixData', () => { + it('should throw if the input is not an object', () => { + const input = 'this is not an object, this is a string'; + try { + CreateOperation['validateSuffixData'](input); + } catch (e) { + expect(e).toEqual(new SidetreeError(ErrorCode.CreateOperationSuffixDataIsNotObject)); + } + }); + }); + describe('parseSuffixData()', async () => { + // TODO: SIP 2 #781 deprecates this. These tests can be siwtched over to validateSuffixData it('should function as expected with type', async () => { const suffixData = { delta_hash: Encoder.encode(Multihash.hash(Buffer.from('some data'))), diff --git a/tests/core/Did.spec.ts b/tests/core/Did.spec.ts index 07787412d..9fe9d0a90 100644 --- a/tests/core/Did.spec.ts +++ b/tests/core/Did.spec.ts @@ -1,10 +1,45 @@ import * as crypto from 'crypto'; import Did from '../../lib/core/versions/latest/Did'; +import Encoder from '../../lib/core/versions/latest/Encoder'; import ErrorCode from '../../lib/core/versions/latest/ErrorCode'; import JasmineSidetreeErrorValidator from '../JasmineSidetreeErrorValidator'; +import JsonCanonicalizer from '../../lib/core/versions/latest/util/JsonCanonicalizer'; import OperationGenerator from '../generators/OperationGenerator'; +import SidetreeError from '../../lib/common/SidetreeError'; describe('DID', async () => { + describe('constructCreateOperationFromEncodedJCS', () => { + it('should throw sidetree error if initial state is not an json', () => { + const testInitialState = Encoder.encode('notJson'); + try { + Did['constructCreateOperationFromEncodedJcs'](testInitialState); + fail('expect to throw sidetree error but did not'); + } catch (e) { + expect(e).toEqual(new SidetreeError(ErrorCode.DidInitialStateJcsIsNotJosn, 'Long form initial state should be encoded jcs.')); + } + }); + + it('should throw sidetree error if initial state is not jcs', () => { + const testInitialState = Encoder.encode(JSON.stringify({ z: 1, a: 2, b: 1 })); + try { + Did['constructCreateOperationFromEncodedJcs'](testInitialState); + fail('expect to throw sidetree error but did not'); + } catch (e) { + expect(e).toEqual(new SidetreeError(ErrorCode.DidInitialStateJcsIsNotJcs, 'Initial state object and JCS string mismatch.')); + } + }); + + it('should throw sidetree error if delta exceeds size limit', () => { + const largeData = crypto.randomBytes(2000).toString('hex');// Intentionally exceeding max size. + const largeDelta = { data: largeData }; + const testInitialState = Encoder.encode(JsonCanonicalizer.canonicalizeAsBuffer({ suffix_data: 'some data', delta: largeDelta })); + + JasmineSidetreeErrorValidator.expectSidetreeErrorToBeThrown(() => { + Did['constructCreateOperationFromEncodedJcs'](testInitialState); + }, ErrorCode.DeltaExceedsMaximumSize); + }); + }); + describe('create()', async () => { it('should create a short-form DID successfully.', async () => { const expectedDidMethodName = 'sidetree'; @@ -18,58 +53,29 @@ describe('DID', async () => { expect(did.uniqueSuffix).toEqual(uniqueSuffix); }); - it('should create a long-form DID successfully.', async () => { - // Create a long-form DID string. - const createOperationData = await OperationGenerator.generateCreateOperation(); - const didMethodName = 'sidetree'; - const didUniqueSuffix = createOperationData.createOperation.didUniqueSuffix; - const shortFormDid = `did:${didMethodName}:${didUniqueSuffix}`; - const encodedSuffixData = createOperationData.createOperation.encodedSuffixData; - const encodedDelta = createOperationData.createOperation.encodedDelta; - const longFormDid = `${shortFormDid}?-sidetree-initial-state=${encodedSuffixData}.${encodedDelta}`; - - const did = await Did.create(longFormDid, didMethodName); - expect(did.isShortForm).toBeFalsy(); - expect(did.didMethodName).toEqual(didMethodName); - expect(did.shortForm).toEqual(shortFormDid); - expect(did.uniqueSuffix).toEqual(didUniqueSuffix); - expect(did.createOperation).toEqual(createOperationData.createOperation); - }); - it('should create a long-form DID with suffix data and delta successfully.', async () => { // Create a long-form DID string. - const createOperationData = await OperationGenerator.generateCreateOperation(); + + const generatedLongFormDidData = await OperationGenerator.generateLongFormDid(); const didMethodName = 'sidetree'; - const didUniqueSuffix = createOperationData.createOperation.didUniqueSuffix; - const shortFormDid = `did:${didMethodName}:${didUniqueSuffix}`; - const encodedSuffixData = createOperationData.createOperation.encodedSuffixData; - const encodedDelta = createOperationData.createOperation.encodedDelta; - const longFormDid = `${shortFormDid}:${encodedSuffixData}.${encodedDelta}`; - const did = await Did.create(longFormDid, didMethodName); + const did = await Did.create(generatedLongFormDidData.longFormDid, didMethodName); expect(did.isShortForm).toBeFalsy(); expect(did.didMethodName).toEqual(didMethodName); - expect(did.shortForm).toEqual(shortFormDid); - expect(did.uniqueSuffix).toEqual(didUniqueSuffix); - expect(did.createOperation).toEqual(createOperationData.createOperation); + expect(did.shortForm).toEqual(generatedLongFormDidData.shortFormDid); + expect(did.uniqueSuffix).toEqual(generatedLongFormDidData.didUniqueSuffix); }); - it('should create a testnet long-form DID successfully.', async () => { + it('should create a testnet long-form DID with suffix data and delta successfully.', async () => { // Create a long-form DID string. - const createOperationData = await OperationGenerator.generateCreateOperation(); - const didMethodName = 'sidetree:testnet'; // A method name with network ID. - const didUniqueSuffix = createOperationData.createOperation.didUniqueSuffix; - const shortFormDid = `did:${didMethodName}:${didUniqueSuffix}`; - const encodedSuffixData = createOperationData.createOperation.encodedSuffixData; - const encodedDelta = createOperationData.createOperation.encodedDelta; - const longFormDid = `${shortFormDid}?-sidetree-initial-state=${encodedSuffixData}.${encodedDelta}`; + const generatedLongFormDidData = await OperationGenerator.generateLongFormDid(undefined, undefined, 'testnet'); + const didMethodName = 'sidetree:testnet'; - const did = await Did.create(longFormDid, didMethodName); + const did = await Did.create(generatedLongFormDidData.longFormDid, didMethodName); expect(did.isShortForm).toBeFalsy(); expect(did.didMethodName).toEqual(didMethodName); - expect(did.shortForm).toEqual(shortFormDid); - expect(did.uniqueSuffix).toEqual(didUniqueSuffix); - expect(did.createOperation).toEqual(createOperationData.createOperation); + expect(did.shortForm).toEqual(generatedLongFormDidData.shortFormDid); + expect(did.uniqueSuffix).toEqual(generatedLongFormDidData.didUniqueSuffix); }); it('should throw error if more than one query param is provided', async () => { diff --git a/tests/core/DocumentComposer.spec.ts b/tests/core/DocumentComposer.spec.ts index cc6c5156a..d83a16be1 100644 --- a/tests/core/DocumentComposer.spec.ts +++ b/tests/core/DocumentComposer.spec.ts @@ -11,7 +11,7 @@ describe('DocumentComposer', async () => { describe('transformToExternalDocument', () => { it('should output the expected resolution result given key(s) across all purpose types.', async () => { - const [anySigningPublicKey] = await OperationGenerator.generateKeyPair('anySigningKey'); // All purposes will be included by default. + const [anySigningPublicKey] = await OperationGenerator.generateKeyPair('anySigningKey'); // All purposes will be included by default. const [authPublicKey] = await OperationGenerator.generateKeyPair('authPublicKey', [PublicKeyPurpose.Auth]); const document = { public_keys: [anySigningPublicKey, authPublicKey] @@ -34,7 +34,7 @@ describe('DocumentComposer', async () => { }); expect(result.didDocument).toEqual({ id: 'did:method:suffix', - '@context': [ 'https://www.w3.org/ns/did/v1', { '@base': 'did:method:suffix' } ], + '@context': ['https://www.w3.org/ns/did/v1', { '@base': 'did:method:suffix' }], service: undefined, publicKey: [{ id: '#anySigningKey', @@ -57,7 +57,7 @@ describe('DocumentComposer', async () => { }); it('should output method metadata with the given `published` value.', async () => { - const [anySigningPublicKey] = await OperationGenerator.generateKeyPair('anySigningKey'); // All purposes will be included by default. + const [anySigningPublicKey] = await OperationGenerator.generateKeyPair('anySigningKey'); // All purposes will be included by default. const [authPublicKey] = await OperationGenerator.generateKeyPair('authPublicKey', [PublicKeyPurpose.Auth]); const document = { public_keys: [anySigningPublicKey, authPublicKey] @@ -108,7 +108,8 @@ describe('DocumentComposer', async () => { service_endpoints: [{ id: 'someId', type: 'someType', - endpoint: 'someEndpoint'}] + endpoint: 'someEndpoint' + }] }; const result = DocumentComposer['addServiceEndpoints'](document, patch); @@ -291,29 +292,43 @@ describe('DocumentComposer', async () => { expect(() => { DocumentComposer['validateAddServiceEndpointsPatch'](patch); }).toThrow(expectedError); }); - it('should throw DocumentComposerPatchServiceEndpointServiceEndpointNotString if endpoint is not a string', () => { + it('should allow an non-array object as endpoint.', () => { + const patch = { + action: 'add-service-endpoint', + service_endpoints: [{ + id: 'someId', + type: 'someType', + endpoint: { anyObject: '123' } + }] + }; + + // Expecting this call to succeed without errors. + DocumentComposer['validateAddServiceEndpointsPatch'](patch); + }); + + it('should throw error if endpoint is an array.', () => { const patch = { action: 'add-service-endpoint', service_endpoints: [{ id: 'someId', type: 'someType', - endpoint: undefined + endpoint: [] }] }; - const expectedError = new SidetreeError(ErrorCode.DocumentComposerPatchServiceEndpointServiceEndpointNotString); + const expectedError = new SidetreeError(ErrorCode.DocumentComposerPatchServiceEndpointValueCannotBeAnArray); expect(() => { DocumentComposer['validateAddServiceEndpointsPatch'](patch); }).toThrow(expectedError); }); - it('should throw DocumentComposerPatchServiceEndpointServiceEndpointTooLong if endpoint is too long', () => { + it('should throw error if endpoint has an invalid type.', () => { const patch = { action: 'add-service-endpoint', service_endpoints: [{ id: 'someId', type: 'someType', - endpoint: 'https://www.1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678900.long' + endpoint: 123 // Invalid endpoint type. }] }; - const expectedError = new SidetreeError(ErrorCode.DocumentComposerPatchServiceEndpointServiceEndpointTooLong); + const expectedError = new SidetreeError(ErrorCode.DocumentComposerPatchServiceEndpointMustBeStringOrNonArrayObject); expect(() => { DocumentComposer['validateAddServiceEndpointsPatch'](patch); }).toThrow(expectedError); }); @@ -608,8 +623,8 @@ describe('DocumentComposer', async () => { }; await JasmineSidetreeErrorValidator.expectSidetreeErrorToBeThrownAsync( - async () => { DocumentComposer['validateDocument'](document); }, - ErrorCode.DocumentComposerUnknownPropertyInDocument + async () => { DocumentComposer['validateDocument'](document); }, + ErrorCode.DocumentComposerUnknownPropertyInDocument ); }); }); diff --git a/tests/core/DownloadManager.spec.ts b/tests/core/DownloadManager.spec.ts index 1c1097218..83471a78b 100644 --- a/tests/core/DownloadManager.spec.ts +++ b/tests/core/DownloadManager.spec.ts @@ -1,7 +1,7 @@ -import ICas from '../../lib/core/interfaces/ICas'; +import * as timeSpan from 'time-span'; import DownloadManager from '../../lib/core/DownloadManager'; +import ICas from '../../lib/core/interfaces/ICas'; import MockCas from '../mocks/MockCas'; -import timeSpan = require('time-span'); describe('DownloadManager', async () => { const maxConcurrentDownloads = 3; @@ -36,9 +36,9 @@ describe('DownloadManager', async () => { // Queue 4 downloads. const maxContentSizeInBytes = 20000000; - void downloadManager.download(content1, maxContentSizeInBytes); - void downloadManager.download(content2, maxContentSizeInBytes); - void downloadManager.download(content3, maxContentSizeInBytes); + downloadManager.download(content1, maxContentSizeInBytes); + downloadManager.download(content2, maxContentSizeInBytes); + downloadManager.download(content3, maxContentSizeInBytes); await downloadManager.download(content4, maxContentSizeInBytes); // Since there is only 3 concurrent download lanes, diff --git a/tests/core/Encoder.spec.ts b/tests/core/Encoder.spec.ts index b669dcfcc..469ea8ae1 100644 --- a/tests/core/Encoder.spec.ts +++ b/tests/core/Encoder.spec.ts @@ -18,7 +18,7 @@ describe('Encoder', async () => { await JasmineSidetreeErrorValidator.expectSidetreeErrorToBeThrownAsync( async () => Encoder.decodeAsBuffer(input), ErrorCode.EncoderValidateBase64UrlStringInputNotBase64UrlString - ); + ); done(); }); }); diff --git a/tests/core/MapFile.spec.ts b/tests/core/MapFile.spec.ts index 8da340675..2ab2f3e37 100644 --- a/tests/core/MapFile.spec.ts +++ b/tests/core/MapFile.spec.ts @@ -5,8 +5,8 @@ import JasmineSidetreeErrorValidator from '../JasmineSidetreeErrorValidator'; import MapFile from '../../lib/core/versions/latest/MapFile'; import MapFileModel from '../../lib/core/versions/latest/models/MapFileModel'; import Multihash from '../../lib/core/versions/latest/Multihash'; -import SidetreeError from '../../lib/common/SidetreeError'; import OperationGenerator from '../generators/OperationGenerator'; +import SidetreeError from '../../lib/common/SidetreeError'; describe('MapFile', async () => { describe('parse()', async () => { @@ -21,7 +21,7 @@ describe('MapFile', async () => { it('should throw if the buffer is not compressed', async () => { const mapFileModel: MapFileModel = { - chunks: [ { chunk_file_uri: 'EiB4ypIXxG9aFhXv2YC8I2tQvLEBbQAsNzHmph17vMfVYA' } ] + chunks: [{ chunk_file_uri: 'EiB4ypIXxG9aFhXv2YC8I2tQvLEBbQAsNzHmph17vMfVYA' }] }; const fileBuffer = Buffer.from(JSON.stringify(mapFileModel)); diff --git a/tests/core/MongoDbOperationQueue.spec.ts b/tests/core/MongoDbOperationQueue.spec.ts index 035f46cde..4b6fe2908 100644 --- a/tests/core/MongoDbOperationQueue.spec.ts +++ b/tests/core/MongoDbOperationQueue.spec.ts @@ -80,7 +80,7 @@ describe('MongoDbOperationQueue', async () => { expect(dequeuedOperations.length).toBe(0); dequeuedOperations = await operationQueue.dequeue(2); - let remainingOperations = await operationQueue.peek(operationCount); + const remainingOperations = await operationQueue.peek(operationCount); expect(dequeuedOperations.length).toEqual(2); expect(dequeuedOperations[0].operationBuffer).toEqual(queuedOperations[0].operationBuffer); expect(dequeuedOperations[1].operationBuffer).toEqual(queuedOperations[1].operationBuffer); @@ -119,7 +119,7 @@ describe('MongoDbOperationQueue', async () => { } catch (error) { if (error instanceof SidetreeError && error.code === ErrorCode.BatchWriterAlreadyHasOperationForDid) { - return; // Expected Sidetree error. + // Expected Sidetree error. } else { throw error; // Unexpected error, throw to fail the test. } @@ -139,7 +139,7 @@ describe('MongoDbOperationQueue', async () => { await generateAndQueueOperations(operationQueue, 1); } catch (error) { if (error.code === 'unexpected-error') { - return; // Expected behavior. + // Expected behavior. } else { throw error; // Unexpected behavior, throw to fail the test. } diff --git a/tests/core/MongoDbOperationStore.spec.ts b/tests/core/MongoDbOperationStore.spec.ts index 6ba31b55c..31253c9e3 100644 --- a/tests/core/MongoDbOperationStore.spec.ts +++ b/tests/core/MongoDbOperationStore.spec.ts @@ -33,8 +33,8 @@ async function createOperationChain ( let currentPublicKey = signingKey; let currentPrivateKey = signingPrivateKey; - for (let i = 1; i < chainLength ; i++) { - const transactionNumberToUse = transactionNumber ? transactionNumber : i; + for (let i = 1; i < chainLength; i++) { + const transactionNumberToUse = transactionNumber || i; const transactionTimeToUse = transactionNumberToUse; const [newPublicKey, newPrivateKey] = await OperationGenerator.generateKeyPair(`key${i}`); @@ -75,7 +75,7 @@ function checkEqual (operation1: AnchoredOperationModel, operation2: AnchoredOpe function checkEqualArray (putOperations: AnchoredOperationModel[], gotOperations: AnchoredOperationModel[]): void { expect(gotOperations.length).toEqual(putOperations.length); - for (let i = 0 ; i < putOperations.length ; i++) { + for (let i = 0; i < putOperations.length; i++) { checkEqual(gotOperations[i], putOperations[i]); } } @@ -271,7 +271,7 @@ describe('MongoDbOperationStore', async () => { const operationChain = await createOperationChain(anchoredOperationModel, chainSize, signingPublicKey, signingPrivateKey); // Insert operations in reverse transaction time order - for (let i = chainSize - 1 ; i >= 0 ; i--) { + for (let i = chainSize - 1; i >= 0; i--) { await operationStore.put([operationChain[i]]); } @@ -300,7 +300,7 @@ describe('MongoDbOperationStore', async () => { const returnedOperationsAfterDeletion = await operationStore.get(didUniqueSuffix); // Expected remaining operations is the first operation + the last 5 update operations. - let expectedRemainingOperations = [anchoredOperationModel]; + const expectedRemainingOperations = [anchoredOperationModel]; expectedRemainingOperations.push(...operationChain.slice(5)); checkEqualArray(expectedRemainingOperations, returnedOperationsAfterDeletion); }); @@ -326,7 +326,7 @@ describe('MongoDbOperationStore', async () => { const returnedOperationsAfterDeletion = await operationStore.get(didUniqueSuffix); // Expected remaining operations is the first operation + the last 5 update operations. - let expectedRemainingOperations = [anchoredOperationModel]; + const expectedRemainingOperations = [anchoredOperationModel]; expectedRemainingOperations.push(...operationChain.slice(5)); checkEqualArray(expectedRemainingOperations, returnedOperationsAfterDeletion); }); diff --git a/tests/core/MongoDbTransactionStore.spec.ts b/tests/core/MongoDbTransactionStore.spec.ts index 3d441f498..b6bdcb962 100644 --- a/tests/core/MongoDbTransactionStore.spec.ts +++ b/tests/core/MongoDbTransactionStore.spec.ts @@ -1,9 +1,9 @@ import Config from '../../lib/core/models/Config'; import ITransactionStore from '../../lib/core/interfaces/ITransactionStore'; +import { MongoClient } from 'mongodb'; import MongoDb from '../common/MongoDb'; import MongoDbTransactionStore from '../../lib/common/MongoDbTransactionStore'; import TransactionModel from '../../lib/common/models/TransactionModel'; -import { MongoClient } from 'mongodb'; /** * Creates a MongoDbTransactionStore and initializes it. @@ -206,7 +206,7 @@ describe('MongoDbTransactionStore', async () => { const remainingTransactions = await transactionStore.getTransactions(); expect(remainingTransactions.length).toEqual(9); - for (let transaction of remainingTransactions) { + for (const transaction of remainingTransactions) { expect(transaction.transactionTimeHash !== hashToDelete).toBeTruthy(); } }); diff --git a/tests/core/MongoDbUnresolvableTransactionStore.spec.ts b/tests/core/MongoDbUnresolvableTransactionStore.spec.ts index 35d3f98ad..b38a41b5e 100644 --- a/tests/core/MongoDbUnresolvableTransactionStore.spec.ts +++ b/tests/core/MongoDbUnresolvableTransactionStore.spec.ts @@ -1,8 +1,8 @@ import Config from '../../lib/core/models/Config'; +import { MongoClient } from 'mongodb'; import MongoDb from '../common/MongoDb'; import MongoDbUnresolvableTransactionStore from '../../lib/core/MongoDbUnresolvableTransactionStore'; import TransactionModel from '../../lib/common/models/TransactionModel'; -import { MongoClient } from 'mongodb'; /** * Creates a MongoDbUnresolvableTransactionStore and initializes it. @@ -131,8 +131,8 @@ describe('MongoDbUnresolvableTransactionStore', async () => { const maxReturnCount = 2; let unresolvableTransactionsDueForRetry = await store.getUnresolvableTransactionsDueForRetry(maxReturnCount); expect(unresolvableTransactionsDueForRetry.length).toEqual(2); - // Simulate successful resolution of the 2 returend transactions and removing them from the store. - for (let transaction of unresolvableTransactionsDueForRetry) { + // Simulate successful resolution of the 2 returned transactions and removing them from the store. + for (const transaction of unresolvableTransactionsDueForRetry) { await store.removeUnresolvableTransaction(transaction); } unresolvableTransactions = await store.getUnresolvableTransactions(); @@ -141,8 +141,8 @@ describe('MongoDbUnresolvableTransactionStore', async () => { // Get remaining 1 unresolvable transaction due for retry. unresolvableTransactionsDueForRetry = await store.getUnresolvableTransactionsDueForRetry(maxReturnCount); expect(unresolvableTransactionsDueForRetry.length).toEqual(1); - // Simulate successful resolution of the 1 returend transaction and removing it from the store. - for (let transaction of unresolvableTransactionsDueForRetry) { + // Simulate successful resolution of the 1 returned transaction and removing it from the store. + for (const transaction of unresolvableTransactionsDueForRetry) { await store.removeUnresolvableTransaction(transaction); } unresolvableTransactions = await store.getUnresolvableTransactions(); diff --git a/tests/core/Multihash.spec.ts b/tests/core/Multihash.spec.ts index c0eb3bc0d..50feb4c98 100644 --- a/tests/core/Multihash.spec.ts +++ b/tests/core/Multihash.spec.ts @@ -40,7 +40,7 @@ describe('Multihash', async () => { }); it('should return false if encountered an unexpected error.', async () => { - const multihashHashSpy = spyOn(Multihash as any, 'verify').and.throwError('Simulated error message.'); + const multihashHashSpy = spyOn(Multihash as any, 'verifyEncodedMultihashForContent').and.throwError('Simulated error message.'); const result = Multihash.isValidHash('revealValue', 'commitmentHash'); expect(multihashHashSpy).toHaveBeenCalled(); @@ -80,10 +80,27 @@ describe('Multihash', async () => { // Simulate an error thrown. spyOn(Encoder, 'decodeAsBuffer').and.throwError('any error'); - const validHash = (Multihash as any).verify(Buffer.from('anyValue'), 'unusedMultihashValue'); + const validHash = (Multihash as any).verifyEncodedMultihashForContent(Buffer.from('anyValue'), 'unusedMultihashValue'); expect(validHash).toBeFalsy(); }); + + it('should return false if given encoded multihash is not using the canonical encoding.', async () => { + const anyContent = Buffer.from('any content'); + + // Canonical encoded multihash of 'any content' is 'EiDDidVHVekuMIYV3HI5nfp8KP6s3_W44Pd-MO5b-XK5iQ' + const defaultContentEncodedMultihash = 'EiDDidVHVekuMIYV3HI5nfp8KP6s3_W44Pd-MO5b-XK5iQ'; + const modifiedContentEncodedMultihash = 'EiDDidVHVekuMIYV3HI5nfp8KP6s3_W44Pd-MO5b-XK5iR'; + + // Two multihash strings decodes into the same buffer. + expect(Encoder.decodeAsBuffer(defaultContentEncodedMultihash)).toEqual(Encoder.decodeAsBuffer(modifiedContentEncodedMultihash)); + + const validHashCheckResult = (Multihash as any).verifyEncodedMultihashForContent(anyContent, defaultContentEncodedMultihash); + const invalidHashCheckResult = (Multihash as any).verifyEncodedMultihashForContent(anyContent, modifiedContentEncodedMultihash); + + expect(validHashCheckResult).toBeTruthy(); + expect(invalidHashCheckResult).toBeFalsy(); + }); }); describe('verifyDoubleHash()', async () => { diff --git a/tests/core/Observer.spec.ts b/tests/core/Observer.spec.ts index 11479ee84..03007406b 100644 --- a/tests/core/Observer.spec.ts +++ b/tests/core/Observer.spec.ts @@ -1,6 +1,6 @@ import * as retry from 'async-retry'; -import AnchoredDataSerializer from '../../lib/core/versions/latest/AnchoredDataSerializer'; import AnchorFile from '../../lib/core/versions/latest/AnchorFile'; +import AnchoredDataSerializer from '../../lib/core/versions/latest/AnchoredDataSerializer'; import Blockchain from '../../lib/core/Blockchain'; import ChunkFile from '../../lib/core/versions/latest/ChunkFile'; import DownloadManager from '../../lib/core/DownloadManager'; @@ -9,10 +9,10 @@ import ErrorCode from '../../lib/common/SharedErrorCode'; import FetchResult from '../../lib/common/models/FetchResult'; import FetchResultCode from '../../lib/common/enums/FetchResultCode'; import IOperationStore from '../../lib/core/interfaces/IOperationStore'; -import Ipfs from '../../lib/ipfs/Ipfs'; import IVersionManager from '../../lib/core/interfaces/IVersionManager'; -import MockBlockchain from '../mocks/MockBlockchain'; +import Ipfs from '../../lib/ipfs/Ipfs'; import MapFile from '../../lib/core/versions/latest/MapFile'; +import MockBlockchain from '../mocks/MockBlockchain'; import MockOperationStore from '../mocks/MockOperationStore'; import MockTransactionStore from '../mocks/MockTransactionStore'; import MockVersionManager from '../mocks/MockVersionManager'; @@ -20,9 +20,9 @@ import Multihash from '../../lib/core/versions/latest/Multihash'; import Observer from '../../lib/core/Observer'; import OperationGenerator from '../generators/OperationGenerator'; import SidetreeError from '../../lib/common/SidetreeError'; -import TransactionSelector from '../../lib/core/versions/latest/TransactionSelector'; import TransactionModel from '../../lib/common/models/TransactionModel'; import TransactionProcessor from '../../lib/core/versions/latest/TransactionProcessor'; +import TransactionSelector from '../../lib/core/versions/latest/TransactionSelector'; describe('Observer', async () => { const config = require('../json/config-test.json'); @@ -74,31 +74,31 @@ describe('Observer', async () => { it('should record transactions processed with expected outcome.', async () => { // Prepare the mock response from blockchain service. const initialTransactionFetchResponseBody = { - 'moreTransactions': false, - 'transactions': [ + moreTransactions: false, + transactions: [ { - 'transactionNumber': 1, - 'transactionTime': 1000, - 'transactionTimeHash': '1000', - 'anchorString': '1stTransaction', - 'transactionFeePaid': 1, - 'normalizedTransactionFee': 1, - 'writer': 'writer1' + transactionNumber: 1, + transactionTime: 1000, + transactionTimeHash: '1000', + anchorString: '1stTransaction', + transactionFeePaid: 1, + normalizedTransactionFee: 1, + writer: 'writer1' }, { - 'transactionNumber': 2, - 'transactionTime': 1000, - 'transactionTimeHash': '1000', - 'anchorString': '2ndTransaction', - 'transactionFeePaid': 2, - 'normalizedTransactionFee': 2, - 'writer': 'writer2' + transactionNumber: 2, + transactionTime: 1000, + transactionTimeHash: '1000', + anchorString: '2ndTransaction', + transactionFeePaid: 2, + normalizedTransactionFee: 2, + writer: 'writer2' } ] }; const subsequentTransactionFetchResponseBody = { - 'moreTransactions': false, - 'transactions': [] + moreTransactions: false, + transactions: [] }; const blockchainClient = new Blockchain(config.blockchainServiceUri); @@ -241,7 +241,7 @@ describe('Observer', async () => { [FetchResultCode.NotAFile, 'is not a file'], [FetchResultCode.InvalidHash, 'is not a valid hash'] ]; - for (let tuple of invalidAnchorFileTestsInput) { + for (const tuple of invalidAnchorFileTestsInput) { const mockFetchReturnCode = tuple[0]; const expectedConsoleLogSubstring = tuple[1]; @@ -294,73 +294,73 @@ describe('Observer', async () => { it('should detect and handle block reorganization correctly.', async () => { // Prepare the mock response from blockchain service. const initialTransactionFetchResponseBody = { - 'moreTransactions': false, - 'transactions': [ + moreTransactions: false, + transactions: [ { - 'transactionNumber': 1, - 'transactionTime': 1000, - 'transactionTimeHash': '1000', - 'anchorString': '1stTransaction', - 'transactionFeePaid': 1, - 'normalizedTransactionFee': 1, - 'writer': 'writer1' + transactionNumber: 1, + transactionTime: 1000, + transactionTimeHash: '1000', + anchorString: '1stTransaction', + transactionFeePaid: 1, + normalizedTransactionFee: 1, + writer: 'writer1' }, { - 'transactionNumber': 2, - 'transactionTime': 2000, - 'transactionTimeHash': '2000', - 'anchorString': '2ndTransaction', - 'transactionFeePaid': 1, - 'normalizedTransactionFee': 1, - 'writer': 'writer2' + transactionNumber: 2, + transactionTime: 2000, + transactionTimeHash: '2000', + anchorString: '2ndTransaction', + transactionFeePaid: 1, + normalizedTransactionFee: 1, + writer: 'writer2' }, { - 'transactionNumber': 3, - 'transactionTime': 3000, - 'transactionTimeHash': '3000', - 'anchorString': '3rdTransaction', - 'transactionFeePaid': 1, - 'normalizedTransactionFee': 1, - 'writer': 'writer3' + transactionNumber: 3, + transactionTime: 3000, + transactionTimeHash: '3000', + anchorString: '3rdTransaction', + transactionFeePaid: 1, + normalizedTransactionFee: 1, + writer: 'writer3' } ] }; const transactionFetchResponseBodyAfterBlockReorg = { - 'moreTransactions': false, - 'transactions': [ + moreTransactions: false, + transactions: [ { - 'transactionNumber': 2, - 'transactionTime': 2001, - 'transactionTimeHash': '2001', - 'anchorString': '2ndTransactionNew', - 'transactionFeePaid': 1, - 'normalizedTransactionFee': 1, - 'writer': 'writer1' + transactionNumber: 2, + transactionTime: 2001, + transactionTimeHash: '2001', + anchorString: '2ndTransactionNew', + transactionFeePaid: 1, + normalizedTransactionFee: 1, + writer: 'writer1' }, { - 'transactionNumber': 3, - 'transactionTime': 3001, - 'transactionTimeHash': '3000', - 'anchorString': '3rdTransactionNew', - 'transactionFeePaid': 1, - 'normalizedTransactionFee': 1, - 'writer': 'writer2' + transactionNumber: 3, + transactionTime: 3001, + transactionTimeHash: '3000', + anchorString: '3rdTransactionNew', + transactionFeePaid: 1, + normalizedTransactionFee: 1, + writer: 'writer2' }, { - 'transactionNumber': 4, - 'transactionTime': 4000, - 'transactionTimeHash': '4000', - 'anchorString': '4thTransaction', - 'transactionFeePaid': 1, - 'normalizedTransactionFee': 1, - 'writer': 'writer3' + transactionNumber: 4, + transactionTime: 4000, + transactionTimeHash: '4000', + anchorString: '4thTransaction', + transactionFeePaid: 1, + normalizedTransactionFee: 1, + writer: 'writer3' } ] }; const subsequentTransactionFetchResponseBody = { - 'moreTransactions': false, - 'transactions': [] + moreTransactions: false, + transactions: [] }; const blockchainClient = new Blockchain(config.blockchainServiceUri); @@ -436,13 +436,13 @@ describe('Observer', async () => { it('should not rollback if blockchain time in bitcoin service is behind core service.', async () => { const anchoredData = AnchoredDataSerializer.serialize({ anchorFileHash: '1stTransaction', numberOfOperations: 1 }); const transaction = { - 'transactionNumber': 1, - 'transactionTime': 1000, - 'transactionTimeHash': '1000', - 'anchorString': anchoredData, - 'transactionFeePaid': 1, - 'normalizedTransactionFee': 1, - 'writer': 'writer' + transactionNumber: 1, + transactionTime: 1000, + transactionTimeHash: '1000', + anchorString: anchoredData, + transactionFeePaid: 1, + normalizedTransactionFee: 1, + writer: 'writer' }; // Prep the transaction store with some initial state. diff --git a/tests/core/Operation.spec.ts b/tests/core/Operation.spec.ts index 113953539..ed0f40f9b 100644 --- a/tests/core/Operation.spec.ts +++ b/tests/core/Operation.spec.ts @@ -1,7 +1,7 @@ import Encoder from '../../lib/core/versions/latest/Encoder'; import ErrorCode from '../../lib/core/versions/latest/ErrorCode'; -import Operation from '../../lib/core/versions/latest/Operation'; import Multihash from '../../lib/core/versions/latest/Multihash'; +import Operation from '../../lib/core/versions/latest/Operation'; import SidetreeError from '../../lib/common/SidetreeError'; describe('Operation', async () => { @@ -18,6 +18,17 @@ describe('Operation', async () => { }); }); + describe('validateDelta', () => { + it('should throw sidetree error if input is not an object', () => { + const input = 'this is not an object, this is a string'; + try { + Operation.validateDelta(input); + } catch (e) { + expect(e).toEqual(new SidetreeError(ErrorCode.DeltaIsNotObject)); + } + }); + }); + describe('parseDelta()', async () => { it('should throw if delta is not string', async () => { await expectAsync(Operation.parseDelta(123)).toBeRejectedWith(new SidetreeError(ErrorCode.DeltaMissingOrNotString)); diff --git a/tests/core/OperationProcessor.spec.ts b/tests/core/OperationProcessor.spec.ts index ef90fd54f..2231d7590 100644 --- a/tests/core/OperationProcessor.spec.ts +++ b/tests/core/OperationProcessor.spec.ts @@ -1,12 +1,12 @@ import AnchoredOperationModel from '../../lib/core/models/AnchoredOperationModel'; import CreateOperation from '../../lib/core/versions/latest/CreateOperation'; import DeactivateOperation from '../../lib/core/versions/latest/DeactivateOperation'; +import DidState from '../../lib/core/models/DidState'; import Document from '../../lib/core/versions/latest/Document'; import DocumentModel from '../../lib/core/versions/latest/models/DocumentModel'; -import DidState from '../../lib/core/models/DidState'; import ErrorCode from '../../lib/core/versions/latest/ErrorCode'; -import IOperationStore from '../../lib/core/interfaces/IOperationStore'; import IOperationProcessor from '../../lib/core/interfaces/IOperationProcessor'; +import IOperationStore from '../../lib/core/interfaces/IOperationStore'; import IVersionManager from '../../lib/core/interfaces/IVersionManager'; import Jwk from '../../lib/core/versions/latest/util/Jwk'; import JwkEs256k from '../../lib/core/models/JwkEs256k'; @@ -76,7 +76,7 @@ async function createUpdateSequence ( function getFactorial (n: number): number { let factorial = 1; - for (let i = 2 ; i <= n ; ++i) { + for (let i = 2; i <= n; ++i) { factorial *= i; } return factorial; @@ -89,11 +89,11 @@ function getFactorial (n: number): number { function getPermutation (size: number, index: number): Array { const permutation: Array = []; - for (let i = 0 ; i < size ; ++i) { + for (let i = 0; i < size; ++i) { permutation.push(i); } - for (let i = 0 ; i < size ; ++i) { + for (let i = 0; i < size; ++i) { const j = i + Math.floor(index / getFactorial(size - i - 1)); index = index % getFactorial(size - i - 1); @@ -226,7 +226,7 @@ describe('OperationProcessor', async () => { const numberOfUpdates = 10; const ops = await createUpdateSequence(didUniqueSuffix, createOp, numberOfUpdates, signingPrivateKey); - for (let i = numberOfUpdates ; i >= 0 ; --i) { + for (let i = numberOfUpdates; i >= 0; --i) { await operationStore.put([ops[i]]); } const didState = await resolver.resolve(didUniqueSuffix); @@ -239,9 +239,9 @@ describe('OperationProcessor', async () => { const ops = await createUpdateSequence(didUniqueSuffix, createOp, numberOfUpdates, signingPrivateKey); const numberOfOps = ops.length; - let numberOfPermutations = getFactorial(numberOfOps); + const numberOfPermutations = getFactorial(numberOfOps); - for (let i = 0 ; i < numberOfPermutations; ++i) { + for (let i = 0; i < numberOfPermutations; ++i) { const permutation = getPermutation(numberOfOps, i); operationStore = new MockOperationStore(); resolver = new Resolver(versionManager, operationStore); @@ -305,7 +305,7 @@ describe('OperationProcessor', async () => { const ops = await createUpdateSequence(didUniqueSuffix, createOp, numberOfUpdates, signingPrivateKey); // elide i = 0, the create operation - for (let i = 1 ; i < ops.length ; ++i) { + for (let i = 1; i < ops.length; ++i) { await operationStore.put([ops[i]]); } @@ -381,12 +381,12 @@ describe('OperationProcessor', async () => { let namedAnchoredCreateOperationModel: AnchoredOperationModel; let didState: DidState | undefined; let nextRecoveryCommitmentHash: string; - let isValidHashSpy: jasmine.Spy; + let verifyEncodedMultihashForContentSpy: jasmine.Spy; // Create a DID before each test. beforeEach(async () => { - isValidHashSpy = spyOn(Multihash, 'isValidHash'); - isValidHashSpy.and.callThrough(); + verifyEncodedMultihashForContentSpy = spyOn(Multihash, 'verifyEncodedMultihashForContent'); + verifyEncodedMultihashForContentSpy.and.callThrough(); // MUST reset the DID state back to `undefined` for each test. didState = undefined; @@ -425,8 +425,8 @@ describe('OperationProcessor', async () => { const anyDid = OperationGenerator.generateRandomHash(); const [, anyRecoveryPrivateKey] = await OperationGenerator.generateKeyPair('anyRecoveryKey'); const deactivateOperationData = await OperationGenerator.createDeactivateOperation(anyDid, anyRecoveryPrivateKey); - const anchoredDeactivateOperation - = OperationGenerator.createAnchoredOperationModelFromOperationModel(deactivateOperationData.deactivateOperation, 1, 1, 1); + const anchoredDeactivateOperation = + OperationGenerator.createAnchoredOperationModelFromOperationModel(deactivateOperationData.deactivateOperation, 1, 1, 1); const newDidState = await operationProcessor.apply(anchoredDeactivateOperation, undefined); @@ -464,7 +464,7 @@ describe('OperationProcessor', async () => { }); it('should apply the create operation with { } as document if encoded data and suffix data do not match', async () => { - isValidHashSpy.and.returnValue(false); + verifyEncodedMultihashForContentSpy.and.returnValue(false); const createOperationData = await OperationGenerator.generateAnchoredCreateOperation({ transactionTime: 1, transactionNumber: 1, operationIndex: 1 }); const newDidState = await operationProcessor.apply(createOperationData.anchoredOperationModel, undefined); expect(newDidState!.lastOperationTransactionNumber).toEqual(1); @@ -608,7 +608,7 @@ describe('OperationProcessor', async () => { ); const recoverOperation = await RecoverOperation.parse(Buffer.from(JSON.stringify(recoverOperationRequest))); const anchoredRecoverOperationModel = OperationGenerator.createAnchoredOperationModelFromOperationModel(recoverOperation, 2, 2, 2); - isValidHashSpy.and.returnValue(false); + verifyEncodedMultihashForContentSpy.and.returnValue(false); const newDidState = await operationProcessor.apply(anchoredRecoverOperationModel, didState); expect(newDidState!.lastOperationTransactionNumber).toEqual(2); expect(newDidState!.document).toEqual({ }); diff --git a/tests/core/RequestHandler.spec.ts b/tests/core/RequestHandler.spec.ts index 231c44746..6ab239f27 100644 --- a/tests/core/RequestHandler.spec.ts +++ b/tests/core/RequestHandler.spec.ts @@ -1,7 +1,8 @@ -import * as crypto from 'crypto'; - import * as createFixture from '../fixtures/create/create.json'; +import * as crypto from 'crypto'; import * as deactivateFixture from '../fixtures/deactivate/deactivate.json'; +import * as legacyLongFormResultingDocument from '../fixtures/legacyLongFormDid/resultingDocument.json'; +import * as longFormResultingDocument from '../fixtures/longFormDid/resultingDocument.json'; import * as recoverFixture from '../fixtures/recover/recover.json'; import * as updateFixture from '../fixtures/update/update.json'; @@ -9,12 +10,11 @@ import AnchoredOperationModel from '../../lib/core/models/AnchoredOperationModel import BatchScheduler from '../../lib/core/BatchScheduler'; import BatchWriter from '../../lib/core/versions/latest/BatchWriter'; import ChunkFile from '../../lib/core/versions/latest/ChunkFile'; +import Compressor from '../../lib/core/versions/latest/util/Compressor'; +import Config from '../../lib/core/models/Config'; import CreateOperation from '../../lib/core/versions/latest/CreateOperation'; import Did from '../../lib/core/versions/latest/Did'; import DidState from '../../lib/core/models/DidState'; -import Compressor from '../../lib/core/versions/latest/util/Compressor'; -import Config from '../../lib/core/models/Config'; -import Encoder from '../../lib/core/versions/latest/Encoder'; import ErrorCode from '../../lib/core/versions/latest/ErrorCode'; import ICas from '../../lib/core/interfaces/ICas'; import IOperationStore from '../../lib/core/interfaces/IOperationStore'; @@ -38,12 +38,13 @@ import ResponseStatus from '../../lib/common/enums/ResponseStatus'; import SidetreeError from '../../lib/common/SidetreeError'; const util = require('util'); +const fs = require('fs'); describe('RequestHandler', () => { // Suppress console logging during testing so we get a compact test summary in console. - console.info = () => { return; }; - console.error = () => { return; }; - console.debug = () => { return; }; + console.info = () => { }; + console.error = () => { }; + console.debug = () => { }; const config: Config = require('../json/config-test.json'); const didMethodName = config.didMethodName; @@ -136,6 +137,21 @@ describe('RequestHandler', () => { await batchScheduler.writeOperationBatch(); }); + it('should resolve LEGACY long form did from test vectors correctly', async () => { + // TODO: SIP2 #781 delete this test when legacy format is deprecated + const longFormFixture = fs.readFileSync('./tests/fixtures/longFormDid/longFormDid.txt', 'utf8'); + const response = await requestHandler.handleResolveRequest(longFormFixture); + expect(response.status).toEqual(ResponseStatus.Succeeded); + expect(response.body).toEqual(legacyLongFormResultingDocument); + }); + + it('should resolve long form did from test vectors correctly', async () => { + const longFormFixture = fs.readFileSync('./tests/fixtures/longFormDid/longFormDid.txt', 'utf8'); + const response = await requestHandler.handleResolveRequest(longFormFixture); + expect(response.status).toEqual(ResponseStatus.Succeeded); + expect(response.body).toEqual(longFormResultingDocument); + }); + it('should process create operation from test vectors correctly', async () => { const createOperationBuffer = Buffer.from(JSON.stringify(createFixture)); const response = await requestHandler.handleOperationRequest(createOperationBuffer); @@ -185,7 +201,10 @@ describe('RequestHandler', () => { const createOperationRequest = createOperationData.operationRequest; const getRandomBytesAsync = util.promisify(crypto.randomBytes); const largeBuffer = await getRandomBytesAsync(4000); - createOperationRequest.delta = Encoder.encode(largeBuffer); + createOperationRequest.delta = { + update_commitment: largeBuffer.toString(), + patches: [] + }; const createOperationBuffer = Buffer.from(JSON.stringify(createOperationRequest)); const response = await requestHandler.handleOperationRequest(createOperationBuffer); @@ -223,7 +242,7 @@ describe('RequestHandler', () => { validateDidReferencesInDidDocument(response.body.didDocument, did); }); - it('should return a resolved DID Document given a valid long-form DID.', async () => { + it('should return a resolved DID Document given a valid long-form DID with query param initial state format.', async () => { // Create a long-form DID string. const createOperationData = await OperationGenerator.generateCreateOperation(); const didMethodName = 'sidetree'; @@ -242,6 +261,19 @@ describe('RequestHandler', () => { validateDidReferencesInDidDocument(response.body.didDocument, longFormDid); }); + it('should return a resolved DID Document given a valid long-form DID with JCS format.', async () => { + // Create a long-form DID string. + const longFormDid = (await OperationGenerator.generateLongFormDid()).longFormDid; + + const response = await requestHandler.handleResolveRequest(longFormDid); + const httpStatus = Response.toHttpStatus(response.status); + + expect(httpStatus).toEqual(200); + expect(response.body).toBeDefined(); + + validateDidReferencesInDidDocument(response.body.didDocument, longFormDid); + }); + it('should return NotFound given an unknown DID.', async () => { const response = await requestHandler.handleResolveRequest('did:sidetree:EiAgE-q5cRcn4JHh8ETJGKqaJv1z2OgjmN3N-APx0aAvHg'); const httpStatus = Response.toHttpStatus(response.status); @@ -255,7 +287,7 @@ describe('RequestHandler', () => { const httpStatus = Response.toHttpStatus(response.status); expect(httpStatus).toEqual(400); - expect(response.body.code).toEqual(ErrorCode.DidInitialStateValueDoesNotContainTwoParts); + expect(response.body.code).toEqual(ErrorCode.EncoderValidateBase64UrlStringInputNotBase64UrlString); }); it('should respond with HTTP 200 when DID deactivate operation request is successful.', async () => { @@ -299,7 +331,7 @@ describe('RequestHandler', () => { }); it('[Bug #817] should return status as `deactivated` if DID is deactivated.', async () => { - // Intentionally not + // Intentionally not including `nextRecoveryCommitmentHash` and `nextUpdateCommitmentHash` to simulate deactivated state. const document = { unused: 'unused' }; const mockedResolverReturnedDidState: DidState = { document, @@ -404,14 +436,14 @@ function validateDidReferencesInDidDocument (didDocument: any, did: string) { expect(didDocument.id).toEqual(did); if (didDocument.publicKey) { - for (let publicKeyEntry of didDocument.publicKey) { + for (const publicKeyEntry of didDocument.publicKey) { expect(publicKeyEntry.controller).toEqual(''); expect((publicKeyEntry.id as string).startsWith('#')); } } if (didDocument.service) { - for (let serviceEntry of didDocument.service) { + for (const serviceEntry of didDocument.service) { expect((serviceEntry.id as string).startsWith('#')); } } diff --git a/tests/core/Resolver.spec.ts b/tests/core/Resolver.spec.ts index 9605925e5..28f81b042 100644 --- a/tests/core/Resolver.spec.ts +++ b/tests/core/Resolver.spec.ts @@ -1,21 +1,21 @@ import * as createFixture from '../fixtures/create/create.json'; import * as createResultingDocument from '../fixtures/create/resultingDocument.json'; -import * as deactivateFixtureCreate from '../fixtures/deactivate/create.json'; import * as deactivateFixture from '../fixtures/deactivate/deactivate.json'; +import * as deactivateFixtureCreate from '../fixtures/deactivate/create.json'; import * as deactivateResultingDocument from '../fixtures/deactivate/resultingDocument.json'; -import * as recoverFixtureCreate from '../fixtures/recover/create.json'; import * as recoverFixture from '../fixtures/recover/recover.json'; +import * as recoverFixtureCreate from '../fixtures/recover/create.json'; import * as recoverResultingDocument from '../fixtures/recover/resultingDocument.json'; -import * as updateFixtureCreate from '../fixtures/update/create.json'; import * as updateFixture from '../fixtures/update/update.json'; +import * as updateFixtureCreate from '../fixtures/update/create.json'; import * as updateResultingDocument from '../fixtures/update/resultingDocument.json'; import AnchoredOperationModel from '../../lib/core/models/AnchoredOperationModel'; import CreateOperation from '../../lib/core/versions/latest/CreateOperation'; import DeactivateOperation from '../../lib/core/versions/latest/DeactivateOperation'; -import DocumentComposer from '../../lib/core/versions/latest/DocumentComposer'; -import Document from '../../lib/core/versions/latest/Document'; import DidState from '../../lib/core/models/DidState'; +import Document from '../../lib/core/versions/latest/Document'; +import DocumentComposer from '../../lib/core/versions/latest/DocumentComposer'; import IOperationProcessor from '../../lib/core/interfaces/IOperationProcessor'; import IOperationStore from '../../lib/core/interfaces/IOperationStore'; import Jwk from '../../lib/core/versions/latest/util/Jwk'; @@ -307,13 +307,16 @@ describe('Resolver', () => { // Generate 3 anchored recover operations with the same reveal value but different anchored time. const recoveryOperation1Data = await OperationGenerator.generateRecoverOperation({ didUniqueSuffix: createOperationData.createOperation.didUniqueSuffix, - recoveryPrivateKey: createOperationData.recoveryPrivateKey }); + recoveryPrivateKey: createOperationData.recoveryPrivateKey + }); const recoveryOperation2Data = await OperationGenerator.generateRecoverOperation({ didUniqueSuffix: createOperationData.createOperation.didUniqueSuffix, - recoveryPrivateKey: createOperationData.recoveryPrivateKey }); + recoveryPrivateKey: createOperationData.recoveryPrivateKey + }); const recoveryOperation3Data = await OperationGenerator.generateRecoverOperation({ didUniqueSuffix: createOperationData.createOperation.didUniqueSuffix, - recoveryPrivateKey: createOperationData.recoveryPrivateKey }); + recoveryPrivateKey: createOperationData.recoveryPrivateKey + }); const recoveryOperation1 = OperationGenerator.createAnchoredOperationModelFromOperationModel(recoveryOperation1Data.recoverOperation, 2, 2, 2); const recoveryOperation2 = OperationGenerator.createAnchoredOperationModelFromOperationModel(recoveryOperation2Data.recoverOperation, 3, 3, 3); const recoveryOperation3 = OperationGenerator.createAnchoredOperationModelFromOperationModel(recoveryOperation3Data.recoverOperation, 4, 4, 4); @@ -361,8 +364,8 @@ describe('Resolver', () => { // Intentionally insert earliest valid recover operation in between the other two operations to test sorting. // Intentionally using the resolver's map construction method to test operations with the same reveal value are placed in the same array. - const updateCommitValueToOperationMap: Map - = await (resolver as any).constructCommitValueToOperationLookupMap([updateOperation3, updateOperation1, updateOperation2]); + const updateCommitValueToOperationMap: Map = + await (resolver as any).constructCommitValueToOperationLookupMap([updateOperation3, updateOperation1, updateOperation2]); const nextUpdateCommitment = createOperationData.createOperation.delta!.updateCommitment; const updatesWithSameReveal = updateCommitValueToOperationMap.get(nextUpdateCommitment); expect(updatesWithSameReveal).toBeDefined(); diff --git a/tests/core/TransactionProcessor.spec.ts b/tests/core/TransactionProcessor.spec.ts index 86402f169..f23310441 100644 --- a/tests/core/TransactionProcessor.spec.ts +++ b/tests/core/TransactionProcessor.spec.ts @@ -1,5 +1,5 @@ -import AnchoredDataSerializer from '../../lib/core/versions/latest/AnchoredDataSerializer'; import AnchorFile from '../../lib/core/versions/latest/AnchorFile'; +import AnchoredDataSerializer from '../../lib/core/versions/latest/AnchoredDataSerializer'; import ChunkFile from '../../lib/core/versions/latest/ChunkFile'; import Compressor from '../../lib/core/versions/latest/util/Compressor'; import DownloadManager from '../../lib/core/DownloadManager'; @@ -218,7 +218,7 @@ describe('TransactionProcessor', () => { }; await JasmineSidetreeErrorValidator.expectSidetreeErrorToBeThrownAsync( - () => transactionProcessor['downloadAndVerifyAnchorFile'](mockTransaction,'mock_hash', 999999), // Some really large paid operation count. + () => transactionProcessor['downloadAndVerifyAnchorFile'](mockTransaction, 'mock_hash', 999999), // Some really large paid operation count. ErrorCode.TransactionProcessorPaidOperationCountExceedsLimit); done(); @@ -324,7 +324,7 @@ describe('TransactionProcessor', () => { const paidBatchSize = 2; const downloadedAnchorFile = await transactionProcessor['downloadAndVerifyAnchorFile'](mockTransaction, 'mock_hash', paidBatchSize); - expect(downloadedAnchorFile.model).toEqual(mockAnchorFileModel); + expect(JSON.stringify(downloadedAnchorFile.model)).toEqual(JSON.stringify(mockAnchorFileModel)); done(); }); }); diff --git a/tests/core/TransactionSelector.spec.ts b/tests/core/TransactionSelector.spec.ts index aed8feeb7..49cf28776 100644 --- a/tests/core/TransactionSelector.spec.ts +++ b/tests/core/TransactionSelector.spec.ts @@ -3,8 +3,8 @@ import ErrorCode from '../../lib/core/versions/latest/ErrorCode'; import ITransactionStore from '../../lib/core/interfaces/ITransactionStore'; import JasmineSidetreeErrorValidator from '../JasmineSidetreeErrorValidator'; import MockTransactionStore from '../mocks/MockTransactionStore'; -import TransactionSelector from '../../lib/core/versions/latest/TransactionSelector'; import TransactionModel from '../../lib/common/models/TransactionModel'; +import TransactionSelector from '../../lib/core/versions/latest/TransactionSelector'; describe('TransactionSelector', () => { diff --git a/tests/core/ValueTimeLockVerifier.spec.ts b/tests/core/ValueTimeLockVerifier.spec.ts index 9fe72c6d7..de5a8d3b3 100644 --- a/tests/core/ValueTimeLockVerifier.spec.ts +++ b/tests/core/ValueTimeLockVerifier.spec.ts @@ -1,8 +1,8 @@ import ErrorCode from '../../lib/core/versions/latest/ErrorCode'; import JasmineSidetreeErrorValidator from '../JasmineSidetreeErrorValidator'; import ProtocolParameters from '../../lib/core/versions/latest/ProtocolParameters'; -import ValueTimeLockVerifier from '../../lib/core/versions/latest/ValueTimeLockVerifier'; import ValueTimeLockModel from '../../lib/common/models/ValueTimeLockModel'; +import ValueTimeLockVerifier from '../../lib/core/versions/latest/ValueTimeLockVerifier'; describe('ValueTimeLockVerifier', () => { let versionMetadataFetcher: any = {}; diff --git a/tests/core/VersionManager.spec.ts b/tests/core/VersionManager.spec.ts index 502c09445..18e472a4f 100644 --- a/tests/core/VersionManager.spec.ts +++ b/tests/core/VersionManager.spec.ts @@ -10,12 +10,12 @@ import MockBlockchain from '../mocks/MockBlockchain'; import MockCas from '../mocks/MockCas'; import MockOperationStore from '../mocks/MockOperationStore'; import MockTransactionStore from '../mocks/MockTransactionStore'; +import OperationGenerator from '../generators/OperationGenerator'; import OperationType from '../../lib/core/enums/OperationType'; import Resolver from '../../lib/core/Resolver'; import TransactionModel from '../../lib/common/models/TransactionModel'; import VersionManager from '../../lib/core/VersionManager'; import VersionModel from '../../lib/common/models/VersionModel'; -import OperationGenerator from '../generators/OperationGenerator'; describe('VersionManager', async () => { @@ -121,12 +121,12 @@ describe('VersionManager', async () => { // Setting up loading of mock ITransactionSelector implementations. const mockTransactionSelector1 = class { /* tslint:disable-next-line */ - selectQualifiedTransactions () { return [] } + selectQualifiedTransactions () { return []; } }; const anyTransactionModel = OperationGenerator.generateTransactionModel(); const mockTransactionSelector2 = class { /* tslint:disable-next-line */ - selectQualifiedTransactions () { return [anyTransactionModel] } + selectQualifiedTransactions () { return [anyTransactionModel]; } }; spyOn(versionManager as any, 'loadDefaultExportsForVersion').and.callFake(async (version: string, className: string) => { if (className === 'TransactionSelector') { diff --git a/tests/core/util/Jwk.spec.ts b/tests/core/util/Jwk.spec.ts index 3868ae7de..c9be78079 100644 --- a/tests/core/util/Jwk.spec.ts +++ b/tests/core/util/Jwk.spec.ts @@ -1,4 +1,5 @@ import ErrorCode from '../../../lib/core/versions/latest/ErrorCode'; +import JasmineSidetreeErrorValidator from '../../JasmineSidetreeErrorValidator'; import Jwk from '../../../lib/core/versions/latest/util/Jwk'; import SidetreeError from '../../../lib/common/SidetreeError'; @@ -63,5 +64,33 @@ describe('Jwk', async () => { expect(() => { Jwk.validateJwkEs256k(jwk); }).toThrow(new SidetreeError(ErrorCode.JwkEs256kMissingOrInvalidTypeY)); }); + + it('should throw error if given key contains invalid x length.', async () => { + const jwk = { + kty: 'EC', + crv: 'secp256k1', + x: 'incorrectLength', + y: 'v0-Q5H3vcfAfQ4zsebJQvMrIg3pcsaJzRvuIYZ3_UOY' + }; + + JasmineSidetreeErrorValidator.expectSidetreeErrorToBeThrown( + () => Jwk.validateJwkEs256k(jwk), + ErrorCode.JwkEs256kHasIncorrectLengthOfX + ); + }); + + it('should throw error if given key contains invalid y length.', async () => { + const jwk = { + kty: 'EC', + crv: 'secp256k1', + x: '5s3-bKjD1Eu_3NJu8pk7qIdOPl1GBzU_V8aR3xiacoM', + y: 'incorrectLength' + }; + + JasmineSidetreeErrorValidator.expectSidetreeErrorToBeThrown( + () => Jwk.validateJwkEs256k(jwk), + ErrorCode.JwkEs256kHasIncorrectLengthOfY + ); + }); }); }); diff --git a/tests/core/util/Jws.spec.ts b/tests/core/util/Jws.spec.ts index e418323b8..847d6bf4b 100644 --- a/tests/core/util/Jws.spec.ts +++ b/tests/core/util/Jws.spec.ts @@ -1,8 +1,8 @@ +import Encoder from '../../../lib/core/versions/latest/Encoder'; import ErrorCode from '../../../lib/core/versions/latest/ErrorCode'; import Jwk from '../../../lib/core/versions/latest/util/Jwk'; import Jws from '../../../lib/core/versions/latest/util/Jws'; import SidetreeError from '../../../lib/common/SidetreeError'; -import Encoder from '../../../lib/core/versions/latest/Encoder'; describe('Jws', async () => { describe('parseCompactJws()', async () => { diff --git a/tests/core/versions/test-version-1/RequestHandler.ts b/tests/core/versions/test-version-1/RequestHandler.ts index b5e1494d1..166a12038 100644 --- a/tests/core/versions/test-version-1/RequestHandler.ts +++ b/tests/core/versions/test-version-1/RequestHandler.ts @@ -8,8 +8,11 @@ import ResponseModel from '../../../../lib/common/models/ResponseModel'; */ export default class RequestHandler implements IRequestHandler { - // tslint:disable-next-line: max-line-length - public constructor (private resolver: Resolver, private operationQueue: IOperationQueue, private didMethodName: string, private supportedAlgorithms: number[]) { + public constructor ( + private resolver: Resolver, + private operationQueue: IOperationQueue, + private didMethodName: string, + private supportedAlgorithms: number[]) { console.debug(this.resolver, this.operationQueue, this.didMethodName, this.supportedAlgorithms); } diff --git a/tests/fixtures/create/create.json b/tests/fixtures/create/create.json index b4bca7a14..0c028e6d7 100644 --- a/tests/fixtures/create/create.json +++ b/tests/fixtures/create/create.json @@ -1,5 +1,40 @@ { "type": "create", - "suffix_data": "eyJkZWx0YV9oYXNoIjoiRWlDTW5jb0xrODlIeWpUYzhRbUU0VVVoNFpzMlhiOGlGTWxBak51WFlDbjZCdyIsInJlY292ZXJ5X2NvbW1pdG1lbnQiOiJFaUF5VklubUZfbnpwYWVmeVdqNkJvTkdkWV85RlJOZWYtV0kzY3JySzZ2SXlBIn0", - "delta": "eyJ1cGRhdGVfY29tbWl0bWVudCI6IkVpQ2prREdERE9udVpxNEtpdzJpY3VkSU45NWJvRE1yR2RZeDRwejhiQnQyYnciLCJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljX2tleXMiOlt7ImlkIjoic2lnbmluZ0tleSIsInR5cGUiOiJFY2RzYVNlY3AyNTZrMVZlcmlmaWNhdGlvbktleTIwMTkiLCJqd2siOnsia3R5IjoiRUMiLCJjcnYiOiJzZWNwMjU2azEiLCJ4IjoiSk5PMVhsRGVRNDhxUHhrNkFNbjZFNjMzcWJYdTVXS19uX05rZjBvNWtXYyIsInkiOiJRd1BwQkp1NXRQWkhpMml5VzlZbXRtWWl0YzdiS29ObmNCazhSYWc2RVNRIn0sInB1cnBvc2UiOlsiYXV0aCIsImdlbmVyYWwiXX1dLCJzZXJ2aWNlX2VuZHBvaW50cyI6W3siaWQiOiJzZXJ2aWNlRW5kcG9pbnRJZDEyMyIsInR5cGUiOiJzb21lVHlwZSIsImVuZHBvaW50IjoiaHR0cHM6Ly93d3cudXJsLmNvbSJ9XX19XX0" + "suffix_data": { + "delta_hash": "EiCU610_CcYVE-2o4R9zI6EPDjMDVLZ-wOnqqYnUTOevyQ", + "recovery_commitment": "EiAzN_y4WBLpZsjRXg8GbpRtXAhddATz-vmq3AxMOV2Huw" + }, + "delta": { + "update_commitment": "EiDYsJIQG1zJUDmud_hBA4tSg6jfRYXnm9E5Mo4rLfbSTA", + "patches": [ + { + "action": "replace", + "document": { + "public_keys": [ + { + "id": "signingKey", + "type": "EcdsaSecp256k1VerificationKey2019", + "jwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "rNTDcwHYaCdf7JWuaNnKA-Vw2Na6ZKneRPdg0ADIPjM", + "y": "d40NCILv5q7C0-6aSKA59-6J2iR0q-znGKpcJilr8-w" + }, + "purpose": [ + "auth", + "general" + ] + } + ], + "service_endpoints": [ + { + "id": "serviceEndpointId123", + "type": "someType", + "endpoint": "https://www.url.com" + } + ] + } + } + ] + } } \ No newline at end of file diff --git a/tests/fixtures/create/resultingDocument.json b/tests/fixtures/create/resultingDocument.json index ceb610ab8..cd494a7ad 100644 --- a/tests/fixtures/create/resultingDocument.json +++ b/tests/fixtures/create/resultingDocument.json @@ -1,11 +1,11 @@ { "@context": "https://www.w3.org/ns/did-resolution/v1", "didDocument": { - "id": "did:sidetree:EiBbDEkbYF5XUBraDOv92wSLaZfetEJETtWvboqFngyAGw", + "id": "did:sidetree:EiCv_jz1Xa3JFPt4ctQ2nr_gf0Xm1hQ_3WWnQq7gh89HxA", "@context": [ "https://www.w3.org/ns/did/v1", { - "@base": "did:sidetree:EiBbDEkbYF5XUBraDOv92wSLaZfetEJETtWvboqFngyAGw" + "@base": "did:sidetree:EiCv_jz1Xa3JFPt4ctQ2nr_gf0Xm1hQ_3WWnQq7gh89HxA" } ], "service": [ @@ -23,8 +23,8 @@ "publicKeyJwk": { "kty": "EC", "crv": "secp256k1", - "x": "JNO1XlDeQ48qPxk6AMn6E633qbXu5WK_n_Nkf0o5kWc", - "y": "QwPpBJu5tPZHi2iyW9YmtmYitc7bKoNncBk8Rag6ESQ" + "x": "rNTDcwHYaCdf7JWuaNnKA-Vw2Na6ZKneRPdg0ADIPjM", + "y": "d40NCILv5q7C0-6aSKA59-6J2iR0q-znGKpcJilr8-w" } } ], @@ -34,7 +34,7 @@ }, "methodMetadata": { "published": true, - "recoveryCommitment": "EiAyVInmF_nzpaefyWj6BoNGdY_9FRNef-WI3crrK6vIyA", - "updateCommitment": "EiCjkDGDDOnuZq4Kiw2icudIN95boDMrGdYx4pz8bBt2bw" + "recoveryCommitment": "EiAzN_y4WBLpZsjRXg8GbpRtXAhddATz-vmq3AxMOV2Huw", + "updateCommitment": "EiDYsJIQG1zJUDmud_hBA4tSg6jfRYXnm9E5Mo4rLfbSTA" } } \ No newline at end of file diff --git a/tests/fixtures/deactivate/create.json b/tests/fixtures/deactivate/create.json index b4bca7a14..25173b4cf 100644 --- a/tests/fixtures/deactivate/create.json +++ b/tests/fixtures/deactivate/create.json @@ -1,5 +1,40 @@ { "type": "create", - "suffix_data": "eyJkZWx0YV9oYXNoIjoiRWlDTW5jb0xrODlIeWpUYzhRbUU0VVVoNFpzMlhiOGlGTWxBak51WFlDbjZCdyIsInJlY292ZXJ5X2NvbW1pdG1lbnQiOiJFaUF5VklubUZfbnpwYWVmeVdqNkJvTkdkWV85RlJOZWYtV0kzY3JySzZ2SXlBIn0", - "delta": "eyJ1cGRhdGVfY29tbWl0bWVudCI6IkVpQ2prREdERE9udVpxNEtpdzJpY3VkSU45NWJvRE1yR2RZeDRwejhiQnQyYnciLCJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljX2tleXMiOlt7ImlkIjoic2lnbmluZ0tleSIsInR5cGUiOiJFY2RzYVNlY3AyNTZrMVZlcmlmaWNhdGlvbktleTIwMTkiLCJqd2siOnsia3R5IjoiRUMiLCJjcnYiOiJzZWNwMjU2azEiLCJ4IjoiSk5PMVhsRGVRNDhxUHhrNkFNbjZFNjMzcWJYdTVXS19uX05rZjBvNWtXYyIsInkiOiJRd1BwQkp1NXRQWkhpMml5VzlZbXRtWWl0YzdiS29ObmNCazhSYWc2RVNRIn0sInB1cnBvc2UiOlsiYXV0aCIsImdlbmVyYWwiXX1dLCJzZXJ2aWNlX2VuZHBvaW50cyI6W3siaWQiOiJzZXJ2aWNlRW5kcG9pbnRJZDEyMyIsInR5cGUiOiJzb21lVHlwZSIsImVuZHBvaW50IjoiaHR0cHM6Ly93d3cudXJsLmNvbSJ9XX19XX0" + "suffix_data": { + "delta_hash": "EiBXzQSnImjbD3gft4mjQhLMLfVox4_DBtrRl08zjMtHzg", + "recovery_commitment": "EiAw39-ZSetGCMhDoQ_KX4CxUlK4uv5gy_P3RAvA-BtRNw" + }, + "delta": { + "update_commitment": "EiBrAmPqRn0qeBu_Z9OwLaOFCpSVmOa0tWxuixD2SqFr5A", + "patches": [ + { + "action": "replace", + "document": { + "public_keys": [ + { + "id": "signingKey", + "type": "EcdsaSecp256k1VerificationKey2019", + "jwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "Nv3LwDZc26GQTxk9ZseI4iSJBAF3O51RZvR6WFgu1X8", + "y": "RsBvDSzlDuoyAjUSf0ZlChWqlrGEtoJpjrgh9iMvkGo" + }, + "purpose": [ + "auth", + "general" + ] + } + ], + "service_endpoints": [ + { + "id": "serviceEndpointId123", + "type": "someType", + "endpoint": "https://www.url.com" + } + ] + } + } + ] + } } \ No newline at end of file diff --git a/tests/fixtures/deactivate/deactivate.json b/tests/fixtures/deactivate/deactivate.json index b68c1363e..33d6ce724 100644 --- a/tests/fixtures/deactivate/deactivate.json +++ b/tests/fixtures/deactivate/deactivate.json @@ -1,5 +1,5 @@ { "type": "deactivate", - "did_suffix": "EiBbDEkbYF5XUBraDOv92wSLaZfetEJETtWvboqFngyAGw", - "signed_data": "eyJhbGciOiJFUzI1NksifQ.eyJkaWRfc3VmZml4IjoiRWlCYkRFa2JZRjVYVUJyYURPdjkyd1NMYVpmZXRFSkVUdFd2Ym9xRm5neUFHdyIsInJlY292ZXJ5X2tleSI6eyJrdHkiOiJFQyIsImNydiI6InNlY3AyNTZrMSIsIngiOiJCNnBhMVZydFhvUXZaRXV0TF9EM1RjS3FiVVlQdFZfQzFxRHpTSmZhUmFZIiwieSI6Ik43UnJCQkNDOG9aY04yU2d5YUJjdWQ3d29JajhvbnBIckRlQUV2SlFLVGcifX0.J74fJ5qyTvCUUaV6xB7PlX_vgaWU8EBDb23-fZ3BoLE_bwyCWNMOCjghJ7LNQgUAhd6MnfwsTibOmP9KbvsRkw" + "did_suffix": "EiA2MrsUyXcZIJDFn8bLW6eZAGcsR8ymDudSjU7T9CUXGw", + "signed_data": "eyJhbGciOiJFUzI1NksifQ.eyJkaWRfc3VmZml4IjoiRWlBMk1yc1V5WGNaSUpERm44YkxXNmVaQUdjc1I4eW1EdWRTalU3VDlDVVhHdyIsInJlY292ZXJ5X2tleSI6eyJrdHkiOiJFQyIsImNydiI6InNlY3AyNTZrMSIsIngiOiJyM1FXalloUkVUZlBjUmtqdWJLRlFpWC1DS3d0Y09WQzUwNjFsUjVCYzZnIiwieSI6IjJGb3FkR2ZUZHRhcFVJUnVGcE1Ta0lKbDdTNzRGVFFqc1BlOWhTQUdURncifX0.w2vdtMSkpc56TegNNUu5cEeQBXZ_RpTtXRJP8-aEBAFcr3ftsxNqvQTdPvQpnqS4LPeaHChWqEAa-dGiVKaUUQ" } \ No newline at end of file diff --git a/tests/fixtures/legacyLongFormDid/longFormDid.txt b/tests/fixtures/legacyLongFormDid/longFormDid.txt new file mode 100644 index 000000000..70bfa5000 --- /dev/null +++ b/tests/fixtures/legacyLongFormDid/longFormDid.txt @@ -0,0 +1 @@ +did:sidetree:EiC5-1uBg-YC2DvQRbI6eihDvk7DOYaQ08OB0I3jCe9Ydg?-ion-initial-state=eyJkZWx0YV9oYXNoIjoiRWlBbExNMC1qem1DWi1FcElVZ0laQ2piWk5yMDFfVVBMbnd5MHdfT3I0Rks0dyIsInJlY292ZXJ5X2NvbW1pdG1lbnQiOiJFaUJDNGhTMVVHeVNnTmYzbWFMdnNKRUpxX05aQUlKa0pndTNKMTJMeGNESE93In0.eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljX2tleXMiOlt7ImlkIjoiYW55U2lnbmluZ0tleUlkIiwiandrIjp7ImNydiI6InNlY3AyNTZrMSIsImt0eSI6IkVDIiwieCI6ImFHc01HMHU5Rlg2STU0cGVJS3FZb2tqblFQR2hMVVlUT1FOYzNuT3ZFMVEiLCJ5IjoiZmppbHFoZVdRWWtITkU3MHNoTVJ5TURyWnA4RUdDZkVfYUwzaC15Sm1RQSJ9LCJwdXJwb3NlIjpbImF1dGgiLCJnZW5lcmFsIl0sInR5cGUiOiJFY2RzYVNlY3AyNTZrMVZlcmlmaWNhdGlvbktleTIwMTkifV0sInNlcnZpY2VfZW5kcG9pbnRzIjpbeyJlbmRwb2ludCI6Imh0dHA6Ly9hbnkuZW5kcG9pbnQiLCJpZCI6ImFueVNlcnZpY2VFbmRwb2ludElkIiwidHlwZSI6ImFueVR5cGUifV19fV0sInVwZGF0ZV9jb21taXRtZW50IjoiRWlERkM2RE9Ed0JNeG5kX19oMTFSeDRObjFlOHpubFlPUjJhLVBqeUNva2NGZyJ9 \ No newline at end of file diff --git a/tests/fixtures/legacyLongFormDid/resultingDocument.json b/tests/fixtures/legacyLongFormDid/resultingDocument.json new file mode 100644 index 000000000..e4d3319a6 --- /dev/null +++ b/tests/fixtures/legacyLongFormDid/resultingDocument.json @@ -0,0 +1,37 @@ +{ + "@context": "https://www.w3.org/ns/did-resolution/v1", + "didDocument": { + "id": "did:sidetree:EiDahaOGH-liLLdDtTxEAdc8i-cfCz-WUcQdRJheMVNn3A:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljX2tleXMiOlt7ImlkIjoiYW55U2lnbmluZ0tleUlkIiwiandrIjp7ImNydiI6InNlY3AyNTZrMSIsImt0eSI6IkVDIiwieCI6Ikg2MXZxQW1fLVRDM09yRlNxUHJFZlNmZzQyMk5SOFFIUHFyMG1MeDY0RE0iLCJ5IjoiczBXbldZODdKcmlCamJ5b1kzRmRVbWlmSzdKSlJMUjY1R3RQdGhYZXl1YyJ9LCJwdXJwb3NlIjpbImF1dGgiXSwidHlwZSI6IkVjZHNhU2VjcDI1NmsxVmVyaWZpY2F0aW9uS2V5MjAxOSJ9XSwic2VydmljZV9lbmRwb2ludHMiOlt7ImVuZHBvaW50IjoiaHR0cDovL2FueS5lbmRwb2ludCIsImlkIjoiYW55U2VydmljZUVuZHBvaW50SWQiLCJ0eXBlIjoiYW55VHlwZSJ9XX19XSwidXBkYXRlX2NvbW1pdG1lbnQiOiJFaUJNV0UySkZhRmlwUGR0aGNGaVFlay1TWFRNaTVJV0lGWEFOOGhLRkN5TEp3In0sInN1ZmZpeF9kYXRhIjp7ImRlbHRhX2hhc2giOiJFaUJQNmdBT3h4M1lPTDhQWlBaRzNtZWRGZ2RxV1NEYXlWWDN1MVcyZi1JUEVRIiwicmVjb3ZlcnlfY29tbWl0bWVudCI6IkVpQmc4b3F2VTBacV9INUJvcW1XZjBJcmhldFE5MXdYYzVmRFBwSWpCOXdXNXcifX0", + "@context": [ + "https://www.w3.org/ns/did/v1", + { + "@base": "did:sidetree:EiDahaOGH-liLLdDtTxEAdc8i-cfCz-WUcQdRJheMVNn3A:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljX2tleXMiOlt7ImlkIjoiYW55U2lnbmluZ0tleUlkIiwiandrIjp7ImNydiI6InNlY3AyNTZrMSIsImt0eSI6IkVDIiwieCI6Ikg2MXZxQW1fLVRDM09yRlNxUHJFZlNmZzQyMk5SOFFIUHFyMG1MeDY0RE0iLCJ5IjoiczBXbldZODdKcmlCamJ5b1kzRmRVbWlmSzdKSlJMUjY1R3RQdGhYZXl1YyJ9LCJwdXJwb3NlIjpbImF1dGgiXSwidHlwZSI6IkVjZHNhU2VjcDI1NmsxVmVyaWZpY2F0aW9uS2V5MjAxOSJ9XSwic2VydmljZV9lbmRwb2ludHMiOlt7ImVuZHBvaW50IjoiaHR0cDovL2FueS5lbmRwb2ludCIsImlkIjoiYW55U2VydmljZUVuZHBvaW50SWQiLCJ0eXBlIjoiYW55VHlwZSJ9XX19XSwidXBkYXRlX2NvbW1pdG1lbnQiOiJFaUJNV0UySkZhRmlwUGR0aGNGaVFlay1TWFRNaTVJV0lGWEFOOGhLRkN5TEp3In0sInN1ZmZpeF9kYXRhIjp7ImRlbHRhX2hhc2giOiJFaUJQNmdBT3h4M1lPTDhQWlBaRzNtZWRGZ2RxV1NEYXlWWDN1MVcyZi1JUEVRIiwicmVjb3ZlcnlfY29tbWl0bWVudCI6IkVpQmc4b3F2VTBacV9INUJvcW1XZjBJcmhldFE5MXdYYzVmRFBwSWpCOXdXNXcifX0" + } + ], + "service": [ + { + "id": "#anyServiceEndpointId", + "type": "anyType", + "serviceEndpoint": "http://any.endpoint" + } + ], + "authentication": [ + { + "id": "#anySigningKeyId", + "controller": "", + "type": "EcdsaSecp256k1VerificationKey2019", + "publicKeyJwk": { + "crv": "secp256k1", + "kty": "EC", + "x": "H61vqAm_-TC3OrFSqPrEfSfg422NR8QHPqr0mLx64DM", + "y": "s0WnWY87JriBjbyoY3FdUmifK7JJRLR65GtPthXeyuc" + } + } + ] + }, + "methodMetadata": { + "published": false, + "recoveryCommitment": "EiBg8oqvU0Zq_H5BoqmWf0IrhetQ91wXc5fDPpIjB9wW5w", + "updateCommitment": "EiBMWE2JFaFipPdthcFiQek-SXTMi5IWIFXAN8hKFCyLJw" + } + } \ No newline at end of file diff --git a/tests/fixtures/longFormDid/longFormDid.txt b/tests/fixtures/longFormDid/longFormDid.txt new file mode 100644 index 000000000..7967df320 --- /dev/null +++ b/tests/fixtures/longFormDid/longFormDid.txt @@ -0,0 +1 @@ +did:sidetree:EiDahaOGH-liLLdDtTxEAdc8i-cfCz-WUcQdRJheMVNn3A:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljX2tleXMiOlt7ImlkIjoiYW55U2lnbmluZ0tleUlkIiwiandrIjp7ImNydiI6InNlY3AyNTZrMSIsImt0eSI6IkVDIiwieCI6Ikg2MXZxQW1fLVRDM09yRlNxUHJFZlNmZzQyMk5SOFFIUHFyMG1MeDY0RE0iLCJ5IjoiczBXbldZODdKcmlCamJ5b1kzRmRVbWlmSzdKSlJMUjY1R3RQdGhYZXl1YyJ9LCJwdXJwb3NlIjpbImF1dGgiXSwidHlwZSI6IkVjZHNhU2VjcDI1NmsxVmVyaWZpY2F0aW9uS2V5MjAxOSJ9XSwic2VydmljZV9lbmRwb2ludHMiOlt7ImVuZHBvaW50IjoiaHR0cDovL2FueS5lbmRwb2ludCIsImlkIjoiYW55U2VydmljZUVuZHBvaW50SWQiLCJ0eXBlIjoiYW55VHlwZSJ9XX19XSwidXBkYXRlX2NvbW1pdG1lbnQiOiJFaUJNV0UySkZhRmlwUGR0aGNGaVFlay1TWFRNaTVJV0lGWEFOOGhLRkN5TEp3In0sInN1ZmZpeF9kYXRhIjp7ImRlbHRhX2hhc2giOiJFaUJQNmdBT3h4M1lPTDhQWlBaRzNtZWRGZ2RxV1NEYXlWWDN1MVcyZi1JUEVRIiwicmVjb3ZlcnlfY29tbWl0bWVudCI6IkVpQmc4b3F2VTBacV9INUJvcW1XZjBJcmhldFE5MXdYYzVmRFBwSWpCOXdXNXcifX0 \ No newline at end of file diff --git a/tests/fixtures/longFormDid/resultingDocument.json b/tests/fixtures/longFormDid/resultingDocument.json new file mode 100644 index 000000000..f16e8df11 --- /dev/null +++ b/tests/fixtures/longFormDid/resultingDocument.json @@ -0,0 +1,37 @@ +{ + "@context": "https://www.w3.org/ns/did-resolution/v1", + "didDocument": { + "id": "did:sidetree:EiDahaOGH-liLLdDtTxEAdc8i-cfCz-WUcQdRJheMVNn3A:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljX2tleXMiOlt7ImlkIjoiYW55U2lnbmluZ0tleUlkIiwiandrIjp7ImNydiI6InNlY3AyNTZrMSIsImt0eSI6IkVDIiwieCI6Ikg2MXZxQW1fLVRDM09yRlNxUHJFZlNmZzQyMk5SOFFIUHFyMG1MeDY0RE0iLCJ5IjoiczBXbldZODdKcmlCamJ5b1kzRmRVbWlmSzdKSlJMUjY1R3RQdGhYZXl1YyJ9LCJwdXJwb3NlIjpbImF1dGgiXSwidHlwZSI6IkVjZHNhU2VjcDI1NmsxVmVyaWZpY2F0aW9uS2V5MjAxOSJ9XSwic2VydmljZV9lbmRwb2ludHMiOlt7ImVuZHBvaW50IjoiaHR0cDovL2FueS5lbmRwb2ludCIsImlkIjoiYW55U2VydmljZUVuZHBvaW50SWQiLCJ0eXBlIjoiYW55VHlwZSJ9XX19XSwidXBkYXRlX2NvbW1pdG1lbnQiOiJFaUJNV0UySkZhRmlwUGR0aGNGaVFlay1TWFRNaTVJV0lGWEFOOGhLRkN5TEp3In0sInN1ZmZpeF9kYXRhIjp7ImRlbHRhX2hhc2giOiJFaUJQNmdBT3h4M1lPTDhQWlBaRzNtZWRGZ2RxV1NEYXlWWDN1MVcyZi1JUEVRIiwicmVjb3ZlcnlfY29tbWl0bWVudCI6IkVpQmc4b3F2VTBacV9INUJvcW1XZjBJcmhldFE5MXdYYzVmRFBwSWpCOXdXNXcifX0", + "@context": [ + "https://www.w3.org/ns/did/v1", + { + "@base": "did:sidetree:EiDahaOGH-liLLdDtTxEAdc8i-cfCz-WUcQdRJheMVNn3A:eyJkZWx0YSI6eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljX2tleXMiOlt7ImlkIjoiYW55U2lnbmluZ0tleUlkIiwiandrIjp7ImNydiI6InNlY3AyNTZrMSIsImt0eSI6IkVDIiwieCI6Ikg2MXZxQW1fLVRDM09yRlNxUHJFZlNmZzQyMk5SOFFIUHFyMG1MeDY0RE0iLCJ5IjoiczBXbldZODdKcmlCamJ5b1kzRmRVbWlmSzdKSlJMUjY1R3RQdGhYZXl1YyJ9LCJwdXJwb3NlIjpbImF1dGgiXSwidHlwZSI6IkVjZHNhU2VjcDI1NmsxVmVyaWZpY2F0aW9uS2V5MjAxOSJ9XSwic2VydmljZV9lbmRwb2ludHMiOlt7ImVuZHBvaW50IjoiaHR0cDovL2FueS5lbmRwb2ludCIsImlkIjoiYW55U2VydmljZUVuZHBvaW50SWQiLCJ0eXBlIjoiYW55VHlwZSJ9XX19XSwidXBkYXRlX2NvbW1pdG1lbnQiOiJFaUJNV0UySkZhRmlwUGR0aGNGaVFlay1TWFRNaTVJV0lGWEFOOGhLRkN5TEp3In0sInN1ZmZpeF9kYXRhIjp7ImRlbHRhX2hhc2giOiJFaUJQNmdBT3h4M1lPTDhQWlBaRzNtZWRGZ2RxV1NEYXlWWDN1MVcyZi1JUEVRIiwicmVjb3ZlcnlfY29tbWl0bWVudCI6IkVpQmc4b3F2VTBacV9INUJvcW1XZjBJcmhldFE5MXdYYzVmRFBwSWpCOXdXNXcifX0" + } + ], + "service": [ + { + "id": "#anyServiceEndpointId", + "type": "anyType", + "serviceEndpoint": "http://any.endpoint" + } + ], + "authentication": [ + { + "id": "#anySigningKeyId", + "controller": "", + "type": "EcdsaSecp256k1VerificationKey2019", + "publicKeyJwk": { + "crv": "secp256k1", + "kty": "EC", + "x": "H61vqAm_-TC3OrFSqPrEfSfg422NR8QHPqr0mLx64DM", + "y": "s0WnWY87JriBjbyoY3FdUmifK7JJRLR65GtPthXeyuc" + } + } + ] + }, + "methodMetadata": { + "published": false, + "recoveryCommitment": "EiBg8oqvU0Zq_H5BoqmWf0IrhetQ91wXc5fDPpIjB9wW5w", + "updateCommitment": "EiBMWE2JFaFipPdthcFiQek-SXTMi5IWIFXAN8hKFCyLJw" + } +} \ No newline at end of file diff --git a/tests/fixtures/recover/create.json b/tests/fixtures/recover/create.json index b4bca7a14..151e5f8fd 100644 --- a/tests/fixtures/recover/create.json +++ b/tests/fixtures/recover/create.json @@ -1,5 +1,40 @@ { "type": "create", - "suffix_data": "eyJkZWx0YV9oYXNoIjoiRWlDTW5jb0xrODlIeWpUYzhRbUU0VVVoNFpzMlhiOGlGTWxBak51WFlDbjZCdyIsInJlY292ZXJ5X2NvbW1pdG1lbnQiOiJFaUF5VklubUZfbnpwYWVmeVdqNkJvTkdkWV85RlJOZWYtV0kzY3JySzZ2SXlBIn0", - "delta": "eyJ1cGRhdGVfY29tbWl0bWVudCI6IkVpQ2prREdERE9udVpxNEtpdzJpY3VkSU45NWJvRE1yR2RZeDRwejhiQnQyYnciLCJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljX2tleXMiOlt7ImlkIjoic2lnbmluZ0tleSIsInR5cGUiOiJFY2RzYVNlY3AyNTZrMVZlcmlmaWNhdGlvbktleTIwMTkiLCJqd2siOnsia3R5IjoiRUMiLCJjcnYiOiJzZWNwMjU2azEiLCJ4IjoiSk5PMVhsRGVRNDhxUHhrNkFNbjZFNjMzcWJYdTVXS19uX05rZjBvNWtXYyIsInkiOiJRd1BwQkp1NXRQWkhpMml5VzlZbXRtWWl0YzdiS29ObmNCazhSYWc2RVNRIn0sInB1cnBvc2UiOlsiYXV0aCIsImdlbmVyYWwiXX1dLCJzZXJ2aWNlX2VuZHBvaW50cyI6W3siaWQiOiJzZXJ2aWNlRW5kcG9pbnRJZDEyMyIsInR5cGUiOiJzb21lVHlwZSIsImVuZHBvaW50IjoiaHR0cHM6Ly93d3cudXJsLmNvbSJ9XX19XX0" + "suffix_data": { + "delta_hash": "EiDTzZm-O3bXgMyCXnU9xV139lhFbGEUESX1Dne4R5Y43g", + "recovery_commitment": "EiCvlhOvSfQIIAn0fwhEnZhgXCDQGPHEsnYqGqlS-NBaPA" + }, + "delta": { + "update_commitment": "EiAC9isOHRi5cd2jiswsoYZ8oA_9TNTLP0kdvBoJquNg8Q", + "patches": [ + { + "action": "replace", + "document": { + "public_keys": [ + { + "id": "signingKey", + "type": "EcdsaSecp256k1VerificationKey2019", + "jwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "Rjqcd5L1eE76aiBdH8y59jNset_kCkVmALdD8T-zOlU", + "y": "mCtG-bOiA1WGfL8WPqtgFXAyQrcpxbHzaUCOwJQ0LQU" + }, + "purpose": [ + "auth", + "general" + ] + } + ], + "service_endpoints": [ + { + "id": "serviceEndpointId123", + "type": "someType", + "endpoint": "https://www.url.com" + } + ] + } + } + ] + } } \ No newline at end of file diff --git a/tests/fixtures/recover/recover.json b/tests/fixtures/recover/recover.json index 512788af8..958008526 100644 --- a/tests/fixtures/recover/recover.json +++ b/tests/fixtures/recover/recover.json @@ -1,6 +1,38 @@ { "type": "recover", - "did_suffix": "EiBbDEkbYF5XUBraDOv92wSLaZfetEJETtWvboqFngyAGw", - "signed_data": "eyJhbGciOiJFUzI1NksifQ.eyJkZWx0YV9oYXNoIjoiRWlDbG1BbHZvamNGTjlnMGkzSUNpVE1KbnRTLWZtdFVLR0ZxSmtqS3lwaTZJUSIsInJlY292ZXJ5X2tleSI6eyJrdHkiOiJFQyIsImNydiI6InNlY3AyNTZrMSIsIngiOiJCNnBhMVZydFhvUXZaRXV0TF9EM1RjS3FiVVlQdFZfQzFxRHpTSmZhUmFZIiwieSI6Ik43UnJCQkNDOG9aY04yU2d5YUJjdWQ3d29JajhvbnBIckRlQUV2SlFLVGcifSwicmVjb3ZlcnlfY29tbWl0bWVudCI6IkVpQ05pQ05WUm5maE00QnQ3bHVSc3RDeTVmX29jRVpjY01iWENpMzJyZjRPMEEifQ.oX8OxtSHUVcrTbvq25gPvGquM4QLZC3yQGvCRqosKn6D-nqxgaBLXvAVWTF5AlRdJYhHNon3VsTlr-6XxqTClw", - "delta": "eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljX2tleXMiOlt7ImlkIjoibmV3U2lnbmluZ0tleSIsInR5cGUiOiJFY2RzYVNlY3AyNTZrMVZlcmlmaWNhdGlvbktleTIwMTkiLCJqd2siOnsia3R5IjoiRUMiLCJjcnYiOiJzZWNwMjU2azEiLCJ4IjoieDYtRnN2Y1J0dHI4d0xDQWRQQ0RHbmh2NTRlQ2lJMm03MllYZTRFYkRRUSIsInkiOiI4VjdiT2xUcG1FRmNDMEpHbDg0aWVRdGRWOHk5ckJWZG94TkRUbnlkYk1jIn0sInB1cnBvc2UiOlsiYXV0aCIsImdlbmVyYWwiXX1dLCJzZXJ2aWNlX2VuZHBvaW50cyI6W3siaWQiOiJuZXdEdW1teUVuZHBvaW50IiwidHlwZSI6InNvbWVUeXBlIiwiZW5kcG9pbnQiOiJodHRwczovL3d3dy51cmwuY29tIn1dfX1dLCJ1cGRhdGVfY29tbWl0bWVudCI6IkVpQVY5T3ZkejBCWVFuZkNPaVF5NGUyWlZ1a0JCZE1yRFhsTFUxSnlhMkJtd1EifQ" + "did_suffix": "EiCUIeUqj-Y9ceSYRjWJmGw-YTpB98DVsRfmcDBGko9x7Q", + "signed_data": "eyJhbGciOiJFUzI1NksifQ.eyJkZWx0YV9oYXNoIjoiRWlEUkpZQ2NmdGxTX3V5eG52VjVQT3dKcFkzdVc3THVRdU4tSElURXJMemJhUSIsInJlY292ZXJ5X2tleSI6eyJrdHkiOiJFQyIsImNydiI6InNlY3AyNTZrMSIsIngiOiJ2Q3ZPZnUzamVGakVOM2VaYXFJSEhESmExQWNTWWNoaDFsOTd5TzdZeGE0IiwieSI6IjVmamNzV1JkalNOeENtd0VCRFpuSndLdUNnZ2lidE8xV2g3U3VfX3laZjAifSwicmVjb3ZlcnlfY29tbWl0bWVudCI6IkVpRHZOXzI2WUJrakdwWndEUFI4U2t2Y05FTGdZT3AzZk83b1pVZW51T3EzV1EifQ.nfzlXC3SD2s4K6U1dzFM5IH-vg4N5fSMCK8SQuyZRPovnMPGOBeo72RgaSd1O8q2bZktkW4aBVG5XsPiT4kNJQ", + "delta": { + "patches": [ + { + "action": "replace", + "document": { + "public_keys": [ + { + "id": "newDocumentKey", + "type": "EcdsaSecp256k1VerificationKey2019", + "jwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "SlZcxuiumcdkU3SG1IMfRlsNwYe-soW-T6EQHxp2mH0", + "y": "A_FoCyt9mi3mn46bjTEsRRN70LPQLKGlW4CfMm8aA7M" + }, + "purpose": [ + "auth", + "general" + ] + } + ], + "service_endpoints": [ + { + "id": "newId", + "type": "someType", + "endpoint": "https://www.url.com" + } + ] + } + } + ], + "update_commitment": "EiAxp_CSxxPkNzkCrvjGyDy10hmgHy0cuYHLywWyFIipnQ" + } } \ No newline at end of file diff --git a/tests/fixtures/recover/resultingDocument.json b/tests/fixtures/recover/resultingDocument.json index f51f19ae2..11287e3b3 100644 --- a/tests/fixtures/recover/resultingDocument.json +++ b/tests/fixtures/recover/resultingDocument.json @@ -1,40 +1,40 @@ { "@context": "https://www.w3.org/ns/did-resolution/v1", "didDocument": { - "id": "did:sidetree:EiBbDEkbYF5XUBraDOv92wSLaZfetEJETtWvboqFngyAGw", + "id": "did:sidetree:EiCUIeUqj-Y9ceSYRjWJmGw-YTpB98DVsRfmcDBGko9x7Q", "@context": [ "https://www.w3.org/ns/did/v1", { - "@base": "did:sidetree:EiBbDEkbYF5XUBraDOv92wSLaZfetEJETtWvboqFngyAGw" + "@base": "did:sidetree:EiCUIeUqj-Y9ceSYRjWJmGw-YTpB98DVsRfmcDBGko9x7Q" } ], "service": [ { - "id": "#newDummyEndpoint", + "id": "#newId", "type": "someType", "serviceEndpoint": "https://www.url.com" } ], "publicKey": [ { - "id": "#newSigningKey", + "id": "#newDocumentKey", "controller": "", "type": "EcdsaSecp256k1VerificationKey2019", "publicKeyJwk": { "kty": "EC", "crv": "secp256k1", - "x": "x6-FsvcRttr8wLCAdPCDGnhv54eCiI2m72YXe4EbDQQ", - "y": "8V7bOlTpmEFcC0JGl84ieQtdV8y9rBVdoxNDTnydbMc" + "x": "SlZcxuiumcdkU3SG1IMfRlsNwYe-soW-T6EQHxp2mH0", + "y": "A_FoCyt9mi3mn46bjTEsRRN70LPQLKGlW4CfMm8aA7M" } } ], "authentication": [ - "#newSigningKey" + "#newDocumentKey" ] }, "methodMetadata": { "published": true, - "recoveryCommitment": "EiCNiCNVRnfhM4Bt7luRstCy5f_ocEZccMbXCi32rf4O0A", - "updateCommitment": "EiAV9Ovdz0BYQnfCOiQy4e2ZVukBBdMrDXlLU1Jya2BmwQ" + "recoveryCommitment": "EiDvN_26YBkjGpZwDPR8SkvcNELgYOp3fO7oZUenuOq3WQ", + "updateCommitment": "EiAxp_CSxxPkNzkCrvjGyDy10hmgHy0cuYHLywWyFIipnQ" } } \ No newline at end of file diff --git a/tests/fixtures/uniqueSuffix/resultingSuffix.txt b/tests/fixtures/uniqueSuffix/resultingSuffix.txt new file mode 100644 index 000000000..c2ac8597b --- /dev/null +++ b/tests/fixtures/uniqueSuffix/resultingSuffix.txt @@ -0,0 +1 @@ +EiDMM0OSF_J1SyCRFj-NtsyuXLP1HoFl-77QejaEwMW-kA \ No newline at end of file diff --git a/tests/fixtures/uniqueSuffix/suffixData.json b/tests/fixtures/uniqueSuffix/suffixData.json new file mode 100644 index 000000000..a5bb36887 --- /dev/null +++ b/tests/fixtures/uniqueSuffix/suffixData.json @@ -0,0 +1,4 @@ +{ + "delta_hash": "EiDv_M8oOqyYyWtvqAGG8CpXJlKXP4Q5D4H0zE55-PQqGw", + "recovery_commitment": "EiAL35tvU7ge-hZm2cBRG5IrY2St2NSXUar-H8RYBMKSCg" +} \ No newline at end of file diff --git a/tests/fixtures/update/create.json b/tests/fixtures/update/create.json index b4bca7a14..5853adc4e 100644 --- a/tests/fixtures/update/create.json +++ b/tests/fixtures/update/create.json @@ -1,5 +1,40 @@ { "type": "create", - "suffix_data": "eyJkZWx0YV9oYXNoIjoiRWlDTW5jb0xrODlIeWpUYzhRbUU0VVVoNFpzMlhiOGlGTWxBak51WFlDbjZCdyIsInJlY292ZXJ5X2NvbW1pdG1lbnQiOiJFaUF5VklubUZfbnpwYWVmeVdqNkJvTkdkWV85RlJOZWYtV0kzY3JySzZ2SXlBIn0", - "delta": "eyJ1cGRhdGVfY29tbWl0bWVudCI6IkVpQ2prREdERE9udVpxNEtpdzJpY3VkSU45NWJvRE1yR2RZeDRwejhiQnQyYnciLCJwYXRjaGVzIjpbeyJhY3Rpb24iOiJyZXBsYWNlIiwiZG9jdW1lbnQiOnsicHVibGljX2tleXMiOlt7ImlkIjoic2lnbmluZ0tleSIsInR5cGUiOiJFY2RzYVNlY3AyNTZrMVZlcmlmaWNhdGlvbktleTIwMTkiLCJqd2siOnsia3R5IjoiRUMiLCJjcnYiOiJzZWNwMjU2azEiLCJ4IjoiSk5PMVhsRGVRNDhxUHhrNkFNbjZFNjMzcWJYdTVXS19uX05rZjBvNWtXYyIsInkiOiJRd1BwQkp1NXRQWkhpMml5VzlZbXRtWWl0YzdiS29ObmNCazhSYWc2RVNRIn0sInB1cnBvc2UiOlsiYXV0aCIsImdlbmVyYWwiXX1dLCJzZXJ2aWNlX2VuZHBvaW50cyI6W3siaWQiOiJzZXJ2aWNlRW5kcG9pbnRJZDEyMyIsInR5cGUiOiJzb21lVHlwZSIsImVuZHBvaW50IjoiaHR0cHM6Ly93d3cudXJsLmNvbSJ9XX19XX0" + "suffix_data": { + "delta_hash": "EiBtq6E2h8k82eh3ZFA8quDB6mD8wfwarQ44dqQBbJnhhw", + "recovery_commitment": "EiAPN6FqOS487qru2NgQmnu6zFOTeOaPsH7HbGQmLmUZpQ" + }, + "delta": { + "update_commitment": "EiDuOprunhX2vM9ZOSY4M-C7QUQDGSe_g3AKJGoOPMpevA", + "patches": [ + { + "action": "replace", + "document": { + "public_keys": [ + { + "id": "signingKey", + "type": "EcdsaSecp256k1VerificationKey2019", + "jwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "rnua16iJUNo443KlZedBm9_xVDUMTTtNN4mm1JlBGyA", + "y": "L2etlQlBnmL5CX7MRkEuewOV8UHYp2OKYBFpoEsD1Zw" + }, + "purpose": [ + "auth", + "general" + ] + } + ], + "service_endpoints": [ + { + "id": "serviceEndpointId123", + "type": "someType", + "endpoint": "https://www.url.com" + } + ] + } + } + ] + } } \ No newline at end of file diff --git a/tests/fixtures/update/resultingDocument.json b/tests/fixtures/update/resultingDocument.json index f191db13b..2cbe3c2c9 100644 --- a/tests/fixtures/update/resultingDocument.json +++ b/tests/fixtures/update/resultingDocument.json @@ -1,11 +1,11 @@ { "@context": "https://www.w3.org/ns/did-resolution/v1", "didDocument": { - "id": "did:sidetree:EiBbDEkbYF5XUBraDOv92wSLaZfetEJETtWvboqFngyAGw", + "id": "did:sidetree:EiBx43KticwcUiN9X93G-msVuB5xLVWlcNYFT8GJXm5Nwg", "@context": [ "https://www.w3.org/ns/did/v1", { - "@base": "did:sidetree:EiBbDEkbYF5XUBraDOv92wSLaZfetEJETtWvboqFngyAGw" + "@base": "did:sidetree:EiBx43KticwcUiN9X93G-msVuB5xLVWlcNYFT8GJXm5Nwg" } ], "service": [ @@ -23,30 +23,30 @@ "publicKeyJwk": { "kty": "EC", "crv": "secp256k1", - "x": "JNO1XlDeQ48qPxk6AMn6E633qbXu5WK_n_Nkf0o5kWc", - "y": "QwPpBJu5tPZHi2iyW9YmtmYitc7bKoNncBk8Rag6ESQ" + "x": "rnua16iJUNo443KlZedBm9_xVDUMTTtNN4mm1JlBGyA", + "y": "L2etlQlBnmL5CX7MRkEuewOV8UHYp2OKYBFpoEsD1Zw" } }, { - "id": "#additionalKey", + "id": "#newKeyId", "controller": "", "type": "EcdsaSecp256k1VerificationKey2019", "publicKeyJwk": { "kty": "EC", "crv": "secp256k1", - "x": "AEWEjyzZNUky980Yo7AayJbth5P_5ntuNawJarPmgHg", - "y": "kjHFCDH-ZfVxd1EZXjAyWoYSX8r7A3Po2qBcImrprwU" + "x": "Chs7Z51pFL2YDr5Mn6abWOSHi2LJjo7kZKhM5BuOvbM", + "y": "9MggrGvRNHHls0hXEsZZETLizRvqsQxJtPMqiLwGyVI" } } ], "authentication": [ "#signingKey", - "#additionalKey" + "#newKeyId" ] }, "methodMetadata": { "published": true, - "recoveryCommitment": "EiAyVInmF_nzpaefyWj6BoNGdY_9FRNef-WI3crrK6vIyA", - "updateCommitment": "EiCsjz_gpVsVvp1ndilC8HLJ8ZiPLAP9tq5pqX9N5fm79w" + "recoveryCommitment": "EiAPN6FqOS487qru2NgQmnu6zFOTeOaPsH7HbGQmLmUZpQ", + "updateCommitment": "EiB3AABjBMu2u61U7J1JJL-Srrt5Ypz0Ec6dYMQW1V4dXA" } } \ No newline at end of file diff --git a/tests/fixtures/update/update.json b/tests/fixtures/update/update.json index 5cb2fcbb3..c92d8a260 100644 --- a/tests/fixtures/update/update.json +++ b/tests/fixtures/update/update.json @@ -1,6 +1,29 @@ { "type": "update", - "did_suffix": "EiBbDEkbYF5XUBraDOv92wSLaZfetEJETtWvboqFngyAGw", - "delta": "eyJwYXRjaGVzIjpbeyJhY3Rpb24iOiJhZGQtcHVibGljLWtleXMiLCJwdWJsaWNfa2V5cyI6W3siaWQiOiJhZGRpdGlvbmFsS2V5IiwidHlwZSI6IkVjZHNhU2VjcDI1NmsxVmVyaWZpY2F0aW9uS2V5MjAxOSIsImp3ayI6eyJrdHkiOiJFQyIsImNydiI6InNlY3AyNTZrMSIsIngiOiJBRVdFanl6Wk5Va3k5ODBZbzdBYXlKYnRoNVBfNW50dU5hd0phclBtZ0hnIiwieSI6ImtqSEZDREgtWmZWeGQxRVpYakF5V29ZU1g4cjdBM1BvMnFCY0ltcnByd1UifSwicHVycG9zZSI6WyJhdXRoIiwiZ2VuZXJhbCJdfV19XSwidXBkYXRlX2NvbW1pdG1lbnQiOiJFaUNzanpfZ3BWc1Z2cDFuZGlsQzhITEo4WmlQTEFQOXRxNXBxWDlONWZtNzl3In0", - "signed_data": "eyJhbGciOiJFUzI1NksifQ.eyJ1cGRhdGVfa2V5Ijp7Imt0eSI6IkVDIiwiY3J2Ijoic2VjcDI1NmsxIiwieCI6IlFLQWVOc0hYOGwycEZxQmhhQVI2SmdSMWRjVUR0Q011cTYzc0ZtZEVGTWciLCJ5IjoiSGs3dy0xZExGWFY4Ymk5TW4xQlptZjJ1ZEVyekZYZy1HOWQ4cUw3T3IyUSJ9LCJkZWx0YV9oYXNoIjoiRWlEVHBrVndDa29UbUw5X0Z1OEh4WGQ0MnJzdVRCRjhuUHdxVTM3bndLNTdLQSJ9.7vEjFkKLtjk53IUDUJHKhfCvEfkgxngSg57e1sg4vRiw022--Aye6srkn-OANvEmmlnB9whCKRNDvsVg8w8fKA" + "did_suffix": "EiBx43KticwcUiN9X93G-msVuB5xLVWlcNYFT8GJXm5Nwg", + "delta": { + "patches": [ + { + "action": "add-public-keys", + "public_keys": [ + { + "id": "newKeyId", + "type": "EcdsaSecp256k1VerificationKey2019", + "jwk": { + "kty": "EC", + "crv": "secp256k1", + "x": "Chs7Z51pFL2YDr5Mn6abWOSHi2LJjo7kZKhM5BuOvbM", + "y": "9MggrGvRNHHls0hXEsZZETLizRvqsQxJtPMqiLwGyVI" + }, + "purpose": [ + "auth", + "general" + ] + } + ] + } + ], + "update_commitment": "EiB3AABjBMu2u61U7J1JJL-Srrt5Ypz0Ec6dYMQW1V4dXA" + }, + "signed_data": "eyJhbGciOiJFUzI1NksifQ.eyJ1cGRhdGVfa2V5Ijp7Imt0eSI6IkVDIiwiY3J2Ijoic2VjcDI1NmsxIiwieCI6ImxQd1JQNHViQ0VYdkVLd09wb0NxUG13YlFFeHFpd2s3cm9NSjJTaFJfRFUiLCJ5IjoiaHkzSmw5VTJ1a3hDa2tlcmN4alV4TVZOTXp3cVVFUDhuV3NYdTRwWU5kcyJ9LCJkZWx0YV9oYXNoIjoiRWlDOWx4eHlnOWhiVEFmUnByekVBTm1uckx3cGFaRHZOb2NGdmVrVy1EQVhidyJ9.LBtyNgEaC_ocpsSOtMobHuikV-Lm-9ptK9t-9XQ2r7L94nO2TLl6ZDG7DmUUrYkNlrSOLhYaGfo8N5t7rkkQ-Q" } \ No newline at end of file diff --git a/tests/generators/OperationGenerator.ts b/tests/generators/OperationGenerator.ts index fab9aee62..da03c45d7 100644 --- a/tests/generators/OperationGenerator.ts +++ b/tests/generators/OperationGenerator.ts @@ -1,12 +1,14 @@ import * as crypto from 'crypto'; -import AnchoredOperationModel from '../../lib/core/models/AnchoredOperationModel'; import AnchorFile from '../../lib/core/versions/latest/AnchorFile'; +import AnchoredOperationModel from '../../lib/core/models/AnchoredOperationModel'; import CreateOperation from '../../lib/core/versions/latest/CreateOperation'; +import DataGenerator from './DataGenerator'; import DeactivateOperation from '../../lib/core/versions/latest/DeactivateOperation'; import DocumentModel from '../../lib/core/versions/latest/models/DocumentModel'; import Encoder from '../../lib/core/versions/latest/Encoder'; -import JwkEs256k from '../../lib/core/models/JwkEs256k'; +import JsonCanonicalizer from '../../lib/core/versions/latest/util/JsonCanonicalizer'; import Jwk from '../../lib/core/versions/latest/util/Jwk'; +import JwkEs256k from '../../lib/core/models/JwkEs256k'; import Jws from '../../lib/core/versions/latest/util/Jws'; import Multihash from '../../lib/core/versions/latest/Multihash'; import OperationModel from '../../lib/core/versions/latest/models/OperationModel'; @@ -17,7 +19,6 @@ import RecoverOperation from '../../lib/core/versions/latest/RecoverOperation'; import ServiceEndpointModel from '../../lib/core/versions/latest/models/ServiceEndpointModel'; import TransactionModel from '../../lib/common/models/TransactionModel'; import UpdateOperation from '../../lib/core/versions/latest/UpdateOperation'; -import DataGenerator from './DataGenerator'; interface AnchoredCreateOperationGenerationInput { transactionNumber: number; @@ -117,6 +118,63 @@ export default class OperationGenerator { }; } + /** + * generate a long form did + * @param recoveryPublicKey + * @param updatePublicKey + * @param otherPublicKeys + * @param serviceEndpoints + */ + public static async generateLongFormDid ( + otherPublicKeys?: PublicKeyModel[], + serviceEndpoints?: ServiceEndpointModel[], + network?: string) { + + const document = { + public_keys: otherPublicKeys || [], + service_endpoints: serviceEndpoints || [] + }; + + const patches = [{ + action: 'replace', + document + }]; + + const [recoveryPublicKey] = await Jwk.generateEs256kKeyPair(); + const [updatePublicKey] = await Jwk.generateEs256kKeyPair(); + + const delta = { + update_commitment: Multihash.canonicalizeThenDoubleHashThenEncode(updatePublicKey), + patches + }; + + const deltaHash = Multihash.canonicalizeThenHashThenEncode(delta); + + const suffixData = { + delta_hash: deltaHash, + recovery_commitment: Multihash.canonicalizeThenDoubleHashThenEncode(recoveryPublicKey) + }; + + const didUniqueSuffix = CreateOperation['computeJcsDidUniqueSuffix'](suffixData); + + const shortFormDid = network ? `did:sidetree:${network}:${didUniqueSuffix}` : `did:sidetree:${didUniqueSuffix}`; + + const initialState = { + suffix_data: suffixData, + delta: delta + }; + + const canonicalizedInitialStateBuffer = JsonCanonicalizer.canonicalizeAsBuffer(initialState); + const encodedCanonicalizedInitialStateString = Encoder.encode(canonicalizedInitialStateBuffer); + + const longFormDid = `${shortFormDid}:${encodedCanonicalizedInitialStateString}`; + return { + longFormDid, + shortFormDid, + didUniqueSuffix + }; + } + /** * Generates a create operation. */ @@ -258,20 +316,17 @@ export default class OperationGenerator { patches }; - const deltaBuffer = Buffer.from(JSON.stringify(delta)); - const deltaHash = Encoder.encode(Multihash.hash(deltaBuffer)); + const deltaHash = Multihash.canonicalizeThenHashThenEncode(delta); const suffixData = { delta_hash: deltaHash, recovery_commitment: Multihash.canonicalizeThenDoubleHashThenEncode(recoveryPublicKey) }; - const suffixDataEncodedString = Encoder.encode(JSON.stringify(suffixData)); - const deltaEncodedString = Encoder.encode(deltaBuffer); const operation = { type: OperationType.Create, - suffix_data: suffixDataEncodedString, - delta: deltaEncodedString + suffix_data: suffixData, + delta: delta }; return operation; @@ -330,9 +385,7 @@ export default class OperationGenerator { patches, update_commitment: nextUpdateCommitmentHash }; - const deltaJsonString = JSON.stringify(delta); - const deltaHash = Encoder.encode(Multihash.hash(Buffer.from(deltaJsonString))); - const encodedDeltaString = Encoder.encode(deltaJsonString); + const deltaHash = Multihash.canonicalizeThenHashThenEncode(delta); const signedDataPayloadObject = { update_key: updatePublicKey, @@ -343,7 +396,7 @@ export default class OperationGenerator { const updateOperationRequest = { type: OperationType.Update, did_suffix: didUniqueSuffix, - delta: encodedDeltaString, + delta: delta, signed_data: signedData }; @@ -390,8 +443,7 @@ export default class OperationGenerator { update_commitment: nextUpdateCommitmentHash }; - const deltaBuffer = Buffer.from(JSON.stringify(delta)); - const deltaHash = Encoder.encode(Multihash.hash(deltaBuffer)); + const deltaHash = Multihash.canonicalizeThenHashThenEncode(delta); const signedDataPayloadObject = { delta_hash: deltaHash, @@ -400,12 +452,11 @@ export default class OperationGenerator { }; const signedData = await OperationGenerator.signUsingEs256k(signedDataPayloadObject, recoveryPrivateKey); - const deltaEncodedString = Encoder.encode(deltaBuffer); const operation = { type: OperationType.Recover, did_suffix: didUniqueSuffix, signed_data: signedData, - delta: deltaEncodedString + delta: delta }; return operation; @@ -562,9 +613,9 @@ export default class OperationGenerator { for (const id of ids) { serviceEndpoints.push( { - 'id': id, - 'type': 'someType', - 'endpoint': 'https://www.url.com' + id: id, + type: 'someType', + endpoint: 'https://www.url.com' } ); } diff --git a/tests/generators/TestVectorGenerator.ts b/tests/generators/TestVectorGenerator.ts new file mode 100644 index 000000000..b02e32671 --- /dev/null +++ b/tests/generators/TestVectorGenerator.ts @@ -0,0 +1,71 @@ +import * as fs from 'fs'; +import Jwk from "../../lib/core/versions/latest/util/Jwk"; +import Multihash from "../../lib/core/versions/latest/Multihash"; +import OperationGenerator from "./OperationGenerator"; + +export default class TestVectorGenerator { + /** + * Generate and prints out test vectors for operation requests + */ + public static async generateOperationVectors(writeToDisc?: boolean, saveLocation?: string) { + + // generate a create operation request + const createOperationData = await OperationGenerator.generateCreateOperation(); + + // derive an update operation request from the create + const [nextUpdateKey] = await OperationGenerator.generateKeyPair('nextUpdateKey'); + const nextUpdateCommitmentHash = Multihash.canonicalizeThenDoubleHashThenEncode(nextUpdateKey.jwk); + const [anyNewSigningKey] = await OperationGenerator.generateKeyPair('newKeyId'); + const patches = [ + { + action: 'add-public-keys', + public_keys: [ + anyNewSigningKey + ] + } + ]; + const updateRequest = await OperationGenerator.createUpdateOperationRequest( + createOperationData.createOperation.didUniqueSuffix, + createOperationData.updatePublicKey, + createOperationData.updatePrivateKey, + nextUpdateCommitmentHash, + patches + ) + + // derive a recover operation from the create operation + const [newRecoveryPublicKey] = await Jwk.generateEs256kKeyPair(); + const [newSigningPublicKey] = await OperationGenerator.generateKeyPair('keyAfterRecover'); + + const [documentKey] = await OperationGenerator.generateKeyPair('newDocumentKey'); + const newServiceEndpoints = OperationGenerator.generateServiceEndpoints(['newId']); + + const recoverOperationRequest = await OperationGenerator.generateRecoverOperationRequest( + createOperationData.createOperation.didUniqueSuffix, + createOperationData.recoveryPrivateKey, + newRecoveryPublicKey, + newSigningPublicKey, + newServiceEndpoints, + [documentKey] + ); + + const deactivateRequest = await OperationGenerator.createDeactivateOperationRequest(createOperationData.createOperation.didUniqueSuffix, createOperationData.recoveryPrivateKey); + + + const createRequestString = JSON.stringify(createOperationData.operationRequest, null, 2); + const updateRequestString = JSON.stringify(updateRequest, null, 2); + const recoverRequestString = JSON.stringify(recoverOperationRequest, null, 2); + const deactivateRequestString = JSON.stringify(deactivateRequest); + + if (writeToDisc && saveLocation) { + fs.writeFileSync(`${saveLocation}/create.json`, createRequestString); + fs.writeFileSync(`${saveLocation}/update.json`, updateRequestString); + fs.writeFileSync(`${saveLocation}/recover.json`, recoverRequestString); + fs.writeFileSync(`${saveLocation}/deactivate.json`, deactivateRequestString); + } else { + console.log(createRequestString); + console.log(updateRequestString); + console.log(recoverRequestString); + console.log(deactivateRequestString); + } + } +} \ No newline at end of file diff --git a/tests/generators/vegeta.ts b/tests/generators/vegeta.ts index f7baebcd9..ff06dd963 100644 --- a/tests/generators/vegeta.ts +++ b/tests/generators/vegeta.ts @@ -7,7 +7,7 @@ const uniqueDidCount = 20000; const endpointUrl = 'http://localhost:3000/'; const outputFolder = `d:/vegeta-localhost-jws`; -void (async () => { +(async () => { console.info(`Generating load requests...`); const startTime = process.hrtime(); // For calcuating time taken to process operations. await VegetaLoadGenerator.generateLoadFiles(uniqueDidCount, endpointUrl, outputFolder); diff --git a/tests/ipfs/Ipfs.spec.ts b/tests/ipfs/Ipfs.spec.ts index 557132cef..1e8a44352 100644 --- a/tests/ipfs/Ipfs.spec.ts +++ b/tests/ipfs/Ipfs.spec.ts @@ -1,9 +1,9 @@ import FetchResultCode from '../../lib/common/enums/FetchResultCode'; -import ReadableStream from '../../lib/common/ReadableStream'; import ICas from '../../lib/core/interfaces/ICas'; import Ipfs from '../../lib/ipfs/Ipfs'; import IpfsErrorCode from '../../lib/ipfs/IpfsErrorCode'; import JasmineSidetreeErrorValidator from '../JasmineSidetreeErrorValidator'; +import ReadableStream from '../../lib/common/ReadableStream'; import SharedErrorCode from '../../lib/common/SharedErrorCode'; import SidetreeError from '../../lib/common/SidetreeError'; import Timeout from '../../lib/ipfs/Util/Timeout'; diff --git a/tests/ipfs/Timeout.spec.ts b/tests/ipfs/Timeout.spec.ts index 9d49afafd..68060ff53 100644 --- a/tests/ipfs/Timeout.spec.ts +++ b/tests/ipfs/Timeout.spec.ts @@ -1,12 +1,12 @@ import IpfsErrorCode from '../../lib/ipfs/IpfsErrorCode'; -import Timeout from '../../lib/ipfs/Util/Timeout'; import JasmineSidetreeErrorValidator from '../JasmineSidetreeErrorValidator'; +import Timeout from '../../lib/ipfs/Util/Timeout'; describe('Timeout', async () => { describe('timeout()', async () => { it('should timeout if given task took too long.', async (done) => { // A 10 second running promise. - const longRunningPromise = new Promise((resolve, _reject) => { + const longRunningPromise = new Promise((resolve) => { setTimeout( () => { resolve(1); }, 10); @@ -22,7 +22,7 @@ describe('Timeout', async () => { it('should return error thrown by the task.', async (done) => { const error = new Error('some bad error'); - const aPromiseThatThrowsError = new Promise((_resolve, _reject) => { + const aPromiseThatThrowsError = new Promise(() => { throw error; }); diff --git a/tests/json/bitcoin-config-test.json b/tests/json/bitcoin-config-test.json deleted file mode 100644 index 24296c8dc..000000000 --- a/tests/json/bitcoin-config-test.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "bitcoinPeerUri": "unused", - "bitcoinWalletImportString": "cVneRge89Va3mrxv7dehTYTknbgLVan8rY5hkbXUwdwXtZrrueGY", - "sidetreeTransactionPrefix": "sidetree:", - "genesisBlockNumber": 1500000, - "databaseName": "sidetree-bitcoin", - "mongoDbConnectionString": "mongodb://localhost:27017/", - "port": 3002, - "sidetreeTransactionFeeMarkupPercentage": 10 -} diff --git a/tests/json/config-test.json b/tests/json/config-test.json index 808f79093..3d23ef512 100644 --- a/tests/json/config-test.json +++ b/tests/json/config-test.json @@ -1,10 +1,10 @@ { - "port": 3000, + "databaseName": "testMongodbName", "didMethodName": "sidetree", "blockchainServiceUri": "http://127.0.0.1:3002", "batchingIntervalInSeconds": 10, - "observingIntervalInSeconds": 10, "maxConcurrentDownloads": 20, "mongoDbConnectionString": "mongodb://localhost:27017/", - "databaseName": "testMongodbName" + "observingIntervalInSeconds": 10, + "port": 3000 } diff --git a/tests/mocks/MockBlockchain.ts b/tests/mocks/MockBlockchain.ts index 135d93b09..74da91dc1 100644 --- a/tests/mocks/MockBlockchain.ts +++ b/tests/mocks/MockBlockchain.ts @@ -56,6 +56,7 @@ export default class MockBlockchain implements IBlockchain { public get approximateTime (): BlockchainTimeModel { return this.latestTime!; } + /** * Hardcodes the latest time to be returned. */ diff --git a/tests/mocks/MockOperationStore.ts b/tests/mocks/MockOperationStore.ts index 2a3bfa0ba..5bb019d4e 100644 --- a/tests/mocks/MockOperationStore.ts +++ b/tests/mocks/MockOperationStore.ts @@ -68,8 +68,8 @@ export default class MockOperationStore implements IOperationStore { // Sort needed if there was a put operation since last sort. if (updatedSinceLastSort) { - didOps.sort(compareOperation); // in-place sort - didOps = didOps.filter((elem, index, self) => { // remove duplicates + didOps.sort(compareOperation); // in-place sort + didOps = didOps.filter((elem, index, self) => { // remove duplicates return (index === 0) || compareOperation(elem, self[index - 1]) !== 0; }); this.didUpdatedSinceLastSort.set(didUniqueSuffix, false); @@ -96,9 +96,7 @@ export default class MockOperationStore implements IOperationStore { } } - public async deleteUpdatesEarlierThan (_didUniqueSuffix: string, _transactionNumber: number, _operationIndex: number): Promise { - return; - } + public async deleteUpdatesEarlierThan (_didUniqueSuffix: string, _transactionNumber: number, _operationIndex: number): Promise { } /** * Remove operations. A simple linear scan + filter that leaves the @@ -107,7 +105,7 @@ export default class MockOperationStore implements IOperationStore { private static removeOperations (operations: AnchoredOperationModel[], transactionNumber: number) { let writeIndex = 0; - for (let i = 0 ; i < operations.length ; i++) { + for (let i = 0; i < operations.length; i++) { if (operations[i].transactionNumber <= transactionNumber) { operations[writeIndex++] = operations[i]; } @@ -120,7 +118,7 @@ export default class MockOperationStore implements IOperationStore { private ensureDidContainerExist (did: string) { if (this.didToOperations.get(did) === undefined) { - this.didToOperations.set(did, new Array()); + this.didToOperations.set(did, []); this.didUpdatedSinceLastSort.set(did, false); } } diff --git a/tests/mocks/MockTransactionStore.ts b/tests/mocks/MockTransactionStore.ts index 2fc8b5552..e017326b0 100644 --- a/tests/mocks/MockTransactionStore.ts +++ b/tests/mocks/MockTransactionStore.ts @@ -112,10 +112,10 @@ export default class MockTransactionStore implements ITransactionStore, IUnresol } // Locate the index of the given transaction using binary search. - const compareTransactionAndTransactionNumber - = (transaction: TransactionModel, transactionNumber: number) => { return transaction.transactionNumber - transactionNumber; }; - const bestKnownValidRecentProcessedTransactionIndex - = SortedArray.binarySearch(this.processedTransactions, transactionNumber, compareTransactionAndTransactionNumber); + const compareTransactionAndTransactionNumber = + (transaction: TransactionModel, transactionNumber: number) => { return transaction.transactionNumber - transactionNumber; }; + const bestKnownValidRecentProcessedTransactionIndex = + SortedArray.binarySearch(this.processedTransactions, transactionNumber, compareTransactionAndTransactionNumber); // The following conditions should never be possible. if (bestKnownValidRecentProcessedTransactionIndex === undefined) { diff --git a/tests/mocks/MockVersionManager.ts b/tests/mocks/MockVersionManager.ts index e6fac9486..b6ec21df5 100644 --- a/tests/mocks/MockVersionManager.ts +++ b/tests/mocks/MockVersionManager.ts @@ -2,28 +2,29 @@ import IBatchWriter from '../../lib/core/interfaces/IBatchWriter'; import IOperationProcessor from '../../lib/core/interfaces/IOperationProcessor'; import IRequestHandler from '../../lib/core/interfaces/IRequestHandler'; import ITransactionProcessor from '../../lib/core/interfaces/ITransactionProcessor'; -import IVersionManager from '../../lib/core/interfaces/IVersionManager'; import ITransactionSelector from '../../lib/core/interfaces/ITransactionSelector'; +import IVersionManager from '../../lib/core/interfaces/IVersionManager'; /** * Mock version manager for testing. */ export default class MockVersionManager implements IVersionManager { - /* tslint:disable-next-line */ - public constructor () {} - public getBatchWriter (blockchainTime: number): IBatchWriter { throw new Error('Not implemented. Use spyOn to override the functionality. Input: ' + blockchainTime); } + public getOperationProcessor (blockchainTime: number): IOperationProcessor { throw new Error('Not implemented. Use spyOn to override the functionality. Input: ' + blockchainTime); } + public getRequestHandler (blockchainTime: number): IRequestHandler { throw new Error('Not implemented. Use spyOn to override the functionality. Input: ' + blockchainTime); } + public getTransactionProcessor (blockchainTime: number): ITransactionProcessor { throw new Error('Not implemented. Use spyOn to override the functionality. Input: ' + blockchainTime); } + public getTransactionSelector (blockchainTime: number): ITransactionSelector { throw new Error('Not implemented. Use spyOn to override the functionality. Input: ' + blockchainTime); } diff --git a/tslint.json b/tslint.json deleted file mode 100644 index d70f274ca..000000000 --- a/tslint.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "extends": "tslint-config-standard", - "linterOptions": { - "exclude": [ - "node_modules/**", - "**/*.json" - ] - }, - "defaultSeverity": "error", - "rules": { - "max-line-length": [ - true, - 160 - ], - "completed-docs": [ - true, - { - "classes": {"visibilities": ["exported"]}, - "enums": {"visibilities": ["exported"]}, - "functions": {"visibilities": ["exported"]}, - "interfaces": {"visibilities": ["exported"]}, - "methods": {"privacies": ["public", "protected"], - "locations": ["all"]}, - "properties": {"privacies": ["public", "protected"], - "locations": ["all"]}, - "types": {"visibilities": ["all"]} - } - ], - "jsdoc-format": [ - true, - "check-multiline-start" - ], - "variable-name": [ - true, - "check-format", - "allow-leading-underscore" - ], - "semicolon": [ - true, - "always" - ], - "only-arrow-functions": [ - true, - "allow-named-functions" - ] - } -} \ No newline at end of file