on master help sign in
Merge branch 'mino' into feat/indigo-login
b0440cfMatt Condon (shrugs)
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';
3import * as ob from 'urbit-ob';
3import * as ob from 'urbit-ob';
4import ReactSVGComponents from '../ReactSVGComponents';
4import ReactSVGComponents from '../ReactSVGComponents';
5import PropTypes from 'prop-types';
5import PropTypes from 'prop-types';
6−import PaperCollateralRenderer from 'PaperCollateralRenderer';
7import { Button } from './Base';
6import { Button } from './Base';
7+import PaperRenderer from 'components/PaperRenderer';
8
8
9import { WALLET_STATES } from '../../lib/invite';
9import { WALLET_STATES } from '../../lib/invite';
10import { downloadWallet } from '../../lib/invite';
10import { downloadWallet } from '../../lib/invite';
@@ -159,14 +159,13 @@ class Passport extends React.Component {
159 );
159 );
160
160
161 let paperCollateral = wallet ? (
161 let paperCollateral = wallet ? (
162− <PaperCollateralRenderer
162+ <PaperRenderer
163− wallet={{ [pointVal]: wallet }}
163+ point={pointVal}
164− className={'extremely-hidden'}
164+ wallet={wallet}
165 callback={data => {
165 callback={data => {
166 this.setState({ paper: data });
166 this.setState({ paper: data });
167 this.props.pushWalletState(WALLET_STATES.PAPER_READY);
167 this.props.pushWalletState(WALLET_STATES.PAPER_READY);
168 }}
168 }}
169− mode={'REGISTRATION'}
170 />
169 />
171 ) : (
170 ) : (
172 <div />
171 <div />
src/lib/constants.js+19
@@ -13,6 +13,24 @@ const GALAXY_ENTROPY_BITS = 384;
13
13
14const SEED_ENTROPY_BITS = 128;
14const SEED_ENTROPY_BITS = 128;
15
15
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+
16// TODO: this is walletgen-ui specific, move into a wallet router later
34// TODO: this is walletgen-ui specific, move into a wallet router later
17const GEN_STATES = {
35const GEN_STATES = {
18 ENY_NOSTART: Symbol('ENY_NOSTART'),
36 ENY_NOSTART: Symbol('ENY_NOSTART'),
@@ -45,6 +63,7 @@ const PROFILE_STATES = {
45};
63};
46
64
47export {
65export {
66+ GAS_LIMITS,
48 GEN_STATES,
67 GEN_STATES,
49 BUTTON_STATES,
68 BUTTON_STATES,
50 PROFILE_STATES,
69 PROFILE_STATES,
src/lib/invite.js+100−76
@@ -12,6 +12,7 @@ import saveAs from 'file-saver';
12import { sendSignedTransaction, hexify } from './txn';
12import { sendSignedTransaction, hexify } from './txn';
13import { attemptNetworkSeedDerivation } from './keys';
13import { attemptNetworkSeedDerivation } from './keys';
14import { addHexPrefix, WALLET_TYPES } from './wallet';
14import { addHexPrefix, WALLET_TYPES } from './wallet';
15+import { GAS_LIMITS } from './constants';
15
16
16const INVITE_STAGES = {
17const INVITE_STAGES = {
17 INVITE_LOGIN: 'invite login',
18 INVITE_LOGIN: 'invite login',
@@ -38,22 +39,14 @@ const TRANSACTION_STATES = {
38 label: 'Signing Transactions...',
39 label: 'Signing Transactions...',
39 progress: 0.15,
40 progress: 0.15,
40 },
41 },
41− FUNDING_INVITE: {
42+ FUNDING: {
42− label: 'Funding Invite Wallet...',
43+ label: 'Funding Transactions...',
43 progress: 0.3,
44 progress: 0.3,
44 },
45 },
45 CLAIMING: {
46 CLAIMING: {
46 label: 'Claiming Invite...',
47 label: 'Claiming Invite...',
47 progress: 0.55,
48 progress: 0.55,
48 },
49 },
49− FUNDING_RECIPIENT: {
50− label: 'Funding Recipient Wallet...',
51− progress: 0.65,
52− },
53− CONFIGURING: {
54− label: 'Configuring Point...',
55− progress: 0.85,
56− },
57 CLEANING: {
50 CLEANING: {
58 label: 'Cleaning Up...',
51 label: 'Cleaning Up...',
59 progress: 0.95,
52 progress: 0.95,
@@ -108,6 +101,7 @@ async function claimPointFromInvite({
108 web3,
101 web3,
109 contracts,
102 contracts,
110 onUpdate,
103 onUpdate,
104+ transferEth = false,
111}) {
105}) {
112 const askForFunding = (address, minBalance, balance) =>
106 const askForFunding = (address, minBalance, balance) =>
113 onUpdate({
107 onUpdate({
@@ -120,62 +114,29 @@ async function claimPointFromInvite({
120
114
121 const inviteAddress = inviteWallet.address;
115 const inviteAddress = inviteWallet.address;
122
116
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−
163 //
117 //
164− // we're gonna be operating as the new wallet from here on out, so change
118+ // generate transactions
165− // the relevant values
166 //
119 //
167− const newAddress = wallet.ownership.keys.address;
168
120
169− // configure management proxy
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
170− const managementTx = azimuth.ecliptic.setManagementProxy(
130+ const transferTmpTx = azimuth.ecliptic.transferPoint(
171 contracts,
131 contracts,
172 point,
132 point,
173− wallet.management.keys.address
133+ inviteAddress,
134+ false
174 );
135 );
175− managementTx.gas = 200000;
136+ transferTmpTx.gas = GAS_LIMITS.TRANSFER;
176− managementTx.nonce = 0;
177
137
178 // configure networking public keys
138 // configure networking public keys
139+
179 //TODO feels like more of this logic should live in a lib?
140 //TODO feels like more of this logic should live in a lib?
180 const seed = await attemptNetworkSeedDerivation(true, {
141 const seed = await attemptNetworkSeedDerivation(true, {
181 walletType: WALLET_TYPES.TICKET,
142 walletType: WALLET_TYPES.TICKET,
@@ -193,7 +154,7 @@ async function claimPointFromInvite({
193
154
194 const networkKeys = kg.deriveNetworkKeys(networkSeed);
155 const networkKeys = kg.deriveNetworkKeys(networkSeed);
195
156
196− let keysTx = azimuth.ecliptic.configureKeys(
157+ const keysTx = azimuth.ecliptic.configureKeys(
197 contracts,
158 contracts,
198 point,
159 point,
199 addHexPrefix(networkKeys.crypt.public),
160 addHexPrefix(networkKeys.crypt.public),
@@ -201,43 +162,108 @@ async function claimPointFromInvite({
201 1,
162 1,
202 false
163 false
203 );
164 );
204− keysTx.gas = 150000;
165+ keysTx.gas = GAS_LIMITS.CONFIGURE_KEYS;
205− keysTx.nonce = 1;
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);
206
219
207− //TODO refactor this process into function
208 let totalCost = 0;
220 let totalCost = 0;
209− let txs = [managementTx, keysTx];
221+ let inviteNonce = await web3.eth.getTransactionCount(inviteAddress);
210 txs = txs.map(tx => {
222 txs = txs.map(tx => {
223+ tx.from = inviteAddress;
224+ tx.nonce = inviteNonce++;
211− tx.gasPrice = 20000000000;
225+ tx.gasPrice = 20000000000; //NOTE we pay the premium for faster ux
212− tx.from = newAddress;
213 totalCost = totalCost + tx.gasPrice * tx.gas;
226 totalCost = totalCost + tx.gasPrice * tx.gas;
214 return tx;
227 return tx;
215 });
228 });
216
229
217 let txPairs = txs.map(tx => {
230 let txPairs = txs.map(tx => {
218 let stx = new Tx(tx);
231 let stx = new Tx(tx);
219− stx.sign(Buffer.from(wallet.ownership.keys.private, 'hex'));
232+ stx.sign(Buffer.from(inviteWallet.privateKey, 'hex'));
220 return {
233 return {
221− raw: '0x' + stx.serialize().toString('hex'),
234+ raw: hexify(stx.serialize()),
222 signed: stx,
235 signed: stx,
223 };
236 };
224 });
237 });
225
238
239+ //
240+ // ensuring funding for transactions
241+ //
242+
226− progress(TRANSACTION_STATES.FUNDING_RECIPIENT);
243+ progress(TRANSACTION_STATES.FUNDING);
227
244
228− usedTank = await tank.ensureFundsFor(
245+ const usedTank = await tank.ensureFundsFor(
229 web3,
246 web3,
230 point,
247 point,
231− newAddress,
248+ inviteAddress,
232 totalCost,
249 totalCost,
233 txPairs.map(p => p.raw),
250 txPairs.map(p => p.raw),
234− askForFunding
251+ askForFunding,
252+ gotFunding
235 );
253 );
236
254
255+ //
256+ // sending and awaiting transactions
257+ //
258+
237− progress(TRANSACTION_STATES.CONFIGURING);
259+ progress(TRANSACTION_STATES.CLAIMING);
238
260
239 await sendAndAwaitConfirm(web3, txPairs.map(p => Just(p.signed)), usedTank);
261 await sendAndAwaitConfirm(web3, txPairs.map(p => Just(p.signed)), usedTank);
240
262
263+ //
264+ // move leftover eth
265+ //
266+
241 progress(TRANSACTION_STATES.CLEANING);
267 progress(TRANSACTION_STATES.CLEANING);
242
268
243 // if non-trivial eth left in invite wallet, transfer to new ownership
269 // if non-trivial eth left in invite wallet, transfer to new ownership
@@ -245,11 +271,10 @@ async function claimPointFromInvite({
245 const gasPrice = 20000000000;
271 const gasPrice = 20000000000;
246 const gasLimit = 21000;
272 const gasLimit = 21000;
247 const sendEthCost = gasPrice * gasLimit;
273 const sendEthCost = gasPrice * gasLimit;
248− if (balance > sendEthCost) {
274+ if (transferEth && balance > sendEthCost) {
249 const value = balance - sendEthCost;
275 const value = balance - sendEthCost;
250− console.log('sending', value);
251 const tx = {
276 const tx = {
252− to: newAddress,
277+ to: wallet.ownership.keys.address,
253 value: value,
278 value: value,
254 gasPrice: gasPrice,
279 gasPrice: gasPrice,
255 gas: gasLimit,
280 gas: gasLimit,
@@ -261,7 +286,6 @@ async function claimPointFromInvite({
261 web3.eth.sendSignedTransaction(rawTx).catch(err => {
286 web3.eth.sendSignedTransaction(rawTx).catch(err => {
262 console.log('error sending value tx, who cares', err);
287 console.log('error sending value tx, who cares', err);
263 });
288 });
264− console.log('sent old balance');
265 }
289 }
266
290
267 progress(TRANSACTION_STATES.DONE);
291 progress(TRANSACTION_STATES.DONE);
src/lib/routeNames.js+4
@@ -7,6 +7,10 @@ export const ROUTE_NAMES = {
7 INVITES_MANAGE: Symbol('INVITES_MANAGE'),
7 INVITES_MANAGE: Symbol('INVITES_MANAGE'),
8 //
8 //
9 LOGIN: Symbol('LOGIN'),
9 LOGIN: Symbol('LOGIN'),
10+ ADMIN: Symbol('ADMIN'),
11+ REDOWNLOAD: Symbol('REDOWNLOAD'),
12+ RETICKET: Symbol('RETICKET'),
13+ PERMISSIONS: Symbol('PERMISSIONS'),
10 //
14 //
11 ACCEPT_TRANSFER: Symbol('ACCEPT_TRANSFER'),
15 ACCEPT_TRANSFER: Symbol('ACCEPT_TRANSFER'),
12 CANCEL_TRANSFER: Symbol('CANCEL_TRANSFER'),
16 CANCEL_TRANSFER: Symbol('CANCEL_TRANSFER'),
src/lib/router.js+8
@@ -1,6 +1,10 @@
1import Landing from '../views/Landing';
1import Landing from '../views/Landing';
2import Login from '../views/Login.js';
2import Login from '../views/Login.js';
3import InvitesManage from '../views/InvitesManage.js';
3import 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';
4import AcceptTransfer from '../views/AcceptTransfer';
8import AcceptTransfer from '../views/AcceptTransfer';
5import CancelTransfer from '../views/CancelTransfer';
9import CancelTransfer from '../views/CancelTransfer';
6import CreateGalaxy from '../views/CreateGalaxy';
10import CreateGalaxy from '../views/CreateGalaxy';
@@ -28,6 +32,10 @@ export const ROUTES = {
28 [ROUTE_NAMES.INVITE]: Invite,
32 [ROUTE_NAMES.INVITE]: Invite,
29 [ROUTE_NAMES.INVITES_MANAGE]: InvitesManage,
33 [ROUTE_NAMES.INVITES_MANAGE]: InvitesManage,
30 [ROUTE_NAMES.LOGIN]: Login,
34 [ROUTE_NAMES.LOGIN]: Login,
35+ [ROUTE_NAMES.ADMIN]: Admin,
36+ [ROUTE_NAMES.REDOWNLOAD]: Redownload,
37+ [ROUTE_NAMES.RETICKET]: Reticket,
38+ [ROUTE_NAMES.PERMISSIONS]: Permissions,
31 [ROUTE_NAMES.ACCEPT_TRANSFER]: AcceptTransfer,
39 [ROUTE_NAMES.ACCEPT_TRANSFER]: AcceptTransfer,
32 [ROUTE_NAMES.CANCEL_TRANSFER]: CancelTransfer,
40 [ROUTE_NAMES.CANCEL_TRANSFER]: CancelTransfer,
33 [ROUTE_NAMES.VIEW_POINT]: ViewPoint,
41 [ROUTE_NAMES.VIEW_POINT]: ViewPoint,
src/lib/txn.js+1−1
@@ -144,7 +144,7 @@ const sendSignedTransaction = (web3, stx, doubtNonceError, confirmationCb) => {
144 },
144 },
145 });
145 });
146
146
147− const rawTx = addHexPrefix(txn.serialize().toString('hex'));
147+ const rawTx = hexify(txn.serialize());
148
148
149 return new Promise(async (resolve, reject) => {
149 return new Promise(async (resolve, reject) => {
150 web3.eth
150 web3.eth
src/lib/wallet.js+2
@@ -55,6 +55,8 @@ export const keccak256 = str =>
55
55
56export const isValidAddress = a => '0x0' === a || isAddress(a);
56export const isValidAddress = a => '0x0' === a || isAddress(a);
57
57
58+export const isZeroAddress = a => ETH_ZERO_ADDR === a;
59+
58export const toChecksumAddress = address => {
60export const toChecksumAddress = address => {
59 const addr = stripHexPrefix(address).toLowerCase();
61 const addr = stripHexPrefix(address).toLowerCase();
60 const hash = keccak256(addr).toString('hex');
62 const hash = keccak256(addr).toString('hex');
src/store/lib/useBirthdaysStore.js+1−1
@@ -26,7 +26,7 @@ export default function useBirthdayStore() {
26 async point => {
26 async point => {
27 const _contracts = contracts.getOrElse(null);
27 const _contracts = contracts.getOrElse(null);
28 const _web3 = web3.getOrElse(null);
28 const _web3 = web3.getOrElse(null);
29− if (!_contracts || !web3) {
29+ if (!_contracts || !_web3) {
30 return;
30 return;
31 }
31 }
32
32
src/store/lib/usePointStore.js+5−2
@@ -2,6 +2,7 @@ import { useCallback } from 'react';
2
2
3import useDetailsStore from './useDetailsStore';
3import useDetailsStore from './useDetailsStore';
4import useBirthdaysStore from './useBirthdaysStore';
4import useBirthdaysStore from './useBirthdaysStore';
5+import useRekeyDateStore from './useRekeyDateStore';
5import useInvitesStore from './useInvitesStore';
6import useInvitesStore from './useInvitesStore';
6import useControlledPointsStore from './useControlledPointsStore';
7import useControlledPointsStore from './useControlledPointsStore';
7import useEclipticOwnerStore from './useEclipticOwnerStore';
8import useEclipticOwnerStore from './useEclipticOwnerStore';
@@ -9,6 +10,7 @@ import useEclipticOwnerStore from './useEclipticOwnerStore';
9export default function usePointStore() {
10export default function usePointStore() {
10 const { syncDetails, ...details } = useDetailsStore();
11 const { syncDetails, ...details } = useDetailsStore();
11 const { syncBirthday, ...birthdays } = useBirthdaysStore();
12 const { syncBirthday, ...birthdays } = useBirthdaysStore();
13+ const { syncRekeyDate, ...rekeyDates } = useRekeyDateStore();
12 const { syncInvites, ...invites } = useInvitesStore();
14 const { syncInvites, ...invites } = useInvitesStore();
13 const ecliptic = useEclipticOwnerStore();
15 const ecliptic = useEclipticOwnerStore();
14 const {
16 const {
@@ -18,8 +20,8 @@ export default function usePointStore() {
18
20
19 // sync all of the on-chain info required to display a known point
21 // sync all of the on-chain info required to display a known point
20 const syncKnownPoint = useCallback(
22 const syncKnownPoint = useCallback(
21− async point => Promise.all([syncBirthday(point)]),
23+ async point => Promise.all([syncBirthday(point), syncRekeyDate(point)]),
22− [syncBirthday]
24+ [syncBirthday, syncRekeyDate]
23 );
25 );
24
26
25 // sync all of the on-chain info required to display a foreign point
27 // sync all of the on-chain info required to display a foreign point
@@ -39,6 +41,7 @@ export default function usePointStore() {
39 return {
41 return {
40 ...details,
42 ...details,
41 ...birthdays,
43 ...birthdays,
44+ ...rekeyDates,
42 ...invites,
45 ...invites,
43 ...controlledPoints,
46 ...controlledPoints,
44 ...ecliptic,
47 ...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() {
70 const inviteWallet = walletFromMnemonic(seed, DEFAULT_HD_PATH);
70 const inviteWallet = walletFromMnemonic(seed, DEFAULT_HD_PATH);
71 const _inviteWallet = need.wallet(inviteWallet);
71 const _inviteWallet = need.wallet(inviteWallet);
72
72
73− const incoming = await azimuth.azimuth.getTransferringFor(
73+ const owned = await azimuth.azimuth.getOwnedPoints(
74 _contracts,
74 _contracts,
75 _inviteWallet.address
75 _inviteWallet.address
76 );
76 );
77+ const transferring = await azimuth.azimuth.getTransferringFor(
78+ _contracts,
79+ _inviteWallet.address
80+ );
81+ const incoming = [...owned, ...transferring];
77
82
78 let realPoint = Maybe.Nothing();
83 let realPoint = Maybe.Nothing();
79 let wallet = Maybe.Nothing();
84 let wallet = Maybe.Nothing();
src/views/Activate/PassportDownload.js+4−5
@@ -2,13 +2,13 @@ import React, { useState, useEffect, useCallback } from 'react';
2import cn from 'classnames';
2import cn from 'classnames';
3import Maybe from 'folktale/maybe';
3import Maybe from 'folktale/maybe';
4import { Grid, H4, P } from 'indigo-react';
4import { Grid, H4, P } from 'indigo-react';
5−import PaperCollateralRenderer from 'PaperCollateralRenderer';
6
5
7import * as need from 'lib/need';
6import * as need from 'lib/need';
8
7
9import { useActivateFlow } from './ActivateFlow';
8import { useActivateFlow } from './ActivateFlow';
10import { downloadWallet } from 'lib/invite';
9import { downloadWallet } from 'lib/invite';
11import { DownloadButton, ForwardButton } from 'components/Buttons';
10import { DownloadButton, ForwardButton } from 'components/Buttons';
11+import PaperRenderer from 'components/PaperRenderer';
12import { useLocalRouter } from 'lib/LocalRouter';
12import { useLocalRouter } from 'lib/LocalRouter';
13import Steps from 'components/Steps';
13import Steps from 'components/Steps';
14
14
@@ -76,11 +76,10 @@ export default function PassportDownload({ className }) {
76 solid>
76 solid>
77 {!downloaded ? 'Download Passport' : 'Continue'}
77 {!downloaded ? 'Download Passport' : 'Continue'}
78 </Grid.Item>
78 </Grid.Item>
79− <PaperCollateralRenderer
79+ <PaperRenderer
80− className="abs super-hidden"
81− wallet={{ [pointAsString]: wallet }}
80+ point={pointAsString}
81+ wallet={wallet}
82 callback={data => setPaper(Maybe.Just(data))}
82 callback={data => setPaper(Maybe.Just(data))}
83− mode="REGISTRATION"
84 />
83 />
85 </Grid>
84 </Grid>
86 );
85 );
src/views/Activate/PassportTransfer.js+1
@@ -99,6 +99,7 @@ export default function PassportTransfer({ className, resetActivateRouter }) {
99 web3: _web3,
99 web3: _web3,
100 contracts: _contracts,
100 contracts: _contracts,
101 onUpdate: handleUpdate,
101 onUpdate: handleUpdate,
102+ transferEth: true,
102 });
103 });
103
104
104 // set the global wallet
105 // 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';
18import { NETWORK_TYPES } from '../lib/network';
18import { NETWORK_TYPES } from '../lib/network';
19import { canDecodePatp } from '../lib/txn';
19import { canDecodePatp } from '../lib/txn';
20
20
21−import { ETH_ZERO_ADDR, isValidAddress, eqAddr } from '../lib/wallet';
21+import { isValidAddress, isZeroAddress } from '../lib/wallet';
22
22
23import { isValidGalaxy, compose } from '../lib/lib';
23import { isValidGalaxy, compose } from '../lib/lib';
24import { withNetwork } from '../store/network';
24import { withNetwork } from '../store/network';
@@ -103,7 +103,7 @@ class CreateGalaxy extends React.Component {
103 galaxyDec
103 galaxyDec
104 );
104 );
105
105
106− const available = eqAddr(currentOwner, ETH_ZERO_ADDR);
106+ const available = isZeroAddress(currentOwner);
107
107
108 this.setState({ isAvailable: available });
108 this.setState({ isAvailable: available });
109 };
109 };
src/views/Invite/InviteEmail.js+2−2
@@ -35,7 +35,7 @@ import LoadableButton from 'components/LoadableButton';
35import useArray from 'lib/useArray';
35import useArray from 'lib/useArray';
36import useForm from 'indigo-react/lib/useForm';
36import useForm from 'indigo-react/lib/useForm';
37import { buildEmailInputConfig } from 'components/Inputs';
37import { buildEmailInputConfig } from 'components/Inputs';
38−import { MIN_PLANET } from 'lib/constants';
38+import { MIN_PLANET, GAS_LIMITS } from 'lib/constants';
39import Highlighted from 'components/Highlighted';
39import Highlighted from 'components/Highlighted';
40import { useNetwork } from 'store/network';
40import { useNetwork } from 'store/network';
41import { usePointCache } from 'store/pointCache';
41import { usePointCache } from 'store/pointCache';
@@ -45,7 +45,7 @@ import pluralize from 'lib/pluralize';
45import useMailer from 'lib/useMailer';
45import useMailer from 'lib/useMailer';
46
46
47const GAS_PRICE_GWEI = 20; // we pay the premium for faster ux
47const GAS_PRICE_GWEI = 20; // we pay the premium for faster ux
48−const GAS_LIMIT = 400000;
48+const GAS_LIMIT = GAS_LIMITS.GIFT_PLANET;
49const INVITE_COST = toWei((GAS_PRICE_GWEI * GAS_LIMIT).toString(), 'gwei');
49const INVITE_COST = toWei((GAS_PRICE_GWEI * GAS_LIMIT).toString(), 'gwei');
50const HAS_RECEIVED_TEXT = 'This email has already received an invite.';
50const HAS_RECEIVED_TEXT = 'This email has already received an invite.';
51
51
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';
4import { Grid } from 'indigo-react';
4import { Grid } from 'indigo-react';
5
5
6import { usePointCursor } from 'store/pointCursor';
6import { usePointCursor } from 'store/pointCursor';
7+import { usePointCache } from 'store/pointCache';
7import { useHistory } from 'store/history';
8import { useHistory } from 'store/history';
8
9
9import View from 'components/View';
10import View from 'components/View';
@@ -15,6 +16,7 @@ import { matchBlinky } from 'components/Blinky';
15import useInvites from 'lib/useInvites';
16import useInvites from 'lib/useInvites';
16import { useSyncOwnedPoints } from 'lib/useSyncPoints';
17import { useSyncOwnedPoints } from 'lib/useSyncPoints';
17import { ROUTE_NAMES } from 'lib/routeNames';
18import { ROUTE_NAMES } from 'lib/routeNames';
19+import { eqAddr } from 'lib/wallet';
18
20
19import Actions from './Point/Actions';
21import Actions from './Point/Actions';
20import { useWallet } from 'store/wallet';
22import { useWallet } from 'store/wallet';
@@ -22,16 +24,34 @@ import { useWallet } from 'store/wallet';
22export default function Point() {
24export default function Point() {
23 const history = useHistory();
25 const history = useHistory();
24 const { pointCursor } = usePointCursor();
26 const { pointCursor } = usePointCursor();
27+ const { pointCache } = usePointCache();
25 const { wallet } = useWallet();
28 const { wallet } = useWallet();
26
29
27 const point = need.point(pointCursor);
30 const point = need.point(pointCursor);
28
31
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+
29 // fetch the invites for the current cursor
47 // fetch the invites for the current cursor
30 const { availableInvites } = useInvites(point);
48 const { availableInvites } = useInvites(point);
31 const availableInvitesText = matchBlinky(availableInvites);
49 const availableInvitesText = matchBlinky(availableInvites);
32
50
33 const showActions = Maybe.Just.hasInstance(wallet);
51 const showActions = Maybe.Just.hasInstance(wallet);
34
52
53+ const goAdmin = useCallback(() => history.push(ROUTE_NAMES.ADMIN), [history]);
54+
35 const goInvite = useCallback(() => history.push(ROUTE_NAMES.INVITE), [
55 const goInvite = useCallback(() => history.push(ROUTE_NAMES.INVITE), [
36 history,
56 history,
37 ]);
57 ]);
@@ -43,14 +63,16 @@ export default function Point() {
43 <View>
63 <View>
44 <Passport point={Maybe.Just(point)} />
64 <Passport point={Maybe.Just(point)} />
45 <Grid className="pt2">
65 <Grid className="pt2">
46− <Grid.Item full>
66+ <Grid.Item
67+ full
47− <ForwardButton disabled>Admin</ForwardButton>
68+ as={ForwardButton}
69+ disabled={!canAdmin}
70+ onClick={goAdmin}>
71+ Admin
48 </Grid.Item>
72 </Grid.Item>
49 <Grid.Divider />
73 <Grid.Divider />
50− <Grid.Item full>
51− <ForwardButton detail="Boot your computer" disabled>
74+ <Grid.Item full as={ForwardButton} detail="Boot your computer" disabled>
52− Boot Arvo
75+ Boot Arvo
53− </ForwardButton>
54 </Grid.Item>
76 </Grid.Item>
55 {showActions && (
77 {showActions && (
56 <Grid.Item full>
78 <Grid.Item full>
src/views/Point/Actions.js+2−2
@@ -2,7 +2,7 @@ import React from 'react';
2import { azimuth } from 'azimuth-js';
2import { azimuth } from 'azimuth-js';
3import * as need from '../../lib/need';
3import * as need from '../../lib/need';
4
4
5−import { ETH_ZERO_ADDR, CURVE_ZERO_ADDR, eqAddr } from '../../lib/wallet';
5+import { CURVE_ZERO_ADDR, isZeroAddress, eqAddr } from '../../lib/wallet';
6import { H2, P } from '../../components/old/Base';
6import { H2, P } from '../../components/old/Base';
7import { Button } from '../../components/old/Base';
7import { Button } from '../../components/old/Base';
8import { ROUTE_NAMES } from '../../lib/routeNames';
8import { ROUTE_NAMES } from '../../lib/routeNames';
@@ -89,7 +89,7 @@ function Actions() {
89 Nothing: () => false,
89 Nothing: () => false,
90 Just: deets =>
90 Just: deets =>
91 eqAddr(deets.value.owner, addr) &&
91 eqAddr(deets.value.owner, addr) &&
92− !eqAddr(deets.value.transferProxy, ETH_ZERO_ADDR),
92+ !isZeroAddress(deets.value.transferProxy),
93 });
93 });
94
94
95 const displayReminder = pointDetails.matchWith({
95 const displayReminder = pointDetails.matchWith({
src/views/Point/KeysAndMetadata.js+3−6
@@ -1,14 +1,11 @@
1import React from 'react';
1import React from 'react';
2import { H2, H4 } from '../../components/old/Base';
2import { H2, H4 } from '../../components/old/Base';
3
3
4−import { ETH_ZERO_ADDR, eqAddr } from '../../lib/wallet';
4+import { CURVE_ZERO_ADDR, isZeroAddress } from '../../lib/wallet';
5import { Grid } from 'indigo-react';
5import { Grid } from 'indigo-react';
6
6
7−const NULL_KEY =
8− '0x0000000000000000000000000000000000000000000000000000000000000000';
9−
10const renderAddress = addr =>
7const renderAddress = addr =>
11− eqAddr(addr, ETH_ZERO_ADDR) ? (
8+ isZeroAddress(addr) ? (
12 <div>{'(not set)'}</div>
9 <div>{'(not set)'}</div>
13 ) : (
10 ) : (
14 <div>
11 <div>
@@ -27,7 +24,7 @@ const renderAddress = addr =>
27const renderNetworkKey = hex => {
24const renderNetworkKey = hex => {
28 const sl = i => hex.slice(i, i + 4);
25 const sl = i => hex.slice(i, i + 4);
29 const rowFrom = i => `${sl(i)}.${sl(i + 4)}.${sl(i + 8)}.${sl(i + 12)}`;
26 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 ? (
31 <div>{'(not set)'}</div>
28 <div>{'(not set)'}</div>
32 ) : (
29 ) : (
33 <div>
30 <div>
src/views/Points.js+2−2
@@ -3,7 +3,7 @@ import Maybe from 'folktale/maybe';
3import { H1, H2, P, Grid } from 'indigo-react';
3import { H1, H2, P, Grid } from 'indigo-react';
4
4
5import { ROUTE_NAMES } from 'lib/routeNames';
5import { ROUTE_NAMES } from 'lib/routeNames';
6−import { ETH_ZERO_ADDR, eqAddr } from 'lib/wallet';
6+import { isZeroAddress } from 'lib/wallet';
7import useIsEclipticOwner from 'lib/useIsEclipticOwner';
7import useIsEclipticOwner from 'lib/useIsEclipticOwner';
8import { useSyncKnownPoints } from 'lib/useSyncPoints';
8import { useSyncKnownPoints } from 'lib/useSyncPoints';
9
9
@@ -20,7 +20,7 @@ const matchArray = obj =>
20 obj.matchWith({ Nothing: () => [], Just: p => p.value });
20 obj.matchWith({ Nothing: () => [], Just: p => p.value });
21
21
22const hasTransferProxy = (details, point) =>
22const hasTransferProxy = (details, point) =>
23− !eqAddr(details.transferProxy, ETH_ZERO_ADDR);
23+ !isZeroAddress(details.transferProxy);
24
24
25export default function Points() {
25export default function Points() {
26 const { wallet } = useWallet();
26 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+}