on master help sign in
Merge branch 'master' into hm/invites-refactor
commitc635e6986a3527b2310d9439670e85cf44a83179 authorHunter Miller <hunter@thisdot.co> dateTue 15 Feb 2022, 15:55 (5 years ago) parent807883b9875f17
19 files changed+500−180
package-lock.json+8−3 package.json+3−2 src/Bridge.js+8 src/lib/constants.ts+4 src/lib/reticket.js+14−17 src/lib/types/L2Transaction.ts+36−2 src/lib/types/Point.ts+4 src/lib/useReticketL2Spawn.ts+134 src/lib/useRoller.ts+20−39 src/lib/useRollerPoller.ts+42 src/lib/utils/roller.ts+38−14 src/store/lib/useControlledPointsStore.js+3−3 src/store/network.js+3−1 src/views/Invite/Cohort.tsx+2−3 src/views/Login/LoginSelector.tsx+10−5 src/views/Login/Metamask.js+3−1 src/views/Points.scss+11 src/views/Points.tsx+17−13 src/views/UrbitID/ResetKeys/ResetExecute.js → src/views/UrbitID/ResetKeys/ResetExecute.tsx+140−77
package-lock.json+8−3
@@ -4720,6 +4720,11 @@
47204720 "resolved": "https://registry.npmjs.org/@ledgerhq/logs/-/logs-6.2.0.tgz",
47214721 "integrity": "sha512-SLyFyD7ElMhgKWPYedFGCT/ilcbGPgL5hXXYHxOM79Fs5fWi0zaUpt5oGqGMsOAAFaMa9/rbun0pokzPhEFz8A=="
47224722 },
4723+ "@metamask/detect-provider": {
4724+ "version": "1.2.0",
4725+ "resolved": "https://registry.npmjs.org/@metamask/detect-provider/-/detect-provider-1.2.0.tgz",
4726+ "integrity": "sha512-ocA76vt+8D0thgXZ7LxFPyqw3H7988qblgzddTDA6B8a/yU0uKV42QR/DhA+Jh11rJjxW0jKvwb5htA6krNZDQ=="
4727+ },
47234728 "@microsoft/fetch-event-source": {
47244729 "version": "2.0.1",
47254730 "resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz",
@@ -7775,9 +7780,9 @@
77757780 }
77767781 },
77777782 "@urbit/roller-api": {
7778− "version": "0.1.14",
7779− "resolved": "https://registry.npmjs.org/@urbit/roller-api/-/roller-api-0.1.14.tgz",
7780− "integrity": "sha512-8I26tia2n6+7IbSl7pVhW7PSaDVFH+QA4Y4RJvgnYpdQDTpry4cXFynvsiTtlU5p97csBd+hXdNub7JDon4LKg==",
7783+ "version": "0.1.15",
7784+ "resolved": "https://registry.npmjs.org/@urbit/roller-api/-/roller-api-0.1.15.tgz",
7785+ "integrity": "sha512-pP+XtpKOlSdx25+/lHPRtCihLSlVnOXLUk6igA2tCFMdo0cgrH1fCREoT3GlqkwhJQBq+VY08gnyD7Kn58i/Ug==",
77817786 "requires": {
77827787 "@open-rpc/client-js": "1.7.1",
77837788 "@open-rpc/meta-schema": "1.14.2",
package.json+3−2
@@ -1,6 +1,6 @@
11{
22 "name": "bridge",
3− "version": "3.0.0b",
3+ "version": "3.0.0",
44 "description": "An application for interacting with Azimuth",
55 "repository": {
66 "type": "git",
@@ -18,6 +18,7 @@
1818 "@ethereumjs/tx": "^3.3.0",
1919 "@ledgerhq/hw-app-eth": "^6.5.0",
2020 "@ledgerhq/hw-transport-u2f": "^5.36.0-deprecated",
21+ "@metamask/detect-provider": "^1.2.0",
2122 "@reach/disclosure": "^0.15.0",
2223 "@reach/menu-button": "^0.15.1",
2324 "@reach/tabs": "^0.15.0",
@@ -32,7 +33,7 @@
3233 "@types/styled-components": "^5.1.10",
3334 "@types/styled-system": "^5.1.11",
3435 "@types/styled-system__css": "^5.0.15",
35− "@urbit/roller-api": "^0.1.14",
36+ "@urbit/roller-api": "^0.1.15",
3637 "@walletconnect/client": "^1.7.1",
3738 "@walletconnect/qrcode-modal": "^1.7.1",
3839 "@welldone-software/why-did-you-render": "^3.2.3",
src/Bridge.js+8
@@ -25,6 +25,7 @@ import Modal from 'components/L2/Modal';
2525import { useRollerStore } from 'store/rollerStore';
2626import { Box } from '@tlon/indigo-react';
2727import LoadingOverlay from 'components/L2/LoadingOverlay';
28+import { useRollerPoller } from 'lib/useRollerPoller';
2829
2930const INITIAL_NETWORK_TYPE = isRopsten
3031 ? NETWORK_TYPES.ROPSTEN
@@ -72,6 +73,12 @@ function useInitialRoutes() {
7273 return [{ key: ROUTE_NAMES.LOGIN }];
7374}
7475
76+function Poller() {
77+ useRollerPoller();
78+
79+ return null;
80+}
81+
7582function Bridge() {
7683 const initialRoutes = useInitialRoutes();
7784 const { loading, modalText, setModalText } = useRollerStore();
@@ -105,6 +112,7 @@ function Bridge() {
105112 <Box width="280px">{modalText}</Box>
106113 </Modal>
107114 <LoadingOverlay loading={loading} />
115+ <Poller />
108116 </Provider>
109117 </WithErrorBoundary>
110118 );
src/lib/constants.ts+4
@@ -219,6 +219,9 @@ const PASSPORT_TOOLTIP =
219219const LOCKUP_TOOLTIP =
220220 'Stars in a lockup schedule can be withdrawn after a set time duration. The duration is predetermined by the issuer';
221221
222+// the initial network key revision is always 1
223+const INITIAL_NETWORK_KEY_REVISION = 1;
224+
222225export {
223226 CHECK_BLOCK_EVERY_MS,
224227 DEFAULT_GAS_PRICE_GWEI,
@@ -265,4 +268,5 @@ export {
265268 MASTER_TICKET_TOOLTIP,
266269 PASSPORT_TOOLTIP,
267270 LOCKUP_TOOLTIP,
271+ INITIAL_NETWORK_KEY_REVISION,
268272};
src/lib/reticket.js+14−17
@@ -14,13 +14,10 @@ import {
1414 sendSignedTransaction,
1515} from './txn';
1616import getSuggestedGasPrice from './getSuggestedGasPrice';
17−import { GAS_LIMITS } from './constants';
17+import { GAS_LIMITS, INITIAL_NETWORK_KEY_REVISION } from './constants';
1818import { toBN } from 'web3-utils';
1919import { safeToWei } from './lib';
20−import { isPlanet } from './utils/point';
21−
22−// the initial network key revision is always 1
23−const INITIAL_NETWORK_KEY_REVISION = 1;
20+import { isGalaxy, isPlanet } from './utils/point';
2421
2522export const TRANSACTION_PROGRESS = {
2623 GENERATING: 0.01,
@@ -45,6 +42,7 @@ export async function reticketPointBetweenWallets({
4542 nextRevision = INITIAL_NETWORK_KEY_REVISION,
4643 txnSigner,
4744 txnSender,
45+ doSetSpawnProxy = true,
4846}) {
4947 const askForFunding = (address, minBalance, balance) =>
5048 onUpdate({
@@ -109,8 +107,9 @@ export async function reticketPointBetweenWallets({
109107
110108 // set spawn & voting proxies situationally
111109
110+ // skip this step if reticketing an L1 point with L2-migrated spawn proxy
112111 let txs = [transferTmpTx, keysTx, managementTx];
113− if (!isPlanet(point)) {
112+ if (!isPlanet(point) && doSetSpawnProxy) {
114113 const spawnTx = azimuth.ecliptic.setSpawnProxy(
115114 contracts,
116115 point,
@@ -118,18 +117,16 @@ export async function reticketPointBetweenWallets({
118117 );
119118 spawnTx.gas = GAS_LIMITS.SET_PROXY;
120119 txs.push(spawnTx);
120+ }
121121
122− if (
123− azimuth.azimuth.getPointSize(point) === azimuth.azimuth.PointSize.Galaxy
124− ) {
125− const votingTx = azimuth.ecliptic.setVotingProxy(
126− contracts,
127− point,
128− toWallet.voting.keys.address
129− );
130− votingTx.gas = GAS_LIMITS.SET_PROXY;
131− txs.push(votingTx);
132− }
122+ if (isGalaxy(point)) {
123+ const votingTx = azimuth.ecliptic.setVotingProxy(
124+ contracts,
125+ point,
126+ toWallet.voting.keys.address
127+ );
128+ votingTx.gas = GAS_LIMITS.SET_PROXY;
129+ txs.push(votingTx);
133130 }
134131
135132 // transfer configured point to user's new wallet
src/lib/types/L2Transaction.ts+36−2
@@ -1,4 +1,16 @@
1−import RollerRPCAPI, { EthAddress, Proxy, Ship } from '@urbit/roller-api';
1+import RollerRPCAPI, {
2+ Adopt,
3+ CancelEscape,
4+ ConfigureKeys,
5+ Detach,
6+ Escape,
7+ EthAddress,
8+ Proxy,
9+ Reject,
10+ Ship,
11+ Spawn,
12+ TransferPoint,
13+} from '@urbit/roller-api';
214import WalletConnect from '@walletconnect/client';
315import BridgeWallet from './BridgeWallet';
416
@@ -37,7 +49,28 @@ export interface L2TransactionArgs extends SendL2Params {
3749
3850export interface TransactionData {
3951 data: any;
40− method: Function;
52+ method:
53+ | Spawn
54+ | TransferPoint
55+ | ConfigureKeys
56+ | Escape
57+ | CancelEscape
58+ | Adopt
59+ | Detach
60+ | Reject
61+ | (() => Promise<string>);
62+}
63+
64+export interface ReticketProgressCallback {
65+ ({
66+ type,
67+ state,
68+ value,
69+ }: {
70+ type: string;
71+ state: number;
72+ value?: string;
73+ }): void;
4174}
4275
4376export interface ReticketParams {
@@ -46,4 +79,5 @@ export interface ReticketParams {
4679 manager: EthAddress;
4780 fromWallet: BridgeWallet;
4881 toWallet: UrbitWallet;
82+ onUpdate?: ReticketProgressCallback;
4983}
src/lib/types/Point.ts+4
@@ -69,6 +69,7 @@ interface PointConstructorParams {
6969 details: L1Point;
7070 address: string;
7171 l2Quota?: number;
72+ l2Allowance?: number;
7273 isPlaceholder?: boolean;
7374}
7475
@@ -77,6 +78,7 @@ export default class Point {
7778 patp: string;
7879 layer: 1 | 2;
7980 l2Quota: number;
81+ l2Allowance: number;
8082 active: boolean;
8183 authenticationKey: string;
8284 continuityNumber: string;
@@ -132,6 +134,7 @@ export default class Point {
132134 details,
133135 address,
134136 l2Quota = 0,
137+ l2Allowance = 0,
135138 isPlaceholder = false,
136139 }: PointConstructorParams) {
137140 this.value = value;
@@ -139,6 +142,7 @@ export default class Point {
139142
140143 this.layer = details.layer || 1;
141144 this.l2Quota = l2Quota;
145+ this.l2Allowance = l2Allowance;
142146 this.isL2Spawn = Boolean(details.isL2Spawn);
143147 this.active = details.active;
144148 this.authenticationKey = details.authenticationKey;
src/lib/useReticketL2Spawn.tsnew file+134
@@ -0,0 +1,134 @@
1+import { EthAddress, Ship } from '@urbit/roller-api';
2+import { useCallback } from 'react';
3+import Web3 from 'web3';
4+import { ITxData } from '@walletconnect/types';
5+import { INITIAL_NETWORK_KEY_REVISION } from './constants';
6+import { reticketPointBetweenWallets } from './reticket';
7+import BridgeWallet from './types/BridgeWallet';
8+import { ReticketProgressCallback } from './types/L2Transaction';
9+import useRoller from './useRoller';
10+import { registerProxyAddress } from './utils/roller';
11+import { useWallet } from 'store/wallet';
12+import { useWalletConnect } from './useWalletConnect';
13+
14+interface ReticketL2SpawnParams {
15+ fromWallet: BridgeWallet;
16+ fromWalletType: symbol;
17+ fromWalletHdPath: string;
18+ toWallet: UrbitWallet;
19+ point: Ship;
20+ web3: Web3;
21+ contracts: any; // azimuth contracts
22+ networkType: symbol;
23+ onUpdate?: ReticketProgressCallback;
24+ transferEth?: boolean;
25+ nextRevision: number;
26+ to: EthAddress;
27+ txnSigner?: ({
28+ from,
29+ to,
30+ gas,
31+ gasPrice,
32+ value,
33+ data,
34+ nonce,
35+ }: ITxData) => Promise<string>;
36+ txnSender?: ({
37+ from,
38+ to,
39+ gas,
40+ gasPrice,
41+ value,
42+ data,
43+ nonce,
44+ }: ITxData) => Promise<string>;
45+}
46+
47+/**
48+ * This hook is used to reticket points that are still on L1 but have the
49+ * spawn set to L2. It first performs 3-4 L1 transactions (set keys, set
50+ * management / voting proxy, and transfer).
51+ *
52+ * It then sets the spawn proxy using an L2 transaction.
53+ *
54+ * Under the hood, it wraps two existing functions:
55+ * `reticketPointBetweenWallets` for L1, and `registerProxyAddress` for L2.
56+ *
57+ */
58+export const useReticketL2Spawn = () => {
59+ const { api } = useRoller();
60+ const { walletType }: any = useWallet();
61+ const { connector } = useWalletConnect();
62+
63+ const performL2SpawnReticket = useCallback(
64+ async ({
65+ fromWallet,
66+ fromWalletType,
67+ fromWalletHdPath,
68+ toWallet,
69+ point,
70+ web3,
71+ contracts,
72+ networkType,
73+ onUpdate,
74+ transferEth = false,
75+ nextRevision = INITIAL_NETWORK_KEY_REVISION,
76+ to,
77+ txnSigner,
78+ txnSender,
79+ }: ReticketL2SpawnParams) => {
80+ // call reticketPointBetweenWallets, with isL2Spawn set to true; this will
81+ // skip setting the L1 spawn proxy
82+ await reticketPointBetweenWallets({
83+ fromWallet,
84+ fromWalletType,
85+ fromWalletHdPath,
86+ toWallet,
87+ point,
88+ web3,
89+ contracts,
90+ networkType,
91+ onUpdate,
92+ transferEth,
93+ nextRevision,
94+ txnSigner,
95+ txnSender,
96+ doSetSpawnProxy: false,
97+ });
98+
99+ // instead, set spawn proxy on L2
100+ // In most TX sending cases, we would use the fromWallet to sign and send
101+ // the TX. However, because we are transferring this point during the
102+ // reticket, we will instead sign the final set spawn proxy TX with the
103+ // receiving wallet's private key.
104+
105+ // `registerProxyAddress` expects a toWallet object shaped like a
106+ // BridgeWallet with `address` and `privateKey` properties.
107+ const toWalletStub = {
108+ address: toWallet.ownership.keys.address,
109+ privateKey: Buffer.from(toWallet.ownership.keys.private, 'hex'),
110+ };
111+ const proxy = 'own';
112+ const nonce = await api.getNonce({ ship: point, proxy });
113+
114+ await registerProxyAddress(
115+ api,
116+ toWalletStub,
117+ point,
118+ proxy,
119+ 'spawn',
120+ nonce,
121+ toWallet?.spawn?.keys?.address || to,
122+ walletType,
123+ web3,
124+ connector,
125+ true
126+ );
127+ },
128+ [api, connector, walletType]
129+ );
130+
131+ return {
132+ performL2SpawnReticket,
133+ };
134+};
src/lib/useRoller.ts+20−39
@@ -27,7 +27,6 @@ import {
2727 generateInviteWallet,
2828 getPendingSpawns,
2929 submitL2Transaction,
30− getTimeToNextBatch,
3130 registerProxyAddress,
3231 isL2Spawn,
3332} from './utils/roller';
@@ -44,15 +43,13 @@ import {
4443} from '@urbit/roller-api';
4544
4645import Point, { PointField, Points } from './types/Point';
47−import { useTimerStore } from 'store/timerStore';
4846import { ReticketParams, SendL2Params } from './types/L2Transaction';
4947import { L1Point } from './types/L1Point';
5048import { ddmmmYYYY } from './utils/date';
5149import { showNotification } from './utils/notifications';
5250import { useWalletConnect } from './useWalletConnect';
5351import { PendingL1Txn } from './types/PendingL1Transaction';
54−
55−const ONE_SECOND = 1000;
52+import { TRANSACTION_PROGRESS } from './reticket';
5653
5754interface UpdateParams {
5855 point: number;
@@ -89,10 +86,7 @@ export default function useRoller() {
8986 const allPoints: any = usePointCache();
9087 const getDetails = allPoints?.getDetails;
9188
92− const { setNextRoll } = useTimerStore();
93−
9489 const {
95− nextBatchTime,
9690 point,
9791 points,
9892 pointList,
@@ -162,7 +156,7 @@ export default function useRoller() {
162156 );
163157
164158 const fetchConfig = useCallback(async () => {
165− getNextQuotaTime(1);
159+ getNextQuotaTime(Date.now() + 1000);
166160 api
167161 .getRollerConfig()
168162 .then(config => {
@@ -182,12 +176,12 @@ export default function useRoller() {
182176 if (point.l2Quota <= 0) {
183177 setModalText(
184178 `You have reached your weekly L2 transaction limit. You will get another
185− ${config?.rollerQuota} transactions on ${ddmmmYYYY(nextQuotaTime)}.`
179+ ${point.l2Allowance} transactions on ${ddmmmYYYY(nextQuotaTime)}.`
186180 );
187181 }
188182
189183 return point.l2Quota <= 0;
190− }, [config, nextQuotaTime, point, setModalText]);
184+ }, [nextQuotaTime, point, setModalText]);
191185
192186 const initPoint = useCallback(
193187 async (point: string | number) => {
@@ -201,9 +195,9 @@ export default function useRoller() {
201195 const pointNum = Number(point);
202196 try {
203197 const rawDetails = await api.getPoint(pointNum);
204− const l2Quota = isL2Spawn(rawDetails?.dominion)
205− ? await api.getRemainingQuota(pointNum)
206− : 0;
198+ const isL2 = isL2Spawn(rawDetails?.dominion);
199+ const l2Quota = isL2 ? await api.getRemainingQuota(pointNum) : 0;
200+ const l2Allowance = isL2 ? await api.getAllowance(pointNum) : 0;
207201
208202 const details = isL2Spawn(rawDetails?.dominion)
209203 ? toL1Details(rawDetails)
@@ -214,6 +208,7 @@ export default function useRoller() {
214208 details,
215209 address: _wallet.address,
216210 l2Quota,
211+ l2Allowance,
217212 });
218213 } catch (e) {
219214 console.warn(e);
@@ -697,15 +692,20 @@ export default function useRoller() {
697692 manager,
698693 fromWallet,
699694 toWallet,
695+ onUpdate,
700696 }: ReticketParams) => {
701697 const azimuthPoint = await api.getPoint(point);
702698 const proxy = 'own';
703699 const _web3 = web3.getOrElse(null);
704700 let nonce = await api.getNonce({ ship: point, proxy });
701+ const progress = onUpdate
702+ ? (state: number) => onUpdate({ type: 'progress', state })
703+ : () => {};
705704
706705 let requests = [];
707706
708707 // 1. Update networking keys
708+ progress(TRANSACTION_PROGRESS.GENERATING);
709709 const networkRevision = convertToInt(azimuthPoint.network.keys.life, 10);
710710 const nextRevision = networkRevision + 1;
711711 const networkSeed = await deriveNetworkSeedFromUrbitWallet(
@@ -728,6 +728,7 @@ export default function useRoller() {
728728 requests.push(configureKeysRequest);
729729
730730 // 2. Set Management Proxy
731+ progress(TRANSACTION_PROGRESS.SIGNING);
731732 const registerMgmtRequest = await registerProxyAddress(
732733 api,
733734 fromWallet,
@@ -744,6 +745,7 @@ export default function useRoller() {
744745 requests.push(registerMgmtRequest);
745746
746747 // 3. Set Spawn Proxy
748+ progress(TRANSACTION_PROGRESS.FUNDING);
747749 if (!isPlanet(Number(point))) {
748750 const registerSpawnRequest = await registerProxyAddress(
749751 api,
@@ -760,7 +762,9 @@ export default function useRoller() {
760762 nonce = nonce + 1;
761763 requests.push(registerSpawnRequest);
762764 }
765+
763766 // 4. Transfer point
767+ progress(TRANSACTION_PROGRESS.TRANSFERRING);
764768 const transferTxRequest = await submitL2Transaction({
765769 api,
766770 wallet: fromWallet,
@@ -775,7 +779,10 @@ export default function useRoller() {
775779 });
776780 requests.push(transferTxRequest);
777781
782+ progress(TRANSACTION_PROGRESS.CLEANING);
778783 const hashes = await Promise.all(requests);
784+
785+ progress(TRANSACTION_PROGRESS.DONE);
779786 return hashes;
780787 };
781788
@@ -1034,32 +1041,6 @@ export default function useRoller() {
10341041 fetchConfig();
10351042 }, [config, fetchConfig]);
10361043
1037− useEffect(() => {
1038− const time = ONE_SECOND;
1039−
1040− const interval = setInterval(() => {
1041− const nextRoll = getTimeToNextBatch(nextBatchTime, new Date().getTime());
1042− setNextRoll(nextRoll);
1043−
1044− if (nextBatchTime - ONE_SECOND <= new Date().getTime()) {
1045− api.getRollerConfig().then(response => {
1046− setNextBatchTime(response.nextBatch);
1047− });
1048−
1049− getPendingTransactions();
1050−
1051− setTimeout(() => {
1052− if (!point.isDefault) {
1053− console.log('getting invites from batch time');
1054− getInvites(); // This will also get pending txns
1055− }
1056− }, TEN_SECONDS); // Should this be more like a minute?
1057− }
1058− }, time);
1059−
1060− return () => clearInterval(interval);
1061− }, [point, nextBatchTime, getPendingTransactions, getInvites]); // eslint-disable-line react-hooks/exhaustive-deps
1062−
10631044 return {
10641045 api,
10651046 changeSponsorship,
src/lib/useRollerPoller.tsnew file+42
@@ -0,0 +1,42 @@
1+import { useEffect, useState } from 'react';
2+import { useRollerStore } from 'store/rollerStore';
3+import { useTimerStore } from 'store/timerStore';
4+import { ONE_SECOND, TEN_SECONDS } from './constants';
5+import useRoller from './useRoller';
6+import { getTimeToNextBatch } from './utils/roller';
7+
8+export function useRollerPoller() {
9+ const { nextBatchTime, setNextBatchTime, setNextRoll } = useTimerStore();
10+ const { point } = useRollerStore();
11+ const { api, getInvites, getPendingTransactions } = useRoller();
12+ const [time, setTime] = useState(ONE_SECOND);
13+
14+ useEffect(() => {
15+ const interval = setInterval(async () => {
16+ const nextRoll = getTimeToNextBatch(nextBatchTime, new Date().getTime());
17+ setNextRoll(nextRoll);
18+
19+ if (nextBatchTime - ONE_SECOND <= new Date().getTime()) {
20+ const response = await api.getRollerConfig();
21+
22+ if (response.nextBatch === nextBatchTime) {
23+ // exponentially back off or cap at polling every minute
24+ setTime(Math.min(60 * ONE_SECOND, time + time));
25+ return;
26+ }
27+
28+ setTime(ONE_SECOND); // reset the timer
29+ setNextBatchTime(response.nextBatch);
30+ getPendingTransactions();
31+
32+ setTimeout(() => {
33+ if (!point.isDefault) {
34+ getInvites(); // This will also get pending txns
35+ }
36+ }, TEN_SECONDS); // Should this be more like a minute?
37+ }
38+ }, time);
39+
40+ return () => clearInterval(interval);
41+ }, [time, point, nextBatchTime, getPendingTransactions, getInvites]); // eslint-disable-line react-hooks/exhaustive-deps
42+}
src/lib/utils/roller.ts+38−14
@@ -66,7 +66,19 @@ export const generateHashAndSign = async (
6666) => {
6767 if (walletType === WALLET_TYPES.METAMASK) {
6868 const hash = await api.prepareForSigning(nonce, from, type, data);
69− const sig = await web3.eth.personal.sign(hash, wallet.address, '');
69+ let sig;
70+
71+ // same issue as seen in getMetamaskAuthToken
72+ if (window.ethereum) {
73+ sig = window.ethereum.request({
74+ method: 'personal_sign',
75+ params: [hash, wallet.address],
76+ from: wallet.address,
77+ });
78+ } else {
79+ sig = await web3.eth.personal.sign(hash, wallet.address, '');
80+ }
81+
7082 return sig;
7183 } else if (walletType === WALLET_TYPES.WALLET_CONNECT) {
7284 if (!connector || !connector.connected) {
@@ -93,7 +105,8 @@ export const configureKeys = async (
93105 walletType: symbol,
94106 web3: any,
95107 connector: WalletConnect | null,
96− breach?: boolean
108+ breach?: boolean,
109+ force: boolean = false
97110) => {
98111 const from = {
99112 ship: point, //ship to configure keys
@@ -130,7 +143,7 @@ export const configureKeys = async (
130143 web3,
131144 connector
132145 );
133− return api.configureKeys(sig, from, wallet.address, data);
146+ return api.configureKeys(sig, force, from, wallet.address, data);
134147};
135148
136149export const transferPointRequest = async (
@@ -143,7 +156,8 @@ export const transferPointRequest = async (
143156 walletType: symbol,
144157 web3: any,
145158 connector: WalletConnect | null,
146− reset?: boolean
159+ reset?: boolean,
160+ force: boolean = false
147161) => {
148162 const from = {
149163 ship: pointToTransfer,
@@ -167,7 +181,7 @@ export const transferPointRequest = async (
167181 connector
168182 );
169183
170− return api.transferPoint(sig, from, wallet.address, data);
184+ return api.transferPoint(sig, force, from, wallet.address, data);
171185};
172186
173187const proxyType = (proxy: Proxy) => {
@@ -189,15 +203,16 @@ const setProxy = async (
189203 sig: Signature,
190204 from: From,
191205 address: EthAddress,
192− data: AddressParams
206+ data: AddressParams,
207+ force: boolean = false
193208) => {
194209 switch (proxyAddressType) {
195210 case 'manage':
196− return await api.setManagementProxy(sig, from, address, data);
211+ return await api.setManagementProxy(sig, force, from, address, data);
197212 case 'spawn':
198− return await api.setSpawnProxy(sig, from, address, data);
213+ return await api.setSpawnProxy(sig, force, from, address, data);
199214 case 'transfer':
200− return await api.setTransferProxy(sig, from, address, data);
215+ return await api.setTransferProxy(sig, force, from, address, data);
201216 default:
202217 throw new Error(`Unknown proxyType ${proxyAddressType}`);
203218 }
@@ -205,7 +220,7 @@ const setProxy = async (
205220
206221export const registerProxyAddress = async (
207222 api: RollerRPCAPI,
208− wallet: any,
223+ wallet: any, // BridgeWallet
209224 _point: Ship,
210225 proxy: string,
211226 proxyAddressType: string,
@@ -213,7 +228,8 @@ export const registerProxyAddress = async (
213228 address: string,
214229 walletType: symbol,
215230 web3: any,
216− connector: WalletConnect | null
231+ connector: WalletConnect | null,
232+ force?: boolean
217233) => {
218234 const from = {
219235 ship: _point,
@@ -233,7 +249,15 @@ export const registerProxyAddress = async (
233249 connector
234250 );
235251
236− return setProxy(api, proxyAddressType, sig, from, wallet.address, data);
252+ return setProxy(
253+ api,
254+ proxyAddressType,
255+ sig,
256+ from,
257+ wallet.address,
258+ data,
259+ force
260+ );
237261};
238262
239263const getDataAndMethod = (args: L2TransactionArgs) => {
@@ -248,7 +272,7 @@ const getDataAndMethod = (args: L2TransactionArgs) => {
248272 sponsee,
249273 ship,
250274 } = args;
251− const result: TransactionData = { data: { address }, method: () => null };
275+ const result: TransactionData = { data: { address }, method: async () => '' };
252276
253277 switch (args.type) {
254278 case 'transferPoint':
@@ -336,7 +360,7 @@ export const submitL2Transaction = async (args: L2TransactionArgs) => {
336360 connector
337361 );
338362
339− return method(sig, from, wallet.address, data);
363+ return method(sig, false, from, wallet.address, data);
340364};
341365
342366export const reticketL2Point = async () => {};
src/store/lib/useControlledPointsStore.js+3−3
@@ -10,7 +10,7 @@ import { useNetwork } from '../network';
1010import { ONE_MINUTE } from 'lib/constants';
1111
1212export default function useControlledPointsStore() {
13− const { contracts } = useNetwork();
13+ const { contracts, web3 } = useNetwork();
1414 const { wallet } = useWallet();
1515 const { getPoints, getPointsDetails } = useRoller();
1616
@@ -19,7 +19,7 @@ export default function useControlledPointsStore() {
1919 const syncControlledPoints = useCallback(async () => {
2020 const _contracts = contracts.getOrElse(null);
2121 const _wallet = wallet.getOrElse(null);
22− if (!_contracts || !_wallet) {
22+ if (!web3 || Nothing.hasInstance(web3) || !_contracts || !_wallet) {
2323 return;
2424 }
2525
@@ -115,7 +115,7 @@ export default function useControlledPointsStore() {
115115 console.error('failed to fetch controlled points', error);
116116 _setControlledPoints(Just(Result.Error(JSON.stringify(error))));
117117 }
118− }, [contracts, wallet, getPoints, getPointsDetails]);
118+ }, [web3, contracts, wallet, getPoints, getPointsDetails]);
119119
120120 // sync controlled points whenever wallet or contracts changes
121121 useEffect(() => {
src/store/network.js+3−1
@@ -16,6 +16,7 @@ import { isDevelopment } from '../lib/flags';
1616import { Grid } from 'indigo-react';
1717import View from 'components/View';
1818import Blinky from 'components/Blinky';
19+import detectEthereumProvider from '@metamask/detect-provider';
1920
2021function _useNetwork(initialNetworkType = null) {
2122 const [networkType, setNetworkType] = useState(initialNetworkType);
@@ -26,7 +27,8 @@ function _useNetwork(initialNetworkType = null) {
2627
2728 useEffect(() => {
2829 (async () => {
29− if (window.ethereum) {
30+ const provider = await detectEthereumProvider();
31+ if (provider) {
3032 try {
3133 setMetamask(true);
3234 setNetworkType(chainIdToNetworkType(window.ethereum.chainId));
src/views/Invite/Cohort.tsx+2−3
@@ -72,7 +72,6 @@ export default function InviteCohort() {
7272 const { nextRoll } = useTimerStore();
7373
7474 const {
75− config,
7675 ls,
7776 generateInviteCodes,
7877 getPendingTransactions,
@@ -379,7 +378,7 @@ export default function InviteCohort() {
379378 You can generate up to
380379 <strong>{` ${point.l2Quota} `}</strong>
381380 invites. You will be able to generate another
382− <strong>{` ${config?.rollerQuota}`}</strong> invites on
381+ <strong>{` ${point.l2Allowance}`}</strong> invites on
383382 <strong>{` ${ddmmmYYYY(nextQuotaTime)}`}</strong>.
384383 </Box>
385384 )}
@@ -498,7 +497,7 @@ export default function InviteCohort() {
498497 <Box className="invite-limit-message">
499498 You have hit your weekly invite limit. You will be able to generate
500499 another
501− <strong>{` ${config?.rollerQuota}`}</strong> invites on
500+ <strong>{` ${point.l2Allowance}`}</strong> invites on
502501 <strong>{` ${ddmmmYYYY(nextQuotaTime)}`}</strong>.
503502 </Box>
504503 ) : null}
src/views/Login/LoginSelector.tsx+10−5
@@ -79,14 +79,19 @@ export default function LoginSelector({
7979
8080 const selectMetamask = useCallback(async () => {
8181 try {
82+ if (!window.ethereum) {
83+ throw new Error(
84+ 'Device does not have an Ethereum provider available (is Metamask or Brave Wallet installed?)'
85+ );
86+ }
8287 setMetamask(true);
8388 setMetamaskSelected(true);
84− const accounts = await (window as any).ethereum.send(
85− 'eth_requestAccounts'
86− );
87− const wallet = new MetamaskWallet(accounts.result[0]);
89+ const accounts = await window.ethereum.request({
90+ method: 'eth_requestAccounts',
91+ });
92+ const wallet = new MetamaskWallet(accounts[0]);
8893
89− const web3 = new Web3((window as any).ethereum);
94+ const web3 = new Web3(window.ethereum);
9095 const authToken = await getAuthToken({
9196 address: wallet.address,
9297 walletType: WALLET_TYPES.METAMASK,
src/views/Login/Metamask.js+3−1
@@ -33,7 +33,9 @@ export default function Metamask({ className, goHome }) {
3333 const onSubmit = useCallback(async () => {
3434 try {
3535 setMetamask(true);
36− const accounts = await window.ethereum.send('eth_requestAccounts');
36+ const accounts = await window.ethereum.request({
37+ method: 'eth_requestAccounts',
38+ });
3739 const wallet = new MetamaskWallet(accounts.result[0]);
3840
3941 const web3 = new Web3(window.ethereum);
src/views/Points.scss+11
@@ -15,3 +15,14 @@
1515 font-family: 'Inter';
1616 }
1717}
18+
19+div.migrated-info-box {
20+ margin-top: 30px;
21+ font-size: 14px;
22+ display: flex;
23+ flex-flow: row nowrap;
24+
25+ span.message {
26+ padding-left: 5px;
27+ }
28+}
src/views/Points.tsx+17−13
@@ -28,11 +28,12 @@ import L2PointHeader from 'components/L2/Headers/L2PointHeader';
2828import IncomingPoint from 'components/L2/Points/IncomingPoint';
2929import LoadingOverlay from 'components/L2/LoadingOverlay';
3030import PointList from 'components/L2/PointList';
31−import { Box, Text } from '@tlon/indigo-react';
31+import { Box, Icon, Text } from '@tlon/indigo-react';
3232
3333import './Points.scss';
3434import { StarReleaseButton } from './Points/StarReleaseButton';
3535import { maybeGetResult } from 'lib/maybeGetResult';
36+import { isGalaxy, isStar } from 'lib/utils/point';
3637
3738export const isLocked = (details: any, contracts: any) =>
3839 details.owner === contracts.linearSR ||
@@ -172,6 +173,11 @@ export default function Points() {
172173 const displayEmptyState =
173174 !loading && incomingPoints.length === 0 && allPoints.length === 0;
174175
176+ // If the user has any stars or galaxies, give a polite heads up that
177+ // it can take some time for the roller be aware of L1 TXs
178+ const showMigrateNotice = !displayEmptyState &&
179+ allPoints.some(p => isStar(p.value) || isGalaxy(p.value));
180+
175181 const starReleasing = starReleaseDetails
176182 .map((s: any) => (s ? s.total > 0 : false))
177183 .getOrElse(false);
@@ -324,19 +330,17 @@ export default function Points() {
324330 </Grid.Item>
325331 )}
326332
327− {/* {processingPoints.length > 0 && (
328− <Grid.Item full as={Grid} gap={1} className="mv6">
329− <Grid.Item full as={H5}>
330− Awaiting L2 Rollup
331− </Grid.Item>
332− <Grid.Item
333− full
334− as={PointList}
335− points={processingPoints}
336− processing
337− />
333+ {showMigrateNotice && (
334+ <Grid.Item full as={Box} className={'migrated-info-box'}>
335+ <span className={'icon'}>
336+ <Icon icon="Info" />
337+ </span>
338+ <span className={'message'}>
339+ Not seeing a recently-migrated Layer 2 point? It may take a few
340+ minutes for the public roller to be aware of the transaction.
341+ </span>
338342 </Grid.Item>
339− )} */}
343+ )}
340344
341345 <Footer>
342346 <Grid>
src/views/UrbitID/ResetKeys/ResetExecute.js → src/views/UrbitID/ResetKeys/ResetExecute.tsxrenamed+140−77
@@ -7,9 +7,10 @@ import {
77 TRANSACTION_PROGRESS,
88} from 'lib/reticket';
99import * as need from 'lib/need';
10−import { WALLET_TYPES } from 'lib/constants';
10+import { POINT_DOMINIONS, WALLET_TYPES } from 'lib/constants';
1111import { convertToInt } from 'lib/convertToInt';
1212import useBlockWindowClose from 'lib/useBlockWindowClose';
13+import { useReticketL2Spawn } from 'lib/useReticketL2Spawn';
1314
1415import { useNetwork } from 'store/network';
1516import { useWallet } from 'store/wallet';
@@ -25,8 +26,9 @@ import { RestartButton, ForwardButton } from 'components/Buttons';
2526import WarningBox from 'components/WarningBox';
2627import LoadingBar from 'components/LoadingBar';
2728import NeedFundsNotice from 'components/NeedFundsNotice';
29+import { ReticketProgressCallback } from 'lib/types/L2Transaction';
2830
29−const labelForProgress = progress => {
31+const labelForProgress = (progress: number) => {
3032 if (progress <= 0) {
3133 return 'Starting...';
3234 } else if (progress <= TRANSACTION_PROGRESS.GENERATING) {
@@ -44,9 +46,19 @@ const labelForProgress = progress => {
4446 }
4547};
4648
47−export default function ResetExecute({ newWallet, setNewWallet }) {
48− const { popTo, names, reset } = useHistory();
49− const { web3, contracts, networkType } = useNetwork();
49+interface ResetExecuteProps {
50+ // a Maybe<UrbitWallet>
51+ newWallet: any;
52+ // a useState setter, accepts Maybe<UrbitWallet>
53+ setNewWallet: (args: any) => {};
54+}
55+
56+export default function ResetExecute({
57+ newWallet,
58+ setNewWallet,
59+}: ResetExecuteProps) {
60+ const { popTo, names, reset }: any = useHistory();
61+ const { web3, contracts, networkType }: any = useNetwork();
5062 const {
5163 wallet,
5264 setWalletType,
@@ -54,94 +66,34 @@ export default function ResetExecute({ newWallet, setNewWallet }) {
5466 setUrbitWallet,
5567 walletType,
5668 walletHdPath,
57− } = useWallet();
58− const { performL2Reticket } = useRoller();
69+ }: any = useWallet();
70+ const { api, performL2Reticket } = useRoller();
5971 const {
6072 point: { isL2 },
6173 } = useRollerStore();
62− const { pointCursor } = usePointCursor();
63− const { getDetails } = usePointCache();
74+ const { pointCursor }: any = usePointCursor();
75+ const { getDetails }: any = usePointCache();
6476 const {
6577 signTransaction: wcSign,
6678 sendTransaction: wcSend,
6779 connector,
6880 } = useWalletConnect();
81+ const { performL2SpawnReticket } = useReticketL2Spawn();
6982
70− const [generalError, setGeneralError] = useState();
83+ const [generalError, setGeneralError] = useState<Error | undefined>();
7184 const [progress, setProgress] = useState(0);
72− const [needFunds, setNeedFunds] = useState();
85+ const [needFunds, setNeedFunds] = useState<string | boolean | undefined>();
7386 const isDone = progress >= 1.0;
87+ const point = need.point(pointCursor);
7488
7589 useBlockWindowClose();
7690
77− // start reticketing transactions on mount
78− useEffect(() => {
79− (async () => {
80− // due to react shenanigans we may need to wait for the connector
81− if (
82− walletType === WALLET_TYPES.WALLET_CONNECT &&
83− (!connector || !connector.connected)
84− ) {
85− setGeneralError(new Error('Awaiting WalletConnect connection...'));
86− return;
87− }
88− setGeneralError();
89−
90− const point = need.point(pointCursor);
91− const details = need.details(getDetails(point));
92− const networkRevision = convertToInt(details.keyRevisionNumber, 10);
93−
94− // see also comment in useEthereumTransaction
95− const txnSigner =
96− walletType === WALLET_TYPES.WALLET_CONNECT ? wcSign : undefined;
97− const txnSender =
98− walletType === WALLET_TYPES.WALLET_CONNECT ? wcSend : undefined;
99−
100− try {
101− if (isL2) {
102− console.log(newWallet.value);
103− // FIXME: the useEffect is called twice, which is fine since the duplicate
104− // L2 txs will be discarded
105− //
106− await performL2Reticket({
107− point,
108− to: newWallet.value.wallet.ownership.keys.address,
109− manager: newWallet.value.wallet.management.keys.address,
110− toWallet: newWallet.value.wallet,
111− fromWallet: need.wallet(wallet),
112− });
113− } else
114− await reticketPointBetweenWallets({
115− fromWallet: need.wallet(wallet),
116− fromWalletType: walletType,
117− fromWalletHdPath: walletHdPath,
118− toWallet: newWallet.value.wallet,
119− point: point,
120− web3: need.web3(web3),
121− contracts: need.contracts(contracts),
122− networkType,
123− onUpdate: handleUpdate,
124− nextRevision: networkRevision + 1,
125− txnSigner,
126− txnSender,
127− });
128− } catch (err) {
129− console.error(err);
130− setGeneralError(err);
131− }
132− })();
133− //NOTE this was using useLifecycle previously (which is useEffect without
134− // dependencies) and worked fine, but in the wc case we might need to
135− // wait for the connector to get set due to react state shenanigans.
136− // eslint-disable-next-line react-hooks/exhaustive-deps
137− }, [connector]);
138−
13991 const goToRestart = useCallback(() => {
14092 reset();
14193 setNewWallet(Nothing());
14294 }, [reset, setNewWallet]);
14395
144− const loginAndGoHome = useCallback(() => {
96+ const loginAndGoHome = useCallback(async () => {
14597 // set wallet state
14698 resetWallet();
14799 setWalletType(WALLET_TYPES.TICKET);
@@ -149,9 +101,16 @@ export default function ResetExecute({ newWallet, setNewWallet }) {
149101 // (implicit) pointCursor stays the same
150102 // redirect to point
151103 popTo(names.POINT);
152− }, [resetWallet, setWalletType, setUrbitWallet, newWallet, popTo, names]);
104+ }, [
105+ resetWallet,
106+ setWalletType,
107+ setUrbitWallet,
108+ newWallet.value.wallet,
109+ popTo,
110+ names.POINT,
111+ ]);
153112
154− const handleUpdate = useCallback(
113+ const handleUpdate: ReticketProgressCallback = useCallback(
155114 ({ type, state, value }) => {
156115 switch (type) {
157116 case 'progress':
@@ -167,6 +126,108 @@ export default function ResetExecute({ newWallet, setNewWallet }) {
167126 [setProgress, setNeedFunds]
168127 );
169128
129+ const performReticket = useCallback(async () => {
130+ // due to react shenanigans we may need to wait for the connector
131+ if (
132+ walletType === WALLET_TYPES.WALLET_CONNECT &&
133+ (!connector || !connector.connected)
134+ ) {
135+ setGeneralError(new Error('Awaiting WalletConnect connection...'));
136+ return;
137+ }
138+
139+ setGeneralError(undefined);
140+
141+ const l2point = await api.getPoint(point);
142+ const details = need.details(getDetails(point));
143+ const networkRevision = convertToInt(details.keyRevisionNumber, 10);
144+
145+ // see also comment in useEthereumTransaction
146+ const txnSigner =
147+ walletType === WALLET_TYPES.WALLET_CONNECT ? wcSign : undefined;
148+ const txnSender =
149+ walletType === WALLET_TYPES.WALLET_CONNECT ? wcSend : undefined;
150+
151+ try {
152+ // if L1 point with migrated spawn proxy (dominion = 'spawn')
153+ if (l2point.dominion === POINT_DOMINIONS.SPAWN) {
154+ await performL2SpawnReticket({
155+ fromWallet: need.wallet(wallet),
156+ fromWalletType: walletType,
157+ fromWalletHdPath: walletHdPath,
158+ toWallet: newWallet.value.wallet,
159+ to: newWallet.value.wallet.ownership.keys.address,
160+ point: point,
161+ web3: need.web3(web3),
162+ contracts: need.contracts(contracts),
163+ networkType,
164+ onUpdate: handleUpdate,
165+ nextRevision: networkRevision + 1,
166+ txnSigner,
167+ txnSender,
168+ });
169+ // If L2 point
170+ } else if (isL2) {
171+ await performL2Reticket({
172+ point,
173+ to: newWallet.value.wallet.ownership.keys.address,
174+ manager: newWallet.value.wallet.management.keys.address,
175+ toWallet: newWallet.value.wallet,
176+ fromWallet: need.wallet(wallet),
177+ onUpdate: handleUpdate,
178+ });
179+ // Fallback to L1 point flow
180+ } else {
181+ await reticketPointBetweenWallets({
182+ fromWallet: need.wallet(wallet),
183+ fromWalletType: walletType,
184+ fromWalletHdPath: walletHdPath,
185+ toWallet: newWallet.value.wallet,
186+ point: point,
187+ web3: need.web3(web3),
188+ contracts: need.contracts(contracts),
189+ networkType,
190+ onUpdate: handleUpdate,
191+ nextRevision: networkRevision + 1,
192+ txnSigner,
193+ txnSender,
194+ });
195+ }
196+ } catch (err) {
197+ console.error(err);
198+ setGeneralError(err);
199+ }
200+ }, [
201+ api,
202+ connector,
203+ contracts,
204+ getDetails,
205+ handleUpdate,
206+ isL2,
207+ networkType,
208+ newWallet.value.wallet,
209+ performL2Reticket,
210+ performL2SpawnReticket,
211+ point,
212+ wallet,
213+ walletHdPath,
214+ walletType,
215+ wcSend,
216+ wcSign,
217+ web3,
218+ ]);
219+
220+ useEffect(() => {
221+ if (!connector) {
222+ return;
223+ }
224+
225+ performReticket();
226+ // We want to perform the reticket only once, after the WalletConnect
227+ // connector has finished instantiating and setting up a websocket
228+ // eslint-disable-next-line react-hooks/exhaustive-deps
229+ }, [connector]);
230+
170231 const renderAdditionalInfo = () => {
171232 if (generalError) {
172233 console.log(generalError);
@@ -174,7 +235,9 @@ export default function ResetExecute({ newWallet, setNewWallet }) {
174235 return (
175236 <>
176237 <Grid.Item full className="mt4">
177− <ErrorText>{generalError.message.toString()}</ErrorText>
238+ <ErrorText className={''}>
239+ {generalError.message.toString()}
240+ </ErrorText>
178241 </Grid.Item>
179242 <Grid.Item
180243 full