on master help sign in
Merge pull request #835 from willbach/l1-pending-tx

Added pending L1 transaction indicators

commit29d187d09a14580ad84ce00574071cfd024a7255 authorWill Galebach <willbach@gmail.com> dateThu 9 Dec 2021, 08:12 (5 years ago) parentb8452f878adbb6
24 files changed+559−215
src/components/L2/Headers/L2PointHeader.scss+11 src/components/L2/Headers/L2PointHeader.tsx+2 src/components/L2/PendingTransaction.scss+64 src/components/L2/PendingTransaction.tsx+89 src/components/L2/PermissionIndicator.scss+15 src/components/L2/PermissionIndicator.tsx+35 src/components/L2/PointList.tsx+65 src/lib/getSuggestedGasPrice.js+18−8 src/lib/proxy.js+17 src/lib/router.js+1−1 src/lib/types/PendingL1Transaction.ts+22 src/lib/types/Point.ts+25 src/lib/useRoller.ts+16−7 src/store/rollerStore.ts+18 src/views/AcceptTransfer.js → src/views/AcceptTransfer.tsx+16−15 src/views/CancelTransfer.js+8−2 src/views/Point/IssueChild.tsx+12−2 src/views/Point/MigrateL2.tsx+32−17 src/views/Point/Point.scss−47 src/views/Point/Point.tsx+26−38 src/views/Points.tsx+25−62 src/views/UrbitID/SetProxy.js+20−4 src/views/UrbitOS/ChangeSponsor.js+6−4 src/views/UrbitOS/NetworkKeys.tsx+16−8
src/components/L2/Headers/L2PointHeader.scss+11
@@ -12,6 +12,17 @@
1212 margin-right: 8px;
1313 }
1414
15+ .l2-spawn {
16+ border-radius: $medium-border-radius;
17+ color: white;
18+ background-color: black;
19+ text-align: center;
20+ height: 24px;
21+ padding: 8px 7px 0;
22+ font-size: 14px;
23+ margin-right: 4px;
24+ }
25+
1526 .dropdown {
1627 border-radius: $medium-border-radius;
1728 background-color: $washed-gray;
src/components/L2/Headers/L2PointHeader.tsx+2
@@ -83,6 +83,8 @@ const L2PointHeader = ({
8383 {point.value > -1 && (
8484 <LayerIndicator size="lg" layer={point.layer} className="header" />
8585 )}
86+ {/* uncomment for L2 spawn badge */}
87+ {/* {point.isL1 && point.isL2Spawn && <Box className="l2-spawn">L2 Spawn</Box>} */}
8688 </Row>
8789 <Row className="info">
8890 {renderInviteButton()}
src/components/L2/PendingTransaction.scssnew file+64
@@ -0,0 +1,64 @@
1+@import 'style/shared/variables.scss';
2+
3+.pending-transaction {
4+ font-size: 14px;
5+ padding: 8px;
6+ border: 1px solid $light-gray;
7+ border-radius: $large-border-radius;
8+ margin-bottom: 16px;
9+
10+ .title-row {
11+ display: flex;
12+ flex-direction: row;
13+ justify-content: space-between;
14+ align-items: center;
15+ margin-bottom: 4px;
16+
17+ .title {
18+ font-weight: 600;
19+ }
20+
21+ .hash {
22+ color: $dark-gray;
23+
24+ svg {
25+ height: 18px;
26+ width: 18px;
27+ margin-bottom: -4px;
28+ -ms-transform:rotate(315deg); /* IE9 */
29+ -moz-transform: rotate(315deg); /* FF3.5/3.6 */
30+ -o-transform: rotate(315deg); /* Opera 10.5 */
31+ -webkit-transform: rotate(315deg); /* Saf3.1+ */
32+ transform: rotate(315deg); /* Newer browsers */
33+
34+ path {
35+ fill: $dark-gray;
36+ }
37+ }
38+ }
39+
40+ .rollup-timer {
41+ background-color: $rollup-yellow;
42+ padding: 4px 8px;
43+ width: 112px;
44+ border-radius: $medium-border-radius;
45+ display: flex;
46+ flex-direction: row;
47+ justify-content: center;
48+ align-items: center;
49+
50+ svg {
51+ height: 14px;
52+ width: 14px;
53+ margin-right: 4px;
54+ }
55+ }
56+ }
57+
58+ .info-row {
59+ display: flex;
60+ flex-direction: row;
61+ justify-content: space-between;
62+ align-items: center;
63+ }
64+}
src/components/L2/PendingTransaction.tsxnew file+89
@@ -0,0 +1,89 @@
1+import { Box, Row, Icon } from '@tlon/indigo-react';
2+import { L1TxnType } from 'lib/types/PendingL1Transaction';
3+import { useExploreTxUrl } from 'lib/explorer';
4+import LayerIndicator from './LayerIndicator';
5+
6+import './PendingTransaction.scss';
7+
8+export const getPendingTitle = (type?: string) =>
9+ type === 'set-management-proxy'
10+ ? 'Management Address Changed'
11+ : type === 'configure-keys'
12+ ? 'Network Keys Configured'
13+ : type === 'transfer-point'
14+ ? 'Point Transfered'
15+ : type === 'set-transfer-proxy'
16+ ? 'Transfer Proxy Changed'
17+ : type === 'set-spawn-proxy'
18+ ? 'Spawn Proxy Changed'
19+ : type === 'escape'
20+ ? 'Change Sponsor'
21+ : type === 'cancel-escape'
22+ ? 'Cancel Escape'
23+ : type === 'adopt'
24+ ? 'Accept Sponsorship'
25+ : type === 'reject'
26+ ? 'Reject Sponsorship'
27+ : type === 'detach'
28+ ? 'Detach Sponsee'
29+ : '';
30+
31+export const getPendingL1Title = (type?: L1TxnType) =>
32+ type === L1TxnType.setNetworkKeys
33+ ? 'Network Keys Configured'
34+ : type === L1TxnType.managementProxy
35+ ? 'Management Address Changed'
36+ : type === L1TxnType.transferProxy
37+ ? 'Transfer Proxy Changed'
38+ : type === L1TxnType.spawnProxy
39+ ? 'Spawn Proxy Changed'
40+ : type === L1TxnType.spawnPoint
41+ ? 'Spawn Point'
42+ : type === L1TxnType.acceptTransfer
43+ ? 'Accept Transfer'
44+ : type === L1TxnType.migrate
45+ ? 'Migrate to Layer 2'
46+ : type === L1TxnType.migrateSpawn
47+ ? 'Set Spawn Proxy to Layer 2'
48+ : '';
49+
50+interface Props {
51+ title: string;
52+ nextRoll?: string;
53+ hash?: string;
54+ layer: 1 | 2;
55+}
56+
57+const PendingTransactionComponent: React.FC<Props> = ({
58+ title,
59+ nextRoll,
60+ hash,
61+ layer,
62+}) => {
63+ const txHash = useExploreTxUrl(hash);
64+
65+ return (
66+ <Box className="pending-transaction">
67+ <Row className="title-row">
68+ <Box className="title">{title}</Box>
69+ {hash ? (
70+ <a className="hash" href={txHash} rel="noreferrer" target="_blank">
71+ Etherscan
72+ <Icon className="arrow" icon="ArrowEast" />
73+ </a>
74+ ) : (
75+ <Box className="rollup-timer">
76+ <Icon icon="Clock" />
77+ {nextRoll}
78+ </Box>
79+ )}
80+ </Row>
81+ <Row className="info-row">
82+ <LayerIndicator layer={layer} size="sm" />
83+ <Box className="date"></Box>
84+ </Row>
85+ </Box>
86+ );
87+};
88+
89+export default PendingTransactionComponent;
src/components/L2/PermissionIndicator.scssnew file+15
@@ -0,0 +1,15 @@
1+@import 'style/shared/variables.scss';
2+
3+.permission-indicator {
4+ position: absolute;
5+ top: 4px;
6+ right: 4px;
7+ height: 16px;
8+ line-height: 16px;
9+ font-size: 10px;
10+ vertical-align: middle;
11+ border-radius: $medium-border-radius;
12+ color: white;
13+ background-color: $dark-gray;
14+ padding: 0px 4px;
15+}
src/components/L2/PermissionIndicator.tsxnew file+35
@@ -0,0 +1,35 @@
1+import React from 'react';
2+import { Box } from '@tlon/indigo-react';
3+import { PermissionLevel, shouldDisplayPermissionLevel } from 'lib/types/Point';
4+
5+import './PermissionIndicator.scss';
6+
7+export interface PermissionIndicatorProps {
8+ permission: PermissionLevel;
9+ size?: 'sm' | 'smt' | 'md' | 'lg';
10+ className?: string;
11+ selected?: boolean;
12+}
13+
14+const permissionText = {
15+ own: 'OWN',
16+ manage: 'MGMT',
17+ spawn: 'SPWN',
18+ vote: 'VOTE',
19+ transfer: 'TSFR',
20+};
21+
22+const PermissionIndicator = ({
23+ permission,
24+ className = '',
25+}: PermissionIndicatorProps) => {
26+ const classes = `permission-indicator ${permission} ${className}`;
27+
28+ if (!permission || !shouldDisplayPermissionLevel(permission)) {
29+ return null;
30+ }
31+
32+ return <Box className={classes}>{permissionText[permission]}</Box>;
33+};
34+
35+export default PermissionIndicator;
src/components/L2/PointList.tsxnew file+65
@@ -0,0 +1,65 @@
1+import React from 'react';
2+import { Just } from 'folktale/maybe';
3+
4+import { useHistory } from 'store/history';
5+import { usePointCursor } from 'store/pointCursor';
6+import Point from 'lib/types/Point';
7+
8+import { Grid, Flex } from 'indigo-react';
9+import Passport from 'components/Passport';
10+import PermissionIndicator from './PermissionIndicator';
11+
12+interface PointListProps {
13+ points: Point[];
14+ className?: string;
15+ actions?: (point: Point, i: number) => React.Component;
16+ locked?: boolean;
17+ processing?: boolean;
18+}
19+
20+const PointList: React.FC<PointListProps> = function({
21+ points,
22+ className,
23+ actions,
24+ locked = false,
25+ processing = false,
26+ ...rest
27+}) {
28+ const { setPointCursor }: any = usePointCursor();
29+ const { push, names }: any = useHistory();
30+
31+ return (
32+ <Grid gap={4} className={className}>
33+ {points.map((point, i) => (
34+ <Grid.Item
35+ key={point.value}
36+ className={`full fourth-${(i % 4) + 1}-md fourth-${(i % 4) + 1}-lg`}>
37+ <Flex col className="rel">
38+ <Passport.Mini
39+ locked={locked}
40+ processing={processing}
41+ point={point.value}
42+ onClick={
43+ locked || processing
44+ ? undefined
45+ : () => {
46+ setPointCursor(Just(point.value));
47+ push(names.POINT);
48+ }
49+ }
50+ {...rest}
51+ />
52+ {actions && (
53+ <Flex.Item className="mt2">{actions(point, i)}</Flex.Item>
54+ )}
55+ {!locked && (
56+ <PermissionIndicator permission={point.permissionLevel} />
57+ )}
58+ </Flex>
59+ </Grid.Item>
60+ ))}
61+ </Grid>
62+ );
63+};
64+
65+export default PointList;
src/lib/getSuggestedGasPrice.js+18−8
@@ -31,7 +31,7 @@ export default async function getSuggestedGasPrice(networkType) {
3131 case NETWORK_TYPES.LOCAL:
3232 default:
3333 try {
34− const response = await fetch(
34+ const etherscanGas = await fetch(
3535 'https://api.etherscan.io/api' +
3636 '?module=gastracker' +
3737 '&action=gasoracle' +
@@ -42,20 +42,30 @@ export default async function getSuggestedGasPrice(networkType) {
4242 }
4343 );
4444
45− const json = await response.json();
45+ const ethgasstationGas = await fetch(
46+ 'https://ethgasstation.info/json/ethgasAPI.json',
47+ {
48+ method: 'GET',
49+ cache: 'no-cache',
50+ }
51+ );
52+
53+ const etherscanGasJson = await etherscanGas.json();
54+
55+ const ethgasstationGasJson = await ethgasstationGas.json();
4656
4757 return {
4858 fast: {
49− price: minGas(json.fast),
50− wait: formatWait(json.fastWait),
59+ price: minGas(etherscanGasJson.result.FastGasPrice),
60+ wait: formatWait(ethgasstationGasJson.fastWait),
5161 },
5262 average: {
53− price: minGas(json.average),
54− wait: formatWait(json.avgWait),
63+ price: minGas(etherscanGasJson.result.ProposeGasPrice),
64+ wait: formatWait(ethgasstationGasJson.avgWait),
5565 },
5666 low: {
57− price: minGas(json.safeLow),
58− wait: formatWait(json.safeLowWait),
67+ price: minGas(etherscanGasJson.result.SafeGasPrice),
68+ wait: formatWait(ethgasstationGasJson.safeLowWait),
5969 },
6070 };
6171 } catch (e) {
src/lib/proxy.js+17
@@ -1,3 +1,5 @@
1+import { L1TxnType } from './types/PendingL1Transaction';
2+
13export const PROXY_TYPE = {
24 MANAGEMENT: 'MANAGEMENT',
35 SPAWN: 'SPAWN',
@@ -20,6 +22,21 @@ export const proxyTypeToHuman = proxyType => {
2022 }
2123};
2224
25+export const proxyTypeToL1TxnType = proxyType => {
26+ switch (proxyType) {
27+ case PROXY_TYPE.MANAGEMENT:
28+ return L1TxnType.managementProxy;
29+ case PROXY_TYPE.SPAWN:
30+ return L1TxnType.spawnProxy;
31+ case PROXY_TYPE.TRANSFER:
32+ return L1TxnType.transferProxy;
33+ case PROXY_TYPE.VOTING:
34+ return L1TxnType.votingProxy;
35+ default:
36+ throw new Error(`Unknown proxyType: ${proxyType}`);
37+ }
38+};
39+
2340export const proxyTypeToHumanDescription = proxyType => {
2441 switch (proxyType) {
2542 case PROXY_TYPE.MANAGEMENT:
src/lib/router.js+1−1
@@ -20,7 +20,7 @@ import IssueChild from 'views/Point/IssueChild';
2020
2121import PartySetPoolSize from 'views/Party/PartySetPoolSize';
2222import InviteCohort from 'views/Invite/Cohort';
23−import AcceptTransfer from 'views/AcceptTransfer';
23+import AcceptTransfer from 'views/AcceptTransfer.tsx';
2424import CancelTransfer from 'views/CancelTransfer';
2525import Bitcoin from 'views/Bitcoin/Bitcoin';
2626import Xpub from 'views/Bitcoin/Xpub';
src/lib/types/PendingL1Transaction.tsnew file+22
@@ -0,0 +1,22 @@
1+export enum L1TxnType {
2+ migrate = 'migrate',
3+ migrateSpawn = 'migrateSpawn',
4+ spawnProxy = 'spawnProxy',
5+ managementProxy = 'managementProxy',
6+ transferProxy = 'transferProxy',
7+ votingProxy = 'votingProxy',
8+ acceptTransfer = 'acceptTransfer',
9+ spawnPoint = 'spawnPoint',
10+ setNetworkKeys = 'setNetworkKeys',
11+}
12+
13+export interface PendingL1Txn {
14+ id: string;
15+ point: number;
16+ type: L1TxnType;
17+ hash: string;
18+}
19+
20+export interface PendingL1 {
21+ [key: number]: PendingL1Txn[];
22+}
src/lib/types/Point.ts+25
@@ -9,6 +9,17 @@ export interface Points {
99 [key: number]: Point;
1010}
1111
12+export type PermissionLevel =
13+ | 'own'
14+ | 'manage'
15+ | 'spawn'
16+ | 'vote'
17+ | 'transfer'
18+ | null;
19+
20+export const shouldDisplayPermissionLevel = (level: PermissionLevel) =>
21+ level === 'manage' || level === 'spawn' || level === 'vote';
22+
1223export enum PointField {
1324 value = 'value',
1425 layer = 'layer',
@@ -113,6 +124,8 @@ export default class Point {
113124 ownManageSpawn: boolean;
114125 shouldDisplay: boolean;
115126 isOutgoing: boolean;
127+ permissionLevel: PermissionLevel;
128+ displayPermission: boolean;
116129
117130 constructor({
118131 value,
@@ -157,6 +170,18 @@ export default class Point {
157170 this.isSpawnProxy = eqAddr(address, this.spawnProxy);
158171 this.isVotingProxy = eqAddr(address, this.votingProxy);
159172 this.isTransferProxy = eqAddr(address, this.transferProxy);
173+ this.permissionLevel = this.isOwner
174+ ? 'own'
175+ : this.isManagementProxy
176+ ? 'manage'
177+ : this.isSpawnProxy
178+ ? 'spawn'
179+ : this.isVotingProxy
180+ ? 'vote'
181+ : this.isTransferProxy
182+ ? 'transfer'
183+ : null;
184+ this.displayPermission = shouldDisplayPermissionLevel(this.permissionLevel);
160185 this.isManagementProxySet = !isZeroAddress(this.managementProxy);
161186 this.isSpawnProxySet = !isZeroAddress(this.spawnProxy);
162187 this.isVotingProxySet = !isZeroAddress(this.votingProxy);
src/lib/useRoller.ts+16−7
@@ -50,9 +50,18 @@ import { L1Point } from './types/L1Point';
5050import { ddmmmYYYY } from './utils/date';
5151import { showNotification } from './utils/notifications';
5252import { useWalletConnect } from './useWalletConnect';
53+import { PendingL1Txn } from './types/PendingL1Transaction';
5354
5455const ONE_SECOND = 1000;
5556
57+interface UpdateParams {
58+ point: number;
59+ message?: string;
60+ notify?: boolean;
61+ field?: PointField;
62+ l1Txn?: PendingL1Txn;
63+}
64+
5665const inviteTemplate = (
5766 planet: number,
5867 ticket = '',
@@ -98,6 +107,8 @@ export default function useRoller() {
98107 setPoints,
99108 updateInvite,
100109 updatePoint,
110+ storePendingL1Txn,
111+ deletePendingL1Txn,
101112 } = useRollerStore();
102113 const [config, setConfig] = useState<Config | null>(null);
103114
@@ -228,12 +239,9 @@ export default function useRoller() {
228239 );
229240
230241 const checkForUpdates = useCallback(
231− async (
232− point: number,
233− message?: string,
234− notify = true,
235− field?: PointField
236− ) => {
242+ async ({ point, message, notify = true, field, l1Txn }: UpdateParams) => {
243+ if (l1Txn) storePendingL1Txn(l1Txn);
244+
237245 const interval = setInterval(async () => {
238246 const updatedPoint = await initPoint(point);
239247 const changedField = !points[point]
@@ -247,6 +255,7 @@ export default function useRoller() {
247255 if (!updatedPoint.isPlaceholder && changedField) {
248256 updatePoint(updatedPoint);
249257 clearInterval(interval);
258+ if (l1Txn) deletePendingL1Txn(l1Txn);
250259 if (notify) {
251260 showNotification(
252261 `${message || getUpdatedPointMessage(updatedPoint, changedField)}`
@@ -255,7 +264,7 @@ export default function useRoller() {
255264 }
256265 }, TEN_SECONDS);
257266 },
258− [points, initPoint, updatePoint]
267+ [points, initPoint, updatePoint, storePendingL1Txn, deletePendingL1Txn]
259268 );
260269
261270 const spawnPoint = useCallback(
src/store/rollerStore.ts+18
@@ -5,6 +5,7 @@ import { HOUR } from 'lib/utils/roller';
55import { Invite, Invites } from 'lib/types/Invite';
66import Point, { Points } from 'lib/types/Point';
77import { toL1Details } from 'lib/utils/point';
8+import { PendingL1, PendingL1Txn } from 'lib/types/PendingL1Transaction';
89
910const getPointsAndList = (points: Points, point: Point) => ({
1011 points,
@@ -26,6 +27,7 @@ export interface RollerStore {
2627 point: Point;
2728 points: Points;
2829 pointList: Point[];
30+ pendingL1ByPoint: PendingL1;
2931 invites: Invites;
3032 modalText?: string;
3133 recentlyCompleted: number;
@@ -44,6 +46,8 @@ export interface RollerStore {
4446 setPoints: (points: Points) => void;
4547 updateInvite: (point: number, invite: Invite) => void;
4648 updatePoint: (point: Point) => void;
49+ storePendingL1Txn: (txn: PendingL1Txn) => void;
50+ deletePendingL1Txn: (txn: PendingL1Txn) => void;
4751}
4852
4953export const useRollerStore = create<RollerStore>(set => ({
@@ -54,6 +58,7 @@ export const useRollerStore = create<RollerStore>(set => ({
5458 point: EMPTY_POINT,
5559 pointList: [],
5660 points: {},
61+ pendingL1ByPoint: {},
5762 invites: {},
5863 recentlyCompleted: 0,
5964 inviteGeneratingNum: 0,
@@ -110,4 +115,17 @@ export const useRollerStore = create<RollerStore>(set => ({
110115 return getPointsAndList(newPoints, point);
111116 }),
112117 setModalText: (modalText?: string) => set(() => ({ modalText })),
118+ storePendingL1Txn: (txn: PendingL1Txn) =>
119+ set(({ pendingL1ByPoint }) => {
120+ const newPending = Object.assign({}, pendingL1ByPoint);
121+ newPending[txn.point] = [...(pendingL1ByPoint[txn.point] || []), txn];
122+ return { pendingL1ByPoint: newPending };
123+ }),
124+ deletePendingL1Txn: (txn: PendingL1Txn) =>
125+ set(({ pendingL1ByPoint }) => {
126+ const newPending = Object.assign({}, pendingL1ByPoint);
127+ newPending[txn.point] =
128+ pendingL1ByPoint[txn.point]?.filter(({ id }) => id !== txn.id) || [];
129+ return { pendingL1ByPoint: newPending };
130+ }),
113131}));
src/views/AcceptTransfer.js → src/views/AcceptTransfer.tsxrenamed+16−15
@@ -29,10 +29,10 @@ import { useRollerStore } from 'store/rollerStore';
2929import useRoller from 'lib/useRoller';
3030
3131function useAcceptTransfer() {
32− const { contracts } = useNetwork();
33− const { pointCursor } = usePointCursor();
34− const { syncExtras, syncControlledPoints } = usePointCache();
35− const { wallet } = useWallet();
32+ const { contracts }: any = useNetwork();
33+ const { pointCursor }: any = usePointCursor();
34+ const { syncExtras, syncControlledPoints }: any = usePointCache();
35+ const { wallet }: any = useWallet();
3636
3737 const _contracts = need.contracts(contracts);
3838 const _point = need.point(pointCursor);
@@ -57,12 +57,12 @@ function useAcceptTransfer() {
5757}
5858
5959export default function AcceptTransfer() {
60− const { pop } = useLocalRouter();
61− const { setPointCursor } = usePointCursor();
60+ const { pop }: any = useLocalRouter();
61+ const { setPointCursor }: any = usePointCursor();
6262 const { point } = useRollerStore();
6363 const { transferPoint, checkForUpdates } = useRoller();
6464
65− const { wallet } = useWallet();
65+ const { wallet }: any = useWallet();
6666 const _address = need.addressFromWallet(wallet);
6767
6868 const name = useCurrentPointName();
@@ -73,10 +73,10 @@ export default function AcceptTransfer() {
7373
7474 useEffect(() => {
7575 if (completed) {
76− checkForUpdates(
77− point.value,
78− `${point.patp} has been transferred to you!`
79− );
76+ checkForUpdates({
77+ point: point.value,
78+ message: `${point.patp} has been transferred to you!`,
79+ });
8080 }
8181 }, [completed]); // eslint-disable-line react-hooks/exhaustive-deps
8282
@@ -101,10 +101,11 @@ export default function AcceptTransfer() {
101101
102102 const acceptTransfer = useCallback(async () => {
103103 await transferPoint(_address, reset);
104− await checkForUpdates(
105− point.value,
106− `${point.patp} has been transferred to you!`
107− );
104+ await checkForUpdates({
105+ point: point.value,
106+ message: `${point.patp} has been transferred to you!`,
107+ });
108+
108109 goBack();
109110 }, [goBack, transferPoint, checkForUpdates, point, _address, reset]);
110111
src/views/CancelTransfer.js+8−2
@@ -67,7 +67,10 @@ function AdminCancelTransfer() {
6767
6868 useEffect(() => {
6969 if (completed) {
70− checkForUpdates(point.value, `${point.patp} transfer cancelled`);
70+ checkForUpdates({
71+ point: point.value,
72+ message: `${point.patp} transfer cancelled`,
73+ });
7174 }
7275 }, [completed]); // eslint-disable-line react-hooks/exhaustive-deps
7376
@@ -78,7 +81,10 @@ function AdminCancelTransfer() {
7881
7982 const cancelTransfer = useCallback(async () => {
8083 await setProxyAddress('transfer', ETH_ZERO_ADDR);
81− await checkForUpdates(point.value, `${point.patp} transfer cancelled`);
84+ await checkForUpdates({
85+ point: point.value,
86+ message: `${point.patp} transfer cancelled`,
87+ });
8288 goBack();
8389 }, [setProxyAddress, goBack, checkForUpdates, point]);
8490
src/views/Point/IssueChild.tsx+12−2
@@ -24,6 +24,7 @@ import { validateChild } from 'lib/validators';
2424import { convertToInt } from 'lib/convertToInt';
2525import { isGalaxy } from 'lib/utils/point';
2626import useRoller from 'lib/useRoller';
27+import { L1TxnType } from 'lib/types/PendingL1Transaction';
2728
2829import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
2930import View from 'components/View';
@@ -118,7 +119,7 @@ export default function IssueChild() {
118119 if (pointToSpawn && destinationAddress) {
119120 setLoading(true);
120121 await spawnPoint(pointToSpawn, destinationAddress);
121− checkForUpdates(pointToSpawn);
122+ checkForUpdates({ point: pointToSpawn });
122123 setPointToSpawn(null);
123124 setDestinationAddress(null);
124125 setL2SpawnSent(true);
@@ -133,11 +134,20 @@ export default function IssueChild() {
133134 completed,
134135 inputsLocked,
135136 bind,
137+ txHashes,
136138 } = useIssueChild();
137139
138140 useEffect(() => {
139141 if (completed && pointToSpawn) {
140− checkForUpdates(pointToSpawn);
142+ checkForUpdates({
143+ point: pointToSpawn,
144+ l1Txn: {
145+ id: `${pointToSpawn}`,
146+ point: point.value,
147+ type: L1TxnType.spawnPoint,
148+ hash: txHashes[0],
149+ },
150+ });
141151 }
142152 }, [completed]); // eslint-disable-line react-hooks/exhaustive-deps
143153
src/views/Point/MigrateL2.tsx+32−17
@@ -3,14 +3,31 @@ import { Grid } from 'indigo-react';
33import { ecliptic } from 'azimuth-js';
44import * as ob from 'urbit-ob';
55
6+import {
7+ getHideMigrationMessage,
8+ storeHideMigrationMessage,
9+} from 'store/storage/roller';
10+import { useNetwork } from 'store/network';
11+import { usePointCache } from 'store/pointCache';
12+import { useRollerStore } from 'store/rollerStore';
13+
614import { useLocalRouter } from 'lib/LocalRouter';
715import * as need from 'lib/need';
16+import Point, { PointField } from 'lib/types/Point';
17+import useRoller from 'lib/useRoller';
18+import { L1TxnType } from 'lib/types/PendingL1Transaction';
19+import useEthereumTransaction from 'lib/useEthereumTransaction';
20+import { DUMMY_L2_ADDRESS, GAS_LIMITS } from 'lib/constants';
21+import { isPlanet } from 'lib/utils/point';
822
923import View from 'components/View';
1024import L2BackHeader from 'components/L2/Headers/L2BackHeader';
1125import BodyPane from 'components/L2/Window/BodyPane';
1226import HeaderPane from 'components/L2/Window/HeaderPane';
1327import Window from 'components/L2/Window/Window';
28+import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
29+import Dropdown from 'components/L2/Dropdowns/Dropdown';
30+import Sigil from 'components/Sigil';
1431import { ReactComponent as StarIcon } from 'assets/star.svg';
1532
1633import {
@@ -21,23 +38,8 @@ import {
2138 RadioButton,
2239 Row,
2340} from '@tlon/indigo-react';
24−import {
25− getHideMigrationMessage,
26− storeHideMigrationMessage,
27−} from 'store/storage/roller';
28−import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
29−import { useNetwork } from 'store/network';
30−import { usePointCache } from 'store/pointCache';
31−import useEthereumTransaction from 'lib/useEthereumTransaction';
32−import { DUMMY_L2_ADDRESS, GAS_LIMITS } from 'lib/constants';
33−import Dropdown from 'components/L2/Dropdowns/Dropdown';
34−import Sigil from 'components/Sigil';
35−import { isPlanet } from 'lib/utils/point';
3641
3742import './MigrateL2.scss';
38−import { useRollerStore } from 'store/rollerStore';
39−import Point, { PointField } from 'lib/types/Point';
40−import useRoller from 'lib/useRoller';
4143
4244const PointEntry = ({
4345 point,
@@ -113,7 +115,7 @@ export default function MigrateL2() {
113115 );
114116 const hideInfo = getHideMigrationMessage();
115117
116− const { construct, unconstruct, bind, completed } = useMigrate();
118+ const { construct, unconstruct, bind, completed, txHashes } = useMigrate();
117119
118120 useEffect(() => {
119121 if (point.isGalaxy && point.isL1) {
@@ -131,7 +133,20 @@ export default function MigrateL2() {
131133 const message = transfer
132134 ? `${selectedPoint.patp} has been migrated to Layer 2!`
133135 : `${selectedPoint.patp}'s spawn proxy has been set to Layer 2!`;
134− checkForUpdates(selectedPoint.value, message, true, PointField.layer);
136+
137+ checkForUpdates({
138+ point: selectedPoint.value,
139+ message,
140+ notify: true,
141+ field: PointField.layer,
142+ l1Txn: {
143+ id: `${DUMMY_L2_ADDRESS}-${selectedPoint.value}`,
144+ point: selectedPoint.value,
145+ type: transfer ? L1TxnType.migrate : L1TxnType.migrateSpawn,
146+ hash: txHashes[0],
147+ },
148+ });
149+
135150 pop();
136151 }
137152 }, [completed, pop]); // eslint-disable-line react-hooks/exhaustive-deps
src/views/Point/Point.scss−47
@@ -3,53 +3,6 @@
33.point {
44}
55
6−.transaction {
7− font-size: 14px;
8− padding: 8px;
9− border: 1px solid $light-gray;
10− border-radius: $large-border-radius;
11− margin-bottom: 16px;
12−
13− .title-row {
14− display: flex;
15− flex-direction: row;
16− justify-content: space-between;
17− align-items: center;
18−
19− .title {
20− font-weight: 600;
21− }
22−
23− .rollup-timer {
24− background-color: $rollup-yellow;
25− padding: 4px 8px;
26− width: 112px;
27− border-radius: $medium-border-radius;
28− display: flex;
29− flex-direction: row;
30− justify-content: center;
31− align-items: center;
32−
33− svg {
34− height: 14px;
35− width: 14px;
36− margin-right: 4px;
37− }
38− }
39− }
40−
41− .info-row {
42− display: flex;
43− flex-direction: row;
44− justify-content: space-between;
45− align-items: center;
46−
47− .date {
48−
49− }
50− }
51−}
52−
536.network-keys-modal {
547 background-color: white;
558 border-radius: $medium-border-radius;
src/views/Point/Point.tsx+26−38
@@ -23,17 +23,23 @@ import useRoller from 'lib/useRoller';
2323import useSeenMissingKeys from 'lib/useSeenMissingKeys';
2424import { useHasNetworkKeysSet } from 'lib/useHasNetworkKeysSet';
2525
26−import { InviteForm } from './InviteForm';
26+import LoadingOverlay from 'components/L2/LoadingOverlay';
27+import PendingTransaction, { getPendingL1Title, getPendingTitle } from 'components/L2/PendingTransaction';
2728import Modal from 'components/L2/Modal';
29+import { InviteForm } from './InviteForm';
2830import './Point.scss';
29−import LoadingOverlay from 'components/L2/LoadingOverlay';
3031
3132export default function Point() {
3233 const { pop, push, names }: any = useLocalRouter();
3334 const { wallet }: any = useWallet();
3435 const { syncExtras }: any = usePointCache();
3536 const { getInvites, getPendingTransactions } = useRoller();
36− const { pendingTransactions, point, invites } = useRollerStore();
37+ const {
38+ pendingTransactions,
39+ point,
40+ invites,
41+ pendingL1ByPoint,
42+ } = useRollerStore();
3743 const { nextRoll } = useTimerStore();
3844 const networkKeysSet = useHasNetworkKeysSet();
3945 const [seenMissingKeys, setSeenMissingKeys] = useSeenMissingKeys();
@@ -43,6 +49,8 @@ export default function Point() {
4349
4450 const pointRef = useRef<number | null>(null);
4551
52+ const pendingL1Transactions = pendingL1ByPoint[point.value] || [];
53+
4654 const hideModal = useCallback(() => {
4755 if (hideMessage) {
4856 setSeenMissingKeys(true);
@@ -155,41 +163,21 @@ export default function Point() {
155163 )}
156164 {!!otherPending.length &&
157165 otherPending.map((pendingTx, ind) => (
158− <div className="transaction" key={`pending-${ind}`}>
159− <Row className="title-row">
160− <div className="title">
161− {pendingTx.rawTx?.tx?.type === 'set-management-proxy'
162− ? 'Management Address Changed'
163− : pendingTx.rawTx?.tx?.type === 'configure-keys'
164− ? 'Network Keys Configured'
165− : pendingTx.rawTx?.tx?.type === 'transfer-point'
166− ? 'Point Transfered'
167− : pendingTx.rawTx?.tx?.type === 'set-transfer-proxy'
168− ? 'Transfer Proxy Changed'
169− : pendingTx.rawTx?.tx?.type === 'set-spawn-proxy'
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'
179− : pendingTx.rawTx?.tx?.type === 'detach'
180− ? 'Detach Sponsee'
181− : ''}
182− </div>
183− <div className="rollup-timer">
184− <Icon icon="Clock" />
185− {nextRoll}
186− </div>
187− </Row>
188− <Row className="info-row">
189− <LayerIndicator layer={2} size="sm" />
190− <div className="date"></div>
191− </Row>
192− </div>
166+ <PendingTransaction
167+ key={`pending-tx-${ind}`}
168+ layer={2}
169+ nextRoll={nextRoll}
170+ title={getPendingTitle(pendingTx.rawTx?.tx?.type)}
171+ />
172+ ))}
173+ {!!pendingL1Transactions.length &&
174+ pendingL1Transactions.map((pendingTx, ind) => (
175+ <PendingTransaction
176+ key={`pending-tx-${ind}`}
177+ layer={1}
178+ hash={pendingTx.hash}
179+ title={getPendingL1Title(pendingTx.type)}
180+ />
193181 ))}
194182 <Passport
195183 point={Just(point.value)}
src/views/Points.tsx+25−62
@@ -12,15 +12,15 @@ import { useNetwork } from 'store/network';
1212import { useRollerStore } from 'store/rollerStore';
1313
1414import * as need from 'lib/need';
15−import { isZeroAddress, abbreviateAddress } from 'lib/utils/address';
15+import { abbreviateAddress } from 'lib/utils/address';
1616import useIsEclipticOwner from 'lib/useIsEclipticOwner';
1717import { useSyncDetails } from 'lib/useSyncPoints';
1818import useRejectedIncomingPointTransfers from 'lib/useRejectedIncomingPointTransfers';
1919import { pluralize } from 'lib/pluralize';
20+import Point from 'lib/types/Point';
2021import newGithubIssueUrl from 'new-github-issue-url';
2122
2223import View from 'components/View';
23−import Passport from 'components/Passport';
2424import Footer from 'components/Footer';
2525import { ForwardButton } from 'components/Buttons';
2626import CopiableAddress from 'components/copiable/CopiableAddress';
@@ -28,6 +28,7 @@ import NavHeader from 'components/NavHeader';
2828import L2PointHeader from 'components/L2/Headers/L2PointHeader';
2929import IncomingPoint from 'components/L2/Points/IncomingPoint';
3030import LoadingOverlay from 'components/L2/LoadingOverlay';
31+import PointList from 'components/L2/PointList';
3132import { Box, Text } from '@tlon/indigo-react';
3233
3334import './Points.scss';
@@ -42,56 +43,18 @@ export const maybeGetResult = (obj, key, defaultValue) =>
4243 }),
4344 });
4445
45−export const hasTransferProxy = details =>
46− !isZeroAddress(details.transferProxy);
47−
48−export const isLocked = (details, contracts) =>
46+export const isLocked = (details: any, contracts: any) =>
4947 details.owner === contracts.linearSR ||
5048 details.owner === contracts.conditionalSR;
5149
52−const PointList = function({
53− points,
54− className,
55− actions,
56− locked = false,
57− processing = false,
58− ...rest
59−}) {
60− const { setPointCursor } = usePointCursor();
61− const { push, names } = useHistory();
62−
63− return (
64− <Grid gap={4} className={className}>
65− {points.map((point, i) => (
66− <Grid.Item
67− key={point}
68− className={`full fourth-${(i % 4) + 1}-md fourth-${(i % 4) + 1}-lg`}>
69− <Flex col>
70− <Passport.Mini
71− locked={locked}
72− processing={processing}
73− point={point}
74− onClick={
75− locked || processing
76− ? undefined
77− : () => {
78− setPointCursor(Just(point));
79− push(names.POINT);
80− }
81− }
82− {...rest}
83− />
84− {actions && (
85− <Flex.Item className="mt2">{actions(point, i)}</Flex.Item>
86− )}
87− </Flex>
88− </Grid.Item>
89− ))}
90− </Grid>
91− );
92−};
50+interface ActionButtonProps {
51+ actions?: {
52+ text?: string;
53+ onClick?: () => void;
54+ }[];
55+}
9356
94−function ActionButtons({ actions = [] }) {
57+function ActionButtons({ actions = [] }: ActionButtonProps) {
9558 return (
9659 <Flex row>
9760 {actions.map(action => (
@@ -164,9 +127,7 @@ export default function Points() {
164127 [getDetails, controlledPoints, _contracts]
165128 );
166129
167− const allPoints = pointList
168− .filter(({ shouldDisplay }) => shouldDisplay)
169− .map(({ value }) => value);
130+ const allPoints = pointList.filter(({ shouldDisplay }) => shouldDisplay);
170131
171132 // if we can only interact with a single point, jump to the point page.
172133 // if there are any pending transfers, incoming or outgoing, stay on this
@@ -182,7 +143,7 @@ export default function Points() {
182143 (starReleaseDetails.value === null ||
183144 starReleaseDetails.value.total === 0)
184145 ) {
185− setPointCursor(Just(allPoints[0]));
146+ setPointCursor(Just(allPoints[0].value));
186147 push(names.POINT);
187148 }
188149 }, [
@@ -201,11 +162,13 @@ export default function Points() {
201162 const loading = Nothing.hasInstance(controlledPoints);
202163
203164 const ownedPoints = maybeGetResult(controlledPoints, 'ownedPoints', []);
204− const lockedPoints = maybeLockedPoints.getOrElse([]);
165+ const lockedPoints = maybeLockedPoints
166+ .getOrElse([])
167+ .map((value: number) => ({ value }));
205168
206− const processingPoints = pointList
207− .filter(({ shouldDisplay }) => !shouldDisplay)
208− .map(({ value }) => value);
169+ const processingPoints = pointList.filter(
170+ ({ shouldDisplay }) => !shouldDisplay
171+ );
209172
210173 const displayEmptyState =
211174 !loading && incomingPoints.length === 0 && allPoints.length === 0;
@@ -331,14 +294,14 @@ export default function Points() {
331294 <Grid.Item
332295 full
333296 as={PointList}
334− points={outgoingPoints.map(({ value }) => value)}
335− actions={(point, i) => (
297+ points={outgoingPoints}
298+ actions={(point: Point, i: number) => (
336299 <ActionButtons
337300 actions={[
338301 {
339302 text: 'Cancel',
340303 onClick: () => {
341− setPointCursor(Just(point));
304+ setPointCursor(Just(point.value));
342305 // TODO: deep linking to fix this duplicate route
343306 push(names.CANCEL_TRANSFER);
344307 },
@@ -358,11 +321,11 @@ export default function Points() {
358321
359322 {lockedPoints.length > 0 && (
360323 <Grid.Item full as={Grid} gap={1}>
361− <Grid.Item full as={PointList} locked points={allPoints} />
324+ <Grid.Item full as={PointList} locked points={lockedPoints} />
362325 </Grid.Item>
363326 )}
364327
365− {processingPoints.length > 0 && (
328+ {/* {processingPoints.length > 0 && (
366329 <Grid.Item full as={Grid} gap={1} className="mv6">
367330 <Grid.Item full as={H5}>
368331 Awaiting L2 Rollup
@@ -374,7 +337,7 @@ export default function Points() {
374337 processing
375338 />
376339 </Grid.Item>
377− )}
340+ )} */}
378341
379342 <Footer>
380343 <Grid>
src/views/UrbitID/SetProxy.js+20−4
@@ -14,6 +14,7 @@ import {
1414 PROXY_TYPE,
1515 proxyTypeToHuman,
1616 proxyTypeToHumanDescription,
17+ proxyTypeToL1TxnType,
1718} from 'lib/proxy';
1819import * as need from 'lib/need';
1920import { useLocalRouter } from 'lib/LocalRouter';
@@ -152,6 +153,7 @@ export default function SetProxy() {
152153 inputsLocked,
153154 completed,
154155 bind,
156+ txHashes,
155157 } = useSetProxy(data.proxyType);
156158
157159 const setProxy = useCallback(async () => {
@@ -159,6 +161,12 @@ export default function SetProxy() {
159161 if (newAddress === '') return;
160162 try {
161163 await setProxyAddress(getL2ProxyName(data.proxyType), newAddress);
164+
165+ checkForUpdates({
166+ point: point.value,
167+ message: `${point.patp}'s ${humanProxyType} proxy has been set!`,
168+ });
169+
162170 getPendingTransactions(point.value);
163171 pop();
164172 } catch (error) {
@@ -174,14 +182,22 @@ export default function SetProxy() {
174182 data.proxyType,
175183 setProxyAddress,
176184 setLoading,
185+ checkForUpdates,
186+ humanProxyType,
177187 ]);
178188
179189 useEffect(() => {
180190 if (completed) {
181− checkForUpdates(
182− point.value,
183− `${point.patp}'s ${humanProxyType} proxy has been set!`
184− );
191+ checkForUpdates({
192+ point: point.value,
193+ message: `${point.patp}'s ${humanProxyType} proxy has been set!`,
194+ l1Txn: {
195+ id: `${newAddress}-${point.value}`,
196+ point: point.value,
197+ type: proxyTypeToL1TxnType(data.proxyType),
198+ hash: txHashes[0],
199+ },
200+ });
185201 }
186202 }, [completed]); // eslint-disable-line react-hooks/exhaustive-deps
187203
src/views/UrbitOS/ChangeSponsor.js+6−4
@@ -62,10 +62,12 @@ function ChangeSponsor({ onDone }) {
6262 if (newSponsor !== undefined) {
6363 setLoading(true);
6464 await changeSponsor(newSponsor);
65− checkForUpdates(
66− point.value,
67− `${point.patp} has requested ${ob.patp(newSponsor)} as a sponsor`
68− );
65+ checkForUpdates({
66+ point: point.value,
67+ message: `${point.patp} has requested ${ob.patp(
68+ newSponsor
69+ )} as a sponsor`,
70+ });
6971 setLoading(false);
7072 pop();
7173 }
src/views/UrbitOS/NetworkKeys.tsx+16−8
@@ -49,6 +49,7 @@ import HeaderPane from 'components/L2/Window/HeaderPane';
4949import BodyPane from 'components/L2/Window/BodyPane';
5050
5151import './NetworkKeys.scss';
52+import { L1TxnType } from 'lib/types/PendingL1Transaction';
5253
5354function useSetKeys(
5455 manualNetworkSeed: string,
@@ -175,14 +176,21 @@ export default function UrbitOSNetworkKeys({
175176 inputsLocked,
176177 bind,
177178 keyfileBind,
179+ txHashes,
178180 } = useSetKeys(manualNetworkSeed, setManualNetworkSeed);
179181
180182 useEffect(() => {
181183 if (completed) {
182− checkForUpdates(
183− point.value,
184− `${point.patp}'s Network Keys have been set!`
185− );
184+ checkForUpdates({
185+ point: point.value,
186+ message: `${point.patp}'s Network Keys have been set!`,
187+ l1Txn: {
188+ id: `${point.keyRevisionNumber}-network-keys-${point.value}`,
189+ point: point.value,
190+ type: L1TxnType.setNetworkKeys,
191+ hash: txHashes[0],
192+ },
193+ });
186194 }
187195 }, [completed]); // eslint-disable-line react-hooks/exhaustive-deps
188196
@@ -247,10 +255,10 @@ export default function UrbitOSNetworkKeys({
247255 // set since they are in pending in the Roller...
248256 // we could inspect if there's a changeKeys in the local list of pending txs,...
249257 //
250− checkForUpdates(
251− point.value,
252− `${point.patp}'s Network Keys have been set!`
253− );
258+ checkForUpdates({
259+ point: point.value,
260+ message: `${point.patp}'s Network Keys have been set!`,
261+ });
254262 pop();
255263 } catch (error) {
256264 // setError(error);