on master help sign in
Merge branch 'mino' into feat/indigo-login
commitb0440cfd9756b5aaff7c307f3c8f1d523c1b6171 authorMatt Condon (shrugs) <mattgcondon@gmail.com> dateMon 8 Jul 2019, 11:13 (7 years ago) parent82b591460d3cb7
29 files changed+1034−111
find-gas-usage.js+114 src/components/PaperRenderer.js+13 src/components/old/Passport.js+4−5 src/lib/constants.js+19 src/lib/invite.js+100−76 src/lib/routeNames.js+4 src/lib/router.js+8 src/lib/txn.js+1−1 src/lib/wallet.js+2 src/store/lib/useBirthdaysStore.js+1−1 src/store/lib/usePointStore.js+5−2 src/store/lib/useRekeyDateStore.js+59 src/views/Activate/ActivateCode.js+6−1 src/views/Activate/PassportDownload.js+4−5 src/views/Activate/PassportTransfer.js+1 src/views/Admin.js+84 src/views/CreateGalaxy.js+2−2 src/views/Invite/InviteEmail.js+2−2 src/views/Permissions.js+153 src/views/Point.js+28−6 src/views/Point/Actions.js+2−2 src/views/Point/KeysAndMetadata.js+3−6 src/views/Points.js+2−2 src/views/Redownload.js+52 src/views/Reticket.js+97 src/views/Reticket/Confirm.js+63 src/views/Reticket/DoReticket.js+128 src/views/Reticket/Download.js+35 src/views/Reticket/Verify.js+42
find-gas-usage.jsnew file+114
@@ -0,0 +1,114 @@
1+// usage: tweak constants at head of file, then run "node find-gas-usage.js"
2+// for very common events/calls, you may need to use a smaller TO-FROM
3+// range to keep web3 from yelling at you
4+
5+const ORIGIN = 6784800; // launch ceremony begins
6+const PUBLIC = 7033765; // azimuth becomes public
7+const LATEST = 'latest';
8+const AZIMUTH_ADDRESS = '0x223c067f8cf28ae173ee5cafea60ca44c335fecb';
9+const ENDPOINT =
10+ 'https://mainnet.infura.io/v3/196a7f37c7d54211b4a07904ec73ad87';
11+
12+const FROM = ORIGIN;
13+const TO = LATEST;
14+
15+const VENT = 'ChangedKeys';
16+const FUNC = '0x4447e48c'; // configureKeys
17+const ARGS = 5;
18+
19+// const VENT = 'Spawned';
20+// const FUNC = '0xa0d3253f'; // spawn
21+// const ARGS = 2;
22+
23+// const VENT = 'OwnerChanged';
24+// const FUNC = '0x1e79a85b'; // transferPoint
25+// const ARGS = 3;
26+
27+// const VENT = 'ChangedSpawnProxy';
28+// const FUNC = '0xae326221'; // setSpawnProxy
29+// const ARGS = 2;
30+
31+// const VENT = 'ChangedManagementProxy';
32+// const FUNC = '0x8866bb2c'; // setManagementProxy
33+// const ARGS = 2;
34+
35+// const VENT = 'ChangedTransferProxy';
36+// const FUNC = '0x2c7ba564'; // setTransferProxy
37+// const ARGS = 2;
38+
39+// const VENT = 'ChangedVotingProxy';
40+// const FUNC = '0xa60e8bd6'; // setVotingProxy
41+// const ARGS = 2;
42+
43+// const VENT = 'EscapeRequested';
44+// const FUNC = '0xbf5772b9'; // escape
45+// const ARGS = 2;
46+
47+// const VENT = 'EscapeCanceled';
48+// const FUNC = '0xc6d761d4'; // cancelEscape
49+// const ARGS = 1;
50+
51+// const VENT = 'EscapeAccepted';
52+// const FUNC = '0xc1b9d98b'; // adopt
53+// const ARGS = 1;
54+
55+// const VENT = 'EscapeCanceled';
56+// const FUNC = '0xbbe21ca5'; // reject
57+// const ARGS = 1;
58+
59+// const VENT = 'LostSponsor';
60+// const FUNC = '0x073a7804'; // detach
61+// const ARGS = 1;
62+
63+const LENT = 64 * ARGS + FUNC.length;
64+
65+const Web3 = require('web3');
66+const azimuth = require('azimuth-js');
67+
68+(async () => {
69+ const web3 = new Web3(new Web3.providers.HttpProvider(ENDPOINT));
70+ const contracts = await azimuth.initContractsPartial(web3, AZIMUTH_ADDRESS);
71+
72+ const keyEvents = await contracts.azimuth.getPastEvents(VENT, {
73+ fromBlock: FROM,
74+ toBlock: TO,
75+ });
76+
77+ const txsBatch = new web3.BatchRequest();
78+ keyEvents.map(vent => {
79+ txsBatch.add(
80+ web3.eth.getTransaction.request(vent.transactionHash, (err, res) => {})
81+ );
82+ });
83+ const txs = await txsBatch.execute();
84+ const keyTxs = txs.response.filter(tx => {
85+ const head = tx.input.slice(0, 10) === FUNC;
86+ const size = tx.input.length === LENT;
87+ if (head && !size) {
88+ console.log('weird inputs', tx.hash);
89+ }
90+ return head && size;
91+ });
92+
93+ const receiptsBatch = new web3.BatchRequest();
94+ keyTxs.map(tx => {
95+ receiptsBatch.add(
96+ web3.eth.getTransactionReceipt.request(tx.hash, (err, res) => {})
97+ );
98+ });
99+ const recs = await receiptsBatch.execute();
100+
101+ let usage = {};
102+ recs.response.map(rec => {
103+ if (rec.status !== true) {
104+ console.log('rejecting', rec.gasUsed);
105+ return;
106+ }
107+ if (rec.gasUsed in usage) {
108+ usage[rec.gasUsed]++;
109+ } else {
110+ usage[rec.gasUsed] = 1;
111+ }
112+ });
113+ console.log(usage);
114+})();
src/components/PaperRenderer.jsnew file+13
@@ -0,0 +1,13 @@
1+import React from 'react';
2+import PaperCollateralRenderer from 'PaperCollateralRenderer';
3+
4+export default function PaperRenderer({ point, wallet, callback, ...props }) {
5+ return (
6+ <PaperCollateralRenderer
7+ wallet={{ [point]: wallet }}
8+ callback={callback}
9+ className="super-hidden"
10+ mode="REGISTRATION"
11+ />
12+ );
13+}
src/components/old/Passport.js+4−5
@@ -3,8 +3,8 @@ import { pour } from 'sigil-js';
33import * as ob from 'urbit-ob';
44import ReactSVGComponents from '../ReactSVGComponents';
55import PropTypes from 'prop-types';
6−import PaperCollateralRenderer from 'PaperCollateralRenderer';
76import { Button } from './Base';
7+import PaperRenderer from 'components/PaperRenderer';
88
99import { WALLET_STATES } from '../../lib/invite';
1010import { downloadWallet } from '../../lib/invite';
@@ -159,14 +159,13 @@ class Passport extends React.Component {
159159 );
160160
161161 let paperCollateral = wallet ? (
162− <PaperCollateralRenderer
163− wallet={{ [pointVal]: wallet }}
164− className={'extremely-hidden'}
162+ <PaperRenderer
163+ point={pointVal}
164+ wallet={wallet}
165165 callback={data => {
166166 this.setState({ paper: data });
167167 this.props.pushWalletState(WALLET_STATES.PAPER_READY);
168168 }}
169− mode={'REGISTRATION'}
170169 />
171170 ) : (
172171 <div />
src/lib/constants.js+19
@@ -13,6 +13,24 @@ const GALAXY_ENTROPY_BITS = 384;
1313
1414const SEED_ENTROPY_BITS = 128;
1515
16+//TODO move into azimuth-js
17+const GAS_LIMITS = {
18+ SPAWN: 250000,
19+ TRANSFER: 490000,
20+ CONFIGURE_KEYS: 100000,
21+ SET_PROXY: 119000,
22+ //
23+ ESCAPE: 115000, //NOTE low sample size
24+ ADOPT: 100000, //NOTE low sample size
25+ CANCEL_ESCAPE: 200000, //NOTE no samples
26+ REJECT: 200000, //NOTE no samples
27+ DETACH: 200000, //NOTE no samples
28+ //
29+ GIFT_PLANET: 400000, //NOTE low sample size
30+ //
31+ DEFAULT: 500000,
32+};
33+
1634// TODO: this is walletgen-ui specific, move into a wallet router later
1735const GEN_STATES = {
1836 ENY_NOSTART: Symbol('ENY_NOSTART'),
@@ -45,6 +63,7 @@ const PROFILE_STATES = {
4563};
4664
4765export {
66+ GAS_LIMITS,
4867 GEN_STATES,
4968 BUTTON_STATES,
5069 PROFILE_STATES,
src/lib/invite.js+100−76
@@ -12,6 +12,7 @@ import saveAs from 'file-saver';
1212import { sendSignedTransaction, hexify } from './txn';
1313import { attemptNetworkSeedDerivation } from './keys';
1414import { addHexPrefix, WALLET_TYPES } from './wallet';
15+import { GAS_LIMITS } from './constants';
1516
1617const INVITE_STAGES = {
1718 INVITE_LOGIN: 'invite login',
@@ -38,22 +39,14 @@ const TRANSACTION_STATES = {
3839 label: 'Signing Transactions...',
3940 progress: 0.15,
4041 },
41− FUNDING_INVITE: {
42− label: 'Funding Invite Wallet...',
42+ FUNDING: {
43+ label: 'Funding Transactions...',
4344 progress: 0.3,
4445 },
4546 CLAIMING: {
4647 label: 'Claiming Invite...',
4748 progress: 0.55,
4849 },
49− FUNDING_RECIPIENT: {
50− label: 'Funding Recipient Wallet...',
51− progress: 0.65,
52− },
53− CONFIGURING: {
54− label: 'Configuring Point...',
55− progress: 0.85,
56− },
5750 CLEANING: {
5851 label: 'Cleaning Up...',
5952 progress: 0.95,
@@ -108,6 +101,7 @@ async function claimPointFromInvite({
108101 web3,
109102 contracts,
110103 onUpdate,
104+ transferEth = false,
111105}) {
112106 const askForFunding = (address, minBalance, balance) =>
113107 onUpdate({
@@ -120,62 +114,29 @@ async function claimPointFromInvite({
120114
121115 const inviteAddress = inviteWallet.address;
122116
123− progress(TRANSACTION_STATES.GENERATING);
124−
125− // transfer from invite wallet to new wallet
126− const transferTx = azimuth.ecliptic.transferPoint(
127− contracts,
128− point,
129− wallet.ownership.keys.address
130− );
131− transferTx.gas = 500000; //TODO can maybe be lower?
132−
133− // ping gas tank with txs if needed
134− let inviteNonce = await web3.eth.getTransactionCount(inviteAddress);
135− transferTx.nonce = inviteNonce++;
136− transferTx.gasPrice = 20000000000; //NOTE we pay the premium for faster ux
137− transferTx.from = inviteAddress;
138−
139− progress(TRANSACTION_STATES.SIGNING);
140− // NOTE: using web3.eth.accounts.signTransaction is broken (1.0.0-beta51)
141− const transferStx = new Tx(transferTx);
142− transferStx.sign(inviteWallet.privateKey);
143− const rawTransferStx = hexify(transferStx.serialize());
144− const transferCost = transferTx.gas * transferTx.gasPrice;
145−
146− progress(TRANSACTION_STATES.FUNDING_INVITE);
147−
148− let usedTank = await tank.ensureFundsFor(
149− web3,
150− point,
151− inviteAddress,
152− transferCost,
153− [rawTransferStx],
154− askForFunding,
155− gotFunding
156− );
157−
158− progress(TRANSACTION_STATES.CLAIMING);
159−
160− // send transaction
161− await sendAndAwaitConfirm(web3, [Just(transferStx)], usedTank);
162−
163117 //
164− // we're gonna be operating as the new wallet from here on out, so change
165− // the relevant values
118+ // generate transactions
166119 //
167− const newAddress = wallet.ownership.keys.address;
168120
169− // configure management proxy
170− const managementTx = azimuth.ecliptic.setManagementProxy(
121+ progress(TRANSACTION_STATES.GENERATING);
122+
123+ // transfer to invite wallet, so it can configure
124+ //NOTE doing this in the invite wallet instead of the final wallet makes
125+ // failure cases "solvable" by just telling the user to fully retry.
126+ // in other words, the invite flow is effectively idempotent.
127+
128+ //TODO no harm done if we already owned it, but should still get a bool arg
129+ // for skipping this, if it isn't too big a burden for callers
130+ const transferTmpTx = azimuth.ecliptic.transferPoint(
171131 contracts,
172132 point,
173− wallet.management.keys.address
133+ inviteAddress,
134+ false
174135 );
175− managementTx.gas = 200000;
176− managementTx.nonce = 0;
136+ transferTmpTx.gas = GAS_LIMITS.TRANSFER;
177137
178138 // configure networking public keys
139+
179140 //TODO feels like more of this logic should live in a lib?
180141 const seed = await attemptNetworkSeedDerivation(true, {
181142 walletType: WALLET_TYPES.TICKET,
@@ -193,7 +154,7 @@ async function claimPointFromInvite({
193154
194155 const networkKeys = kg.deriveNetworkKeys(networkSeed);
195156
196− let keysTx = azimuth.ecliptic.configureKeys(
157+ const keysTx = azimuth.ecliptic.configureKeys(
197158 contracts,
198159 point,
199160 addHexPrefix(networkKeys.crypt.public),
@@ -201,43 +162,108 @@ async function claimPointFromInvite({
201162 1,
202163 false
203164 );
204− keysTx.gas = 150000;
205− keysTx.nonce = 1;
165+ keysTx.gas = GAS_LIMITS.CONFIGURE_KEYS;
166+
167+ // configure management proxy
168+
169+ const managementTx = azimuth.ecliptic.setManagementProxy(
170+ contracts,
171+ point,
172+ wallet.management.keys.address
173+ );
174+ managementTx.gas = GAS_LIMITS.SET_PROXY;
175+
176+ // set spawn & voting proxies situationally
177+
178+ let txs = [transferTmpTx, keysTx, managementTx];
179+ if (
180+ azimuth.azimuth.getPointSize(point) !== azimuth.azimuth.PointSize.Planet
181+ ) {
182+ const spawnTx = azimuth.ecliptic.setSpawnProxy(
183+ contracts,
184+ point,
185+ wallet.spawn.keys.address
186+ );
187+ spawnTx.gas = GAS_LIMITS.SET_PROXY;
188+ txs.push(spawnTx);
189+
190+ if (
191+ azimuth.azimuth.getPointSize(point) === azimuth.azimuth.PointSize.Galaxy
192+ ) {
193+ const votingTx = azimuth.ecliptic.setVotingProxy(
194+ contracts,
195+ point,
196+ wallet.voting.keys.address
197+ );
198+ votingTx.gas = GAS_LIMITS.SET_PROXY;
199+ txs.push(votingTx);
200+ }
201+ }
202+
203+ // transfer configured point to user's new wallet
204+
205+ const transferFinalTx = azimuth.ecliptic.transferPoint(
206+ contracts,
207+ point,
208+ wallet.ownership.keys.address,
209+ false
210+ );
211+ transferFinalTx.gas = GAS_LIMITS.TRANSFER;
212+ txs.push(transferFinalTx);
213+
214+ //
215+ // finalizing & signing transactions
216+ //
217+
218+ progress(TRANSACTION_STATES.SIGNING);
206219
207− //TODO refactor this process into function
208220 let totalCost = 0;
209− let txs = [managementTx, keysTx];
221+ let inviteNonce = await web3.eth.getTransactionCount(inviteAddress);
210222 txs = txs.map(tx => {
211− tx.gasPrice = 20000000000;
212− tx.from = newAddress;
223+ tx.from = inviteAddress;
224+ tx.nonce = inviteNonce++;
225+ tx.gasPrice = 20000000000; //NOTE we pay the premium for faster ux
213226 totalCost = totalCost + tx.gasPrice * tx.gas;
214227 return tx;
215228 });
216229
217230 let txPairs = txs.map(tx => {
218231 let stx = new Tx(tx);
219− stx.sign(Buffer.from(wallet.ownership.keys.private, 'hex'));
232+ stx.sign(Buffer.from(inviteWallet.privateKey, 'hex'));
220233 return {
221− raw: '0x' + stx.serialize().toString('hex'),
234+ raw: hexify(stx.serialize()),
222235 signed: stx,
223236 };
224237 });
225238
226− progress(TRANSACTION_STATES.FUNDING_RECIPIENT);
239+ //
240+ // ensuring funding for transactions
241+ //
242+
243+ progress(TRANSACTION_STATES.FUNDING);
227244
228− usedTank = await tank.ensureFundsFor(
245+ const usedTank = await tank.ensureFundsFor(
229246 web3,
230247 point,
231− newAddress,
248+ inviteAddress,
232249 totalCost,
233250 txPairs.map(p => p.raw),
234− askForFunding
251+ askForFunding,
252+ gotFunding
235253 );
236254
237− progress(TRANSACTION_STATES.CONFIGURING);
255+ //
256+ // sending and awaiting transactions
257+ //
258+
259+ progress(TRANSACTION_STATES.CLAIMING);
238260
239261 await sendAndAwaitConfirm(web3, txPairs.map(p => Just(p.signed)), usedTank);
240262
263+ //
264+ // move leftover eth
265+ //
266+
241267 progress(TRANSACTION_STATES.CLEANING);
242268
243269 // if non-trivial eth left in invite wallet, transfer to new ownership
@@ -245,11 +271,10 @@ async function claimPointFromInvite({
245271 const gasPrice = 20000000000;
246272 const gasLimit = 21000;
247273 const sendEthCost = gasPrice * gasLimit;
248− if (balance > sendEthCost) {
274+ if (transferEth && balance > sendEthCost) {
249275 const value = balance - sendEthCost;
250− console.log('sending', value);
251276 const tx = {
252− to: newAddress,
277+ to: wallet.ownership.keys.address,
253278 value: value,
254279 gasPrice: gasPrice,
255280 gas: gasLimit,
@@ -261,7 +286,6 @@ async function claimPointFromInvite({
261286 web3.eth.sendSignedTransaction(rawTx).catch(err => {
262287 console.log('error sending value tx, who cares', err);
263288 });
264− console.log('sent old balance');
265289 }
266290
267291 progress(TRANSACTION_STATES.DONE);
src/lib/routeNames.js+4
@@ -7,6 +7,10 @@ export const ROUTE_NAMES = {
77 INVITES_MANAGE: Symbol('INVITES_MANAGE'),
88 //
99 LOGIN: Symbol('LOGIN'),
10+ ADMIN: Symbol('ADMIN'),
11+ REDOWNLOAD: Symbol('REDOWNLOAD'),
12+ RETICKET: Symbol('RETICKET'),
13+ PERMISSIONS: Symbol('PERMISSIONS'),
1014 //
1115 ACCEPT_TRANSFER: Symbol('ACCEPT_TRANSFER'),
1216 CANCEL_TRANSFER: Symbol('CANCEL_TRANSFER'),
src/lib/router.js+8
@@ -1,6 +1,10 @@
11import Landing from '../views/Landing';
22import Login from '../views/Login.js';
33import InvitesManage from '../views/InvitesManage.js';
4+import Admin from '../views/Admin';
5+import Redownload from '../views/Redownload';
6+import Reticket from '../views/Reticket';
7+import Permissions from '../views/Permissions';
48import AcceptTransfer from '../views/AcceptTransfer';
59import CancelTransfer from '../views/CancelTransfer';
610import CreateGalaxy from '../views/CreateGalaxy';
@@ -28,6 +32,10 @@ export const ROUTES = {
2832 [ROUTE_NAMES.INVITE]: Invite,
2933 [ROUTE_NAMES.INVITES_MANAGE]: InvitesManage,
3034 [ROUTE_NAMES.LOGIN]: Login,
35+ [ROUTE_NAMES.ADMIN]: Admin,
36+ [ROUTE_NAMES.REDOWNLOAD]: Redownload,
37+ [ROUTE_NAMES.RETICKET]: Reticket,
38+ [ROUTE_NAMES.PERMISSIONS]: Permissions,
3139 [ROUTE_NAMES.ACCEPT_TRANSFER]: AcceptTransfer,
3240 [ROUTE_NAMES.CANCEL_TRANSFER]: CancelTransfer,
3341 [ROUTE_NAMES.VIEW_POINT]: ViewPoint,
src/lib/txn.js+1−1
@@ -144,7 +144,7 @@ const sendSignedTransaction = (web3, stx, doubtNonceError, confirmationCb) => {
144144 },
145145 });
146146
147− const rawTx = addHexPrefix(txn.serialize().toString('hex'));
147+ const rawTx = hexify(txn.serialize());
148148
149149 return new Promise(async (resolve, reject) => {
150150 web3.eth
src/lib/wallet.js+2
@@ -55,6 +55,8 @@ export const keccak256 = str =>
5555
5656export const isValidAddress = a => '0x0' === a || isAddress(a);
5757
58+export const isZeroAddress = a => ETH_ZERO_ADDR === a;
59+
5860export const toChecksumAddress = address => {
5961 const addr = stripHexPrefix(address).toLowerCase();
6062 const hash = keccak256(addr).toString('hex');
src/store/lib/useBirthdaysStore.js+1−1
@@ -26,7 +26,7 @@ export default function useBirthdayStore() {
2626 async point => {
2727 const _contracts = contracts.getOrElse(null);
2828 const _web3 = web3.getOrElse(null);
29− if (!_contracts || !web3) {
29+ if (!_contracts || !_web3) {
3030 return;
3131 }
3232
src/store/lib/usePointStore.js+5−2
@@ -2,6 +2,7 @@ import { useCallback } from 'react';
22
33import useDetailsStore from './useDetailsStore';
44import useBirthdaysStore from './useBirthdaysStore';
5+import useRekeyDateStore from './useRekeyDateStore';
56import useInvitesStore from './useInvitesStore';
67import useControlledPointsStore from './useControlledPointsStore';
78import useEclipticOwnerStore from './useEclipticOwnerStore';
@@ -9,6 +10,7 @@ import useEclipticOwnerStore from './useEclipticOwnerStore';
910export default function usePointStore() {
1011 const { syncDetails, ...details } = useDetailsStore();
1112 const { syncBirthday, ...birthdays } = useBirthdaysStore();
13+ const { syncRekeyDate, ...rekeyDates } = useRekeyDateStore();
1214 const { syncInvites, ...invites } = useInvitesStore();
1315 const ecliptic = useEclipticOwnerStore();
1416 const {
@@ -18,8 +20,8 @@ export default function usePointStore() {
1820
1921 // sync all of the on-chain info required to display a known point
2022 const syncKnownPoint = useCallback(
21− async point => Promise.all([syncBirthday(point)]),
22− [syncBirthday]
23+ async point => Promise.all([syncBirthday(point), syncRekeyDate(point)]),
24+ [syncBirthday, syncRekeyDate]
2325 );
2426
2527 // sync all of the on-chain info required to display a foreign point
@@ -39,6 +41,7 @@ export default function usePointStore() {
3941 return {
4042 ...details,
4143 ...birthdays,
44+ ...rekeyDates,
4245 ...invites,
4346 ...controlledPoints,
4447 ...ecliptic,
src/store/lib/useRekeyDateStore.jsnew file+59
@@ -0,0 +1,59 @@
1+import { useCallback, useState } from 'react';
2+import { Just, Nothing } from 'folktale/maybe';
3+import * as azimuth from 'azimuth-js';
4+
5+import { useNetwork } from '../network';
6+
7+export default function useRekeyDateStore() {
8+ const { web3 } = useNetwork();
9+ const [rekeyDateCache, _setRekeyDateCache] = useState({});
10+
11+ const addToRekeyDateCache = useCallback(
12+ entry =>
13+ _setRekeyDateCache(cache => ({
14+ ...cache,
15+ ...entry,
16+ })),
17+ [_setRekeyDateCache]
18+ );
19+
20+ const getRekeyDate = useCallback(
21+ point => rekeyDateCache[point] || Nothing(),
22+ [rekeyDateCache]
23+ );
24+
25+ const syncRekeyDate = useCallback(
26+ async point => {
27+ const _web3 = web3.getOrElse(null);
28+ if (!_web3) {
29+ return;
30+ }
31+
32+ //TODO tighter search?
33+ //TODO maybe move into azimuth-js?
34+ const logs = await azimuth.azimuth.getPastEvents('ChangedKeys', {
35+ fromBlock: 0,
36+ toBlock: 'latest',
37+ filter: { point: [point] },
38+ });
39+
40+ if (logs.length === 0) {
41+ //TODO: better encoding for "no rekeyDate" state?
42+ addToRekeyDateCache({
43+ [point]: Just('unknown'),
44+ });
45+ } else {
46+ // last log is most recent
47+ const blockNumber = logs[logs.length - 1].blockNumber;
48+ const block = await _web3.eth.getBlock(blockNumber);
49+ addToRekeyDateCache({
50+ //TODO date format
51+ [point]: Just(new Date(block.timestamp * 1000).toString()),
52+ });
53+ }
54+ },
55+ [web3, addToRekeyDateCache]
56+ );
57+
58+ return { getRekeyDate, syncRekeyDate };
59+}
src/views/Activate/ActivateCode.js+6−1
@@ -70,10 +70,15 @@ export default function ActivateCode() {
7070 const inviteWallet = walletFromMnemonic(seed, DEFAULT_HD_PATH);
7171 const _inviteWallet = need.wallet(inviteWallet);
7272
73− const incoming = await azimuth.azimuth.getTransferringFor(
73+ const owned = await azimuth.azimuth.getOwnedPoints(
7474 _contracts,
7575 _inviteWallet.address
7676 );
77+ const transferring = await azimuth.azimuth.getTransferringFor(
78+ _contracts,
79+ _inviteWallet.address
80+ );
81+ const incoming = [...owned, ...transferring];
7782
7883 let realPoint = Maybe.Nothing();
7984 let wallet = Maybe.Nothing();
src/views/Activate/PassportDownload.js+4−5
@@ -2,13 +2,13 @@ import React, { useState, useEffect, useCallback } from 'react';
22import cn from 'classnames';
33import Maybe from 'folktale/maybe';
44import { Grid, H4, P } from 'indigo-react';
5−import PaperCollateralRenderer from 'PaperCollateralRenderer';
65
76import * as need from 'lib/need';
87
98import { useActivateFlow } from './ActivateFlow';
109import { downloadWallet } from 'lib/invite';
1110import { DownloadButton, ForwardButton } from 'components/Buttons';
11+import PaperRenderer from 'components/PaperRenderer';
1212import { useLocalRouter } from 'lib/LocalRouter';
1313import Steps from 'components/Steps';
1414
@@ -76,11 +76,10 @@ export default function PassportDownload({ className }) {
7676 solid>
7777 {!downloaded ? 'Download Passport' : 'Continue'}
7878 </Grid.Item>
79− <PaperCollateralRenderer
80− className="abs super-hidden"
81− wallet={{ [pointAsString]: wallet }}
79+ <PaperRenderer
80+ point={pointAsString}
81+ wallet={wallet}
8282 callback={data => setPaper(Maybe.Just(data))}
83− mode="REGISTRATION"
8483 />
8584 </Grid>
8685 );
src/views/Activate/PassportTransfer.js+1
@@ -99,6 +99,7 @@ export default function PassportTransfer({ className, resetActivateRouter }) {
9999 web3: _web3,
100100 contracts: _contracts,
101101 onUpdate: handleUpdate,
102+ transferEth: true,
102103 });
103104
104105 // set the global wallet
src/views/Admin.jsnew file+84
@@ -0,0 +1,84 @@
1+import React, { useCallback } from 'react';
2+import { Just } from 'folktale/maybe';
3+import { Grid } from 'indigo-react';
4+import * as need from 'lib/need';
5+
6+import View from 'components/View';
7+import { ForwardButton } from 'components/Buttons';
8+
9+import { ROUTE_NAMES } from 'lib/routeNames';
10+import { eqAddr } from 'lib/wallet';
11+
12+import { useHistory } from 'store/history';
13+import { useWallet } from 'store/wallet';
14+import { usePointCursor } from 'store/pointCursor';
15+import { usePointCache } from 'store/pointCache';
16+import FooterButton from 'components/FooterButton';
17+
18+export default function Admin() {
19+ const history = useHistory();
20+ const { urbitWallet, wallet } = useWallet();
21+ const { pointCursor } = usePointCursor();
22+ const { pointCache } = usePointCache();
23+
24+ const point = need.point(pointCursor);
25+ const pointDetails = need.fromPointCache(pointCache, point);
26+ const address = need.addressFromWallet(wallet);
27+
28+ const goRedownload = useCallback(() => history.push(ROUTE_NAMES.REDOWNLOAD), [
29+ history,
30+ ]);
31+
32+ const goReticket = useCallback(() => history.push(ROUTE_NAMES.RETICKET), [
33+ history,
34+ ]);
35+
36+ const goEditPerms = useCallback(() => history.push(ROUTE_NAMES.PERMISSIONS), [
37+ history,
38+ ]);
39+
40+ const goTransfer = useCallback(() => history.push(ROUTE_NAMES.TRANSFER), [
41+ history,
42+ ]);
43+
44+ const canDownloadPassport = Just.hasInstance(urbitWallet);
45+ const isOwner = eqAddr(address, pointDetails.owner);
46+
47+ return (
48+ <View>
49+ <Grid className="pt2">
50+ <Grid.Item
51+ full
52+ as={ForwardButton}
53+ disabled={!canDownloadPassport}
54+ onClick={goRedownload}
55+ detail="Re-download your paper wallet">
56+ Download Passport
57+ </Grid.Item>
58+ <Grid.Divider />
59+ <Grid.Item
60+ full
61+ as={ForwardButton}
62+ disabled={!isOwner}
63+ onClick={goReticket}
64+ detail="Move to brand new wallet, re-setting all permissions">
65+ Reticket
66+ </Grid.Item>
67+ <Grid.Item
68+ full
69+ as={ForwardButton}
70+ onClick={goEditPerms}
71+ detail="Management, networking keys, etc.">
72+ Edit permissions
73+ </Grid.Item>
74+ </Grid>
75+
76+ <FooterButton
77+ detail="Transfer this identity to a new owner"
78+ disabled={!isOwner}
79+ onClick={goTransfer}>
80+ Transfer
81+ </FooterButton>
82+ </View>
83+ );
84+}
src/views/CreateGalaxy.js+2−2
@@ -18,7 +18,7 @@ import StatelessTransaction from '../components/old/StatelessTransaction';
1818import { NETWORK_TYPES } from '../lib/network';
1919import { canDecodePatp } from '../lib/txn';
2020
21−import { ETH_ZERO_ADDR, isValidAddress, eqAddr } from '../lib/wallet';
21+import { isValidAddress, isZeroAddress } from '../lib/wallet';
2222
2323import { isValidGalaxy, compose } from '../lib/lib';
2424import { withNetwork } from '../store/network';
@@ -103,7 +103,7 @@ class CreateGalaxy extends React.Component {
103103 galaxyDec
104104 );
105105
106− const available = eqAddr(currentOwner, ETH_ZERO_ADDR);
106+ const available = isZeroAddress(currentOwner);
107107
108108 this.setState({ isAvailable: available });
109109 };
src/views/Invite/InviteEmail.js+2−2
@@ -35,7 +35,7 @@ import LoadableButton from 'components/LoadableButton';
3535import useArray from 'lib/useArray';
3636import useForm from 'indigo-react/lib/useForm';
3737import { buildEmailInputConfig } from 'components/Inputs';
38−import { MIN_PLANET } from 'lib/constants';
38+import { MIN_PLANET, GAS_LIMITS } from 'lib/constants';
3939import Highlighted from 'components/Highlighted';
4040import { useNetwork } from 'store/network';
4141import { usePointCache } from 'store/pointCache';
@@ -45,7 +45,7 @@ import pluralize from 'lib/pluralize';
4545import useMailer from 'lib/useMailer';
4646
4747const GAS_PRICE_GWEI = 20; // we pay the premium for faster ux
48−const GAS_LIMIT = 400000;
48+const GAS_LIMIT = GAS_LIMITS.GIFT_PLANET;
4949const INVITE_COST = toWei((GAS_PRICE_GWEI * GAS_LIMIT).toString(), 'gwei');
5050const HAS_RECEIVED_TEXT = 'This email has already received an invite.';
5151
src/views/Permissions.jsnew file+153
@@ -0,0 +1,153 @@
1+import React, { useState, useCallback } from 'react';
2+import { Just, Nothing } from 'folktale/maybe';
3+import * as need from 'lib/need';
4+import { azimuth } from 'azimuth-js';
5+import { Grid } from 'indigo-react';
6+
7+import View from 'components/View';
8+import { ForwardButton } from 'components/Buttons';
9+import FooterButton from 'components/FooterButton';
10+import { matchBlinky } from 'components/Blinky';
11+
12+import { ROUTE_NAMES } from 'lib/routeNames';
13+import { eqAddr } from 'lib/wallet';
14+
15+import useLifecycle from 'lib/useLifecycle';
16+import { useNetwork } from 'store/network';
17+import { useHistory } from 'store/history';
18+import { useWallet } from 'store/wallet';
19+import { usePointCursor } from 'store/pointCursor';
20+import { usePointCache } from 'store/pointCache';
21+
22+//TODO consolidate with azimuth-js' getActivationBlock
23+const getRekeyDate = async (web3, contracts, point) => {
24+ const logs = await contracts.azimuth.getPastEvents('ChangedKeys', {
25+ fromBlock: 0,
26+ toBlock: 'latest',
27+ filter: { point: [point] },
28+ });
29+ if (logs.length === 0) {
30+ return null;
31+ } else {
32+ // last log is most recent
33+ const block = await web3.eth.getBlock(logs[logs.length - 1].blockNumber);
34+ return new Date(block.timestamp * 1000);
35+ }
36+};
37+
38+export default function Permissions() {
39+ const history = useHistory();
40+ const { web3, contracts } = useNetwork();
41+ const { wallet } = useWallet();
42+ const { pointCursor } = usePointCursor();
43+ const { pointCache } = usePointCache();
44+
45+ const [rekeyDate, setRekeyDate] = useState(Nothing());
46+
47+ const point = need.point(pointCursor);
48+ const pointSize = azimuth.getPointSize(point);
49+ const userAddress = need.wallet(wallet).address;
50+
51+ useLifecycle(() => {
52+ getRekeyDate(need.web3(web3), need.contracts(contracts), point).then(
53+ res => {
54+ if (res === null) setRekeyDate(Just('unknown'));
55+ else setRekeyDate(Just(res.toString()));
56+ }
57+ );
58+ });
59+
60+ const goSetManagement = useCallback(
61+ () => history.push(ROUTE_NAMES.SET_MANAGEMENT_PROXY),
62+ [history]
63+ );
64+
65+ const goSetSpawn = useCallback(
66+ () => history.push(ROUTE_NAMES.SET_SPAWN_PROXY),
67+ [history]
68+ );
69+
70+ const goSetVoting = useCallback(
71+ () => history.push(ROUTE_NAMES.SET_VOTING_PROXY),
72+ [history]
73+ );
74+
75+ const goSetKeys = useCallback(() => history.push(ROUTE_NAMES.SET_KEYS), [
76+ history,
77+ ]);
78+
79+ const pointDetails = need.fromPointCache(pointCache, point);
80+
81+ const isOwner = eqAddr(userAddress, pointDetails.owner);
82+ const canManage =
83+ isOwner || eqAddr(userAddress, pointDetails.managementProxy);
84+
85+ const proxyAction = (name, address, onClick) => {
86+ if (address === '0x0000000000000000000000000000000000000000')
87+ address = 'Unset';
88+ return (
89+ <Grid.Item full>
90+ <ForwardButton disabled={!isOwner} onClick={onClick} detail={address}>
91+ {`${name} proxy address`}
92+ </ForwardButton>
93+ </Grid.Item>
94+ );
95+ };
96+
97+ let spawnProxyAction = null;
98+ if (pointSize !== azimuth.PointSize.Planet) {
99+ spawnProxyAction = proxyAction(
100+ 'Spawn',
101+ pointDetails.spawnProxy,
102+ goSetSpawn
103+ );
104+ }
105+
106+ let votingProxyAction = null;
107+ if (pointSize === azimuth.PointSize.Galaxy) {
108+ votingProxyAction = proxyAction(
109+ 'Voting',
110+ pointDetails.votingProxy,
111+ goSetVoting
112+ );
113+ }
114+
115+ const netKeysText =
116+ pointDetails.keyRevisionNumber === 0 ? (
117+ 'Not yet configured'
118+ ) : (
119+ <>
120+ {`Revision #${pointDetails.keyRevisionNumber}, since `}
121+ {matchBlinky(rekeyDate)}
122+ </>
123+ );
124+
125+ return (
126+ <View>
127+ <Grid className="pt2">
128+ <Grid.Item full as={ForwardButton} disabled detail={pointDetails.owner}>
129+ Ownership address
130+ </Grid.Item>
131+ {proxyAction(
132+ 'Management',
133+ pointDetails.managementProxy,
134+ goSetManagement
135+ )}
136+ {spawnProxyAction}
137+ {votingProxyAction}
138+ <Grid.Item
139+ full
140+ as={ForwardButton}
141+ disabled={!canManage}
142+ onClick={goSetKeys}
143+ detail={netKeysText}>
144+ Network keys
145+ </Grid.Item>
146+ </Grid>
147+
148+ <FooterButton detail="Transfer this identity to a new owner" disabled>
149+ Transfer
150+ </FooterButton>
151+ </View>
152+ );
153+}
src/views/Point.js+28−6
@@ -4,6 +4,7 @@ import * as need from 'lib/need';
44import { Grid } from 'indigo-react';
55
66import { usePointCursor } from 'store/pointCursor';
7+import { usePointCache } from 'store/pointCache';
78import { useHistory } from 'store/history';
89
910import View from 'components/View';
@@ -15,6 +16,7 @@ import { matchBlinky } from 'components/Blinky';
1516import useInvites from 'lib/useInvites';
1617import { useSyncOwnedPoints } from 'lib/useSyncPoints';
1718import { ROUTE_NAMES } from 'lib/routeNames';
19+import { eqAddr } from 'lib/wallet';
1820
1921import Actions from './Point/Actions';
2022import { useWallet } from 'store/wallet';
@@ -22,16 +24,34 @@ import { useWallet } from 'store/wallet';
2224export default function Point() {
2325 const history = useHistory();
2426 const { pointCursor } = usePointCursor();
27+ const { pointCache } = usePointCache();
2528 const { wallet } = useWallet();
2629
2730 const point = need.point(pointCursor);
2831
32+ let canAdmin = false;
33+ if (point in pointCache) {
34+ canAdmin = wallet.matchWith({
35+ Nothing: () => false,
36+ Just: wal => {
37+ const pointDetails = need.fromPointCache(pointCache, point);
38+ const address = need.addressFromWallet(wallet);
39+ return (
40+ eqAddr(address, pointDetails.owner) ||
41+ eqAddr(address, pointDetails.managementProxy)
42+ );
43+ },
44+ });
45+ }
46+
2947 // fetch the invites for the current cursor
3048 const { availableInvites } = useInvites(point);
3149 const availableInvitesText = matchBlinky(availableInvites);
3250
3351 const showActions = Maybe.Just.hasInstance(wallet);
3452
53+ const goAdmin = useCallback(() => history.push(ROUTE_NAMES.ADMIN), [history]);
54+
3555 const goInvite = useCallback(() => history.push(ROUTE_NAMES.INVITE), [
3656 history,
3757 ]);
@@ -43,14 +63,16 @@ export default function Point() {
4363 <View>
4464 <Passport point={Maybe.Just(point)} />
4565 <Grid className="pt2">
46− <Grid.Item full>
47− <ForwardButton disabled>Admin</ForwardButton>
66+ <Grid.Item
67+ full
68+ as={ForwardButton}
69+ disabled={!canAdmin}
70+ onClick={goAdmin}>
71+ Admin
4872 </Grid.Item>
4973 <Grid.Divider />
50− <Grid.Item full>
51− <ForwardButton detail="Boot your computer" disabled>
52− Boot Arvo
53− </ForwardButton>
74+ <Grid.Item full as={ForwardButton} detail="Boot your computer" disabled>
75+ Boot Arvo
5476 </Grid.Item>
5577 {showActions && (
5678 <Grid.Item full>
src/views/Point/Actions.js+2−2
@@ -2,7 +2,7 @@ import React from 'react';
22import { azimuth } from 'azimuth-js';
33import * as need from '../../lib/need';
44
5−import { ETH_ZERO_ADDR, CURVE_ZERO_ADDR, eqAddr } from '../../lib/wallet';
5+import { CURVE_ZERO_ADDR, isZeroAddress, eqAddr } from '../../lib/wallet';
66import { H2, P } from '../../components/old/Base';
77import { Button } from '../../components/old/Base';
88import { ROUTE_NAMES } from '../../lib/routeNames';
@@ -89,7 +89,7 @@ function Actions() {
8989 Nothing: () => false,
9090 Just: deets =>
9191 eqAddr(deets.value.owner, addr) &&
92− !eqAddr(deets.value.transferProxy, ETH_ZERO_ADDR),
92+ !isZeroAddress(deets.value.transferProxy),
9393 });
9494
9595 const displayReminder = pointDetails.matchWith({
src/views/Point/KeysAndMetadata.js+3−6
@@ -1,14 +1,11 @@
11import React from 'react';
22import { H2, H4 } from '../../components/old/Base';
33
4−import { ETH_ZERO_ADDR, eqAddr } from '../../lib/wallet';
4+import { CURVE_ZERO_ADDR, isZeroAddress } from '../../lib/wallet';
55import { Grid } from 'indigo-react';
66
7−const NULL_KEY =
8− '0x0000000000000000000000000000000000000000000000000000000000000000';
9−
107const renderAddress = addr =>
11− eqAddr(addr, ETH_ZERO_ADDR) ? (
8+ isZeroAddress(addr) ? (
129 <div>{'(not set)'}</div>
1310 ) : (
1411 <div>
@@ -27,7 +24,7 @@ const renderAddress = addr =>
2724const renderNetworkKey = hex => {
2825 const sl = i => hex.slice(i, i + 4);
2926 const rowFrom = i => `${sl(i)}.${sl(i + 4)}.${sl(i + 8)}.${sl(i + 12)}`;
30− return hex === NULL_KEY ? (
27+ return hex === CURVE_ZERO_ADDR ? (
3128 <div>{'(not set)'}</div>
3229 ) : (
3330 <div>
src/views/Points.js+2−2
@@ -3,7 +3,7 @@ import Maybe from 'folktale/maybe';
33import { H1, H2, P, Grid } from 'indigo-react';
44
55import { ROUTE_NAMES } from 'lib/routeNames';
6−import { ETH_ZERO_ADDR, eqAddr } from 'lib/wallet';
6+import { isZeroAddress } from 'lib/wallet';
77import useIsEclipticOwner from 'lib/useIsEclipticOwner';
88import { useSyncKnownPoints } from 'lib/useSyncPoints';
99
@@ -20,7 +20,7 @@ const matchArray = obj =>
2020 obj.matchWith({ Nothing: () => [], Just: p => p.value });
2121
2222const hasTransferProxy = (details, point) =>
23− !eqAddr(details.transferProxy, ETH_ZERO_ADDR);
23+ !isZeroAddress(details.transferProxy);
2424
2525export default function Points() {
2626 const { wallet } = useWallet();
src/views/Redownload.jsnew file+52
@@ -0,0 +1,52 @@
1+import React, { useState } from 'react';
2+import { Just, Nothing } from 'folktale/maybe';
3+import * as need from 'lib/need';
4+
5+import { usePointCursor } from 'store/pointCursor';
6+
7+import View from 'components/View';
8+import { DownloadButton } from 'components/Buttons';
9+import PaperRenderer from 'components/PaperRenderer';
10+
11+import { downloadWallet } from 'lib/invite';
12+
13+import { useWallet } from 'store/wallet';
14+
15+export default function Redownload() {
16+ const { urbitWallet } = useWallet();
17+ const { pointCursor } = usePointCursor();
18+ const point = need.point(pointCursor);
19+
20+ const [paper, setPaper] = useState(Nothing());
21+
22+ const doDownload = () => {
23+ downloadWallet(paper.value);
24+ };
25+
26+ const paperRenderer = paper.matchWith({
27+ Nothing: () => null,
28+ Just: p => (
29+ <PaperRenderer
30+ point={point}
31+ wallet={urbitWallet.value}
32+ callback={paper => {
33+ setPaper(Just(paper));
34+ }}
35+ />
36+ ),
37+ });
38+
39+ return (
40+ <View>
41+ <DownloadButton
42+ disabled={Nothing.hasInstance(paper)}
43+ onClick={doDownload}>
44+ {paper.matchWith({
45+ Nothing: () => 'Printing and folding...',
46+ Just: _ => 'Download paper wallet',
47+ })}
48+ </DownloadButton>
49+ {paperRenderer}
50+ </View>
51+ );
52+}
src/views/Reticket.jsnew file+97
@@ -0,0 +1,97 @@
1+import React, { useState } from 'react';
2+import { size } from 'lodash';
3+import * as need from 'lib/need';
4+import ob from 'urbit-ob';
5+import { Just, Nothing } from 'folktale/maybe';
6+
7+import { usePointCursor } from 'store/pointCursor';
8+
9+import View from 'components/View';
10+import MiniBackButton from 'components/MiniBackButton';
11+import { LocalRouterProvider } from 'lib/LocalRouter';
12+import Crumbs from 'components/Crumbs';
13+
14+import useRouter from 'lib/useRouter';
15+
16+import { useHistory } from 'store/history';
17+
18+import Confirm from './Reticket/Confirm';
19+import Download from './Reticket/Download';
20+import Verify from './Reticket/Verify';
21+import DoReticket from './Reticket/DoReticket';
22+
23+const NAMES = {
24+ CONFIRM: 'CONFIRM',
25+ DOWNLOAD: 'DOWNLOAD',
26+ VERIFY: 'VERIFY',
27+ RETICKET: 'RETICKET',
28+};
29+
30+const VIEWS = {
31+ CONFIRM: Confirm,
32+ DOWNLOAD: Download,
33+ VERIFY: Verify,
34+ RETICKET: DoReticket,
35+};
36+
37+const STEP_MAX = size(NAMES);
38+
39+export default function Reticket() {
40+ const history = useHistory();
41+ const { Route, ...innerRouter } = useRouter({
42+ names: NAMES,
43+ views: VIEWS,
44+ initialRoutes: [{ key: NAMES.CONFIRM }],
45+ });
46+ const { pointCursor } = usePointCursor();
47+ const point = need.point(pointCursor);
48+ const name = ob.patp(point);
49+
50+ const [newWallet, setNewWallet] = useState(Nothing());
51+
52+ //TODO step # from Route?
53+ const stepNumber = 1;
54+
55+ const storeNewWallet = (wallet, paper) => {
56+ setNewWallet(
57+ Just({
58+ wallet,
59+ paper,
60+ })
61+ );
62+ };
63+
64+ const completed = () => {
65+ history.pop();
66+ };
67+
68+ return (
69+ <LocalRouterProvider value={innerRouter}>
70+ <View>
71+ <Crumbs
72+ routes={[
73+ {
74+ text: name,
75+ action: () => {
76+ return history.pop(2);
77+ },
78+ },
79+ {
80+ text: 'Admin',
81+ action: () => history.pop(),
82+ },
83+ ]}
84+ />
85+ <MiniBackButton onClick={innerRouter.pop} />
86+ Step {stepNumber} of {STEP_MAX}
87+ <Route
88+ newWallet={newWallet}
89+ //
90+ storeNewWallet={storeNewWallet}
91+ //
92+ completed={completed}
93+ />
94+ </View>
95+ </LocalRouterProvider>
96+ );
97+}
src/views/Reticket/Confirm.jsnew file+63
@@ -0,0 +1,63 @@
1+import React, { useState, useCallback } from 'react';
2+import * as need from 'lib/need';
3+import { Just, Nothing } from 'folktale/maybe';
4+
5+import { usePointCursor } from 'store/pointCursor';
6+
7+import View from 'components/View';
8+import { ForwardButton } from 'components/Buttons';
9+import PaperRenderer from 'components/PaperRenderer';
10+
11+import useLifecycle from 'lib/useLifecycle';
12+import { generateWallet } from 'lib/invite';
13+import { useLocalRouter } from 'lib/LocalRouter';
14+
15+import { isDevelopment } from 'lib/flags';
16+
17+export default function Confirm({ newWallet, storeNewWallet }) {
18+ const { push, names } = useLocalRouter();
19+ const { pointCursor } = usePointCursor();
20+ const point = need.point(pointCursor);
21+
22+ const [generatedWallet, setGeneratedWallet] = useState(Nothing());
23+
24+ // start generating new wallet on mount
25+ //TODO also check we can pay for the transactions?
26+ useLifecycle(() => {
27+ (async () => {
28+ const wal = await generateWallet(point);
29+ if (isDevelopment) {
30+ console.log('ticket for', point, wal.ticket);
31+ }
32+ setGeneratedWallet(Just(wal));
33+ })();
34+ });
35+
36+ const next = useCallback(() => push(names.DOWNLOAD), [push, names]);
37+
38+ const paperRenderer = generatedWallet.matchWith({
39+ Nothing: () => null,
40+ Just: gw => (
41+ <PaperRenderer
42+ point={point}
43+ wallet={gw.value}
44+ callback={paper => {
45+ storeNewWallet(gw.value, paper);
46+ }}
47+ />
48+ ),
49+ });
50+
51+ return (
52+ <View>
53+ Maybe we should just duplicate the Login component here? Not everyone who
54+ does this will be logged in with an existing ticket. Also, beware, this
55+ changes your proxy addresses. If you're using smart contracts, this might
56+ break them! It will also change your networking keys!
57+ <ForwardButton disabled={Nothing.hasInstance(newWallet)} onClick={next}>
58+ I understand, continue
59+ </ForwardButton>
60+ {paperRenderer}
61+ </View>
62+ );
63+}
src/views/Reticket/DoReticket.jsnew file+128
@@ -0,0 +1,128 @@
1+import React, { useState, useCallback } from 'react';
2+import { Just } from 'folktale/maybe';
3+
4+import { Grid, H4, Text, ErrorText } from 'indigo-react';
5+import { RestartButton } from 'components/Buttons';
6+import WarningBox from 'components/WarningBox';
7+import LoadingBar from 'components/LoadingBar';
8+import Highlighted from 'components/Highlighted';
9+
10+import { claimPointFromInvite } from 'lib/invite';
11+import { fromWei } from 'lib/txn';
12+
13+import * as need from 'lib/need';
14+import { useNetwork } from 'store/network';
15+import { useWallet } from 'store/wallet';
16+import { usePointCursor } from 'store/pointCursor';
17+import useLifecycle from 'lib/useLifecycle';
18+
19+//TODO maybe we want to drop in PassportTransfer here?
20+export default function DoReticket({ newWallet, completed }) {
21+ const { web3, contracts } = useNetwork();
22+ const { wallet, setUrbitWallet } = useWallet();
23+ const { pointCursor } = usePointCursor();
24+
25+ const [generalError, setGeneralError] = useState();
26+ const [{ label, progress }, setState] = useState({
27+ label: 'Starting...',
28+ progress: 0,
29+ });
30+ const [needFunds, setNeedFunds] = useState();
31+
32+ // start reticketing transactions on mount
33+ useLifecycle(() => {
34+ (async () => {
35+ try {
36+ await claimPointFromInvite({
37+ inviteWallet: need.wallet(wallet),
38+ wallet: newWallet.value.wallet,
39+ point: need.point(pointCursor),
40+ web3: need.web3(web3),
41+ contracts: need.contracts(contracts),
42+ onUpdate: handleUpdate,
43+ });
44+ //TODO would it be better for this to happen in completed()?
45+ setUrbitWallet(Just(newWallet.value.wallet));
46+ completed(); //TODO don't auto-redirect
47+ } catch (err) {
48+ setGeneralError(err);
49+ }
50+ })();
51+ });
52+
53+ const handleUpdate = useCallback(
54+ ({ type, state, value }) => {
55+ switch (type) {
56+ case 'progress':
57+ return setState(state);
58+ case 'askFunding':
59+ return setNeedFunds(value);
60+ case 'gotFunding':
61+ return setNeedFunds(false);
62+ default:
63+ console.error(`Unknown update: ${type}`);
64+ }
65+ },
66+ [setState, setNeedFunds]
67+ );
68+
69+ const renderAdditionalInfo = () => {
70+ if (generalError) {
71+ return (
72+ <>
73+ <Grid.Item full className="mt8">
74+ <ErrorText>{generalError.message.toString()}</ErrorText>
75+ </Grid.Item>
76+ <Grid.Item
77+ full
78+ className="mt3"
79+ as={RestartButton}
80+ //TODO onClick={goToRestart}
81+ />
82+ </>
83+ );
84+ }
85+
86+ if (needFunds) {
87+ return (
88+ <Grid.Item full className="mt8">
89+ <Highlighted>
90+ The address {needFunds.address} needs at least{' '}
91+ {fromWei(needFunds.minBalance)} ETH and currently has{' '}
92+ {fromWei(needFunds.balance)} ETH. Waiting until the account has
93+ enough funds.
94+ </Highlighted>
95+ </Grid.Item>
96+ );
97+ }
98+
99+ if (progress < 100) {
100+ return (
101+ <Grid.Item full as={WarningBox} className="mt8">
102+ Never give your Master Ticket to anyone
103+ </Grid.Item>
104+ );
105+ }
106+
107+ return null;
108+ };
109+
110+ return (
111+ <Grid className={'auto-rows-min'}>
112+ <Grid.Item full as={H4}>
113+ {label}
114+ </Grid.Item>
115+ <Grid.Item full as={Grid} className="mt3" gap={3}>
116+ <Grid.Item full as={LoadingBar} progress={progress} />
117+ <Grid.Item full>
118+ <Text className="f5 green4">
119+ This process can take up to 5 minutes to complete. Don't leave this
120+ page until the process is complete.
121+ </Text>
122+ </Grid.Item>
123+ </Grid.Item>
124+
125+ {renderAdditionalInfo()}
126+ </Grid>
127+ );
128+}
src/views/Reticket/Download.jsnew file+35
@@ -0,0 +1,35 @@
1+import React, { useState, useCallback } from 'react';
2+import { Nothing } from 'folktale/maybe';
3+
4+import View from 'components/View';
5+import { ForwardButton, DownloadButton } from 'components/Buttons';
6+
7+import { useLocalRouter } from 'lib/LocalRouter';
8+import { downloadWallet } from 'lib/invite';
9+
10+export default function Download({ newWallet }) {
11+ const { push, names } = useLocalRouter();
12+
13+ const [hasDownloaded, setHasDownloaded] = useState(false);
14+
15+ const doDownload = () => {
16+ downloadWallet(newWallet.value.paper);
17+ setHasDownloaded(true);
18+ };
19+
20+ const next = useCallback(() => push(names.VERIFY), [push, names]);
21+
22+ return (
23+ <View>
24+ Download the thing, and keep it somewhere safe!
25+ <DownloadButton
26+ disabled={Nothing.hasInstance(newWallet)}
27+ onClick={doDownload}>
28+ Download
29+ </DownloadButton>
30+ <ForwardButton disabled={!hasDownloaded} onClick={next}>
31+ Continue
32+ </ForwardButton>
33+ </View>
34+ );
35+}
src/views/Reticket/Verify.jsnew file+42
@@ -0,0 +1,42 @@
1+import React, { useCallback, useMemo } from 'react';
2+import { Input } from 'indigo-react';
3+
4+import View from 'components/View';
5+import { useTicketInput } from 'components/Inputs';
6+import { ForwardButton } from 'components/Buttons';
7+
8+import { useLocalRouter } from 'lib/LocalRouter';
9+import { validateExactly } from 'lib/validators';
10+
11+const STUB_VERIFY_TICKET = process.env.NODE_ENV === 'development';
12+
13+//TODO deduplicate with PassportVerify
14+export default function Verify({ newWallet }) {
15+ const { push, names } = useLocalRouter();
16+
17+ const ticket = newWallet.value.wallet.ticket;
18+ const validators = useMemo(
19+ () => [validateExactly(ticket, 'Does not match expected master ticket.')],
20+ [ticket]
21+ );
22+ const ticketInput = useTicketInput({
23+ name: 'ticket',
24+ label: 'New master ticket',
25+ initialValue: STUB_VERIFY_TICKET ? ticket : undefined,
26+ autoFocus: true,
27+ validators,
28+ });
29+ const { pass } = ticketInput;
30+
31+ const next = useCallback(() => push(names.RETICKET), [push, names]);
32+
33+ return (
34+ <View>
35+ Prove that you downloaded the new secrets!
36+ <Input {...ticketInput} />
37+ <ForwardButton disabled={!pass} onClick={next}>
38+ Reticket
39+ </ForwardButton>
40+ </View>
41+ );
42+}