Skip to content
Permalink
Browse files

Merge branch 'develop' into parameters-route

  • Loading branch information...
akuanti committed Jul 1, 2019
2 parents accc5fa + 9031bc6 commit 75ae9fb90e24a3d40fbd0bca02882ffa4ea1ae6d
@@ -19,6 +19,12 @@ async function getAllEvents() {
const gatekeeper = new Contract(gatekeeperAddress, Gatekeeper.abi, provider);
const tokenCapacitor = new Contract(tokenCapacitorAddress, TokenCapacitor.abi, provider);

// TEMPORARY WORKAROUND: prevent app from crashing on rinkeby network
const network = await provider.getNetwork();
if (network.chainId === 4) {
return [];
}

const currentBlockNumber = await provider.getBlockNumber();
console.log('currentBlockNumber:', currentBlockNumber);

@@ -1,6 +1,7 @@
import styled from 'styled-components';
import { COLORS } from '../styles';
import { colors } from '../styles';

export const Separator = styled.div`
border-top: 1px solid ${COLORS.grey5};
export const Separator = styled.div<{ width?: string }>`
border-top: 1px solid ${colors.greys.light};
${({ width }: any) => width && `border-top-width: ${width}px`};
`;
@@ -14,10 +14,10 @@ import { timestamp, tsToDeadline } from '../utils/datetime';
import { ISlate, IBallotDates } from '../interfaces';
import { colors } from '../styles';

const TokensBorder = styled.div`
export const TokensBorder = styled.div`
border: 2px solid ${colors.greys.light};
`;
const TokensSection = styled.div`
export const TokensSection = styled.div`
padding: 0 1.3rem 1rem;
color: ${colors.grey};
margin-top: 1em;
@@ -0,0 +1,54 @@
import * as React from 'react';
import isEmpty from 'lodash/isEmpty';
import Button from '../components/Button';
import Flex from '../components/system/Flex';
import { EthereumContext } from '../components/EthereumProvider';
import { BN, convertedToBaseUnits } from '../utils/format';
import { ipfsAddObject } from '../utils/ipfs';

const Donate: React.FC = () => {
const {
account,
contracts: { tokenCapacitor, token },
tcAllowance,
panBalance,
} = React.useContext(EthereumContext);
const [amount, setAmount] = React.useState(0);
const [donor, setDonor] = React.useState('');

async function handleDonate() {
const numTokens = convertedToBaseUnits(amount.toString());
if (BN(numTokens).eq(0) || panBalance.eq(0)) {
console.error('cannot donate zero tokens or panBalance is zero');
return;
}

if (tcAllowance.lt(numTokens) && panBalance.gte(numTokens)) {
await token.functions.approve(tokenCapacitor.address, numTokens);
}

if (!isEmpty(tokenCapacitor)) {
const metadata = {
sender: account,
donor: donor || account,
panAmount: numTokens,
usdAmount: '',
ethAmount: '',
memo: '',
};
const multihash = await ipfsAddObject(metadata);
const metadataHash = Buffer.from(multihash);
await tokenCapacitor.functions.donate(metadata.donor, metadata.panAmount, metadataHash);
}
}

return (
<Flex justifyCenter>
<Button type="default" onClick={handleDonate}>
Donate
</Button>
</Flex>
);
};

export default Donate;
@@ -1,6 +1,6 @@
import * as React from 'react';
import styled from 'styled-components';
import { COLORS } from '../../styles';
import { colors } from '../../styles';
import Button from '../../components/Button';
import { MainContext, IMainContext } from '../../components/MainProvider';
import RouterLink from '../../components/RouterLink';
@@ -11,37 +11,12 @@ import Tag from '../../components/Tag';
import Deadline from '../../components/Deadline';
import RouteTitle from '../../components/RouteTitle';
import Flex from '../../components/system/Flex';
import { DetailContainer, MetaColumn, MainColumn } from '../slates/slate';
import { TokensBorder, TokensSection } from '../../components/SlateSidebar';
import { Separator } from '../../components/Separator';

const Separator = styled.div`
border: 1px solid ${COLORS.grey5};
`;

const Container = styled.div`
display: flex;
border: 2px solid ${COLORS.grey5};
max-width: 1200px;
`;
const MetaColumn = styled.div`
width: 30%;
padding: 1.5rem 0;
border-right: 2px solid ${COLORS.grey5};
`;
const TokensBorder = styled.div`
margin: 0 1em;
border: 2px solid ${COLORS.grey5};
`;
const TokensSection = styled.div`
padding: 0 1.25rem 1rem;
color: ${COLORS.grey3};
margin-top: 1em;
`;
const DarkText = styled.div`
color: ${COLORS.grey1};
`;

const MainColumn = styled.div`
width: 70%;
padding: 1.2rem;
color: ${colors.black};
`;

interface IProposalSidebarProps {
@@ -93,13 +68,13 @@ export const ProposalHeader = ({
export const ProposalSidebar = ({ proposal, includedInSlates }: IProposalSidebarProps): any => {
const button = isPendingTokens('1') ? (
<RouterLink href="/ballots" as="/ballots">
<Button large type="default">
<Button large type="default" m="0.5rem 0 2rem">
{'View Ballot'}
</Button>
</RouterLink>
) : (
<RouterLink href={`/slates/create/grant?id=${proposal.id}`} as={`/slates/create/grant`}>
<Button large type="default">
<Button large type="default" m="0.5rem 0 2rem">
{'Add to a New Slate'}
</Button>
</RouterLink>
@@ -123,17 +98,17 @@ export const ProposalSidebar = ({ proposal, includedInSlates }: IProposalSidebar
{button}
<TokensBorder>
<TokensSection>
<SectionLabel lessMargin>{'TOKENS REQUESTED'}</SectionLabel>
<SectionLabel my="1rem">{'TOKENS REQUESTED'}</SectionLabel>
<DarkText>{proposal.tokensRequested}</DarkText>
</TokensSection>

<Separator />
<Separator width="2" />

<TokensSection>
<SectionLabel lessMargin>{'CREATED BY'}</SectionLabel>
<SectionLabel my="1rem">{'CREATED BY'}</SectionLabel>
<DarkText>{proposal.firstName + ' ' + proposal.lastName}</DarkText>

<SectionLabel lessMargin>{'INCLUDED IN SLATES'}</SectionLabel>
<SectionLabel my="1rem">{'INCLUDED IN SLATES'}</SectionLabel>
{slates}
</TokensSection>
</TokensBorder>
@@ -179,28 +154,38 @@ const Proposal: StatelessPage<IProps> = ({ query: { id } }) => {
</HeaderWrapper>
<RouteTitle>{proposal.title}</RouteTitle>

<Container>
<DetailContainer>
<MetaColumn>
<ProposalSidebar proposal={proposal} includedInSlates={includedInSlates} />
</MetaColumn>
<MainColumn>
<SectionLabel>PROJECT SUMMARY</SectionLabel>
<SectionLabel my="1rem">{'PROJECT SUMMARY'}</SectionLabel>
<DarkText>{proposal.summary}</DarkText>
<SectionLabel>{'PROJECT TIMELINE'}</SectionLabel>
<DarkText>{proposal.projectTimeline}</DarkText>
<SectionLabel>{'PROJECT TEAM'}</SectionLabel>
<DarkText>{proposal.teamBackgrounds}</DarkText>
{proposal.projectTimeline && (
<>
<SectionLabel mt={4} mb="1rem">
{'PROJECT TIMELINE'}
</SectionLabel>
<DarkText>{proposal.projectTimeline}</DarkText>
</>
)}
{proposal.teamBackgrounds && (
<>
<SectionLabel mt={4} mb="1rem">
{'PROJECT TEAM'}
</SectionLabel>
<DarkText>{proposal.teamBackgrounds}</DarkText>
</>
)}
</MainColumn>
</Container>
</DetailContainer>
</FlexColumn>
);
};

const FlexColumn = styled.div`
display: flex;
flex-direction: column;
font-family: 'Roboto';
align-items: center;
`;
const HeaderWrapper = styled.div`
display: flex;
@@ -1,6 +1,6 @@
import * as React from 'react';
import styled from 'styled-components';
import { COLORS } from '../../styles';
import { colors } from '../../styles';
import Box from '../../components/system/Box';
import Card from '../../components/Card';
import { MainContext, IMainContext } from '../../components/MainProvider';
@@ -14,23 +14,23 @@ import SlateSidebar from '../../components/SlateSidebar';
import { splitAddressHumanReadable } from '../../utils/format';

const Incumbent = styled.div`
color: ${COLORS.primary};
color: ${colors.blue};
font-weight: bold;
font-size: 0.8rem;
margin-top: 1rem;
`;

const Container = styled.div`
export const DetailContainer = styled.div`
display: flex;
border: 2px solid ${COLORS.grey5};
border: 2px solid ${colors.greys.light};
max-width: 1200px;
`;
const MetaColumn = styled.div`
export const MetaColumn = styled.div`
width: 30%;
padding: 1.75rem;
border-right: 2px solid ${COLORS.grey5};
border-right: 2px solid ${colors.greys.light};
`;
const MainColumn = styled.div`
export const MainColumn = styled.div`
width: 70%;
padding: 1.75rem 3rem;
`;
@@ -70,7 +70,7 @@ const Slate: StatelessPage<IProps> = ({ query: { id } }) => {
? slate.organization
: splitAddressHumanReadable(slate.recommender)}
</RouteTitle>
<Container>
<DetailContainer>
<MetaColumn>
<SlateSidebar
slate={slate}
@@ -109,7 +109,7 @@ const Slate: StatelessPage<IProps> = ({ query: { id } }) => {
</>
) : null}
</MainColumn>
</Container>
</DetailContainer>
</SlateWrapper>
);
};
@@ -48,6 +48,7 @@ app.prepare().then(() => {
server.get('/parameters', (req, res) => app.render(req, res, '/Parameters'));

server.get('/wallet', (req, res) => app.render(req, res, '/Wallet'));
server.get('/donate', (req, res) => app.render(req, res, '/Donate'));

// ----------------------------------------------
// SLATE STAKING
@@ -0,0 +1,106 @@
# Panvala Permissions API
Using the Permissions API, you can create contracts that have functions that can only be executed through permission granted by the gatekeeper (and therefore, the larger Panvala community).

## General flow
A user makes a proposal, and the contract requests permission from the gatekeeper and stores the `requestID` it receives. The user gets back a `proposalID` to use to execute the proposal later. If the request is approved through being included in an accepted slate, the proposal is accepted. The user can now pass the `proposalID` to a function on the contract to execute the proposal. In that execution function, the contract checks whether the `requestID` associated with its `proposalID` has permission, and then executes the rest of the function if it has.

## Usage
Define proposals for your contract. Users of the contract will need to create proposals, including a metadata hash (e.g. IPFS CID) describing the nature of the proposal.

You can define your data structures any way you want, but it should contain the address of the current gatekeeper, as well as the `requestID` it received from requesting permission, the metadata hash submitted with the proposal, any data required to execute the proposal later, and some way to prevent the proposal from being executed twice.


Below are some examples from `ParameterStore.sol`
```
struct Proposal {
// request identification
address gatekeeper;
uint256 requestID;
// proposal data
bytes metadataHash;
string key;
bytes32 value;
// some way to prevent proposals from being executed twice
bool executed;
}
```

Provide a function for submitting proposals. It should emit an event `ProposalCreated` and return `proposalID`.
```
event ProposalCreated(
uint256 proposalID,
address indexed proposer,
uint256 requestID,
// proposal data
string key,
bytes32 value,
bytes metadataHash
);
```

```
function createProposal(string memory key, bytes32 value, bytes memory metadataHash) public returns(uint256) {
require(metadataHash.length > 0, "metadataHash cannot be empty");
Gatekeeper gatekeeper = _gatekeeper();
Proposal memory p = Proposal({
gatekeeper: address(gatekeeper),
requestID: 0,
key: key,
value: value,
metadataHash: metadataHash,
executed: false
});
// Request permission from the Gatekeeper and store the proposal data for later.
// If the request is approved, a user can execute the proposal by providing the
// proposalID.
uint requestID = gatekeeper.requestPermission(metadataHash);
p.requestID = requestID;
uint proposalID = proposalCount;
proposals[proposalID] = p;
proposalCount = proposalCount.add(1);
emit ProposalCreated(proposalID, msg.sender, requestID, key, value, metadataHash);
return proposalID;
}
```

It's helpful to have a function to create many proposals at once.
```
function createManyProposals(
string[] memory keys,
bytes32[] memory values,
bytes[] memory metadataHashes
) public {
require(keys.length == values.length, "All inputs must have the same length");
require(values.length == metadataHashes.length, "All inputs must have the same length");
for (uint i = 0; i < keys.length; i++) {
string memory key = keys[i];
bytes32 value = values[i];
bytes memory metadataHash = metadataHashes[i];
createProposal(key, value, metadataHash);
}
}
```

Finally, execute the proposal. The execution function should emit an event including the `proposalID` and some data for context.
```
function setValue(uint256 proposalID) public returns(bool) {
require(proposalID < proposalCount, "Invalid proposalID");
Proposal memory p = proposals[proposalID];
Gatekeeper gatekeeper = Gatekeeper(p.gatekeeper);
require(gatekeeper.hasPermission(p.requestID), "Proposal has not been approved");
require(p.executed == false, "Proposal already executed");
proposals[proposalID].executed = true;
set(p.key, p.value);
emit ParameterSet(proposalID, p.key, p.value);
return true;
}
```

0 comments on commit 75ae9fb

Please sign in to comment.
You can’t perform that action at this time.