Skip to content

Commit

Permalink
refactor: cleanup after moving code into vats
Browse files Browse the repository at this point in the history
  • Loading branch information
Chris-Hibbert committed Jun 11, 2021
1 parent d817510 commit e0f4f4a
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 55 deletions.
56 changes: 38 additions & 18 deletions packages/governance/src/binaryBallotCounter.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { makeStore } from '@agoric/store';
import { makePromiseKit } from '@agoric/promise-kit';
import { Far } from '@agoric/marshal';

import { E } from '@agoric/eventual-send';
import { ChoiceMethod, buildBallot } from './ballotBuilder';

const makeWeightedBallot = (ballot, weight) => ({ ballot, weight });
Expand All @@ -15,9 +16,10 @@ const makeBinaryBallot = (question, positionAName, positionBName) => {
assert.typeof(positionBName, 'string');
positions.push(positionAName, positionBName);

return buildBallot(ChoiceMethod.CHOOSE_N, question, positions);
return buildBallot(ChoiceMethod.CHOOSE_N, question, positions, 1);
};

// Exported for testing purposes
const makeBinaryBallotCounter = (question, aName, bName) => {
const template = makeBinaryBallot(question, aName, bName);

Expand All @@ -30,16 +32,19 @@ const makeBinaryBallotCounter = (question, aName, bName) => {
const tallyPromise = makePromiseKit();
const allBallots = makeStore('seat');

// TODO: quorum: by weight, by proportion
const quorum = true;
const getQuestionPositions = () => ({
question,
positionA: aName,
positionB: bName,
});

const recordBallot = (seat, filledBallot, weight = 1n) => {
allBallots.has(seat)
? allBallots.set(seat, makeWeightedBallot(filledBallot, weight))
: allBallots.init(seat, makeWeightedBallot(filledBallot, weight));
};

const countVotes = () => {
const countVotes = async quorumChecker => {
assert(!isOpen, X`can't count votes while the election is open`);

// ballot template has position choices; Each ballot in allBallots should
Expand All @@ -59,7 +64,17 @@ const makeBinaryBallotCounter = (question, aName, bName) => {
tally[choice] += weight;
}
});
if (!quorum) {

const stats = {
spoiled,
votes: allBallots.entries().length,
results: [
{ position: positionA, total: tally[positionA] },
{ position: positionB, total: tally[positionB] },
],
};

if (!(await E(quorumChecker).check(stats))) {
outcomePromise.reject('No quorum');
}

Expand All @@ -71,31 +86,36 @@ const makeBinaryBallotCounter = (question, aName, bName) => {
outcomePromise.resolve("It's a tie!");
}

const stats = {
spoiled,
votes: allBallots.entries().length,
results: [
{ position: positionA, total: tally[positionA] },
{ position: positionB, total: tally[positionB] },
],
};
tallyPromise.resolve(stats);
};

const adminFacet = Far('adminFacet', {
const sharedFacet = {
getBallotTemplate: () => template,
isOpen: () => isOpen,
getQuestionPositions,
};

const creatorFacet = Far('adminFacet', {
closeVoting: () => (isOpen = false),
countVotes,
submitVote: recordBallot,
...sharedFacet,
});

const publicFacet = Far('publicFacet', {
getBallotTemplate: () => template,
isOpen: () => isOpen,
getOutcome: () => outcomePromise.promise,
getStats: () => tallyPromise.promise,
...sharedFacet,
});
return { publicFacet, adminFacet };
return { publicFacet, creatorFacet };
};

const start = zcf => {
const { question, positions } = zcf.getTerms();
return makeBinaryBallotCounter(question, positions[0], positions[1]);
};

harden(start);
harden(makeBinaryBallotCounter);

export { makeBinaryBallotCounter };
export { makeBinaryBallotCounter, start };
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,26 @@ import '@agoric/zoe/exported';
import { E } from '@agoric/eventual-send';

import { makeHandle } from '@agoric/zoe/src/makeHandle';
import { makeBinaryBallotCounter } from '../src/binaryBallotCounter';
import { Far } from '@agoric/marshal';
import { makeBinaryBallotCounter } from '../../src/binaryBallotCounter';

const QUESTION = 'Fish or cut bait?';
const FISH = 'Fish';
const BAIT = 'Cut Bait';

const makeThreshold = seats => {
const check = stats => {
const votes = stats.results.reduce(
(runningTotal, { total }) => runningTotal + total,
0n,
);
return votes >= seats;
};
return Far('checker', { check });
};

test('binary ballot', async t => {
const { publicFacet, adminFacet } = makeBinaryBallotCounter(
const { publicFacet, creatorFacet } = makeBinaryBallotCounter(
QUESTION,
FISH,
BAIT,
Expand All @@ -23,15 +35,15 @@ test('binary ballot', async t => {
const alicePositions = aliceTemplate.getPositions();
t.deepEqual(alicePositions.length, 2);
t.deepEqual(alicePositions[0], FISH);
adminFacet.submitVote(aliceSeat, aliceTemplate.choose(alicePositions[0]));
adminFacet.closeVoting();
adminFacet.countVotes();
creatorFacet.submitVote(aliceSeat, aliceTemplate.choose(alicePositions[0]));
creatorFacet.closeVoting();
creatorFacet.countVotes(makeThreshold(1n));
const outcome = await E(publicFacet).getOutcome();
t.deepEqual(outcome, FISH);
});

test('binary spoiled', async t => {
const { publicFacet, adminFacet } = makeBinaryBallotCounter(
const { publicFacet, creatorFacet } = makeBinaryBallotCounter(
QUESTION,
FISH,
BAIT,
Expand All @@ -42,20 +54,20 @@ test('binary spoiled', async t => {
const alicePositions = aliceTemplate.getPositions();
t.deepEqual(alicePositions.length, 2);
t.deepEqual(alicePositions[0], FISH);
adminFacet.submitVote(aliceSeat, {
creatorFacet.submitVote(aliceSeat, {
question: QUESTION,
chosen: ['no'],
});
adminFacet.closeVoting();
adminFacet.countVotes();
creatorFacet.closeVoting();
creatorFacet.countVotes(makeThreshold(0n));
const outcome = await E(publicFacet).getOutcome();
t.deepEqual(outcome, "It's a tie!");
const tally = await E(publicFacet).getStats();
t.deepEqual(tally.spoiled, 1n);
});

test('binary tied', async t => {
const { publicFacet, adminFacet } = makeBinaryBallotCounter(
const { publicFacet, creatorFacet } = makeBinaryBallotCounter(
QUESTION,
FISH,
BAIT,
Expand All @@ -65,16 +77,16 @@ test('binary tied', async t => {
const bobSeat = makeHandle('Seat');

const positions = aliceTemplate.getPositions();
adminFacet.submitVote(aliceSeat, aliceTemplate.choose(positions[0]));
adminFacet.submitVote(bobSeat, aliceTemplate.choose(positions[1]));
adminFacet.closeVoting();
adminFacet.countVotes();
creatorFacet.submitVote(aliceSeat, aliceTemplate.choose(positions[0]));
creatorFacet.submitVote(bobSeat, aliceTemplate.choose(positions[1]));
creatorFacet.closeVoting();
creatorFacet.countVotes(makeThreshold(2n));
const outcome = await E(publicFacet).getOutcome();
t.deepEqual(outcome, "It's a tie!");
});

test('binary bad vote', async t => {
const { publicFacet, adminFacet } = makeBinaryBallotCounter(
const { publicFacet, creatorFacet } = makeBinaryBallotCounter(
QUESTION,
FISH,
BAIT,
Expand All @@ -83,28 +95,28 @@ test('binary bad vote', async t => {
const aliceSeat = makeHandle('Seat');

t.throws(
() => adminFacet.submitVote(aliceSeat, aliceTemplate.choose('worms')),
() => creatorFacet.submitVote(aliceSeat, aliceTemplate.choose('worms')),
{
message: 'Not a valid position: "worms"',
},
);
});

test('binary no votes', async t => {
const { publicFacet, adminFacet } = makeBinaryBallotCounter(
const { publicFacet, creatorFacet } = makeBinaryBallotCounter(
QUESTION,
FISH,
BAIT,
);

adminFacet.closeVoting();
adminFacet.countVotes();
creatorFacet.closeVoting();
creatorFacet.countVotes(makeThreshold(0n));
const outcome = await E(publicFacet).getOutcome();
t.deepEqual(outcome, "It's a tie!");
});

test('binary still open', async t => {
const { publicFacet, adminFacet } = makeBinaryBallotCounter(
const { publicFacet, creatorFacet } = makeBinaryBallotCounter(
QUESTION,
FISH,
BAIT,
Expand All @@ -115,14 +127,14 @@ test('binary still open', async t => {
const alicePositions = aliceTemplate.getPositions();
t.deepEqual(alicePositions.length, 2);
t.deepEqual(alicePositions[0], 'Fish');
adminFacet.submitVote(aliceSeat, aliceTemplate.choose(alicePositions[0]));
t.throws(() => adminFacet.countVotes(), {
creatorFacet.submitVote(aliceSeat, aliceTemplate.choose(alicePositions[0]));
await t.throwsAsync(() => creatorFacet.countVotes(makeThreshold(1n)), {
message: `can't count votes while the election is open`,
});
});

test('binary weights', async t => {
const { publicFacet, adminFacet } = makeBinaryBallotCounter(
const { publicFacet, creatorFacet } = makeBinaryBallotCounter(
QUESTION,
FISH,
BAIT,
Expand All @@ -133,19 +145,19 @@ test('binary weights', async t => {
const alicePositions = aliceTemplate.getPositions();
t.deepEqual(alicePositions.length, 2);
t.deepEqual(alicePositions[0], 'Fish');
adminFacet.submitVote(
creatorFacet.submitVote(
aliceSeat,
aliceTemplate.choose(alicePositions[0]),
37n,
);
adminFacet.closeVoting();
adminFacet.countVotes();
creatorFacet.closeVoting();
creatorFacet.countVotes(makeThreshold(1n));
const outcome = await E(publicFacet).getOutcome();
t.deepEqual(outcome, 'Fish');
});

test('binary contested', async t => {
const { publicFacet, adminFacet } = makeBinaryBallotCounter(
const { publicFacet, creatorFacet } = makeBinaryBallotCounter(
QUESTION,
FISH,
BAIT,
Expand All @@ -157,17 +169,17 @@ test('binary contested', async t => {
const positions = template.getPositions();
t.deepEqual(positions.length, 2);

adminFacet.submitVote(aliceSeat, template.choose(positions[0]), 23n);
adminFacet.submitVote(bobSeat, template.choose(positions[1]), 47n);
adminFacet.closeVoting();
adminFacet.countVotes();
creatorFacet.submitVote(aliceSeat, template.choose(positions[0]), 23n);
creatorFacet.submitVote(bobSeat, template.choose(positions[1]), 47n);
creatorFacet.closeVoting();
creatorFacet.countVotes(makeThreshold(3n));

const outcome = await E(publicFacet).getOutcome();
t.deepEqual(outcome, BAIT);
});

test('binary revote', async t => {
const { publicFacet, adminFacet } = makeBinaryBallotCounter(
const { publicFacet, creatorFacet } = makeBinaryBallotCounter(
QUESTION,
FISH,
BAIT,
Expand All @@ -179,12 +191,53 @@ test('binary revote', async t => {
const positions = template.getPositions();
t.deepEqual(positions.length, 2);

adminFacet.submitVote(aliceSeat, template.choose(positions[0]), 23n);
adminFacet.submitVote(bobSeat, template.choose(positions[1]), 47n);
adminFacet.submitVote(bobSeat, template.choose(positions[1]), 15n);
adminFacet.closeVoting();
adminFacet.countVotes();
creatorFacet.submitVote(aliceSeat, template.choose(positions[0]), 23n);
creatorFacet.submitVote(bobSeat, template.choose(positions[1]), 47n);
creatorFacet.submitVote(bobSeat, template.choose(positions[1]), 15n);
creatorFacet.closeVoting();
creatorFacet.countVotes(makeThreshold(5n));

const outcome = await E(publicFacet).getOutcome();
t.deepEqual(outcome, FISH);
});

test('binary ballot too many', async t => {
const { publicFacet, creatorFacet } = makeBinaryBallotCounter(
QUESTION,
FISH,
BAIT,
);
const aliceTemplate = publicFacet.getBallotTemplate();
const aliceSeat = makeHandle('Seat');

const alicePositions = aliceTemplate.getPositions();
t.throws(
() =>
creatorFacet.submitVote(
aliceSeat,
aliceTemplate.choose(...alicePositions),
),
{
message: 'only 1 position(s) allowed',
},
);
});

test('binary no quorum', async t => {
const { publicFacet, creatorFacet } = makeBinaryBallotCounter(
QUESTION,
FISH,
BAIT,
);
const aliceTemplate = publicFacet.getBallotTemplate();
const aliceSeat = makeHandle('Seat');

const positions = aliceTemplate.getPositions();
creatorFacet.submitVote(aliceSeat, aliceTemplate.choose(positions[0]));
creatorFacet.closeVoting();
creatorFacet.countVotes(makeThreshold(2n));
await E(publicFacet)
.getOutcome()
.then(o => t.fail(`expected to reject, not ${o}`))
.catch(e => t.deepEqual(e, 'No quorum'));
});

0 comments on commit e0f4f4a

Please sign in to comment.