on master help sign in
Updating residents, sponsorship, spawning, UX issues
a3a8f64Will Galebach
src/Bridge.js+3−1
@@ -21,6 +21,7 @@ import GlobalErrorBoundary from 'components/GlobalErrorBoundary';
21import Modal from 'components/L2/Modal';
21import Modal from 'components/L2/Modal';
22import { useRollerStore } from 'store/rollerStore';
22import { useRollerStore } from 'store/rollerStore';
23import { Box } from '@tlon/indigo-react';
23import { Box } from '@tlon/indigo-react';
24+import LoadingOverlay from 'components/L2/LoadingOverlay';
24
25
25const INITIAL_NETWORK_TYPE = isRopsten
26const INITIAL_NETWORK_TYPE = isRopsten
26 ? NETWORK_TYPES.ROPSTEN
27 ? NETWORK_TYPES.ROPSTEN
@@ -70,7 +71,7 @@ function useInitialRoutes() {
70
71
71function Bridge() {
72function Bridge() {
72 const initialRoutes = useInitialRoutes();
73 const initialRoutes = useInitialRoutes();
73− const { modalText, setModalText } = useRollerStore();
74+ const { loading, modalText, setModalText } = useRollerStore();
74
75
75 // full reload if the user changes their selected network in Metamask
76 // full reload if the user changes their selected network in Metamask
76 //
77 //
@@ -98,6 +99,7 @@ function Bridge() {
98 <Modal show={modalText !== undefined} hide={() => setModalText()}>
99 <Modal show={modalText !== undefined} hide={() => setModalText()}>
99 <Box width="280px">{modalText}</Box>
100 <Box width="280px">{modalText}</Box>
100 </Modal>
101 </Modal>
102+ <LoadingOverlay loading={loading} />
101 </Provider>
103 </Provider>
102 </WithErrorBoundary>
104 </WithErrorBoundary>
103 );
105 );
src/components/L2/Headers/L2PointHeader.tsx+2−2
@@ -26,7 +26,7 @@ const L2PointHeader = ({
26 hideInvites = false,
26 hideInvites = false,
27 showMigrate = false,
27 showMigrate = false,
28}: L2PointHeaderProps) => {
28}: L2PointHeaderProps) => {
29− const { point, invitesLoading } = useRollerStore();
29+ const { point, pointList, invitesLoading } = useRollerStore();
30 const { pointCursor, setPointCursor }: any = usePointCursor();
30 const { pointCursor, setPointCursor }: any = usePointCursor();
31
31
32 const { popTo, push, names }: any = useHistory();
32 const { popTo, push, names }: any = useHistory();
@@ -76,7 +76,7 @@ const L2PointHeader = ({
76 return (
76 return (
77 <Row className="l2-point-header">
77 <Row className="l2-point-header">
78 <Row>
78 <Row>
79− {!hideHome && (
79+ {!hideHome && pointList.length > 1 && (
80 <HeaderButton className="home" icon="Home" onClick={goToHome} />
80 <HeaderButton className="home" icon="Home" onClick={goToHome} />
81 )}
81 )}
82 <AccountsDropdown showMigrate={showMigrate} />
82 <AccountsDropdown showMigrate={showMigrate} />
src/components/L2/LoadingOverlay.tsx+1−1
@@ -17,7 +17,7 @@ const LoadingOverlay = ({
17 <Box className="loading-overlay">
17 <Box className="loading-overlay">
18 <Box className={`${text ? 'solid' : ''}`}>
18 <Box className={`${text ? 'solid' : ''}`}>
19 {!!text && <Box className="loader-text">{text}</Box>}
19 {!!text && <Box className="loader-text">{text}</Box>}
20− <LoadingSpinner foreground="white" background="rgba(0,0,0,0.3)" />
20+ <LoadingSpinner foreground="rgba(0,0,0,0.3)" background="white" />
21 </Box>
21 </Box>
22 </Box>
22 </Box>
23 );
23 );
src/components/Passport.js+12−2
@@ -211,7 +211,13 @@ const Cell = props => {
211 * point is number
211 * point is number
212 *
212 *
213 */
213 */
214−function MiniPassport({ point, inverted, locked = false, ...rest }) {
214+function MiniPassport({
215+ point,
216+ inverted,
217+ locked = false,
218+ processing = false,
219+ ...rest
220+}) {
215 useSyncDetails([point]);
221 useSyncDetails([point]);
216 // const { wallet } = useWallet();
222 // const { wallet } = useWallet();
217 // const address = need.addressFromWallet(wallet);
223 // const address = need.addressFromWallet(wallet);
@@ -226,9 +232,13 @@ function MiniPassport({ point, inverted, locked = false, ...rest }) {
226 makeSigil(50, patp, inverted ? ['white', contrast] : [contrast, 'white']),
232 makeSigil(50, patp, inverted ? ['white', contrast] : [contrast, 'white']),
227 [inverted, patp, contrast]
233 [inverted, patp, contrast]
228 );
234 );
235+ const className = `sigil ${locked ? 'locked' : ''} ${
236+ processing ? 'processing' : ''
237+ }`;
238+
229 return (
239 return (
230 <div {...rest} className="sigil-mini">
240 <div {...rest} className="sigil-mini">
231− <div className={`sigil ${locked ? 'locked' : ''}`}>{sigil}</div>
241+ <div className={className}>{sigil}</div>
232 <span className="patp">{patp}</span>
242 <span className="patp">{patp}</span>
233 {locked && <Icon color="white" icon="Locked" className="lock" />}
243 {locked && <Icon color="white" icon="Locked" className="lock" />}
234 </div>
244 </div>
src/components/SigilMini.scss+5−1
@@ -21,7 +21,11 @@
21 border-radius: $large-border-radius;
21 border-radius: $large-border-radius;
22
22
23 &.locked {
23 &.locked {
24− background-color: $dark-gray;
24+ opacity: 0.6;
25+ }
26+
27+ &.processing {
28+ opacity: 0.6;
25 }
29 }
26 }
30 }
27
31
src/form/Inputs.tsx+3−2
@@ -19,7 +19,8 @@ import {
19 StatelessTextInputProps,
19 StatelessTextInputProps,
20} from '@tlon/indigo-react';
20} from '@tlon/indigo-react';
21
21
22−const PLACEHOLDER_POINT = '~sampel-ponnym';
22+const PLACEHOLDER_STAR = '~sampel';
23+const PLACEHOLDER_PLANET = '~sampel-ponnym';
23const PLACEHOLDER_HD_PATH = DEFAULT_HD_PATH;
24const PLACEHOLDER_HD_PATH = DEFAULT_HD_PATH;
24const PLACEHOLDER_MNEMONIC =
25const PLACEHOLDER_MNEMONIC =
25 'example crew supreme gesture quantum web media hazard theory mercy wing kitten';
26 'example crew supreme gesture quantum web media hazard theory mercy wing kitten';
@@ -122,7 +123,7 @@ export function PointInput({ name, size = 4, ...rest }) {
122 type="text"
123 type="text"
123 label="Urbit ID"
124 label="Urbit ID"
124 name={name}
125 name={name}
125− placeholder={PLACEHOLDER_POINT}
126+ placeholder={size === 4 ? PLACEHOLDER_PLANET : PLACEHOLDER_STAR}
126 autoCapitalize="none"
127 autoCapitalize="none"
127 autoComplete="off"
128 autoComplete="off"
128 autoCorrect="off"
129 autoCorrect="off"
src/indigo-react/components/Input.js+1−1
@@ -125,7 +125,7 @@ export default function Input({
125
125
126 {indicateError && (
126 {indicateError && (
127 <Flex.Item as={ErrorText} className="mv1">
127 <Flex.Item as={ErrorText} className="mv1">
128− {error ? JSON.stringify(error) : submitError}
128+ {error ? error : submitError}
129 </Flex.Item>
129 </Flex.Item>
130 )}
130 )}
131 </Flex>
131 </Flex>
src/lib/constants.ts+5−5
@@ -150,17 +150,17 @@ const TRANSACTION_TYPE_ICONS = {
150};
150};
151
151
152const TRANSACTION_TYPE_TITLES = {
152const TRANSACTION_TYPE_TITLES = {
153− adopt: 'Point Sponsored',
153+ adopt: 'Ship Sponsored',
154 'cancel-escape': 'Escape Canceled',
154 'cancel-escape': 'Escape Canceled',
155 'configure-keys': 'Network Keys Configured',
155 'configure-keys': 'Network Keys Configured',
156 detach: 'Sponsee Detached',
156 detach: 'Sponsee Detached',
157− escape: 'Sponsor Escaped',
157+ escape: 'Escaped Sponsor',
158− reject: 'Sponsorhip Rejected',
158+ reject: 'Sponsorship Rejected',
159 'set-management-proxy': 'Management Proxy Changed',
159 'set-management-proxy': 'Management Proxy Changed',
160 'set-spawn-proxy': 'Spawn Proxy Changed',
160 'set-spawn-proxy': 'Spawn Proxy Changed',
161 'set-transfer-proxy': 'Transfer Proxy Changed',
161 'set-transfer-proxy': 'Transfer Proxy Changed',
162− spawn: 'Point Spawned',
162+ spawn: 'Ship Spawned',
163− 'transfer-point': 'Point Transferred',
163+ 'transfer-point': 'Ship Transferred',
164};
164};
165const DUMMY_L2_ADDRESS = '0x1111111111111111111111111111111111111111';
165const DUMMY_L2_ADDRESS = '0x1111111111111111111111111111111111111111';
166
166
src/lib/types/L2Transaction.ts+6−1
@@ -5,13 +5,18 @@ export type TransactionType =
5 | 'transferPoint'
5 | 'transferPoint'
6 | 'spawn'
6 | 'spawn'
7 | 'configureKeys'
7 | 'configureKeys'
8− | 'escape';
8+ | 'escape'
9+ | 'cancelEscape'
10+ | 'adopt'
11+ | 'detach'
12+ | 'reject';
9
13
10export interface SendL2Params {
14export interface SendL2Params {
11 address?: string; // Destination address
15 address?: string; // Destination address
12 breach?: boolean;
16 breach?: boolean;
13 networkSeed?: any;
17 networkSeed?: any;
14 newSponsor?: Ship;
18 newSponsor?: Ship;
19+ sponsee?: Ship;
15 nonce: number;
20 nonce: number;
16 pointToSpawn?: number;
21 pointToSpawn?: number;
17 proxy: Proxy;
22 proxy: Proxy;
src/lib/types/Point.ts+8
@@ -109,6 +109,7 @@ export default class Point {
109 showInvites: boolean;
109 showInvites: boolean;
110 isDefault: boolean;
110 isDefault: boolean;
111 isPlaceholder: boolean;
111 isPlaceholder: boolean;
112+ shouldDisplay: boolean;
112
113
113 constructor({
114 constructor({
114 value,
115 value,
@@ -173,6 +174,13 @@ export default class Point {
173 this.showInvites = this.canManage || this.canSpawn;
174 this.showInvites = this.canManage || this.canSpawn;
174 this.isDefault = value === -1;
175 this.isDefault = value === -1;
175 this.isPlaceholder = isPlaceholder;
176 this.isPlaceholder = isPlaceholder;
177+ this.shouldDisplay =
178+ !this.isPlaceholder &&
179+ !this.isDefault &&
180+ (this.isOwner ||
181+ this.isSpawnProxy ||
182+ this.isManagementProxy ||
183+ this.isTransferProxy);
176 }
184 }
177
185
178 equals = (point: Point) => {
186 equals = (point: Point) => {
src/lib/useRoller.ts+59−71
@@ -19,7 +19,6 @@ import {
19 setStoredInvites,
19 setStoredInvites,
20} from 'store/storage/roller';
20} from 'store/storage/roller';
21import { usePointCache } from 'store/pointCache';
21import { usePointCache } from 'store/pointCache';
22−import { getOutgoingPoints } from 'views/Points';
23
22
24import { getUpdatedPointMessage, isPlanet, toL1Details } from './utils/point';
23import { getUpdatedPointMessage, isPlanet, toL1Details } from './utils/point';
25import { convertToInt } from './convertToInt';
24import { convertToInt } from './convertToInt';
@@ -30,9 +29,6 @@ import {
30 submitL2Transaction,
29 submitL2Transaction,
31 getTimeToNextBatch,
30 getTimeToNextBatch,
32 registerProxyAddress,
31 registerProxyAddress,
33− adopt,
34− detach,
35− reject,
36 isL2Spawn,
32 isL2Spawn,
37} from './utils/roller';
33} from './utils/roller';
38import { ETH_ZERO_ADDR, ROLLER_HOSTS, TEN_SECONDS } from './constants';
34import { ETH_ZERO_ADDR, ROLLER_HOSTS, TEN_SECONDS } from './constants';
@@ -81,7 +77,6 @@ export default function useRoller() {
81 const { pointCursor }: any = usePointCursor();
77 const { pointCursor }: any = usePointCursor();
82 const { web3, contracts }: any = useNetwork();
78 const { web3, contracts }: any = useNetwork();
83 const allPoints: any = usePointCache();
79 const allPoints: any = usePointCache();
84− const controlledPoints = allPoints?.controlledPoints;
85 const getDetails = allPoints?.getDetails;
80 const getDetails = allPoints?.getDetails;
86
81
87 const { setNextRoll } = useTimerStore();
82 const { setNextRoll } = useTimerStore();
@@ -90,6 +85,7 @@ export default function useRoller() {
90 nextBatchTime,
85 nextBatchTime,
91 point,
86 point,
92 points,
87 points,
88+ pointList,
93 nextQuotaTime,
89 nextQuotaTime,
94 removeInvite,
90 removeInvite,
95 setNextBatchTime,
91 setNextBatchTime,
@@ -263,7 +259,7 @@ export default function useRoller() {
263 );
259 );
264
260
265 const spawnPoint = useCallback(
261 const spawnPoint = useCallback(
266− async (pointToSpawn: number) => {
262+ async (pointToSpawn: number, destinationAddress: string) => {
267 if (quotaReached()) {
263 if (quotaReached()) {
268 return;
264 return;
269 }
265 }
@@ -291,7 +287,7 @@ export default function useRoller() {
291
287
292 await submitL2Transaction({
288 await submitL2Transaction({
293 api,
289 api,
294− address: _wallet.address,
290+ address: destinationAddress,
295 wallet: _wallet,
291 wallet: _wallet,
296 ship: _point,
292 ship: _point,
297 proxy,
293 proxy,
@@ -347,10 +343,11 @@ export default function useRoller() {
347 curPoint
343 curPoint
348 );
344 );
349
345
350− const outgoingPoints = getOutgoingPoints(
346+ const outgoingPoints = pointList
351− controlledPoints,
347+ .filter(
352− getDetails
353− ).filter((p: number) => isPlanet(p) && availablePoints.includes(p));
348+ ({ value, isPlanet }) => isPlanet && availablePoints.includes(value)
349+ )
350+ .map(({ value }) => value);
354
351
355 const invitePlanets = spawnedPoints
352 const invitePlanets = spawnedPoints
356 .concat(
353 .concat(
@@ -405,11 +402,10 @@ export default function useRoller() {
405 api,
402 api,
406 authToken,
403 authToken,
407 contracts,
404 contracts,
408− controlledPoints,
409− getDetails,
410 pointCursor,
405 pointCursor,
411 ls,
406 ls,
412 point,
407 point,
408+ pointList,
413 removeInvite,
409 removeInvite,
414 setInvites,
410 setInvites,
415 setInvitesLoading,
411 setInvitesLoading,
@@ -789,75 +785,40 @@ export default function useRoller() {
789 [api, connector, point, wallet, web3, walletType, quotaReached]
785 [api, connector, point, wallet, web3, walletType, quotaReached]
790 );
786 );
791
787
792− const adoptPoint = useCallback(
788+ const changeSponsorship = useCallback(
793− async (ship: Ship) => {
789+ async (sponsee: Ship, type: 'adopt' | 'detach' | 'reject') => {
794 if (quotaReached()) {
790 if (quotaReached()) {
795 return;
791 return;
796 }
792 }
797
793
798 const _wallet = wallet.getOrElse(null);
794 const _wallet = wallet.getOrElse(null);
795+ const _web3 = web3.getOrElse(null);
796+ const ship = point.value;
799
797
800 const proxy = point.getManagerProxy();
798 const proxy = point.getManagerProxy();
801
799
802 if (proxy === undefined)
800 if (proxy === undefined)
803 throw new Error("Error: Address doesn't match expected proxy");
801 throw new Error("Error: Address doesn't match expected proxy");
804
802
805− const nonce = await api.getNonce({ ship: point.value, proxy });
803+ const nonce = await api.getNonce({ ship, proxy });
806− const txHash = await adopt(api, _wallet, point.value, proxy, nonce, ship);
804+ // const txHash = await adopt(api, _wallet, point.value, proxy, nonce, ship);
807−
805+ const txHash = await submitL2Transaction({
808− return api.getPendingTx(txHash);
806+ api,
809− },
807+ wallet: _wallet,
810− [api, point, wallet, quotaReached]
808+ ship,
811− );
809+ sponsee,
812−
810+ proxy,
813− const kickPoint = useCallback(
811+ nonce,
814− async (ship: Ship) => {
812+ address: _wallet.address,
815− if (quotaReached()) {
813+ type,
816− return;
814+ walletType,
817− }
815+ web3: _web3,
818−
816+ connector,
819− if (point.isDefault) {
817+ });
820− throw new Error('Internal Error: point is default');
821− }
822−
823− const sponsor = point.value;
824− const _wallet = wallet.getOrElse(null);
825− const proxy = point.getManagerProxy();
826−
827− if (proxy === undefined)
828− throw new Error("Error: Address doesn't match expected proxy");
829−
830− const nonce = await api.getNonce({ ship: sponsor, proxy });
831− const txHash = await detach(api, _wallet, sponsor, proxy, nonce, ship);
832−
833− return api.getPendingTx(txHash);
834− },
835− [api, point, wallet, quotaReached]
836− );
837−
838− const rejectPoint = useCallback(
839− async (ship: Ship) => {
840− if (quotaReached()) {
841− return;
842− }
843−
844− if (point.isDefault) {
845− throw new Error('Internal Error: point is default');
846− }
847−
848− const sponsor = point.value;
849− const _wallet = wallet.getOrElse(null);
850− const proxy = point.getManagerProxy();
851−
852− if (proxy === undefined)
853− throw new Error("Error: Address doesn't match expected proxy");
854−
855− const nonce = await api.getNonce({ ship: sponsor, proxy });
856− const txHash = await reject(api, _wallet, sponsor, proxy, nonce, ship);
857
818
858 return api.getPendingTx(txHash);
819 return api.getPendingTx(txHash);
859 },
820 },
860− [api, point, wallet, quotaReached]
821+ [api, point, wallet, walletType, web3, connector, quotaReached]
861 );
822 );
862
823
863 const transferPoint = useCallback(
824 const transferPoint = useCallback(
@@ -960,6 +921,34 @@ export default function useRoller() {
960 [api, point, sendL2Transaction, wallet]
921 [api, point, sendL2Transaction, wallet]
961 );
922 );
962
923
924+ const cancelEscape = useCallback(async () => {
925+ if (point.isDefault) {
926+ throw new Error('Internal Error: point is default');
927+ }
928+
929+ const curPoint = point.value;
930+ const _wallet = wallet.getOrElse(null);
931+
932+ if (!_wallet) {
933+ // not using need because we want a custom error
934+ throw new Error('Internal Error: Missing Wallet');
935+ }
936+
937+ const proxy = point.getManagerProxy();
938+
939+ if (proxy === undefined)
940+ throw new Error("Error: Address doesn't match proxy");
941+
942+ const nonce = await api.getNonce({ ship: curPoint, proxy });
943+
944+ return sendL2Transaction({
945+ nonce,
946+ proxy,
947+ type: 'cancelEscape',
948+ ship: curPoint,
949+ });
950+ }, [api, point, sendL2Transaction, wallet]);
951+
963 const getPointsDetails = useCallback(
952 const getPointsDetails = useCallback(
964 async (
953 async (
965 ownedPoints: number[],
954 ownedPoints: number[],
@@ -1029,9 +1018,10 @@ export default function useRoller() {
1029 }, [point, nextBatchTime, getPendingTransactions, getInvites]); // eslint-disable-line react-hooks/exhaustive-deps
1018 }, [point, nextBatchTime, getPendingTransactions, getInvites]); // eslint-disable-line react-hooks/exhaustive-deps
1030
1019
1031 return {
1020 return {
1032− adoptPoint,
1033 api,
1021 api,
1022+ changeSponsorship,
1034 changeSponsor,
1023 changeSponsor,
1024+ cancelEscape,
1035 checkForUpdates,
1025 checkForUpdates,
1036 config,
1026 config,
1037 configureNetworkingKeys,
1027 configureNetworkingKeys,
@@ -1040,10 +1030,8 @@ export default function useRoller() {
1040 getPoints,
1030 getPoints,
1041 getPointsDetails,
1031 getPointsDetails,
1042 getPendingTransactions,
1032 getPendingTransactions,
1043− kickPoint,
1044 ls,
1033 ls,
1045 performL2Reticket,
1034 performL2Reticket,
1046− rejectPoint,
1047 setProxyAddress,
1035 setProxyAddress,
1048 spawnPoint,
1036 spawnPoint,
1049 transferPoint,
1037 transferPoint,
src/lib/utils/roller.ts+22−101
@@ -83,41 +83,6 @@ export const generateHashAndSign = async (
83 }
83 }
84};
84};
85
85
86−export const spawn = async (
87− api: RollerRPCAPI,
88− wallet: any,
89− _point: number,
90− proxy: string,
91− nonce: number,
92− planet: number,
93− walletType: symbol,
94− web3: any,
95− connector: WalletConnect | null
96−) => {
97− const from = {
98− ship: _point, //ship that is spawning the planet
99− proxy,
100− };
101−
102− const data = {
103− address: wallet.address,
104− ship: planet, // ship to spawn
105− };
106−
107− const sig = await generateHashAndSign(
108− api,
109− wallet,
110− nonce,
111− from,
112− 'spawn',
113− data,
114− walletType,
115− web3,
116− connector
117− );
118− return api.spawn(sig, from, wallet.address, data);
119−};
120−
121export const configureKeys = async (
86export const configureKeys = async (
122 api: RollerRPCAPI,
87 api: RollerRPCAPI,
123 wallet: any,
88 wallet: any,
@@ -205,72 +170,6 @@ export const transferPointRequest = async (
205 return api.transferPoint(sig, from, wallet.address, data);
170 return api.transferPoint(sig, from, wallet.address, data);
206};
171};
207
172
208−export const adopt = async (
209− api: RollerRPCAPI,
210− _wallet: any,
211− sponsor: Ship,
212− proxy: string,
213− nonce: number,
214− ship: Ship
215−) => {
216− const from = {
217− ship: sponsor, // ship that is adopting the planet
218− proxy,
219− };
220−
221− const data = {
222− ship, // ship to adopt
223− };
224−
225− const hash = await api.getUnsignedTx(nonce, from, 'detach', data);
226− const sSig = signTransactionHash(hash, _wallet.privateKey);
227− return api.adopt(sSig, from, _wallet.address, data);
228−};
229−
230−export const detach = async (
231− api: RollerRPCAPI,
232− _wallet: any,
233− sponsor: Ship,
234− proxy: string,
235− nonce: number,
236− ship: Ship
237−) => {
238− const from = {
239− ship: sponsor, // ship that is detaching the planet
240− proxy,
241− };
242−
243− const data = {
244− ship, // ship to detach
245− };
246−
247− const hash = await api.getUnsignedTx(nonce, from, 'detach', data);
248− const sSig = signTransactionHash(hash, _wallet.privateKey);
249− return api.detach(sSig, from, _wallet.address, data);
250−};
251−
252−export const reject = async (
253− api: RollerRPCAPI,
254− _wallet: any,
255− sponsor: Ship,
256− proxy: string,
257− nonce: number,
258− ship: Ship
259−) => {
260− const from = {
261− ship: sponsor, // ship that is rejecting the planet
262− proxy,
263− };
264−
265− const data = {
266− ship, // ship to reject
267− };
268−
269− const hash = await api.getUnsignedTx(nonce, from, 'detach', data);
270− const sSig = signTransactionHash(hash, _wallet.privateKey);
271− return api.reject(sSig, from, _wallet.address, data);
272−};
273−
274const proxyType = (proxy: Proxy) => {
173const proxyType = (proxy: Proxy) => {
275 switch (proxy) {
174 switch (proxy) {
276 case 'manage':
175 case 'manage':
@@ -346,6 +245,8 @@ const getDataAndMethod = (args: L2TransactionArgs) => {
346 reset,
245 reset,
347 pointToSpawn,
246 pointToSpawn,
348 newSponsor,
247 newSponsor,
248+ sponsee,
249+ ship,
349 } = args;
250 } = args;
350 const result: TransactionData = { data: { address }, method: () => null };
251 const result: TransactionData = { data: { address }, method: () => null };
351
252
@@ -384,6 +285,26 @@ const getDataAndMethod = (args: L2TransactionArgs) => {
384 result.data.ship = newSponsor;
285 result.data.ship = newSponsor;
385 delete result.data.address;
286 delete result.data.address;
386 return result;
287 return result;
288+ case 'cancelEscape':
289+ result.method = api.cancelEscape;
290+ result.data.ship = ship;
291+ delete result.data.address;
292+ return result;
293+ case 'adopt':
294+ result.method = api.adopt;
295+ result.data.ship = sponsee;
296+ delete result.data.address;
297+ return result;
298+ case 'detach':
299+ result.method = api.detach;
300+ result.data.ship = sponsee;
301+ delete result.data.address;
302+ return result;
303+ case 'reject':
304+ result.method = api.reject;
305+ result.data.ship = sponsee;
306+ delete result.data.address;
307+ return result;
387 }
308 }
388};
309};
389
310
src/store/rollerStore.ts+4
@@ -19,6 +19,7 @@ export const EMPTY_POINT = new Point({
19});
19});
20
20
21export interface RollerStore {
21export interface RollerStore {
22+ loading: boolean;
22 nextBatchTime: number;
23 nextBatchTime: number;
23 nextQuotaTime: number;
24 nextQuotaTime: number;
24 pendingTransactions: PendingTransaction[];
25 pendingTransactions: PendingTransaction[];
@@ -30,6 +31,7 @@ export interface RollerStore {
30 recentlyCompleted: number;
31 recentlyCompleted: number;
31 inviteGeneratingNum: number;
32 inviteGeneratingNum: number;
32 invitesLoading: boolean;
33 invitesLoading: boolean;
34+ setLoading: (loading: boolean) => void;
33 removeInvite: (point: number, planet: number) => void;
35 removeInvite: (point: number, planet: number) => void;
34 setInvites: (points: number, invites: Invite[]) => void;
36 setInvites: (points: number, invites: Invite[]) => void;
35 setInviteGeneratingNum: (numGenerating: number) => void;
37 setInviteGeneratingNum: (numGenerating: number) => void;
@@ -45,6 +47,7 @@ export interface RollerStore {
45}
47}
46
48
47export const useRollerStore = create<RollerStore>(set => ({
49export const useRollerStore = create<RollerStore>(set => ({
50+ loading: false,
48 nextBatchTime: new Date().getTime() + HOUR,
51 nextBatchTime: new Date().getTime() + HOUR,
49 nextQuotaTime: new Date().getTime() + 24 * HOUR,
52 nextQuotaTime: new Date().getTime() + 24 * HOUR,
50 pendingTransactions: [],
53 pendingTransactions: [],
@@ -55,6 +58,7 @@ export const useRollerStore = create<RollerStore>(set => ({
55 recentlyCompleted: 0,
58 recentlyCompleted: 0,
56 inviteGeneratingNum: 0,
59 inviteGeneratingNum: 0,
57 invitesLoading: false,
60 invitesLoading: false,
61+ setLoading: (loading: boolean) => set(() => ({ loading })),
58 setNextBatchTime: (nextBatchTime: number) => set(() => ({ nextBatchTime })),
62 setNextBatchTime: (nextBatchTime: number) => set(() => ({ nextBatchTime })),
59 setNextQuotaTime: (nextQuotaTime: number) => set(() => ({ nextQuotaTime })),
63 setNextQuotaTime: (nextQuotaTime: number) => set(() => ({ nextQuotaTime })),
60 setPendingTransactions: (pendingTransactions: PendingTransaction[]) =>
64 setPendingTransactions: (pendingTransactions: PendingTransaction[]) =>
src/views/Ops/Requests.tsx+19−23
@@ -1,5 +1,9 @@
1−import * as need from 'lib/need';
1+import { useCallback, useEffect, useState } from 'react';
2−import { Box, LoadingSpinner, Row } from '@tlon/indigo-react';
2+import { Box, Row } from '@tlon/indigo-react';
3+import { Ship } from '@urbit/roller-api';
4+
5+import { useRollerStore } from 'store/rollerStore';
6+import useRoller from 'lib/useRoller';
3
7
4import Window from 'components/L2/Window/Window';
8import Window from 'components/L2/Window/Window';
5import HeaderPane from 'components/L2/Window/HeaderPane';
9import HeaderPane from 'components/L2/Window/HeaderPane';
@@ -7,21 +11,15 @@ import BodyPane from 'components/L2/Window/BodyPane';
7import { useLocalRouter } from 'lib/LocalRouter';
11import { useLocalRouter } from 'lib/LocalRouter';
8import View from 'components/View';
12import View from 'components/View';
9import L2BackHeader from 'components/L2/Headers/L2BackHeader';
13import L2BackHeader from 'components/L2/Headers/L2BackHeader';
10−import { usePointCursor } from 'store/pointCursor';
11−import { useCallback, useEffect, useState } from 'react';
12
14
13−import './Requests.scss';
14−import useRoller from 'lib/useRoller';
15−import { Ship } from '@urbit/roller-api';
16import { RequestRow } from './RequestRow';
15import { RequestRow } from './RequestRow';
16+import './Requests.scss';
17
17
18export const Requests = () => {
18export const Requests = () => {
19 const { pop }: any = useLocalRouter();
19 const { pop }: any = useLocalRouter();
20− const { pointCursor } = usePointCursor();
20+ const { point, setLoading } = useRollerStore();
21− const point = need.point(pointCursor);
22− const { api, adoptPoint, rejectPoint } = useRoller();
21+ const { api, changeSponsorship } = useRoller();
23 const [requestingPoints, setRequestingPoints] = useState<Ship[]>([]);
22 const [requestingPoints, setRequestingPoints] = useState<Ship[]>([]);
24− const [loading, setLoading] = useState<boolean>(false);
25
23
26 const fetchRequests = useCallback(async () => {
24 const fetchRequests = useCallback(async () => {
27 if (!point) {
25 if (!point) {
@@ -29,28 +27,29 @@ export const Requests = () => {
29 }
27 }
30
28
31 setLoading(true);
29 setLoading(true);
32− const { requests } = await api.getSponsoredPoints(point);
30+ const { requests } = await api.getSponsoredPoints(point.value);
33−
34 setRequestingPoints((requests || []).sort());
31 setRequestingPoints((requests || []).sort());
35 setLoading(false);
32 setLoading(false);
36− }, [api, point]);
33+ }, [api, point, setLoading]);
37
34
38 const handleAcceptClick = useCallback(
35 const handleAcceptClick = useCallback(
39 async (point: Ship) => {
36 async (point: Ship) => {
40 setLoading(true);
37 setLoading(true);
41− await adoptPoint(point);
38+ await changeSponsorship(point, 'adopt');
42 await fetchRequests();
39 await fetchRequests();
40+ setLoading(false);
43 },
41 },
44− [adoptPoint, fetchRequests]
42+ [changeSponsorship, fetchRequests, setLoading]
45 );
43 );
46
44
47 const handleRejectClick = useCallback(
45 const handleRejectClick = useCallback(
48 async (point: Ship) => {
46 async (point: Ship) => {
49 setLoading(true);
47 setLoading(true);
50− await rejectPoint(point);
48+ await changeSponsorship(point, 'reject');
51 await fetchRequests();
49 await fetchRequests();
50+ setLoading(false);
52 },
51 },
53− [rejectPoint, fetchRequests]
52+ [changeSponsorship, fetchRequests, setLoading]
54 );
53 );
55
54
56 useEffect(() => {
55 useEffect(() => {
@@ -75,14 +74,11 @@ export const Requests = () => {
75 </HeaderPane>
74 </HeaderPane>
76 <BodyPane>
75 <BodyPane>
77 <Box className="content-container">
76 <Box className="content-container">
78− {loading ? (
79− <Box className={'loading'}>
80− <LoadingSpinner />
81− </Box>
82− ) : requestingPoints.length > 0 ? (
77+ {requestingPoints.length > 0 ? (
83 <ul className="requests-list">
78 <ul className="requests-list">
84 {requestingPoints.map(sp => (
79 {requestingPoints.map(sp => (
85 <RequestRow
80 <RequestRow
81+ key={sp}
86 point={sp}
82 point={sp}
87 onAccept={handleAcceptClick}
83 onAccept={handleAcceptClick}
88 onReject={handleRejectClick}
84 onReject={handleRejectClick}
src/views/Ops/ResidentRow.tsx+9−3
@@ -48,9 +48,15 @@ export const ResidentRow = ({ point, onKick }: ResidentRowProps) => {
48 onClick={() => setShowDropdown(!showDropdown)}
48 onClick={() => setShowDropdown(!showDropdown)}
49 />
49 />
50 {showDropdown && (
50 {showDropdown && (
51+ <>
52+ <Box
53+ className="close-background"
54+ onClick={() => setShowDropdown(false)}
55+ />
51− <Box className={'kick-dropdown'} onClick={handleKickClick}>
56+ <Box className="kick-dropdown" onClick={handleKickClick}>
52− <Text>Kick</Text>
57+ <Text>Kick</Text>
53− </Box>
58+ </Box>
59+ </>
54 )}
60 )}
55 </Box>
61 </Box>
56 </li>
62 </li>
src/views/Ops/Residents.scss+10−1
@@ -57,7 +57,7 @@
57 }
57 }
58
58
59 .kick-dropdown {
59 .kick-dropdown {
60− z-index: 9999;
60+ z-index: 2;
61 display: flex;
61 display: flex;
62 flex-direction: column;
62 flex-direction: column;
63 align-items: flex-start;
63 align-items: flex-start;
@@ -113,4 +113,13 @@
113 }
113 }
114 }
114 }
115 }
115 }
116+
117+ .close-background {
118+ z-index: 1;
119+ position: fixed;
120+ top: 0;
121+ bottom: 0;
122+ right: 0;
123+ left: 0;
124+ }
116}
125}
src/views/Ops/Residents.tsx+17−22
@@ -1,27 +1,25 @@
1−import * as need from 'lib/need';
1+import { useCallback, useEffect, useState } from 'react';
2−import { Box, LoadingSpinner, Row } from '@tlon/indigo-react';
2+import { Box, Row } from '@tlon/indigo-react';
3+import { Ship } from '@urbit/roller-api';
4+
5+import { useRollerStore } from 'store/rollerStore';
6+import useRoller from 'lib/useRoller';
7+import { useLocalRouter } from 'lib/LocalRouter';
3
8
4import Window from 'components/L2/Window/Window';
9import Window from 'components/L2/Window/Window';
5import HeaderPane from 'components/L2/Window/HeaderPane';
10import HeaderPane from 'components/L2/Window/HeaderPane';
6import BodyPane from 'components/L2/Window/BodyPane';
11import BodyPane from 'components/L2/Window/BodyPane';
7−import { useLocalRouter } from 'lib/LocalRouter';
8import View from 'components/View';
12import View from 'components/View';
9import L2BackHeader from 'components/L2/Headers/L2BackHeader';
13import L2BackHeader from 'components/L2/Headers/L2BackHeader';
10−import { usePointCursor } from 'store/pointCursor';
11−import { useCallback, useEffect, useMemo, useState } from 'react';
12
14
13−import './Residents.scss';
14−import useRoller from 'lib/useRoller';
15−import { Ship } from '@urbit/roller-api';
16import { ResidentRow } from './ResidentRow';
15import { ResidentRow } from './ResidentRow';
16+import './Residents.scss';
17
17
18export const Residents = () => {
18export const Residents = () => {
19 const { pop }: any = useLocalRouter();
19 const { pop }: any = useLocalRouter();
20− const { pointCursor } = usePointCursor();
20+ const { point, setLoading } = useRollerStore();
21− const point = useMemo(() => need.point(pointCursor), [pointCursor]);
22− const { api, kickPoint } = useRoller();
21+ const { api, changeSponsorship } = useRoller();
23 const [sponsoredPoints, setSponsoredPoints] = useState<Ship[]>([]);
22 const [sponsoredPoints, setSponsoredPoints] = useState<Ship[]>([]);
24− const [loading, setLoading] = useState<boolean>(false);
25
23
26 const fetchResidents = useCallback(async () => {
24 const fetchResidents = useCallback(async () => {
27 if (!point) {
25 if (!point) {
@@ -30,18 +28,19 @@ export const Residents = () => {
30
28
31 setLoading(true);
29 setLoading(true);
32 // TODO: remove this hack once the API is updated
30 // TODO: remove this hack once the API is updated
33− const { residentes, residents } = await api.getSponsoredPoints(point);
31+ const { residentes, residents } = await api.getSponsoredPoints(point.value);
34 setSponsoredPoints((residents || residentes || []).sort());
32 setSponsoredPoints((residents || residentes || []).sort());
35 setLoading(false);
33 setLoading(false);
36− }, [api, point]);
34+ }, [api, point, setLoading]);
37
35
38 const handleKick = useCallback(
36 const handleKick = useCallback(
39 async (point: Ship) => {
37 async (point: Ship) => {
40 setLoading(true);
38 setLoading(true);
41− await kickPoint(point);
39+ await changeSponsorship(point, 'detach');
42 await fetchResidents();
40 await fetchResidents();
41+ setLoading(false);
43 },
42 },
44− [fetchResidents, kickPoint]
43+ [fetchResidents, changeSponsorship, setLoading]
45 );
44 );
46
45
47 useEffect(() => {
46 useEffect(() => {
@@ -66,14 +65,10 @@ export const Residents = () => {
66 </HeaderPane>
65 </HeaderPane>
67 <BodyPane>
66 <BodyPane>
68 <Box className="content-container">
67 <Box className="content-container">
69− {loading ? (
70− <Box className={'loading'}>
71− <LoadingSpinner />
72− </Box>
73− ) : sponsoredPoints.length > 0 ? (
68+ {sponsoredPoints.length > 0 ? (
74 <ul className="residents-list">
69 <ul className="residents-list">
75 {sponsoredPoints.map(sp => (
70 {sponsoredPoints.map(sp => (
76− <ResidentRow point={sp} onKick={handleKick} />
71+ <ResidentRow key={sp} point={sp} onKick={handleKick} />
77 ))}
72 ))}
78 </ul>
73 </ul>
79 ) : (
74 ) : (
src/views/Point/AddressButton.tsx+1−1
@@ -22,7 +22,7 @@ export const AddressButton = ({ children, ...rest }: AddressButtonProps) => {
22 );
22 );
23
23
24 return (
24 return (
25− <Button onClick={onClick} {...rest} className={'secondary'}>
25+ <Button onClick={onClick} {...rest} className={'secondary'} type="button">
26 {children}
26 {children}
27 </Button>
27 </Button>
28 );
28 );
src/views/Point/IssueChild.tsx+42−17
@@ -7,6 +7,7 @@ import BodyPane from 'components/L2/Window/BodyPane';
7
7
8import * as azimuth from 'azimuth-js';
8import * as azimuth from 'azimuth-js';
9import ob from 'urbit-ob';
9import ob from 'urbit-ob';
10+import { Box, Icon, Row, Text } from '@tlon/indigo-react';
10
11
11import { useNetwork } from 'store/network';
12import { useNetwork } from 'store/network';
12import { usePointCache } from 'store/pointCache';
13import { usePointCache } from 'store/pointCache';
@@ -20,9 +21,15 @@ import { patp2dec } from 'lib/patp2dec';
20import { getSpawnCandidate } from 'lib/child';
21import { getSpawnCandidate } from 'lib/child';
21import { useLocalRouter } from 'lib/LocalRouter';
22import { useLocalRouter } from 'lib/LocalRouter';
22import { validateChild } from 'lib/validators';
23import { validateChild } from 'lib/validators';
24+import { convertToInt } from 'lib/convertToInt';
25+import { isGalaxy } from 'lib/utils/point';
26+import useRoller from 'lib/useRoller';
23
27
24import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
28import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
25import View from 'components/View';
29import View from 'components/View';
30+import CopiableAddress from 'components/copiable/CopiableAddress';
31+import L2BackHeader from 'components/L2/Headers/L2BackHeader';
32+import LoadingOverlay from 'components/L2/LoadingOverlay';
26import { PointInput, AddressInput } from 'form/Inputs';
33import { PointInput, AddressInput } from 'form/Inputs';
27import {
34import {
28 composeValidator,
35 composeValidator,
@@ -32,16 +39,10 @@ import {
32} from 'form/validators';
39} from 'form/validators';
33import BridgeForm from 'form/BridgeForm';
40import BridgeForm from 'form/BridgeForm';
34import FormError from 'form/FormError';
41import FormError from 'form/FormError';
35−import CopiableAddress from 'components/copiable/CopiableAddress';
36−import { convertToInt } from 'lib/convertToInt';
37−import { Box, Icon, Row, Text } from '@tlon/indigo-react';
38−import { isGalaxy } from 'lib/utils/point';
39−import L2BackHeader from 'components/L2/Headers/L2BackHeader';
40
42
41−import './IssueChild.scss';
42import { PatpBadge } from './PatpBadge';
43import { PatpBadge } from './PatpBadge';
43−import useRoller from 'lib/useRoller';
44import { AddressButton } from './AddressButton';
44import { AddressButton } from './AddressButton';
45+import './IssueChild.scss';
45
46
46export function useIssueChild() {
47export function useIssueChild() {
47 const { contracts }: any = useNetwork();
48 const { contracts }: any = useNetwork();
@@ -76,7 +77,9 @@ export default function IssueChild() {
76 );
77 );
77 const [candidates, setCandidates] = useState<string[]>([]);
78 const [candidates, setCandidates] = useState<string[]>([]);
78 const [pointToSpawn, setPointToSpawn] = useState<number | null>();
79 const [pointToSpawn, setPointToSpawn] = useState<number | null>();
80+ const [destinationAddress, setDestinationAddress] = useState<string | null>();
79 const [l2SpawnSent, setL2SpawnSent] = useState(false);
81 const [l2SpawnSent, setL2SpawnSent] = useState(false);
82+ const [loading, setLoading] = useState(false);
80
83
81 const fetchAvailablePoints = useCallback(async () => {
84 const fetchAvailablePoints = useCallback(async () => {
82 const available = await api.getUnspawned(_point);
85 const available = await api.getUnspawned(_point);
@@ -112,13 +115,15 @@ export default function IssueChild() {
112 }, [fetchAvailablePoints, shuffle]);
115 }, [fetchAvailablePoints, shuffle]);
113
116
114 const spawnNewPoint = useCallback(async () => {
117 const spawnNewPoint = useCallback(async () => {
115− if (pointToSpawn) {
118+ if (pointToSpawn && destinationAddress) {
119+ setLoading(true);
116− await spawnPoint(pointToSpawn);
120+ await spawnPoint(pointToSpawn, destinationAddress);
117 checkForUpdates(pointToSpawn);
121 checkForUpdates(pointToSpawn);
118 setPointToSpawn(null);
122 setPointToSpawn(null);
119 setL2SpawnSent(true);
123 setL2SpawnSent(true);
124+ setLoading(false);
120 }
125 }
121− }, [pointToSpawn, spawnPoint, checkForUpdates]);
126+ }, [pointToSpawn, destinationAddress, spawnPoint, checkForUpdates]);
122
127
123 const {
128 const {
124 isDefaultState,
129 isDefaultState,
@@ -173,16 +178,37 @@ export default function IssueChild() {
173 const onValues = useCallback(
178 const onValues = useCallback(
174 ({ valid, values }) => {
179 ({ valid, values }) => {
175 if (valid) {
180 if (valid) {
181+ console.log('VALID')
176− const pointDecimal = patp2dec(values.point);
182+ construct(patp2dec(values.point), values.owner);
177− construct(pointDecimal, values.owner);
178− setPointToSpawn(pointDecimal);
179 } else {
183 } else {
180 unconstruct();
184 unconstruct();
181 }
185 }
186+
187+ try {
188+ setPointToSpawn(patp2dec(values.point));
189+ } catch (err) {
190+ setPointToSpawn(null);
191+ }
192+
193+ setDestinationAddress(values.owner);
182 },
194 },
183 [construct, unconstruct]
195 [construct, unconstruct]
184 );
196 );
185
197
198+ const validPoint =
199+ pointToSpawn && availablePoints && availablePoints.has(pointToSpawn);
200+ const validAddress =
201+ destinationAddress && !buildAddressValidator()(destinationAddress);
202+
203+ const getButtonText = () => {
204+ if (!validPoint) {
205+ return `Please enter a valid ${point.isStar ? 'planet' : 'star'}`;
206+ } else if (!validAddress) {
207+ return `Please enter a valid destination address`;
208+ }
209+ return `Spawn ${ob.patp(pointToSpawn)}`;
210+ };
211+
186 return (
212 return (
187 <View
213 <View
188 pop={pop}
214 pop={pop}
@@ -276,11 +302,9 @@ export default function IssueChild() {
276 className="mt4"
302 className="mt4"
277 center
303 center
278 solid
304 solid
279− disabled={!pointToSpawn}
305+ disabled={!validPoint || !validAddress}
280 onClick={spawnNewPoint}>
306 onClick={spawnNewPoint}>
281− {pointToSpawn
307+ {getButtonText()}
282− ? `Spawn ${ob.patp(pointToSpawn)}`
283− : `Please enter a ${point.isStar ? 'planet' : 'star'}`}
284 </Grid.Item>
308 </Grid.Item>
285 ) : (
309 ) : (
286 <Grid.Item
310 <Grid.Item
@@ -296,6 +320,7 @@ export default function IssueChild() {
296 </Box>
320 </Box>
297 </BodyPane>
321 </BodyPane>
298 </Window>
322 </Window>
323+ <LoadingOverlay loading={loading} />
299 </View>
324 </View>
300 );
325 );
301}
326}
src/views/Point/Point.tsx+13−3
@@ -66,12 +66,14 @@ export default function Point() {
66 const getTransactions = async () => {
66 const getTransactions = async () => {
67 setLoading(true);
67 setLoading(true);
68 getPendingTransactions();
68 getPendingTransactions();
69+ if (point.isStar && (point.canSpawn || point.canManage)) {
69− getInvites();
70+ getInvites();
71+ }
70 setTimeout(() => setLoading(false), 100);
72 setTimeout(() => setLoading(false), 100);
71 };
73 };
72
74
73 await getTransactions();
75 await getTransactions();
74− }, [getInvites, getPendingTransactions, setLoading]);
76+ }, [getInvites, getPendingTransactions, setLoading]); // eslint-disable-line react-hooks/exhaustive-deps
75
77
76 useEffect(() => {
78 useEffect(() => {
77 loadL1Info();
79 loadL1Info();
@@ -166,8 +168,16 @@ export default function Point() {
166 ? 'Transfer Proxy Changed'
168 ? 'Transfer Proxy Changed'
167 : pendingTx.rawTx?.tx?.type === 'set-spawn-proxy'
169 : pendingTx.rawTx?.tx?.type === 'set-spawn-proxy'
168 ? 'Spawn Proxy Changed'
170 ? 'Spawn Proxy Changed'
171+ : pendingTx.rawTx?.tx?.type === 'escape'
172+ ? 'Change Sponsor'
173+ : pendingTx.rawTx?.tx?.type === 'cancel-escape'
174+ ? 'Cancel Escape'
175+ : pendingTx.rawTx?.tx?.type === 'adopt'
176+ ? 'Accept Sponsorship'
177+ : pendingTx.rawTx?.tx?.type === 'reject'
178+ ? 'Reject Sponsorship'
169 : pendingTx.rawTx?.tx?.type === 'detach'
179 : pendingTx.rawTx?.tx?.type === 'detach'
170− ? 'Sponsee Detached'
180+ ? 'Detach Sponsee'
171 : ''}
181 : ''}
172 </div>
182 </div>
173 <div className="rollup-timer">
183 <div className="rollup-timer">
src/views/Points.js+30−29
@@ -28,7 +28,6 @@ import NavHeader from 'components/NavHeader';
28import L2PointHeader from 'components/L2/Headers/L2PointHeader';
28import L2PointHeader from 'components/L2/Headers/L2PointHeader';
29import IncomingPoint from 'components/L2/Points/IncomingPoint';
29import IncomingPoint from 'components/L2/Points/IncomingPoint';
30import LoadingOverlay from 'components/L2/LoadingOverlay';
30import LoadingOverlay from 'components/L2/LoadingOverlay';
31−import { onlyUnique } from 'lib/utils/array';
32
31
33export const maybeGetResult = (obj, key, defaultValue) =>
32export const maybeGetResult = (obj, key, defaultValue) =>
34 obj.matchWith({
33 obj.matchWith({
@@ -43,31 +42,6 @@ export const maybeGetResult = (obj, key, defaultValue) =>
43export const hasTransferProxy = details =>
42export const hasTransferProxy = details =>
44 !isZeroAddress(details.transferProxy);
43 !isZeroAddress(details.transferProxy);
45
44
46−export const getOutgoingPoints = (controlledPoints, getDetails) => {
47− return controlledPoints
48− .chain(points =>
49− points.matchWith({
50− Error: () => Nothing(),
51− Ok: c => {
52− const points = c.value.ownedPoints.map(point =>
53− getDetails(point).chain(details =>
54− Just({ point: point, has: hasTransferProxy(details) })
55− )
56− );
57− if (points.every(p => Just.hasInstance(p))) {
58− const outgoing = points
59− .filter(p => p.value.has)
60− .map(p => p.value.point);
61− return Just(outgoing);
62− } else {
63− return Nothing();
64− }
65− },
66− })
67− )
68− .getOrElse([]);
69−};
70−
71export const isLocked = (details, contracts) =>
45export const isLocked = (details, contracts) =>
72 details.owner === contracts.linearSR ||
46 details.owner === contracts.linearSR ||
73 details.owner === contracts.conditionalSR;
47 details.owner === contracts.conditionalSR;
@@ -77,6 +51,7 @@ const PointList = function({
77 className,
51 className,
78 actions,
52 actions,
79 locked = false,
53 locked = false,
54+ processing = false,
80 ...rest
55 ...rest
81}) {
56}) {
82 const { setPointCursor } = usePointCursor();
57 const { setPointCursor } = usePointCursor();
@@ -91,9 +66,10 @@ const PointList = function({
91 <Flex col>
66 <Flex col>
92 <Passport.Mini
67 <Passport.Mini
93 locked={locked}
68 locked={locked}
69+ processing={processing}
94 point={point}
70 point={point}
95 onClick={
71 onClick={
96− locked
72+ locked || processing
97 ? undefined
73 ? undefined
98 : () => {
74 : () => {
99 setPointCursor(Just(point));
75 setPointCursor(Just(point));
@@ -143,7 +119,11 @@ export default function Points() {
143 const { syncStarReleaseDetails, starReleaseDetails } = useStarReleaseCache();
119 const { syncStarReleaseDetails, starReleaseDetails } = useStarReleaseCache();
144
120
145 const outgoingPoints = useMemo(
121 const outgoingPoints = useMemo(
122+ () =>
146− () => pointList.filter(({ isTransferProxySet }) => isTransferProxySet),
123+ pointList.filter(
124+ ({ isTransferProxySet, isTransferProxy }) =>
125+ isTransferProxySet && !isTransferProxy
126+ ),
147 [pointList]
127 [pointList]
148 );
128 );
149
129
@@ -237,7 +217,14 @@ export default function Points() {
237 const lockedPoints = maybeLockedPoints.getOrElse([]);
217 const lockedPoints = maybeLockedPoints.getOrElse([]);
238
218
239 const allPoints = pointList
219 const allPoints = pointList
220+ .filter(
240− .filter(({ isTransferProxySet }) => !isTransferProxySet)
221+ ({ isTransferProxySet, shouldDisplay }) =>
222+ !isTransferProxySet && shouldDisplay
223+ )
224+ .map(({ value }) => value);
225+
226+ const processingPoints = pointList
227+ .filter(({ shouldDisplay }) => !shouldDisplay)
241 .map(({ value }) => value);
228 .map(({ value }) => value);
242
229
243 const displayEmptyState =
230 const displayEmptyState =
@@ -393,6 +380,20 @@ export default function Points() {
393 </Grid.Item>
380 </Grid.Item>
394 )}
381 )}
395
382
383+ {processingPoints.length > 0 && (
384+ <Grid.Item full as={Grid} gap={1} className="mv6">
385+ <Grid.Item full as={H5}>
386+ Awaiting L2 Rollup
387+ </Grid.Item>
388+ <Grid.Item
389+ full
390+ as={PointList}
391+ points={processingPoints}
392+ processing
393+ />
394+ </Grid.Item>
395+ )}
396+
396 <Footer>
397 <Footer>
397 <Grid>
398 <Grid>
398 <Grid.Divider />
399 <Grid.Divider />
src/views/Transactions/TransactionHistory.scss+1
@@ -253,6 +253,7 @@
253 background-color: $rollup-yellow;
253 background-color: $rollup-yellow;
254 color: #000;
254 color: #000;
255 width: 100px;
255 width: 100px;
256+ width: 105px;
256
257
257 svg {
258 svg {
258 path {
259 path {
src/views/Transactions/TransactionHistory.tsx+10−8
@@ -33,7 +33,7 @@ const TransactionHistory = () => {
33 const [loading, setLoading] = useState(true);
33 const [loading, setLoading] = useState(true);
34 const [transactions, setTransactions] = useState<RollerTransaction[]>([]);
34 const [transactions, setTransactions] = useState<RollerTransaction[]>([]);
35 const [dropdownOpen, setDropdownOpen] = useState<boolean>(false);
35 const [dropdownOpen, setDropdownOpen] = useState<boolean>(false);
36− const [selectedPatp, setSelectedPatp] = useState<string | null>(null);
36+ const [selectedPatp, setSelectedPatp] = useState<string | null>();
37
37
38 // Pre-filter the results if navigating from Point view
38 // Pre-filter the results if navigating from Point view
39 const filterByPoint = useMemo(() => {
39 const filterByPoint = useMemo(() => {
@@ -51,7 +51,7 @@ const TransactionHistory = () => {
51 const txns = await api.getHistory(address);
51 const txns = await api.getHistory(address);
52 setTransactions(txns);
52 setTransactions(txns);
53 if (filterByPoint) {
53 if (filterByPoint) {
54− setSelectedPatp(filterByPoint);
54+ setSelectedPatp(filterByPoint.slice(1));
55 }
55 }
56 setLoading(false);
56 setLoading(false);
57 }, [address, api, filterByPoint]);
57 }, [address, api, filterByPoint]);
@@ -64,10 +64,10 @@ const TransactionHistory = () => {
64 const txnsByPatp = useMemo(() => {
64 const txnsByPatp = useMemo(() => {
65 return transactions.reduce((memo, tx) => {
65 return transactions.reduce((memo, tx) => {
66 const patp = tx.ship;
66 const patp = tx.ship;
67− if (Object.keys(memo).includes(patp)) {
67+ if (patp && Object.keys(memo).includes(patp as string)) {
68 memo[patp].push(tx);
68 memo[patp].push(tx);
69 } else {
69 } else {
70− memo[patp] = [tx];
70+ memo[patp || '~'] = [tx];
71 }
71 }
72 return memo;
72 return memo;
73 }, {} as GroupedTransactions);
73 }, {} as GroupedTransactions);
@@ -90,8 +90,7 @@ const TransactionHistory = () => {
90 useEffect(() => {
90 useEffect(() => {
91 fetchTransactions();
91 fetchTransactions();
92 // Load the TX history only once
92 // Load the TX history only once
93− // eslint-disable-next-line react-hooks/exhaustive-deps
93+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
94− }, []);
95
94
96 return (
95 return (
97 <View
96 <View
@@ -149,7 +148,10 @@ const TransactionHistory = () => {
149 <BodyPane>
148 <BodyPane>
150 {loading ? (
149 {loading ? (
151 <Box className={'loading'}>
150 <Box className={'loading'}>
152− <LoadingSpinner />
151+ <LoadingSpinner
152+ foreground="rgba(0,0,0,0.3)"
153+ background="white"
154+ />
153 </Box>
155 </Box>
154 ) : (
156 ) : (
155 <Box className="transaction-container">
157 <Box className="transaction-container">
@@ -161,7 +163,7 @@ const TransactionHistory = () => {
161 <PatpRow patp={patp} />
163 <PatpRow patp={patp} />
162 {txnsByPatp[patp]
164 {txnsByPatp[patp]
163 .sort((a, b) => {
165 .sort((a, b) => {
164− return b.time - a.time;
166+ return (b?.time || 0) - (a?.time || 0);
165 })
167 })
166 .map(tx => (
168 .map(tx => (
167 <TransactionRow key={tx.time} {...tx} />
169 <TransactionRow key={tx.time} {...tx} />
src/views/Transactions/TransactionRow.tsx+9−8
@@ -1,19 +1,20 @@
1+import { useMemo } from 'react';
1import cn from 'classnames';
2import cn from 'classnames';
2import * as ob from 'urbit-ob';
3import * as ob from 'urbit-ob';
3import { Box, Icon, Row } from '@tlon/indigo-react';
4import { Box, Icon, Row } from '@tlon/indigo-react';
4import { RollerTransaction } from '@urbit/roller-api';
5import { RollerTransaction } from '@urbit/roller-api';
5import { format, fromUnixTime } from 'date-fns';
6import { format, fromUnixTime } from 'date-fns';
6−import { titleize } from 'form/formatters';
7+
8+import { useTimerStore } from 'store/timerStore';
7import {
9import {
8 TRANSACTION_STATUS_ICONS,
10 TRANSACTION_STATUS_ICONS,
9 TRANSACTION_TYPE_ICONS,
11 TRANSACTION_TYPE_ICONS,
10 TRANSACTION_TYPE_TITLES,
12 TRANSACTION_TYPE_TITLES,
11} from 'lib/constants';
13} from 'lib/constants';
12import { abbreviateAddress } from 'lib/utils/address';
14import { abbreviateAddress } from 'lib/utils/address';
13−import { useMemo } from 'react';
14−import { useRollerStore } from 'store/rollerStore';
15−import LayerIndicator from 'components/L2/LayerIndicator';
16import { isPlanet, isStar } from 'lib/utils/point';
15import { isPlanet, isStar } from 'lib/utils/point';
16+import { titleize } from 'form/formatters';
17+import LayerIndicator from 'components/L2/LayerIndicator';
17
18
18export const TransactionRow = ({
19export const TransactionRow = ({
19 ship,
20 ship,
@@ -22,7 +23,7 @@ export const TransactionRow = ({
22 status,
23 status,
23 time,
24 time,
24}: RollerTransaction) => {
25}: RollerTransaction) => {
25− const { nextRoll } = useRollerStore();
26+ const { nextRoll } = useTimerStore();
26
27
27 // For spawn events (associated with the spawner), we want to show the spawnee tier;
28 // For spawn events (associated with the spawner), we want to show the spawnee tier;
28 // For all other events we will show the event's ship's tier
29 // For all other events we will show the event's ship's tier
@@ -40,15 +41,15 @@ export const TransactionRow = ({
40 }, [ship, type]);
41 }, [ship, type]);
41
42
42 const label = useMemo(() => {
43 const label = useMemo(() => {
43− return TRANSACTION_TYPE_TITLES[type].replace('Point', pointLabel) || type;
44+ return TRANSACTION_TYPE_TITLES[type] || type;
44− }, [pointLabel, type]);
45+ }, [type]);
45
46
46 const icon = useMemo(() => {
47 const icon = useMemo(() => {
47 return <Icon icon={TRANSACTION_TYPE_ICONS[type] || 'Bug'} />;
48 return <Icon icon={TRANSACTION_TYPE_ICONS[type] || 'Bug'} />;
48 }, [type]);
49 }, [type]);
49
50
50 const shortHash = useMemo(() => {
51 const shortHash = useMemo(() => {
51− return abbreviateAddress(hash);
52+ return hash ? abbreviateAddress(hash) : '';
52 }, [hash]);
53 }, [hash]);
53
54
54 const parsedDate = useMemo(() => {
55 const parsedDate = useMemo(() => {
src/views/UrbitID/Transfer.js+1−1
@@ -120,7 +120,7 @@ export default function AdminTransfer() {
120 <>
120 <>
121 {!completed && (
121 {!completed && (
122 <Grid.Item full as={Text} className={cn('f5 wrap')}>
122 <Grid.Item full as={Text} className={cn('f5 wrap')}>
123− Transfer {name} to a new owner.
123+ Transfer <code>{name}</code> to a new owner.
124 </Grid.Item>
124 </Grid.Item>
125 )}
125 )}
126 {!completed && needLockupWarning && (
126 {!completed && needLockupWarning && (
src/views/UrbitOS/ChangeSponsor.js+82−47
@@ -2,33 +2,31 @@ import React, { useState, useMemo, useCallback, useEffect } from 'react';
2import { Grid, Button } from 'indigo-react';
2import { Grid, Button } from 'indigo-react';
3import * as ob from 'urbit-ob';
3import * as ob from 'urbit-ob';
4import { azimuth, ecliptic } from 'azimuth-js';
4import { azimuth, ecliptic } from 'azimuth-js';
5+import { Box, Row } from '@tlon/indigo-react';
5
6
6import { useNetwork } from 'store/network';
7import { useNetwork } from 'store/network';
7import { usePointCache } from 'store/pointCache';
8import { usePointCache } from 'store/pointCache';
9+import { usePointCursor } from 'store/pointCursor';
10+import { useRollerStore } from 'store/rollerStore';
11+import { useHistory } from 'store/history';
12+
13+import * as need from 'lib/need';
14+import { validateMinimumPatpByteLength } from 'lib/validators';
15+import useEthereumTransaction from 'lib/useEthereumTransaction';
16+import useRoller from 'lib/useRoller';
8
17
9import { PointInput } from 'form/Inputs';
18import { PointInput } from 'form/Inputs';
10import FormError from 'form/FormError';
19import FormError from 'form/FormError';
11import BridgeForm from 'form/BridgeForm';
20import BridgeForm from 'form/BridgeForm';
12−
13−import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
14−import ViewHeader from 'components/ViewHeader';
15import {
21import {
16 composeValidator,
22 composeValidator,
17 buildPointValidator,
23 buildPointValidator,
18 hasErrors,
24 hasErrors,
19} from 'form/validators';
25} from 'form/validators';
20−
26+import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
21−import * as need from 'lib/need';
22−import { validateMinimumPatpByteLength } from 'lib/validators';
23−import { usePointCursor } from 'store/pointCursor';
24−import useEthereumTransaction from 'lib/useEthereumTransaction';
25−import { isStar } from 'lib/utils/point';
26import Window from 'components/L2/Window/Window';
27import Window from 'components/L2/Window/Window';
27import HeaderPane from 'components/L2/Window/HeaderPane';
28import HeaderPane from 'components/L2/Window/HeaderPane';
28−import { Box, Row } from '@tlon/indigo-react';
29import BodyPane from 'components/L2/Window/BodyPane';
29import BodyPane from 'components/L2/Window/BodyPane';
30−import { useRollerStore } from 'store/rollerStore';
31−import useRoller from 'lib/useRoller';
32
30
33function useChangeSponsor() {
31function useChangeSponsor() {
34 const { contracts } = useNetwork();
32 const { contracts } = useNetwork();
@@ -51,23 +49,27 @@ function useChangeSponsor() {
51
49
52function ChangeSponsor({ onDone }) {
50function ChangeSponsor({ onDone }) {
53 const { contracts } = useNetwork();
51 const { contracts } = useNetwork();
54− const { pointCursor } = usePointCursor();
52+ const { pop } = useHistory();
55− const { changeSponsor } = useRoller();
53+ const { changeSponsor, checkForUpdates } = useRoller();
56− const {
57− point: { isL2 },
58− } = useRollerStore();
54+ const { point, setLoading } = useRollerStore();
59
55
60 const _contracts = need.contracts(contracts);
56 const _contracts = need.contracts(contracts);
61− const point = need.point(pointCursor);
62 const { construct, unconstruct, inputsLocked, bind } = useChangeSponsor();
57 const { construct, unconstruct, inputsLocked, bind } = useChangeSponsor();
63
58
64 const [newSponsor, setNewSponsor] = useState();
59 const [newSponsor, setNewSponsor] = useState();
65
60
66 const requestNewSponsor = useCallback(async () => {
61 const requestNewSponsor = useCallback(async () => {
67 if (newSponsor !== undefined) {
62 if (newSponsor !== undefined) {
63+ setLoading(true);
68− changeSponsor(newSponsor);
64+ await changeSponsor(newSponsor);
65+ checkForUpdates(
66+ point.value,
67+ `${point.patp} has requested ${ob.patp(newSponsor)} as a sponsor`
68+ );
69+ setLoading(false);
70+ pop();
69 }
71 }
70− }, [newSponsor, changeSponsor]);
72+ }, [point, newSponsor, changeSponsor, setLoading, checkForUpdates, pop]);
71
73
72 const validateFormAsync = useCallback(
74 const validateFormAsync = useCallback(
73 async values => {
75 async values => {
@@ -92,7 +94,7 @@ function ChangeSponsor({ onDone }) {
92 );
94 );
93
95
94 const validate = useMemo(() => {
96 const validate = useMemo(() => {
95− const sponsorSize = isStar(point) ? 1 : 2;
97+ const sponsorSize = point.isStar ? 1 : 2;
96 return composeValidator(
98 return composeValidator(
97 {
99 {
98 sponsor: buildPointValidator(sponsorSize, [
100 sponsor: buildPointValidator(sponsorSize, [
@@ -117,6 +119,8 @@ function ChangeSponsor({ onDone }) {
117 [construct, unconstruct]
119 [construct, unconstruct]
118 );
120 );
119
121
122+ const buttonText = 'Request New Sponsor';
123+
120 return (
124 return (
121 <Window>
125 <Window>
122 <HeaderPane>
126 <HeaderPane>
@@ -135,17 +139,17 @@ function ChangeSponsor({ onDone }) {
135 disabled={inputsLocked}
139 disabled={inputsLocked}
136 label="New sponsor"
140 label="New sponsor"
137 className="mv4"
141 className="mv4"
142+ size={2}
138 />
143 />
139 <Grid.Item full as={FormError} />
144 <Grid.Item full as={FormError} />
140− {isL2 ? (
145+ {point.isL2 ? (
141 <Grid.Item
146 <Grid.Item
142 as={Button}
147 as={Button}
143 full
148 full
144− className=""
145 center
149 center
146 solid
150 solid
147 onClick={requestNewSponsor}>
151 onClick={requestNewSponsor}>
148− {'Request New Sponsor'}
152+ {buttonText}
149 </Grid.Item>
153 </Grid.Item>
150 ) : (
154 ) : (
151 <Grid.Item
155 <Grid.Item
@@ -153,7 +157,7 @@ function ChangeSponsor({ onDone }) {
153 as={InlineEthereumTransaction}
157 as={InlineEthereumTransaction}
154 {...bind}
158 {...bind}
155 onReturn={onDone}
159 onReturn={onDone}
156− label="Request"
160+ label={buttonText}
157 />
161 />
158 )}
162 )}
159 </Box>
163 </Box>
@@ -183,35 +187,66 @@ function useCancelEscape() {
183
187
184function CurrentEscape({ onDone }) {
188function CurrentEscape({ onDone }) {
185 const { construct, bind } = useCancelEscape();
189 const { construct, bind } = useCancelEscape();
190+ const { point } = useRollerStore();
186
191
187− const { pointCursor } = usePointCursor();
192+ const { pop } = useHistory();
188− const { getDetails } = usePointCache();
193+ const { cancelEscape, checkForUpdates } = useRoller();
189−
194+ const {
190− const point = need.point(pointCursor);
195+ point: { isL2 },
191−
196+ setLoading,
192− const details = need.details(getDetails(point));
197+ } = useRollerStore();
193
198
194− const newSponsor = useMemo(() => ob.patp(details.escapeRequestedTo), [
199+ const newSponsor = useMemo(() => ob.patp(point.escapeRequestedTo), [point]);
195− details,
196− ]);
197
200
198 useEffect(() => {
201 useEffect(() => {
199 construct();
202 construct();
200 }, [construct]);
203 }, [construct]);
204+
205+ const cancelRequest = useCallback(async () => {
206+ if (newSponsor !== undefined) {
207+ setLoading(true);
208+ await cancelEscape(newSponsor);
209+ checkForUpdates(point.value, `${point.patp}'s sponsor change cancelled`);
210+ setLoading(false);
211+ pop();
212+ }
213+ }, [point, newSponsor, cancelEscape, setLoading, checkForUpdates, pop]);
214+
215+ const buttonText = 'Cancel Request';
216+
201 return (
217 return (
202− <Grid>
218+ <Window>
203− <Grid.Item full as={ViewHeader}>
219+ <HeaderPane>
220+ <Row className="header-row">
221+ <h5>Change Sponsor</h5>
222+ </Row>
223+ </HeaderPane>
224+ <BodyPane>
225+ <Box width="100%" fontSize={14} className="mb4">
204− Currently requesting <span className="mono">{newSponsor}</span> as a new
226+ Currently requesting <span className="mono">{newSponsor}</span> as a
205− sponsor
227+ new sponsor
228+ </Box>
229+ {isL2 ? (
230+ <Grid.Item
231+ as={Button}
232+ full
233+ center
234+ solid
235+ style={{ width: '100%' }}
236+ onClick={cancelRequest}>
237+ {buttonText}
206− </Grid.Item>
238+ </Grid.Item>
239+ ) : (
207− <Grid.Item
240+ <Grid.Item
208− full
241+ full
209− as={InlineEthereumTransaction}
242+ as={InlineEthereumTransaction}
210− label="Cancel Request"
243+ label={buttonText}
211− {...bind}
244+ {...bind}
212− onReturn={onDone}
245+ onReturn={onDone}
213− />
246+ />
214− </Grid>
247+ )}
248+ </BodyPane>
249+ </Window>
215 );
250 );
216}
251}
217
252