Add ALM snapshots (#382)

This commit is contained in:
Gerardo Nardelli 2020-07-06 15:33:23 -03:00 committed by GitHub
parent caf2e2b4d3
commit 8c268d6f06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 309 additions and 51 deletions

2
alm/.gitignore vendored

@ -1,5 +1,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
src/snapshots/*.json
# dependencies # dependencies
/node_modules /node_modules
/.pnp /.pnp

@ -17,6 +17,7 @@
"@use-it/interval": "^0.1.3", "@use-it/interval": "^0.1.3",
"customize-cra": "^1.0.0", "customize-cra": "^1.0.0",
"date-fns": "^2.14.0", "date-fns": "^2.14.0",
"dotenv": "^8.2.0",
"fast-memoize": "^2.5.2", "fast-memoize": "^2.5.2",
"promise-retry": "^2.0.1", "promise-retry": "^2.0.1",
"react": "^16.13.1", "react": "^16.13.1",
@ -30,11 +31,12 @@
"web3-eth-contract": "1.2.7" "web3-eth-contract": "1.2.7"
}, },
"scripts": { "scripts": {
"start": "./load-env.sh react-app-rewired start", "start": "yarn createSnapshots && ./load-env.sh react-app-rewired start",
"build": "./load-env.sh react-app-rewired build", "build": "yarn createSnapshots && ./load-env.sh react-app-rewired build",
"test": "react-app-rewired test", "test": "react-app-rewired test",
"eject": "react-app-rewired eject", "eject": "react-app-rewired eject",
"lint": "eslint '*/**/*.{js,ts,tsx}' --ignore-path ../.eslintignore" "lint": "eslint '*/**/*.{js,ts,tsx}' --ignore-path ../.eslintignore",
"createSnapshots": "node scripts/createSnapshots.js"
}, },
"eslintConfig": { "eslintConfig": {
"extends": "react-app" "extends": "react-app"

@ -0,0 +1,118 @@
const { BRIDGE_VALIDATORS_ABI, HOME_AMB_ABI } = require('commons')
const path = require('path')
require('dotenv').config()
const Web3 = require('web3')
const fs = require('fs')
const {
COMMON_HOME_RPC_URL,
COMMON_HOME_BRIDGE_ADDRESS,
COMMON_FOREIGN_RPC_URL,
COMMON_FOREIGN_BRIDGE_ADDRESS
} = process.env
const generateSnapshot = async (side, url, bridgeAddress) => {
const snapshotPath = `../src/snapshots/${side}.json`
const snapshotFullPath = path.join(__dirname, snapshotPath)
const snapshot = {}
const web3 = new Web3(new Web3.providers.HttpProvider(url))
const currentBlockNumber = await web3.eth.getBlockNumber()
snapshot.snapshotBlockNumber = currentBlockNumber
// Save chainId
snapshot.chainId = await web3.eth.getChainId()
const bridgeContract = new web3.eth.Contract(HOME_AMB_ABI, bridgeAddress)
// Save RequiredBlockConfirmationChanged events
let requiredBlockConfirmationChangedEvents = await bridgeContract.getPastEvents('RequiredBlockConfirmationChanged', {
fromBlock: 0,
toBlock: currentBlockNumber
})
// In case RequiredBlockConfirmationChanged was not emitted during initialization in early versions of AMB
// manually generate an event for this. Example Sokol - Kovan bridge
if (requiredBlockConfirmationChangedEvents.length === 0) {
const deployedAtBlock = await bridgeContract.methods.deployedAtBlock().call()
const blockConfirmations = await bridgeContract.methods.requiredBlockConfirmations().call()
requiredBlockConfirmationChangedEvents.push({
blockNumber: parseInt(deployedAtBlock),
returnValues: {
requiredBlockConfirmations: blockConfirmations
}
})
}
snapshot.RequiredBlockConfirmationChanged = requiredBlockConfirmationChangedEvents.map(e => ({
blockNumber: e.blockNumber,
returnValues: {
requiredBlockConfirmations: e.returnValues.requiredBlockConfirmations
}
}))
const validatorAddress = await bridgeContract.methods.validatorContract().call()
const validatorContract = new web3.eth.Contract(BRIDGE_VALIDATORS_ABI, validatorAddress)
// Save RequiredSignaturesChanged events
const RequiredSignaturesChangedEvents = await validatorContract.getPastEvents('RequiredSignaturesChanged', {
fromBlock: 0,
toBlock: currentBlockNumber
})
snapshot.RequiredSignaturesChanged = RequiredSignaturesChangedEvents.map(e => ({
blockNumber: e.blockNumber,
returnValues: {
requiredSignatures: e.returnValues.requiredSignatures
}
}))
// Save ValidatorAdded events
const validatorAddedEvents = await validatorContract.getPastEvents('ValidatorAdded', {
fromBlock: 0,
toBlock: currentBlockNumber
})
snapshot.ValidatorAdded = validatorAddedEvents.map(e => ({
blockNumber: e.blockNumber,
returnValues: {
validator: e.returnValues.validator
},
event: 'ValidatorAdded'
}))
// Save ValidatorRemoved events
const validatorRemovedEvents = await validatorContract.getPastEvents('ValidatorRemoved', {
fromBlock: 0,
toBlock: currentBlockNumber
})
snapshot.ValidatorRemoved = validatorRemovedEvents.map(e => ({
blockNumber: e.blockNumber,
returnValues: {
validator: e.returnValues.validator
},
event: 'ValidatorRemoved'
}))
// Write snapshot
fs.writeFileSync(snapshotFullPath, JSON.stringify(snapshot, null, 2))
}
const main = async () => {
await Promise.all([
generateSnapshot('home', COMMON_HOME_RPC_URL, COMMON_HOME_BRIDGE_ADDRESS),
generateSnapshot('foreign', COMMON_FOREIGN_RPC_URL, COMMON_FOREIGN_BRIDGE_ADDRESS)
])
}
main()
.then(() => process.exit(0))
.catch(error => {
console.log('Error while creating snapshots')
console.error(error)
process.exit(0)
})

@ -7,10 +7,7 @@ import styled from 'styled-components'
import { ExecutionData } from '../hooks/useMessageConfirmations' import { ExecutionData } from '../hooks/useMessageConfirmations'
import { GreyLabel, RedLabel, SuccessLabel } from './commons/Labels' import { GreyLabel, RedLabel, SuccessLabel } from './commons/Labels'
import { ExplorerTxLink } from './commons/ExplorerTxLink' import { ExplorerTxLink } from './commons/ExplorerTxLink'
import { Thead, AgeTd, StatusTd } from './commons/Table'
const Thead = styled.thead`
border-bottom: 2px solid #9e9e9e;
`
const StyledExecutionConfirmation = styled.div` const StyledExecutionConfirmation = styled.div`
margin-top: 30px; margin-top: 30px;
@ -55,12 +52,12 @@ export const ExecutionConfirmation = ({ executionData, isHome }: ExecutionConfir
<tbody> <tbody>
<tr> <tr>
<td>{formattedValidator ? formattedValidator : <SimpleLoading />}</td> <td>{formattedValidator ? formattedValidator : <SimpleLoading />}</td>
<td className="text-center">{getExecutionStatusElement(executionData.status)}</td> <StatusTd className="text-center">{getExecutionStatusElement(executionData.status)}</StatusTd>
<td className="text-center"> <AgeTd className="text-center">
<ExplorerTxLink href={txExplorerLink} target="_blank"> <ExplorerTxLink href={txExplorerLink} target="_blank">
{executionData.timestamp > 0 ? formatTimestamp(executionData.timestamp) : ''} {executionData.timestamp > 0 ? formatTimestamp(executionData.timestamp) : ''}
</ExplorerTxLink> </ExplorerTxLink>
</td> </AgeTd>
</tr> </tr>
</tbody> </tbody>
</table> </table>

@ -7,10 +7,7 @@ import styled from 'styled-components'
import { ConfirmationParam } from '../hooks/useMessageConfirmations' import { ConfirmationParam } from '../hooks/useMessageConfirmations'
import { GreyLabel, RedLabel, SuccessLabel } from './commons/Labels' import { GreyLabel, RedLabel, SuccessLabel } from './commons/Labels'
import { ExplorerTxLink } from './commons/ExplorerTxLink' import { ExplorerTxLink } from './commons/ExplorerTxLink'
import { Thead, AgeTd, StatusTd } from './commons/Table'
const Thead = styled.thead`
border-bottom: 2px solid #9e9e9e;
`
const RequiredConfirmations = styled.label` const RequiredConfirmations = styled.label`
font-size: 14px; font-size: 14px;
@ -70,14 +67,14 @@ export const ValidatorsConfirmations = ({
return ( return (
<tr key={i}> <tr key={i}>
<td>{windowWidth < 850 ? formatTxHash(validator) : validator}</td> <td>{windowWidth < 850 ? formatTxHash(validator) : validator}</td>
<td className="text-center">{getValidatorStatusElement(displayedStatus)}</td> <StatusTd className="text-center">{getValidatorStatusElement(displayedStatus)}</StatusTd>
<td className="text-center"> <AgeTd className="text-center">
<ExplorerTxLink href={explorerLink} target="_blank"> <ExplorerTxLink href={explorerLink} target="_blank">
{confirmation && confirmation.timestamp > 0 {confirmation && confirmation.timestamp > 0
? formatTimestamp(confirmation.timestamp) ? formatTimestamp(confirmation.timestamp)
: elementIfNoTimestamp} : elementIfNoTimestamp}
</ExplorerTxLink> </ExplorerTxLink>
</td> </AgeTd>
</tr> </tr>
) )
})} })}

@ -0,0 +1,13 @@
import styled from 'styled-components'
export const Thead = styled.thead`
border-bottom: 2px solid #9e9e9e;
`
export const StatusTd = styled.td`
width: 150px;
`
export const AgeTd = styled.td`
width: 180px;
`

@ -3,6 +3,7 @@ import { TransactionReceipt } from 'web3-eth'
import { useStateProvider } from '../state/StateProvider' import { useStateProvider } from '../state/StateProvider'
import { Contract } from 'web3-eth-contract' import { Contract } from 'web3-eth-contract'
import { getRequiredBlockConfirmations } from '../utils/contract' import { getRequiredBlockConfirmations } from '../utils/contract'
import { foreignSnapshotProvider, homeSnapshotProvider, SnapshotProvider } from '../services/SnapshotProvider'
export interface UseBlockConfirmationsParams { export interface UseBlockConfirmationsParams {
fromHome: boolean fromHome: boolean
@ -17,17 +18,19 @@ export const useBlockConfirmations = ({ receipt, fromHome }: UseBlockConfirmatio
const callRequireBlockConfirmations = async ( const callRequireBlockConfirmations = async (
contract: Contract, contract: Contract,
receipt: TransactionReceipt, receipt: TransactionReceipt,
setResult: Function setResult: Function,
snapshotProvider: SnapshotProvider
) => { ) => {
const result = await getRequiredBlockConfirmations(contract, receipt.blockNumber) const result = await getRequiredBlockConfirmations(contract, receipt.blockNumber, snapshotProvider)
setResult(result) setResult(result)
} }
useEffect( useEffect(
() => { () => {
const bridgeContract = fromHome ? home.bridgeContract : foreign.bridgeContract const bridgeContract = fromHome ? home.bridgeContract : foreign.bridgeContract
const snapshotProvider = fromHome ? homeSnapshotProvider : foreignSnapshotProvider
if (!bridgeContract || !receipt) return if (!bridgeContract || !receipt) return
callRequireBlockConfirmations(bridgeContract, receipt, setBlockConfirmations) callRequireBlockConfirmations(bridgeContract, receipt, setBlockConfirmations, snapshotProvider)
}, },
[home.bridgeContract, foreign.bridgeContract, receipt, fromHome] [home.bridgeContract, foreign.bridgeContract, receipt, fromHome]
) )

@ -1,7 +1,8 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { getWeb3 } from '../utils/web3' import { getChainId, getWeb3 } from '../utils/web3'
import { SnapshotProvider } from '../services/SnapshotProvider'
export const useNetwork = (url: string) => { export const useNetwork = (url: string, snapshotProvider: SnapshotProvider) => {
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [chainId, setChainId] = useState(0) const [chainId, setChainId] = useState(0)
const web3 = getWeb3(url) const web3 = getWeb3(url)
@ -9,14 +10,14 @@ export const useNetwork = (url: string) => {
useEffect( useEffect(
() => { () => {
setLoading(true) setLoading(true)
const getChainId = async () => { const getWeb3ChainId = async () => {
const id = await web3.eth.getChainId() const id = await getChainId(web3, snapshotProvider)
setChainId(id) setChainId(id)
setLoading(false) setLoading(false)
} }
getChainId() getWeb3ChainId()
}, },
[web3.eth] [web3, snapshotProvider]
) )
return { return {

@ -5,6 +5,7 @@ import { getRequiredSignatures, getValidatorAddress, getValidatorList } from '..
import { BRIDGE_VALIDATORS_ABI } from '../abis' import { BRIDGE_VALIDATORS_ABI } from '../abis'
import { useStateProvider } from '../state/StateProvider' import { useStateProvider } from '../state/StateProvider'
import { TransactionReceipt } from 'web3-eth' import { TransactionReceipt } from 'web3-eth'
import { foreignSnapshotProvider, homeSnapshotProvider, SnapshotProvider } from '../services/SnapshotProvider'
export interface useValidatorContractParams { export interface useValidatorContractParams {
fromHome: boolean fromHome: boolean
@ -28,16 +29,22 @@ export const useValidatorContract = ({ receipt, fromHome }: useValidatorContract
const callRequiredSignatures = async ( const callRequiredSignatures = async (
contract: Maybe<Contract>, contract: Maybe<Contract>,
receipt: TransactionReceipt, receipt: TransactionReceipt,
setResult: Function setResult: Function,
snapshotProvider: SnapshotProvider
) => { ) => {
if (!contract) return if (!contract) return
const result = await getRequiredSignatures(contract, receipt.blockNumber) const result = await getRequiredSignatures(contract, receipt.blockNumber, snapshotProvider)
setResult(result) setResult(result)
} }
const callValidatorList = async (contract: Maybe<Contract>, receipt: TransactionReceipt, setResult: Function) => { const callValidatorList = async (
contract: Maybe<Contract>,
receipt: TransactionReceipt,
setResult: Function,
snapshotProvider: SnapshotProvider
) => {
if (!contract) return if (!contract) return
const result = await getValidatorList(contract, receipt.blockNumber) const result = await getValidatorList(contract, receipt.blockNumber, snapshotProvider)
setResult(result) setResult(result)
} }
@ -55,10 +62,11 @@ export const useValidatorContract = ({ receipt, fromHome }: useValidatorContract
useEffect( useEffect(
() => { () => {
if (!receipt) return if (!receipt) return
callRequiredSignatures(validatorContract, receipt, setRequiredSignatures) const snapshotProvider = fromHome ? homeSnapshotProvider : foreignSnapshotProvider
callValidatorList(validatorContract, receipt, setValidatorList) callRequiredSignatures(validatorContract, receipt, setRequiredSignatures, snapshotProvider)
callValidatorList(validatorContract, receipt, setValidatorList, snapshotProvider)
}, },
[validatorContract, receipt] [validatorContract, receipt, fromHome]
) )
return { return {

@ -0,0 +1,69 @@
const initialValue = {
chainId: 0,
RequiredBlockConfirmationChanged: [],
RequiredSignaturesChanged: [],
ValidatorAdded: [],
ValidatorRemoved: [],
snapshotBlockNumber: 0
}
export interface SnapshotEvent {
blockNumber: number
returnValues: any
}
export interface SnapshotValidatorEvent {
blockNumber: number
returnValues: any
event: string
}
export interface Snapshot {
chainId: number
RequiredBlockConfirmationChanged: SnapshotEvent[]
RequiredSignaturesChanged: SnapshotEvent[]
ValidatorAdded: SnapshotValidatorEvent[]
ValidatorRemoved: SnapshotValidatorEvent[]
snapshotBlockNumber: number
}
export class SnapshotProvider {
private data: Snapshot
constructor(side: string) {
let data = initialValue
try {
data = require(`../snapshots/${side}.json`)
} catch (e) {
console.log('Snapshot not found')
}
this.data = data
}
chainId() {
return this.data.chainId
}
snapshotBlockNumber() {
return this.data.snapshotBlockNumber
}
requiredBlockConfirmationEvents(toBlock: number) {
return this.data.RequiredBlockConfirmationChanged.filter(e => e.blockNumber <= toBlock)
}
requiredSignaturesEvents(toBlock: number) {
return this.data.RequiredSignaturesChanged.filter(e => e.blockNumber <= toBlock)
}
validatorAddedEvents(fromBlock: number) {
return this.data.ValidatorAdded.filter(e => e.blockNumber >= fromBlock)
}
validatorRemovedEvents(fromBlock: number) {
return this.data.ValidatorRemoved.filter(e => e.blockNumber >= fromBlock)
}
}
export const homeSnapshotProvider = new SnapshotProvider('home')
export const foreignSnapshotProvider = new SnapshotProvider('foreign')

@ -11,6 +11,7 @@ import {
import Web3 from 'web3' import Web3 from 'web3'
import { useBridgeContracts } from '../hooks/useBridgeContracts' import { useBridgeContracts } from '../hooks/useBridgeContracts'
import { Contract } from 'web3-eth-contract' import { Contract } from 'web3-eth-contract'
import { foreignSnapshotProvider, homeSnapshotProvider } from '../services/SnapshotProvider'
export interface BaseNetworkParams { export interface BaseNetworkParams {
chainId: number chainId: number
@ -47,8 +48,8 @@ const initialState = {
const StateContext = createContext<StateContext>(initialState) const StateContext = createContext<StateContext>(initialState)
export const StateProvider = ({ children }: { children: ReactNode }) => { export const StateProvider = ({ children }: { children: ReactNode }) => {
const homeNetwork = useNetwork(HOME_RPC_URL) const homeNetwork = useNetwork(HOME_RPC_URL, homeSnapshotProvider)
const foreignNetwork = useNetwork(FOREIGN_RPC_URL) const foreignNetwork = useNetwork(FOREIGN_RPC_URL, foreignSnapshotProvider)
const { homeBridge, foreignBridge } = useBridgeContracts({ const { homeBridge, foreignBridge } = useBridgeContracts({
homeWeb3: homeNetwork.web3, homeWeb3: homeNetwork.web3,
foreignWeb3: foreignNetwork.web3 foreignWeb3: foreignNetwork.web3

@ -1,10 +1,24 @@
import { Contract } from 'web3-eth-contract' import { Contract } from 'web3-eth-contract'
import { EventData } from 'web3-eth-contract'
import { SnapshotProvider } from '../services/SnapshotProvider'
export const getRequiredBlockConfirmations = async (contract: Contract, blockNumber: number) => { export const getRequiredBlockConfirmations = async (
const events = await contract.getPastEvents('RequiredBlockConfirmationChanged', { contract: Contract,
fromBlock: 0, blockNumber: number,
snapshotProvider: SnapshotProvider
) => {
const eventsFromSnapshot = snapshotProvider.requiredBlockConfirmationEvents(blockNumber)
const snapshotBlockNumber = snapshotProvider.snapshotBlockNumber()
let contractEvents: EventData[] = []
if (blockNumber > snapshotBlockNumber) {
contractEvents = await contract.getPastEvents('RequiredBlockConfirmationChanged', {
fromBlock: snapshotBlockNumber + 1,
toBlock: blockNumber toBlock: blockNumber
}) })
}
const events = [...eventsFromSnapshot, ...contractEvents]
let blockConfirmations let blockConfirmations
if (events.length > 0) { if (events.length > 0) {
@ -21,11 +35,23 @@ export const getRequiredBlockConfirmations = async (contract: Contract, blockNum
export const getValidatorAddress = (contract: Contract) => contract.methods.validatorContract().call() export const getValidatorAddress = (contract: Contract) => contract.methods.validatorContract().call()
export const getRequiredSignatures = async (contract: Contract, blockNumber: number) => { export const getRequiredSignatures = async (
const events = await contract.getPastEvents('RequiredSignaturesChanged', { contract: Contract,
fromBlock: 0, blockNumber: number,
snapshotProvider: SnapshotProvider
) => {
const eventsFromSnapshot = snapshotProvider.requiredSignaturesEvents(blockNumber)
const snapshotBlockNumber = snapshotProvider.snapshotBlockNumber()
let contractEvents: EventData[] = []
if (blockNumber > snapshotBlockNumber) {
contractEvents = await contract.getPastEvents('RequiredSignaturesChanged', {
fromBlock: snapshotBlockNumber + 1,
toBlock: blockNumber toBlock: blockNumber
}) })
}
const events = [...eventsFromSnapshot, ...contractEvents]
// Use the value form last event before the transaction // Use the value form last event before the transaction
const event = events[events.length - 1] const event = events[events.length - 1]
@ -33,19 +59,26 @@ export const getRequiredSignatures = async (contract: Contract, blockNumber: num
return parseInt(requiredSignatures) return parseInt(requiredSignatures)
} }
export const getValidatorList = async (contract: Contract, blockNumber: number) => { export const getValidatorList = async (contract: Contract, blockNumber: number, snapshotProvider: SnapshotProvider) => {
let currentList: string[] = await contract.methods.validatorList().call() const addedEventsFromSnapshot = snapshotProvider.validatorAddedEvents(blockNumber)
const [added, removed] = await Promise.all([ const removedEventsFromSnapshot = snapshotProvider.validatorRemovedEvents(blockNumber)
const snapshotBlockNumber = snapshotProvider.snapshotBlockNumber()
const fromBlock = snapshotBlockNumber > blockNumber ? snapshotBlockNumber + 1 : blockNumber
const [currentList, added, removed] = await Promise.all([
contract.methods.validatorList().call(),
contract.getPastEvents('ValidatorAdded', { contract.getPastEvents('ValidatorAdded', {
fromBlock: blockNumber fromBlock
}), }),
contract.getPastEvents('ValidatorRemoved', { contract.getPastEvents('ValidatorRemoved', {
fromBlock: blockNumber fromBlock
}) })
]) ])
// Ordered desc // Ordered desc
const orderedEvents = [...added, ...removed].sort(({ blockNumber: prev }, { blockNumber: next }) => next - prev) const orderedEvents = [...addedEventsFromSnapshot, ...added, ...removedEventsFromSnapshot, ...removed].sort(
({ blockNumber: prev }, { blockNumber: next }) => next - prev
)
// Stored as a Set to avoid duplicates // Stored as a Set to avoid duplicates
const validatorList = new Set(currentList) const validatorList = new Set(currentList)

@ -5,6 +5,7 @@ import { AbiItem } from 'web3-utils'
import memoize from 'fast-memoize' import memoize from 'fast-memoize'
import promiseRetry from 'promise-retry' import promiseRetry from 'promise-retry'
import { HOME_AMB_ABI, FOREIGN_AMB_ABI } from '../abis' import { HOME_AMB_ABI, FOREIGN_AMB_ABI } from '../abis'
import { SnapshotProvider } from '../services/SnapshotProvider'
export interface MessageObject { export interface MessageObject {
id: string id: string
@ -61,3 +62,11 @@ export const getBlock = async (web3: Web3, blockNumber: number): Promise<BlockTr
} }
return result return result
}) })
export const getChainId = async (web3: Web3, snapshotProvider: SnapshotProvider) => {
let id = snapshotProvider.chainId()
if (id === 0) {
id = await web3.eth.getChainId()
}
return id
}

@ -7550,6 +7550,11 @@ dotenv@^7.0.0:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-7.0.0.tgz#a2be3cd52736673206e8a85fb5210eea29628e7c" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-7.0.0.tgz#a2be3cd52736673206e8a85fb5210eea29628e7c"
integrity sha512-M3NhsLbV1i6HuGzBUH8vXrtxOk+tWmzWKDMbAVSUp3Zsjm7ywFeuwrUXhmhQyRK1q5B5GGy7hcXPbj3bnfZg2g== integrity sha512-M3NhsLbV1i6HuGzBUH8vXrtxOk+tWmzWKDMbAVSUp3Zsjm7ywFeuwrUXhmhQyRK1q5B5GGy7hcXPbj3bnfZg2g==
dotenv@^8.2.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
dotignore@~0.1.2: dotignore@~0.1.2:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/dotignore/-/dotignore-0.1.2.tgz#f942f2200d28c3a76fbdd6f0ee9f3257c8a2e905" resolved "https://registry.yarnpkg.com/dotignore/-/dotignore-0.1.2.tgz#f942f2200d28c3a76fbdd6f0ee9f3257c8a2e905"