on master help sign in
Merge branch 'master' into linear-release
commite0bf2e07cd423da3403336226b26b9f90d33987d authorLiam Fitzgerald <liam@urizen.co> dateTue 10 Dec 2019, 13:33 (7 years ago) parentc144079ae28797
20 files changed+418−49
README.md+1−1 package-lock.json+7−7 package.json+3−3 src/components/InlineEthereumTransaction.js+6−2 src/components/NavHeader.js+9 src/components/Passport.js+12−7 src/components/View.js+29−2 src/lib/constants.js+2−2 src/lib/inviteMail.js+8−2 src/lib/router.js+2 src/lib/useBlockWindowClose.js+17 src/lib/useMailer.js+15−12 src/views/Activate/PassportTransfer.js+3 src/views/Admin/AdminNetworkingKeys.js+4−4 src/views/Admin/Reticket/ReticketDownload.js+1−1 src/views/Admin/Reticket/ReticketExecute.js+3 src/views/Invite/InviteEmail.js+14 src/views/Point.js+22 src/views/Points.js+8−6 src/views/Senate.js+252
README.md+1−1
@@ -6,7 +6,7 @@ An application for interacting with Azimuth.
66
77## Usage
88
9−The latest version of Bridge is accessible online at [bridge.azimuth.network](https://bridge.azimuth.network). No setup needed.
9+**The latest version of Bridge is accessible online at [bridge.urbit.org](https://bridge.urbit.org). No setup needed.**
1010
1111If you would rather host Bridge on your own machine, read on.
1212
package-lock.json+7−7
@@ -1,6 +1,6 @@
11{
22 "name": "bridge",
3− "version": "1.4.1",
3+ "version": "2.1.0",
44 "lockfileVersion": 1,
55 "requires": true,
66 "dependencies": {
@@ -2591,9 +2591,9 @@
25912591 }
25922592 },
25932593 "azimuth-js": {
2594− "version": "0.16.0",
2595− "resolved": "https://registry.npmjs.org/azimuth-js/-/azimuth-js-0.16.0.tgz",
2596− "integrity": "sha512-dV3Z3X/G2zjnAHn8fiVvTdMLuIGN6asm74ZP2xzQgJ0VFPKPgyAefFUxR88UtacQEODTrqY9/sfk+QWu2yMSgw==",
2594+ "version": "0.17.0",
2595+ "resolved": "https://registry.npmjs.org/azimuth-js/-/azimuth-js-0.17.0.tgz",
2596+ "integrity": "sha512-g1/4gfhMHrj6NjCp96c5FBS202nIBnGiXcGTl9i0kh3rB6AE85rd0Un+z+qOhrVJbUdJF0axTvw3xsB5o5u+7Q==",
25972597 "requires": {
25982598 "azimuth-solidity": "1.2.1",
25992599 "ethereumjs-util": "^5.2.0",
@@ -17864,9 +17864,9 @@
1786417864 "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg="
1786517865 },
1786617866 "urbit-key-generation": {
17867− "version": "0.17.3",
17868− "resolved": "https://registry.npmjs.org/urbit-key-generation/-/urbit-key-generation-0.17.3.tgz",
17869− "integrity": "sha512-9mtWS10WS/OEMzaW9C0m0rDmtuCJ9BMHF4tSyJfjIDlf97pxTz6MNIMGlXP8CrRs3Z3rTVgb3eU9I69KcAc7cg==",
17867+ "version": "0.17.4",
17868+ "resolved": "https://registry.npmjs.org/urbit-key-generation/-/urbit-key-generation-0.17.4.tgz",
17869+ "integrity": "sha512-cheWFbCm9IhLmLLrR7uN/liySYwb/6VBuThDoDXHOb/hHt5yckri5MFq4RK+s0pe5RVEs2psbGKpUgyg3c2UWg==",
1787017870 "requires": {
1787117871 "argon2-wasm": "^0.9.0",
1787217872 "bip32": "^1.0.2",
package.json+3−3
@@ -1,6 +1,6 @@
11{
22 "name": "bridge",
3− "version": "1.4.1",
3+ "version": "2.1.0",
44 "description": "An application for interacting with Azimuth",
55 "repository": {
66 "type": "git",
@@ -19,7 +19,7 @@
1919 "@ledgerhq/hw-transport-u2f": "^4.61.0",
2020 "@welldone-software/why-did-you-render": "^3.2.3",
2121 "async-retry": "^1.2.3",
22− "azimuth-js": "^0.16.0",
22+ "azimuth-js": "^0.17.0",
2323 "azimuth-solidity": "^1.2.0",
2424 "bip32": "^1.0.2",
2525 "bip39": "^2.5.0",
@@ -49,7 +49,7 @@
4949 "react-teleporter": "^1.1.0",
5050 "secp256k1": "^3.5.2",
5151 "trezor-connect": "^7.0.1",
52− "urbit-key-generation": "^0.17.3",
52+ "urbit-key-generation": "^0.17.4",
5353 "urbit-ob": "^4.1.4",
5454 "urbit-paper-renderer": "^2.0.9",
5555 "urbit-sigil-js": "^1.3.5",
src/components/InlineEthereumTransaction.js+6−2
@@ -289,9 +289,13 @@ function HashReceiptList({ txHashes }) {
289289
290290 {txHashes &&
291291 txHashes.map((txHash, i) => (
292− <Flex.Item key={i} className="mb3" as={Flex}>
292+ <Flex.Item as={Flex}>
293293 <>
294− <Flex.Item flex as="code" className="f6 mono gray4 wrap mr3">
294+ <Flex.Item
295+ key={i}
296+ flex
297+ as="code"
298+ className="f6 mono gray4 wrap">
295299 {txHash}
296300 </Flex.Item>
297301 <Flex.Item as={LinkButton} href={txUrls[i]}>
src/components/NavHeader.jsnew file+9
@@ -0,0 +1,9 @@
1+import React from 'react';
2+import { createTeleporter } from 'react-teleporter';
3+
4+const NavHeaderTeleporter = createTeleporter();
5+
6+const NavHeader = NavHeaderTeleporter.Source;
7+NavHeader.Target = props => <NavHeaderTeleporter.Target as="div" {...props} />;
8+
9+export default NavHeader;
src/components/Passport.js+12−7
@@ -1,4 +1,4 @@
1−import React from 'react';
1+import React, { useMemo } from 'react';
22import { Just, Nothing } from 'folktale/maybe';
33import ob from 'urbit-ob';
44import BN from 'bn.js';
@@ -107,6 +107,10 @@ function Passport({
107107
108108 const fgColor = inverted ? 'black' : 'white';
109109
110+ const sigil = useMemo(
111+ () => Just.hasInstance(point) && makeSigil(64, patp, [bgColor, fgColor]),
112+ [patp, point, bgColor, fgColor]
113+ );
110114 return (
111115 <div
112116 style={{
@@ -132,7 +136,7 @@ function Passport({
132136 display: 'flex',
133137 alignItems: 'center',
134138 }}>
135− {Just.hasInstance(point) && makeSigil(64, patp, [bgColor, fgColor])}
139+ {sigil}
136140 <div
137141 style={{
138142 marginLeft: '16px',
@@ -239,6 +243,11 @@ function MiniPassport({ point, inverted, ...rest }) {
239243 const patp = ob.patp(point);
240244 const permissions = usePermissionsForPoint(address, point);
241245 const keyType = buildKeyType(permissions);
246+ const sigil = useMemo(
247+ () =>
248+ makeSigil(44, patp, inverted ? ['white', 'black'] : ['black', 'white']),
249+ [inverted, patp]
250+ );
242251 return (
243252 <div
244253 style={{
@@ -264,11 +273,7 @@ function MiniPassport({ point, inverted, ...rest }) {
264273 display: 'flex',
265274 alignItems: 'center',
266275 }}>
267− {makeSigil(
268− 44,
269− patp,
270− inverted ? ['white', 'black'] : ['black', 'white']
271− )}
276+ {sigil}
272277 <div
273278 style={{
274279 marginLeft: '16px',
src/components/View.js+29−2
@@ -6,6 +6,7 @@ import Footer from './Footer';
66import useBreakpoints from 'lib/useBreakpoints';
77import MiniBackButton from './MiniBackButton';
88import { useHistory } from 'store/history';
9+import NavHeader from './NavHeader';
910
1011const EXPECT_LOGOUT_WHEN_POPPING_AT_DEPTH = 2;
1112
@@ -29,9 +30,32 @@ function View({
2930
3031 const goBack = useCallback(() => pop(), [pop]);
3132
33+ const goLogout = useCallback(() => pop(size - 1), [pop, size]);
34+
3235 const showBackButton = size > 1 && !!pop;
3336 const backIsLogout = size === EXPECT_LOGOUT_WHEN_POPPING_AT_DEPTH;
3437
38+ const Header = useCallback(
39+ ({ logout }) => {
40+ return showBackButton ? (
41+ <Flex.Item
42+ as={Flex}
43+ className={cn(insetPadding, 'flex-row-r justify-between pb5')}>
44+ <Flex.Item
45+ onClick={goLogout}
46+ as="a"
47+ className="f5 gray4 underline mr2 pointer">
48+ Logout
49+ </Flex.Item>
50+ <Flex.Item as={NavHeader.Target} />
51+ </Flex.Item>
52+ ) : (
53+ <Flex.Item className={insetPadding} as={NavHeader.Target} />
54+ );
55+ },
56+ [insetPadding, showBackButton]
57+ );
58+
3559 return (
3660 <Flex
3761 row={!isMobile}
@@ -51,7 +75,7 @@ function View({
5175 col
5276 style={{ width: '48px' }}
5377 className={cn(insetPadding)}>
54− {showBackButton && (
78+ {showBackButton && !backIsLogout && (
5579 <MiniBackButton
5680 hpadding={!isMobile}
5781 vpadding={isMobile}
@@ -62,7 +86,10 @@ function View({
6286 </Flex.Item>
6387
6488 <Flex.Item flex={1} as={Flex} col justify="between">
65− <Flex.Item className={cn('pb5', insetPadding)}>{children}</Flex.Item>
89+ <Flex.Item className="pb5 ">
90+ <Header logout={goBack} />
91+ {children}
92+ </Flex.Item>
6693 <Flex.Item as={Footer.Target} />
6794 </Flex.Item>
6895
src/lib/constants.js+2−2
@@ -21,7 +21,7 @@ const SEED_ENTROPY_BITS = 128;
2121//TODO move into azimuth-js
2222const GAS_LIMITS = {
2323 SPAWN: 250000,
24− TRANSFER: 560000,
24+ TRANSFER: 560000, //NOTE biggest, also update gas tank's max
2525 CONFIGURE_KEYS: 100000,
2626 SET_PROXY: 140000,
2727 //
@@ -31,7 +31,7 @@ const GAS_LIMITS = {
3131 REJECT: 200000, //NOTE no samples
3232 DETACH: 200000, //NOTE no samples
3333 //
34− GIFT_PLANET: 420000, //NOTE low sample size
34+ GIFT_PLANET: 420000, //NOTE low sample size, //NOTE also update in gas tank
3535 //
3636 SEND_ETH: 21000,
3737 //
src/lib/inviteMail.js+8−2
@@ -30,8 +30,14 @@ async function hasReceived(recipient) {
3030 return res.hasReceived;
3131}
3232
33−function sendMail(recipient, ticket, sender, tx) {
34− return sendRequest('/send-ticket', { recipient, ticket, sender, tx });
33+function sendMail(recipient, ticket, sender, message, tx) {
34+ return sendRequest('/send-ticket', {
35+ recipient,
36+ ticket,
37+ sender,
38+ message,
39+ tx,
40+ });
3541}
3642
3743export { hasReceived, sendMail };
src/lib/router.js+2
@@ -1,6 +1,7 @@
11import Disclaimer from 'views/Disclaimer';
22import Login from 'views/Login.js';
33import Admin from 'views/Admin';
4+import Senate from 'views/Senate';
45import CreateGalaxy from 'views/CreateGalaxy';
56import IssueChild from 'views/IssueChild';
67import StarRelease from 'views/Release';
@@ -26,6 +27,7 @@ export const ROUTES = {
2627 [ROUTE_NAMES.ISSUE_CHILD]: IssueChild,
2728 [ROUTE_NAMES.INVITE]: Invite,
2829 [ROUTE_NAMES.ADMIN]: Admin,
30+ [ROUTE_NAMES.SENATE]: Senate,
2931 // TODO: nest this route under the Party.js router
3032 [ROUTE_NAMES.PARTY_SET_POOL_SIZE]: PartySetPoolSize,
3133 [ROUTE_NAMES.ACCEPT_TRANSFER]: AcceptTransfer,
src/lib/useBlockWindowClose.jsnew file+17
@@ -0,0 +1,17 @@
1+import { useEffect } from 'react';
2+
3+function onBeforeUnload(e) {
4+ e.preventDefault();
5+
6+ e.returnValue = '';
7+}
8+
9+export default function useBlockWindowClose() {
10+ return useEffect(() => {
11+ window.addEventListener('beforeunload', onBeforeUnload);
12+
13+ return function cleanup() {
14+ window.removeEventListener('beforeunload', onBeforeUnload);
15+ };
16+ });
17+}
src/lib/useMailer.js+15−12
@@ -26,18 +26,21 @@ export default function useMailer() {
2626
2727 // prefix to avoid clobbering sendMail import
2828 // also throws if return value is false
29− const _sendMail = useCallback(async (email, ticket, sender, rawTx) => {
30− if (STUB_MAILER) {
31− console.log(`${email} - ${ticket}`);
32− await timeout(Math.random() * 1000); // simulate request with randomness
33− return true;
34− }
35−
36− const success = await sendMail(email, ticket, sender, rawTx);
37− if (!success) {
38− throw new Error('Failed to send mail');
39− }
40− }, []);
29+ const _sendMail = useCallback(
30+ async (email, ticket, sender, message, rawTx) => {
31+ if (STUB_MAILER) {
32+ console.log(`${email} - ${ticket}`);
33+ await timeout(Math.random() * 1000); // simulate request with randomness
34+ return true;
35+ }
36+
37+ const success = await sendMail(email, ticket, sender, message, rawTx);
38+ if (!success) {
39+ throw new Error('Failed to send mail');
40+ }
41+ },
42+ []
43+ );
4144
4245 return { getHasReceived, sendMail: _sendMail };
4346}
src/views/Activate/PassportTransfer.js+3
@@ -8,6 +8,7 @@ import { usePointCursor } from 'store/pointCursor';
88
99import * as need from 'lib/need';
1010import useLifecycle from 'lib/useLifecycle';
11+import useBlockWindowClose from 'lib/useBlockWindowClose';
1112import {
1213 reticketPointBetweenWallets,
1314 TRANSACTION_PROGRESS,
@@ -60,6 +61,8 @@ export default function PassportTransfer({ className, resetActivateRouter }) {
6061 [replaceWith, names]
6162 );
6263
64+ useBlockWindowClose();
65+
6366 const goToRestart = useCallback(() => {
6467 // NOTE: because we're already on the ACTIVATE view in the history,
6568 // react (intelligently) doesn't trigger a re-render and that means the
src/views/Admin/AdminNetworkingKeys.js+4−4
@@ -97,7 +97,7 @@ function useSetKeys() {
9797 return networkSeed.value;
9898 }
9999
100− randomSeed.current = randomSeed.current || randomHex(64);
100+ randomSeed.current = randomSeed.current || randomHex(32); // 32 bytes
101101 setNdNetworkSeed(randomSeed.current);
102102
103103 return randomSeed.current;
@@ -178,7 +178,7 @@ export default function AdminNetworkingKeys() {
178178 composeValidator(
179179 {
180180 useNetworkSeed: buildCheckboxValidator(),
181− networkSeed: buildHexValidator(32),
181+ networkSeed: buildHexValidator(64), // 64 chars
182182 useDiscontinuity: buildCheckboxValidator(),
183183 },
184184 validateForm
@@ -355,7 +355,7 @@ export default function AdminNetworkingKeys() {
355355 full
356356 as={HexInput}
357357 name="networkSeed"
358− label="Network Seed (64 bytes)"
358+ label="Network Seed (32 bytes)"
359359 disabled={inputsLocked}
360360 />
361361 </Condition>
@@ -363,7 +363,7 @@ export default function AdminNetworkingKeys() {
363363 full
364364 as={CheckboxInput}
365365 name="useDiscontinuity"
366− label="Trigger New Continuity Era"
366+ label="Breach Continuity"
367367 disabled={inputsLocked}
368368 />
369369
src/views/Admin/Reticket/ReticketDownload.js+1−1
@@ -22,7 +22,7 @@ export default function ReticketDownload({ newWallet }) {
2222 return (
2323 <Grid className="mt4">
2424 <Grid.Item full as={Text}>
25− Download the thing, and keep it somewhere safe!
25+ Download the new passport, and keep it somewhere safe!
2626 </Grid.Item>
2727 <Grid.Item
2828 full
src/views/Admin/Reticket/ReticketExecute.js+3
@@ -10,6 +10,7 @@ import * as need from 'lib/need';
1010import useLifecycle from 'lib/useLifecycle';
1111import { WALLET_TYPES } from 'lib/wallet';
1212import convertToInt from 'lib/convertToInt';
13+import useBlockWindowClose from 'lib/useBlockWindowClose';
1314
1415import { useNetwork } from 'store/network';
1516import { useWallet } from 'store/wallet';
@@ -52,6 +53,8 @@ export default function ReticketExecute({ newWallet, setNewWallet }) {
5253 const [needFunds, setNeedFunds] = useState();
5354 const isDone = progress >= 1.0;
5455
56+ useBlockWindowClose();
57+
5558 // start reticketing transactions on mount
5659 useLifecycle(() => {
5760 (async () => {
src/views/Invite/InviteEmail.js+14
@@ -11,6 +11,7 @@ import * as azimuth from 'azimuth-js';
1111import {
1212 Grid,
1313 Flex,
14+ Input,
1415 IconButton,
1516 HelpText,
1617 Text,
@@ -127,6 +128,7 @@ export default function InviteEmail() {
127128 const [errors, addError] = useSetState();
128129
129130 // manage general state that affects the whole form
131+ const [message, setMessage] = useState('');
130132 const [status, setStatus] = useState(STATUS.INPUT);
131133 const [needFunds, setNeedFunds] = useState(null);
132134 const [generalError, setGeneralError] = useState(null);
@@ -347,6 +349,7 @@ export default function InviteEmail() {
347349 invite.email,
348350 invite.ticket,
349351 ob.patp(point),
352+ message,
350353 invite.rawTx
351354 );
352355 } catch (error) {
@@ -391,6 +394,7 @@ export default function InviteEmail() {
391394
392395 const onSubmit = useCallback(
393396 async values => {
397+ setMessage(values.message);
394398 cachedEmails.current = values.emails.reduce((memo, email, i) => {
395399 memo[nameForEmailField(i)] = email;
396400 return memo;
@@ -599,6 +603,16 @@ export default function InviteEmail() {
599603 );
600604 })}
601605
606+ <Grid.Item
607+ full
608+ as={Input}
609+ type="text"
610+ name="message"
611+ label="Message (optional)"
612+ placeholder="Welcome aboard!"
613+ value={message}
614+ />
615+
602616 {renderButton({ handleSubmit, fields })}
603617
604618 {needFunds && (
src/views/Point.js+22
@@ -35,6 +35,7 @@ export default function Point() {
3535 isActiveOwner,
3636 canManage,
3737 canSpawn,
38+ canVote,
3839 } = useCurrentPermissions();
3940
4041 // fetch the invites for the current cursor
@@ -42,6 +43,8 @@ export default function Point() {
4243
4344 const goAdmin = useCallback(() => push(names.ADMIN), [push, names]);
4445
46+ const goSenate = useCallback(() => push(names.SENATE), [push, names]);
47+
4548 const goInvite = useCallback(() => push(names.INVITE), [push, names]);
4649
4750 const goPartiesSetPoolSize = useCallback(
@@ -90,6 +93,24 @@ export default function Point() {
9093 }
9194 })();
9295
96+ const senateButton = (() => {
97+ if (azimuth.getPointSize(point) !== azimuth.PointSize.Galaxy) {
98+ return null;
99+ }
100+ return (
101+ <>
102+ <Grid.Item
103+ full
104+ as={ForwardButton}
105+ disabled={!canVote}
106+ onClick={goSenate}>
107+ Senate
108+ </Grid.Item>
109+ <Grid.Divider />
110+ </>
111+ );
112+ })();
113+
93114 // sync the current cursor
94115 useSyncOwnedPoints([point]);
95116
@@ -126,6 +147,7 @@ export default function Point() {
126147 <Grid.Divider />
127148 </>
128149 )}
150+ {senateButton}
129151 <Grid.Item full as={BootUrbitOSButton} />
130152 <Grid.Divider />
131153 <Grid.Item
src/views/Points.js+8−6
@@ -22,6 +22,7 @@ import Passport from 'components/Passport';
2222import Footer from 'components/Footer';
2323import { ForwardButton } from 'components/Buttons';
2424import CopiableAddress from 'components/CopiableAddress';
25+import NavHeader from 'components/NavHeader';
2526
2627const maybeGetResult = (obj, key, defaultValue) =>
2728 obj.matchWith({
@@ -247,13 +248,14 @@ export default function Points() {
247248
248249 return (
249250 <View pop={pop} inset>
251+ <NavHeader>
252+ <CopiableAddress
253+ text={address}
254+ className="f6 mono gray4 mb4 us-none pointer">
255+ {abbreviateAddress(address)}
256+ </CopiableAddress>
257+ </NavHeader>
250258 <Grid>
251− <Grid.Item full as={H1} className="f6 mono gray4 mb4 us-none pointer">
252− <CopiableAddress text={address}>
253− {abbreviateAddress(address)}
254− </CopiableAddress>
255− </Grid.Item>
256−
257259 {displayEmptyState && (
258260 <Grid.Item full as={HelpText} className="mt8 t-center">
259261 No points to display. This wallet is not the owner or proxy for any
src/views/Senate.jsnew file+252
@@ -0,0 +1,252 @@
1+import React, { useCallback, useMemo, useState } from 'react';
2+import cn from 'classnames';
3+import { Grid, Text, Input, P, LinkButton } from 'indigo-react';
4+import * as azimuth from 'azimuth-js';
5+import ob from 'urbit-ob';
6+
7+import { useNetwork } from 'store/network';
8+import { usePointCache } from 'store/pointCache';
9+import { usePointCursor } from 'store/pointCursor';
10+
11+import * as need from 'lib/need';
12+import useEthereumTransaction from 'lib/useEthereumTransaction';
13+import { GAS_LIMITS } from 'lib/constants';
14+import patp2dec from 'lib/patp2dec';
15+import { getSpawnCandidate } from 'lib/child';
16+import { useLocalRouter } from 'lib/LocalRouter';
17+import useConstant from 'lib/useConstant';
18+
19+import { keccak256 } from 'lib/wallet';
20+
21+import ViewHeader from 'components/ViewHeader';
22+import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
23+import View from 'components/View';
24+import { PointInput, AddressInput } from 'form/Inputs';
25+import {
26+ composeValidator,
27+ buildPointValidator,
28+ buildAddressValidator,
29+ hasErrors,
30+} from 'form/validators';
31+import BridgeForm from 'form/BridgeForm';
32+import FormError from 'form/FormError';
33+import CopiableAddress from 'components/CopiableAddress';
34+import convertToInt from 'lib/convertToInt';
35+
36+function useCastDocumentVote() {
37+ const { contracts } = useNetwork();
38+ const { pointCursor } = usePointCursor();
39+ const { syncDetails } = usePointCache();
40+
41+ const _contracts = need.contracts(contracts);
42+ const _point = need.point(pointCursor);
43+
44+ return useEthereumTransaction(
45+ useCallback(
46+ (hash, accept) => {
47+ return azimuth.ecliptic.castDocumentVote(
48+ _contracts,
49+ _point,
50+ hash,
51+ accept
52+ );
53+ },
54+ [_contracts, _point]
55+ ),
56+ useCallback(() => null, []), //TODO update polls[hash].hasVoted ?
57+ GAS_LIMITS.DEFAULT //TODO update once we have a decent sample size
58+ );
59+}
60+
61+export default function Senate() {
62+ const { pop } = useLocalRouter();
63+ const { contracts } = useNetwork();
64+ const { pointCursor } = usePointCursor();
65+
66+ const _contracts = need.contracts(contracts);
67+ const _point = convertToInt(need.point(pointCursor), 10);
68+
69+ const {
70+ construct,
71+ unconstruct,
72+ inputsLocked,
73+ completed,
74+ bind,
75+ } = useCastDocumentVote();
76+
77+ const [documentHash, setDocumentHash] = useState('0x...');
78+ const [majorities, setMajorities] = useState([]);
79+ const [proposals, setProposals] = useState([]);
80+ const [polls, setPolls] = useState({});
81+ const [loading, setLoading] = useState(true);
82+ const [votingOn, setVotingOn] = useState({});
83+
84+ //TODO update every second?
85+ const now = Math.round(new Date().getTime() / 1000);
86+
87+ useMemo(async () => {
88+ let proposals = await azimuth.polls.getDocumentProposals(_contracts);
89+ let majorities = await azimuth.polls.getDocumentMajorities(_contracts);
90+ let polls = {};
91+ for (let doc of proposals) {
92+ let poll = await azimuth.polls.getDocumentPoll(_contracts, doc);
93+ poll.endTime = polls.start + poll.duration;
94+ poll.hasVoted = await azimuth.polls.hasVotedOnDocumentPoll(
95+ _contracts,
96+ _point,
97+ doc
98+ );
99+ polls[doc] = poll;
100+ }
101+ setMajorities(majorities);
102+ setProposals(proposals);
103+ setPolls(polls);
104+ setLoading(false);
105+ return;
106+ }, [_contracts]);
107+
108+ const [open, closed] = useMemo(() => {
109+ let open = [];
110+ let closed = [];
111+ if (loading) return [open, closed];
112+ for (let doc of proposals) {
113+ if (now > polls[doc].endTime) {
114+ closed.push(doc);
115+ } else {
116+ open.push(doc);
117+ }
118+ }
119+ return [open, closed];
120+ }, [loading, proposals, polls, now]);
121+
122+ const doVote = useCallback(
123+ (hash, accept) => {
124+ construct(hash, accept);
125+ setVotingOn({ hash, accept });
126+ },
127+ [construct, setVotingOn]
128+ );
129+
130+ const didVote = useCallback(() => {
131+ polls[votingOn].hasVoted = true;
132+ setPolls(polls);
133+ setVotingOn({});
134+ }, [polls, setPolls, votingOn]);
135+
136+ const majorityList = useMemo(() => {
137+ return majorities.map(doc => {
138+ return (
139+ <Grid.Item full as={Text}>
140+ <a
141+ target="_blank"
142+ rel="noopener noreferrer"
143+ href={`https://github.com/urbit/azimuth/blob/master/proposals/${doc}.txt`}>
144+ <code>{doc}</code>↗
145+ </a>
146+ </Grid.Item>
147+ );
148+ });
149+ }, [majorities]);
150+
151+ const openList = useMemo(() => {
152+ return open.map(doc => {
153+ const hasVoted = polls[doc].hasVoted;
154+ return (
155+ <>
156+ <Grid.Item full as={Text}>
157+ <P>
158+ <code>{doc}</code>
159+ </P>
160+ <P>
161+ {hasVoted ? (
162+ 'Your vote has been cast.'
163+ ) : (
164+ <>
165+ <LinkButton onClick={() => doVote(doc, true)}>
166+ {votingOn.hash === doc && votingOn.accept ? (
167+ <b>support</b>
168+ ) : (
169+ <>support</>
170+ )}
171+ </LinkButton>
172+ {' / '}
173+ <LinkButton onClick={() => doVote(doc, false)}>
174+ {votingOn.hash === doc && !votingOn.accept ? (
175+ <b>reject</b>
176+ ) : (
177+ <>reject</>
178+ )}
179+ </LinkButton>
180+ </>
181+ )}
182+ </P>
183+ </Grid.Item>
184+ <Grid.Divider />
185+ </>
186+ );
187+ });
188+ }, [open, polls, votingOn]);
189+
190+ const onDocumentChange = useCallback(({ valid, values }) => {
191+ if (values.document) {
192+ const hash = '0x' + keccak256(values.document.trim()).toString('hex');
193+ setDocumentHash(hash);
194+ } else {
195+ setDocumentHash('0x...');
196+ }
197+ }, []);
198+
199+ return (
200+ <View pop={pop} inset>
201+ <Grid>
202+ <Grid.Item full as={ViewHeader}>
203+ Senate: Document Proposals
204+ </Grid.Item>
205+
206+ <Grid.Item full as={Grid}>
207+ <Grid.Item full as={P}>
208+ Accepted documents:
209+ </Grid.Item>
210+ {majorityList}
211+ </Grid.Item>
212+
213+ <Grid.Divider />
214+
215+ <Grid.Item full as={Grid}>
216+ <Grid.Item full as={P}>
217+ Open document polls:
218+ </Grid.Item>
219+ {openList}
220+ </Grid.Item>
221+
222+ <BridgeForm onValues={onDocumentChange}>
223+ {({ onSubmit, values }) => (
224+ // <>
225+ <Grid.Item
226+ full
227+ as={InlineEthereumTransaction}
228+ {...bind}
229+ onReturn={didVote}
230+ />
231+
232+ // <Grid.Divider />
233+ //
234+ // <Grid.Item
235+ // full
236+ // as={Input}
237+ // type="textarea"
238+ // placeholder="Input some text to find its hash..."
239+ // name="document"
240+ // className="mt4"
241+ // />
242+ //
243+ // <Grid.Item full as={CopiableAddress}>
244+ // {documentHash}
245+ // </Grid.Item>
246+ // </>
247+ )}
248+ </BridgeForm>
249+ </Grid>
250+ </View>
251+ );
252+}