mirror of
https://github.com/tornadocash/tornado-nova
synced 2024-02-02 14:53:56 +01:00
ethers; tests
This commit is contained in:
parent
01c4930dcd
commit
cd18bef60b
14
TODO
14
TODO
@ -1,11 +1,9 @@
|
|||||||
* shuffle outputs and inputs
|
|
||||||
* utxo data encryption for recipient as for mining
|
* utxo data encryption for recipient as for mining
|
||||||
- combine privkey hash and ethereum public key = address
|
- combine privkey hash and ethereum public key = address
|
||||||
* outputs merging
|
* outputs merging (second snark with 32 inputs) (poma)
|
||||||
* switch web3 to etherjs
|
* wasmsnark (poma)
|
||||||
* tests
|
|
||||||
* wasmsnark
|
|
||||||
* ERC20?
|
|
||||||
* design
|
|
||||||
* relayer
|
* relayer
|
||||||
* race condition ?
|
|
||||||
|
* design
|
||||||
|
* race condition ? (sequencer or something)
|
||||||
|
* the current trusted setup is not secure
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
"@nomiclabs/hardhat-waffle": "^2.0.1",
|
"@nomiclabs/hardhat-waffle": "^2.0.1",
|
||||||
"@openzeppelin/contracts": "^3.4.0",
|
"@openzeppelin/contracts": "^3.4.0",
|
||||||
"bignumber.js": "^9.0.0",
|
"bignumber.js": "^9.0.0",
|
||||||
|
"chai": "^4.3.4",
|
||||||
"circom": "^0.5.45",
|
"circom": "^0.5.45",
|
||||||
"circom_runtime": "^0.1.13",
|
"circom_runtime": "^0.1.13",
|
||||||
"circomlib": "git+https://github.com/tornadocash/circomlib.git#d20d53411d1bef61f38c99a8b36d5d0cc4836aa1",
|
"circomlib": "git+https://github.com/tornadocash/circomlib.git#d20d53411d1bef61f38c99a8b36d5d0cc4836aa1",
|
||||||
|
92
src/index.js
92
src/index.js
@ -1,29 +1,27 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
const MerkleTree = require('fixed-merkle-tree')
|
const MerkleTree = require('fixed-merkle-tree')
|
||||||
const Web3 = require('web3')
|
|
||||||
const { ethers } = require('hardhat')
|
const { ethers } = require('hardhat')
|
||||||
const { BigNumber } = ethers
|
|
||||||
const { toFixedHex, poseidonHash2, getExtDataHash, FIELD_SIZE } = require('./utils')
|
const { toFixedHex, poseidonHash2, getExtDataHash, FIELD_SIZE } = require('./utils')
|
||||||
const Utxo = require('./utxo')
|
const Utxo = require('./utxo')
|
||||||
|
|
||||||
let contract, web3
|
|
||||||
const { prove } = require('./prover')
|
const { prove } = require('./prover')
|
||||||
const MERKLE_TREE_HEIGHT = 5
|
const MERKLE_TREE_HEIGHT = 5
|
||||||
const RPC_URL = 'http://localhost:8545'
|
|
||||||
|
|
||||||
async function buildMerkleTree() {
|
async function buildMerkleTree({ tornadoPool }) {
|
||||||
console.log('Getting contract state...')
|
console.log('Getting contract state...')
|
||||||
const events = await contract.getPastEvents('NewCommitment', { fromBlock: 0, toBlock: 'latest' })
|
const filter = tornadoPool.filters.NewCommitment()
|
||||||
|
const events = await tornadoPool.queryFilter(filter, 0)
|
||||||
|
|
||||||
const leaves = events
|
const leaves = events
|
||||||
.sort((a, b) => a.returnValues.index - b.returnValues.index) // todo sort by event date
|
.sort((a, b) => a.args.index - b.args.index) // todo sort by event date
|
||||||
.map((e) => toFixedHex(e.returnValues.commitment))
|
.map((e) => toFixedHex(e.args.commitment))
|
||||||
// console.log('leaves', leaves)
|
// console.log('leaves', leaves)
|
||||||
return new MerkleTree(MERKLE_TREE_HEIGHT, leaves, { hashFunction: poseidonHash2 })
|
return new MerkleTree(MERKLE_TREE_HEIGHT, leaves, { hashFunction: poseidonHash2 })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getProof({ inputs, outputs, tree, extAmount, fee, recipient, relayer }) {
|
async function getProof({ inputs, outputs, tree, extAmount, fee, recipient, relayer }) {
|
||||||
// todo shuffle inputs and outputs
|
// todo shuffle inputs and outputs
|
||||||
if (inputs.length !== 2 || outputs.length !== 2 ) {
|
if (inputs.length !== 2 || outputs.length !== 2) {
|
||||||
throw new Error('Unsupported number of inputs/outputs')
|
throw new Error('Unsupported number of inputs/outputs')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,23 +61,23 @@ async function getProof({ inputs, outputs, tree, extAmount, fee, recipient, rela
|
|||||||
let input = {
|
let input = {
|
||||||
root: oldRoot,
|
root: oldRoot,
|
||||||
newRoot: tree.root(),
|
newRoot: tree.root(),
|
||||||
inputNullifier: inputs.map(x => x.getNullifier()),
|
inputNullifier: inputs.map((x) => x.getNullifier()),
|
||||||
outputCommitment: outputs.map(x => x.getCommitment()),
|
outputCommitment: outputs.map((x) => x.getCommitment()),
|
||||||
extAmount,
|
extAmount,
|
||||||
fee,
|
fee,
|
||||||
extDataHash,
|
extDataHash,
|
||||||
|
|
||||||
// data for 2 transaction inputs
|
// data for 2 transaction inputs
|
||||||
inAmount: inputs.map(x => x.amount),
|
inAmount: inputs.map((x) => x.amount),
|
||||||
inPrivateKey: inputs.map(x => x.privkey),
|
inPrivateKey: inputs.map((x) => x.privkey),
|
||||||
inBlinding: inputs.map(x => x.blinding),
|
inBlinding: inputs.map((x) => x.blinding),
|
||||||
inPathIndices: inputMerklePathIndices,
|
inPathIndices: inputMerklePathIndices,
|
||||||
inPathElements: inputMerklePathElements,
|
inPathElements: inputMerklePathElements,
|
||||||
|
|
||||||
// data for 2 transaction outputs
|
// data for 2 transaction outputs
|
||||||
outAmount: outputs.map(x => x.amount),
|
outAmount: outputs.map((x) => x.amount),
|
||||||
outBlinding: outputs.map(x => x.blinding),
|
outBlinding: outputs.map((x) => x.blinding),
|
||||||
outPubkey: outputs.map(x => x.pubkey),
|
outPubkey: outputs.map((x) => x.pubkey),
|
||||||
outPathIndices: outputIndex >> 1,
|
outPathIndices: outputIndex >> 1,
|
||||||
outPathElements: outputPath,
|
outPathElements: outputPath,
|
||||||
}
|
}
|
||||||
@ -92,8 +90,8 @@ async function getProof({ inputs, outputs, tree, extAmount, fee, recipient, rela
|
|||||||
const args = [
|
const args = [
|
||||||
toFixedHex(input.root),
|
toFixedHex(input.root),
|
||||||
toFixedHex(input.newRoot),
|
toFixedHex(input.newRoot),
|
||||||
inputs.map(x => toFixedHex(x.getNullifier())),
|
inputs.map((x) => toFixedHex(x.getNullifier())),
|
||||||
outputs.map(x => toFixedHex(x.getCommitment())),
|
outputs.map((x) => toFixedHex(x.getCommitment())),
|
||||||
toFixedHex(extAmount),
|
toFixedHex(extAmount),
|
||||||
toFixedHex(fee),
|
toFixedHex(fee),
|
||||||
extData,
|
extData,
|
||||||
@ -107,7 +105,7 @@ async function getProof({ inputs, outputs, tree, extAmount, fee, recipient, rela
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deposit() {
|
async function deposit({ tornadoPool }) {
|
||||||
const amount = 1e6
|
const amount = 1e6
|
||||||
const inputs = [new Utxo(), new Utxo()]
|
const inputs = [new Utxo(), new Utxo()]
|
||||||
const outputs = [new Utxo({ amount }), new Utxo()]
|
const outputs = [new Utxo({ amount }), new Utxo()]
|
||||||
@ -115,7 +113,7 @@ async function deposit() {
|
|||||||
const { proof, args } = await getProof({
|
const { proof, args } = await getProof({
|
||||||
inputs,
|
inputs,
|
||||||
outputs,
|
outputs,
|
||||||
tree: await buildMerkleTree(),
|
tree: await buildMerkleTree({ tornadoPool }),
|
||||||
extAmount: amount,
|
extAmount: amount,
|
||||||
fee: 0,
|
fee: 0,
|
||||||
recipient: 0,
|
recipient: 0,
|
||||||
@ -123,24 +121,25 @@ async function deposit() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
console.log('Sending deposit transaction...')
|
console.log('Sending deposit transaction...')
|
||||||
const receipt = await contract.methods
|
const receipt = await tornadoPool.transaction(proof, ...args, {
|
||||||
.transaction(proof, ...args)
|
value: amount,
|
||||||
.send({ value: amount, from: web3.eth.defaultAccount, gas: 1e6 })
|
gasLimit: 1e6,
|
||||||
console.log(`Receipt ${receipt.transactionHash}`)
|
})
|
||||||
|
console.log(`Receipt ${receipt.hash}`)
|
||||||
return outputs[0]
|
return outputs[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
async function transact(utxo) {
|
async function transact({ tornadoPool, utxo }) {
|
||||||
const inputs = [utxo, new Utxo()]
|
const inputs = [utxo, new Utxo()]
|
||||||
const outputs = [
|
const outputs = [
|
||||||
new Utxo({ amount: utxo.amount / 4 }),
|
new Utxo({ amount: utxo.amount / 4 }),
|
||||||
new Utxo({ amount: utxo.amount * 3 / 4, privkey: utxo.privkey}),
|
new Utxo({ amount: (utxo.amount * 3) / 4, privkey: utxo.privkey }),
|
||||||
]
|
]
|
||||||
|
|
||||||
const { proof, args } = await getProof({
|
const { proof, args } = await getProof({
|
||||||
inputs,
|
inputs,
|
||||||
outputs,
|
outputs,
|
||||||
tree: await buildMerkleTree(),
|
tree: await buildMerkleTree({ tornadoPool }),
|
||||||
extAmount: 0,
|
extAmount: 0,
|
||||||
fee: 0,
|
fee: 0,
|
||||||
recipient: 0,
|
recipient: 0,
|
||||||
@ -148,49 +147,28 @@ async function transact(utxo) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
console.log('Sending transfer transaction...')
|
console.log('Sending transfer transaction...')
|
||||||
const receipt = await contract.methods
|
const receipt = await tornadoPool.transaction(proof, ...args, { gasLimit: 1e6 })
|
||||||
.transaction(proof, ...args)
|
console.log(`Receipt ${receipt.hash}`)
|
||||||
.send({ from: web3.eth.defaultAccount, gas: 1e6 })
|
|
||||||
console.log(`Receipt ${receipt.transactionHash}`)
|
|
||||||
return outputs[0]
|
return outputs[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
async function withdraw(utxo) {
|
async function withdraw({ tornadoPool, utxo, recipient }) {
|
||||||
const inputs = [utxo, new Utxo()]
|
const inputs = [utxo, new Utxo()]
|
||||||
const outputs = [new Utxo(), new Utxo()]
|
const outputs = [new Utxo(), new Utxo()]
|
||||||
|
|
||||||
const { proof, args } = await getProof({
|
const { proof, args } = await getProof({
|
||||||
inputs,
|
inputs,
|
||||||
outputs,
|
outputs,
|
||||||
tree: await buildMerkleTree(),
|
tree: await buildMerkleTree({ tornadoPool }),
|
||||||
extAmount: FIELD_SIZE.sub(utxo.amount),
|
extAmount: FIELD_SIZE.sub(utxo.amount),
|
||||||
fee: 0,
|
fee: 0,
|
||||||
recipient: '0xc2Ba33d4c0d2A92fb4f1a07C273c5d21E688Eb48',
|
recipient,
|
||||||
relayer: 0,
|
relayer: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('Sending withdraw transaction...')
|
console.log('Sending withdraw transaction...')
|
||||||
const receipt = await contract.methods
|
const receipt = await tornadoPool.transaction(proof, ...args, { gasLimit: 1e6 })
|
||||||
.transaction(proof, ...args)
|
console.log(`Receipt ${receipt.hash}`)
|
||||||
.send({ from: web3.eth.defaultAccount, gas: 1e6 })
|
|
||||||
console.log(`Receipt ${receipt.transactionHash}`)
|
|
||||||
|
|
||||||
let bal = await web3.eth.getBalance('0xc2Ba33d4c0d2A92fb4f1a07C273c5d21E688Eb48')
|
|
||||||
console.log('balance', bal)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
module.exports = { deposit, withdraw, transact }
|
||||||
web3 = new Web3(new Web3.providers.HttpProvider(RPC_URL, { timeout: 5 * 60 * 1000 }), null, {
|
|
||||||
transactionConfirmationBlocks: 1,
|
|
||||||
})
|
|
||||||
netId = await web3.eth.net.getId()
|
|
||||||
const contractData = require('../artifacts/contracts/TornadoPool.sol/TornadoPool.json')
|
|
||||||
contract = new web3.eth.Contract(contractData.abi, '0x0E801D84Fa97b50751Dbf25036d067dCf18858bF')
|
|
||||||
web3.eth.defaultAccount = (await web3.eth.getAccounts())[0]
|
|
||||||
|
|
||||||
const utxo1 = await deposit()
|
|
||||||
const utxo2 = await transact(utxo1)
|
|
||||||
await withdraw(utxo2)
|
|
||||||
}
|
|
||||||
|
|
||||||
main()
|
|
||||||
|
26
src/utils.js
26
src/utils.js
@ -1,7 +1,7 @@
|
|||||||
const crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
const ethers = require('ethers')
|
const { ethers } = require('hardhat')
|
||||||
const BigNumber = ethers.BigNumber
|
const BigNumber = ethers.BigNumber
|
||||||
const {poseidon} = require('circomlib')
|
const { poseidon } = require('circomlib')
|
||||||
|
|
||||||
const poseidonHash = (items) => BigNumber.from(poseidon(items).toString())
|
const poseidonHash = (items) => BigNumber.from(poseidon(items).toString())
|
||||||
const poseidonHash2 = (a, b) => poseidonHash([a, b])
|
const poseidonHash2 = (a, b) => poseidonHash([a, b])
|
||||||
@ -12,17 +12,19 @@ const FIELD_SIZE = BigNumber.from(
|
|||||||
/** Generate random number of specified byte length */
|
/** Generate random number of specified byte length */
|
||||||
const randomBN = (nbytes = 31) => BigNumber.from(crypto.randomBytes(nbytes))
|
const randomBN = (nbytes = 31) => BigNumber.from(crypto.randomBytes(nbytes))
|
||||||
|
|
||||||
function getExtDataHash({recipient, relayer, encryptedOutput1, encryptedOutput2}) {
|
function getExtDataHash({ recipient, relayer, encryptedOutput1, encryptedOutput2 }) {
|
||||||
const abi = new ethers.utils.AbiCoder()
|
const abi = new ethers.utils.AbiCoder()
|
||||||
|
|
||||||
const encodedData = abi.encode(
|
const encodedData = abi.encode(
|
||||||
['tuple(address recipient,address relayer,bytes encryptedOutput1,bytes encryptedOutput2)'],
|
['tuple(address recipient,address relayer,bytes encryptedOutput1,bytes encryptedOutput2)'],
|
||||||
[{
|
[
|
||||||
|
{
|
||||||
recipient: toFixedHex(recipient, 20),
|
recipient: toFixedHex(recipient, 20),
|
||||||
relayer: toFixedHex(relayer, 20),
|
relayer: toFixedHex(relayer, 20),
|
||||||
encryptedOutput1: encryptedOutput1,
|
encryptedOutput1: encryptedOutput1,
|
||||||
encryptedOutput2: encryptedOutput2,
|
encryptedOutput2: encryptedOutput2,
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
)
|
)
|
||||||
const hash = ethers.utils.keccak256(encodedData)
|
const hash = ethers.utils.keccak256(encodedData)
|
||||||
return BigNumber.from(hash).mod(FIELD_SIZE)
|
return BigNumber.from(hash).mod(FIELD_SIZE)
|
||||||
@ -32,8 +34,8 @@ function getExtDataHash({recipient, relayer, encryptedOutput1, encryptedOutput2}
|
|||||||
const toFixedHex = (number, length = 32) =>
|
const toFixedHex = (number, length = 32) =>
|
||||||
'0x' +
|
'0x' +
|
||||||
(number instanceof Buffer
|
(number instanceof Buffer
|
||||||
? number.toString('hex')
|
? number.toString('hex')
|
||||||
: BigNumber.from(number).toHexString().slice(2)
|
: BigNumber.from(number).toHexString().slice(2)
|
||||||
).padStart(length * 2, '0')
|
).padStart(length * 2, '0')
|
||||||
|
|
||||||
const toBuffer = (value, length) =>
|
const toBuffer = (value, length) =>
|
||||||
@ -45,6 +47,14 @@ const toBuffer = (value, length) =>
|
|||||||
'hex',
|
'hex',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async function takeSnapshot() {
|
||||||
|
return await ethers.provider.send('evm_snapshot', [])
|
||||||
|
}
|
||||||
|
|
||||||
|
async function revertSnapshot(id) {
|
||||||
|
await ethers.provider.send('evm_revert', [id])
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
FIELD_SIZE,
|
FIELD_SIZE,
|
||||||
randomBN,
|
randomBN,
|
||||||
@ -53,4 +63,6 @@ module.exports = {
|
|||||||
poseidonHash,
|
poseidonHash,
|
||||||
poseidonHash2,
|
poseidonHash2,
|
||||||
getExtDataHash,
|
getExtDataHash,
|
||||||
|
takeSnapshot,
|
||||||
|
revertSnapshot,
|
||||||
}
|
}
|
||||||
|
14
src/utxo.js
14
src/utxo.js
@ -10,19 +10,19 @@ function fromPrivkey(privkey) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Utxo {
|
class Utxo {
|
||||||
constructor({amount, pubkey, privkey, blinding, index} = {}) {
|
constructor({ amount, pubkey, privkey, blinding, index } = {}) {
|
||||||
if (!pubkey) {
|
if (!pubkey) {
|
||||||
if (privkey) {
|
if (privkey) {
|
||||||
pubkey = fromPrivkey(privkey).pubkey
|
pubkey = fromPrivkey(privkey).pubkey
|
||||||
} else {
|
} else {
|
||||||
({pubkey, privkey} = fromPrivkey(randomBN()))
|
;({ pubkey, privkey } = fromPrivkey(randomBN()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.amount = BigNumber.from(amount || 0);
|
this.amount = BigNumber.from(amount || 0)
|
||||||
this.blinding = blinding || randomBN();
|
this.blinding = blinding || randomBN()
|
||||||
this.pubkey = pubkey;
|
this.pubkey = pubkey
|
||||||
this.privkey = privkey;
|
this.privkey = privkey
|
||||||
this.index = index;
|
this.index = index
|
||||||
}
|
}
|
||||||
|
|
||||||
getCommitment() {
|
getCommitment() {
|
||||||
|
45
test/full.test.js
Normal file
45
test/full.test.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
/* global ethers */
|
||||||
|
const { expect, should } = require('chai')
|
||||||
|
should()
|
||||||
|
|
||||||
|
const { poseidonHash2, toFixedHex, takeSnapshot, revertSnapshot } = require('../src/utils')
|
||||||
|
|
||||||
|
const MERKLE_TREE_HEIGHT = 5
|
||||||
|
const MerkleTree = require('fixed-merkle-tree')
|
||||||
|
|
||||||
|
const { deposit, transact, withdraw } = require('../src/index')
|
||||||
|
|
||||||
|
describe('TornadoPool', () => {
|
||||||
|
let snapshotId, tornadoPool
|
||||||
|
|
||||||
|
/* prettier-ignore */
|
||||||
|
before(async function () {
|
||||||
|
const Verifier = await ethers.getContractFactory('Verifier')
|
||||||
|
const verifier = await Verifier.deploy()
|
||||||
|
await verifier.deployed()
|
||||||
|
|
||||||
|
const tree = new MerkleTree(MERKLE_TREE_HEIGHT, [], { hashFunction: poseidonHash2 })
|
||||||
|
const root = await tree.root()
|
||||||
|
|
||||||
|
const Pool = await ethers.getContractFactory('TornadoPool')
|
||||||
|
tornadoPool = await Pool.deploy(verifier.address, toFixedHex(root))
|
||||||
|
|
||||||
|
snapshotId = await takeSnapshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should deposit, transact and withdraw', async function () {
|
||||||
|
const utxo1 = await deposit({ tornadoPool })
|
||||||
|
const utxo2 = await transact({ tornadoPool, utxo: utxo1 })
|
||||||
|
|
||||||
|
const recipient = '0xc2Ba33d4c0d2A92fb4f1a07C273c5d21E688Eb48'
|
||||||
|
await withdraw({ tornadoPool, utxo: utxo2, recipient })
|
||||||
|
|
||||||
|
let bal = await ethers.provider.getBalance(recipient)
|
||||||
|
expect(bal).to.be.gt(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await revertSnapshot(snapshotId)
|
||||||
|
snapshotId = await takeSnapshot()
|
||||||
|
})
|
||||||
|
})
|
@ -1773,9 +1773,9 @@ blake2b-wasm@^1.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
nanoassert "^1.0.0"
|
nanoassert "^1.0.0"
|
||||||
|
|
||||||
"blake2b-wasm@git+https://github.com/jbaylina/blake2b-wasm.git":
|
"blake2b-wasm@https://github.com/jbaylina/blake2b-wasm.git":
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "git+https://github.com/jbaylina/blake2b-wasm.git#0d5f024b212429c7f50a7f533aa3a2406b5b42b3"
|
resolved "https://github.com/jbaylina/blake2b-wasm.git#0d5f024b212429c7f50a7f533aa3a2406b5b42b3"
|
||||||
dependencies:
|
dependencies:
|
||||||
nanoassert "^1.0.0"
|
nanoassert "^1.0.0"
|
||||||
|
|
||||||
@ -2078,7 +2078,7 @@ caseless@~0.12.0:
|
|||||||
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
|
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
|
||||||
integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
|
integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
|
||||||
|
|
||||||
chai@^4.2.0:
|
chai@^4.2.0, chai@^4.3.4:
|
||||||
version "4.3.4"
|
version "4.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.4.tgz#b55e655b31e1eac7099be4c08c21964fce2e6c49"
|
resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.4.tgz#b55e655b31e1eac7099be4c08c21964fce2e6c49"
|
||||||
integrity sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==
|
integrity sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==
|
||||||
|
Loading…
Reference in New Issue
Block a user