Skip to content

Commit 303b2e4

Browse files
Manuel Gellfartbh2smith
andauthored
add: pending tx status and donation button (#437)
* add: donate button to add tx to csv * restructured generate transfer menu * Add two sections to help modal * show transaction status display after executing / proposing a tx Co-authored-by: Ben Smith <bh2smith@gmail.com>
1 parent 5e10c01 commit 303b2e4

15 files changed

+515
-143
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@ Its as simple as uploading a CSV "transfer" file and submitting a single transac
1212

1313
In this article, we demonstrate how to use the Gnosis Safe - CSV Airdrop
1414

15+
## ❤️ Donate ❤️
16+
17+
If you find this app useful for your onchain activities or would like to contribute, donations are welcomed at
18+
19+
```
20+
0xD011a7e124181336ED417B737A495745F150d248
21+
```
22+
23+
This can also be done directly in the the app on your next airdrop via the "Donate" button!
24+
1525
## Loading the App in Gnosis Safe Interface
1626

1727
The current version is deployed on IPFS at

src/App.tsx

Lines changed: 18 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { useSafeAppsSDK } from "@gnosis.pm/safe-apps-react-sdk";
2-
import { BaseTransaction } from "@gnosis.pm/safe-apps-sdk";
3-
import { Breadcrumb, BreadcrumbElement, Button, Card, Divider, Loader, Text } from "@gnosis.pm/safe-react-components";
2+
import { BaseTransaction, GatewayTransactionDetails } from "@gnosis.pm/safe-apps-sdk";
3+
import { Breadcrumb, BreadcrumbElement, Button, Card, Divider, Loader } from "@gnosis.pm/safe-react-components";
44
import { setUseWhatChange } from "@simbathesailor/use-what-changed";
55
import React, { useCallback, useState, useContext } from "react";
66
import styled from "styled-components";
77

88
import { CSVForm } from "./components/CSVForm";
99
import { Header } from "./components/Header";
10+
import { Loading } from "./components/Loading";
1011
import { Summary } from "./components/Summary";
12+
import { TransactionStatusScreen } from "./components/TransactionStatusScreen";
1113
import { MessageContext } from "./contexts/MessageContextProvider";
1214
import { useBalances } from "./hooks/balances";
1315
import { useTokenList } from "./hooks/token";
@@ -21,11 +23,9 @@ const App: React.FC = () => {
2123
const balanceLoader = useBalances();
2224
const [tokenTransfers, setTokenTransfers] = useState<Transfer[]>([]);
2325
const { messages } = useContext(MessageContext);
24-
25-
const [submitting, setSubmitting] = useState(false);
2626
const [parsing, setParsing] = useState(false);
2727
const { sdk } = useSafeAppsSDK();
28-
28+
const [pendingTx, setPendingTx] = useState<GatewayTransactionDetails>();
2929
const assetTransfers = tokenTransfers.filter(
3030
(transfer) => transfer.token_type === "erc20" || transfer.token_type === "native",
3131
) as AssetTransfer[];
@@ -34,7 +34,6 @@ const App: React.FC = () => {
3434
) as CollectibleTransfer[];
3535

3636
const submitTx = useCallback(async () => {
37-
setSubmitting(true);
3837
try {
3938
const txs: BaseTransaction[] = [];
4039
txs.push(...buildAssetTransfers(assetTransfers));
@@ -43,11 +42,10 @@ const App: React.FC = () => {
4342
console.log(`Encoded ${txs.length} transfers.`);
4443
const sendTxResponse = await sdk.txs.send({ txs });
4544
const safeTx = await sdk.txs.getBySafeTxHash(sendTxResponse.safeTxHash);
46-
console.log({ safeTx });
45+
setPendingTx(safeTx);
4746
} catch (e) {
4847
console.error(e);
4948
}
50-
setSubmitting(false);
5149
}, [assetTransfers, collectibleTransfers, sdk.txs]);
5250

5351
return (
@@ -56,42 +54,25 @@ const App: React.FC = () => {
5654
{
5755
<>
5856
{isLoading || balanceLoader.isLoading ? (
59-
<>
60-
<div
61-
style={{
62-
display: "flex",
63-
flexDirection: "column",
64-
alignItems: "center",
65-
width: "100%",
66-
paddingTop: "36px",
67-
}}
68-
>
69-
<Text size={"xl"} strong>
70-
Loading tokenlist and balances...
71-
</Text>
72-
<Loader size={"md"} />
73-
</div>
74-
</>
57+
<Loading />
7558
) : (
7659
<Card className="cardWithCustomShadow">
77-
<Breadcrumb>
78-
<BreadcrumbElement text="CSV Transfer File" iconType="paste" />
79-
</Breadcrumb>
80-
<CSVForm updateTransferTable={setTokenTransfers} setParsing={setParsing} />
81-
<Divider />
60+
{!pendingTx && (
61+
<>
62+
<Breadcrumb>
63+
<BreadcrumbElement text="CSV Transfer File" iconType="paste" />
64+
</Breadcrumb>
65+
<CSVForm updateTransferTable={setTokenTransfers} setParsing={setParsing} />
66+
<Divider />
67+
</>
68+
)}
8269
<Breadcrumb>
8370
<BreadcrumbElement text="Summary" iconType="transactionsInactive" />
8471
<BreadcrumbElement text="Transfers" color="placeHolder" />
8572
</Breadcrumb>
8673
<Summary assetTransfers={assetTransfers} collectibleTransfers={collectibleTransfers} />
87-
{submitting ? (
88-
<>
89-
<Loader size="md" />
90-
<br />
91-
<Button size="lg" color="secondary" onClick={() => setSubmitting(false)}>
92-
Cancel
93-
</Button>
94-
</>
74+
{pendingTx ? (
75+
<TransactionStatusScreen tx={pendingTx} reset={() => setPendingTx(undefined)} />
9576
) : (
9677
<Button
9778
style={{ alignSelf: "flex-start", marginTop: 16, marginBottom: 16 }}

src/components/CSVEditor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const CSVEditor = (props: CSVEditorProps): JSX.Element => {
2121
onChange={(newCode) => props.onChange(newCode)}
2222
value={props.csvText}
2323
theme="tomorrow"
24-
width={"700px"}
24+
width={"100%"}
2525
mode={"text"}
2626
minLines={6}
2727
maxLines={20}

src/components/CSVForm.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Text } from "@gnosis.pm/safe-react-components";
2+
import { Grid } from "@material-ui/core";
23
import debounce from "lodash.debounce";
34
import React, { useContext, useEffect, useMemo, useState } from "react";
45
import styled from "styled-components";
@@ -133,17 +134,24 @@ export const CSVForm = (props: CSVFormProps): JSX.Element => {
133134
a CSV file in a single transaction.
134135
</Text>
135136
<Text size="lg">
136-
Upload, edit or paste your asset transfer CSV <br /> (token_type,token_address,receiver,amount,id)
137+
Upload, edit or paste your asset transfer CSV <br /> (
138+
<span style={{ fontFamily: "monospace" }}>token_type,token_address,receiver,amount,id</span>)
137139
</Text>
138140

139141
<CSVEditor csvText={csvText} onChange={onChangeTextHandler} />
140-
141-
<CSVUpload onChange={onChangeTextHandler} />
142-
<GenerateTransfersMenu
143-
assetBalance={assetBalance}
144-
collectibleBalance={collectibleBalance}
145-
setCsvText={setCsvText}
146-
/>
142+
<Grid container direction="row" spacing={2}>
143+
<Grid item xs={12} md={8}>
144+
<CSVUpload onChange={onChangeTextHandler} />
145+
</Grid>
146+
<Grid item xs={12} md={4}>
147+
<GenerateTransfersMenu
148+
assetBalance={assetBalance}
149+
collectibleBalance={collectibleBalance}
150+
setCsvText={setCsvText}
151+
csvText={csvText}
152+
/>
153+
</Grid>
154+
</Grid>
147155
</Form>
148156
);
149157
};

src/components/CSVUpload.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ const styles = createStyles({
7676
padding: "20px",
7777
borderWidth: 2,
7878
borderRadius: 2,
79-
width: "660px",
8079
borderColor: "rgba(0, 0, 0, 0.23)",
8180
borderStyle: "dashed",
8281
backgroundColor: "#fafafa",

src/components/DonateDialog.tsx

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { useSafeAppsSDK } from "@gnosis.pm/safe-apps-react-sdk";
2+
import { GenericModal, Button, Select, TextFieldInput, Icon } from "@gnosis.pm/safe-react-components";
3+
import { InputAdornment, Typography } from "@material-ui/core";
4+
import { BigNumber, ethers } from "ethers";
5+
import { useEffect, useState } from "react";
6+
import { AssetBalance } from "src/hooks/balances";
7+
import { networkInfo } from "src/networks";
8+
import { DONATION_ADDRESS } from "src/utils";
9+
10+
export const DonateDialog = ({
11+
onSubmit,
12+
isOpen,
13+
onClose,
14+
assetBalance,
15+
csvText,
16+
}: {
17+
onSubmit: (donationRow: string) => void;
18+
isOpen: boolean;
19+
onClose: () => void;
20+
assetBalance: AssetBalance;
21+
csvText: string;
22+
}) => {
23+
const { safe } = useSafeAppsSDK();
24+
const nativeSymbol = networkInfo.get(safe.chainId)?.currencySymbol || "ETH";
25+
26+
const items = assetBalance?.map((asset) => ({
27+
id: asset.tokenAddress || "0x0",
28+
label: asset.token?.name || nativeSymbol,
29+
subLabel: `${ethers.utils.formatUnits(asset.balance, asset.decimals)} ${asset.token?.symbol || nativeSymbol}`,
30+
}));
31+
const [selectedToken, setSelectedToken] = useState<string | undefined>(
32+
items && items.length > 0 ? items[0].id : undefined,
33+
);
34+
const [selectedAmount, setSelectedAmount] = useState<string>("0");
35+
const [amountError, setAmountError] = useState<string>();
36+
37+
useEffect(() => {
38+
try {
39+
if (typeof selectedAmount === "undefined") {
40+
setAmountError(undefined);
41+
return;
42+
}
43+
const selectedBalance = assetBalance?.find(
44+
(asset) => asset.tokenAddress === selectedToken || (selectedToken === "0x0" && asset.tokenAddress === null),
45+
);
46+
if (!selectedBalance) {
47+
setAmountError("Select an asset with balance > 0");
48+
return;
49+
}
50+
51+
if (
52+
BigNumber.from(selectedBalance.balance).lt(
53+
ethers.utils.parseUnits(Number(selectedAmount).toString(), selectedBalance.decimals),
54+
)
55+
) {
56+
setAmountError("Balance of selected asset too low");
57+
return;
58+
} else {
59+
setAmountError(undefined);
60+
}
61+
} catch (error) {
62+
console.error(error);
63+
setAmountError("Amount must be a number");
64+
}
65+
}, [selectedAmount, selectedToken, assetBalance]);
66+
67+
const handleSubmit = () => {
68+
if (selectedToken && selectedAmount) {
69+
const headerRow = csvText.split(/\r\n|\r|\n/)[0];
70+
const donationCSVRow = headerRow
71+
.replace("token_type", "erc20")
72+
.replace("token_address", selectedToken === "0x0" ? "" : selectedToken)
73+
.replace("receiver", DONATION_ADDRESS)
74+
.replace("amount", selectedAmount)
75+
.replace("id", "");
76+
77+
onSubmit(`${csvText}\n${donationCSVRow}`);
78+
onClose();
79+
}
80+
};
81+
82+
if (!isOpen || items.length === 0) {
83+
return null;
84+
}
85+
return (
86+
<GenericModal
87+
onClose={onClose}
88+
title="Donate to project"
89+
body={
90+
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
91+
<Typography>
92+
Select an asset and amount. The resulting transaction will be appended to the end of the current CSV.
93+
</Typography>
94+
<Select
95+
activeItemId={selectedToken || items[0].id}
96+
items={items}
97+
name={"Token"}
98+
label={"The token you want to donate"}
99+
onItemClick={setSelectedToken}
100+
/>
101+
<TextFieldInput
102+
id="amount"
103+
label="Amount"
104+
name="amount"
105+
error={amountError}
106+
disabled={typeof selectedToken === "undefined"}
107+
value={selectedAmount}
108+
InputProps={{
109+
startAdornment: (
110+
<InputAdornment position="start">
111+
<Icon size="md" type="assets" />
112+
</InputAdornment>
113+
),
114+
}}
115+
onChange={(e) => setSelectedAmount(e.target.value)}
116+
/>
117+
</div>
118+
}
119+
footer={
120+
<div style={{ display: "flex", justifyContent: "space-between" }}>
121+
<Button
122+
size="md"
123+
color="primary"
124+
onClick={handleSubmit}
125+
disabled={Boolean(amountError) || !Boolean(selectedToken) || !Boolean(selectedAmount)}
126+
>
127+
Add to CSV
128+
</Button>
129+
<Button size="md" color="secondary" onClick={onClose}>
130+
Abort
131+
</Button>
132+
</div>
133+
}
134+
/>
135+
);
136+
};

0 commit comments

Comments
 (0)