The repository is home to the specification for the Kosu governance portal, primarily an interface for the ValidatorRegistry
contract (a modified token-curated registry).
The repository contains the master governance.sketch
file with the full UI/UX designs, and this README contains annotations of those designs and descriptions of each important state.
A custom helper library (gov-portal-helper
) has been created to assist with the creation of this portal. Documentation for this library is found in the documentation section.
Conceptually helpful topics and other background info required to work with this project. This document assumes familiarity with basic Ethereum concepts such as contracts, signatures, transactions, web3.js
, providers, and MetaMask.
This governance portal is primarily an interface for a token curated registry (TCR), a type of Ethereum contract system designed to generate an arbitrary list of data by rewarding token-holders for curating its contents.
Paradigm's TCR curates a set of validators who run the Kosu network. Any entity may apply to the registry (create a "proposal"), where after a confirmation period they are accepted into the registry as a validator, so long as they have not been challenged. Applicants submit "stake" with the proposals, an amount of tokens they include with their proposal to indicate their seriousness of intent to join. At any point (even after the confirmation period) their listing may be "challenged", when another entity puts up an equal amount of stake to trigger a vote.
The resulting vote on the challenge ends either with the challenge passing, in which case the listing owner is removed from the registry and has their tokens distributed to the challenger and winning voters, or the challenge fails, in which case the listing remains and the challengers tokens are distributed to the listing owner and the winning voters.
- Registry: the TCR, specifically the Kosu
ValidatorRegistry
contract and the set of listings currently in the registry - Listing: a non-specific term for any entity in the registry, either a validator (confirmed) or a proposal (pending, yet to be confirmed).
- Proposal: a new listing in the registry; a pending application to be a validator that is automatically confirmed after a period of time without being challenged.
- Validator: not only an actual validator on the proof-of-stake Kosu network, but more commonly in this context refers to a listing that has been confirmed to the registry.
- Challenge: an active poll against a listing (either a proposal or a challenge) initiated by a token holder. A challenge resolves after a commit-reveal vote period, and results in distribution of the loser's (either the challenger or the listing) stake to the winning voters and stakeholder (the listing in the case of a failed challenge, or the challenger in the case of a successful challenge).
- Challenger: an entity who has initiated a challenge against a proposal or a validator listing (note that a challenger who's challenge fails looses their staked tokens to the winning voters and the listing owner).
- Voter: an entity who is participating in a challenge vote by committing their own tokens to one side of the poll (note, a voter may never loose their tokens if they vote on the losing side, they can only benefit if they win).
- Stake: a term used to describe A) an amount of tokens that may be lost in certain outcomes, or B) the act of committing tokens into a staked position.
- ValidatorRegistry: the specific Ethereum contract that implements the functionality of this governance system within the greater Kosu protocol.
- Tendermint public key: (also just "public key") each Kosu validator has a validating key pair, and their public key is used to identify them within the contract system. Each primary state field (
gov.validators
, etc.) is a mapping of these public keys to objects with data about those listings/challenges.
The gov-portal-helper
library contains a single class, Gov
, which manages and abstracts most functionality needed to interact with the Kosu system.
The three main properties listed below (and in more detail in the documentation section) should be used for the main page (discussed below) that lists "proposals", "validators", and "challenges". These objects may be directly read from the Gov
prototype, loaded via prototype methods, or queried each time a gov_update
event is emitted from the gov.ee
event-emitter object.
A Gov
instance contains the following properties, which track the primary state of the portal system.
gov.validators
- a map of Tendermint public keys (as hex-encoded strings) toValidator
objects, with one entry for each current validator within the registry.gov.proposals
- a map of Tendermint public keys (as hex-encoded strings) toProposal
objects, with one entry for each current pending proposal within the registry.gov.challenges
- a map of Tendermint public keys (as hex-encoded strings) toStoreChallenge
objects, with one entry for listing that is currently being challenged.
All three properties above are loaded from the current contract system state when gov.init()
is called, and further updated each time the contract system's state changes. After main page load, the gov_update
event can be used to detect updates to the above state fields.
All sateful data (challenges, proposals, validators, past challenges, etc.) for the governance portal is loaded from the Ethereum blockchain, through the MetaMask extension which provides access to a remote Ethereum node.
As such, this portal is useless without MetaMask, and MetaMask must be connected (via a call to ethereum.enable()
) prior to any data being displayed. The actual call to ethereum.enable()
is handled by initialization of a Gov
instance (described below).
This brings up a few points to note:
- Not having MetaMask installed should be treated as an error state, with a special page prompting the user to install the extension, or otherwise use a web3 browser.
- On or just after page load, the
Gov
instance should be created. A call togov.init()
handles the MetaMask connection, and prompts the user to grant site access. Thegov.init()
call should not be made automatically, but instead should be triggered by the user clicking "connect to MetaMask" in the top nav bar. - Browser compatibility should also be handled. The only supported browsers are Chrome and Brave, unless a
window.ethereum
object is detected, in which case it can be assumed to be a compatible dApp browser.
More details on gov
initialization are included in the description and documentation sections.
Most token values (stake size, reward rate, etc.) are showed in "base units" of the token, which are also refereed to as wei
, the smallest unit of most ERC-20 tokens. Becuase wei amounts will have between ~17-23 digits, amounts should be converted to ether prior to being displayed to the user.
A gov
instance provides gov.weiToEther(wei)
to convert from wei, and gov.etherToWei(ether)
to convert the other direction.
These values are returned as strings, and should be converted to BigNumber
instances where necessary. The static method Gov.BigNumber
may be used to create new BigNumber
instances from strings.
Also note that "Ether" (proper noun) refers to the Ether cryptocurrency of the Ethereum network, while "ether" (lowercase) refers to the unit of 1*10^18 wei.
This section contains the "meat" of the specification, with diagrams from the sketch file and annotations.
- This is the home/index page of the governance portal, which should be displayed prior to MetaMask connection
- The "Connect to MetaMask" button in the top-right nav-bar should trigger a call to
gov.init()
which will prompt the user to allow the site access to their MetaMaskcoinbase
account. - Various exceptions (as promise rejections) from
gov.init()
indicate various failure states:// init life-cycle (example only) (async () => { const gov = new Gov(); try { await gov.init(); } catch (error) { switch (error.message) { case "user denied site access": /* user clicked "reject" on MM prompt */ break; case "non-ethereum browser detected": /* incompatible browser */ break; default: { /* normal failure case, something unexpected went wrong */ } } } })();
The screenshots in this section are cropped from the same overview state, found here.
- After a successful (no exceptions) call to
gov.init()
, data for the main page can be loaded.- This call should be triggered by the user clicking "connect to MetaMask".
- The users balance of KOSU tokens should be displayed in the top-right nav par, next to their address.
- The user's address is stored as
gov.coinbase
(afterinit
has completed). - The user's Kosu balance can be loaded (as a
BigNumber
, units of wei) with the following code:// the following requires `gov.init()` to have completed successfully const coinbase = gov.coinbase; const balance = await gov.kosu.kosuToken.balanceOf(coinbase);
- The user's address is stored as
- After initialization, the
gov
instance will begin to load validators, challenges, and proposals, into its state. - Each time a new proposal, challenge, or validator is loaded, the
gov_update
event will be emitted fromgov.ee
. - The raw map (object) for each of the following sub-sections can then be loaded from the
gov
instance.
- Active proposals should be loaded from
gov.proposals
(either by directly viewing that object, or a call togov.currentProposals()
).- Be sure to see the
Proposal
type definition as well, which defines each object in thegov.proposals
map. - Proposals on the main page display several values, which can be loaded (or computed) from each proposal object.
- The titular hex string for each proposal card should be the key for the proposal in the
gov.proposals
object. This value is the public key for the listing application, encoded as a hex string. - The "new proposal" or "ending soon" badges should be displayed based on the
proposal.acceptUnix
field:- If
acceptUnix
is within one day of the current timestamp, "ending soon" should be displayed on the listing. - If
acceptUnix
is more than one week in the future, "new proposal" should be displayed on the listing. - If
acceptUnix
is less than a week in the future, but more than a day, no badge should be displayed.
- If
- The title for a proposal is "
X
wants to become a validator," and theX
should be replaced with a shortened hex-string of theproposal.owner
Ethereum address (0x23ba...d2f
, for example).- Note that this hex string is 20 bytes (an Ethereum address, 42 chars with the "0x" prefix) and the listing key is a 32 byte (66 char with the "0x" prefix) Tendermint public key.
- That is to say
proposal.owner
strictly is NOT EQUAL to the key of the object within the mapping.
- The "Stake size" field should be loaded from
proposal.stakeSize
, and should be converted to units of ether prior to being displayed. - The "Daily reward" field should be loaded from
proposal.dailyReward
and should be converted to units of ether period to being displayed. Decimals can be truncated after 6-8 significant digits. - The "Estimated vote power" field should be loaded from
proposal.power
, which is a decimal number from 0-100 indicating a percentage of network vote power. The decimals can be truncated after a few significant digits, based on aesthetics/spacing. - The "Proposal ends in:" field must be computed based on the
proposal.acceptUnix
field, which indicates the Unix timestamp at which the proposal is confirmed. I recommend constructing aDate
object from the timestamp, and using its methods to compute days, hours, and minutes. Seconds should not be shown, as the timestamp is purely an estimate. - The "View" button should take the user to the detail page for the proposal (detailed in a later section).
- Be sure to see the
- Active challenges should be loaded from
gov.challenges
(either by directly viewing that object, or a call togov.currentChallenges()
).- Be sure to see the
StoreChallenge
type definition as well, which defines each object in thegov.challenges
map. - Challenges on the main page display a number, or index, which corresponds to the underlying
pollId
used to track that challenge in the contract system. - The number for each active challenge should be loaded from each
challenge.challengeId
and displayed (after conversion). - The "new challenge" or "ending soon" badge should be displayed on a challenge card, based on
challenge.challengeEndUnix
:- If
challengeEndUnix
is within a day of the current timestamp, "ending soon" should be displayed. - If
challengeEndUnix
is equal to or more than one week away, "new challenge" should be displayed. - If
challengeEndUnix
is in less than a week, but more than a day away, no badge should be displayed.
- If
- The "Challenger stake" field should be loaded from
challenge.challengerStake
and displayed after conversion to units of ether. - The "Potential reward" field must be computed based on the value of
challenge.challengerStake
:- The maximum possible reward refers to the proportion of listing/challenger stake that could be rewarded to participating voters.
- This value is based on a variable contract parameter, but for now, can be assumed to be 30% of the
challengerStake
. - Keep in mind
challenge.challengerStake
is an instance ofBigNumber
, so the appropriate methods must be used to calculate0.3 * challengerStake
.
- The "Challenge ends in:" field should be computed, much like the "Proposal ends in:" field above, from each
challenge.challengeEndUnix
value which is the estimated Unix timestamp (in seconds) that the challenge will end. - The displayed countdown should only be precise to the minute, as the provided timestamp is simply an estimation.
- The "View" button on each card should bring the user to the detail page for that challenge (discussed in a further section).
- Be sure to see the
- Validators should be loaded from
gov.validators
(either by directly viewing that object, or a call togov.currentValidators()
).- Be sure to see the
Validator
type definition as well, which defines each object in thegov.validators
map. - The validators table has the following column headers:
- Address is the Ethereum address of a given validator.
- Stake is the validator Kosu token stake size, displayed in ether units.
- Daily reward is the estimated number of tokens minted to the validator per day (in units of ether).
- Vote power is a percentage indicating a given validators influence and power during consensus.
- Uptime is a percentage representing how many blocks the validator has signed compared to how many they should have signed, if they were online all the time. This value for now should be populated as "N/A" for each listing. Eventually, this value will be loaded from a remote RPC API.
- Age is simply how long a given validator has been confirmed in the registry (displayed in human units of time, even though the value is represented as a block number).
- For each validator (each
validator
ingov.validators
), the fields described above should be loaded/computed as follows.- Address should be the Ethereum address loaded from
validator.owner
, and can be displayed as a "hyphenated" hex string. - Stake should be loaded from
validator.stakeSize
(which is aBigNumber
) and converted to units of ether prior to being displayed. - Daily reward should be loaded from
validator.dailyReward
, aBigNumber
instance, and converted to units of ether prior to being displayed. - Vote power should be loaded from
validator.power
which is aBigNumber
instance with a value between 0 and 100. The decimal value should be truncated so as to not display too many decimals. This truncation can be done based on space, or a fixed number of significant digits. Keep in mind, a validator may have less than 1% vote power. - Uptime is not a real value at this time, and should be displayed as "N/A" for each listing.
- Age should be computed based on
validator.confirmationUnix
which is the Unix timestamp in seconds that the validator was confirmed. The "Age" value should display the number of days, hours, and minutes since this time.
- Address should be the Ethereum address loaded from
- Clicking on a validator entry (one of the rows) should take the user to the validator detail page for that listing (described in a later section).
- Be sure to see the
- Past challenges is a section containing a table with information about past challenges and the results of each.
- Be sure to be familiar with the
PastChallenge
andListingSnapshot
types. - Past challenges are stored and loaded separately from the primary
gov
properites (proposals
,challenges
, andvalidators
) and are not updated in real time.- Instead, past challenges must be loaded by calling
gov.getHistoricalChallenges()
:// following requires `gov.init()` to have completed successfully const pastChallenges = await gov.getHistoricalChallenges(); // the number of challenges (use index +1 to display under "ID") console.log(pastChallenges.length);
- Instead, past challenges must be loaded by calling
- Be sure to be familiar with the
- The "Past Challenges" table has the following column headers:
- ID is the challenge's unique ID, which increments from 0.
- Challenger is the Ethereum address of the entity that initiated the challenge.
- Type indicates weather the challenge was against an active "validator" or against a pending "proposal".
- Result indicates the result of the challenge, where "accepted" indicates the challenge won, and "rejected" indicates the challenge failed/lost.
- Tokens at Stake is the total number of tokens at stake during the challenge, equal to the challenge stake plus the listing owner's stake (see below).
- Time indicates the time a given challenge ended (vote period ends).
- For each table entry (loaded from the array returned by
gov.getHistoricalChallenges()
) and each column field above:- ID can be loaded from
challenge.listingSnapshot.currentChallenge
or can be loaded from the challenge's index within the array returned bygetHistoricalChallenges
. - Challenger should be loaded from
challenge.challenger
(an Ethereum address). - Type is computed based on
challenge.listingSnapshot.status
based on the following:- If
listingSnapshot.status === 1
, "Type" should be "Proposal" (blue). - If
listingSnapshot.status === 2
, "Type" should be "Validator" (orange). - If
listingSnapshot.status
is not1
or2
, that entry should not be displayed.
- If
- Result should be based off the boolean
challenge.passed
field:- If
challenge.passed === true
then "Result" should be "Accepted" (green). - If
challenge.passed === false
then "Result" should be "Rejected" (red).
- If
- Tokens at Stake should be computed as the sum of
challenge.balance
andchallenge.listingSnapshot.stakedBalance
and displayed in units of ether.- Keep in mind both
challenge.balance
andchallenge.listingSnapshot.stakedBalance
areBigNumber
instances, and stored in units of wei which must be converted prior to displaying.
- Keep in mind both
- Time should be calculated based on the timestamp of the
challenge.challengeEnd
block number.- This timestamp can be loaded from the
gov.getPastBlockTimestamp(n)
method, wheren
is passed in aschallenge.challengeEnd
. - For example:
// gov.init() must have completed successfully prior to this working for (let i = 0; i < challenges.length; i++) { const challenge = challenges[i]; const challengeEndBlock = challenge.challengeEnd.toNumber(); // use this value to display "Time" const challengeEndTimestamp = await gov.getPastBlockTimestamp( challengeEndBlock ); }
- This timestamp can be loaded from the
- ID can be loaded from
- Clicking on a challenge should take to a past challenge detail page for that challenge (discussed in a later section).
- Clicking on a proposal from the main page directs the user to the proposal detail page (below).
- On the proposal detail page, the user can be prompted to challenge the proposal.
- In addition to the screenshots below, be sure to see the mobile version, and the full sketch file.
- This is the primary detail page for active proposals.
- The majority of the data for this page can be loaded from the corresponding
gov.proposals
object. - Each detail page should correspond to one of the objects in
gov.proposals
, where eachproposal
is key'd by the listing's Tendermint public key. - Page features/fields:
- Tendermint public key (mid/top left) should correspond to the object property key of the current
gov.proposals
object. X
wants to become a validator the value forX
(Ethereum address) should be loaded fromproposal.owner
.- If unchallenged... the countdown to when the proposal becomes a validator should be computed based on the
proposal.acceptUnix
and should be displayed as a countdown of days, hours, and minutes (seconds should be ignored due toacceptUnix
being an estimate). - Card: Stake size should be loaded from
proposal.stakeSize
(remember it is aBigNumber
, and must be converted to ether prior to display). - Card: Daily reward should be loaded from
proposal.dailyReward
(must be converted to ether prior to being displayed). - Card: Estimated vote power should be loaded from
proposal.power
and displayed as a percentage, keeping in mind some digits should not be displayed (i.e. trailing decimals). - Button: Challenge proposal triggers a change to the next state and can ultimately lead to a call to initiate a challenge (see next sub-section).
- Tendermint public key (mid/top left) should correspond to the object property key of the current
- This state offers the user another opportunity to confirm their intent to challenge a listing.
- It provides a text entry field for the user to provided additional information to support their challenge.
- The "this challenge will cost" field should be loaded from the value of
proposal.stakeSize
and displayed in units of ether. - The "Challenge" button should trigger the following (async code):
// inside async method, assume `proposal` is the proposal object from gov.proposals // assume `listingPubKey` is the key of that `proposal` object within the mapping const stringDetails = loadStringDetails() // pseudocode, load from input box // the current proposal's public key const listingKey = listingPubKey; const receipt = await gov.kosu.validatorRegistry.challengeListing( listingKey, stringDetails, ); // `receipt.transactionHash` is the txId of the challenge (can be used to view on Etherscan, etc).
- These states exist for active challenges (currently in
gov.challenges
). - The first state below ("main detail") shows information about the challenge, and allows the user to vote (see subsequent sub-sections).
- The "Vote on this challenge" section will vary based on what phase the challenge is in:
- Commit period starts immediately after the challenge is initiated, and it lasts until the reveal period starts.
- Reveal period starts after
commitPeriod
blocks from the challenge initialization, and lasts until thechallengeEnd
block. After that point, the challenge is finalized and should be accessed using thegetHistoricalChallenges
method.
- Be sure to view the full sketch file for un-included states that can be inferred from others.
- There are additional screenshots in
./images
for various assumed states, as well as mobile layout.
During the commit period, the first state below should be shown (under "main detail"), and during the reveal period, the "reveal period" state should be shown, where the two voting buttons are replaced with a single "reveal vote" button.
Example of detecting commit vs reveal period for challenge #5:
// gov.init() must have been called at this point.
const currentBlock = await gov.currentBlockNumber();
const challengeInfo = await gov.getChallengeInfo("5");
const {
challengeStart,
endCommitPeriod,
challengeEnd,
} = challengeInfo;
if (challengeStart <= currentBlock && currentBlock < endCommitPeriod) {
// challenge is _IN COMMIT PERIOD_
// votes _MAY BE COMMITTED_
} else if (endCommitPeriod <= currentBlock && currentBlock < challengeEnd) {
// challenge is _IN REVEAL PERIOD_
// votes _MAY NOT BE COMMITTED, ONLY REVEALED_
} else if (challengeEnd <= currentBlock) {
// reveal period is OVER, and challenge is finalized
}
This state shows a challenge during the commit phase.
- The detail page exists for each
challenge
in thegov.challenges
object. - This state should be shown when
challenge.challengeType === "validator"
. - Page fields/values:
- Challenge #
N
whereN
is thechallenge.challengeId
value (as a string). - Validator's public key is the object key (public key) in
gov.challenges
that corresponds to thischallenge
object. X
is challenging validatorY
whereX
ischallenge.challenger
andY
ischallenge.listingOwner
.- Note, this state shows a validator challenge. If the challenge is against a proposal (`challenge.challengeType === "proposal") a separate state should be shown.
- Entity y believes that... this string should be loaded and populated from
challenge.challengeDetails
. - This challenge will end in... this countdown should be computed based on
challenge.challengeEndUnix
and should update in real-time (seconds should now be shown). - Potential reward should be equal to 30% of
challenge.challengerStake
(display in ether). - Challenger stake should be loaded from
challenge.challengerStake
(and displayed in ether). - Validator voting power should only be displayed if
challenge.challengeType === "validator"
and can be computed as follows:// assume `listingPublicKey` is the challenge-ee public key (the key in `gov.challenges`) const powerBigNum = await gov._getValidatorPower(listingPublicKey); // BigNumber
- Challenge #
- The voting section should be displayed on both "validator" and "proposal" challenges.
- Depending on the challenge state the buttons should display either:
- Two buttons (as seen in image), one for each option if in the commit period.
- One "reveal" button (displayed in later section) if the challenge is in the reveal period.
- If
challenge.challengeType === "proposal"
this state should be used (minor changes from above). - The commit/reveal logic is the same for when to display voting buttons vs. when to display "reveal" button (see here).
- During the commit period, users may vote to remove or vote to keep (both challenges and proposals).
- Clicking either vote option prompts for another confirmation (displayed above).
- Subsequent confirmation should trigger the following logic:
// load from user input box, displayed above (keep in mind wei conversion), should be a `BigNumber` const tokensToCommit; // load from challenge page, should be `BigNumber` const challengeId; /** * Vote "value" is a string, either "1" or "0": * - use "1" if the vote is to support the challenge (remove a validator/proposal) * - use "0" if the vote is to support the validator/proposal (vote against the challenge) */ const value = "1" // will prompt user to sign transaction // will fail if user does not have enough tokens or allowance // this will store the vote salt and value in a cookie, to be used later for reveal const txId = await gov.commitVote(challengeId, value, tokensToCommit);
- This state should be shown after a successful commit vote transaction (above).
- For now, "account page" should be a null link (
#
). - The "Add a reminder" link should use the Google Calendar URL API to create an event (see example).
- An example of a GET request that prompts the user to create an event (copy/paste to browser to try):
https://www.google.com/calendar/render?action=TEMPLATE&text=Reveal+vote+on+challenge+%233&details=During+the+reveal+period%2C+you+may+reveal+your+vote+on+this+challenge.+If+you+do+not+reveal+a+vote%2C+you+will+not+be+rewarded+if+you+voted+on+the+winning+side.&location=gov.kosu.io&dates=20190613T162100Z%2F20190614T162100Z
- The event can be created for the duration of the "reveal period" which can be calculated as follows:
const challengeStart = challenge.challengeEnd - gov.params.challengePeriod; const startReveal = challengeStart + gov.params.commitPeriod; const endReveal = challenge.challengeEnd; // the calendar event should be created between these two unix timestamps const startRevealUnix = await gov.estimateFutureBlockTimestamp(startReveal) const endRevealUnix = await gov.estimateFutureBlockTimestamp(endReveal);
- This state should be shown when a challenge is in the "reveal" period.
- Weather a challenge is in reveal period or not can be determined as described here.
- If the user clicks "reveal", the following logic should be triggered:
const challengeId = new BigNumber(challenge.challengeId); // will trigger MetaMask popup, assuming a vote was previously committed from the same browser const revealTxId = await gov.revealVote(challengeId);
Be sure to catch promise rejections from gov.revealVote
in the case that the user did not commit a vote from the same browser.
- Each historical (past) challenge will have a detail page, hyperlinked from the table shown above.
- A historical challenge can be either against a "proposal" or against a "validator, and in both cases, the challenge can fail or pass.
- A historical (resolved) challenge against a validator.
- Display orange "validator" badge at top if
challenge.listingSnapshot.status === 2
(if1
, it is a past challenge against a proposal, see below). - The outcome of the challenge (badge underneath "Challenge #") should be:
- "Accepted" if
challenge.passed === true
, or - "Rejected" if
challenge.passed === false
.
- "Accepted" if
- Page fields/values:
- Challenge #
N
whereN
is the ID of the challenge (its position within the array, or taken fromlistingSnapshot
) - Validator
X
was ... challenged byY
where X is the Ethereum address of the validator, loaded frompastChallenge.listingSnapshot.owner
andY
is the challenger address, loaded frompastChallenge.challenger
. - Stake size should be loaded from
challenge.balance
and displayed in units of ether. - Daily reward should be computed based on
challenge.listingSnapshot.rewardRate
:// this value will be a `BigNumber` in units of wei (convert before displaying) const dailyReward = gov._estimateDailyReward(challenge.listingSnapshot.rewardRate);
- Results:
- Based on
challenge.passed
, we can assume which side has the majority of tokens. - The value for the majority (winning) outcome can be loaded from
challenge.winningTokens
. - The minority (losing) value can be computed as the difference between
challenge.voterTotal
andchallenge.winningTokens
.
- Based on
- Button: Go back should bring the user back to the main page.
- Challenge #
- The logic and values for this state are identical to the one above (past challenge against a validator), except that the badge underneath "Challenge #" shows a blue "proposal", rather than orange "validator".
- Below is the README for
@kosu/gov-portal-helper
package. - The published package may be found on the NPM registry.
- For safety from
number
precision issues, most numerical values are expected and returned asBigNumber
instances, so be sure to be familiar with that API.
Gov
is a helper library for interacting with the Kosu validator governance
system (primarily the Kosu ValidatorRegistry
contract).
It is designed with the browser in mind, and is intended to be used in front- end projects for simplifying interaction with the governance system.
Add gov-portal-helper
to your project via npm
or yarn
.
# install with yarn
yarn add @kosu/gov-portal-helper
# install with npm
yarn add @kosu/gov-portal-helper
- Validator
Represents an active validator in the registry.
- Proposal
Represents a pending listing application for a spot on the
ValidatorRegistry
.- StoreChallenge
Represents a current challenge (in the
gov
state).- PastChallenge
Represents a historical challenge, and its outcome.
- ListingSnapshot
Represents a listing at the time it was challenged.
- Vote
Represents a stored vote in a challenge poll.
Gov
is a helper library for interacting with the Kosu validator governance
system (primarily the Kosu ValidatorRegistry
contract).
It is designed with the browser in mind, and is intended to be used in front- end projects for simplifying interaction with the governance system.
Methods may be used to load the current proposals
, validators
, and
challenges
from the prototype's state, or the gov.ee
object (an EventEmitter)
may be used to detect updates to the state, emitted as gov_update
events.
After a gov_update
event, the read methods for proposals
, challenges
,
and validators
must be called to load the current listings. Alternatively,
access the objects directly with gov.listings
, etc.
Kind: global class
- Gov
- new Gov()
- instance
- .init()
- .currentProposals() ⇒
Map.<Proposal>
- .currentValidators() ⇒
Map.<Validator>
- .currentChallenges() ⇒
Map.<StoreChallenge>
- .weiToEther(wei) ⇒
string
- .etherToWei(ether) ⇒
string
- .commitVote(challengeId, value, amount) ⇒
Promise.<string>
- .revealVote(challengeId) ⇒
Promise.<string>
- .estimateFutureBlockTimestamp(blockNumber) ⇒
Promise.<number>
- .getPastBlockTimestamp(blockNumber) ⇒
Promise.<number>
- .getHistoricalChallenges() ⇒
Promise.<Array.<PastChallenge>>
- .getChallengeInfo(challengeId) ⇒
Promise.<ChallengeInfo>
- .currentBlockNumber() ⇒
number
- static
Create a new Gov
instance (gov
). Requires no arguments, but can be
set to "debug" mode by passing true
or 1
(or another truthy object to
the constructor).
Prior to using most gov
functionality, the async gov.init()
method
must be called, which will initialize the module and load state from
the Kosu contract system.
Main initialization function for the gov
module. You must call init
prior to interacting with most module functionality, and gov.init()
will
load the current registry status (validators, proposals, etc.) so it should
be called early-on in the page life-cycle.
Performs many functions, including:
- prompt user to connect MetaMask
- load user's address (the "coinbase")
- load the current Ethereum block height
- load and process the latest ValidatorRegistry state
Kind: instance method of Gov
gov.currentProposals() ⇒ Map.<Proposal>
Load the current proposal map from state.
Kind: instance method of Gov
Returns: Map.<Proposal>
-
a map where the key is the listing public key, and the value is a proposal object
Example
const proposals = gov.currentProposals();
gov.currentValidators() ⇒ Map.<Validator>
Load the current validators map from state.
Kind: instance method of Gov
Returns: Map.<Validator>
-
a map where the key is the listing public key, and the value is a validator object
Example
const validators = gov.currentValidators();
gov.currentChallenges() ⇒ Map.<StoreChallenge>
Load the current challenges map from state.
Kind: instance method of Gov
Returns: Map.<StoreChallenge>
-
a map where the key is the listing public key, and the value is a challenge object
Example
const challenges = gov.currentChallenges();
Convert a number of tokens, denominated in the smallest unit - "wei" - to "full" units, called "ether". One ether = 1*10^18 wei.
All contract calls require amounts in wei, but the user should be shown amounts in ether.
Kind: instance method of Gov
Returns: string
-
the same amount in ether, string used for precision
Param | Type | Description |
---|---|---|
wei | BigNumber | string |
the token amount in wei to convert |
Example
gov.weiToEther("100000000000000000000") // > "100"
gov.weiToEther(100000000000000000000) // > "100"
Convert a number of tokens (full units, called "ether") to "wei", the smallest denomination of most ERC-20 tokens with 18 decimals.
All contract calls require amounts in wei, but the user should be shown amounts in ether.
Kind: instance method of Gov
Returns: string
-
the same amount in wei, string used for precision
Param | Type | Description |
---|---|---|
ether | BigNumber | string |
the token amount to convert |
Example
gov.etherToWei(10) // > "10000000000000000000"
gov.etherToWei("1") // > "1000000000000000000"
Commit a vote in an active a challenge poll.
This method creates a vote (with value and salt), encodes it, and submits it as a transaction (requires MetaMask signature).
It stores the vote data in a browser cookie so it may be revealed later, which means voters must reveal a vote with the same browser they used to commit it.
Kind: instance method of Gov
Returns: Promise.<string>
-
the transaction hash of the commit tx
Param | Type | Description |
---|---|---|
challengeId | BigNumber |
the pollId of the challenge, as a string |
value | string |
the vote value, "1" to vote for the challenge, "0" to vote against |
amount | BigNumber |
the number of tokens (in wei) to commit in the vote |
Example
// we are looking at challenge #13, and want to vote AGAINST it with 10 tokens
const pollId = new BigNumber(13);
const amount = new BigNumber(gov.etherToWei("10"));
const value = "0";
// will prompt for MetaMask signature
const commitVoteTxId = await gov.commitVote(pollId, value, amount);
// ... some time passes, we now want to reveal ...
// load vote from cookie and reveal
const revealTxId = await gov.revealVote(new BigNumber("13"));
// ... wait for Tx's to confirm or whatever, etc.
Reveal a previously committed vote, by challengeId (as a BigNumber).
For this method to work, the user must have committed a vote during the commit period for the given challenge.
This method must also be called during the reveal period in order for the transaction not to fail.
Calling this method will trigger a MetaMask pop-up asking for the user's signature and approval.
Kind: instance method of Gov
Returns: Promise.<string>
-
the transaction hash of the reveal tx.
Param | Type | Description |
---|---|---|
challengeId | BigNumber |
the challenge to reveal a stored vote for |
Estimate the UNIX timestamp (in seconds) at which a given block
will be
mined.
Kind: instance method of Gov
Returns: Promise.<number>
-
the block's estimated UNIX timestamp (in seconds)
Param | Type | Description |
---|---|---|
blockNumber | number |
the block number to estimate the timestamp for |
Example
const block = 6102105;
const unixTs = gov.estimateFutureBlockTimestamp(block);
// use as a normal date object (multiply by 1000 to get ms)
const blockDate = new Date(ts * 1000);
Retrieve the Unix timestamp of a block that has already been mined. Should be used to display times of things that have happened (validator confirmed, etc.).
Kind: instance method of Gov
Returns: Promise.<number>
-
the Unix timestamp of the specified blockNumber
Param | Type | Description |
---|---|---|
blockNumber | number |
the block to get the unix timestamp for |
Example
await gov.getPastBlockTimestamp(515237) // > 1559346404
This method returns an array (described below) that contains information about all past challenges. Intended to be used for the "Past Challenges" section.
Kind: instance method of Gov
Returns: Promise.<Array.<PastChallenge>>
-
all historical challenges
.
Returns an object with the block numbers of important times for a given
challenge. Between challengeStart
and endCommitPeriod
, votes may be
committed (submitted) to the challenge.
Between endCommitPeriod
and challengeEnd
, votes may be revealed with
the same salt and vote value.
Kind: instance method of Gov
Returns: Promise.<ChallengeInfo>
-
the block numbers for this challenge
Param | Type | Description |
---|---|---|
challengeId | string | number | BigNumber |
the ID of the challenge to query |
Example
const info = await gov.getChallengeInfo(new BigNumber(1));
const currentBlock = await gov.currentBlockNumber();
if (currentBlock < endCommitPeriod && currentBlock >= challengeStart) {
// in "commit" period; voters may submit votes
} else if (currentBlock >= endCommitPeriod && currentBlock <= challengeEnd) {
// in "reveal" period; voters may reveal votes
} else {
// challenge has ended (or issues with block numbers)
}
Returns the current block height (as a number).
Kind: instance method of Gov
Returns: number
-
The current (or most recent) Ethereum block height.
The value 0
as an instance ofBigNumber
.
Kind: static property of Gov
The value 1
as an instance ofBigNumber
.
Kind: static property of Gov
The value 100
as an instance ofBigNumber
.
Kind: static property of Gov
Estimated blocks per day (mainnet only).
Kind: static property of Gov
Represents an active validator in the registry.
Kind: global typedef
Properties
Name | Type | Description |
---|---|---|
owner | string |
the Ethereum address of the validator |
stakeSize | BigNumber |
the staked balance (in wei) of the validator |
dailyReward | BigNumber |
the approximate daily reward to the validator (in wei) |
confirmationUnix | number |
the unix timestamp of the block the validator was confirmed in |
power | BigNumber |
the validators approximate current vote power on the Kosu network |
details | string |
arbitrary details provided by the validator when they applied |
Represents a pending listing application for a spot on the ValidatorRegistry
.
Kind: global typedef
Properties
Name | Type | Description |
---|---|---|
owner | string |
the Ethereum address of the applicant |
stakeSize | BigNumber |
the total stake the applicant is including with their proposal (in wei) |
dailyReward | BigNumber |
the approximate daily reward (in wei) the applicant is requesting |
power | BigNumber |
the estimated vote power the listing would receive if accepted right now |
details | string |
arbitrary details provided by the applicant with their proposal |
acceptUnix | number |
the approximate unix timestamp the listing will be accepted, if not challenged |
Represents a current challenge (in the gov
state).
Kind: global typedef
Properties
Name | Type | Description |
---|---|---|
listingOwner | string |
the Ethereum address of the owner of the challenged listing |
listingStake | BigNumber |
the total stake of the challenged listing |
listingPower | BigNumber |
the current vote power of the listing (if they are a validator) |
challenger | string |
the Ethereum address of the challenger |
challengeId | BigNumber |
the incremental ID of the current challenge |
challengerStake | BigNumber |
the staked balance of the challenger |
challengeEndUnix | number |
the estimated unix timestamp the challenge ends at |
challengeEnd | BigNumber |
the block at which the challenge reveal period ends |
totalTokens | BigNumber |
if finalized, the total number of tokens from participating voters |
winningTokens | BigNumber |
if finalized, the number of tokens that voted on the winning side |
result | string |
the final result of the challenge; "passed", "failed", or |
challengeType | string |
the type of listing the challenge is against, either a "validator" or a "proposal" |
listingDetails | string |
details provided by the listing holder |
challengeDetails | string |
details provided by the challenger |
Represents a historical challenge, and its outcome.
Kind: global typedef
Properties
Name | Type | Description |
---|---|---|
balance | BigNumber |
the number of tokens (in wei) staked in the challenge |
challengeEnd | BigNumber |
the block the challenge ends at |
challenger | string |
the Ethereum address of the challenger |
details | string |
additional details provided by the challenger |
finalized | boolean |
|
listingKey | string |
the key that corresponds to the challenged listing |
listingSnapshot | ListingSnapshot |
an object representing the state of the challenged listing at the time of challenge |
passed | boolean |
|
pollId | BigNumber |
the incremental ID used to identify the poll |
voterTotal | BigNumber |
the total number of tokens participating in the vote |
winningTokens | BigNumber |
the total number of tokens voting for the winning option |
Represents a listing at the time it was challenged.
Kind: global typedef
Properties
Name | Type | Description |
---|---|---|
applicationBlock | BigNumber |
the block the listing application was submitted |
confirmationBlock | BigNumber |
the block the listing was confirmed (0 if unconfirmed) |
currentChallenge | BigNumber |
the ID of the current challenge against the listing |
details | string |
arbitrary details provided by the listing applicant |
exitBlock | BigNumber |
the block (if any) the listing exited at |
lastRewardBlock | BigNumber |
the last block the listing owner claimed rewards for |
owner | string |
the Ethereum address of the listing owner |
rewardRate | BigNumber |
the number of tokens (in wei) rewarded to the listing per reward period |
stakedBalance | BigNumber |
the number of tokens staked by the listing owner (in wei) |
status | number |
the number representing the listing status (0: no listing, 1: proposal, 2: validator, 3: in-challenge, 4: exiting) |
tendermintPublicKey | string |
the 32 byte Tendermint public key of the listing holder |
Represents a stored vote in a challenge poll.
Kind: global typedef
Properties
Name | Type | Description |
---|---|---|
id | BigNumber |
the challengeId the vote is for |
value | string |
the vote value (should be "1" or "0" for challenge votes) |
salt | string |
a secret string used to hash the vote; must use same salt in commit as reveal |
encoded | string |
the encoded vote, as passed to the contract system |