on master help sign in
Merge branch 'master' into mino-cohort-screens
commit9f467d570e7d13423ed783e59c78ca14c6f233e7 authorLiam Fitzgerald <liam@urizen.co> dateFri 3 Jan 2020, 09:59 (7 years ago) parent59f91dd8cbf321
32 files changed+1100−113
README.md+1−1 package-lock.json+71−37 package.json+5−5 src/components/Passport.js+1−14 src/form/validators.js+13−3 src/indigo-react/indigo.css+13−1 src/lib/contracts.js+6 src/lib/inviteMail.js+1−1 src/lib/point.js+13 src/lib/routeNames.js+1 src/lib/router.js+4 src/lib/starRelease.js+101 src/lib/tank.js+1−1 src/lib/useBlockWindowClose.js+17 src/lib/useKeyfileGenerator.js+10−6 src/lib/validators.js+6 src/lib/wallet.js+10−4 src/store/Provider.js+2 src/store/lib/useStarReleaseStore.js+63 src/store/starRelease.js+20 src/views/Activate/PassportTransfer.js+3 src/views/Admin/AdminNetworkingKeys.js+35−3 src/views/Admin/Reticket/ReticketDownload.js+1−1 src/views/Admin/Reticket/ReticketExecute.js+3 src/views/Login/Mnemonic.js+36−23 src/views/Party/PartySetPoolSize.js+18−1 src/views/Point.js+22 src/views/Points.js+32−12 src/views/Release.js+81 src/views/Release/Active.js+101 src/views/Release/Locked.js+179 src/views/Senate.js+230
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+71−37
@@ -1,6 +1,6 @@
11{
22 "name": "bridge",
3− "version": "1.4.1",
3+ "version": "2.2.0",
44 "lockfileVersion": 1,
55 "requires": true,
66 "dependencies": {
@@ -1448,54 +1448,54 @@
14481448 }
14491449 },
14501450 "@ledgerhq/devices": {
1451− "version": "4.70.0",
1452− "resolved": "https://registry.npmjs.org/@ledgerhq/devices/-/devices-4.70.0.tgz",
1453− "integrity": "sha512-xgcPls2BJivdG4pK8l4RFBf0CaMUNApP0sb+Izr9OlwbOH9iyLcLL2XC9AJuvoHGO1DUkDrCM2TkXJIx3Ui+Xg==",
1451+ "version": "5.0.0",
1452+ "resolved": "https://registry.npmjs.org/@ledgerhq/devices/-/devices-5.0.0.tgz",
1453+ "integrity": "sha512-wEnf2RMFELgj4q1ApG2bNzyobYgeX3Jl92FA8af/1VfcBB9esApFAO8cuE7aQruv2S6k5sTR5RMMnomrS9myiw==",
14541454 "requires": {
1455− "@ledgerhq/errors": "^4.70.0",
1456− "@ledgerhq/logs": "^4.70.0",
1455+ "@ledgerhq/errors": "^5.0.0",
1456+ "@ledgerhq/logs": "^5.0.0",
14571457 "rxjs": "^6.5.3"
14581458 }
14591459 },
14601460 "@ledgerhq/errors": {
1461− "version": "4.70.0",
1462− "resolved": "https://registry.npmjs.org/@ledgerhq/errors/-/errors-4.70.0.tgz",
1463− "integrity": "sha512-XFFuVGJ34dhkIfqTUGudPvnSx3cRGuZ+oVr8lR4UFuc29TwDVjf1X4GJ5wQU4XUqS4rNe+I+Tq7BK7V1DFX9lQ=="
1461+ "version": "5.0.0",
1462+ "resolved": "https://registry.npmjs.org/@ledgerhq/errors/-/errors-5.0.0.tgz",
1463+ "integrity": "sha512-sguPj6FeYOdkjuQEBfZSM6j5xYBk9RK1L9AfkLlO0zXSYHFZ+9OAlTA6Q0d5W7xmktx+32tUYxiT+JQ9WaeGmw=="
14641464 },
14651465 "@ledgerhq/hw-app-eth": {
1466− "version": "4.70.0",
1467− "resolved": "https://registry.npmjs.org/@ledgerhq/hw-app-eth/-/hw-app-eth-4.70.0.tgz",
1468− "integrity": "sha512-wifUwsdlENpW0TPBDlhDk+NADW3fIcaudIBEaEURvmOXoy0BGdfxfgeBuXrPW9UUiF6VLWVEjlmlSEDJI3GrFw==",
1466+ "version": "5.1.0",
1467+ "resolved": "https://registry.npmjs.org/@ledgerhq/hw-app-eth/-/hw-app-eth-5.1.0.tgz",
1468+ "integrity": "sha512-TjOVdDV4J6+yfEnF8TI5e5RjbP9tI16MUsHHGWNVH8NFK34KJjDJurcjVECktDXsMxYXldlPS50otCSwJqn8JQ==",
14691469 "requires": {
1470− "@ledgerhq/errors": "^4.70.0",
1471− "@ledgerhq/hw-transport": "^4.70.0"
1470+ "@ledgerhq/errors": "^5.0.0",
1471+ "@ledgerhq/hw-transport": "^5.0.0"
14721472 }
14731473 },
14741474 "@ledgerhq/hw-transport": {
1475− "version": "4.70.0",
1476− "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport/-/hw-transport-4.70.0.tgz",
1477− "integrity": "sha512-kl3TGlpy6iH3byVT+9Df9ObN3Ahi7O6eg8FHAWORO4jGUHROPiI8FYL03DTEvBXG5GqyKN1NfvvcKyC3N2dL2w==",
1475+ "version": "5.0.0",
1476+ "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport/-/hw-transport-5.0.0.tgz",
1477+ "integrity": "sha512-TS2h3RmFQj6igKGfE8ThN1Mk8+w4lK+xHvrDUymkJHzbQhboRTGcgp6psg5F3L2ZYKIq8Q3NFDSqhfygI2ZFqg==",
14781478 "requires": {
1479− "@ledgerhq/devices": "^4.70.0",
1480− "@ledgerhq/errors": "^4.70.0",
1479+ "@ledgerhq/devices": "^5.0.0",
1480+ "@ledgerhq/errors": "^5.0.0",
14811481 "events": "^3.0.0"
14821482 }
14831483 },
14841484 "@ledgerhq/hw-transport-u2f": {
1485− "version": "4.70.0",
1486− "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport-u2f/-/hw-transport-u2f-4.70.0.tgz",
1487− "integrity": "sha512-ttQtwdg/reTUWUwySwf13+3u3skr6/l3MaTFwxMri9H6sq+EicrcH+8pD74JtZ3gT/GhESskwGpo7QzfaO+YQQ==",
1485+ "version": "5.0.0",
1486+ "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport-u2f/-/hw-transport-u2f-5.0.0.tgz",
1487+ "integrity": "sha512-8936tpH5Ey8tQaycL2iTRWpb/dc5XgFnrCtzRPXiF/UbtIZ6Cy3l9G6Wd/OyzwipT1AZbJNf4l07WUOJFUcL7w==",
14881488 "requires": {
1489− "@ledgerhq/errors": "^4.70.0",
1490− "@ledgerhq/hw-transport": "^4.70.0",
1491− "@ledgerhq/logs": "^4.70.0",
1489+ "@ledgerhq/errors": "^5.0.0",
1490+ "@ledgerhq/hw-transport": "^5.0.0",
1491+ "@ledgerhq/logs": "^5.0.0",
14921492 "u2f-api": "0.2.7"
14931493 }
14941494 },
14951495 "@ledgerhq/logs": {
1496− "version": "4.70.0",
1497− "resolved": "https://registry.npmjs.org/@ledgerhq/logs/-/logs-4.70.0.tgz",
1498− "integrity": "sha512-hyMppU1b2tBDRyKJiV71RnN51Tkkriv/7DHQF9thAmrwEDFmfmEwAQaS2VyeIogir0HD7aDcKg7DnHqgiWe/8g=="
1496+ "version": "5.0.0",
1497+ "resolved": "https://registry.npmjs.org/@ledgerhq/logs/-/logs-5.0.0.tgz",
1498+ "integrity": "sha512-+Rgnp0T5ximUUwEUCSRbJNTdfVFrchO7sqY4MtVChXSMqu8vvzeg9KKakAE6t9IKEnoQ7aaG/BIEDtLXvrgRBQ=="
14991499 },
15001500 "@mrmlnc/readdir-enhanced": {
15011501 "version": "2.2.1",
@@ -2591,13 +2591,32 @@
25912591 }
25922592 },
25932593 "azimuth-js": {
2594− "version": "0.15.1",
2595− "resolved": "https://registry.npmjs.org/azimuth-js/-/azimuth-js-0.15.1.tgz",
2596− "integrity": "sha512-EEjDPlv4LQcgF5xhRQg2o81IrcVCu+3K87AI/NVRl37rcQyTCrCzjfeafOTqUQ8PvQXk+wf0Vrtrc+80yJju4g==",
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": {
2598− "azimuth-solidity": "1.2.0",
2598+ "azimuth-solidity": "1.2.1",
25992599 "ethereumjs-util": "^5.2.0",
2600− "urbit-ob": "^4.0.1"
2600+ "urbit-ob": "4.1.4"
2601+ },
2602+ "dependencies": {
2603+ "azimuth-solidity": {
2604+ "version": "1.2.1",
2605+ "resolved": "https://registry.npmjs.org/azimuth-solidity/-/azimuth-solidity-1.2.1.tgz",
2606+ "integrity": "sha512-T2tnvxKQ2tbmnCt0qSH2tjCiTee+A1XdHCFimU48CqdUimuYR8Mmj2mRe9GHqY0x6C8TX0ifwJL5pnR996sJLw==",
2607+ "requires": {
2608+ "babel-polyfill": "^6.26.0",
2609+ "babel-register": "^6.26.0",
2610+ "openzeppelin-solidity": "1.12.0",
2611+ "truffle": "4.1.11"
2612+ },
2613+ "dependencies": {
2614+ "openzeppelin-solidity": {
2615+ "version": "1.12.0",
2616+ "bundled": true
2617+ }
2618+ }
2619+ }
26012620 }
26022621 },
26032622 "azimuth-solidity": {
@@ -17490,13 +17509,28 @@
1749017509 "integrity": "sha512-ThJH58GNFKhCw3gIoOtwf3tNwuYjbyEeiGdeq4mNMYWdJctnI896KUqn6PVt7jmNVepqa1bcKQtnMB1HtjsDMA=="
1749117510 },
1749217511 "trezor-connect": {
17493− "version": "7.0.5",
17494− "resolved": "https://registry.npmjs.org/trezor-connect/-/trezor-connect-7.0.5.tgz",
17495− "integrity": "sha512-cGHcNuO/kGVF6b1mp5VB/RwXcXwqZJDPLp3opx7vM+BQ8xB4oDAUdL+T8aCKRbDv6HwP/wvGwoaok/+9kYOPfA==",
17512+ "version": "8.0.12",
17513+ "resolved": "https://registry.npmjs.org/trezor-connect/-/trezor-connect-8.0.12.tgz",
17514+ "integrity": "sha512-z9N43lHpFoHJlzqFGQcYGwtlsj+DJ5P6Df6EpTEKnur7tY3XPGcVPJmHYZxOujNYThiTBovgs0FgAON+KUZ21A==",
1749617515 "requires": {
17497− "@babel/runtime": "^7.3.1",
17516+ "@babel/runtime": "^7.7.2",
1749817517 "events": "^3.0.0",
1749917518 "whatwg-fetch": "^3.0.0"
17519+ },
17520+ "dependencies": {
17521+ "@babel/runtime": {
17522+ "version": "7.7.7",
17523+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.7.tgz",
17524+ "integrity": "sha512-uCnC2JEVAu8AKB5do1WRIsvrdJ0flYx/A/9f/6chdacnEZ7LmavjdsDXr5ksYBegxtuTPR5Va9/+13QF/kFkCA==",
17525+ "requires": {
17526+ "regenerator-runtime": "^0.13.2"
17527+ }
17528+ },
17529+ "regenerator-runtime": {
17530+ "version": "0.13.3",
17531+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz",
17532+ "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw=="
17533+ }
1750017534 }
1750117535 },
1750217536 "trim-newlines": {
package.json+5−5
@@ -1,6 +1,6 @@
11{
22 "name": "bridge",
3− "version": "2.0.0",
3+ "version": "2.2.1",
44 "description": "An application for interacting with Azimuth",
55 "repository": {
66 "type": "git",
@@ -15,11 +15,11 @@
1515 },
1616 "dependencies": {
1717 "@hot-loader/react-dom": "^16.8.6",
18− "@ledgerhq/hw-app-eth": "^4.61.0",
19− "@ledgerhq/hw-transport-u2f": "^4.61.0",
18+ "@ledgerhq/hw-app-eth": "^5.1.0",
19+ "@ledgerhq/hw-transport-u2f": "^5.0.0",
2020 "@welldone-software/why-did-you-render": "^3.2.3",
2121 "async-retry": "^1.2.3",
22− "azimuth-js": "^0.15.1",
22+ "azimuth-js": "^0.17.0",
2323 "azimuth-solidity": "^1.2.0",
2424 "bip32": "^1.0.2",
2525 "bip39": "^2.5.0",
@@ -48,7 +48,7 @@
4848 "react-scripts": "3.0.1",
4949 "react-teleporter": "^1.1.0",
5050 "secp256k1": "^3.5.2",
51− "trezor-connect": "^7.0.1",
51+ "trezor-connect": "^8.0.12",
5252 "urbit-key-generation": "^0.17.4",
5353 "urbit-ob": "^4.1.4",
5454 "urbit-paper-renderer": "^2.0.9",
src/components/Passport.js+1−14
@@ -8,6 +8,7 @@ import * as need from 'lib/need';
88import { chunkStr, Matrix, walk, rand } from 'lib/card';
99import usePermissionsForPoint from 'lib/usePermissionsForPoint';
1010import { useSyncOwnedPoints } from 'lib/useSyncPoints';
11+import { buildKeyType } from 'lib/point';
1112
1213import { useWallet } from 'store/wallet';
1314
@@ -50,20 +51,6 @@ function makeSigil(size, patp, colors) {
5051 }
5152}
5253
53−const buildKeyType = permissions => {
54− if (permissions.isOwner) {
55− return 'Ownership';
56− } else if (permissions.isManagementProxy) {
57− return 'Management';
58− } else if (permissions.isSpawnProxy) {
59− return 'Spawn';
60− } else if (permissions.isVotingProxy) {
61− return 'Voting';
62− } else if (permissions.isTransferProxy) {
63− return 'Transfer';
64− }
65−};
66−
6754const symbols = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
6855
6956/**
src/form/validators.js+13−3
@@ -4,6 +4,7 @@ import {
44 validateNotEmpty,
55 validatePatq,
66 validateMnemonic,
7+ validateHdPath,
78 validatePoint,
89 validateMaximumPatpByteLength,
910 validateOneOf,
@@ -11,6 +12,7 @@ import {
1112 validateHexLength,
1213 validateEthereumAddress,
1314 validateGreaterThan,
15+ validateLessThan,
1416 validateEmail,
1517 validateNotNullAddress,
1618 validateHexPrefix,
@@ -60,6 +62,8 @@ export const hasErrors = iter =>
6062
6163export const buildPatqValidator = (validators = []) =>
6264 buildValidator([validateNotEmpty, validatePatq, ...validators]);
65+export const buildAnyMnemonicValidator = () =>
66+ buildValidator([validateNotEmpty]);
6367export const buildMnemonicValidator = () =>
6468 buildValidator([validateNotEmpty, validateMnemonic]);
6569export const buildCheckboxValidator = mustBe =>
@@ -68,7 +72,7 @@ export const buildCheckboxValidator = mustBe =>
6872 ]);
6973export const buildPassphraseValidator = () => buildValidator([]);
7074// TODO: validate hdpath format
71−export const buildHdPathValidator = () => buildValidator([validateNotEmpty]);
75+export const buildHdPathValidator = () => buildValidator([validateHdPath]);
7276export const buildPointValidator = (size = 4, validators = []) =>
7377 buildValidator([
7478 validateNotEmpty,
@@ -101,8 +105,14 @@ export const buildAddressValidator = () =>
101105 validateNotNullAddress,
102106 validateEthereumAddress,
103107 ]);
104−export const buildNumberValidator = (min = 0) =>
105− buildValidator([validateGreaterThan(min)]);
108+export const buildNumberValidator = (min = 0, max = null) => {
109+ let validators = [validateGreaterThan(min)];
110+ if (max !== null) {
111+ validators.push(validateLessThan(max));
112+ }
113+ return buildValidator(validators);
114+};
115+
106116export const buildEmailValidator = validate =>
107117 buildValidator([validateNotEmpty, validateEmail], validate);
108118
src/indigo-react/indigo.css+13−1
@@ -159,7 +159,16 @@ textarea {
159159 line-height: 1.15;
160160 /* 1 */
161161 margin: 0;
162− /* 2 */ }
162+/* 2 */ }
163+
164+/**
165+ *
166+ * Allow input to flex properly
167+ *
168+ */
169+input {
170+ min-width: 0;
171+}
163172
164173/**
165174 * Show the overflow in IE.
@@ -3396,6 +3405,9 @@ caption,
33963405.h-auto {
33973406 height: auto; }
33983407
3408+.h-full {
3409+ height: 100%; }
3410+
33993411@media only screen and (min-width: 550px) {
34003412 .h-auto-ns {
34013413 height: auto; } }
src/lib/contracts.js+6
@@ -4,18 +4,24 @@ const CONTRACT_ADDRESSES = {
44 azimuth: '0x863d9c2e5c4c133596cfac29d55255f0d0f86381',
55 polls: '0x935452c45eda2958976a429c9733c40302995efd',
66 delegatedSending: '0xb71c0b6cee1bcae56dfe95cd9d3e41ddd7eafc43',
7+ linearSR: '0xa32c3019b256880A8c5458379188eea6F3934e2F',
8+ conditionalSR: '0x35EB3B102d9C1B69Ac1469C1B1FE1799850CD3EB',
79 },
810 ROPSTEN: {
911 ecliptic: '0x8b9f86a28921d9c705b3113a755fb979e1bd1bce',
1012 azimuth: '0x308ab6a6024cf198b57e008d0ac9ad0219886579',
1113 polls: '0xf5DA85f0d285A0F88af2388DD177A221872C8971',
1214 delegatedSending: '0x3e8ca510354bc2fdbbd6150252d93105c9c27bbe',
15+ linearSR: '0x1f8edd031ee414740aedb39b84fb8f2f66ca422f',
16+ conditionalSR: '0x1f8edd031ee414740aedb39b84fb8f2f66ca422f',
1317 },
1418 MAINNET: {
1519 ecliptic: '0x6ac07b7c4601b5ce11de8dfe6335b871c7c4dd4d',
1620 azimuth: '0x223c067f8cf28ae173ee5cafea60ca44c335fecb',
1721 polls: '0x7fecab617c868bb5996d99d95200d2fa708218e4',
1822 delegatedSending: '0xF7908Ab1F1e352F83c5ebc75051c0565AEaea5FB',
23+ linearSR: '0x86cd9cd0992f04231751e3761de45cecea5d1801',
24+ conditionalSR: '0x8c241098c3d3498fe1261421633fd57986d74aea',
1925 },
2026};
2127
src/lib/inviteMail.js+1−1
@@ -2,7 +2,7 @@
22// did not succeed", you might need to visit localhost:3001 or whatever
33// explicitly and tell your browser that's safe to access.
44// https://stackoverflow.com/a/53011185/1334324
5−const baseUrl = 'https://localhost:3002';
5+const baseUrl = 'https://onboarding-emails.urbit.org:3002';
66
77function sendRequest(where, what) {
88 return new Promise((resolve, reject) => {
src/lib/point.jsnew file+13
@@ -0,0 +1,13 @@
1+export const buildKeyType = permissions => {
2+ if (permissions.isOwner) {
3+ return 'Ownership';
4+ } else if (permissions.isManagementProxy) {
5+ return 'Management';
6+ } else if (permissions.isSpawnProxy) {
7+ return 'Spawn';
8+ } else if (permissions.isVotingProxy) {
9+ return 'Voting';
10+ } else if (permissions.isTransferProxy) {
11+ return 'Transfer';
12+ }
13+};
src/lib/routeNames.js+1
@@ -21,4 +21,5 @@ export const ROUTE_NAMES = {
2121 ISSUE_CHILD: Symbol('ISSUE_CHILD'),
2222 PARTY_SET_POOL_SIZE: Symbol('PARTY_SET_POOL_SIZE'),
2323 PARTY: Symbol('PARTY'),
24+ STAR_RELEASE: Symbol('STAR_RELEASE'),
2425};
src/lib/router.js+4
@@ -1,8 +1,10 @@
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';
7+import StarRelease from 'views/Release';
68
79import Point from 'views/Point';
810import Points from 'views/Points';
@@ -26,10 +28,12 @@ export const ROUTES = {
2628 [ROUTE_NAMES.ISSUE_CHILD]: IssueChild,
2729 [ROUTE_NAMES.INVITE]: Invite,
2830 [ROUTE_NAMES.ADMIN]: Admin,
31+ [ROUTE_NAMES.SENATE]: Senate,
2932 // TODO: nest this route under the Party.js router
3033 [ROUTE_NAMES.PARTY_SET_POOL_SIZE]: PartySetPoolSize,
3134 [ROUTE_NAMES.ACCEPT_TRANSFER]: AcceptTransfer,
3235 // TODO: replace this with deep link to AdminCancelTransfer
3336 [ROUTE_NAMES.CANCEL_TRANSFER]: CancelTransfer,
3437 [ROUTE_NAMES.PARTY]: ShowParty,
38+ [ROUTE_NAMES.STAR_RELEASE]: StarRelease,
3539};
src/lib/starRelease.jsnew file+101
@@ -0,0 +1,101 @@
1+import { linearSR, conditionalSR } from 'azimuth-js';
2+
3+function mapRange(amount, cb) {
4+ return [...Array(amount).keys()].map(cb);
5+}
6+
7+// Preferentially withdraw from conditional contracts and use linear
8+// to mop up the rest
9+export function generateWithdrawTxs(contracts, batchLimits, amount, to) {
10+ const [left, ...csrTxs] = generateConditionalWithdrawTxs(
11+ contracts,
12+ batchLimits,
13+ amount,
14+ to
15+ );
16+ const lsrTxs = generateLinearWithdrawTxs(contracts, left, to);
17+ return [...csrTxs, ...lsrTxs];
18+}
19+
20+/**
21+ * Recurse over batchLimits to generate a valid number of withdraw txs.
22+ * amount may exceed the number of withdraw txs available in the consolidated SR contract.
23+ *
24+ * @param {object} contracts - The azimuth contracts object
25+ * @param {array} batchLimits - An array of the maximum withdraw limit for each batch
26+ * @param {amount} amount - The number of txs to be generated
27+ * @param {string} to - The addresss to withdraw to
28+ * @return {array} An array with the number of transactions missing as the head
29+ * (i.e. requested amount exceeded available), and the generated transactions
30+ * as the rest of the list
31+ */
32+function generateConditionalWithdrawTxs(
33+ contracts,
34+ batchLimits,
35+ amount,
36+ to,
37+ offset = 0
38+) {
39+ // Successfully withdrawn all requested, none missing
40+ if (amount <= 0) {
41+ return [0];
42+ }
43+ // Return missing amount in head of list
44+ if (batchLimits.length === 0) {
45+ return [amount];
46+ }
47+ const [next, ...rest] = batchLimits;
48+ const currentAmount = Math.min(amount, next);
49+ return [
50+ ...generateConditionalWithdrawTxs(
51+ contracts,
52+ rest,
53+ amount - currentAmount,
54+ to,
55+ offset + 1
56+ ),
57+ ...mapRange(currentAmount, () =>
58+ conditionalSR.withdrawTo(contracts, offset, to)
59+ ),
60+ ];
61+}
62+
63+function generateLinearWithdrawTxs(contracts, amount, to) {
64+ return mapRange(amount, () => linearSR.withdrawTo(contracts, to));
65+}
66+
67+export async function getConditional(contracts, address) {
68+ const [commitment, batches] = await Promise.all([
69+ conditionalSR.getCommitment(contracts, address),
70+ conditionalSR.getBatches(contracts, address),
71+ ]);
72+
73+ if (commitment === null || batches === null) {
74+ return { total: 0, available: 0, withdrawn: 0, batchLimits: [] };
75+ }
76+
77+ const { total } = commitment;
78+ const withdrawLimits = await Promise.all(
79+ batches.map((_, idx) =>
80+ conditionalSR.getWithdrawLimit(contracts, address, idx)
81+ )
82+ );
83+ const available = withdrawLimits.reduce((acc, val) => acc + val, 0);
84+
85+ const withdrawnAmounts = await conditionalSR.getWithdrawn(contracts, address);
86+
87+ const withdrawn = withdrawnAmounts.reduce((acc, val) => acc + val, 0);
88+ const batchLimits = withdrawLimits.map(
89+ (limit, idx) => limit - withdrawnAmounts[idx]
90+ );
91+
92+ return { total, available, withdrawn, batchLimits };
93+}
94+
95+export async function getLinear(contracts, address) {
96+ const { amount, withdrawn } = await linearSR.getBatch(contracts, address);
97+
98+ const available = // Contract errors on withdrawing from a user that does not have any
99+ amount > 0 ? await linearSR.getWithdrawLimit(contracts, address) : 0;
100+ return { available, withdrawn, total: amount };
101+}
src/lib/tank.js+1−1
@@ -8,7 +8,7 @@ import { RETRY_OPTIONS, waitForTransactionConfirm } from './txn';
88// did not succeed", you might need to visit localhost:3001 or whatever
99// explicitly and tell your browser that's safe to access.
1010// https://stackoverflow.com/a/53011185/1334324
11−const baseUrl = 'https://localhost:3001';
11+const baseUrl = 'https://gas-tank.urbit.org:3001';
1212
1313function sendRequest(where, what) {
1414 return new Promise((resolve, reject) => {
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/useKeyfileGenerator.js+10−6
@@ -1,4 +1,4 @@
1−import { useCallback, useEffect, useState } from 'react';
1+import { useCallback, useMemo, useEffect, useState } from 'react';
22import { Just, Nothing } from 'folktale/maybe';
33import saveAs from 'file-saver';
44import ob from 'urbit-ob';
@@ -79,8 +79,6 @@ export default function useKeyfileGenerator(manualNetworkSeed) {
7979
8080 const pair = deriveNetworkKeys(_networkSeed);
8181
82− setCode(generateCode(pair));
83−
8482 if (!keysMatchChain(pair, _details)) {
8583 setGenerating(false);
8684 setNotice('Derived networking keys do not match on-chain details.');
@@ -89,6 +87,7 @@ export default function useKeyfileGenerator(manualNetworkSeed) {
8987 }
9088
9189 setNotice();
90+ setCode(generateCode(pair));
9291 setKeyfile(compileNetworkingKey(pair, _point, networkRevision));
9392 setGenerating(false);
9493 }, [
@@ -103,16 +102,20 @@ export default function useKeyfileGenerator(manualNetworkSeed) {
103102 _point,
104103 ]);
105104
105+ const filename = useMemo(() => {
106+ return `${ob.patp(_point).slice(1)}-${networkRevision}.key`;
107+ // TODO: ^ unifiy "remove tilde" calls
108+ }, [_point, networkRevision]);
109+
106110 const download = useCallback(() => {
107111 saveAs(
108112 new Blob([keyfile], {
109113 type: 'text/plain;charset=utf-8',
110114 }),
111− `${ob.patp(_point).slice(1)}-${networkRevision}.key`
112− // TODO: ^ unifiy "remove tilde" calls
115+ filename
113116 );
114117 setDownloaded(true);
115− }, [_point, keyfile, networkRevision]);
118+ }, [filename, keyfile]);
116119
117120 useEffect(() => {
118121 generate();
@@ -123,6 +126,7 @@ export default function useKeyfileGenerator(manualNetworkSeed) {
123126 available,
124127 downloaded,
125128 download,
129+ filename,
126130 notice,
127131 code,
128132 };
src/lib/validators.js+6
@@ -24,6 +24,10 @@ const isHexPrefixed = /^0x/;
2424export const validateMnemonic = v =>
2525 !bip39.validateMnemonic(v) && 'This is not a valid mnemonic.';
2626
27+const isHdPath = /^m(\/[0-9]+'?)*$/;
28+export const validateHdPath = v =>
29+ !isHdPath.test(v) && 'Invalid HD derivation path.';
30+
2731// Checks an empty field
2832export const validateNotEmpty = v =>
2933 (v === undefined || v.length === 0) && 'This field is required.';
@@ -93,6 +97,8 @@ export const validateMaximumLength = l => v =>
9397export const validateGreaterThan = l => v =>
9498 !(v > l) && `Must be at least ${l}.`;
9599
100+export const validateLessThan = l => v => !(v < l) && `Must be less than ${l}`;
101+
96102export const validateInSet = (set, error) => v => !set.has(v) && error;
97103
98104export const validateMaximumPatpByteLength = byteLength =>
src/lib/wallet.js+10−4
@@ -81,10 +81,16 @@ export const urbitWalletFromTicket = async (ticket, point, passphrase) => {
8181 });
8282};
8383
84−export const walletFromMnemonic = (mnemonic, hdpath, passphrase) => {
85− const seed = bip39.validateMnemonic(mnemonic)
86− ? Just(bip39.mnemonicToSeed(mnemonic, passphrase))
87− : Nothing();
84+export const walletFromMnemonic = (
85+ mnemonic,
86+ hdpath,
87+ passphrase,
88+ skipMnemonicCheck
89+) => {
90+ const seed =
91+ skipMnemonicCheck || bip39.validateMnemonic(mnemonic)
92+ ? Just(bip39.mnemonicToSeed(mnemonic, passphrase))
93+ : Nothing();
8894
8995 const toWallet = (sd, path) => {
9096 let wal;
src/store/Provider.js+2
@@ -5,6 +5,7 @@ import { OnlineProvider } from './online';
55import { NetworkProvider } from './network';
66import { WalletProvider } from './wallet';
77import { PointCursorProvider } from './pointCursor';
8+import { StarReleaseCacheProvider } from './starRelease';
89import { PointCacheProvider } from './pointCache';
910import { TxnCursorProvider } from './txnCursor';
1011import { MediaQueryProvider } from 'lib/MediaQuery';
@@ -19,4 +20,5 @@ export default nest([
1920 PointCacheProvider,
2021 TxnCursorProvider,
2122 MediaQueryProvider,
23+ StarReleaseCacheProvider,
2224]);
src/store/lib/useStarReleaseStore.jsnew file+63
@@ -0,0 +1,63 @@
1+import { useState, useCallback } from 'react';
2+import { Nothing, Just } from 'folktale/maybe';
3+
4+import { useNetwork } from 'store/network';
5+import { useWallet } from 'store/wallet';
6+import {
7+ generateWithdrawTxs,
8+ getLinear,
9+ getConditional,
10+} from 'lib/starRelease';
11+
12+export default function useStarRelease() {
13+ const { contracts } = useNetwork();
14+ const { wallet } = useWallet();
15+
16+ const [starReleaseDetails, _setDetails] = useState(Nothing());
17+
18+ const [batchLimits, setBatchLimits] = useState([]);
19+
20+ const syncStarReleaseDetails = useCallback(async () => {
21+ const _wallet = wallet.getOrElse(null);
22+ const _contracts = contracts.getOrElse(null);
23+ if (!(_contracts && _wallet)) {
24+ return;
25+ }
26+ _setDetails(Nothing());
27+
28+ const address = _wallet.address;
29+
30+ try {
31+ const [linear, conditional] = await Promise.all([
32+ getLinear(_contracts, address),
33+ getConditional(_contracts, address),
34+ ]);
35+
36+ const keys = Object.keys(linear);
37+ const result = keys.reduce(
38+ (acc, key) => ({ ...acc, [key]: linear[key] + conditional[key] }),
39+ {}
40+ );
41+ setBatchLimits(conditional.batchLimits);
42+
43+ _setDetails(Just(result));
44+ } catch (e) {
45+ _setDetails(Nothing());
46+ console.error(e);
47+ }
48+ }, [contracts, wallet]);
49+
50+ const withdraw = useCallback(
51+ (amount, to) => {
52+ const _wallet = wallet.getOrElse(null);
53+ const _contracts = contracts.getOrElse(null);
54+ if (!(_contracts && _wallet)) {
55+ return [];
56+ }
57+ return generateWithdrawTxs(_contracts, batchLimits, amount, to);
58+ },
59+ [batchLimits, contracts, wallet]
60+ );
61+
62+ return { starReleaseDetails, syncStarReleaseDetails, withdraw };
63+}
src/store/starRelease.jsnew file+20
@@ -0,0 +1,20 @@
1+import React, { createContext, useContext } from 'react';
2+
3+import useStarReleaseStore from './lib/useStarReleaseStore';
4+
5+export const StarReleaseCacheContext = createContext(null);
6+
7+export function StarReleaseCacheProvider({ children }) {
8+ const cache = useStarReleaseStore();
9+
10+ return (
11+ <StarReleaseCacheContext.Provider value={cache}>
12+ {children}
13+ </StarReleaseCacheContext.Provider>
14+ );
15+}
16+
17+// Hook version
18+export function useStarReleaseCache() {
19+ return useContext(StarReleaseCacheContext);
20+}
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+35−3
@@ -2,6 +2,7 @@ import React, { useCallback, useMemo, useRef, useState } from 'react';
22import { Just, Nothing } from 'folktale/maybe';
33import { Grid, Text, H5, Flex, ToggleInput, CheckboxInput } from 'indigo-react';
44import * as azimuth from 'azimuth-js';
5+import ob from 'urbit-ob';
56import { randomHex } from 'web3-utils';
67
78import { usePointCursor } from 'store/pointCursor';
@@ -73,6 +74,7 @@ function useSetKeys() {
7374
7475 const {
7576 available: keyfileAvailable,
77+ filename,
7678 bind: keyfileBind,
7779 } = useKeyfileGenerator(ndNetworkSeed);
7880
@@ -136,6 +138,7 @@ function useSetKeys() {
136138 return {
137139 completed,
138140 ndNetworkSeed,
141+ filename,
139142 keyfileBind,
140143 ...rest,
141144 };
@@ -156,6 +159,8 @@ export default function AdminNetworkingKeys() {
156159 // since NaN > 0 === false and that's a reasonable result
157160 });
158161
162+ const [usedDiscontinuity, setUsedDiscontinuity] = useState(false);
163+
159164 const {
160165 isDefaultState,
161166 construct,
@@ -163,6 +168,7 @@ export default function AdminNetworkingKeys() {
163168 broadcasting,
164169 completed,
165170 inputsLocked,
171+ filename,
166172 bind,
167173 keyfileBind,
168174 } = useSetKeys();
@@ -193,6 +199,7 @@ export default function AdminNetworkingKeys() {
193199 values.useNetworkSeed ? values.networkSeed : undefined,
194200 values.useDiscontinuity
195201 );
202+ setUsedDiscontinuity(values.useDiscontinuity);
196203 } else {
197204 unconstruct();
198205 }
@@ -201,7 +208,7 @@ export default function AdminNetworkingKeys() {
201208 form.change('networkSeed', '');
202209 }
203210 },
204− [construct, unconstruct]
211+ [construct, unconstruct, setUsedDiscontinuity]
205212 );
206213
207214 const initialValues = useMemo(
@@ -214,6 +221,32 @@ export default function AdminNetworkingKeys() {
214221
215222 const goRelocate = useCallback(() => push(names.RELOCATE), [push, names]);
216223
224+ const usageMessage = useMemo(() => {
225+ const need = 'You need this keyfile to authenticate with Arvo.';
226+ const boot = (
227+ <>
228+ boot fresh with{' '}
229+ <code>
230+ urbit -w {ob.patp(point).slice(1)} -k {filename}
231+ </code>
232+ </>
233+ );
234+ if (usedDiscontinuity) {
235+ return (
236+ <>
237+ {need} Since you broke continuity, (re)move your old files, and {boot}
238+ </>
239+ );
240+ } else {
241+ return (
242+ <>
243+ {need} If you have already booted Arvo, run{' '}
244+ <code>|rekey '0wkeyfile.contents'</code>. Otherwise, {boot}
245+ </>
246+ );
247+ }
248+ }, [usedDiscontinuity, filename, point]);
249+
217250 const renderTitle = () => {
218251 if (completed) {
219252 return 'Networking keys are now set. Download your Keyfile to authenticate Arvo.';
@@ -323,8 +356,7 @@ export default function AdminNetworkingKeys() {
323356 {completed && (
324357 <>
325358 <Grid.Item full as={NoticeBox} className="mb3">
326− You need this keyfile to authenticate with Arvo. If you have
327− already booted Arvo, run <code>|rekey '0wkeyfile.contents'</code>
359+ {usageMessage}
328360 </Grid.Item>
329361 <Grid.Item full as={DownloadKeyfileButton} solid {...keyfileBind} />
330362 </>
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/Login/Mnemonic.js+36−23
@@ -1,15 +1,16 @@
1−import React, { useCallback, useMemo } from 'react';
1+import React, { useCallback, useState, useMemo } from 'react';
22import cn from 'classnames';
33import { Just, Nothing } from 'folktale/maybe';
44import { Grid, CheckboxInput } from 'indigo-react';
55
66import { useWallet } from 'store/wallet';
77
8−import { walletFromMnemonic, WALLET_TYPES } from 'lib/wallet';
8+import { walletFromMnemonic, WALLET_TYPES, DEFAULT_HD_PATH } from 'lib/wallet';
99import useLoginView from 'lib/useLoginView';
1010import { MnemonicInput, HdPathInput, PassphraseInput } from 'form/Inputs';
1111import {
1212 composeValidator,
13+ buildAnyMnemonicValidator,
1314 buildMnemonicValidator,
1415 buildCheckboxValidator,
1516 buildPassphraseValidator,
@@ -23,32 +24,36 @@ import SubmitButton from 'form/SubmitButton';
2324export default function Mnemonic({ className, goHome }) {
2425 useLoginView(WALLET_TYPES.MNEMONIC);
2526
26− const {
27− setWallet,
28− setAuthMnemonic,
29− walletHdPath,
30− setWalletHdPath,
31− } = useWallet();
27+ const { setWallet, setAuthMnemonic, setWalletHdPath } = useWallet();
3228
33− const validate = useMemo(
34− () =>
35− composeValidator({
36− useAdvanced: buildCheckboxValidator(),
37− mnemonic: buildMnemonicValidator(),
38− passphrase: buildPassphraseValidator(),
39− hdpath: buildHdPathValidator(),
40− }),
41− []
42− );
29+ const [anyMnemonic, setAnyMnemonic] = useState(false);
30+
31+ const validate = useMemo(() => {
32+ const mnemonicValidator = anyMnemonic
33+ ? buildAnyMnemonicValidator()
34+ : buildMnemonicValidator();
35+ return composeValidator({
36+ useAdvanced: buildCheckboxValidator(),
37+ mnemonic: mnemonicValidator,
38+ passphrase: buildPassphraseValidator(),
39+ hdpath: buildHdPathValidator(),
40+ });
41+ }, [anyMnemonic]);
4342
4443 // when the properties change, re-derive wallet and set global state
4544 const onValues = useCallback(
4645 ({ valid, values }) => {
46+ setAnyMnemonic(values.anyMnemonic);
4747 if (valid) {
4848 setWalletHdPath(values.hdpath);
4949 setAuthMnemonic(Just(values.mnemonic));
5050 setWallet(
51− walletFromMnemonic(values.mnemonic, values.hdpath, values.passphrase)
51+ walletFromMnemonic(
52+ values.mnemonic,
53+ values.hdpath,
54+ values.passphrase,
55+ values.anyMnemonic
56+ )
5257 );
5358 } else {
5459 setAuthMnemonic(Nothing());
@@ -58,10 +63,11 @@ export default function Mnemonic({ className, goHome }) {
5863 [setAuthMnemonic, setWallet, setWalletHdPath]
5964 );
6065
61− const initialValues = useMemo(
62− () => ({ hdpath: walletHdPath, useAdvanced: false }),
63− [walletHdPath]
64− );
66+ const initialValues = {
67+ hdpath: DEFAULT_HD_PATH,
68+ useAdvanced: false,
69+ anyMnemonic: false,
70+ };
6571
6672 return (
6773 <Grid className={cn('mt4', className)}>
@@ -79,6 +85,13 @@ export default function Mnemonic({ className, goHome }) {
7985 label="BIP39 Mnemonic"
8086 />
8187
88+ <Grid.Item
89+ full
90+ as={CheckboxInput}
91+ name="anyMnemonic"
92+ label="Skip mnemonic validation"
93+ />
94+
8295 <Condition when="useAdvanced" is={true}>
8396 <Grid.Item
8497 full
src/views/Party/PartySetPoolSize.js+18−1
@@ -13,6 +13,7 @@ import { GAS_LIMITS } from 'lib/constants';
1313import { useLocalRouter } from 'lib/LocalRouter';
1414import useCurrentPermissions from 'lib/useCurrentPermissions';
1515import patp2dec from 'lib/patp2dec';
16+import convertToInt from 'lib/convertToInt';
1617
1718import ViewHeader from 'components/ViewHeader';
1819import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
@@ -56,6 +57,15 @@ function useSetPoolSize() {
5657export default function PartySetPoolSize() {
5758 const { pop } = useLocalRouter();
5859 const { contracts } = useNetwork();
60+ const { pointCursor } = usePointCursor();
61+ const { getDetails } = usePointCache();
62+
63+ const _point = need.point(pointCursor);
64+ const _details = need.details(getDetails(_point));
65+ const hasKeysSet =
66+ 0 !== convertToInt(_details.encryptionKey, 16) &&
67+ 0 !== convertToInt(_details.authenticationKey, 16) &&
68+ 0 !== _details.cryptoSuiteVersion;
5969
6070 const _contracts = need.contracts(contracts);
6171
@@ -104,7 +114,14 @@ export default function PartySetPoolSize() {
104114 <CopiableAddress>
105115 {_contracts.delegatedSending.address}
106116 </CopiableAddress>{' '}
107− for invitations to be available.
117+ for invitations to be available for use.
118+ </Grid.Item>
119+ )}
120+
121+ {!hasKeysSet && (
122+ <Grid.Item full as={WarningBox} className="mb4 f6">
123+ Networking keys must be configured for invitations to be available
124+ for use.
108125 </Grid.Item>
109126 )}
110127
src/views/Point.js+22
@@ -40,6 +40,7 @@ export default function Point() {
4040 isActiveOwner,
4141 canManage,
4242 canSpawn,
43+ canVote,
4344 } = useCurrentPermissions();
4445
4546 // fetch the invites for the current cursor
@@ -66,6 +67,8 @@ export default function Point() {
6667
6768 const goAdmin = useCallback(() => push(names.ADMIN), [push, names]);
6869
70+ const goSenate = useCallback(() => push(names.SENATE), [push, names]);
71+
6972 const goInvite = useCallback(() => push(names.INVITE), [push, names]);
7073
7174 const goParty = useCallback(() => push(names.PARTY), [push, names]);
@@ -120,6 +123,24 @@ export default function Point() {
120123 }
121124 })();
122125
126+ const senateButton = (() => {
127+ if (azimuth.getPointSize(point) !== azimuth.PointSize.Galaxy) {
128+ return null;
129+ }
130+ return (
131+ <>
132+ <Grid.Item
133+ full
134+ as={ForwardButton}
135+ disabled={!canVote}
136+ onClick={goSenate}>
137+ Senate
138+ </Grid.Item>
139+ <Grid.Divider />
140+ </>
141+ );
142+ })();
143+
123144 // sync the current cursor
124145 useSyncOwnedPoints([point]);
125146
@@ -219,6 +240,7 @@ export default function Point() {
219240 <Grid.Divider />
220241 </>
221242 )}
243+ {senateButton}
222244 <Grid.Item full as={BootUrbitOSButton} />
223245 <Grid.Divider />
224246 <Grid.Item
src/views/Points.js+32−12
@@ -7,6 +7,7 @@ import { useHistory } from 'store/history';
77import { useWallet } from 'store/wallet';
88import { usePointCache } from 'store/pointCache';
99import { usePointCursor } from 'store/pointCursor';
10+import { useStarReleaseCache } from 'store/starRelease';
1011
1112import * as need from 'lib/need';
1213import { isZeroAddress, abbreviateAddress } from 'lib/wallet';
@@ -91,6 +92,7 @@ export default function Points() {
9192 rejectedPoints,
9293 addRejectedPoint,
9394 ] = useRejectedIncomingPointTransfers();
95+ const { syncStarReleaseDetails, starReleaseDetails } = useStarReleaseCache();
9496
9597 const maybeOutgoingPoints = useMemo(
9698 () =>
@@ -124,7 +126,10 @@ export default function Points() {
124126 // if there are any pending transfers, incoming or outgoing, stay on this
125127 // page, because those can only be completed/cancelled here.
126128 useEffect(() => {
127− if (Nothing.hasInstance(maybeOutgoingPoints)) {
129+ if (
130+ Nothing.hasInstance(maybeOutgoingPoints) ||
131+ Nothing.hasInstance(starReleaseDetails)
132+ ) {
128133 return;
129134 }
130135 controlledPoints.matchWith({
@@ -145,7 +150,8 @@ export default function Points() {
145150 if (
146151 all.length === 1 &&
147152 incoming.length === 0 &&
148− maybeOutgoingPoints.value.length === 0
153+ maybeOutgoingPoints.value.length === 0 &&
154+ starReleaseDetails.value.total === 0
149155 ) {
150156 setPointCursor(Just(all[0]));
151157 popAndPush(names.POINT);
@@ -161,6 +167,7 @@ export default function Points() {
161167 setPointCursor,
162168 popAndPush,
163169 names,
170+ starReleaseDetails,
164171 ]);
165172
166173 const address = need.addressFromWallet(wallet);
@@ -188,6 +195,13 @@ export default function Points() {
188195 const displayEmptyState =
189196 !loading && incomingPoints.length === 0 && allPoints.length === 0;
190197
198+ const starReleasing = starReleaseDetails
199+ .map(s => s.total > 0)
200+ .getOrElse(false);
201+
202+ useEffect(() => {
203+ syncStarReleaseDetails();
204+ }, [syncStarReleaseDetails]);
191205 // sync display details for known points
192206 useSyncKnownPoints([
193207 ...ownedPoints,
@@ -204,8 +218,8 @@ export default function Points() {
204218 push,
205219 ]);
206220
207− const goViewPoint = useCallback(() => push(names.VIEW_POINT), [
208− names.VIEW_POINT,
221+ const goStarRelease = useCallback(() => push(names.STAR_RELEASE), [
222+ names.STAR_RELEASE,
209223 push,
210224 ]);
211225
@@ -213,7 +227,8 @@ export default function Points() {
213227 loading ||
214228 (allPoints.length === 1 &&
215229 incomingPoints.length === 0 &&
216− outgoingPoints.length === 0)
230+ outgoingPoints.length === 0 &&
231+ !starReleasing)
217232 ) {
218233 return (
219234 <View inset pop={pop}>
@@ -336,13 +351,18 @@ export default function Points() {
336351 <Grid.Divider />
337352 </>
338353 )}
339− <Grid.Item
340− full
341− as={ForwardButton}
342− detail="View an ID"
343− onClick={goViewPoint}>
344− View a point
345− </Grid.Item>
354+ {starReleasing && (
355+ <>
356+ <Grid.Item
357+ full
358+ as={ForwardButton}
359+ detail="You have points being released"
360+ onClick={goStarRelease}>
361+ View Star Release
362+ </Grid.Item>
363+ <Grid.Divider />
364+ </>
365+ )}
346366 </Grid>
347367 </Footer>
348368 </Grid>
src/views/Release.jsnew file+81
@@ -0,0 +1,81 @@
1+import React, { useState, useCallback } from 'react';
2+import { H1, Grid } from 'indigo-react';
3+import { Just } from 'folktale/maybe';
4+
5+import * as need from 'lib/need';
6+
7+import { useHistory } from 'store/history';
8+import { useWallet } from 'store/wallet';
9+import { usePointCursor } from 'store/pointCursor';
10+
11+import View from 'components/View';
12+import Tabs from 'components/Tabs';
13+import Crumbs from 'components/Crumbs';
14+import CopiableAddress from 'components/CopiableAddress';
15+
16+import Active from 'views/Release/Active';
17+import Locked from 'views/Release/Locked';
18+
19+const NAMES = {
20+ ACTIVE: 'ACTIVE',
21+ LOCKED: 'LOCKED',
22+};
23+
24+const VIEWS = {
25+ [NAMES.ACTIVE]: Active,
26+ [NAMES.LOCKED]: Locked,
27+};
28+const OPTIONS = [
29+ { text: 'Active', value: NAMES.ACTIVE },
30+ { text: 'Locked', value: NAMES.LOCKED },
31+];
32+export default function ReleaseView() {
33+ const { pop, push, names } = useHistory();
34+ const { wallet } = useWallet();
35+
36+ const { setPointCursor } = usePointCursor();
37+
38+ const goPoint = useCallback(
39+ point => {
40+ setPointCursor(Just(point));
41+
42+ push(names.POINT);
43+ },
44+ [push, names, setPointCursor]
45+ );
46+ // inputs
47+ const [currentTab, setCurrentTab] = useState(NAMES.ACTIVE);
48+
49+ const goActive = useCallback(() => {
50+ setCurrentTab(NAMES.ACTIVE);
51+ }, [setCurrentTab]);
52+
53+ const address = need.addressFromWallet(wallet);
54+
55+ return (
56+ <View pop={pop} inset>
57+ <Grid>
58+ <Grid.Item full as={Crumbs} />
59+ <Grid.Item full as={H1} className="f6 mono gray4 us-none pointer">
60+ <CopiableAddress text={address}>{address}</CopiableAddress>
61+ </Grid.Item>
62+
63+ <Grid.Item
64+ full
65+ as={Tabs}
66+ className="mt1"
67+ // Tabs
68+ views={VIEWS}
69+ options={OPTIONS}
70+ currentTab={currentTab}
71+ onTabChange={setCurrentTab}
72+ // Tab extra
73+ goPoint={goPoint}
74+ goActive={goActive}
75+ //
76+ //
77+ />
78+ </Grid>
79+ </View>
80+ );
81+}
src/views/Release/Active.jsnew file+101
@@ -0,0 +1,101 @@
1+import React, { useCallback } from 'react';
2+import { Grid } from 'indigo-react';
3+import cn from 'classnames';
4+import * as ob from 'urbit-ob';
5+
6+import Sigil from 'components/Sigil';
7+
8+import { usePointCache } from 'store/pointCache';
9+import { useWallet } from 'store/wallet';
10+
11+import * as need from 'lib/need';
12+import usePermissionsForPoint from 'lib/usePermissionsForPoint';
13+import { useSyncOwnedPoints } from 'lib/useSyncPoints';
14+import { buildKeyType } from 'lib/point';
15+
16+function ActiveViewRow({ point, goPoint }) {
17+ const { wallet } = useWallet();
18+ const address = need.addressFromWallet(wallet);
19+ useSyncOwnedPoints([point]);
20+ const permissions = usePermissionsForPoint(address, point);
21+
22+ const keyType = buildKeyType(permissions);
23+ const patp = ob.patp(point);
24+ const sigilSize = 50;
25+
26+ const onClick = useCallback(() => goPoint(point), [point, goPoint]);
27+ return (
28+ <>
29+ <Grid.Item className="flex-row align-center" cols={[1, 3]}>
30+ {patp}{' '}
31+ </Grid.Item>
32+ <Grid.Item className="flex-row align-center" cols={[3, 6]}>
33+ <div
34+ style={{
35+ display: 'inline-block',
36+ height: `${sigilSize}px`,
37+ width: `${sigilSize}px`,
38+ }}>
39+ <Sigil patp={patp} size={25} colors={['#FFFFFF', '#000000']} />
40+ </div>
41+ </Grid.Item>
42+ <Grid.Item className="flex-row align-center" cols={[6, 10]}>
43+ {keyType}
44+ </Grid.Item>
45+ <Grid.Item
46+ onClick={onClick}
47+ cols={[10, 13]}
48+ className="flex-row-r align-center mr2">
49+ ->
50+ </Grid.Item>
51+ <Grid.Divider />
52+ </>
53+ );
54+}
55+
56+export default function ActiveView({ className, goPoint }) {
57+ // const points = [2560, 2561, 2562];
58+ const { controlledPoints } = usePointCache();
59+ console.log(controlledPoints);
60+ const points = controlledPoints
61+ .matchWith({
62+ Nothing: () => [],
63+ Just: ps =>
64+ ps.value.matchWith({
65+ Error: () => null,
66+ Ok: ({ value }) => {
67+ return [
68+ ...value.ownedPoints,
69+ ...value.votingPoints,
70+ ...value.managingPoints,
71+ ...value.spawningPoints,
72+ ];
73+ },
74+ }),
75+ })
76+ .filter(Boolean)
77+ .filter(p => 255 < p && p < 65536);
78+
79+ return (
80+ <Grid full className={cn('f5', className)}>
81+ <Grid.Item className="pv3 " cols={[1, 6]} third={1}>
82+ Point
83+ </Grid.Item>
84+ <Grid.Item className="pv3 " cols={[6, 10]}>
85+ Key Type
86+ </Grid.Item>
87+ <Grid.Divider />
88+ {points.length === 0 && (
89+ <Grid.Item full className="flex-center pv8 f5 gray4">
90+ You have no active stars
91+ </Grid.Item>
92+ )}
93+ {points.map(point => (
94+ <ActiveViewRow
95+ key={point}
96+ goPoint={goPoint}
97+ point={point}></ActiveViewRow>
98+ ))}
99+ </Grid>
100+ );
101+}
src/views/Release/Locked.jsnew file+179
@@ -0,0 +1,179 @@
1+import React, { useCallback, useEffect, useMemo } from 'react';
2+import cn from 'classnames';
3+import { Grid, Flex } from 'indigo-react';
4+import { Just } from 'folktale/maybe';
5+
6+import * as need from 'lib/need';
7+import useEthereumTransaction from 'lib/useEthereumTransaction';
8+import pluralize from 'lib/pluralize';
9+
10+import { useWallet } from 'store/wallet';
11+import { usePointCache } from 'store/pointCache';
12+import { useStarReleaseCache } from 'store/starRelease';
13+
14+import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
15+import { matchBlinky } from 'components/Blinky';
16+
17+import { NumberInput, AddressInput } from 'form/Inputs';
18+import BridgeForm from 'form/BridgeForm';
19+import FormError from 'form/FormError';
20+import { composeValidator, buildNumberValidator } from 'form/validators';
21+import { convertToNumber } from 'form/formatters';
22+
23+function MatchPluralizeStars({ quantity }) {
24+ const _quantity = quantity.getOrElse(0);
25+ console.log(quantity);
26+ return (
27+ <>
28+ {' '}
29+ <span
30+ className={cn({
31+ green2: _quantity > 0,
32+ red4: Just.hasInstance(quantity) && _quantity === 0,
33+ })}>
34+ {matchBlinky(quantity)}
35+ </span>
36+ {pluralize(_quantity, ' star ', ' stars ')}
37+ </>
38+ );
39+}
40+
41+function AddressIndicator({ isOwn, className }) {
42+ return isOwn ? (
43+ <div className={cn('green2', className)}>✓ Current Address</div>
44+ ) : (
45+ <div className={cn('red4', className)}>! Different address</div>
46+ );
47+}
48+
49+function useWithdrawStars() {
50+ const { syncControlledPoints } = usePointCache();
51+
52+ const { syncStarReleaseDetails, withdraw } = useStarReleaseCache();
53+ return useEthereumTransaction(
54+ useCallback((to, amount) => withdraw(amount, to), [withdraw]),
55+ useCallback(
56+ () => Promise.all([syncControlledPoints(), syncStarReleaseDetails()]),
57+ [syncStarReleaseDetails, syncControlledPoints]
58+ )
59+ );
60+}
61+
62+export default function Locked({ className, goActive }) {
63+ const { wallet } = useWallet();
64+ const address = need.addressFromWallet(wallet);
65+ const {
66+ bind,
67+ construct,
68+ unconstruct,
69+ inputsLocked,
70+ completed,
71+ reset,
72+ } = useWithdrawStars();
73+
74+ const { syncStarReleaseDetails, starReleaseDetails } = useStarReleaseCache();
75+
76+ useEffect(() => {
77+ syncStarReleaseDetails();
78+ }, [syncStarReleaseDetails]);
79+
80+ const onValues = useCallback(
81+ ({ valid, values }) => {
82+ if (valid) {
83+ construct(values.address, convertToNumber(values.numStars));
84+ } else {
85+ unconstruct();
86+ }
87+ },
88+ [construct, unconstruct]
89+ );
90+
91+ const canWithdraw = starReleaseDetails.map(b => b.available - b.withdrawn);
92+ const total = starReleaseDetails.map(b => b.total);
93+
94+ const validate = useMemo(
95+ () =>
96+ composeValidator(
97+ {
98+ numStars: buildNumberValidator(0, canWithdraw + 1),
99+ },
100+ (values, errors) => (inputsLocked ? false : errors)
101+ ),
102+ [canWithdraw, inputsLocked]
103+ );
104+
105+ const initialValues = { address };
106+
107+ return (
108+ <Grid gap={completed ? 0 : 6} className={cn('mt4', className)} full>
109+ <BridgeForm
110+ initialValues={initialValues}
111+ onValues={onValues}
112+ validate={validate}>
113+ {({ values, form }) => (
114+ <>
115+ {!completed && (
116+ <>
117+ <Grid.Item full className="f5">
118+ You have <MatchPluralizeStars quantity={canWithdraw} />
119+ available to withdraw
120+ <br />
121+ You have
122+ <MatchPluralizeStars quantity={total} />
123+ total
124+ </Grid.Item>
125+ <Grid.Item
126+ cols={[1, 4]}
127+ label="Stars"
128+ as={NumberInput}
129+ placeholder="#"
130+ name="numStars"
131+ disabled={inputsLocked}
132+ accessory={
133+ <div className="flex-center h-full">
134+ / &nbsp;{matchBlinky(canWithdraw)}
135+ </div>
136+ }
137+ />
138+ <Grid.Item full className="f5" cols={[1, 13]} as={Flex} col>
139+ <Flex.Item
140+ name="address"
141+ as={AddressInput}
142+ label="Withdraw To"
143+ disabled={inputsLocked}
144+ />
145+ <Flex.Item
146+ as={AddressIndicator}
147+ isOwn={address === values.address}
148+ />
149+ </Grid.Item>
150+ </>
151+ )}
152+
153+ <Grid.Item full as={FormError} />
154+ {completed && (
155+ <>
156+ <Grid.Item full className="pv4">
157+ {values.numStars}{' '}
158+ {pluralize(values.numStars, 'star has', 'stars have')} been
159+ withdrawn
160+ </Grid.Item>
161+ <Grid.Divider />
162+ </>
163+ )}
164+ <Grid.Item
165+ full
166+ as={InlineEthereumTransaction}
167+ {...bind}
168+ label="Withdraw Stars"
169+ onReturn={() => {
170+ reset();
171+ form.reset();
172+ }}
173+ />
174+ </>
175+ )}
176+ </BridgeForm>
177+ </Grid>
178+ );
179+}
src/views/Senate.jsnew file+230
@@ -0,0 +1,230 @@
1+import React, { useCallback, useMemo, useState } from 'react';
2+import { Grid, Text, P, LinkButton } from 'indigo-react';
3+import * as azimuth from 'azimuth-js';
4+
5+import { useNetwork } from 'store/network';
6+import { usePointCursor } from 'store/pointCursor';
7+
8+import * as need from 'lib/need';
9+import useEthereumTransaction from 'lib/useEthereumTransaction';
10+import { GAS_LIMITS } from 'lib/constants';
11+import { useLocalRouter } from 'lib/LocalRouter';
12+
13+import { keccak256 } from 'lib/wallet';
14+
15+import ViewHeader from 'components/ViewHeader';
16+import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
17+import View from 'components/View';
18+import BridgeForm from 'form/BridgeForm';
19+import convertToInt from 'lib/convertToInt';
20+
21+function useCastDocumentVote() {
22+ const { contracts } = useNetwork();
23+ const { pointCursor } = usePointCursor();
24+
25+ const _contracts = need.contracts(contracts);
26+ const _point = need.point(pointCursor);
27+
28+ return useEthereumTransaction(
29+ useCallback(
30+ (hash, accept) => {
31+ return azimuth.ecliptic.castDocumentVote(
32+ _contracts,
33+ _point,
34+ hash,
35+ accept
36+ );
37+ },
38+ [_contracts, _point]
39+ ),
40+ useCallback(() => null, []), //TODO update polls[hash].hasVoted ?
41+ GAS_LIMITS.DEFAULT //TODO update once we have a decent sample size
42+ );
43+}
44+
45+export default function Senate() {
46+ const { pop } = useLocalRouter();
47+ const { contracts } = useNetwork();
48+ const { pointCursor } = usePointCursor();
49+
50+ const _contracts = need.contracts(contracts);
51+ const _point = convertToInt(need.point(pointCursor), 10);
52+
53+ const { construct, bind } = useCastDocumentVote();
54+
55+ // const [documentHash, setDocumentHash] = useState('0x...');
56+ const [majorities, setMajorities] = useState([]);
57+ const [proposals, setProposals] = useState([]);
58+ const [polls, setPolls] = useState({});
59+ const [loading, setLoading] = useState(true);
60+ const [votingOn, setVotingOn] = useState({});
61+
62+ //TODO update every second?
63+ const now = Math.round(new Date().getTime() / 1000);
64+
65+ useMemo(async () => {
66+ let proposals = await azimuth.polls.getDocumentProposals(_contracts);
67+ let majorities = await azimuth.polls.getDocumentMajorities(_contracts);
68+ let polls = {};
69+ for (let doc of proposals) {
70+ let poll = await azimuth.polls.getDocumentPoll(_contracts, doc);
71+ poll.endTime = convertToInt(poll.start) + convertToInt(poll.duration);
72+ poll.hasVoted = await azimuth.polls.hasVotedOnDocumentPoll(
73+ _contracts,
74+ _point,
75+ doc
76+ );
77+ polls[doc] = poll;
78+ }
79+ setMajorities(majorities);
80+ setProposals(proposals);
81+ setPolls(polls);
82+ setLoading(false);
83+ return;
84+ }, [_contracts, _point]);
85+
86+ const [open, closed] = useMemo(() => {
87+ let open = [];
88+ let closed = [];
89+ if (loading) return [open, closed];
90+ for (let doc of proposals) {
91+ if (now > polls[doc].endTime) {
92+ closed.push(doc);
93+ } else {
94+ open.push(doc);
95+ }
96+ }
97+ return [open, closed];
98+ }, [loading, proposals, polls, now]);
99+
100+ const doVote = useCallback(
101+ (hash, accept) => {
102+ construct(hash, accept);
103+ setVotingOn({ hash, accept });
104+ },
105+ [construct, setVotingOn]
106+ );
107+
108+ const didVote = useCallback(() => {
109+ polls[votingOn].hasVoted = true;
110+ setPolls(polls);
111+ setVotingOn({});
112+ }, [polls, setPolls, votingOn]);
113+
114+ const majorityList = useMemo(() => {
115+ return majorities.map(doc => {
116+ return (
117+ <Grid.Item full as={Text}>
118+ <a
119+ target="_blank"
120+ rel="noopener noreferrer"
121+ href={`https://raw.githubusercontent.com/urbit/azimuth/master/proposals/${doc}.txt`}>
122+ <code>{doc}</code>↗
123+ </a>
124+ </Grid.Item>
125+ );
126+ });
127+ }, [majorities]);
128+
129+ const openList = useMemo(() => {
130+ return open.map(doc => {
131+ const hasVoted = polls[doc].hasVoted;
132+ return (
133+ <>
134+ <Grid.Item full as={Text}>
135+ <P>
136+ <code>{doc}</code>
137+ </P>
138+ <P>
139+ {hasVoted ? (
140+ 'Your vote has been cast.'
141+ ) : (
142+ <>
143+ <LinkButton onClick={() => doVote(doc, true)}>
144+ {votingOn.hash === doc && votingOn.accept ? (
145+ <b>support</b>
146+ ) : (
147+ <>support</>
148+ )}
149+ </LinkButton>
150+ {' / '}
151+ <LinkButton onClick={() => doVote(doc, false)}>
152+ {votingOn.hash === doc && !votingOn.accept ? (
153+ <b>reject</b>
154+ ) : (
155+ <>reject</>
156+ )}
157+ </LinkButton>
158+ </>
159+ )}
160+ </P>
161+ </Grid.Item>
162+ <Grid.Divider />
163+ </>
164+ );
165+ });
166+ }, [open, polls, votingOn, doVote]);
167+
168+ const onDocumentChange = useCallback(({ valid, values }) => {
169+ // if (values.document) {
170+ // const hash = '0x' + keccak256(values.document.trim()).toString('hex');
171+ // setDocumentHash(hash);
172+ // } else {
173+ // setDocumentHash('0x...');
174+ // }
175+ }, []);
176+
177+ return (
178+ <View pop={pop} inset>
179+ <Grid>
180+ <Grid.Item full as={ViewHeader}>
181+ Senate: Document Proposals
182+ </Grid.Item>
183+
184+ <Grid.Item full as={Grid}>
185+ <Grid.Item full as={P}>
186+ Accepted documents:
187+ </Grid.Item>
188+ {majorityList}
189+ </Grid.Item>
190+
191+ <Grid.Divider />
192+
193+ <Grid.Item full as={Grid}>
194+ <Grid.Item full as={P}>
195+ Open document polls:
196+ </Grid.Item>
197+ {openList}
198+ </Grid.Item>
199+
200+ <BridgeForm onValues={onDocumentChange}>
201+ {({ onSubmit, values }) => (
202+ // <>
203+ <Grid.Item
204+ full
205+ as={InlineEthereumTransaction}
206+ {...bind}
207+ onReturn={didVote}
208+ />
209+
210+ // <Grid.Divider />
211+ //
212+ // <Grid.Item
213+ // full
214+ // as={Input}
215+ // type="textarea"
216+ // placeholder="Input some text to find its hash..."
217+ // name="document"
218+ // className="mt4"
219+ // />
220+ //
221+ // <Grid.Item full as={CopiableAddress}>
222+ // {documentHash}
223+ // </Grid.Item>
224+ // </>
225+ )}
226+ </BridgeForm>
227+ </Grid>
228+ </View>
229+ );
230+}