on master help sign in
Merge branch 'master' into linear-release
e0bf2e0Liam Fitzgerald
README.md+1−1
@@ -6,7 +6,7 @@ An application for interacting with Azimuth.
6
6
7## Usage
7## Usage
8
8
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.**
10
10
11If you would rather host Bridge on your own machine, read on.
11If you would rather host Bridge on your own machine, read on.
12
12
package-lock.json+7−7
@@ -1,6 +1,6 @@
1{
1{
2 "name": "bridge",
2 "name": "bridge",
3− "version": "1.4.1",
3+ "version": "2.1.0",
4 "lockfileVersion": 1,
4 "lockfileVersion": 1,
5 "requires": true,
5 "requires": true,
6 "dependencies": {
6 "dependencies": {
@@ -2591,9 +2591,9 @@
2591 }
2591 }
2592 },
2592 },
2593 "azimuth-js": {
2593 "azimuth-js": {
2594− "version": "0.16.0",
2594+ "version": "0.17.0",
2595− "resolved": "https://registry.npmjs.org/azimuth-js/-/azimuth-js-0.16.0.tgz",
2595+ "resolved": "https://registry.npmjs.org/azimuth-js/-/azimuth-js-0.17.0.tgz",
2596− "integrity": "sha512-dV3Z3X/G2zjnAHn8fiVvTdMLuIGN6asm74ZP2xzQgJ0VFPKPgyAefFUxR88UtacQEODTrqY9/sfk+QWu2yMSgw==",
2596+ "integrity": "sha512-g1/4gfhMHrj6NjCp96c5FBS202nIBnGiXcGTl9i0kh3rB6AE85rd0Un+z+qOhrVJbUdJF0axTvw3xsB5o5u+7Q==",
2597 "requires": {
2597 "requires": {
2598 "azimuth-solidity": "1.2.1",
2598 "azimuth-solidity": "1.2.1",
2599 "ethereumjs-util": "^5.2.0",
2599 "ethereumjs-util": "^5.2.0",
@@ -17864,9 +17864,9 @@
17864 "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg="
17864 "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg="
17865 },
17865 },
17866 "urbit-key-generation": {
17866 "urbit-key-generation": {
17867− "version": "0.17.3",
17867+ "version": "0.17.4",
17868− "resolved": "https://registry.npmjs.org/urbit-key-generation/-/urbit-key-generation-0.17.3.tgz",
17868+ "resolved": "https://registry.npmjs.org/urbit-key-generation/-/urbit-key-generation-0.17.4.tgz",
17869− "integrity": "sha512-9mtWS10WS/OEMzaW9C0m0rDmtuCJ9BMHF4tSyJfjIDlf97pxTz6MNIMGlXP8CrRs3Z3rTVgb3eU9I69KcAc7cg==",
17869+ "integrity": "sha512-cheWFbCm9IhLmLLrR7uN/liySYwb/6VBuThDoDXHOb/hHt5yckri5MFq4RK+s0pe5RVEs2psbGKpUgyg3c2UWg==",
17870 "requires": {
17870 "requires": {
17871 "argon2-wasm": "^0.9.0",
17871 "argon2-wasm": "^0.9.0",
17872 "bip32": "^1.0.2",
17872 "bip32": "^1.0.2",
package.json+3−3
@@ -1,6 +1,6 @@
1{
1{
2 "name": "bridge",
2 "name": "bridge",
3− "version": "1.4.1",
3+ "version": "2.1.0",
4 "description": "An application for interacting with Azimuth",
4 "description": "An application for interacting with Azimuth",
5 "repository": {
5 "repository": {
6 "type": "git",
6 "type": "git",
@@ -19,7 +19,7 @@
19 "@ledgerhq/hw-transport-u2f": "^4.61.0",
19 "@ledgerhq/hw-transport-u2f": "^4.61.0",
20 "@welldone-software/why-did-you-render": "^3.2.3",
20 "@welldone-software/why-did-you-render": "^3.2.3",
21 "async-retry": "^1.2.3",
21 "async-retry": "^1.2.3",
22− "azimuth-js": "^0.16.0",
22+ "azimuth-js": "^0.17.0",
23 "azimuth-solidity": "^1.2.0",
23 "azimuth-solidity": "^1.2.0",
24 "bip32": "^1.0.2",
24 "bip32": "^1.0.2",
25 "bip39": "^2.5.0",
25 "bip39": "^2.5.0",
@@ -49,7 +49,7 @@
49 "react-teleporter": "^1.1.0",
49 "react-teleporter": "^1.1.0",
50 "secp256k1": "^3.5.2",
50 "secp256k1": "^3.5.2",
51 "trezor-connect": "^7.0.1",
51 "trezor-connect": "^7.0.1",
52− "urbit-key-generation": "^0.17.3",
52+ "urbit-key-generation": "^0.17.4",
53 "urbit-ob": "^4.1.4",
53 "urbit-ob": "^4.1.4",
54 "urbit-paper-renderer": "^2.0.9",
54 "urbit-paper-renderer": "^2.0.9",
55 "urbit-sigil-js": "^1.3.5",
55 "urbit-sigil-js": "^1.3.5",
src/components/InlineEthereumTransaction.js+6−2
@@ -289,9 +289,13 @@ function HashReceiptList({ txHashes }) {
289
289
290 {txHashes &&
290 {txHashes &&
291 txHashes.map((txHash, i) => (
291 txHashes.map((txHash, i) => (
292− <Flex.Item key={i} className="mb3" as={Flex}>
292+ <Flex.Item as={Flex}>
293 <>
293 <>
294+ <Flex.Item
295+ key={i}
296+ flex
297+ as="code"
294− <Flex.Item flex as="code" className="f6 mono gray4 wrap mr3">
298+ className="f6 mono gray4 wrap">
295 {txHash}
299 {txHash}
296 </Flex.Item>
300 </Flex.Item>
297 <Flex.Item as={LinkButton} href={txUrls[i]}>
301 <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';
2import { Just, Nothing } from 'folktale/maybe';
2import { Just, Nothing } from 'folktale/maybe';
3import ob from 'urbit-ob';
3import ob from 'urbit-ob';
4import BN from 'bn.js';
4import BN from 'bn.js';
@@ -107,6 +107,10 @@ function Passport({
107
107
108 const fgColor = inverted ? 'black' : 'white';
108 const fgColor = inverted ? 'black' : 'white';
109
109
110+ const sigil = useMemo(
111+ () => Just.hasInstance(point) && makeSigil(64, patp, [bgColor, fgColor]),
112+ [patp, point, bgColor, fgColor]
113+ );
110 return (
114 return (
111 <div
115 <div
112 style={{
116 style={{
@@ -132,7 +136,7 @@ function Passport({
132 display: 'flex',
136 display: 'flex',
133 alignItems: 'center',
137 alignItems: 'center',
134 }}>
138 }}>
135− {Just.hasInstance(point) && makeSigil(64, patp, [bgColor, fgColor])}
139+ {sigil}
136 <div
140 <div
137 style={{
141 style={{
138 marginLeft: '16px',
142 marginLeft: '16px',
@@ -239,6 +243,11 @@ function MiniPassport({ point, inverted, ...rest }) {
239 const patp = ob.patp(point);
243 const patp = ob.patp(point);
240 const permissions = usePermissionsForPoint(address, point);
244 const permissions = usePermissionsForPoint(address, point);
241 const keyType = buildKeyType(permissions);
245 const keyType = buildKeyType(permissions);
246+ const sigil = useMemo(
247+ () =>
248+ makeSigil(44, patp, inverted ? ['white', 'black'] : ['black', 'white']),
249+ [inverted, patp]
250+ );
242 return (
251 return (
243 <div
252 <div
244 style={{
253 style={{
@@ -264,11 +273,7 @@ function MiniPassport({ point, inverted, ...rest }) {
264 display: 'flex',
273 display: 'flex',
265 alignItems: 'center',
274 alignItems: 'center',
266 }}>
275 }}>
267− {makeSigil(
276+ {sigil}
268− 44,
269− patp,
270− inverted ? ['white', 'black'] : ['black', 'white']
271− )}
272 <div
277 <div
273 style={{
278 style={{
274 marginLeft: '16px',
279 marginLeft: '16px',
src/components/View.js+29−2
@@ -6,6 +6,7 @@ import Footer from './Footer';
6import useBreakpoints from 'lib/useBreakpoints';
6import useBreakpoints from 'lib/useBreakpoints';
7import MiniBackButton from './MiniBackButton';
7import MiniBackButton from './MiniBackButton';
8import { useHistory } from 'store/history';
8import { useHistory } from 'store/history';
9+import NavHeader from './NavHeader';
9
10
10const EXPECT_LOGOUT_WHEN_POPPING_AT_DEPTH = 2;
11const EXPECT_LOGOUT_WHEN_POPPING_AT_DEPTH = 2;
11
12
@@ -29,9 +30,32 @@ function View({
29
30
30 const goBack = useCallback(() => pop(), [pop]);
31 const goBack = useCallback(() => pop(), [pop]);
31
32
33+ const goLogout = useCallback(() => pop(size - 1), [pop, size]);
34+
32 const showBackButton = size > 1 && !!pop;
35 const showBackButton = size > 1 && !!pop;
33 const backIsLogout = size === EXPECT_LOGOUT_WHEN_POPPING_AT_DEPTH;
36 const backIsLogout = size === EXPECT_LOGOUT_WHEN_POPPING_AT_DEPTH;
34
37
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+
35 return (
59 return (
36 <Flex
60 <Flex
37 row={!isMobile}
61 row={!isMobile}
@@ -51,7 +75,7 @@ function View({
51 col
75 col
52 style={{ width: '48px' }}
76 style={{ width: '48px' }}
53 className={cn(insetPadding)}>
77 className={cn(insetPadding)}>
54− {showBackButton && (
78+ {showBackButton && !backIsLogout && (
55 <MiniBackButton
79 <MiniBackButton
56 hpadding={!isMobile}
80 hpadding={!isMobile}
57 vpadding={isMobile}
81 vpadding={isMobile}
@@ -62,7 +86,10 @@ function View({
62 </Flex.Item>
86 </Flex.Item>
63
87
64 <Flex.Item flex={1} as={Flex} col justify="between">
88 <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>
66 <Flex.Item as={Footer.Target} />
93 <Flex.Item as={Footer.Target} />
67 </Flex.Item>
94 </Flex.Item>
68
95
src/lib/constants.js+2−2
@@ -21,7 +21,7 @@ const SEED_ENTROPY_BITS = 128;
21//TODO move into azimuth-js
21//TODO move into azimuth-js
22const GAS_LIMITS = {
22const GAS_LIMITS = {
23 SPAWN: 250000,
23 SPAWN: 250000,
24− TRANSFER: 560000,
24+ TRANSFER: 560000, //NOTE biggest, also update gas tank's max
25 CONFIGURE_KEYS: 100000,
25 CONFIGURE_KEYS: 100000,
26 SET_PROXY: 140000,
26 SET_PROXY: 140000,
27 //
27 //
@@ -31,7 +31,7 @@ const GAS_LIMITS = {
31 REJECT: 200000, //NOTE no samples
31 REJECT: 200000, //NOTE no samples
32 DETACH: 200000, //NOTE no samples
32 DETACH: 200000, //NOTE no samples
33 //
33 //
34− GIFT_PLANET: 420000, //NOTE low sample size
34+ GIFT_PLANET: 420000, //NOTE low sample size, //NOTE also update in gas tank
35 //
35 //
36 SEND_ETH: 21000,
36 SEND_ETH: 21000,
37 //
37 //
src/lib/inviteMail.js+8−2
@@ -30,8 +30,14 @@ async function hasReceived(recipient) {
30 return res.hasReceived;
30 return res.hasReceived;
31}
31}
32
32
33−function sendMail(recipient, ticket, sender, tx) {
33+function sendMail(recipient, ticket, sender, message, tx) {
34− return sendRequest('/send-ticket', { recipient, ticket, sender, tx });
34+ return sendRequest('/send-ticket', {
35+ recipient,
36+ ticket,
37+ sender,
38+ message,
39+ tx,
40+ });
35}
41}
36
42
37export { hasReceived, sendMail };
43export { hasReceived, sendMail };
src/lib/router.js+2
@@ -1,6 +1,7 @@
1import Disclaimer from 'views/Disclaimer';
1import Disclaimer from 'views/Disclaimer';
2import Login from 'views/Login.js';
2import Login from 'views/Login.js';
3import Admin from 'views/Admin';
3import Admin from 'views/Admin';
4+import Senate from 'views/Senate';
4import CreateGalaxy from 'views/CreateGalaxy';
5import CreateGalaxy from 'views/CreateGalaxy';
5import IssueChild from 'views/IssueChild';
6import IssueChild from 'views/IssueChild';
6import StarRelease from 'views/Release';
7import StarRelease from 'views/Release';
@@ -26,6 +27,7 @@ export const ROUTES = {
26 [ROUTE_NAMES.ISSUE_CHILD]: IssueChild,
27 [ROUTE_NAMES.ISSUE_CHILD]: IssueChild,
27 [ROUTE_NAMES.INVITE]: Invite,
28 [ROUTE_NAMES.INVITE]: Invite,
28 [ROUTE_NAMES.ADMIN]: Admin,
29 [ROUTE_NAMES.ADMIN]: Admin,
30+ [ROUTE_NAMES.SENATE]: Senate,
29 // TODO: nest this route under the Party.js router
31 // TODO: nest this route under the Party.js router
30 [ROUTE_NAMES.PARTY_SET_POOL_SIZE]: PartySetPoolSize,
32 [ROUTE_NAMES.PARTY_SET_POOL_SIZE]: PartySetPoolSize,
31 [ROUTE_NAMES.ACCEPT_TRANSFER]: AcceptTransfer,
33 [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() {
26
26
27 // prefix to avoid clobbering sendMail import
27 // prefix to avoid clobbering sendMail import
28 // also throws if return value is false
28 // also throws if return value is false
29− const _sendMail = useCallback(async (email, ticket, sender, rawTx) => {
29+ const _sendMail = useCallback(
30+ async (email, ticket, sender, message, rawTx) => {
30− if (STUB_MAILER) {
31+ if (STUB_MAILER) {
31− console.log(`${email} - ${ticket}`);
32+ console.log(`${email} - ${ticket}`);
32− await timeout(Math.random() * 1000); // simulate request with randomness
33+ await timeout(Math.random() * 1000); // simulate request with randomness
33− return true;
34+ return true;
34− }
35+ }
35−
36+
36− const success = await sendMail(email, ticket, sender, rawTx);
37+ const success = await sendMail(email, ticket, sender, message, rawTx);
37− if (!success) {
38+ if (!success) {
38− throw new Error('Failed to send mail');
39+ throw new Error('Failed to send mail');
39− }
40+ }
40− }, []);
41+ },
42+ []
43+ );
41
44
42 return { getHasReceived, sendMail: _sendMail };
45 return { getHasReceived, sendMail: _sendMail };
43}
46}
src/views/Activate/PassportTransfer.js+3
@@ -8,6 +8,7 @@ import { usePointCursor } from 'store/pointCursor';
8
8
9import * as need from 'lib/need';
9import * as need from 'lib/need';
10import useLifecycle from 'lib/useLifecycle';
10import useLifecycle from 'lib/useLifecycle';
11+import useBlockWindowClose from 'lib/useBlockWindowClose';
11import {
12import {
12 reticketPointBetweenWallets,
13 reticketPointBetweenWallets,
13 TRANSACTION_PROGRESS,
14 TRANSACTION_PROGRESS,
@@ -60,6 +61,8 @@ export default function PassportTransfer({ className, resetActivateRouter }) {
60 [replaceWith, names]
61 [replaceWith, names]
61 );
62 );
62
63
64+ useBlockWindowClose();
65+
63 const goToRestart = useCallback(() => {
66 const goToRestart = useCallback(() => {
64 // NOTE: because we're already on the ACTIVATE view in the history,
67 // NOTE: because we're already on the ACTIVATE view in the history,
65 // react (intelligently) doesn't trigger a re-render and that means the
68 // 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() {
97 return networkSeed.value;
97 return networkSeed.value;
98 }
98 }
99
99
100− randomSeed.current = randomSeed.current || randomHex(64);
100+ randomSeed.current = randomSeed.current || randomHex(32); // 32 bytes
101 setNdNetworkSeed(randomSeed.current);
101 setNdNetworkSeed(randomSeed.current);
102
102
103 return randomSeed.current;
103 return randomSeed.current;
@@ -178,7 +178,7 @@ export default function AdminNetworkingKeys() {
178 composeValidator(
178 composeValidator(
179 {
179 {
180 useNetworkSeed: buildCheckboxValidator(),
180 useNetworkSeed: buildCheckboxValidator(),
181− networkSeed: buildHexValidator(32),
181+ networkSeed: buildHexValidator(64), // 64 chars
182 useDiscontinuity: buildCheckboxValidator(),
182 useDiscontinuity: buildCheckboxValidator(),
183 },
183 },
184 validateForm
184 validateForm
@@ -355,7 +355,7 @@ export default function AdminNetworkingKeys() {
355 full
355 full
356 as={HexInput}
356 as={HexInput}
357 name="networkSeed"
357 name="networkSeed"
358− label="Network Seed (64 bytes)"
358+ label="Network Seed (32 bytes)"
359 disabled={inputsLocked}
359 disabled={inputsLocked}
360 />
360 />
361 </Condition>
361 </Condition>
@@ -363,7 +363,7 @@ export default function AdminNetworkingKeys() {
363 full
363 full
364 as={CheckboxInput}
364 as={CheckboxInput}
365 name="useDiscontinuity"
365 name="useDiscontinuity"
366− label="Trigger New Continuity Era"
366+ label="Breach Continuity"
367 disabled={inputsLocked}
367 disabled={inputsLocked}
368 />
368 />
369
369
src/views/Admin/Reticket/ReticketDownload.js+1−1
@@ -22,7 +22,7 @@ export default function ReticketDownload({ newWallet }) {
22 return (
22 return (
23 <Grid className="mt4">
23 <Grid className="mt4">
24 <Grid.Item full as={Text}>
24 <Grid.Item full as={Text}>
25− Download the thing, and keep it somewhere safe!
25+ Download the new passport, and keep it somewhere safe!
26 </Grid.Item>
26 </Grid.Item>
27 <Grid.Item
27 <Grid.Item
28 full
28 full
src/views/Admin/Reticket/ReticketExecute.js+3
@@ -10,6 +10,7 @@ import * as need from 'lib/need';
10import useLifecycle from 'lib/useLifecycle';
10import useLifecycle from 'lib/useLifecycle';
11import { WALLET_TYPES } from 'lib/wallet';
11import { WALLET_TYPES } from 'lib/wallet';
12import convertToInt from 'lib/convertToInt';
12import convertToInt from 'lib/convertToInt';
13+import useBlockWindowClose from 'lib/useBlockWindowClose';
13
14
14import { useNetwork } from 'store/network';
15import { useNetwork } from 'store/network';
15import { useWallet } from 'store/wallet';
16import { useWallet } from 'store/wallet';
@@ -52,6 +53,8 @@ export default function ReticketExecute({ newWallet, setNewWallet }) {
52 const [needFunds, setNeedFunds] = useState();
53 const [needFunds, setNeedFunds] = useState();
53 const isDone = progress >= 1.0;
54 const isDone = progress >= 1.0;
54
55
56+ useBlockWindowClose();
57+
55 // start reticketing transactions on mount
58 // start reticketing transactions on mount
56 useLifecycle(() => {
59 useLifecycle(() => {
57 (async () => {
60 (async () => {
src/views/Invite/InviteEmail.js+14
@@ -11,6 +11,7 @@ import * as azimuth from 'azimuth-js';
11import {
11import {
12 Grid,
12 Grid,
13 Flex,
13 Flex,
14+ Input,
14 IconButton,
15 IconButton,
15 HelpText,
16 HelpText,
16 Text,
17 Text,
@@ -127,6 +128,7 @@ export default function InviteEmail() {
127 const [errors, addError] = useSetState();
128 const [errors, addError] = useSetState();
128
129
129 // manage general state that affects the whole form
130 // manage general state that affects the whole form
131+ const [message, setMessage] = useState('');
130 const [status, setStatus] = useState(STATUS.INPUT);
132 const [status, setStatus] = useState(STATUS.INPUT);
131 const [needFunds, setNeedFunds] = useState(null);
133 const [needFunds, setNeedFunds] = useState(null);
132 const [generalError, setGeneralError] = useState(null);
134 const [generalError, setGeneralError] = useState(null);
@@ -347,6 +349,7 @@ export default function InviteEmail() {
347 invite.email,
349 invite.email,
348 invite.ticket,
350 invite.ticket,
349 ob.patp(point),
351 ob.patp(point),
352+ message,
350 invite.rawTx
353 invite.rawTx
351 );
354 );
352 } catch (error) {
355 } catch (error) {
@@ -391,6 +394,7 @@ export default function InviteEmail() {
391
394
392 const onSubmit = useCallback(
395 const onSubmit = useCallback(
393 async values => {
396 async values => {
397+ setMessage(values.message);
394 cachedEmails.current = values.emails.reduce((memo, email, i) => {
398 cachedEmails.current = values.emails.reduce((memo, email, i) => {
395 memo[nameForEmailField(i)] = email;
399 memo[nameForEmailField(i)] = email;
396 return memo;
400 return memo;
@@ -599,6 +603,16 @@ export default function InviteEmail() {
599 );
603 );
600 })}
604 })}
601
605
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+
602 {renderButton({ handleSubmit, fields })}
616 {renderButton({ handleSubmit, fields })}
603
617
604 {needFunds && (
618 {needFunds && (
src/views/Point.js+22
@@ -35,6 +35,7 @@ export default function Point() {
35 isActiveOwner,
35 isActiveOwner,
36 canManage,
36 canManage,
37 canSpawn,
37 canSpawn,
38+ canVote,
38 } = useCurrentPermissions();
39 } = useCurrentPermissions();
39
40
40 // fetch the invites for the current cursor
41 // fetch the invites for the current cursor
@@ -42,6 +43,8 @@ export default function Point() {
42
43
43 const goAdmin = useCallback(() => push(names.ADMIN), [push, names]);
44 const goAdmin = useCallback(() => push(names.ADMIN), [push, names]);
44
45
46+ const goSenate = useCallback(() => push(names.SENATE), [push, names]);
47+
45 const goInvite = useCallback(() => push(names.INVITE), [push, names]);
48 const goInvite = useCallback(() => push(names.INVITE), [push, names]);
46
49
47 const goPartiesSetPoolSize = useCallback(
50 const goPartiesSetPoolSize = useCallback(
@@ -90,6 +93,24 @@ export default function Point() {
90 }
93 }
91 })();
94 })();
92
95
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+
93 // sync the current cursor
114 // sync the current cursor
94 useSyncOwnedPoints([point]);
115 useSyncOwnedPoints([point]);
95
116
@@ -126,6 +147,7 @@ export default function Point() {
126 <Grid.Divider />
147 <Grid.Divider />
127 </>
148 </>
128 )}
149 )}
150+ {senateButton}
129 <Grid.Item full as={BootUrbitOSButton} />
151 <Grid.Item full as={BootUrbitOSButton} />
130 <Grid.Divider />
152 <Grid.Divider />
131 <Grid.Item
153 <Grid.Item
src/views/Points.js+8−6
@@ -22,6 +22,7 @@ import Passport from 'components/Passport';
22import Footer from 'components/Footer';
22import Footer from 'components/Footer';
23import { ForwardButton } from 'components/Buttons';
23import { ForwardButton } from 'components/Buttons';
24import CopiableAddress from 'components/CopiableAddress';
24import CopiableAddress from 'components/CopiableAddress';
25+import NavHeader from 'components/NavHeader';
25
26
26const maybeGetResult = (obj, key, defaultValue) =>
27const maybeGetResult = (obj, key, defaultValue) =>
27 obj.matchWith({
28 obj.matchWith({
@@ -247,13 +248,14 @@ export default function Points() {
247
248
248 return (
249 return (
249 <View pop={pop} inset>
250 <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>
250 <Grid>
258 <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−
257 {displayEmptyState && (
259 {displayEmptyState && (
258 <Grid.Item full as={HelpText} className="mt8 t-center">
260 <Grid.Item full as={HelpText} className="mt8 t-center">
259 No points to display. This wallet is not the owner or proxy for any
261 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+}