diff --git a/app/api/controllers/attestation_data.ts b/app/api/controllers/attestation_data.ts new file mode 100644 index 0000000..77bd9c8 --- /dev/null +++ b/app/api/controllers/attestation_data.ts @@ -0,0 +1,46 @@ +import * as m from '@shared/models' + +// perform attestation on an existing request +export const show = async (req: any, res: any) => { + const ad = await m.AttestationData.findById(req.params.attestation_data_id) + + if (!ad) { + return res.status(404).json({success: false, message: 'Not found'}) + } + + const adRaw = ad.get({plain: true}) + + if (!ad.testChallenge(req.query.passphrase)) { + res + .status(401) + .json({success: false, message: 'Invalid credentials for attestation data'}) + } + + res.json({ + success: true, + attestation_data: { + id: adRaw.id, + created: adRaw.created, + updated: adRaw.updated, + data: adRaw.datatype === 'text' ? adRaw.dtext : adRaw.dblob, + datatype: adRaw.datatype, + messageType: adRaw.messageType, + }, + }) +} + +export const destroy = async (req: any, res: any) => { + const ad = await m.AttestationData.findById(req.params.attestation_data_id) + + if (!ad) { + return res.status(404).json({success: false, message: 'Not found'}) + } + + if (!ad.testChallenge(req.query.passphrase)) { + res + .status(401) + .json({success: false, message: 'Invalid credentials for attestation data'}) + } + + await ad.destroy() +} diff --git a/app/api/index.ts b/app/api/index.ts index 0d4853b..f0df34c 100644 --- a/app/api/index.ts +++ b/app/api/index.ts @@ -1,11 +1,13 @@ import * as express from 'express' // import * as path from 'path' import {env} from '@shared/environment' -import * as attCtrl from '@api/controllers/attestation' -import * as reqCtrl from '@api/controllers/request' import {sha256} from 'ethereumjs-util' import * as bodyParser from 'body-parser' +import * as attCtrl from '@api/controllers/attestation' +import * as reqCtrl from '@api/controllers/request' +import * as atDatCtrl from '@api/controllers/attestation_data' + const app = express() interface IRequestWithRawBody extends express.Request { @@ -49,6 +51,9 @@ app.post('/api/requests/send', reqCtrl.sendjob) app.get('/api/attestations', attCtrl.show) app.post('/api/attestations', attCtrl.perform) +app.get('/api/attestation_data/:attestation_data_id', atDatCtrl.show) +app.delete('/api/attestation_data/:attestation_data_id', atDatCtrl.destroy) + app.listen(13000, () => console.log('App listening on port 13000')) export default app diff --git a/app/migrations/20180920161631-create-attestation-data.js b/app/migrations/20180920161631-create-attestation-data.js new file mode 100644 index 0000000..4b99414 --- /dev/null +++ b/app/migrations/20180920161631-create-attestation-data.js @@ -0,0 +1,30 @@ +'use strict' +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.sequelize.query(` + CREATE TYPE "attestation_data_datatype" + AS ENUM('text','blob'); + + CREATE TABLE "attestationData" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + "created" timestamp without time zone DEFAULT now() NOT NULL, + "updated" timestamp without time zone DEFAULT now() NOT NULL, + + "attestationId" uuid, + "messageType" enum_whisper_msg_types, + + "datatype" attestation_data_datatype, + "dtext" text, + "dblob" blob, + + "challenge" varchar(256) + ); + `) + }, + down: (queryInterface, Sequelize) => { + return queryInterface.sequelize.query(` + drop table AttestationData; + drop type attestation_data_datatype; + `) + }, +} diff --git a/app/package.json b/app/package.json index a4f51eb..c8fb977 100644 --- a/app/package.json +++ b/app/package.json @@ -34,6 +34,8 @@ "@types/webpack": "3.8.1", "@types/winston": "^2.3.7", "async": "^2.6.0", + "attestations-lib": "https://github.com/hellobloom/attestations-lib/releases/download/v0.2.1/attestations-lib-v0.2.1.tgz", + "axios": "^0.18.0", "babel-preset-stage-3": "^6.24.1", "babel-register": "^6.26.0", "bignumber.js": "^5.0.0", diff --git a/app/shared/attestations/validateAttestParams.ts b/app/shared/attestations/validateAttestParams.ts index aecc48d..afebd1a 100644 --- a/app/shared/attestations/validateAttestParams.ts +++ b/app/shared/attestations/validateAttestParams.ts @@ -4,8 +4,8 @@ import {uniq} from 'lodash' import {TUnvalidated} from '@shared/params/validation' import * as U from '@shared/utils' import {getFormattedTypedDataReleaseTokensLegacy} from '@shared/ethereum/signingLogic' +import {TAttestationDataJSONB} from '@shared/models/Attestation' import {AttestationTypeID, HashingLogic} from '@bloomprotocol/attestations-lib' -import {IAttestationDataJSONB} from '@shared/models/Attestations/Attestation' import BigNumber from 'bignumber.js' import {requiredField} from '@shared/requiredField' import {serverLogger} from '@shared/logger' @@ -30,7 +30,7 @@ export interface IUnvalidatedAttestParams { reward: BigNumber paymentNonce: string requesterSig: string - data: IAttestationDataJSONB + data: TAttestationDataJSONB types: AttestationTypeID[] requestNonce: string subjectSig: string diff --git a/app/shared/attestations/validateJobDetails.ts b/app/shared/attestations/validateJobDetails.ts index 11d5a23..1ce5a9c 100644 --- a/app/shared/attestations/validateJobDetails.ts +++ b/app/shared/attestations/validateJobDetails.ts @@ -10,7 +10,10 @@ import { TAttestationTypeNames, HashingLogic, } from '@bloomprotocol/attestations-lib' -import {IAttestationDataJSONB} from '@shared/models/Attestations/Attestation' +import { + TMaybeAttestationDataJSONB, + TAttestationDataJSONB, +} from '@shared/models/Attestation' import {requiredField} from '@shared/requiredField' import {every} from 'lodash' @@ -29,8 +32,18 @@ interface ISuccess { export type TValidateJobDetailsOutput = IInvalidParamError | ISuccess +export interface IMaybeJobDetails { + data: TMaybeAttestationDataJSONB + requestNonce: string + types: number[] + subject: string + subjectSig: string + attester: string + requester: string +} + export interface IJobDetails { - data: IAttestationDataJSONB + data: TMaybeAttestationDataJSONB requestNonce: string types: number[] subject: string @@ -250,7 +263,7 @@ export const validateSubjectDataComponent = ( } export const validateSubjectData = ( - input: IAttestationDataJSONB, + input: TAttestationDataJSONB, type: AttestationTypeID[] ): boolean => { console.log(`validate input: ${JSON.stringify(input)}`) diff --git a/app/shared/environment.ts b/app/shared/environment.ts index 7789d0f..63fd4be 100644 --- a/app/shared/environment.ts +++ b/app/shared/environment.ts @@ -5,6 +5,7 @@ import {toBuffer} from 'ethereumjs-util' dotenv.config() interface IEnvironmentConfig { + hostname: string apiKey: string appId: string appPort: number @@ -185,6 +186,7 @@ const topics: any = envVar('WHISPER_TOPICS', 'json') }) export const env: IEnvironmentConfig = { + hostname: envVar('HOSTNAME'), apiKey: envVar('API_KEY_SHA256'), appId: envVar('APP_ID', 'string', true), // Mark with something meaningful to indicate which environment, e.g., attestation-kit_dev_bob appPort: envVar('PORT', 'int', false, 3000), diff --git a/app/shared/models/Attestations/Attestation.ts b/app/shared/models/Attestation.ts similarity index 91% rename from app/shared/models/Attestations/Attestation.ts rename to app/shared/models/Attestation.ts index c39b697..3baf09f 100644 --- a/app/shared/models/Attestations/Attestation.ts +++ b/app/shared/models/Attestation.ts @@ -36,9 +36,26 @@ export interface IAttestationResult { certainty?: number } +export interface IDataRetrievalKit { + dataHostedExternally: boolean + nodeHost: string + attestationDataId: string + passphrase: string +} + +export interface IDataRetrievalOther { + dataHostedExternally: boolean + retrievalUrl: string + deletionUrl: string +} + export type AttestationRole = 'attester' | 'requester' -export type IAttestationDataJSONB = IEmailAttestationJSONB | IPhoneAttestationJSONB +export type TAttestationDataJSONB = IEmailAttestationJSONB | IPhoneAttestationJSONB + +export type TUnresolvedAttestationData = IDataRetrievalKit | IDataRetrievalOther + +export type TMaybeAttestationDataJSONB = TUnresolvedAttestationData | TAttestationDataJSONB export const AttestationStatusDataType = Sequelize.DataType.ENUM( Object.keys(AttestationStatus) @@ -90,7 +107,7 @@ export default class Attestation extends Sequelize.Model { get data() { return this.getDataValue('data') } - set data(value: IAttestationDataJSONB) { + set data(value: TAttestationDataJSONB) { this.setDataValue('data', value) } @@ -160,7 +177,8 @@ export default class Attestation extends Sequelize.Model { ) } - validateJobDetailsView(): IJobDetails { + validateJobDetailsView = (): IJobDetails => { + if (!this.data || !this.data.data) throw new Error('Invalid data') return { // Making sure the data prop only contains what the validate job details cares about data: { diff --git a/app/shared/models/AttestationData.ts b/app/shared/models/AttestationData.ts new file mode 100644 index 0000000..f29a416 --- /dev/null +++ b/app/shared/models/AttestationData.ts @@ -0,0 +1,72 @@ +import * as Sequelize from 'sequelize-typescript' +import {Attestation} from '@shared/models' +import {WhisperMsgDataType} from '@shared/models/NegotiationMsg' +import {bufferToHex, sha256} from 'ethereumjs-util' + +@Sequelize.Table({tableName: 'attestation_data'}) +export default class AttestationData extends Sequelize.Model { + @Sequelize.Column({ + type: Sequelize.DataType.UUID, + allowNull: false, + autoIncrement: false, + primaryKey: true, + }) + id: string + + @Sequelize.CreatedAt created: Date + + @Sequelize.UpdatedAt updated: Date + + @Sequelize.Column({ + type: Sequelize.DataType.UUID, + allowNull: false, + autoIncrement: false, + primaryKey: true, + }) + attestationId: string + + @Sequelize.BelongsTo(() => Attestation, { + foreignKey: 'attestationId', + }) + Attestation?: Attestation + + // Column to specify data storage type + @Sequelize.Column({ + allowNull: false, + type: WhisperMsgDataType, + }) + messageType: string + + // Column to specify data storage type + @Sequelize.Column({ + allowNull: false, + type: Sequelize.DataType.STRING, + }) + datatype: 'text' | 'blob' + + // Column for data stored as text + @Sequelize.Column({ + allowNull: false, + type: Sequelize.DataType.TEXT, + }) + dtext: string + + // Column for data stored as blob + @Sequelize.Column({ + allowNull: false, + type: Sequelize.DataType.BLOB, + }) + dblob: Buffer + + // Cryptographic challenge (SHA256'ed password) + @Sequelize.Column({ + allowNull: false, + type: Sequelize.DataType.STRING, + }) + challenge: string + + testChallenge = (passphrase: string) => { + let passphraseHash = bufferToHex(sha256(passphrase)) + return passphrase && this.challenge && passphraseHash === this.challenge + } +} diff --git a/app/shared/models/Attestations/Negotiation.ts b/app/shared/models/Negotiation.ts similarity index 100% rename from app/shared/models/Attestations/Negotiation.ts rename to app/shared/models/Negotiation.ts diff --git a/app/shared/models/Attestations/NegotiationMsg.ts b/app/shared/models/NegotiationMsg.ts similarity index 100% rename from app/shared/models/Attestations/NegotiationMsg.ts rename to app/shared/models/NegotiationMsg.ts diff --git a/app/shared/models/Attestations/WhisperFilters.ts b/app/shared/models/WhisperFilters.ts similarity index 100% rename from app/shared/models/Attestations/WhisperFilters.ts rename to app/shared/models/WhisperFilters.ts diff --git a/app/shared/models/Attestations/generateNonce.ts b/app/shared/models/generateNonce.ts similarity index 100% rename from app/shared/models/Attestations/generateNonce.ts rename to app/shared/models/generateNonce.ts diff --git a/app/shared/models/index.ts b/app/shared/models/index.ts index 0fcf263..08b7774 100644 --- a/app/shared/models/index.ts +++ b/app/shared/models/index.ts @@ -1,10 +1,11 @@ import {Sequelize} from 'sequelize-typescript' import {env} from '@shared/environment' import * as config from '../../config/database' -import Negotiation from '@shared/models/Attestations/Negotiation' -import NegotiationMsg from '@shared/models/Attestations/NegotiationMsg' -import WhisperFilters from '@shared/models/Attestations/WhisperFilters' -import Attestation from '@shared/models/Attestations/Attestation' +import Negotiation from '@shared/models/Negotiation' +import NegotiationMsg from '@shared/models/NegotiationMsg' +import WhisperFilters from '@shared/models/WhisperFilters' +import Attestation from '@shared/models/Attestation' +import AttestationData from '@shared/models/AttestationData' import GasPrice from '@shared/models/GasPrice' import Ping from '@shared/models/Ping' @@ -30,6 +31,7 @@ sequelize.addModels([ NegotiationMsg, WhisperFilters, Attestation, + AttestationData, GasPrice, Ping, ]) @@ -43,6 +45,7 @@ export { sequelize, WhisperFilters, Attestation, + AttestationData, GasPrice, Ping, } diff --git a/app/shared/whisper/attesterActions.test.ts b/app/shared/whisper/attesterActions.test.ts index 7b0c4f4..28f8393 100644 --- a/app/shared/whisper/attesterActions.test.ts +++ b/app/shared/whisper/attesterActions.test.ts @@ -1,22 +1,15 @@ import * as newrelic from 'newrelic' const uuid = require('uuidv4') import BigNumber from 'bignumber.js' -import { - handleSolicitation, - handleJobDetails, -} from '@shared/whisper/attesterActions' -import { - ISolicitation, - ISendJobDetails, - EMsgTypes, -} from '@shared/whisper/msgTypes' +import {handleSolicitation, handleJobDetails} from '@shared/whisper/attesterActions' +import {ISolicitation, ISendJobDetails, EMsgTypes} from '@shared/whisper/msgTypes' import {NegotiationMsg} from '@shared/models' import {MessageSubscribers} from '@shared/whisper/subscriptionHandler' import {toBuffer, bufferToHex} from 'ethereumjs-util' import * as Wallet from 'ethereumjs-wallet' import {signSessionID, signPaymentAuthorization} from '@shared/ethereum/signingLogic' -import {PersistDataTypes} from '@shared/whisper/persistDataHandler' import {signAttestationRequest} from '@shared/ethereum/signingLogic' +import {PersistDataTypes} from '@shared/whisper/persistDataHandler' import { attesterWallet, requesterWallet, diff --git a/app/shared/whisper/attesterActions.ts b/app/shared/whisper/attesterActions.ts index d86b83a..386e3e2 100644 --- a/app/shared/whisper/attesterActions.ts +++ b/app/shared/whisper/attesterActions.ts @@ -35,9 +35,11 @@ import { ExternalActionTypes, } from '@shared/whisper/externalActionHandler' import {hashedTopicToAttestationType} from '@shared/attestations/AttestationUtils' +import {TAttestationDataJSONB} from '@shared/models/Attestation' import {env} from '@shared/environment' +import axios from 'axios' import * as Web3 from 'web3' -import {AttestationTypeID, HashingLogic} from '@bloomprotocol/attestations-lib' +// import {AttestationTypeID, HashingLogic} from '@bloomprotocol/attestations-lib' export const listenForSolicitations = async ( listeningTopic: string, @@ -168,9 +170,10 @@ const startAttestation = ( subjectData: message.subjectData, subjectRequestNonce: message.subjectRequestNonce, type: hashedTopicToAttestationType[messageTopic], - typeIds: message.subjectData.data.map( + typeIds: message.typeIds, + /* typeIds: message.subjectData.data.map( (a: HashingLogic.IAttestationData) => AttestationTypeID[a.type] - ), + ), */ subjectSignature: message.subjectSignature, paymentSignature: message.paymentSignature, paymentNonce: message.paymentNonce, @@ -207,20 +210,50 @@ const startAttestation = ( return decision } +// Retrieve the subject data if the message instructs it to be retrieved over HTTPS out-of-band, and then return the modified message if so +export const checkForMessageRetrieval = async (message: ISendJobDetails) => { + try { + let info = message.subjectData as any + if (info.dataHostedExternally) { + var dataObj + if (info.retrievalUrl) { + dataObj = (await axios.get(info.retrievalUrl)).data + if (info.deletionUrl) { + await axios.delete(info.deletionUrl) + } + } else { + var resourceUrl = `https://${info.nodeHost}/attestation_data/${ + info.attestationDataId + }?passphrase=${info.passphrase}` + dataObj = (await axios.get(resourceUrl)).data + await axios.delete(resourceUrl) + } + message.subjectData = JSON.parse(dataObj.data) + } + return message + } catch (error) { + console.log(error) + throw new Error('Asynchronous data retrieval failed') + } +} + export const handleJobDetails: TMsgHandler = async ( message: ISendJobDetails, messageTopic: string, attesterWallet: Wallet.Wallet ) => { + message = await checkForMessageRetrieval(message) + try { let decision: IMessageDecision const _isApprovedRequester = await isApprovedRequester(message) const _rewardMatchesBid = await rewardMatchesBid(message) const _validateSubjectData = validateSubjectData( - message.subjectData, - message.subjectData.data.map( + message.subjectData as TAttestationDataJSONB, + message.typeIds + /* message.subjectData.data.map( (a: HashingLogic.IAttestationData) => AttestationTypeID[a.type] - ) + ) */ ) serverLogger.info(`validate output: ${_validateSubjectData}`) diff --git a/app/shared/whisper/msgTypes.ts b/app/shared/whisper/msgTypes.ts index f345856..3d8e177 100644 --- a/app/shared/whisper/msgTypes.ts +++ b/app/shared/whisper/msgTypes.ts @@ -1,4 +1,7 @@ -import {IAttestationDataJSONB} from '@shared/models/Attestations/Attestation' +import { + TAttestationDataJSONB, + TUnresolvedAttestationData, +} from '@shared/models/Attestation' export enum EMsgTypes { ping = 'ping', @@ -70,7 +73,7 @@ export interface ISubmitSubjectData extends IBloomWhisperResponse { export interface ISendJobDetails extends IBloomWhisperResponse { messageType: EMsgTypes.sendJobDetails reward: string - subjectData: IAttestationDataJSONB + subjectData: TUnresolvedAttestationData | TAttestationDataJSONB subjectRequestNonce: string typeIds: number[] subjectAddress: string diff --git a/app/shared/whisper/persistDataHandler.ts b/app/shared/whisper/persistDataHandler.ts index 378b732..7557b98 100644 --- a/app/shared/whisper/persistDataHandler.ts +++ b/app/shared/whisper/persistDataHandler.ts @@ -1,7 +1,7 @@ import BigNumber from 'bignumber.js' import {Negotiation, NegotiationMsg, Attestation} from '@shared/models' import {toBuffer} from 'ethereumjs-util' -import {IAttestationDataJSONB} from '@shared/models/Attestations/Attestation' +import {TMaybeAttestationDataJSONB} from '@shared/models/Attestation' import {serverLogger} from '@shared/logger' export enum PersistDataTypes { @@ -72,7 +72,7 @@ export interface IStoreJobDetails { subject: string attester: string requester: string - subjectData: IAttestationDataJSONB + subjectData: TMaybeAttestationDataJSONB subjectRequestNonce: string typeIds: number[] type: string diff --git a/app/shared/whisper/requesterActions.ts b/app/shared/whisper/requesterActions.ts index 326729e..3c13d3a 100644 --- a/app/shared/whisper/requesterActions.ts +++ b/app/shared/whisper/requesterActions.ts @@ -2,7 +2,11 @@ import * as newrelic from 'newrelic' const uuid = require('uuidv4') import BigNumber from 'bignumber.js' import * as Wallet from 'ethereumjs-wallet' -import {toBuffer} from 'ethereumjs-util' +import {bufferToHex, sha256, toBuffer} from 'ethereumjs-util' +import {AttestationData} from '@shared/models' +import {generateNonce} from '@shared/models/generateNonce' +import {IDataRetrievalKit} from '@shared/models/Attestation' +import {env} from '@shared/environment' import { IMessageDecision, @@ -240,6 +244,28 @@ export const sendJobDetails = async ( requesterWallet.getPrivateKey() ) + // Store the subject data in DB and send retrieval details if it's too big + let subjectDataString = JSON.stringify(jobDetails.data.data) + if (subjectDataString.length > 10000) { + var passphrase = generateNonce() + .toString(16) + .substr(0, 64) + const attData = await AttestationData.create({ + attestationId: attestation.id, + challenge: bufferToHex(sha256(passphrase)), + messageType: 'storeSendJobDetails', + datatype: 'text', + dtext: subjectDataString, + }) + const dataRetrievalInstructions: IDataRetrievalKit = { + dataHostedExternally: true, + nodeHost: env.hostname, + attestationDataId: attData.id, + passphrase: passphrase, + } + jobDetails.data.data = dataRetrievalInstructions + } + const messageResponse: ISendJobDetails = { messageType: EMsgTypes.sendJobDetails, replyTo: 'new', diff --git a/app/worker/jobs/submit-attestation.ts b/app/worker/jobs/submit-attestation.ts index f99ee82..9c8a75d 100644 --- a/app/worker/jobs/submit-attestation.ts +++ b/app/worker/jobs/submit-attestation.ts @@ -2,7 +2,7 @@ import {attesterWallet} from '@shared/attestations/attestationWallets' import * as newrelic from 'newrelic' import {Attestation} from '@shared/models' import {sendAttestTx} from '@shared/attestations/sendAttest' -import {IAttestationResult} from '@shared/models/Attestations/Attestation' +import {IAttestationResult} from '@shared/models/Attestation' import {notifyAttestationCompleted} from '@shared/webhookHandler' import {serverLogger} from '@shared/logger' import {env} from '@shared/environment' diff --git a/app/yarn.lock b/app/yarn.lock index f58a438..80c018d 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -824,6 +824,19 @@ atob@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/atob/-/atob-2.0.3.tgz#19c7a760473774468f20b2d2d03372ad7d4cbf5d" +"attestations-lib@https://github.com/hellobloom/attestations-lib/releases/download/v0.2.1/attestations-lib-v0.2.1.tgz": + version "0.2.1" + resolved "https://github.com/hellobloom/attestations-lib/releases/download/v0.2.1/attestations-lib-v0.2.1.tgz#d2b77ee6107df3695f7fea6c41a3afa736573278" + dependencies: + "@types/lodash" "^4.14.116" + js-sha3 "^0.8.0" + lodash "^4.17.10" + merkletreejs "^0.0.10" + ts-node "^7.0.1" + tsconfig-paths "^3.5.0" + uuidv4 "^1.0.1" + web3-utils "^1.0.0-beta.36" + autoprefixer@^6.3.1: version "6.7.7" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.7.7.tgz#1dbd1c835658e35ce3f9984099db00585c782014" @@ -847,6 +860,14 @@ aws4@^1.2.1, aws4@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" +axios@^0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.0.tgz#32d53e4851efdc0a11993b6cd000789d70c05102" + integrity sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI= + dependencies: + follow-redirects "^1.3.0" + is-buffer "^1.1.5" + babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" @@ -2501,7 +2522,7 @@ date-now@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" -debug@*, debug@3.1.0, debug@^3.0.0, debug@^3.1.0: +debug@*, debug@3.1.0, debug@=3.1.0, debug@^3.0.0, debug@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" dependencies: @@ -3602,6 +3623,12 @@ follow-redirects@^1.0.0: dependencies: debug "^3.1.0" +follow-redirects@^1.3.0: + version "1.5.8" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.8.tgz#1dbfe13e45ad969f813e86c00e5296f525c885a1" + dependencies: + debug "=3.1.0" + for-each@^0.3.2, for-each@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.2.tgz#2c40450b9348e97f281322593ba96704b9abd4d4" @@ -5885,6 +5912,15 @@ merkle-patricia-tree@^2.1.2: rlp "^2.0.0" semaphore ">=1.0.1" +merkletreejs@^0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/merkletreejs/-/merkletreejs-0.0.10.tgz#c282e714d9db10b1ac2ac478b1dfae242e76e795" + integrity sha512-sxcnWhzm+dXAB4NWIx6olRE7euMV1eCL6L9Hyhe8cyz601n7l3jq8/uxAfcR6nBofFYy6k55Fe9etcUs7NznWg== + dependencies: + buffer-reverse "^1.0.1" + crypto-js "^3.1.9-1" + is-buffer "^2.0.3" + merkletreejs@^0.0.11: version "0.0.11" resolved "https://registry.yarnpkg.com/merkletreejs/-/merkletreejs-0.0.11.tgz#c477b262f307151c23852d08ca0e225079aab026"