Browse Source

Pinner init

develop
Ezerous 4 years ago
parent
commit
2e3cada302
  1. 2
      docker/Makefile
  2. 2
      docker/docker-compose.yml
  3. 2
      packages/concordia-app/src/options/web3Options.js
  4. 12
      packages/concordia-app/src/views/Home/index.jsx
  5. 82
      packages/concordia-contracts/contracts/Forum.sol
  6. 2
      packages/concordia-contracts/contracts/Migrations.sol
  7. 4
      packages/concordia-contracts/package.json
  8. 2
      packages/concordia-contracts/truffle-config.js
  9. 10
      packages/concordia-pinner/.gitattributes
  10. 40
      packages/concordia-pinner/.gitignore
  11. 33
      packages/concordia-pinner/package.json
  12. 44
      packages/concordia-pinner/src/index.js
  13. 28
      packages/concordia-pinner/src/options/ipfsOptions.js
  14. 91
      packages/concordia-pinner/src/options/libp2pBundle.js
  15. 12
      packages/concordia-pinner/src/utils/ipfsUtils.js
  16. 56
      packages/concordia-pinner/src/utils/orbitUtils.js
  17. 1
      packages/concordia-rendezvous/package.json
  18. 1208
      yarn.lock

2
docker/Makefile

@ -28,7 +28,7 @@ run-ganache-test:
# Rendezvous targets # Rendezvous targets
run-rendezvous: run-rendezvous:
@docker network create --driver bridge concordia_rendezvous_network || true &&\ @docker network create --driver bridge concordia_rendezvous_network || true &&\
docker run -d -p 9090:9090 --name concordia-rendezvous libp2p/js-libp2p-webrtc-star:version-0.20.1 docker run -d -p 9090:9090 --name concordia-rendezvous libp2p/js-libp2p-webrtc-star:version-0.20.5
# Contracts targets # Contracts targets
build-contracts: build-contracts:

2
docker/docker-compose.yml

@ -21,7 +21,7 @@ services:
restart: always restart: always
rendezvous: rendezvous:
image: libp2p/js-libp2p-webrtc-star:version-0.20.1 image: libp2p/js-libp2p-webrtc-star:version-0.20.5
container_name: concordia-rendezvous container_name: concordia-rendezvous
networks: networks:
rendezvous_network: rendezvous_network:

2
packages/concordia-app/src/options/web3Options.js

@ -17,7 +17,7 @@ const web3WebsocketOptions = {
}; };
const web3 = (WEB3_HOST !== undefined && WEB3_PORT !== undefined) const web3 = (WEB3_HOST !== undefined && WEB3_PORT !== undefined)
? new Web3.providers.WebsocketProvider(`ws://${WEB3_HOST}:${WEB3_PORT}`) ? new Web3(new Web3.providers.WebsocketProvider(`ws://${WEB3_HOST}:${WEB3_PORT}`))
: new Web3(Web3.givenProvider || new Web3.providers.WebsocketProvider( : new Web3(Web3.givenProvider || new Web3.providers.WebsocketProvider(
`ws://${WEB3_HOST_DEFAULT}:${WEB3_PORT_DEFAULT}`, web3WebsocketOptions, `ws://${WEB3_HOST_DEFAULT}:${WEB3_PORT_DEFAULT}`, web3WebsocketOptions,
)); ));

12
packages/concordia-app/src/views/Home/index.jsx

