mirror of
https://github.com/semaphore-protocol/semaphore.git
synced 2026-04-28 03:00:41 -04:00
Merge pull request #129 from semaphore-protocol/refactor/extensions
Refactoring of extensions contracts
Former-commit-id: 2c2c02f82b
This commit is contained in:
@@ -30,7 +30,7 @@ contract Semaphore is ISemaphore, SemaphoreCore, SemaphoreGroups {
|
||||
/// @param merkleTreeDepth: Depth of the tree.
|
||||
modifier onlySupportedMerkleTreeDepth(uint256 merkleTreeDepth) {
|
||||
if (address(verifiers[merkleTreeDepth]) == address(0)) {
|
||||
revert Semaphore__TreeDepthIsNotSupported();
|
||||
revert Semaphore__MerkleTreeDepthIsNotSupported();
|
||||
}
|
||||
_;
|
||||
}
|
||||
|
||||
@@ -14,20 +14,11 @@ contract SemaphoreVoting is ISemaphoreVoting, SemaphoreCore, SemaphoreGroups {
|
||||
/// @dev Gets a poll id and returns the poll data.
|
||||
mapping(uint256 => Poll) internal polls;
|
||||
|
||||
/// @dev Since there can be multiple verifier contracts (each associated with a certain tree depth),
|
||||
/// it is necessary to pass the addresses of the previously deployed contracts with the associated
|
||||
/// tree depth. Depending on the depth chosen when creating the poll, a certain verifier will be
|
||||
/// used to verify that the proof is correct.
|
||||
/// @param merkleTreeDepths: Three depths used in verifiers.
|
||||
/// @param verifierAddresses: Verifier addresses.
|
||||
constructor(uint256[] memory merkleTreeDepths, address[] memory verifierAddresses) {
|
||||
require(
|
||||
merkleTreeDepths.length == verifierAddresses.length,
|
||||
"SemaphoreVoting: parameters lists does not have the same length"
|
||||
);
|
||||
|
||||
for (uint8 i = 0; i < merkleTreeDepths.length; ) {
|
||||
verifiers[merkleTreeDepths[i]] = IVerifier(verifierAddresses[i]);
|
||||
/// @dev Initializes the Semaphore verifiers used to verify the user's ZK proofs.
|
||||
/// @param _verifiers: List of Semaphore verifiers (address and related Merkle tree depth).
|
||||
constructor(Verifier[] memory _verifiers) {
|
||||
for (uint8 i = 0; i < _verifiers.length; ) {
|
||||
verifiers[_verifiers[i].merkleTreeDepth] = IVerifier(_verifiers[i].contractAddress);
|
||||
|
||||
unchecked {
|
||||
++i;
|
||||
@@ -38,7 +29,10 @@ contract SemaphoreVoting is ISemaphoreVoting, SemaphoreCore, SemaphoreGroups {
|
||||
/// @dev Checks if the poll coordinator is the transaction sender.
|
||||
/// @param pollId: Id of the poll.
|
||||
modifier onlyCoordinator(uint256 pollId) {
|
||||
require(polls[pollId].coordinator == _msgSender(), "SemaphoreVoting: caller is not the poll coordinator");
|
||||
if (polls[pollId].coordinator != _msgSender()) {
|
||||
revert Semaphore__CallerIsNotThePollCoordinator();
|
||||
}
|
||||
|
||||
_;
|
||||
}
|
||||
|
||||
@@ -48,10 +42,9 @@ contract SemaphoreVoting is ISemaphoreVoting, SemaphoreCore, SemaphoreGroups {
|
||||
address coordinator,
|
||||
uint256 merkleTreeDepth
|
||||
) public override {
|
||||
require(
|
||||
address(verifiers[merkleTreeDepth]) != address(0),
|
||||
"SemaphoreVoting: Merkle tree depth value is not supported"
|
||||
);
|
||||
if (address(verifiers[merkleTreeDepth]) == address(0)) {
|
||||
revert Semaphore__MerkleTreeDepthIsNotSupported();
|
||||
}
|
||||
|
||||
_createGroup(pollId, merkleTreeDepth, 0);
|
||||
|
||||
@@ -66,14 +59,18 @@ contract SemaphoreVoting is ISemaphoreVoting, SemaphoreCore, SemaphoreGroups {
|
||||
|
||||
/// @dev See {ISemaphoreVoting-addVoter}.
|
||||
function addVoter(uint256 pollId, uint256 identityCommitment) public override onlyCoordinator(pollId) {
|
||||
require(polls[pollId].state == PollState.Created, "SemaphoreVoting: voters can only be added before voting");
|
||||
if (polls[pollId].state != PollState.Created) {
|
||||
revert Semaphore__PollHasAlreadyBeenStarted();
|
||||
}
|
||||
|
||||
_addMember(pollId, identityCommitment);
|
||||
}
|
||||
|
||||
/// @dev See {ISemaphoreVoting-addVoter}.
|
||||
function startPoll(uint256 pollId, uint256 encryptionKey) public override onlyCoordinator(pollId) {
|
||||
require(polls[pollId].state == PollState.Created, "SemaphoreVoting: poll has already been started");
|
||||
if (polls[pollId].state != PollState.Created) {
|
||||
revert Semaphore__PollHasAlreadyBeenStarted();
|
||||
}
|
||||
|
||||
polls[pollId].state = PollState.Ongoing;
|
||||
|
||||
@@ -89,7 +86,9 @@ contract SemaphoreVoting is ISemaphoreVoting, SemaphoreCore, SemaphoreGroups {
|
||||
) public override onlyCoordinator(pollId) {
|
||||
Poll memory poll = polls[pollId];
|
||||
|
||||
require(poll.state == PollState.Ongoing, "SemaphoreVoting: vote can only be cast in an ongoing poll");
|
||||
if (poll.state != PollState.Ongoing) {
|
||||
revert Semaphore__PollIsNotOngoing();
|
||||
}
|
||||
|
||||
uint256 merkleTreeDepth = getMerkleTreeDepth(pollId);
|
||||
uint256 merkleTreeRoot = getMerkleTreeRoot(pollId);
|
||||
@@ -106,7 +105,9 @@ contract SemaphoreVoting is ISemaphoreVoting, SemaphoreCore, SemaphoreGroups {
|
||||
|
||||
/// @dev See {ISemaphoreVoting-publishDecryptionKey}.
|
||||
function endPoll(uint256 pollId, uint256 decryptionKey) public override onlyCoordinator(pollId) {
|
||||
require(polls[pollId].state == PollState.Ongoing, "SemaphoreVoting: poll is not ongoing");
|
||||
if (polls[pollId].state != PollState.Ongoing) {
|
||||
revert Semaphore__PollIsNotOngoing();
|
||||
}
|
||||
|
||||
polls[pollId].state = PollState.Ended;
|
||||
|
||||
|
||||
@@ -16,20 +16,11 @@ contract SemaphoreWhistleblowing is ISemaphoreWhistleblowing, SemaphoreCore, Sem
|
||||
/// @dev Gets an editor address and return their entity.
|
||||
mapping(address => uint256) private entities;
|
||||
|
||||
/// @dev Since there can be multiple verifier contracts (each associated with a certain tree depth),
|
||||
/// it is necessary to pass the addresses of the previously deployed contracts with the associated
|
||||
/// tree depth. Depending on the depth chosen when creating the entity, a certain verifier will be
|
||||
/// used to verify that the proof is correct.
|
||||
/// @param merkleTreeDepths: Three depths used in verifiers.
|
||||
/// @param verifierAddresses: Verifier addresses.
|
||||
constructor(uint256[] memory merkleTreeDepths, address[] memory verifierAddresses) {
|
||||
require(
|
||||
merkleTreeDepths.length == verifierAddresses.length,
|
||||
"SemaphoreWhistleblowing: parameters lists does not have the same length"
|
||||
);
|
||||
|
||||
for (uint8 i = 0; i < merkleTreeDepths.length; ) {
|
||||
verifiers[merkleTreeDepths[i]] = IVerifier(verifierAddresses[i]);
|
||||
/// @dev Initializes the Semaphore verifiers used to verify the user's ZK proofs.
|
||||
/// @param _verifiers: List of Semaphore verifiers (address and related Merkle tree depth).
|
||||
constructor(Verifier[] memory _verifiers) {
|
||||
for (uint8 i = 0; i < _verifiers.length; ) {
|
||||
verifiers[_verifiers[i].merkleTreeDepth] = IVerifier(_verifiers[i].contractAddress);
|
||||
|
||||
unchecked {
|
||||
++i;
|
||||
@@ -40,7 +31,10 @@ contract SemaphoreWhistleblowing is ISemaphoreWhistleblowing, SemaphoreCore, Sem
|
||||
/// @dev Checks if the editor is the transaction sender.
|
||||
/// @param entityId: Id of the entity.
|
||||
modifier onlyEditor(uint256 entityId) {
|
||||
require(entityId == entities[_msgSender()], "SemaphoreWhistleblowing: caller is not the editor");
|
||||
if (entityId != entities[_msgSender()]) {
|
||||
revert Semaphore__CallerIsNotTheEditor();
|
||||
}
|
||||
|
||||
_;
|
||||
}
|
||||
|
||||
@@ -50,10 +44,9 @@ contract SemaphoreWhistleblowing is ISemaphoreWhistleblowing, SemaphoreCore, Sem
|
||||
address editor,
|
||||
uint256 merkleTreeDepth
|
||||
) public override {
|
||||
require(
|
||||
address(verifiers[merkleTreeDepth]) != address(0),
|
||||
"SemaphoreWhistleblowing: Merkle tree depth value is not supported"
|
||||
);
|
||||
if (address(verifiers[merkleTreeDepth]) == address(0)) {
|
||||
revert Semaphore__MerkleTreeDepthIsNotSupported();
|
||||
}
|
||||
|
||||
_createGroup(entityId, merkleTreeDepth, 0);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ pragma solidity 0.8.4;
|
||||
/// @dev Interface of a Semaphore contract.
|
||||
interface ISemaphore {
|
||||
error Semaphore__CallerIsNotTheGroupAdmin();
|
||||
error Semaphore__TreeDepthIsNotSupported();
|
||||
error Semaphore__MerkleTreeDepthIsNotSupported();
|
||||
error Semaphore__MerkleTreeRootIsExpired();
|
||||
error Semaphore__MerkleTreeRootIsNotPartOfTheGroup();
|
||||
|
||||
|
||||
@@ -4,12 +4,22 @@ pragma solidity 0.8.4;
|
||||
/// @title SemaphoreVoting interface.
|
||||
/// @dev Interface of SemaphoreVoting contract.
|
||||
interface ISemaphoreVoting {
|
||||
error Semaphore__CallerIsNotThePollCoordinator();
|
||||
error Semaphore__MerkleTreeDepthIsNotSupported();
|
||||
error Semaphore__PollHasAlreadyBeenStarted();
|
||||
error Semaphore__PollIsNotOngoing();
|
||||
|
||||
enum PollState {
|
||||
Created,
|
||||
Ongoing,
|
||||
Ended
|
||||
}
|
||||
|
||||
struct Verifier {
|
||||
address contractAddress;
|
||||
uint256 merkleTreeDepth;
|
||||
}
|
||||
|
||||
struct Poll {
|
||||
address coordinator;
|
||||
PollState state;
|
||||
|
||||
@@ -4,6 +4,14 @@ pragma solidity 0.8.4;
|
||||
/// @title SemaphoreWhistleblowing interface.
|
||||
/// @dev Interface of SemaphoreWhistleblowing contract.
|
||||
interface ISemaphoreWhistleblowing {
|
||||
error Semaphore__CallerIsNotTheEditor();
|
||||
error Semaphore__MerkleTreeDepthIsNotSupported();
|
||||
|
||||
struct Verifier {
|
||||
address contractAddress;
|
||||
uint256 merkleTreeDepth;
|
||||
}
|
||||
|
||||
/// @dev Emitted when a new entity is created.
|
||||
/// @param entityId: Id of the entity.
|
||||
/// @param editor: Editor of the entity.
|
||||
|
||||
@@ -4,8 +4,8 @@ import { task, types } from "hardhat/config"
|
||||
|
||||
task("deploy:semaphore-voting", "Deploy a SemaphoreVoting contract")
|
||||
.addOptionalParam<boolean>("logs", "Print the logs", true, types.boolean)
|
||||
.addParam<boolean>("verifier", "Verifier contract address", undefined, types.string)
|
||||
.setAction(async ({ logs, verifier }, { ethers }): Promise<Contract> => {
|
||||
.addParam("verifiers", "Tree depths and verifier addresses", undefined, types.json)
|
||||
.setAction(async ({ logs, verifiers }, { ethers }): Promise<Contract> => {
|
||||
const poseidonABI = poseidonContract.generateABI(2)
|
||||
const poseidonBytecode = poseidonContract.createCode(2)
|
||||
|
||||
@@ -35,7 +35,7 @@ task("deploy:semaphore-voting", "Deploy a SemaphoreVoting contract")
|
||||
}
|
||||
})
|
||||
|
||||
const contract = await ContractFactory.deploy([20], [verifier])
|
||||
const contract = await ContractFactory.deploy(verifiers)
|
||||
|
||||
await contract.deployed()
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import { task, types } from "hardhat/config"
|
||||
|
||||
task("deploy:semaphore-whistleblowing", "Deploy a SemaphoreWhistleblowing contract")
|
||||
.addOptionalParam<boolean>("logs", "Print the logs", true, types.boolean)
|
||||
.addParam<boolean>("verifier", "Verifier contract address", undefined, types.string)
|
||||
.setAction(async ({ logs, verifier }, { ethers }): Promise<Contract> => {
|
||||
.addParam("verifiers", "Verifier contract address", undefined, types.json)
|
||||
.setAction(async ({ logs, verifiers }, { ethers }): Promise<Contract> => {
|
||||
const poseidonABI = poseidonContract.generateABI(2)
|
||||
const poseidonBytecode = poseidonContract.createCode(2)
|
||||
|
||||
@@ -35,7 +35,7 @@ task("deploy:semaphore-whistleblowing", "Deploy a SemaphoreWhistleblowing contra
|
||||
}
|
||||
})
|
||||
|
||||
const contract = await ContractFactory.deploy([20], [verifier])
|
||||
const contract = await ContractFactory.deploy(verifiers)
|
||||
|
||||
await contract.deployed()
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ describe("Semaphore", () => {
|
||||
it("Should not create a group if the tree depth is not supported", async () => {
|
||||
const transaction = contract["createGroup(uint256,uint256,uint256,address)"](groupId, 10, 0, accounts[0])
|
||||
|
||||
await expect(transaction).to.be.revertedWith("Semaphore__TreeDepthIsNotSupported()")
|
||||
await expect(transaction).to.be.revertedWith("Semaphore__MerkleTreeDepthIsNotSupported()")
|
||||
})
|
||||
|
||||
it("Should create a group", async () => {
|
||||
|
||||
@@ -28,7 +28,11 @@ describe("SemaphoreVoting", () => {
|
||||
|
||||
before(async () => {
|
||||
const { address: verifierAddress } = await run("deploy:verifier", { logs: false, depth: treeDepth })
|
||||
contract = await run("deploy:semaphore-voting", { logs: false, verifier: verifierAddress })
|
||||
contract = await run("deploy:semaphore-voting", {
|
||||
logs: false,
|
||||
verifiers: [{ merkleTreeDepth: treeDepth, contractAddress: verifierAddress }]
|
||||
})
|
||||
|
||||
accounts = await ethers.getSigners()
|
||||
coordinator = await accounts[1].getAddress()
|
||||
})
|
||||
@@ -37,7 +41,7 @@ describe("SemaphoreVoting", () => {
|
||||
it("Should not create a poll with a wrong depth", async () => {
|
||||
const transaction = contract.createPoll(pollIds[0], coordinator, 10)
|
||||
|
||||
await expect(transaction).to.be.revertedWith("SemaphoreVoting: Merkle tree depth value is not supported")
|
||||
await expect(transaction).to.be.revertedWith("Semaphore__MerkleTreeDepthIsNotSupported()")
|
||||
})
|
||||
|
||||
it("Should not create a poll greater than the snark scalar field", async () => {
|
||||
@@ -67,7 +71,7 @@ describe("SemaphoreVoting", () => {
|
||||
it("Should not start the poll if the caller is not the coordinator", async () => {
|
||||
const transaction = contract.startPoll(pollIds[0], encryptionKey)
|
||||
|
||||
await expect(transaction).to.be.revertedWith("SemaphoreVoting: caller is not the poll coordinator")
|
||||
await expect(transaction).to.be.revertedWith("Semaphore__CallerIsNotThePollCoordinator()")
|
||||
})
|
||||
|
||||
it("Should start the poll", async () => {
|
||||
@@ -79,7 +83,7 @@ describe("SemaphoreVoting", () => {
|
||||
it("Should not start a poll if it has already been started", async () => {
|
||||
const transaction = contract.connect(accounts[1]).startPoll(pollIds[0], encryptionKey)
|
||||
|
||||
await expect(transaction).to.be.revertedWith("SemaphoreVoting: poll has already been started")
|
||||
await expect(transaction).to.be.revertedWith("Semaphore__PollHasAlreadyBeenStarted()")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -94,7 +98,7 @@ describe("SemaphoreVoting", () => {
|
||||
|
||||
const transaction = contract.addVoter(pollIds[0], identityCommitment)
|
||||
|
||||
await expect(transaction).to.be.revertedWith("SemaphoreVoting: caller is not the poll coordinator")
|
||||
await expect(transaction).to.be.revertedWith("Semaphore__CallerIsNotThePollCoordinator()")
|
||||
})
|
||||
|
||||
it("Should not add a voter if the poll has already been started", async () => {
|
||||
@@ -103,7 +107,7 @@ describe("SemaphoreVoting", () => {
|
||||
|
||||
const transaction = contract.connect(accounts[1]).addVoter(pollIds[0], identityCommitment)
|
||||
|
||||
await expect(transaction).to.be.revertedWith("SemaphoreVoting: voters can only be added before voting")
|
||||
await expect(transaction).to.be.revertedWith("Semaphore__PollHasAlreadyBeenStarted()")
|
||||
})
|
||||
|
||||
it("Should add a voter to an existing poll", async () => {
|
||||
@@ -158,7 +162,7 @@ describe("SemaphoreVoting", () => {
|
||||
it("Should not cast a vote if the caller is not the coordinator", async () => {
|
||||
const transaction = contract.castVote(bytes32Vote, publicSignals.nullifierHash, pollIds[0], solidityProof)
|
||||
|
||||
await expect(transaction).to.be.revertedWith("SemaphoreVoting: caller is not the poll coordinator")
|
||||
await expect(transaction).to.be.revertedWith("Semaphore__CallerIsNotThePollCoordinator()")
|
||||
})
|
||||
|
||||
it("Should not cast a vote if the poll is not ongoing", async () => {
|
||||
@@ -166,7 +170,7 @@ describe("SemaphoreVoting", () => {
|
||||
.connect(accounts[1])
|
||||
.castVote(bytes32Vote, publicSignals.nullifierHash, pollIds[2], solidityProof)
|
||||
|
||||
await expect(transaction).to.be.revertedWith("SemaphoreVoting: vote can only be cast in an ongoing poll")
|
||||
await expect(transaction).to.be.revertedWith("Semaphore__PollIsNotOngoing()")
|
||||
})
|
||||
|
||||
it("Should not cast a vote if the proof is not valid", async () => {
|
||||
@@ -200,7 +204,7 @@ describe("SemaphoreVoting", () => {
|
||||
it("Should not end the poll if the caller is not the coordinator", async () => {
|
||||
const transaction = contract.endPoll(pollIds[1], decryptionKey)
|
||||
|
||||
await expect(transaction).to.be.revertedWith("SemaphoreVoting: caller is not the poll coordinator")
|
||||
await expect(transaction).to.be.revertedWith("Semaphore__CallerIsNotThePollCoordinator()")
|
||||
})
|
||||
|
||||
it("Should end the poll", async () => {
|
||||
@@ -212,7 +216,7 @@ describe("SemaphoreVoting", () => {
|
||||
it("Should not end a poll if it has already been ended", async () => {
|
||||
const transaction = contract.connect(accounts[1]).endPoll(pollIds[1], encryptionKey)
|
||||
|
||||
await expect(transaction).to.be.revertedWith("SemaphoreVoting: poll is not ongoing")
|
||||
await expect(transaction).to.be.revertedWith("Semaphore__PollIsNotOngoing()")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -26,7 +26,11 @@ describe("SemaphoreWhistleblowing", () => {
|
||||
|
||||
before(async () => {
|
||||
const { address: verifierAddress } = await run("deploy:verifier", { logs: false, depth: treeDepth })
|
||||
contract = await run("deploy:semaphore-whistleblowing", { logs: false, verifier: verifierAddress })
|
||||
contract = await run("deploy:semaphore-whistleblowing", {
|
||||
logs: false,
|
||||
verifiers: [{ merkleTreeDepth: treeDepth, contractAddress: verifierAddress }]
|
||||
})
|
||||
|
||||
accounts = await ethers.getSigners()
|
||||
editor = await accounts[1].getAddress()
|
||||
})
|
||||
@@ -35,9 +39,7 @@ describe("SemaphoreWhistleblowing", () => {
|
||||
it("Should not create an entity with a wrong depth", async () => {
|
||||
const transaction = contract.createEntity(entityIds[0], editor, 10)
|
||||
|
||||
await expect(transaction).to.be.revertedWith(
|
||||
"SemaphoreWhistleblowing: Merkle tree depth value is not supported"
|
||||
)
|
||||
await expect(transaction).to.be.revertedWith("Semaphore__MerkleTreeDepthIsNotSupported()")
|
||||
})
|
||||
|
||||
it("Should not create an entity greater than the snark scalar field", async () => {
|
||||
@@ -70,7 +72,7 @@ describe("SemaphoreWhistleblowing", () => {
|
||||
|
||||
const transaction = contract.addWhistleblower(entityIds[0], identityCommitment)
|
||||
|
||||
await expect(transaction).to.be.revertedWith("SemaphoreWhistleblowing: caller is not the editor")
|
||||
await expect(transaction).to.be.revertedWith("Semaphore__CallerIsNotTheEditor()")
|
||||
})
|
||||
|
||||
it("Should add a whistleblower to an existing entity", async () => {
|
||||
@@ -107,7 +109,7 @@ describe("SemaphoreWhistleblowing", () => {
|
||||
|
||||
const transaction = contract.removeWhistleblower(entityIds[0], identityCommitment, siblings, pathIndices)
|
||||
|
||||
await expect(transaction).to.be.revertedWith("SemaphoreWhistleblowing: caller is not the editor")
|
||||
await expect(transaction).to.be.revertedWith("Semaphore__CallerIsNotTheEditor()")
|
||||
})
|
||||
|
||||
it("Should remove a whistleblower from an existing entity", async () => {
|
||||
@@ -168,7 +170,7 @@ describe("SemaphoreWhistleblowing", () => {
|
||||
solidityProof
|
||||
)
|
||||
|
||||
await expect(transaction).to.be.revertedWith("SemaphoreWhistleblowing: caller is not the editor")
|
||||
await expect(transaction).to.be.revertedWith("Semaphore__CallerIsNotTheEditor()")
|
||||
})
|
||||
|
||||
it("Should not publish a leak if the proof is not valid", async () => {
|
||||
|
||||
16
yarn.lock
16
yarn.lock
@@ -13375,7 +13375,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"semaphore@>=1.0.1, semaphore@workspace:.":
|
||||
"semaphore@npm:>=1.0.1, semaphore@npm:^1.0.3, semaphore@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "semaphore@npm:1.1.0"
|
||||
checksum: d2445d232ad9959048d4748ef54eb01bc7b60436be2b42fb7de20c4cffacf70eafeeecd3772c1baf408cfdce3805fa6618a4389590335671f18cde54ef3cfae4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"semaphore@workspace:.":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "semaphore@workspace:."
|
||||
dependencies:
|
||||
@@ -13426,13 +13433,6 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"semaphore@npm:^1.0.3, semaphore@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "semaphore@npm:1.1.0"
|
||||
checksum: d2445d232ad9959048d4748ef54eb01bc7b60436be2b42fb7de20c4cffacf70eafeeecd3772c1baf408cfdce3805fa6618a4389590335671f18cde54ef3cfae4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.3.0, semver@npm:^5.5.0, semver@npm:^5.5.1, semver@npm:^5.6.0, semver@npm:^5.7.0":
|
||||
version: 5.7.1
|
||||
resolution: "semver@npm:5.7.1"
|
||||
|
||||
Reference in New Issue
Block a user