first cut at backend part of rollup implementation
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 40s
Details
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 40s
Details
This commit is contained in:
parent
33a458aba1
commit
073f6e61aa
|
@ -1,14 +1,14 @@
|
|||
{
|
||||
"localhost": {
|
||||
"DAO": "0x21E65E57a2F7DF8B85E0F59b57afc024595dD833",
|
||||
"Work1": "0x0686417a476C37701734dd405e381b7d9B247d22",
|
||||
"Onboarding": "0x495c5AF3fD0B1c2bA3e959f11A48d6FC8C246633",
|
||||
"Proposals": "0x43e45a19FAD2932D08c8D9B07f6830d1250DA71D",
|
||||
"Rollup": "0x5E614c4d8C956A937e4a6acC6a9459CAAE193feA",
|
||||
"Work2": "0xE3CC69EF45312959F9F752C971C35F553a165559",
|
||||
"Reputation": "0xb8C047c11eF88A02cd2C706120ef27D67586785a",
|
||||
"Forum": "0xdAA95487F0027C67473A4A560dD6628c49d218A9",
|
||||
"Bench": "0x05d4aE5d0097d47C9FcC0191d7f68F175a8122Db"
|
||||
"DAO": "0xc7E04c11eD94E375857b885b3e6E1Db30C061348",
|
||||
"Work1": "0x1bEffEB10E9f5714a8e385FfcA84046688677eA8",
|
||||
"Onboarding": "0xFC40076c675693441C6e553FEdDD3A3348db81E4",
|
||||
"Proposals": "0xa1349A27D43d0F71CeDD75904ACc8f8CF8F81582",
|
||||
"Rollup": "0x1361c87D5972a71cBCA34f6EAD928358deaC750D",
|
||||
"Work2": "0x691Bcb6a8378Cec103BE58Dfa037DC57E6FFf4d1",
|
||||
"Reputation": "0xfC979dbae6Cd0f35CC240889663B523B35c5F101",
|
||||
"Forum": "0xaf247e316A081871e713F492279D2360bd162401",
|
||||
"Bench": "0xC8BCE8171e626d07E5095256F703B1df23a67362"
|
||||
},
|
||||
"sepolia": {
|
||||
"DAO": "0x8e5bd58B2ca8910C5F9be8de847d6883B15c60d2",
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -19,7 +19,8 @@
|
|||
"fastq": "^1.17.1",
|
||||
"level": "^8.0.1",
|
||||
"matrix-bot-sdk": "^0.7.1",
|
||||
"object-hash": "^3.0.0"
|
||||
"object-hash": "^3.0.0",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.56.0",
|
||||
|
|
|
@ -20,7 +20,8 @@
|
|||
"fastq": "^1.17.1",
|
||||
"level": "^8.0.1",
|
||||
"matrix-bot-sdk": "^0.7.1",
|
||||
"object-hash": "^3.0.0"
|
||||
"object-hash": "^3.0.0",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.56.0",
|
||||
|
|
|
@ -13,8 +13,8 @@ const {
|
|||
const login = async () => {
|
||||
console.log(`MATRIX_HOMESERVER_URL="${MATRIX_HOMESERVER_URL}"`);
|
||||
const auth = new MatrixAuth(MATRIX_HOMESERVER_URL);
|
||||
const client = await auth.passwordLogin(MATRIX_USER, MATRIX_PASSWORD);
|
||||
console.log(`MATRIX_ACCESS_TOKEN="${client.accessToken}"`);
|
||||
const matrixClient = await auth.passwordLogin(MATRIX_USER, MATRIX_PASSWORD);
|
||||
console.log(`MATRIX_ACCESS_TOKEN="${matrixClient.accessToken}"`);
|
||||
};
|
||||
|
||||
login();
|
||||
|
|
|
@ -1,28 +1,13 @@
|
|||
const { getMatrixClient } = require('../matrix-bot');
|
||||
const { matrixClient } = require('../matrix-bot');
|
||||
const { matrixUserToAuthorAddress } = require('../util/db');
|
||||
const write = require('../util/forum/write');
|
||||
const { dao, wallet } = require('../util/contracts');
|
||||
const { wallet } = require('../util/contracts');
|
||||
const addPostWithRetry = require('../util/add-post-with-retry');
|
||||
|
||||
const {
|
||||
ETH_NETWORK,
|
||||
} = process.env;
|
||||
|
||||
const addPostWithRetry = async (authors, hash, citations, retryDelay = 5000) => {
|
||||
try {
|
||||
await dao.addPost(authors, hash, citations);
|
||||
} catch (e) {
|
||||
if (e.code === 'REPLACEMENT_UNDERPRICED') {
|
||||
console.log('retry delay (sec):', retryDelay / 1000);
|
||||
await Promise.delay(retryDelay);
|
||||
return addPostWithRetry(authors, hash, citations, retryDelay * 2);
|
||||
} if (e.reason === 'A post with this postId already exists') {
|
||||
return { alreadyAdded: true };
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
return { alreadyAdded: false };
|
||||
};
|
||||
|
||||
module.exports = async (req, res) => {
|
||||
const {
|
||||
body: {
|
||||
|
@ -43,8 +28,7 @@ module.exports = async (req, res) => {
|
|||
console.log('roomId', roomId);
|
||||
console.log('eventId', eventId);
|
||||
|
||||
const client = getMatrixClient();
|
||||
const event = await client.getEvent(roomId, eventId);
|
||||
const event = await matrixClient.getEvent(roomId, eventId);
|
||||
console.log('event', event);
|
||||
|
||||
let authorAddress;
|
||||
|
@ -82,7 +66,7 @@ module.exports = async (req, res) => {
|
|||
} else {
|
||||
console.log(`Added post to blockchain for matrix event ${eventUri}`);
|
||||
// Send matrix event reply to the targeted event, notifying of this blockchain post
|
||||
await client.replyNotice(roomId, event, `Added to ${ETH_NETWORK} blockchain as post ${hash}`);
|
||||
await matrixClient.replyNotice(roomId, event, `Added to ${ETH_NETWORK} blockchain as post ${hash}`);
|
||||
}
|
||||
|
||||
res.json({ postId: hash, alreadyAdded });
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const { registerMessageHandler } = require('../matrix-bot');
|
||||
const { registerMatrixMessageHandler } = require('../matrix-bot');
|
||||
const { setTargetRoomId } = require('../matrix-bot/outbound-queue');
|
||||
|
||||
const {
|
||||
|
@ -31,7 +31,7 @@ const handleCommand = async (client, roomId, event) => {
|
|||
if (instanceId === BOT_INSTANCE_ID) {
|
||||
setTargetRoomId(roomId);
|
||||
await appState.put('targetRoomId', roomId);
|
||||
await client.replyNotice(roomId, event, `Proposal events will be sent to this room for network ${ETH_NETWORK}`);
|
||||
await client.replyNotice(roomId, event, `Events will be sent to this room (${roomId}) for network ${ETH_NETWORK}`);
|
||||
}
|
||||
} else if (proposalRegex.test(body)) {
|
||||
const [, , proposalIndexStr] = proposalRegex.exec(body);
|
||||
|
@ -55,7 +55,7 @@ const handleCommand = async (client, roomId, event) => {
|
|||
};
|
||||
|
||||
const start = () => {
|
||||
registerMessageHandler(handleCommand);
|
||||
registerMatrixMessageHandler(handleCommand);
|
||||
};
|
||||
|
||||
module.exports = {
|
|
@ -1,6 +1,7 @@
|
|||
const { proposals } = require('../util/contracts');
|
||||
const read = require('../util/forum/read');
|
||||
const { sendNewProposalEvent } = require('../matrix-bot/outbound-queue');
|
||||
const { sendMatrixText } = require('../matrix-bot/outbound-queue');
|
||||
const { proposalEventIds } = require('../util/db');
|
||||
|
||||
// Subscribe to proposal events
|
||||
const start = () => {
|
||||
|
@ -29,8 +30,16 @@ const start = () => {
|
|||
message += `\n\n${JSON.stringify(post.embeddedData, null, 2)}`;
|
||||
}
|
||||
|
||||
// The outbound queue handles deduplication
|
||||
sendNewProposalEvent(proposalIndex, message);
|
||||
try {
|
||||
await proposalEventIds.get(Number(proposalIndex));
|
||||
// If this doesn't throw, it means we already sent a message for this proposal
|
||||
} catch (e) {
|
||||
if (e.status === 404) {
|
||||
console.log('sending new proposal event to room', { message });
|
||||
const { eventId } = await sendMatrixText(message);
|
||||
await proposalEventIds.put(Number(proposalIndex), eventId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
proposals.on('ProposalAccepted', async (proposalIndex) => {
|
|
@ -4,7 +4,7 @@ const {
|
|||
matrixUserToAuthorAddress,
|
||||
authorAddressToMatrixUser,
|
||||
} = require('../util/db');
|
||||
const { registerEventHandler } = require('../matrix-bot');
|
||||
const { registerMatrixEventHandler } = require('../matrix-bot');
|
||||
|
||||
const handleRegisterIdentity = async (client, roomId, event) => {
|
||||
if (event.type !== 'io.dgov.identity.register') return;
|
||||
|
@ -34,7 +34,7 @@ const handleRegisterIdentity = async (client, roomId, event) => {
|
|||
};
|
||||
|
||||
const start = () => {
|
||||
registerEventHandler(handleRegisterIdentity);
|
||||
registerMatrixEventHandler(handleRegisterIdentity);
|
||||
};
|
||||
|
||||
module.exports = {
|
|
@ -0,0 +1,293 @@
|
|||
const Promise = require('bluebird');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
const { getContractAddressByNetworkName } = require('../util/contract-config');
|
||||
const { registerDecider } = require('./validation-pools');
|
||||
const { registerMatrixEventHandler, sendMatrixEvent, sendMatrixText } = require('../matrix-bot');
|
||||
const { matrixPools, matrixUserToAuthorAddress, applicationData } = require('../util/db');
|
||||
const {
|
||||
rollup, wallet, work2, dao,
|
||||
} = require('../util/contracts');
|
||||
const read = require('../util/forum/read');
|
||||
const write = require('../util/forum/write');
|
||||
const addPostWithRetry = require('../util/add-post-with-retry');
|
||||
|
||||
const {
|
||||
ETH_NETWORK,
|
||||
} = process.env;
|
||||
|
||||
let batchWorker;
|
||||
|
||||
const rollupAddress = getContractAddressByNetworkName(ETH_NETWORK, 'Rollup');
|
||||
|
||||
const getBatchPostAuthorWeights = async (batchItems) => {
|
||||
const weights = {};
|
||||
await Promise.each(batchItems, async (postId) => {
|
||||
// TODO: try/catch
|
||||
const post = await read(postId);
|
||||
// TODO: try/catch
|
||||
const matrixPool = await matrixPools.get(postId);
|
||||
const { fee, result: { votePasses, quorumMet } } = matrixPool;
|
||||
post.authors.forEach(({ authorAddress, weightPPM }) => {
|
||||
weights[authorAddress] = weights[authorAddress] ?? 0;
|
||||
if (votePasses && quorumMet) {
|
||||
// scale by matrix pool outcome and strength
|
||||
weights[authorAddress] += weightPPM * fee;
|
||||
}
|
||||
// TODO: Rewards for policing
|
||||
// TODO: Propagation via references
|
||||
});
|
||||
// Rescale author weights so they sum to 1000000
|
||||
const sumOfWeights = Object.values(weights).reduce((t, v) => t + v, 0);
|
||||
const scaledWeights = weights
|
||||
.map((weight) => Math.floor((weight * 1000000) / sumOfWeights));
|
||||
const sumOfScaledWeights = Object.values(scaledWeights).reduce((t, v) => t + v, 0);
|
||||
scaledWeights[0] += 1000000 - sumOfScaledWeights;
|
||||
const authors = Object.entries(scaledWeights)
|
||||
.map(([authorAddress, weightPPM]) => ({ authorAddress, weightPPM }));
|
||||
return authors;
|
||||
});
|
||||
};
|
||||
|
||||
const submitBatchPost = async (batchItems) => {
|
||||
const authors = await getBatchPostAuthorWeights(batchItems);
|
||||
// TODO: Compute citations as aggregate of the citations of posts in the batch
|
||||
const citations = [];
|
||||
const content = `Batch of ${batchItems.length} items`;
|
||||
const embeddedData = {
|
||||
matrixPools: batchItems.map((x) => x.postId),
|
||||
nonce: uuidv4().replace(/-/i, ''),
|
||||
};
|
||||
const sender = await wallet.getAddress();
|
||||
const contentToVerify = `${content}\n\n${JSON.stringify(embeddedData, null, 2)}`;
|
||||
const signature = await wallet.signMessage(contentToVerify);
|
||||
// Write to the forum database
|
||||
const { hash: batchPostId } = await write({
|
||||
sender, authors, citations, content, embeddedData, signature,
|
||||
});
|
||||
// Add rollup post on-chain
|
||||
await addPostWithRetry(authors, batchPostId, citations);
|
||||
// Call Rollup.submitBatch
|
||||
const poolDuration = 60;
|
||||
await rollup.submitBatch(batchPostId, batchItems.length, poolDuration);
|
||||
};
|
||||
|
||||
const evaluateMatrixPoolOutcome = async (postId) => {
|
||||
const matrixPool = await matrixPools.get(postId);
|
||||
// This should already contain all the info we need to evaluate the outcome
|
||||
const { stakes, quorum, winRatio } = matrixPool;
|
||||
const stakedFor = stakes
|
||||
.filter((x) => x.inFavor)
|
||||
.reduce((total, { amount }) => total + amount, 0);
|
||||
const stakedAgainst = stakes
|
||||
.filter((x) => !x.inFavor)
|
||||
.reduce((total, { amount }) => total + amount, 0);
|
||||
const votePasses = stakedFor * winRatio[1] >= (stakedFor + stakedAgainst) * winRatio[0];
|
||||
const totalSupply = await dao.totalSupply();
|
||||
const quorumMet = BigInt(stakedFor + stakedAgainst) * quorum[1] >= totalSupply * quorum[0];
|
||||
const result = {
|
||||
stakedFor, stakedAgainst, totalSupply, votePasses, quorumMet,
|
||||
};
|
||||
console.log('Matrix pool outcome evaluated', result);
|
||||
matrixPool.result = result;
|
||||
await matrixPools.put(postId, matrixPool);
|
||||
sendMatrixEvent('io.dgov.pool.result', result);
|
||||
|
||||
// Even if we're not the current batch worker, keep track of batch items
|
||||
let batchItems;
|
||||
try {
|
||||
batchItems = await applicationData.get('batchItems');
|
||||
} catch (e) {
|
||||
batchItems = [];
|
||||
}
|
||||
batchItems.push(postId);
|
||||
await applicationData.put('batchItems', batchItems);
|
||||
|
||||
if (batchWorker === '0x0000000000000000000000000000000000000000') {
|
||||
// TODO: If there's no batch worker, we should stake our availability
|
||||
// and then submit the batch immediately.
|
||||
await submitBatchPost(batchItems);
|
||||
}
|
||||
|
||||
if (batchWorker === await wallet.getAddress()) {
|
||||
// TODO: If we are the batch worker, we should wait an appropriate amout of time /
|
||||
// number of matrix pools before submitting a batch.
|
||||
}
|
||||
};
|
||||
|
||||
const validateWorkEvidence = async (sender, post) => {
|
||||
let valid = false;
|
||||
if (sender === work2.target) {
|
||||
const expectedContent = 'This is a work evidence post';
|
||||
valid = post.content.startsWith(expectedContent);
|
||||
}
|
||||
console.log(`Work evidence ${valid ? 'matched' : 'did not match'} the expected content`);
|
||||
return valid;
|
||||
};
|
||||
|
||||
const start = async () => {
|
||||
console.log('registering validation pool decider for rollup');
|
||||
registerDecider(async (pool, post) => {
|
||||
// If this is not sent by the work1 contract, it's not of interest here.
|
||||
if (pool.sender !== rollupAddress) return false;
|
||||
|
||||
// A rollup post should contain
|
||||
// - a list of off-chain validation pools
|
||||
// - authorship corresponding to the result of those off-chain pools
|
||||
if (!post.embeddedData?.matrixPools) return false;
|
||||
|
||||
// Our task here is to check whether the posted result agrees with our own computations
|
||||
const expectedAuthors = await getBatchPostAuthorWeights(post.embeddedData.matrixPools);
|
||||
if (expectedAuthors.length !== post.authors.length) return false;
|
||||
return post.authors.every(({ authorAddress, weightPPM }) => {
|
||||
const expectedAuthor = expectedAuthors.find((x) => x.authorAddress === authorAddress);
|
||||
return weightPPM === expectedAuthor.weightPPM;
|
||||
});
|
||||
});
|
||||
|
||||
// Check for an assigned batch worker
|
||||
batchWorker = await rollup.batchWorker();
|
||||
console.log('At startup', { batchWorker });
|
||||
|
||||
rollup.on('BatchWorkerAssigned', async (batchWorker_) => {
|
||||
console.log('Batch worker assigned:', batchWorker);
|
||||
batchWorker = batchWorker_;
|
||||
});
|
||||
|
||||
/// `sender` is the address that called Rollup.addItem on chain, i.e. the Work2 contract.
|
||||
rollup.on('BatchItemAdded', async (postId, sender, fee) => {
|
||||
// If we are the batch worker or there is no batch worker, initiate a matrix pool
|
||||
if (batchWorker === await wallet.getAddress()
|
||||
|| batchWorker === '0x0000000000000000000000000000000000000000') {
|
||||
const duration = 20;
|
||||
const quorum = [1, 3];
|
||||
const winRatio = [1, 2];
|
||||
const params = {
|
||||
sender, fee, duration, quorum, winRatio,
|
||||
};
|
||||
|
||||
let post;
|
||||
try {
|
||||
post = await read(postId);
|
||||
} catch (e) {
|
||||
console.error(`Post ID ${postId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize a matrix pool
|
||||
try {
|
||||
await matrixPools.get(postId);
|
||||
// If this doesn't throw, it means we or someone else already sent this event
|
||||
console.log(`Matrix pool start event has already been sent for postId ${postId}`);
|
||||
} catch (e) {
|
||||
if (e.status === 404) {
|
||||
console.log('sending matrix pool start event');
|
||||
const { roomId, eventId } = sendMatrixEvent('io.dgov.pool.start', {
|
||||
postId,
|
||||
...params,
|
||||
});
|
||||
// Register our own stake and send a message
|
||||
const currentRep = await dao.balanceOf(await wallet.getAddress());
|
||||
const valid = await validateWorkEvidence(sender, post);
|
||||
const stake = { amount: currentRep, account: await wallet.getAddress(), inFavor: valid };
|
||||
sendMatrixEvent('io.dgov.pool.stake', { postId, amount: currentRep, inFavor: valid });
|
||||
await matrixPools.put(postId, {
|
||||
postId,
|
||||
roomId,
|
||||
eventId,
|
||||
...params,
|
||||
stakes: [stake],
|
||||
});
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Since we're assuming responsibility as the batch worker,
|
||||
// set a timeout to evaulate the outcome
|
||||
setTimeout(() => evaluateMatrixPoolOutcome(postId), duration * 1000);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registerMatrixEventHandler(async (client, roomId, event) => {
|
||||
switch (event.type) {
|
||||
case 'io.dgov.pool.start': {
|
||||
const { postId, sender, ...params } = event.content;
|
||||
// We can use LevelDB to store information about validation pools
|
||||
const eventId = event.event_id;
|
||||
console.log('Matrix pool started', { postId, ...params });
|
||||
// Validate the target post, and stake for/against
|
||||
let post;
|
||||
try {
|
||||
post = await read(postId);
|
||||
} catch (e) {
|
||||
console.error(`Post ID ${postId} not found`);
|
||||
break;
|
||||
}
|
||||
const currentRep = await dao.balanceOf(await wallet.getAddress());
|
||||
const valid = await validateWorkEvidence(sender, post);
|
||||
const stake = { amount: currentRep, account: await wallet.getAddress(), inFavor: valid };
|
||||
sendMatrixEvent('io.dgov.pool.stake', { postId, amount: currentRep, inFavor: valid });
|
||||
await matrixPools.put(postId, {
|
||||
roomId, eventId, ...params, stakes: [stake],
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'io.dgov.pool.stake': {
|
||||
const { postId, amount, inFavor } = event.content;
|
||||
let account;
|
||||
try {
|
||||
account = await matrixUserToAuthorAddress(event.sender);
|
||||
} catch (e) {
|
||||
// Error, sender has not registered their matrix identity
|
||||
sendMatrixText(`Matrix user ${event.sender} has not registered their wallet address`);
|
||||
return;
|
||||
}
|
||||
let matrixPool;
|
||||
try {
|
||||
matrixPool = await matrixPools.get(postId);
|
||||
} catch (e) {
|
||||
// Error. matrix pool not found
|
||||
sendMatrixText(`Received stake for unknown matrix pool, for post ${postId}`);
|
||||
return;
|
||||
}
|
||||
const stake = { account, amount, inFavor };
|
||||
matrixPool.stakes = matrixPool.stakes ?? [];
|
||||
matrixPool.stakes.push(stake);
|
||||
await matrixPools.put(postId, matrixPool);
|
||||
console.log(`registered stake in matrix pool for post ${postId} by ${account}`);
|
||||
break;
|
||||
}
|
||||
case 'io.dgov.pool.result': {
|
||||
// This should be sent by the current batch worker
|
||||
// TODO: Compare batch worker's result with ours to verify and provide early warning
|
||||
break;
|
||||
}
|
||||
case 'io.dgov.rollup.submit': {
|
||||
// This should include the identifier of the on-chain validation pool
|
||||
// TODO: Compare batch worker's result with ours to verify
|
||||
break;
|
||||
}
|
||||
default:
|
||||
}
|
||||
});
|
||||
|
||||
// Put in our availability stake to do the rollup work
|
||||
// Then check if there is a rollup worker.
|
||||
// It's possible we're the first or only worker;
|
||||
// if there is no worker assigned yet, we can submit the first one.
|
||||
// The procedure will be the same every time we are the rollup worker:
|
||||
// - Stake availability to be the next rollup worker;
|
||||
// - Create a rollup post -- compute authorship based on matrix pool results
|
||||
// - Send matrix event 'io.dgov.rollup.submit'? May not be necessary,
|
||||
// as there will be notification from the contract itself.
|
||||
// However, it would be nice to have in the tiemline of the room,
|
||||
// and it could be nice to give the participants a heads up to expect the batch.
|
||||
// We could even require it as part of batch validation.
|
||||
// - Call DAO.addPost(authors, postId)
|
||||
// - Call Rollup.submitBatch(postId, batchSize, poolDuration)
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
};
|
|
@ -50,13 +50,13 @@ const start = async () => {
|
|||
return;
|
||||
}
|
||||
// Stake all available reputation
|
||||
console.log(`STAKING ${inFavor ? 'in favor of' : 'against'} pool ${poolIndex}`);
|
||||
const currentRep = await dao.balanceOf(wallet.getAddress());
|
||||
const currentRep = await dao.balanceOf(await wallet.getAddress());
|
||||
console.log(`STAKING ${currentRep} ${inFavor ? 'in favor of' : 'against'} pool ${poolIndex}`);
|
||||
try {
|
||||
await dao.stakeOnValidationPool(poolIndex, currentRep, inFavor);
|
||||
} catch (e) {
|
||||
// Maybe the end time passed?
|
||||
console.error(`STAKING ${inFavor ? 'in favor of' : 'against'} pool ${poolIndex} failed, reason: ${e.reason}`);
|
||||
console.error(`STAKING failed, reason: ${e.reason}`);
|
||||
}
|
||||
});
|
||||
};
|
|
@ -2,7 +2,7 @@ require('dotenv').config();
|
|||
|
||||
const api = require('./api');
|
||||
const matrixBot = require('./matrix-bot');
|
||||
const topics = require('./topics');
|
||||
const eventHandlers = require('./event-handlers');
|
||||
|
||||
const {
|
||||
ENABLE_API,
|
||||
|
@ -17,4 +17,4 @@ if (ENABLE_MATRIX !== 'false') {
|
|||
matrixBot.start();
|
||||
}
|
||||
|
||||
topics.start();
|
||||
eventHandlers.start();
|
||||
|
|
|
@ -15,7 +15,7 @@ const {
|
|||
const storageProvider = new SimpleFsStorageProvider(BOT_STORAGE_PATH);
|
||||
const cryptoProvider = new RustSdkCryptoStorageProvider(BOT_CRYPTO_STORAGE_PATH);
|
||||
console.log('MATRIX_HOMESERVER_URL:', MATRIX_HOMESERVER_URL);
|
||||
const client = new MatrixClient(
|
||||
const matrixClient = new MatrixClient(
|
||||
MATRIX_HOMESERVER_URL,
|
||||
MATRIX_ACCESS_TOKEN,
|
||||
storageProvider,
|
||||
|
@ -23,38 +23,42 @@ const client = new MatrixClient(
|
|||
);
|
||||
let joinedRooms;
|
||||
|
||||
const { startOutboundQueue } = require('./outbound-queue');
|
||||
const { startOutboundQueue, sendMatrixEvent, sendMatrixText } = require('./outbound-queue');
|
||||
|
||||
const start = async () => {
|
||||
// Automatically join a room to which we are invited
|
||||
AutojoinRoomsMixin.setupOnClient(client);
|
||||
AutojoinRoomsMixin.setupOnClient(matrixClient);
|
||||
|
||||
joinedRooms = await client.getJoinedRooms();
|
||||
joinedRooms = await matrixClient.getJoinedRooms();
|
||||
console.log('joined rooms:', joinedRooms);
|
||||
|
||||
client.start().then(() => {
|
||||
console.log('Bot started!');
|
||||
matrixClient.start().then(() => {
|
||||
console.log('Matrix bot started!');
|
||||
// Start the outbound queue
|
||||
startOutboundQueue(client);
|
||||
startOutboundQueue(matrixClient);
|
||||
});
|
||||
};
|
||||
|
||||
const registerMessageHandler = (eventHandler) => {
|
||||
client.on('room.message', (roomId, event) => eventHandler(client, roomId, event));
|
||||
const registerMatrixMessageHandler = (eventHandler) => {
|
||||
matrixClient.on('room.message', async (roomId, event) => {
|
||||
if (event.sender === await matrixClient.getUserId()) return;
|
||||
eventHandler(matrixClient, roomId, event);
|
||||
});
|
||||
};
|
||||
|
||||
const registerEventHandler = (eventHandler) => {
|
||||
client.on('room.event', (roomId, event) => {
|
||||
const registerMatrixEventHandler = (eventHandler) => {
|
||||
matrixClient.on('room.event', async (roomId, event) => {
|
||||
if (event.sender === await matrixClient.getUserId()) return;
|
||||
if (event.state_key !== undefined) return; // state event
|
||||
eventHandler(client, roomId, event);
|
||||
eventHandler(matrixClient, roomId, event);
|
||||
});
|
||||
};
|
||||
|
||||
const getMatrixClient = () => client;
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
getMatrixClient,
|
||||
registerMessageHandler,
|
||||
registerEventHandler,
|
||||
matrixClient,
|
||||
registerMatrixMessageHandler,
|
||||
registerMatrixEventHandler,
|
||||
sendMatrixEvent,
|
||||
sendMatrixText,
|
||||
};
|
||||
|
|
|
@ -1,32 +1,30 @@
|
|||
const fastq = require('fastq');
|
||||
|
||||
const {
|
||||
proposalEventIds,
|
||||
} = require('../util/db');
|
||||
const { applicationData } = require('../util/db');
|
||||
|
||||
let client;
|
||||
let matrixClient;
|
||||
let targetRoomId;
|
||||
|
||||
const setTargetRoomId = (roomId) => {
|
||||
const setTargetRoomId = async (roomId) => {
|
||||
targetRoomId = roomId;
|
||||
console.log('target room ID:', targetRoomId);
|
||||
await applicationData.put('targetRoomId', targetRoomId);
|
||||
};
|
||||
|
||||
const processOutboundQueue = async ({ type, ...args }) => {
|
||||
console.log('processing outbound queue item');
|
||||
console.log('processing outbound queue item', { type, args });
|
||||
if (!targetRoomId) return;
|
||||
switch (type) {
|
||||
case 'NewProposal': {
|
||||
const { proposalIndex, text } = args;
|
||||
try {
|
||||
await proposalEventIds.get(Number(proposalIndex));
|
||||
// If this doesn't throw, it means we already sent a message for this proposal
|
||||
} catch (e) {
|
||||
if (e.status === 404) {
|
||||
console.log('sending to room', targetRoomId, { text });
|
||||
const eventId = await client.sendText(targetRoomId, text);
|
||||
await proposalEventIds.put(Number(proposalIndex), eventId);
|
||||
}
|
||||
}
|
||||
case 'MatrixEvent': {
|
||||
const { eventType, content, onSend } = args;
|
||||
const eventId = await matrixClient.sendEvent(targetRoomId, eventType, content);
|
||||
onSend(targetRoomId, eventId);
|
||||
break;
|
||||
}
|
||||
case 'MatrixText': {
|
||||
const { text, onSend } = args;
|
||||
const eventId = await matrixClient.sendText(targetRoomId, text);
|
||||
onSend(targetRoomId, eventId);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
@ -34,22 +32,45 @@ const processOutboundQueue = async ({ type, ...args }) => {
|
|||
};
|
||||
|
||||
const outboundQueue = fastq(processOutboundQueue, 1);
|
||||
// Pause until client is set
|
||||
// Pause until matrixClient is set
|
||||
outboundQueue.pause();
|
||||
|
||||
const startOutboundQueue = (c) => {
|
||||
client = c;
|
||||
// Resume now that client is set
|
||||
const startOutboundQueue = async (matrixClient_) => {
|
||||
matrixClient = matrixClient_;
|
||||
try {
|
||||
targetRoomId = await applicationData.get('targetRoomId');
|
||||
console.log('target room ID:', targetRoomId);
|
||||
} catch (e) {
|
||||
// No target room set
|
||||
console.warn('target room ID is not set -- will not be able to send messages until it is set. Use !target <bot-id>');
|
||||
}
|
||||
outboundQueue.resume();
|
||||
};
|
||||
|
||||
const sendNewProposalEvent = (proposalIndex, text) => {
|
||||
outboundQueue.push({ type: 'NewProposal', proposalIndex, text });
|
||||
};
|
||||
const sendMatrixEvent = async (type, content) => new Promise((resolve) => {
|
||||
outboundQueue.push({
|
||||
eventType: 'MatrixEvent',
|
||||
content,
|
||||
onSend: ((roomId, eventId) => {
|
||||
resolve({ roomId, eventId });
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
const sendMatrixText = async (text) => new Promise((resolve) => {
|
||||
outboundQueue.push({
|
||||
eventType: 'MatrixText',
|
||||
text,
|
||||
onSend: ((roomId, eventId) => {
|
||||
resolve({ roomId, eventId });
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
setTargetRoomId,
|
||||
outboundQueue,
|
||||
startOutboundQueue,
|
||||
sendNewProposalEvent,
|
||||
sendMatrixEvent,
|
||||
sendMatrixText,
|
||||
};
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
const { getContractAddressByNetworkName } = require('../util/contract-config');
|
||||
const { registerDecider } = require('./validation-pools');
|
||||
const { registerEventHandler } = require('../matrix-bot');
|
||||
const { matrixPools } = require('../util/db');
|
||||
|
||||
const {
|
||||
ETH_NETWORK,
|
||||
} = process.env;
|
||||
|
||||
const rollupAddress = getContractAddressByNetworkName(ETH_NETWORK, 'Rollup');
|
||||
|
||||
const start = async () => {
|
||||
console.log('registering validation pool decider for rollup');
|
||||
registerDecider((pool, post) => {
|
||||
// If this is not sent by the work1 contract, it's not of interest here.
|
||||
if (pool.sender !== rollupAddress) return false;
|
||||
|
||||
// A rollup post should contain
|
||||
// - a list of off-chain validation pools
|
||||
// - authorship corresponding to the result of those off-chain pools
|
||||
if (!post.embeddedData?.matrixPools) return false;
|
||||
|
||||
// TODO: Compute expected result by fetching off-chain pool matrix events
|
||||
// TODO: Or precompute? Since we want to be computing that anyway.
|
||||
return false;
|
||||
});
|
||||
|
||||
// We can use LevelDB to store information about validation pools
|
||||
|
||||
registerEventHandler(async (client, roomId, event) => {
|
||||
switch (event.type) {
|
||||
case 'io.dgov.pool.start': {
|
||||
// Use the event id as the validation pool id
|
||||
const poolId = event.event_id;
|
||||
// TODO: This event should include validation pool params
|
||||
console.log('Matrix pool started', { poolId });
|
||||
await matrixPools.put(poolId, {});
|
||||
break;
|
||||
}
|
||||
case 'io.dgov.pool.stake': {
|
||||
// TODO: Keep track of stakes
|
||||
break;
|
||||
}
|
||||
case 'io.dgov.pool.result': {
|
||||
// This should be sent by the current batch worker
|
||||
// TODO: Compare batch worker's result with ours to verify and provide early warning
|
||||
break;
|
||||
}
|
||||
case 'io.dgov.rollup.submit': {
|
||||
// TODO: Compare batch worker's result with ours to verify
|
||||
break;
|
||||
}
|
||||
default:
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
const { dao } = require('./contracts');
|
||||
|
||||
const addPostWithRetry = async (authors, hash, citations, retryDelay = 5000) => {
|
||||
try {
|
||||
await dao.addPost(authors, hash, citations);
|
||||
} catch (e) {
|
||||
if (e.code === 'REPLACEMENT_UNDERPRICED') {
|
||||
console.log('retry delay (sec):', retryDelay / 1000);
|
||||
await Promise.delay(retryDelay);
|
||||
return addPostWithRetry(authors, hash, citations, retryDelay * 2);
|
||||
} if (e.reason === 'A post with this postId already exists') {
|
||||
return { alreadyAdded: true };
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
return { alreadyAdded: false };
|
||||
};
|
||||
|
||||
module.exports = addPostWithRetry;
|
|
@ -3,6 +3,8 @@ const ethers = require('ethers');
|
|||
const { getContractAddressByNetworkName } = require('./contract-config');
|
||||
const DAOArtifact = require('../../contractArtifacts/DAO.json');
|
||||
const ProposalsArtifact = require('../../contractArtifacts/Proposals.json');
|
||||
const RollupArtifact = require('../../contractArtifacts/Rollup.json');
|
||||
const Work2Artifact = require('../../contractArtifacts/Work2.json');
|
||||
|
||||
const {
|
||||
ETH_NETWORK,
|
||||
|
@ -41,4 +43,14 @@ module.exports = {
|
|||
ProposalsArtifact.abi,
|
||||
wallet,
|
||||
),
|
||||
rollup: new ethers.Contract(
|
||||
getContractAddressByNetworkName(ETH_NETWORK, 'Rollup'),
|
||||
RollupArtifact.abi,
|
||||
wallet,
|
||||
),
|
||||
work2: new ethers.Contract(
|
||||
getContractAddressByNetworkName(ETH_NETWORK, 'Work2'),
|
||||
Work2Artifact.abi,
|
||||
wallet,
|
||||
),
|
||||
};
|
||||
|
|
|
@ -3,6 +3,7 @@ const { Level } = require('level');
|
|||
const dataDir = process.env.LEVEL_DATA_DIR || 'data';
|
||||
|
||||
module.exports = {
|
||||
applicationData: new Level(`${dataDir}/applicationData}`, { valueEncoding: 'json' }),
|
||||
forum: new Level(`${dataDir}/forum`, { valueEncoding: 'json' }),
|
||||
authorAddresses: new Level(`${dataDir}/authorAddresses`),
|
||||
authorPrivKeys: new Level(`${dataDir}/authorPrivKeys`),
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
{
|
||||
"localhost": {
|
||||
"DAO": "0x21E65E57a2F7DF8B85E0F59b57afc024595dD833",
|
||||
"Work1": "0x0686417a476C37701734dd405e381b7d9B247d22",
|
||||
"Onboarding": "0x495c5AF3fD0B1c2bA3e959f11A48d6FC8C246633",
|
||||
"Proposals": "0x43e45a19FAD2932D08c8D9B07f6830d1250DA71D",
|
||||
"Rollup": "0x5E614c4d8C956A937e4a6acC6a9459CAAE193feA",
|
||||
"Work2": "0xE3CC69EF45312959F9F752C971C35F553a165559",
|
||||
"Reputation": "0xb8C047c11eF88A02cd2C706120ef27D67586785a",
|
||||
"Forum": "0xdAA95487F0027C67473A4A560dD6628c49d218A9",
|
||||
"Bench": "0x05d4aE5d0097d47C9FcC0191d7f68F175a8122Db"
|
||||
"DAO": "0xc7E04c11eD94E375857b885b3e6E1Db30C061348",
|
||||
"Work1": "0x1bEffEB10E9f5714a8e385FfcA84046688677eA8",
|
||||
"Onboarding": "0xFC40076c675693441C6e553FEdDD3A3348db81E4",
|
||||
"Proposals": "0xa1349A27D43d0F71CeDD75904ACc8f8CF8F81582",
|
||||
"Rollup": "0x1361c87D5972a71cBCA34f6EAD928358deaC750D",
|
||||
"Work2": "0x691Bcb6a8378Cec103BE58Dfa037DC57E6FFf4d1",
|
||||
"Reputation": "0xfC979dbae6Cd0f35CC240889663B523B35c5F101",
|
||||
"Forum": "0xaf247e316A081871e713F492279D2360bd162401",
|
||||
"Bench": "0xC8BCE8171e626d07E5095256F703B1df23a67362"
|
||||
},
|
||||
"sepolia": {
|
||||
"DAO": "0x8e5bd58B2ca8910C5F9be8de847d6883B15c60d2",
|
||||
|
|
|
@ -8,17 +8,21 @@ contract Rollup is Availability {
|
|||
constructor(DAO dao) Availability(dao) {}
|
||||
|
||||
struct BatchItem {
|
||||
address author;
|
||||
address sender;
|
||||
address worker;
|
||||
uint stakeAmount;
|
||||
uint fee;
|
||||
string postId;
|
||||
}
|
||||
|
||||
mapping(uint => BatchItem) items;
|
||||
uint itemCount;
|
||||
address batchWorker;
|
||||
mapping(uint => BatchItem) public items;
|
||||
uint public itemCount;
|
||||
address public batchWorker;
|
||||
uint batchWorkerStakeIndex;
|
||||
|
||||
event BatchItemAdded(string postId, address sender, uint fee);
|
||||
event BatchWorkerAssigned(address batchWorker);
|
||||
|
||||
/// Instead of initiating a validation pool, call this method to include
|
||||
/// the stakes and fee in the next batch validation pool
|
||||
function addItem(
|
||||
|
@ -27,10 +31,12 @@ contract Rollup is Availability {
|
|||
string calldata postId
|
||||
) public payable {
|
||||
BatchItem storage item = items[itemCount++];
|
||||
item.author = author;
|
||||
item.sender = msg.sender;
|
||||
item.worker = author;
|
||||
item.stakeAmount = stakeAmount;
|
||||
item.fee = msg.value;
|
||||
item.postId = postId;
|
||||
emit BatchItemAdded(postId, item.sender, item.fee);
|
||||
}
|
||||
|
||||
/// To be called by the currently assigned batch worker,
|
||||
|
@ -67,7 +73,7 @@ contract Rollup is Availability {
|
|||
for (uint i = 0; i < itemCount; i++) {
|
||||
dao.delegatedStakeOnValidationPool(
|
||||
poolIndex,
|
||||
items[i].author,
|
||||
items[i].worker,
|
||||
items[i].stakeAmount,
|
||||
true
|
||||
);
|
||||
|
@ -95,5 +101,6 @@ contract Rollup is Availability {
|
|||
// Select the next worker
|
||||
batchWorkerStakeIndex = assignWork();
|
||||
batchWorker = stakes[batchWorkerStakeIndex].worker;
|
||||
emit BatchWorkerAssigned(batchWorker);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -90,6 +90,10 @@ contract DAO {
|
|||
return rep.balanceOf(account);
|
||||
}
|
||||
|
||||
function totalSupply() public view returns (uint256) {
|
||||
return rep.totalSupply();
|
||||
}
|
||||
|
||||
function allowance(
|
||||
address owner,
|
||||
address spender
|
||||
|
@ -110,7 +114,8 @@ contract DAO {
|
|||
address to,
|
||||
uint256 amount
|
||||
) public {
|
||||
return rep.forwardAllowance(owner, to, amount);
|
||||
rep.spendAllowance(owner, msg.sender, amount);
|
||||
rep.approve(owner, to, rep.allowance(owner, to) + amount);
|
||||
}
|
||||
|
||||
/// Authorize a contract to transfer REP, and call that contract's acceptAvailability method
|
||||
|
@ -119,15 +124,11 @@ contract DAO {
|
|||
uint256 value,
|
||||
uint duration
|
||||
) external returns (bool) {
|
||||
rep.approve(msg.sender, to, allowance(msg.sender, to) + value);
|
||||
rep.approve(msg.sender, to, rep.allowance(msg.sender, to) + value);
|
||||
IAcceptAvailability(to).acceptAvailability(msg.sender, value, duration);
|
||||
return true;
|
||||
}
|
||||
|
||||
function totalSupply() public view returns (uint256) {
|
||||
return rep.totalSupply();
|
||||
}
|
||||
|
||||
function propagateReputation(string memory postId, int amount) public {
|
||||
forum.propagateReputation(postId, amount, false, 0);
|
||||
}
|
||||
|
|
|
@ -50,19 +50,6 @@ contract Reputation is ERC20("Reputation", "REP") {
|
|||
revert("REP transfer is not allowed");
|
||||
}
|
||||
|
||||
function forwardAllowance(
|
||||
address owner,
|
||||
address to,
|
||||
uint256 amount
|
||||
) public {
|
||||
require(
|
||||
msg.sender == address(dao),
|
||||
"Only DAO contract may call spendAllowance"
|
||||
);
|
||||
_spendAllowance(owner, msg.sender, amount);
|
||||
_approve(owner, to, allowance(owner, to) + amount);
|
||||
}
|
||||
|
||||
function spendAllowance(
|
||||
address owner,
|
||||
address spender,
|
||||
|
@ -78,7 +65,7 @@ contract Reputation is ERC20("Reputation", "REP") {
|
|||
function approve(address owner, address spender, uint256 value) public {
|
||||
require(
|
||||
msg.sender == address(dao),
|
||||
"Only DAO contract may call update"
|
||||
"Only DAO contract may call approve"
|
||||
);
|
||||
_approve(owner, spender, value);
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
{
|
||||
"localhost": {
|
||||
"DAO": "0x21E65E57a2F7DF8B85E0F59b57afc024595dD833",
|
||||
"Work1": "0x0686417a476C37701734dd405e381b7d9B247d22",
|
||||
"Onboarding": "0x495c5AF3fD0B1c2bA3e959f11A48d6FC8C246633",
|
||||
"Proposals": "0x43e45a19FAD2932D08c8D9B07f6830d1250DA71D",
|
||||
"Rollup": "0x5E614c4d8C956A937e4a6acC6a9459CAAE193feA",
|
||||
"Work2": "0xE3CC69EF45312959F9F752C971C35F553a165559",
|
||||
"Reputation": "0xb8C047c11eF88A02cd2C706120ef27D67586785a",
|
||||
"Forum": "0xdAA95487F0027C67473A4A560dD6628c49d218A9",
|
||||
"Bench": "0x05d4aE5d0097d47C9FcC0191d7f68F175a8122Db"
|
||||
"DAO": "0xc7E04c11eD94E375857b885b3e6E1Db30C061348",
|
||||
"Work1": "0x1bEffEB10E9f5714a8e385FfcA84046688677eA8",
|
||||
"Onboarding": "0xFC40076c675693441C6e553FEdDD3A3348db81E4",
|
||||
"Proposals": "0xa1349A27D43d0F71CeDD75904ACc8f8CF8F81582",
|
||||
"Rollup": "0x1361c87D5972a71cBCA34f6EAD928358deaC750D",
|
||||
"Work2": "0x691Bcb6a8378Cec103BE58Dfa037DC57E6FFf4d1",
|
||||
"Reputation": "0xfC979dbae6Cd0f35CC240889663B523B35c5F101",
|
||||
"Forum": "0xaf247e316A081871e713F492279D2360bd162401",
|
||||
"Bench": "0xC8BCE8171e626d07E5095256F703B1df23a67362"
|
||||
},
|
||||
"sepolia": {
|
||||
"DAO": "0x8e5bd58B2ca8910C5F9be8de847d6883B15c60d2",
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -19,7 +19,13 @@ function AddPostModal({
|
|||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
// Upload content to API
|
||||
const post = new Post({ content, authors: [{ weightPPM: 1000000, authorAddress: account }] });
|
||||
const post = new Post({
|
||||
content,
|
||||
authors: [{ weightPPM: 1000000, authorAddress: account }],
|
||||
embeddedData: {
|
||||
nonce: crypto.randomUUID().replaceAll('-', ''),
|
||||
},
|
||||
});
|
||||
// Include metamask signature
|
||||
await post.sign(provider, account);
|
||||
// Clear the input and hide the modal
|
||||
|
|
|
@ -289,6 +289,17 @@ function MainTabs() {
|
|||
showRequestWork
|
||||
/>
|
||||
)}
|
||||
{work2 && (
|
||||
<WorkContract
|
||||
workContract={work2}
|
||||
showAvailabilityActions={false}
|
||||
showAvailabilityAmount={false}
|
||||
onlyShowAvailable
|
||||
title="Work Contract 2"
|
||||
verb="Work"
|
||||
showRequestWork
|
||||
/>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab eventKey="proposals" title="Proposals">
|
||||
<Proposals />
|
||||
|
|
|
@ -34,9 +34,14 @@ function ProposePriceChangeModal({
|
|||
const handleClose = () => setShow(false);
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
const post = new Post({ content, authors: [{ weightPPM: 1000000, authorAddress: account }] });
|
||||
// Include price as embedded data
|
||||
post.embeddedData = { proposedPrice };
|
||||
const post = new Post({
|
||||
content,
|
||||
authors: [{ weightPPM: 1000000, authorAddress: account }],
|
||||
embeddedData: {
|
||||
proposedPrice,
|
||||
nonce: crypto.randomUUID().replaceAll('-', ''),
|
||||
},
|
||||
});
|
||||
// Include metamask signature
|
||||
await post.sign(provider, account);
|
||||
// Clear the input and hide the modal
|
||||
|
|
Loading…
Reference in New Issue