@ -8,20 +8,20 @@ import './styles.css';
import { drizzle } from '../../redux/store'; import { drizzle } from '../../redux/store';
import { FORUM_CONTRACT } from '../../constants/contracts/ContractNames'; import { FORUM_CONTRACT } from '../../constants/contracts/ContractNames';
const { contracts: { [FORUM_CONTRACT]: { methods: { getNumberOfTopics } } } } = drizzle; const { contracts: { [FORUM_CONTRACT]: { methods: { numTopics } } } } = drizzle;
const Home = () => { const Home = () => {
const [numberOfTopicsCallHash, setNumberOfTopicsCallHash] = useState(''); const [numberOfTopicsCallHash, setNumberOfTopicsCallHash] = useState('');
const getNumberOfTopicsResults = useSelector((state) => state.contracts[FORUM_CONTRACT].getNumberOfTopics); const numTopicsResults = useSelector((state) => state.contracts[FORUM_CONTRACT].numTopics);
useEffect(() => { useEffect(() => {
setNumberOfTopicsCallHash(getNumberOfTopics.cacheCall()); setNumberOfTopicsCallHash(numTopics.cacheCall());
}, []); }, []);
const numberOfTopics = useMemo(() => (getNumberOfTopicsResults[numberOfTopicsCallHash] !== undefined const numberOfTopics = useMemo(() => (numTopicsResults[numberOfTopicsCallHash] !== undefined
? parseInt(getNumberOfTopicsResults[numberOfTopicsCallHash].value, 10) ? parseInt(numTopicsResults[numberOfTopicsCallHash].value, 10)
: null), : null),
[getNumberOfTopicsResults, numberOfTopicsCallHash]); [numTopicsResults, numberOfTopicsCallHash]);
return useMemo(() => ( return useMemo(() => (
<Container id="home-container" textAlign="center"> <Container id="home-container" textAlign="center">

82
packages/concordia-contracts/contracts/Forum.sol

@ -1,8 +1,12 @@
//SPDX-License-Identifier: MIT //SPDX-License-Identifier: MIT
pragma solidity 0.7.1; pragma solidity 0.8.0;
pragma experimental ABIEncoderV2;
contract Forum { contract Forum {
// Error messages for require()
string public constant USER_HAS_NOT_SIGNED_UP = "User hasn't signed up yet.";
string public constant USERNAME_TAKEN = "Username is already taken.";
string public constant TOPIC_DOES_NOT_EXIST = "Topic doesn't exist.";
string public constant POST_DOES_NOT_EXIST = "Post doesn't exist.";
//----------------------------------------USER---------------------------------------- //----------------------------------------USER----------------------------------------
struct User { struct User {
@ -13,40 +17,41 @@ contract Forum {
bool signedUp; // Helper variable for hasUserSignedUp() bool signedUp; // Helper variable for hasUserSignedUp()
} }
mapping (address => User) users; mapping(address => User) users;
mapping (string => address) userAddresses; mapping(string => address) usernameAddresses;
address[] userAddresses;
event UserSignedUp(string username, address userAddress); event UserSignedUp(string username, address userAddress);
event UsernameUpdated(string newName, string oldName, address userAddress); event UsernameUpdated(string newName, string oldName, address userAddress);
function signUp(string memory username) public returns (bool) { function signUp(string memory username) public returns (bool) {
require (!hasUserSignedUp(msg.sender), "User has already signed up."); require(!hasUserSignedUp(msg.sender), USER_HAS_NOT_SIGNED_UP);
require(!isUserNameTaken(username), "Username is already taken."); require(!isUserNameTaken(username), USERNAME_TAKEN);
users[msg.sender] = User(username, users[msg.sender] = User(username, new uint[](0), new uint[](0), block.timestamp, true);
new uint[](0), new uint[](0), block.timestamp, true); usernameAddresses[username] = msg.sender;
userAddresses[username] = msg.sender; userAddresses.push(msg.sender);
emit UserSignedUp(username, msg.sender); emit UserSignedUp(username, msg.sender);
return true; return true;
} }
function updateUsername(string memory newUsername) public returns (bool) { function updateUsername(string memory newUsername) public returns (bool) {
require (hasUserSignedUp(msg.sender), "User hasn't signed up yet."); require(hasUserSignedUp(msg.sender), USER_HAS_NOT_SIGNED_UP);
require(!isUserNameTaken(newUsername), "Username is already taken."); require(!isUserNameTaken(newUsername), USERNAME_TAKEN);
string memory oldUsername = getUsername(msg.sender); string memory oldUsername = getUsername(msg.sender);
delete userAddresses[users[msg.sender].username]; delete usernameAddresses[users[msg.sender].username];
users[msg.sender].username = newUsername; users[msg.sender].username = newUsername;
userAddresses[newUsername] = msg.sender; usernameAddresses[newUsername] = msg.sender;
emit UsernameUpdated(newUsername, oldUsername, msg.sender); emit UsernameUpdated(newUsername, oldUsername, msg.sender);
return true; return true;
} }
function getUsername(address userAddress) public view returns (string memory) { function getUsername(address userAddress) public view returns (string memory) {
require (hasUserSignedUp(userAddress), "User hasn't signed up yet."); require(hasUserSignedUp(userAddress), USER_HAS_NOT_SIGNED_UP);
return users[userAddress].username; return users[userAddress].username;
} }
function getUserAddress(string memory username) public view returns (address) { function getUserAddress(string memory username) public view returns (address) {
return userAddresses[username]; return usernameAddresses[username];
} }
function hasUserSignedUp(address userAddress) public view returns (bool) { function hasUserSignedUp(address userAddress) public view returns (bool) {
@ -54,31 +59,35 @@ contract Forum {
} }
function isUserNameTaken(string memory username) public view returns (bool) { function isUserNameTaken(string memory username) public view returns (bool) {
if (getUserAddress(username)!=address(0)) if (getUserAddress(username) != address(0))
return true; return true;
return false; return false;
} }
function getUserTopics(address userAddress) public view returns (uint[] memory) { function getUserTopics(address userAddress) public view returns (uint[] memory) {
require (hasUserSignedUp(userAddress), "User hasn't signed up yet."); require(hasUserSignedUp(userAddress), USER_HAS_NOT_SIGNED_UP);
return users[userAddress].topicIDs; return users[userAddress].topicIDs;
} }
function getUserPosts(address userAddress) public view returns (uint[] memory) { function getUserPosts(address userAddress) public view returns (uint[] memory) {
require (hasUserSignedUp(userAddress), "User hasn't signed up yet."); require(hasUserSignedUp(userAddress), USER_HAS_NOT_SIGNED_UP);
return users[userAddress].postIDs; return users[userAddress].postIDs;
} }
function getUserDateOfRegister(address userAddress) public view returns (uint) { function getUserDateOfRegister(address userAddress) public view returns (uint) {
require (hasUserSignedUp(userAddress), "User hasn't signed up yet."); require(hasUserSignedUp(userAddress), USER_HAS_NOT_SIGNED_UP);
return users[userAddress].timestamp; return users[userAddress].timestamp;
} }
function getUser(address userAddress) public view returns (User memory) { function getUser(address userAddress) public view returns (User memory) {
require(hasUserSignedUp(userAddress), "User hasn't signed up yet."); require(hasUserSignedUp(userAddress), USER_HAS_NOT_SIGNED_UP);
return users[userAddress]; return users[userAddress];
} }
function getUserAddresses() public view returns (address[] memory) {
return userAddresses;
}
//----------------------------------------POSTING---------------------------------------- //----------------------------------------POSTING----------------------------------------
struct Topic { struct Topic {
uint topicID; uint topicID;
@ -94,17 +103,17 @@ contract Forum {
uint topicID; uint topicID;
} }
uint numTopics; // Total number of topics uint public numTopics; // Total number of topics
uint numPosts; // Total number of posts uint public numPosts; // Total number of posts
mapping (uint => Topic) topics; mapping(uint => Topic) topics;
mapping (uint => Post) posts; mapping(uint => Post) posts;
event TopicCreated(uint topicID, uint postID); event TopicCreated(uint topicID, uint postID);
event PostCreated(uint postID, uint topicID); event PostCreated(uint postID, uint topicID);
function createTopic() public returns (uint, uint) { function createTopic() public returns (uint, uint) {
require(hasUserSignedUp(msg.sender)); // Only registered users can create topics require(hasUserSignedUp(msg.sender), USER_HAS_NOT_SIGNED_UP);
//Creates topic //Creates topic
uint topicID = numTopics++; uint topicID = numTopics++;
topics[topicID] = Topic(topicID, msg.sender, block.timestamp, new uint[](0)); topics[topicID] = Topic(topicID, msg.sender, block.timestamp, new uint[](0));
@ -121,8 +130,8 @@ contract Forum {
} }
function createPost(uint topicID) public returns (uint) { function createPost(uint topicID) public returns (uint) {
require(hasUserSignedUp(msg.sender)); // Only registered users can create posts require(hasUserSignedUp(msg.sender), USER_HAS_NOT_SIGNED_UP);
require(topicID<numTopics); // Only allow posting to a topic that exists require(topicExists(topicID), TOPIC_DOES_NOT_EXIST);
uint postID = numPosts++; uint postID = numPosts++;
posts[postID] = Post(postID, msg.sender, block.timestamp, topicID); posts[postID] = Post(postID, msg.sender, block.timestamp, topicID);
topics[topicID].postIDs.push(postID); topics[topicID].postIDs.push(postID);
@ -131,12 +140,16 @@ contract Forum {
return postID; return postID;
} }
function getNumberOfTopics() public view returns (uint) { function topicExists(uint topicID) public view returns (bool) {
return numTopics; return topicID < numTopics;
}
function postExists(uint postID) public view returns (bool) {
return postID < numPosts;
} }
function getTopic(uint topicID) public view returns (address, string memory, uint, uint[] memory) { function getTopic(uint topicID) public view returns (address, string memory, uint, uint[] memory) {
require(topicID<numTopics); require(topicExists(topicID), TOPIC_DOES_NOT_EXIST);
return ( return (
topics[topicID].author, topics[topicID].author,
users[topics[topicID].author].username, users[topics[topicID].author].username,
@ -146,12 +159,17 @@ contract Forum {
} }
function getTopicPosts(uint topicID) public view returns (uint[] memory) { function getTopicPosts(uint topicID) public view returns (uint[] memory) {
require(topicID<numTopics); // Topic should exist require(topicExists(topicID), TOPIC_DOES_NOT_EXIST);
return topics[topicID].postIDs; return topics[topicID].postIDs;
} }
function getTopicAuthor(uint topicID) public view returns (address) {
require(topicExists(topicID), TOPIC_DOES_NOT_EXIST);
return topics[topicID].author;
}
function getPost(uint postID) public view returns (address, string memory, uint, uint) { function getPost(uint postID) public view returns (address, string memory, uint, uint) {
require(postID<numPosts); require(postExists(postID), POST_DOES_NOT_EXIST);
return ( return (
posts[postID].author, posts[postID].author,
users[posts[postID].author].username, users[posts[postID].author].username,

2
packages/concordia-contracts/contracts/Migrations.sol

@ -1,5 +1,5 @@
//SPDX-License-Identifier: MIT //SPDX-License-Identifier: MIT
pragma solidity 0.7.1; pragma solidity 0.8.0;
contract Migrations { contract Migrations {
address public owner; address public owner;

4
packages/concordia-contracts/package.json

@ -15,8 +15,8 @@
"_migrate": "yarn truffle migrate" "_migrate": "yarn truffle migrate"
}, },
"dependencies": { "dependencies": {
"@openzeppelin/contracts": "~3.2.0", "@openzeppelin/contracts": "~3.3.0",
"truffle": "~5.1.45" "truffle": "~5.1.58"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^6.8.0", "eslint": "^6.8.0",

2
packages/concordia-contracts/truffle-config.js

@ -10,7 +10,7 @@ module.exports = {
// to customize your Truffle configuration! // to customize your Truffle configuration!
compilers: { compilers: {
solc: { solc: {
version: '0.7.1', version: '0.8.0',
}, },
}, },
contracts_build_directory: path.join(__dirname, 'build/'), contracts_build_directory: path.join(__dirname, 'build/'),

10
packages/concordia-pinner/.gitattributes

@ -0,0 +1,10 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text=auto eol=lf
# Denote all files that are truly binary and should not be modified.
*.png binary
*.jpg binary
*.ico binary
# Solidity
*.sol linguist-language=Solidity

40
packages/concordia-pinner/.gitignore

@ -0,0 +1,40 @@
# Node
/node_modules
packages/*/node_modules
packages/concordia-contracts/build
# IDE
.DS_Store
.idea
# Build Directories
/build
/src/build
/packages/concordia-app/build
/packages/concordia-contracts/build
# Logs
/log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Docker volumes
docker/volumes
docker/ganache/volumes
docker/reports
# Env var files
docker/env/concordia.env
docker/env/contracts.env
# Misc
.env.local
.env.development.local
.env.test.local
.env.production.local
# IPFS & OrbitDB Storage
ipfs
orbitdb

33
packages/concordia-pinner/package.json

@ -0,0 +1,33 @@
{
"name": "concordia-pinner",
"description": "An OrbitDB pinning service for Concordia.",
"version": "0.1.0",
"private": true,
"main": "src/index.js",
"scripts": {
"start": "node -r esm src/index.js",
"clean": "rimraf ipfs orbitdb"
},
"license": "MIT",
"dependencies": {
"@ezerous/eth-identity-provider": "~0.1.2",
"esm": "~3.2.25",
"ipfs": "~0.52.1",
"level": "~6.0.1",
"libp2p": "~0.30.0",
"libp2p-bootstrap": "~0.12.1",
"libp2p-gossipsub": "~0.7.0",
"libp2p-kad-dht": "~0.20.1",
"libp2p-mdns": "~0.15.0",
"libp2p-mplex": "~0.10.0",
"libp2p-noise": "~2.0.1",
"libp2p-tcp": "~0.15.1",
"libp2p-webrtc-star": "~0.20.2",
"orbit-db": "~0.26.0",
"orbit-db-identity-provider": "~0.3.1",
"rimraf": "~3.0.2",
"web3": "~1.3.0",
"web3-eth-contract": "^1.3.1",
"wrtc": "~0.4.6"
}
}

44
packages/concordia-pinner/src/index.js

@ -0,0 +1,44 @@
import Web3 from 'web3';
import Contract from 'web3-eth-contract';
import { forumContract } from 'concordia-contracts';
import { createOrbitInstance, getPeerDatabases, openKVDBs } from './utils/orbitUtils';
async function main () {
const web3ProviderUrl = 'ws://127.0.0.1:8545';
const web3 = new Web3(new Web3.providers.WebsocketProvider(web3ProviderUrl));
const networkId = await web3.eth.net.getId();
const contractAddress = forumContract.networks[networkId].address;
Contract.setProvider(web3ProviderUrl);
const contract = new Contract(forumContract.abi, contractAddress);
const orbit = await createOrbitInstance(contractAddress);
// Open & replicate databases of existing users
const userAddresses = await contract.methods.getUserAddresses().call();
const peerDBs = await getPeerDatabases(orbit, userAddresses);
await openKVDBs(orbit, peerDBs);
// Listen for new users and subscribe to their databases
const eventJsonInterface = web3.utils._.find(
contract._jsonInterface,
obj => obj.name === "UserSignedUp" && obj.type === 'event'
);
web3.eth.subscribe('logs', {
address: contractAddress,
topics: [eventJsonInterface.signature]
}, function(error, result){
if (!error) {
const eventObj = web3.eth.abi.decodeLog(
eventJsonInterface.inputs,
result.data,
result.topics.slice(1)
)
console.log(`UserSignedUp!`, eventObj[1])
getPeerDatabases(orbit, userAddresses).then(peerDBs => openKVDBs(orbit, peerDBs));
}
});
}
main();

28
packages/concordia-pinner/src/options/ipfsOptions.js

@ -0,0 +1,28 @@
import libp2pBundle from './libp2pBundle'
export default {
repo: 'ipfs',
config: {
Addresses: {
Swarm: [
// Use local signaling server (see also rendezvous script in package.json)
// For more information: https://github.com/libp2p/js-libp2p-webrtc-star
`/ip4/127.0.0.1/tcp/9090/wss/p2p-webrtc-star`,
// Use the following public servers if needed
// '/dns4/wrtc-star1.par.dwebops.pub/tcp/443/wss/p2p-webrtc-star',
// '/dns4/ wrtc-star2.sjc.dwebops.pub/tcp/443/wss/p2p-webrtc-star'
],
},
},
libp2p: libp2pBundle,
EXPERIMENTAL: {
pubsub: true,
},
preload: {
enabled: false,
},
init: {
emptyRepo: true,
},
}

91
packages/concordia-pinner/src/options/libp2pBundle.js

@ -0,0 +1,91 @@
import Libp2p from 'libp2p';
import wrtc from 'wrtc';
import MulticastDNS from 'libp2p-mdns';
import WebrtcStar from 'libp2p-webrtc-star';
import Bootstrap from 'libp2p-bootstrap';
import Gossipsub from 'libp2p-gossipsub';
import KadDHT from 'libp2p-kad-dht';
import MPLEX from 'libp2p-mplex';
import { NOISE } from 'libp2p-noise';
// See also: https://github.com/libp2p/js-libp2p/blob/master/doc/CONFIGURATION.md
export default (opts) => {
return new Libp2p({
peerId: opts.peerId,
addresses: {
listen: ['/ip4/127.0.0.1/tcp/9090/wss/p2p-webrtc-star']
},
connectionManager: {
minPeers: 25,
maxPeers: 100,
pollInterval: 5000
},
modules: {
transport: [
WebrtcStar
],
streamMuxer: [
MPLEX
],
connEncryption: [
NOISE
],
peerDiscovery: [
MulticastDNS,
Bootstrap
],
dht: KadDHT,
pubsub: Gossipsub
},
config: {
transport: {
[WebrtcStar.prototype[Symbol.toStringTag]]: {
wrtc
}
},
peerDiscovery: {
autoDial: true,
mdns: {
enabled: true,
interval: 10000
},
bootstrap: {
enabled: true,
interval: 30e3,
list: opts.config.Bootstrap
}
},
relay: {
enabled: true,
hop: {
enabled: true,
active: true
}
},
dht: {
enabled: true,
kBucketSize: 20,
randomWalk: {
enabled: true,
interval: 10e3,
timeout: 2e3
}
},
pubsub: {
enabled: true,
emitself: true
}
},
metrics: {
enabled: true,
computeThrottleMaxQueueSize: 1000,
computeThrottleTimeout: 2000,
movingAverageIntervals: [
60 * 1000, // 1 minute
5 * 60 * 1000, // 5 minutes
15 * 60 * 1000 // 15 minutes
],
maxOldPeersRetention: 50
}
})
}

12
packages/concordia-pinner/src/utils/ipfsUtils.js

@ -0,0 +1,12 @@
export function displayIPFSStats(ipfs){
//TODO: investigate occasional printing of duplicate peers
setInterval(async () => {
try {
const peers = await ipfs.swarm.peers()
console.log(`Number of peers: ${peers.length}`)
console.dir(peers)
} catch (err) {
console.log('IPFS peer logging error:', err)
}
}, 3000)
}

56
packages/concordia-pinner/src/utils/orbitUtils.js

@ -0,0 +1,56 @@
import IPFS from 'ipfs';
import OrbitDB from 'orbit-db';
import Identities from 'orbit-db-identity-provider';
import { EthereumContractIdentityProvider } from '@ezerous/eth-identity-provider';
import Web3 from 'web3';
import ipfsOptions from '../options/ipfsOptions';
import { displayIPFSStats } from './ipfsUtils';
export async function createOrbitInstance(contractAddress){
Identities.addIdentityProvider(EthereumContractIdentityProvider);
EthereumContractIdentityProvider.setWeb3(new Web3()); // We need a fully-featured new Web3 for signature verification
EthereumContractIdentityProvider.setContractAddress(contractAddress);
const ipfs = await IPFS.create(ipfsOptions);
displayIPFSStats(ipfs);
return await OrbitDB.createInstance(ipfs);
}
export async function getPeerDatabases(orbit, userAddresses) {
const peerDBs = [];
for (const userAddress of userAddresses) {
peerDBs.push(await determineKVAddress({ orbit, dbName:'user', userAddress }));
peerDBs.push(await determineKVAddress({ orbit, dbName:'posts', userAddress }));
peerDBs.push(await determineKVAddress({ orbit, dbName:'topics', userAddress }));
}
return peerDBs;
}
export async function openKVDBs(orbit, databases) {
for (const db of databases){
const store = await orbit.keyvalue(db);
store.events.on('replicated', (address) => console.log(`Replicated ${address}`));
console.log(`Opened ${db}`);
}
}
// TODO: share code below with frontend (?)
async function determineDBAddress({
orbit, dbName, type, identityId,
}) {
const ipfsMultihash = (await orbit.determineAddress(dbName, type, {
accessController: { write: [identityId] },
})).root;
return `/orbitdb/${ipfsMultihash}/${dbName}`;
}
async function determineKVAddress({ orbit, dbName, userAddress }) {
return determineDBAddress({
orbit, dbName, type: 'keyvalue', identityId: userAddress + EthereumContractIdentityProvider.contractAddress,
});
}

1
packages/concordia-rendezvous/package.json

@ -4,7 +4,6 @@
"private": true, "private": true,
"description": "Rendezvous server for Concordia", "description": "Rendezvous server for Concordia",
"scripts": { "scripts": {
"rendezvous": "star-signal --port=9090 --host=127.0.0.1",
"start": "star-signal --port=9090 --host=127.0.0.1" "start": "star-signal --port=9090 --host=127.0.0.1"
}, },
"dependencies": { "dependencies": {

1208
yarn.lock

File diff suppressed because it is too large
Loading…
Cancel
Save