`
70 | - Admin: `http://localhost:8020/`
71 | - IPFS:
72 | - `127.0.0.1:5001` or `/ip4/127.0.0.1/tcp/5001`
73 | - Postgres:
74 | - `postgresql://graph-node:let-me-in@localhost:5432/graph-node`
75 |
76 | Once this is up and running, you can use
77 | [`graph-cli`](https://github.com/graphprotocol/graph-cli) to create and
78 | deploy your subgraph to the running Graph Node.
79 |
--------------------------------------------------------------------------------
/packages/react-app/src/components/Provider.jsx:
--------------------------------------------------------------------------------
1 | import { Badge, Button } from "antd";
2 | import { useBlockNumber, usePoller } from "eth-hooks";
3 | import React, { useState } from "react";
4 | // import { WalletOutlined } from '@ant-design/icons';
5 | import Address from "./Address";
6 |
7 | export default function Provider(props) {
8 | const [showMore, setShowMore] = useState(false);
9 | const [status, setStatus] = useState("processing");
10 | const [network, setNetwork] = useState();
11 | const [signer, setSigner] = useState();
12 | const [address, setAddress] = useState();
13 |
14 | const blockNumber = useBlockNumber(props.provider);
15 |
16 | usePoller(async () => {
17 | if (props.provider && typeof props.provider.getNetwork === "function") {
18 | try {
19 | const newNetwork = await props.provider.getNetwork();
20 | setNetwork(newNetwork);
21 | if (newNetwork.chainId > 0) {
22 | setStatus("success");
23 | } else {
24 | setStatus("warning");
25 | }
26 | } catch (e) {
27 | console.log(e);
28 | setStatus("processing");
29 | }
30 | try {
31 | const newSigner = await props.provider.getSigner();
32 | setSigner(newSigner);
33 | const newAddress = await newSigner.getAddress();
34 | setAddress(newAddress);
35 | // eslint-disable-next-line no-empty
36 | } catch (e) {}
37 | }
38 | }, 1377);
39 |
40 | if (
41 | typeof props.provider === "undefined" ||
42 | typeof props.provider.getNetwork !== "function" ||
43 | !network ||
44 | !network.chainId
45 | ) {
46 | return (
47 | {
51 | setShowMore(!showMore);
52 | }}
53 | >
54 | {props.name}
55 |
56 | );
57 | }
58 |
59 | let showExtra = "";
60 | if (showMore) {
61 | showExtra = (
62 |
63 |
64 | id:
65 | {network ? network.chainId : ""}
66 |
67 |
68 | name:
69 | {network ? network.name : ""}
70 |
71 |
72 | );
73 | }
74 |
75 | let showWallet = "";
76 | if (typeof signer !== "undefined" && address) {
77 | showWallet = (
78 |
79 |
80 |
81 |
82 |
83 | );
84 | }
85 |
86 | return (
87 | {
91 | setShowMore(!showMore);
92 | }}
93 | >
94 | {props.name} {showWallet} #{blockNumber} {showExtra}
95 |
96 | );
97 | }
98 |
--------------------------------------------------------------------------------
/packages/react-app/src/hooks/ContractLoader.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-dynamic-require */
2 | /* eslint-disable global-require */
3 | import { Contract } from "@ethersproject/contracts";
4 | import { useEffect, useState } from "react";
5 |
6 | /*
7 | ~ What it does? ~
8 |
9 | Loads your local contracts and gives options to read values from contracts
10 | or write transactions into them
11 |
12 | ~ How can I use? ~
13 |
14 | const readContracts = useContractLoader(localProvider) // or
15 | const writeContracts = useContractLoader(userProvider)
16 |
17 | ~ Features ~
18 |
19 | - localProvider enables reading values from contracts
20 | - userProvider enables writing transactions into contracts
21 | - Example of keeping track of "purpose" variable by loading contracts into readContracts
22 | and using ContractReader.js hook:
23 | const purpose = useContractReader(readContracts,"YourContract", "purpose")
24 | - Example of using setPurpose function from our contract and writing transactions by Transactor.js helper:
25 | tx( writeContracts.YourContract.setPurpose(newPurpose) )
26 | */
27 |
28 | const loadContract = (contractName, signer) => {
29 | const newContract = new Contract(
30 | require(`../contracts/${contractName}.address.js`),
31 | require(`../contracts/${contractName}.abi.js`),
32 | signer,
33 | );
34 | try {
35 | newContract.bytecode = require(`../contracts/${contractName}.bytecode.js`);
36 | } catch (e) {
37 | console.log(e);
38 | }
39 | return newContract;
40 | };
41 |
42 | export default function useContractLoader(providerOrSigner) {
43 | const [contracts, setContracts] = useState();
44 | useEffect(() => {
45 | async function loadContracts() {
46 | if (typeof providerOrSigner !== "undefined") {
47 | try {
48 | // we need to check to see if this providerOrSigner has a signer or not
49 | let signer;
50 | let accounts;
51 | if (providerOrSigner && typeof providerOrSigner.listAccounts === "function") {
52 | accounts = await providerOrSigner.listAccounts();
53 | }
54 |
55 | if (accounts && accounts.length > 0) {
56 | signer = providerOrSigner.getSigner();
57 | } else {
58 | signer = providerOrSigner;
59 | }
60 |
61 | const contractList = require("../contracts/contracts.js");
62 |
63 | const newContracts = contractList.reduce((accumulator, contractName) => {
64 | accumulator[contractName] = loadContract(contractName, signer);
65 | return accumulator;
66 | }, {});
67 | setContracts(newContracts);
68 | } catch (e) {
69 | console.log("ERROR LOADING CONTRACTS!!", e);
70 | }
71 | }
72 | }
73 | loadContracts();
74 | }, [providerOrSigner]);
75 | return contracts;
76 | }
77 |
--------------------------------------------------------------------------------
/packages/react-app/src/hooks/ContractReader.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import useOnBlock from "./OnBlock";
3 | import usePoller from "./Poller";
4 |
5 | const DEBUG = false;
6 |
7 | /*
8 | ~ What it does? ~
9 |
10 | Enables you to read values from contracts and keep track of them in the local React states
11 |
12 | ~ How can I use? ~
13 |
14 | const purpose = useContractReader(readContracts,"YourContract", "purpose")
15 |
16 | ~ Features ~
17 |
18 | - Provide readContracts by loading contracts (see more on ContractLoader.js)
19 | - Specify the name of the contract, in this case it is "YourContract"
20 | - Specify the name of the variable in the contract, in this case we keep track of "purpose" variable
21 | - Pass an args array if the function requires
22 | - Pass pollTime - if no pollTime is specified, the function will update on every new block
23 | */
24 |
25 | export default function useContractReader(contracts, contractName, functionName, args, pollTime, formatter, onChange) {
26 | let adjustPollTime = 0;
27 | if (pollTime) {
28 | adjustPollTime = pollTime;
29 | } else if (!pollTime && typeof args === "number") {
30 | // it's okay to pass poll time as last argument without args for the call
31 | adjustPollTime = args;
32 | }
33 |
34 | const [value, setValue] = useState();
35 | useEffect(() => {
36 | if (typeof onChange === "function") {
37 | setTimeout(onChange.bind(this, value), 1);
38 | }
39 | }, [value, onChange]);
40 |
41 | const updateValue = async () => {
42 | try {
43 | let newValue;
44 | if (DEBUG) console.log("CALLING ", contractName, functionName, "with args", args);
45 | if (args && args.length > 0) {
46 | newValue = await contracts[contractName][functionName](...args);
47 | if (DEBUG)
48 | console.log("contractName", contractName, "functionName", functionName, "args", args, "RESULT:", newValue);
49 | } else {
50 | newValue = await contracts[contractName][functionName]();
51 | }
52 | if (formatter && typeof formatter === "function") {
53 | newValue = formatter(newValue);
54 | }
55 | // console.log("GOT VALUE",newValue)
56 | if (newValue !== value) {
57 | setValue(newValue);
58 | }
59 | } catch (e) {
60 | console.log(e);
61 | }
62 | };
63 |
64 | // Only pass a provider to watch on a block if we have a contract and no PollTime
65 | useOnBlock(contracts && contracts[contractName] && adjustPollTime === 0 && contracts[contractName].provider, () => {
66 | if (contracts && contracts[contractName] && adjustPollTime === 0) {
67 | updateValue();
68 | }
69 | });
70 |
71 | // Use a poller if a pollTime is provided
72 | usePoller(
73 | async () => {
74 | if (contracts && contracts[contractName] && adjustPollTime > 0) {
75 | if (DEBUG) console.log("polling!", contractName, functionName);
76 | updateValue();
77 | }
78 | },
79 | adjustPollTime,
80 | contracts && contracts[contractName],
81 | );
82 |
83 | return value;
84 | }
85 |
--------------------------------------------------------------------------------
/packages/react-app/scripts/ipfs.js:
--------------------------------------------------------------------------------
1 | const ipfsAPI = require("ipfs-http-client");
2 | const chalk = require("chalk");
3 | const { clearLine } = require("readline");
4 |
5 | const { globSource } = ipfsAPI;
6 |
7 | const infura = { host: "ipfs.infura.io", port: "5001", protocol: "https" };
8 | // run your own ipfs daemon: https://docs.ipfs.io/how-to/command-line-quick-start/#install-ipfs
9 | // const localhost = { host: "localhost", port: "5001", protocol: "http" };
10 |
11 | const ipfs = ipfsAPI(infura);
12 |
13 | const ipfsGateway = "https://ipfs.io/ipfs/";
14 |
15 | const addOptions = {
16 | pin: true,
17 | };
18 |
19 | const pushDirectoryToIPFS = async path => {
20 | try {
21 | const response = await ipfs.add(globSource(path, { recursive: true }), addOptions);
22 | return response;
23 | } catch (e) {
24 | return {};
25 | }
26 | };
27 |
28 | const publishHashToIPNS = async ipfsHash => {
29 | try {
30 | const response = await ipfs.name.publish(`/ipfs/${ipfsHash}`);
31 | return response;
32 | } catch (e) {
33 | return {};
34 | }
35 | };
36 |
37 | const nodeMayAllowPublish = ipfsClient => {
38 | // You must have your own IPFS node in order to publish an IPNS name
39 | // This contains a blacklist of known nodes which do not allow users to publish IPNS names.
40 | const nonPublishingNodes = ["ipfs.infura.io"];
41 | const { host } = ipfsClient.getEndpointConfig();
42 | return !nonPublishingNodes.some(nodeUrl => host.includes(nodeUrl));
43 | };
44 |
45 | const deploy = async () => {
46 | console.log("🛰 Sending to IPFS...");
47 | const { cid } = await pushDirectoryToIPFS("./build");
48 | if (!cid) {
49 | console.log(`📡 App deployment failed`);
50 | return false;
51 | }
52 | console.log(`📡 App deployed to IPFS with hash: ${chalk.cyan(cid.toString())}`);
53 |
54 | console.log();
55 |
56 | let ipnsName = "";
57 | if (nodeMayAllowPublish(ipfs)) {
58 | console.log(`✍️ Publishing /ipfs/${cid.toString()} to IPNS...`);
59 | process.stdout.write(" Publishing to IPNS can take up to roughly two minutes.\r");
60 | ipnsName = (await publishHashToIPNS(cid.toString())).name;
61 | clearLine(process.stdout, 0);
62 | if (!ipnsName) {
63 | console.log(" Publishing IPNS name on node failed.");
64 | }
65 | console.log(`🔖 App published to IPNS with name: ${chalk.cyan(ipnsName)}`);
66 | console.log();
67 | }
68 |
69 | console.log("🚀 Deployment to IPFS complete!");
70 | console.log();
71 |
72 | console.log(`Use the link${ipnsName && "s"} below to access your app:`);
73 | console.log(` IPFS: ${chalk.cyan(`${ipfsGateway}${cid.toString()}`)}`);
74 | if (ipnsName) {
75 | console.log(` IPNS: ${chalk.cyan(`${ipfsGateway}${ipnsName}`)}`);
76 | console.log();
77 | console.log(
78 | "Each new deployment will have a unique IPFS hash while the IPNS name will always point at the most recent deployment.",
79 | );
80 | console.log(
81 | "It is recommended that you share the IPNS link so that people always see the newest version of your app.",
82 | );
83 | }
84 | console.log();
85 | return true;
86 | };
87 |
88 | deploy();
89 |
--------------------------------------------------------------------------------
/packages/react-app/src/components/TokenDetailedDisplay.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | import { Button, message } from "antd";
4 | import { CopyOutlined, DeleteOutlined } from "@ant-design/icons";
5 |
6 | import { copy } from "../helpers/EditorHelper";
7 |
8 | export default function TokenDetailedDisplay({
9 | tokenSettingsHelper,
10 | token,
11 | tokenCoreDisplay,
12 | network,
13 | setItemDetailed,
14 | }) {
15 | const [addressCopied, setAddressCopied] = useState(false);
16 |
17 | const tokenLink = network.blockExplorer + "token/" + token.address;
18 |
19 | return (
20 | <>
21 |
22 | {tokenCoreDisplay && tokenCoreDisplay(token)}
23 |
24 |
25 |
30 | {token.hasOwnProperty("address") ? (
31 |
32 |
window.open(tokenLink, "_blank")}
40 | >
41 | {token.address}
42 |
43 |
46 | copy(token.address, () =>
47 | message.success(Copied Contract Address ),
48 | )
49 | }
50 | />
51 |
52 | ) : (
53 |
Native Token
54 | )}
55 |
56 |
57 | {setItemDetailed && (
58 | <>
59 | {tokenSettingsHelper.isCustomItem(token) && (
60 | <>
61 |
62 | }
66 | disabled={tokenSettingsHelper.isItemsTheSame(token, tokenSettingsHelper.getSelectedItem())}
67 | onClick={() => {
68 | tokenSettingsHelper.removeCustomItem(token);
69 | setItemDetailed();
70 | }}
71 | >
72 | Remove Custom Token
73 |
74 |
75 |
76 |
77 | {tokenSettingsHelper.isItemsTheSame(token, tokenSettingsHelper.getSelectedItem()) && (
78 |
Cannot remove the selected token!
79 | )}
80 |
81 | >
82 | )}
83 | >
84 | )}
85 | >
86 | );
87 | }
88 |
--------------------------------------------------------------------------------
/docker/graph-node/start:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | save_coredumps() {
4 | graph_dir=/var/lib/graph
5 | datestamp=$(date +"%Y-%m-%dT%H:%M:%S")
6 | ls /core.* >& /dev/null && have_cores=yes || have_cores=no
7 | if [ -d "$graph_dir" -a "$have_cores" = yes ]
8 | then
9 | core_dir=$graph_dir/cores
10 | mkdir -p $core_dir
11 | exec >> "$core_dir"/messages 2>&1
12 | echo "${HOSTNAME##*-} Saving core dump on ${HOSTNAME} at ${datestamp}"
13 |
14 | dst="$core_dir/$datestamp-${HOSTNAME}"
15 | mkdir "$dst"
16 | cp /usr/local/bin/graph-node "$dst"
17 | cp /proc/loadavg "$dst"
18 | [ -f /Dockerfile ] && cp /Dockerfile "$dst"
19 | tar czf "$dst/etc.tgz" /etc/
20 | dmesg -e > "$dst/dmesg"
21 | # Capture environment variables, but filter out passwords
22 | env | sort | sed -r -e 's/^(postgres_pass|ELASTICSEARCH_PASSWORD)=.*$/\1=REDACTED/' > "$dst/env"
23 |
24 | for f in /core.*
25 | do
26 | echo "${HOSTNAME##*-} Found core dump $f"
27 | mv "$f" "$dst"
28 | done
29 | echo "${HOSTNAME##*-} Saving done"
30 | fi
31 | }
32 |
33 | wait_for_ipfs() {
34 | # Take the IPFS URL in $1 apart and extract host and port. If no explicit
35 | # host is given, use 443 for https, and 80 otherwise
36 | if [[ "$1" =~ ^((https?)://)?([^:/]+)(:([0-9]+))? ]]
37 | then
38 | proto=${BASH_REMATCH[2]:-http}
39 | host=${BASH_REMATCH[3]}
40 | port=${BASH_REMATCH[5]}
41 | if [ -z "$port" ]
42 | then
43 | [ "$proto" = "https" ] && port=443 || port=80
44 | fi
45 | wait_for "$host:$port" -t 120
46 | else
47 | echo "invalid IPFS URL: $1"
48 | exit 1
49 | fi
50 | }
51 |
52 | start_query_node() {
53 | export DISABLE_BLOCK_INGESTOR=true
54 | graph-node \
55 | --postgres-url "$postgres_url" \
56 | --ethereum-rpc $ethereum \
57 | --ipfs "$ipfs"
58 | }
59 |
60 | start_index_node() {
61 | # Only the index node with the name set in BLOCK_INGESTOR should ingest
62 | # blocks
63 | if [[ ${node_id} != "${BLOCK_INGESTOR}" ]]; then
64 | export DISABLE_BLOCK_INGESTOR=true
65 | fi
66 |
67 | graph-node \
68 | --node-id "${node_id//-/_}" \
69 | --postgres-url "$postgres_url" \
70 | --ethereum-rpc $ethereum \
71 | --ipfs "$ipfs"
72 | }
73 |
74 | start_combined_node() {
75 | graph-node \
76 | --postgres-url "$postgres_url" \
77 | --ethereum-rpc $ethereum \
78 | --ipfs "$ipfs"
79 | }
80 |
81 | postgres_url="postgresql://$postgres_user:$postgres_pass@$postgres_host/$postgres_db"
82 |
83 | wait_for_ipfs "$ipfs"
84 | wait_for "$postgres_host:5432" -t 120
85 | sleep 5
86 |
87 | trap save_coredumps EXIT
88 |
89 | export PGAPPNAME="${node_id-$HOSTNAME}"
90 |
91 | case "${node_role-combined-node}" in
92 | query-node)
93 | start_query_node
94 | ;;
95 | index-node)
96 | start_index_node
97 | ;;
98 | combined-node)
99 | start_combined_node
100 | ;;
101 | *)
102 | echo "Unknown mode for start-node: $1"
103 | echo "usage: start (combined-node|query-node|index-node)"
104 | exit 1
105 | esac
106 |
--------------------------------------------------------------------------------
/packages/react-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@scaffold-eth/react-app",
3 | "version": "1.0.0",
4 | "homepage": ".",
5 | "browserslist": [
6 | ">0.2%",
7 | "not dead",
8 | "not op_mini all"
9 | ],
10 | "dependencies": {
11 | "@ant-design/icons": "^4.2.2",
12 | "@apollo/react-hooks": "^4.0.0",
13 | "@monerium/sdk": "^2.10.0",
14 | "@ramp-network/ramp-instant-sdk": "^2.2.0",
15 | "@testing-library/jest-dom": "^5.11.4",
16 | "@testing-library/react": "^11.1.0",
17 | "@testing-library/user-event": "^12.1.8",
18 | "@uniswap/sdk": "^3.0.3",
19 | "@uniswap/v2-periphery": "^1.1.0-beta.0",
20 | "@walletconnect/utils": "^2.7.2",
21 | "@walletconnect/web3-provider": "^1.6.5",
22 | "@walletconnect/web3wallet": "^1.7.0",
23 | "antd": "^4.7.0",
24 | "apollo-boost": "^0.4.9",
25 | "apollo-client": "^2.6.10",
26 | "apollo-utilities": "^1.3.4",
27 | "axios": "^0.20.0",
28 | "bignumber.js": "^9.1.2",
29 | "bnc-notify": "^1.5.0",
30 | "burner-provider": "^1.0.38",
31 | "dotenv": "^8.2.0",
32 | "eth-hooks": "^1.1.2",
33 | "eth-url-parser": "^1.0.4",
34 | "ethers": "^5.7.2",
35 | "graphiql": "^1.0.5",
36 | "graphql": "^15.3.0",
37 | "iban": "^0.0.14",
38 | "ibantools": "^4.3.4",
39 | "isomorphic-fetch": "^3.0.0",
40 | "moment": "^2.29.4",
41 | "node-watch": "^0.7.1",
42 | "postcss": "^8.2.6",
43 | "qrcode.react": "^1.0.0",
44 | "react": "^16.14.0",
45 | "react-blockies": "^1.4.1",
46 | "react-css-theme-switcher": "^0.2.2",
47 | "react-dom": "^16.14.0",
48 | "react-infinite-scroll-component": "^6.1.0",
49 | "react-qr-reader-es6": "2.2.1-2",
50 | "react-router-dom": "^5.2.0",
51 | "react-scripts": "^3.4.3",
52 | "typescript": "^4.9.4",
53 | "web3modal": "^1.9.11",
54 | "zksync-web3": "^0.14.3"
55 | },
56 | "devDependencies": {
57 | "@testing-library/dom": "^6.12.2",
58 | "@types/react": "^16.9.19",
59 | "autoprefixer": "^10.2.4",
60 | "chalk": "^4.1.0",
61 | "eslint": "^7.5.0",
62 | "eslint-config-airbnb": "^18.2.0",
63 | "eslint-config-prettier": "^6.11.0",
64 | "eslint-plugin-babel": "^5.3.1",
65 | "eslint-plugin-import": "^2.22.1",
66 | "eslint-plugin-jsx-a11y": "^6.4.1",
67 | "eslint-plugin-prettier": "^3.1.4",
68 | "eslint-plugin-react": "^7.22.0",
69 | "eslint-plugin-react-hooks": "^4.2.0",
70 | "gulp": "^4.0.2",
71 | "gulp-csso": "^4.0.1",
72 | "gulp-debug": "^4.0.0",
73 | "gulp-less": "^4.0.1",
74 | "gulp-postcss": "^9.0.0",
75 | "ipfs-http-client": "^45.0.0",
76 | "less-plugin-npm-import": "^2.1.0",
77 | "prettier": "^2.0.5",
78 | "s3-folder-upload": "^2.3.1",
79 | "surge": "^0.21.5"
80 | },
81 | "eslintConfig": {
82 | "extends": "react-app"
83 | },
84 | "scripts": {
85 | "build": "react-scripts --openssl-legacy-provider build",
86 | "eject": "react-scripts eject",
87 | "start": "react-scripts --openssl-legacy-provider start",
88 | "test": "react-scripts test",
89 | "lint": "eslint --config ./.eslintrc.js --ignore-path ./.eslintignore ./src/**/*",
90 | "ipfs": "node ./scripts/ipfs.js",
91 | "surge": "surge ./build",
92 | "s3": "node ./scripts/s3.js",
93 | "ship": "yarn surge",
94 | "theme": "npx gulp less",
95 | "watch": "node ./scripts/watch.js"
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/packages/hardhat/scripts/publish.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const chalk = require("chalk");
3 | const bre = require("hardhat");
4 |
5 | const publishDir = "../react-app/src/contracts";
6 | const graphDir = "../subgraph"
7 |
8 | function publishContract(contractName) {
9 | console.log(
10 | " 💽 Publishing",
11 | chalk.cyan(contractName),
12 | "to",
13 | chalk.gray(publishDir)
14 | );
15 | try {
16 | let contract = fs
17 | .readFileSync(`${bre.config.paths.artifacts}/contracts/${contractName}.sol/${contractName}.json`)
18 | .toString();
19 | const address = fs
20 | .readFileSync(`${bre.config.paths.artifacts}/${contractName}.address`)
21 | .toString();
22 | contract = JSON.parse(contract);
23 | let graphConfigPath = `${graphDir}/config/config.json`
24 | let graphConfig
25 | try {
26 | if (fs.existsSync(graphConfigPath)) {
27 | graphConfig = fs
28 | .readFileSync(graphConfigPath)
29 | .toString();
30 | } else {
31 | graphConfig = '{}'
32 | }
33 | } catch (e) {
34 | console.log(e)
35 | }
36 |
37 | graphConfig = JSON.parse(graphConfig)
38 | graphConfig[contractName + "Address"] = address
39 | fs.writeFileSync(
40 | `${publishDir}/${contractName}.address.js`,
41 | `module.exports = "${address}";`
42 | );
43 | fs.writeFileSync(
44 | `${publishDir}/${contractName}.abi.js`,
45 | `module.exports = ${JSON.stringify(contract.abi, null, 2)};`
46 | );
47 | fs.writeFileSync(
48 | `${publishDir}/${contractName}.bytecode.js`,
49 | `module.exports = "${contract.bytecode}";`
50 | );
51 |
52 | const folderPath = graphConfigPath.replace("/config.json","")
53 | if (!fs.existsSync(folderPath)){
54 | fs.mkdirSync(folderPath);
55 | }
56 | fs.writeFileSync(
57 | graphConfigPath,
58 | JSON.stringify(graphConfig, null, 2)
59 | );
60 | fs.writeFileSync(
61 | `${graphDir}/abis/${contractName}.json`,
62 | JSON.stringify(contract.abi, null, 2)
63 | );
64 |
65 | console.log(" 📠 Published "+chalk.green(contractName)+" to the frontend.")
66 |
67 | return true;
68 | } catch (e) {
69 | if(e.toString().indexOf("no such file or directory")>=0){
70 | console.log(chalk.yellow(" ⚠️ Can't publish "+contractName+" yet (make sure it getting deployed)."))
71 | }else{
72 | console.log(e);
73 | return false;
74 | }
75 | }
76 | }
77 |
78 | async function main() {
79 | if (!fs.existsSync(publishDir)) {
80 | fs.mkdirSync(publishDir);
81 | }
82 | const finalContractList = [];
83 | fs.readdirSync(bre.config.paths.sources).forEach((file) => {
84 | if (file.indexOf(".sol") >= 0) {
85 | const contractName = file.replace(".sol", "");
86 | // Add contract to list if publishing is successful
87 | if (publishContract(contractName)) {
88 | finalContractList.push(contractName);
89 | }
90 | }
91 | });
92 | fs.writeFileSync(
93 | `${publishDir}/contracts.js`,
94 | `module.exports = ${JSON.stringify(finalContractList)};`
95 | );
96 | }
97 | main()
98 | .then(() => process.exit(0))
99 | .catch((error) => {
100 | console.error(error);
101 | process.exit(1);
102 | });
103 |
--------------------------------------------------------------------------------
/packages/react-app/src/components/CustomRPC.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Button, Input, Spin } from "antd";
4 | import { ApiOutlined, DeleteOutlined, DisconnectOutlined } from "@ant-design/icons";
5 |
6 | import { PasteButton } from ".";
7 |
8 | import { getShortRPC, CUSTOM_RPC_KEY } from "../helpers/NetworkSettingsHelper";
9 |
10 | export default function CustomRPC({
11 | network,
12 | userValue,
13 | setUserValue,
14 | validRPC,
15 | setValidRPC,
16 | loading,
17 | storedRPC,
18 | networkSettingsHelper,
19 | setTargetNetwork,
20 | }) {
21 | return (
22 | <>
23 |
24 |
25 | {
29 | setUserValue(e.target.value);
30 | }}
31 | disabled={storedRPC}
32 | />
33 |
34 |
37 |
38 |
39 |
50 |
51 | >
52 | );
53 | }
54 |
55 | const RPC = ({
56 | network,
57 | rpc,
58 | setUserValue,
59 | validRPC,
60 | setValidRPC,
61 | loading,
62 | storedRPC,
63 | networkSettingsHelper,
64 | setTargetNetwork,
65 | }) => {
66 | if (loading) {
67 | return ;
68 | }
69 |
70 | if (validRPC === undefined) {
71 | return <>>;
72 | }
73 |
74 | return (
75 |
76 |
{validRPC ? getShortRPC(rpc) : "Invalid RPC"}
77 |
78 | {validRPC ?
:
}
79 |
80 | {(validRPC || (!validRPC && storedRPC)) && (
81 |
88 | )}
89 |
90 | );
91 | };
92 |
93 | const RemoveButton = ({ networkSettingsHelper, network, setTargetNetwork, setUserValue, setValidRPC }) => {
94 | return (
95 |
96 | }
98 | onClick={async () => {
99 | networkSettingsHelper.removeItemSetting(network, CUSTOM_RPC_KEY);
100 | setTargetNetwork(networkSettingsHelper.getSelectedItem(true));
101 | setUserValue();
102 | setValidRPC();
103 | }}
104 | >
105 | Delete RPC
106 |
107 |
108 | );
109 | };
110 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@scaffold-eth/monorepo",
3 | "version": "1.0.0",
4 | "keywords": [
5 | "ethereum",
6 | "react",
7 | "uniswap",
8 | "workspaces",
9 | "yarn"
10 | ],
11 | "private": true,
12 | "scripts": {
13 | "react-app:build": "yarn workspace @scaffold-eth/react-app build --max-old-space-size=12288",
14 | "react-app:eject": "yarn workspace @scaffold-eth/react-app eject",
15 | "react-app:start": "yarn workspace @scaffold-eth/react-app start",
16 | "react-app:test": "yarn workspace @scaffold-eth/react-app test",
17 | "build": "yarn workspace @scaffold-eth/react-app build --max-old-space-size=12288",
18 | "chain": "yarn workspace @scaffold-eth/hardhat chain",
19 | "fork": "yarn workspace @scaffold-eth/hardhat fork",
20 | "node": "yarn workspace @scaffold-eth/hardhat chain",
21 | "test": "yarn workspace @scaffold-eth/hardhat test",
22 | "start": "yarn workspace @scaffold-eth/react-app start",
23 | "compile": "yarn workspace @scaffold-eth/hardhat compile",
24 | "deploy": "yarn workspace @scaffold-eth/hardhat deploy",
25 | "watch": "yarn workspace @scaffold-eth/hardhat watch",
26 | "accounts": "yarn workspace @scaffold-eth/hardhat accounts",
27 | "balance": "yarn workspace @scaffold-eth/hardhat balance",
28 | "send": "yarn workspace @scaffold-eth/hardhat send",
29 | "ipfs": "yarn workspace @scaffold-eth/react-app ipfs",
30 | "surge": "yarn workspace @scaffold-eth/react-app surge",
31 | "s3": "yarn workspace @scaffold-eth/react-app s3",
32 | "ship": "yarn workspace @scaffold-eth/react-app ship",
33 | "generate": "yarn workspace @scaffold-eth/hardhat generate",
34 | "account": "yarn workspace @scaffold-eth/hardhat account",
35 | "mineContractAddress": "cd packages/hardhat && npx hardhat mineContractAddress",
36 | "wallet": "cd packages/hardhat && npx hardhat wallet",
37 | "fundedwallet": "cd packages/hardhat && npx hardhat fundedwallet",
38 | "flatten": "cd packages/hardhat && npx hardhat flatten",
39 | "graph-run-node": "cd docker/graph-node && docker-compose up",
40 | "graph-remove-node": "cd docker/graph-node && docker-compose down",
41 | "graph-prepare": "mustache packages/subgraph/config/config.json packages/subgraph/src/subgraph.template.yaml > packages/subgraph/subgraph.yaml",
42 | "graph-codegen": "yarn workspace @scaffold-eth/subgraph graph codegen",
43 | "graph-build": "yarn workspace @scaffold-eth/subgraph graph build",
44 | "graph-create-local": "yarn workspace @scaffold-eth/subgraph graph create --node http://localhost:8020/ scaffold-eth/your-contract",
45 | "graph-remove-local": "yarn workspace @scaffold-eth/subgraph graph remove --node http://localhost:8020/ scaffold-eth/your-contract",
46 | "graph-deploy-local": "yarn workspace @scaffold-eth/subgraph graph deploy --node http://localhost:8020/ --ipfs http://localhost:5001 scaffold-eth/your-contract",
47 | "graph-ship-local": "yarn graph-prepare && yarn graph-codegen && yarn graph-deploy-local",
48 | "deploy-and-graph": "yarn deploy && yarn graph-ship-local",
49 | "theme": "yarn workspace @scaffold-eth/react-app theme",
50 | "watch-theme": "yarn workspace @scaffold-eth/react-app watch"
51 | },
52 | "workspaces": {
53 | "packages": [
54 | "packages/*"
55 | ],
56 | "nohoist": [
57 | "**/@graphprotocol/graph-ts",
58 | "**/@graphprotocol/graph-ts/**",
59 | "**/hardhat",
60 | "**/hardhat/**"
61 | ]
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/packages/react-app/src/components/BytesStringInput.jsx:
--------------------------------------------------------------------------------
1 | import { Input } from "antd";
2 | import React, { useEffect, useState } from "react";
3 |
4 | const { utils, constants } = require("ethers");
5 |
6 | /*
7 | ~ What it does? ~
8 |
9 | Displays input field with options to convert between STRING and BYTES32
10 |
11 | ~ How can I use? ~
12 |
13 | {
18 | setValue(value);
19 | }}
20 | />
21 |
22 | ~ Features ~
23 |
24 | - Provide value={value} to specify initial string
25 | - Provide placeholder="Enter value..." value for the input
26 | - Control input change by onChange={value => { setValue(value);}}
27 |
28 | */
29 |
30 | export default function BytesStringInput(props) {
31 | const [mode, setMode] = useState("STRING");
32 | const [display, setDisplay] = useState();
33 | const [value, setValue] = useState(constants.HashZero);
34 |
35 | // current value is the value in bytes32
36 | const currentValue = typeof props.value !== "undefined" ? props.value : value;
37 |
38 | const option = title => {
39 | return (
40 | {
43 | if (mode === "STRING") {
44 | setMode("BYTES32");
45 | if (!utils.isHexString(currentValue)) {
46 | /* in case user enters invalid bytes32 number,
47 | it considers it as string and converts to bytes32 */
48 | const changedValue = utils.formatBytes32String(currentValue);
49 | setDisplay(changedValue);
50 | } else {
51 | setDisplay(currentValue);
52 | }
53 | } else {
54 | setMode("STRING");
55 | if (currentValue && utils.isHexString(currentValue)) {
56 | setDisplay(utils.parseBytes32String(currentValue));
57 | } else {
58 | setDisplay(currentValue);
59 | }
60 | }
61 | }}
62 | >
63 | {title}
64 |
65 | );
66 | };
67 |
68 | let addonAfter;
69 | if (mode === "STRING") {
70 | addonAfter = option("STRING 🔀");
71 | } else {
72 | addonAfter = option("BYTES32 🔀");
73 | }
74 |
75 | useEffect(() => {
76 | if (!currentValue) {
77 | setDisplay("");
78 | }
79 | }, [currentValue]);
80 |
81 | return (
82 | {
88 | const newValue = e.target.value;
89 | if (mode === "STRING") {
90 | // const ethValue = parseFloat(newValue) / props.price;
91 | // setValue(ethValue);
92 | if (typeof props.onChange === "function") {
93 | props.onChange(utils.formatBytes32String(newValue));
94 | }
95 | setValue(utils.formatBytes32String(newValue));
96 | setDisplay(newValue);
97 | } else {
98 | if (typeof props.onChange === "function") {
99 | props.onChange(newValue);
100 | }
101 | setValue(newValue);
102 | setDisplay(newValue);
103 | }
104 | }}
105 | />
106 | );
107 | }
108 |
--------------------------------------------------------------------------------
/packages/react-app/src/components/index.js:
--------------------------------------------------------------------------------
1 | export { default as Account } from "./Account";
2 | export { default as Address } from "./Address";
3 | export { default as AddressInput } from "./AddressInput";
4 | export { default as AmountDollarSwitch } from "./AmountDollarSwitch";
5 | export { default as Balance } from "./Balance";
6 | export { default as Blockie } from "./Blockie";
7 | export { default as BytesStringInput } from "./BytesStringInput";
8 | export { default as CustomRPC } from "./CustomRPC";
9 | export { default as ERC20Balance } from "./ERC20Balance";
10 | export { default as ERC20Input } from "./ERC20Input";
11 | export { default as Contract } from "./Contract";
12 | export { default as EtherInput } from "./EtherInput";
13 | export { default as Faucet } from "./Faucet";
14 | export { default as GasGauge } from "./GasGauge";
15 | export { default as Header } from "./Header";
16 | export { default as IFrame } from "./IFrame";
17 | export { default as LogoOnLogo } from "./LogoOnLogo";
18 | export { default as Monerium } from "./Monerium";
19 | export { default as MoneriumBalances } from "./MoneriumBalances";
20 | export { default as MoneriumCrossChainAddressSelector } from "./MoneriumCrossChainAddressSelector";
21 | export { default as MoneriumDescription } from "./MoneriumDescription";
22 | export { default as MoneriumHeader } from "./MoneriumHeader";
23 | export { default as MoneriumIban } from "./MoneriumIban";
24 | export { default as MoneriumOnChainCrossChainRadio } from "./MoneriumOnChainCrossChainRadio";
25 | export { default as MoneriumPunkNotConnected } from "./MoneriumPunkNotConnected";
26 | export { default as NetworkDetailedDisplay } from "./NetworkDetailedDisplay";
27 | export { default as NetworkDisplay } from "./NetworkDisplay";
28 | export { default as PasteButton } from "./PasteButton";
29 | export { default as Provider } from "./Provider";
30 | export { default as Punk } from "./Punk";
31 | export { default as PunkBlockie } from "./PunkBlockie";
32 | export { default as QRPunkBlockie } from "./QRPunkBlockie";
33 | export { default as Ramp } from "./Ramp";
34 | export { default as Reload } from "./Reload";
35 | export { default as SelectorWithSettings } from "./SelectorWithSettings";
36 | export { default as SettingsModal } from "./SettingsModal";
37 | export { default as Swap } from "./Swap";
38 | export { default as ThemeSwitch } from "./ThemeSwitch";
39 | export { default as Timeline } from "./Timeline";
40 | export { default as TokenDetailedDisplay } from "./TokenDetailedDisplay";
41 | export { default as TokenBalance } from "./TokenBalance";
42 | export { default as TokenDisplay } from "./TokenDisplay";
43 | export { default as TokenImportDisplay } from "./TokenImportDisplay";
44 | export { default as TransactionDisplay } from "./TransactionDisplay";
45 | export { default as TransactionHistory } from "./TransactionHistory";
46 | export { default as TransactionResponseDisplay } from "./TransactionResponseDisplay";
47 | export { default as TransactionResponses } from "./TransactionResponses";
48 | export { default as Wallet } from "./Wallet";
49 | export { default as WalletImport } from "./WalletImport";
50 | export { default as WalletConnectActiveSessions } from "./WalletConnectActiveSessions";
51 | export { default as WalletConnectTransactionDisplay } from "./WalletConnectTransactionDisplay";
52 | export { default as WalletConnectTransactionPopUp } from "./WalletConnectTransactionPopUp";
53 | export { default as WalletConnectV2ConnectionError } from "./WalletConnectV2ConnectionError";
54 |
--------------------------------------------------------------------------------
/packages/react-app/src/helpers/NetworkSettingsHelper.js:
--------------------------------------------------------------------------------
1 | import { StaticJsonRpcProvider } from "@ethersproject/providers";
2 |
3 | import { getBLockExplorer, getChain } from "./ChainHelper";
4 |
5 | export const NETWORK_SETTINGS_STORAGE_KEY = "networkSettings";
6 |
7 | export const SELECTED_BLOCK_EXPORER_NAME_KEY = "selectedBlockExplorerName";
8 | export const SELECTED_BLOCK_EXPORER_URL_KEY = "selectedBlockExplorerURL";
9 | export const CUSTOM_RPC_KEY = "customRPC";
10 |
11 | export const getNetworkWithSettings = (network, networkSettings) => {
12 | const networkWithSettings = {};
13 |
14 | const selectedBlockExplorerURL = networkSettings[SELECTED_BLOCK_EXPORER_URL_KEY];
15 |
16 | if (selectedBlockExplorerURL) {
17 | networkWithSettings.blockExplorer = selectedBlockExplorerURL;
18 | }
19 |
20 | const customRPC = networkSettings[CUSTOM_RPC_KEY];
21 |
22 | if (customRPC) {
23 | networkWithSettings.rpcUrl = customRPC;
24 | }
25 |
26 | return { ...network, ...networkWithSettings };
27 | };
28 |
29 | export const getShortRPC = rpc => {
30 | const RPC_URL_BEGINNING = "://";
31 |
32 | try {
33 | let startIndex = rpc.indexOf(RPC_URL_BEGINNING);
34 |
35 | if (startIndex === -1) {
36 | startIndex = 0;
37 | } else {
38 | startIndex = startIndex + RPC_URL_BEGINNING.length;
39 | }
40 |
41 | let endIndex = rpc.indexOf("/", startIndex);
42 |
43 | if (endIndex === -1) {
44 | endIndex = rpc.length;
45 | }
46 |
47 | return rpc.substring(startIndex, endIndex);
48 | } catch (error) {
49 | console.error("Couldn't get short RPC", error);
50 |
51 | return rpc;
52 | }
53 | };
54 |
55 | export const migrateSelectedNetworkStorageSetting = networkSettingsHelper => {
56 | // Old code
57 | // const cachedNetwork = window.localStorage.getItem("network");
58 |
59 | try {
60 | const oldKey = "network";
61 |
62 | const oldValue = localStorage.getItem(oldKey);
63 |
64 | if (!oldValue) {
65 | return;
66 | }
67 |
68 | localStorage.removeItem(oldKey);
69 |
70 | networkSettingsHelper.updateSelectedName(oldValue);
71 | } catch (error) {
72 | console.error("Coudn't migrate selected token name setting", error);
73 | }
74 | };
75 |
76 | export const validateRPC = async (
77 | setLoading,
78 | network,
79 | rpc,
80 | currentPunkAddress,
81 | networkSettingsHelper,
82 | setTargetNetwork,
83 | setValidRPC,
84 | ) => {
85 | setLoading(true);
86 |
87 | const validRPC = await isValidRPC(rpc, currentPunkAddress, network);
88 |
89 | if (validRPC) {
90 | networkSettingsHelper.updateItemSettings(network, {
91 | [CUSTOM_RPC_KEY]: rpc,
92 | });
93 |
94 | setTargetNetwork(networkSettingsHelper.getSelectedItem(true));
95 | }
96 |
97 | setValidRPC(validRPC);
98 |
99 | setLoading(false);
100 | };
101 |
102 | const isValidRPC = async (rpc, currentPunkAddress, network) => {
103 | try {
104 | const provider = new StaticJsonRpcProvider(rpc);
105 |
106 | const nonce = await provider.getTransactionCount(currentPunkAddress);
107 |
108 | const rpcNetwork = await provider.getNetwork();
109 |
110 | if (network?.chainId !== rpcNetwork?.chainId) {
111 | console.log("ChainId doesn't match current networ's chainId", network?.chainId, rpcNetwork?.chainId);
112 |
113 | return false;
114 | }
115 |
116 | return true;
117 | } catch (error) {
118 | console.log("Couldn't validate RPC", error);
119 |
120 | return false;
121 | }
122 | };
123 |
--------------------------------------------------------------------------------
/packages/react-app/src/components/Faucet.jsx:
--------------------------------------------------------------------------------
1 | import { SendOutlined } from "@ant-design/icons";
2 | import { parseEther } from "@ethersproject/units";
3 | import { Button, Input, Tooltip } from "antd";
4 | import { useLookupAddress } from "eth-hooks";
5 | import React, { useCallback, useState } from "react";
6 | import Blockies from "react-blockies";
7 | import { Transactor } from "../helpers";
8 |
9 | // improved a bit by converting address to ens if it exists
10 | // added option to directly input ens name
11 | // added placeholder option
12 |
13 | /*
14 | ~ What it does? ~
15 |
16 | Displays a local faucet to send ETH to given address, also wallet is provided
17 |
18 | ~ How can I use? ~
19 |
20 |
26 |
27 | ~ Features ~
28 |
29 | - Provide price={price} of ether and convert between USD and ETH in a wallet
30 | - Provide localProvider={localProvider} to be able to send ETH to given address
31 | - Provide ensProvider={mainnetProvider} and your address will be replaced by ENS name
32 | (ex. "0xa870" => "user.eth") or you can enter directly ENS name instead of address
33 | works both in input field & wallet
34 | - Provide placeholder="Send local faucet" value for the input
35 | */
36 |
37 | export default function Faucet(props) {
38 | const [address, setAddress] = useState();
39 |
40 | let blockie;
41 | if (address && typeof address.toLowerCase === "function") {
42 | blockie = ;
43 | } else {
44 | blockie =
;
45 | }
46 |
47 | const ens = useLookupAddress(props.ensProvider, address);
48 |
49 | const updateAddress = useCallback(
50 | async newValue => {
51 | if (typeof newValue !== "undefined") {
52 | let address = newValue;
53 | if (address.indexOf(".eth") > 0 || address.indexOf(".xyz") > 0) {
54 | try {
55 | const possibleAddress = await props.ensProvider.resolveName(address);
56 | if (possibleAddress) {
57 | address = possibleAddress;
58 | }
59 | // eslint-disable-next-line no-empty
60 | } catch (e) {}
61 | }
62 | setAddress(address);
63 | }
64 | },
65 | [props.ensProvider, props.onChange],
66 | );
67 |
68 | const tx = Transactor(props.localProvider);
69 |
70 | return (
71 |
72 | {
79 | // setAddress(e.target.value);
80 | updateAddress(e.target.value);
81 | }}
82 | suffix={
83 |
84 | {
86 | tx({
87 | to: address,
88 | value: parseEther("0.01"),
89 | });
90 | setAddress("");
91 | }}
92 | shape="circle"
93 | icon={ }
94 | />
95 | {/* */}
96 |
97 | }
98 | />
99 |
100 | );
101 | }
102 |
--------------------------------------------------------------------------------
/packages/react-app/src/components/Account.jsx:
--------------------------------------------------------------------------------
1 | import { LoginOutlined, LogoutOutlined } from "@ant-design/icons";
2 | import { Tooltip } from "antd";
3 | import React from "react";
4 | import { useThemeSwitcher } from "react-css-theme-switcher";
5 | import Address from "./Address";
6 | import Balance from "./Balance";
7 | import Wallet from "./Wallet";
8 |
9 | /*
10 | ~ What it does? ~
11 |
12 | Displays an Address, Balance, and Wallet as one Account component,
13 | also allows users to log in to existing accounts and log out
14 |
15 | ~ How can I use? ~
16 |
17 |
28 |
29 | ~ Features ~
30 |
31 | - Provide address={address} and get balance corresponding to the given address
32 | - Provide localProvider={localProvider} to access balance on local network
33 | - Provide userProvider={userProvider} to display a wallet
34 | - Provide mainnetProvider={mainnetProvider} and your address will be replaced by ENS name
35 | (ex. "0xa870" => "user.eth")
36 | - Provide price={price} of ether and get your balance converted to dollars
37 | - Provide web3Modal={web3Modal}, loadWeb3Modal={loadWeb3Modal}, logoutOfWeb3Modal={logoutOfWeb3Modal}
38 | to be able to log in/log out to/from existing accounts
39 | - Provide blockExplorer={blockExplorer}, click on address and get the link
40 | (ex. by default "https://etherscan.io/" or for xdai "https://blockscout.com/poa/xdai/")
41 | */
42 |
43 | export default function Account({
44 | address,
45 | userProvider,
46 | localProvider,
47 | mainnetProvider,
48 | price,
49 | minimized,
50 | web3Modal,
51 | loadWeb3Modal,
52 | logoutOfWeb3Modal,
53 | blockExplorer,
54 | }) {
55 | const modalButtons = [];
56 | if (web3Modal) {
57 | if (web3Modal.cachedProvider) {
58 | modalButtons.push(
59 |
60 | {/**/}
61 |
62 | {/* */}
63 | ,
64 | );
65 | } else {
66 | modalButtons.push(
67 |
68 | {/**/}
69 |
70 | {/* */}
71 | ,
72 | );
73 | }
74 | }
75 |
76 | const { currentTheme } = useThemeSwitcher();
77 |
78 | const display = minimized ? (
79 | ""
80 | ) : (
81 |
82 | {address ? (
83 |
84 | ) : (
85 | "Connecting..."
86 | )}
87 |
88 |
95 |
96 | );
97 |
98 | return <>{modalButtons}>;
99 | }
100 |
--------------------------------------------------------------------------------
/packages/react-app/src/components/TransactionResponses.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 |
3 | import { HistoryOutlined } from "@ant-design/icons";
4 |
5 | import { TransactionHistory } from "./";
6 | import { TransactionManager } from "../helpers/TransactionManager";
7 |
8 | import { getDeletedOrderIdsFromLocalStorage, getChainId } from "../helpers/MoneriumHelper";
9 |
10 | export default function TransactionResponses({provider, signer, injectedProvider, address, chainId, blockExplorer, moneriumOrders, showHistory, setShowHistory}) {
11 | const transactionManager = new TransactionManager(provider, signer, true);
12 |
13 | const [transactionResponsesArray, setTransactionResponsesArray] = useState([]);
14 |
15 | let filteredMoneriumOrders = undefined;
16 |
17 | const initTransactionResponsesArray = () => {
18 | const deletedOrderIdsFromLocalStorage = getDeletedOrderIdsFromLocalStorage();
19 |
20 | if (moneriumOrders) {
21 | filteredMoneriumOrders = moneriumOrders.filter(
22 | moneriumOrder => {
23 | return (getChainId(moneriumOrder) == chainId) && !deletedOrderIdsFromLocalStorage.includes(moneriumOrder.id);
24 | })
25 | }
26 |
27 | if (injectedProvider !== undefined) {
28 | setTransactionResponsesArray([]);
29 | }
30 | else {
31 | if (!address || !chainId) {
32 | return;
33 | }
34 |
35 | const onChainTransactions = filterResponsesAddressAndChainId(
36 | transactionManager.getTransactionResponsesArray());
37 |
38 | let onAndOffChainTransactions = onChainTransactions;
39 |
40 | if (moneriumOrders) {
41 | onAndOffChainTransactions = onChainTransactions.concat(filteredMoneriumOrders);
42 | }
43 |
44 | setTransactionResponsesArray(onAndOffChainTransactions);
45 | }
46 | }
47 |
48 | const filterResponsesAddressAndChainId = (transactionResponsesArray) => {
49 | return transactionResponsesArray.filter(
50 | transactionResponse => {
51 | return ((transactionResponse.from == address) || (transactionResponse?.origin == address)) && (transactionResponse.chainId == chainId);
52 | })
53 | }
54 |
55 | useEffect(() => {
56 | initTransactionResponsesArray();
57 |
58 | // Listen for storage change events from the same and from other windows as well
59 | window.addEventListener("storage", initTransactionResponsesArray);
60 | window.addEventListener(transactionManager.getLocalStorageChangedEventName(), initTransactionResponsesArray);
61 |
62 | return () => {
63 | window.removeEventListener("storage", initTransactionResponsesArray);
64 | window.removeEventListener(transactionManager.getLocalStorageChangedEventName(), initTransactionResponsesArray);
65 | }
66 | }, [injectedProvider, address, chainId, moneriumOrders]);
67 |
68 | return (
69 | <>
70 | {((transactionResponsesArray.length > 0) || (filteredMoneriumOrders && filteredMoneriumOrders.length > 0)) &&
71 | {
75 | setShowHistory(!showHistory)
76 | }
77 | }
78 | >
79 |
80 |
81 | }
82 |
83 | {(transactionResponsesArray.length > 0) && showHistory && }
84 | >
85 | );
86 | }
87 |
--------------------------------------------------------------------------------
/packages/react-app/src/components/MoneriumBalances.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | import { EuroOutlined } from "@ant-design/icons";
4 |
5 | import { LogoOnLogo } from "./";
6 | import { getShortAddress } from "../helpers/MoneriumHelper";
7 |
8 | import { useLocalStorage} from "../hooks";
9 |
10 | export default function MoneriumBalances( { clientData, currentPunkAddress } ) {
11 | const [showBalances, setShowBalances] = useLocalStorage("showBalances", true);
12 |
13 | const EUReBalance = ( {chainImgSrc, balance} ) => {
14 | return (
15 |
16 |
17 |
23 |
24 |
25 | €{balance ? balance : "0.00"}
26 |
27 |
28 | );
29 | }
30 |
31 | const EUReBalances = ( {balances} ) => {
32 | const ethereumImgSrc = "ethereum-bgfill-icon.svg";
33 | const ethereumBalance = balances.ethereum;
34 |
35 | const polygonImgSrc = "polygon-bgfill-icon.svg";
36 | const polygonBalance = balances.polygon;
37 |
38 | const gnosisImgSrc = "gnosis-bgfill-icon.svg";
39 | const gnosisBalance = balances.gnosis;
40 |
41 | return (
42 |
43 |
44 |
45 |
46 |
47 | );
48 | }
49 |
50 | const accountAddress = currentPunkAddress;
51 | let part1 = accountAddress && accountAddress.substr(2,20)
52 | let part2= accountAddress && accountAddress.substr(22)
53 | const x = parseInt(part1, 16)%100
54 | const y = parseInt(part2, 16)%100
55 | const iconPunkSize = 32;
56 |
57 | return (
58 | <>
59 |
60 | {showBalances &&
61 |
62 |
63 |
64 |
65 | {getShortAddress(accountAddress)}
66 |
67 |
68 | }
69 |
70 |
{
74 | setShowBalances(!showBalances)
75 | }
76 | }
77 | >
78 |
79 |
80 |
81 |
82 | {showBalances &&
83 |
84 |
85 | }
86 | >
87 | );
88 | }
--------------------------------------------------------------------------------
/packages/react-app/src/components/Address.jsx:
--------------------------------------------------------------------------------
1 | import { Skeleton, Typography } from "antd";
2 | import React from "react";
3 | import Blockies from "react-blockies";
4 | import { useThemeSwitcher } from "react-css-theme-switcher";
5 | import { useLookupAddress } from "../hooks";
6 |
7 | // changed value={address} to address={address}
8 |
9 | /*
10 | ~ What it does? ~
11 |
12 | Displays an address with a blockie image and option to copy address
13 |
14 | ~ How can I use? ~
15 |
16 |
22 |
23 | ~ Features ~
24 |
25 | - Provide ensProvider={mainnetProvider} and your address will be replaced by ENS name
26 | (ex. "0xa870" => "user.eth")
27 | - Provide blockExplorer={blockExplorer}, click on address and get the link
28 | (ex. by default "https://etherscan.io/" or for xdai "https://blockscout.com/poa/xdai/")
29 | - Provide fontSize={fontSize} to change the size of address text
30 | */
31 |
32 | const { Text } = Typography;
33 |
34 | const blockExplorerLink = (address, blockExplorer) =>
35 | `${blockExplorer || "https://etherscan.io/"}${"address/"}${address}`;
36 |
37 | export default function Address(props) {
38 | const address = props.value || props.address;
39 |
40 | const ens = useLookupAddress(props.ensProvider, address);
41 |
42 | const { currentTheme } = useThemeSwitcher();
43 |
44 | if (!address) {
45 | return (
46 |
47 |
48 |
49 | );
50 | }
51 |
52 | let displayAddress = address.substr(0, 6);
53 |
54 | if (ens && ens.indexOf("0x") < 0) {
55 | displayAddress = ens;
56 | } else if (props.size === "short") {
57 | displayAddress += "..." + address.substr(-4);
58 | } else if (props.size === "long") {
59 | displayAddress = address;
60 | }
61 |
62 | const etherscanLink = blockExplorerLink(address, props.blockExplorer);
63 | if (props.minimized) {
64 | return (
65 |
66 |
72 |
73 |
74 |
75 | );
76 | }
77 |
78 | let text;
79 | if (props.onChange) {
80 | text = (
81 |
82 |
88 | {displayAddress}
89 |
90 |
91 | );
92 | } else {
93 | text = (
94 |
95 |
101 | {displayAddress}
102 |
103 |
104 | );
105 | }
106 |
107 | return (
108 |
109 |
110 |
111 |
112 |
113 | {text}
114 |
115 |
116 | );
117 | }
118 |
--------------------------------------------------------------------------------
/packages/react-app/src/helpers/PolygonNativeMetaTransaction.js:
--------------------------------------------------------------------------------
1 | // Tokens on Polygon implement Meta Transactions
2 | // https://github.com/maticnetwork/pos-portal/blob/ece4e54546a4e075f3a03b2699bc6bd92a5bc065/contracts/common/NativeMetaTransaction.sol
3 | // https://wiki.polygon.technology/docs/pos/design/transactions/meta-transactions/meta-transactions
4 |
5 | // Instead of calling the transfer function on the token contract and pay for the transaction fee,
6 | // we can sign the transfer message, and call the executeMetaTransaction function with the signature,
7 | // from a different "relayer account", who has MATIC to pay for the transaction.
8 |
9 | import { createEthersWallet } from "./EIP1559Helper";
10 | import { sendTransactionViaRelayerAccount } from "./PolygonRelayerAccountHelper";
11 | import { NETWORKS } from "../constants";
12 |
13 | import { getTransferTxParams } from "./ERC20Helper";
14 |
15 | const { ethers } = require("ethers");
16 |
17 | const ABI = [
18 | "function getNonce(address user) view returns (uint256)",
19 | "function executeMetaTransaction(address userAddress,bytes calldata functionSignature, bytes32 sigR, bytes32 sigS, uint8 sigV) payable returns (bytes)"
20 | ];
21 |
22 | const getExecuteMetaTransactionSignature = async (token, signer, nonce, functionSignature) => {
23 | const name = token.NativeMetaTransaction.name;
24 | const version = token.NativeMetaTransaction.ERC712_VERSION;
25 | const verifyingContract = token.address;
26 | const salt = ethers.utils.hexZeroPad(ethers.utils.hexlify(NETWORKS.polygon.chainId), 32);
27 |
28 | return ethers.utils.splitSignature(
29 | await signer._signTypedData(
30 | {
31 | name,
32 | version,
33 | verifyingContract,
34 | salt
35 | },
36 | {
37 | MetaTransaction: [
38 | {
39 | name: "nonce",
40 | type: "uint256",
41 | },
42 | {
43 | name: "from",
44 | type: "address",
45 | },
46 | {
47 | name: "functionSignature",
48 | type: "bytes",
49 | }
50 | ],
51 | },
52 | {
53 | nonce,
54 | from: signer.address,
55 | functionSignature
56 | }
57 | )
58 | )
59 | }
60 |
61 | export const transferNativeMetaTransaction = async (token, to, amount) => {
62 | console.log("Gasless token transfer: ", token, to, amount);
63 |
64 | const transferTxParams = await getTransferTxParams(token, to, amount);
65 | console.log("transferTxParams", transferTxParams);
66 |
67 | const transferData = transferTxParams.data;
68 | console.log("transferData", transferData);
69 |
70 | const signerWallet = createEthersWallet();
71 |
72 | const provider = new ethers.providers.JsonRpcProvider(NETWORKS.polygon.rpcUrl);
73 | const contract = new ethers.Contract(token.address, ABI, provider);
74 |
75 | const signerNonce = await contract.getNonce(signerWallet.address);
76 | console.log("signerNonce", signerNonce);
77 |
78 | const executeMetaTransactionSignature =
79 | await getExecuteMetaTransactionSignature(token, signerWallet, signerNonce, transferData);
80 |
81 | console.log("executeMetaTransactionSignature", executeMetaTransactionSignature);
82 |
83 | const txParams =
84 | await contract.populateTransaction.executeMetaTransaction(
85 | signerWallet.address, transferData, executeMetaTransactionSignature.r, executeMetaTransactionSignature.s, executeMetaTransactionSignature.v);
86 |
87 | console.log("txParams", JSON.parse(JSON.stringify(txParams)));
88 |
89 | txParams.chainId = NETWORKS.polygon.chainId;
90 |
91 | return sendTransactionViaRelayerAccount(txParams, signerWallet.address, provider);
92 | }
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
--------------------------------------------------------------------------------
/packages/react-app/src/helpers/SepaPaymentQrCodeHelper.jsx:
--------------------------------------------------------------------------------
1 | // This is almost the same as https://github.com/derhuerst/sepa-payment-qr-code
2 | // ToDo: Switch back to the original library once https://github.com/derhuerst/sepa-payment-qr-code/pull/4 is resolved.
3 |
4 | const {isValid: isValidIBAN, electronicFormat: serializeIBAN} = require('iban')
5 |
6 | const SERVICE_TAG = 'BCD'
7 | const VERSION = '002'
8 | const CHARACTER_SET = 1
9 | const IDENTIFICATION_CODE = 'SCT'
10 |
11 | const assertNonEmptyString = (val, name) => {
12 | if ('string' !== typeof val || !val) {
13 | throw new Error(name + ' must be a non-empty string.')
14 | }
15 | }
16 |
17 | export const generateQrCode = data => {
18 | if (!data) throw new Error('data must be an object.')
19 |
20 | // > AT-21 Name of the Beneficiary
21 | assertNonEmptyString(data.name, 'data.name')
22 | if (data.name.length > 70) throw new Error('data.name must have <=70 characters')
23 |
24 | // > AT-23 BIC of the Beneficiary Bank
25 | if ('bic' in data) {
26 | assertNonEmptyString(data.bic, 'data.bic')
27 | if (data.bic.length > 11) throw new Error('data.bic must have <=11 characters')
28 | // todo: validate more?
29 | }
30 |
31 | // > AT-20 Account number of the Beneficiary
32 | // > Only IBAN is allowed.
33 | assertNonEmptyString(data.iban, 'data.iban')
34 | if (!isValidIBAN(data.iban)) {
35 | throw new Error('data.iban must be a valid iban code.')
36 | }
37 |
38 | // > AT-04 Amount of the Credit Transfer in Euro
39 | if ('amount' in data) {
40 | if ('number' !== typeof data.amount) throw new Error('data.amount must be a number.')
41 | if (data.amount < 0.01 || data.amount > 999999999.99) {
42 | throw new Error('data.amount must be >=0.01 and <=999999999.99.')
43 | }
44 | }
45 |
46 | // > AT-44 Purpose of the Credit Transfer
47 | if ('purposeCode' in data) {
48 | assertNonEmptyString(data.purposeCode, 'data.purposeCode')
49 | if (data.purposeCode.length > 4) throw new Error('data.purposeCode must have <=4 characters')
50 | // todo: validate against AT-44
51 | }
52 |
53 | // > AT-05 Remittance Information (Structured)
54 | // > Creditor Reference (ISO 11649 RF Creditor Reference may be used)
55 | if ('structuredReference' in data) {
56 | assertNonEmptyString(data.structuredReference, 'data.structuredReference')
57 | if (data.structuredReference.length > 35) throw new Error('data.structuredReference must have <=35 characters')
58 | // todo: validate against AT-05
59 | }
60 | // > AT-05 Remittance Information (Unstructured)
61 | if ('unstructuredReference' in data) {
62 | assertNonEmptyString(data.unstructuredReference, 'data.unstructuredReference')
63 | if (data.unstructuredReference.length > 140) throw new Error('data.unstructuredReference must have <=140 characters')
64 | }
65 | if (('structuredReference' in data) && ('unstructuredReference' in data)) {
66 | throw new Error('Use either data.structuredReference or data.unstructuredReference.')
67 | }
68 |
69 | // > Beneficiary to originator information
70 | if ('information' in data) {
71 | assertNonEmptyString(data.information, 'data.information')
72 | if (data.information.length > 70) throw new Error('data.information must have <=70 characters')
73 | }
74 |
75 | return [
76 | SERVICE_TAG,
77 | VERSION,
78 | CHARACTER_SET,
79 | IDENTIFICATION_CODE,
80 | data.bic,
81 | data.name,
82 | serializeIBAN(data.iban),
83 | data.amount ? 'EUR' + data.amount.toFixed(2) : data.amount,
84 | data.purposeCode || '',
85 | data.structuredReference || '',
86 | data.unstructuredReference || '',
87 | data.information || ''
88 | ].join('\n')
89 | }
--------------------------------------------------------------------------------
/packages/react-app/src/helpers/communicator.ts:
--------------------------------------------------------------------------------
1 | import { MutableRefObject, useEffect, useState } from "react";
2 | import { MessageFormatter } from "./messageFormatter";
3 | import { SDKMessageEvent, MethodToResponse, Methods, ErrorResponse, RequestId } from "./types";
4 | import { getSDKVersion } from "./utils";
5 |
6 | type MessageHandler = (
7 | msg: SDKMessageEvent,
8 | ) => void | MethodToResponse[Methods] | ErrorResponse | Promise;
9 |
10 | export enum LegacyMethods {
11 | getEnvInfo = "getEnvInfo",
12 | }
13 |
14 | type SDKMethods = Methods | LegacyMethods;
15 |
16 | class AppCommunicator {
17 | private iframeRef: MutableRefObject;
18 | private handlers = new Map();
19 |
20 | constructor(iframeRef: MutableRefObject) {
21 | this.iframeRef = iframeRef;
22 |
23 | window.addEventListener("message", this.handleIncomingMessage);
24 | }
25 |
26 | on = (method: SDKMethods, handler: MessageHandler): void => {
27 | this.handlers.set(method, handler);
28 | };
29 |
30 | private isValidMessage = (msg: SDKMessageEvent): boolean => {
31 | if (msg.data.hasOwnProperty("isCookieEnabled")) {
32 | return true;
33 | }
34 |
35 | const sentFromIframe = this.iframeRef.current?.contentWindow === msg.source;
36 | const knownMethod = Object.values(Methods).includes(msg.data.method);
37 |
38 | return sentFromIframe && knownMethod;
39 | };
40 |
41 | private canHandleMessage = (msg: SDKMessageEvent): boolean => {
42 | return Boolean(this.handlers.get(msg.data.method));
43 | };
44 |
45 | send = (data: unknown, requestId: RequestId, error = false): void => {
46 | const sdkVersion = getSDKVersion();
47 | const msg = error
48 | ? MessageFormatter.makeErrorResponse(requestId, data as string, sdkVersion)
49 | : MessageFormatter.makeResponse(requestId, data, sdkVersion);
50 | // console.log("send", { msg });
51 | this.iframeRef.current?.contentWindow?.postMessage(msg, "*");
52 | };
53 |
54 | handleIncomingMessage = async (msg: SDKMessageEvent): Promise => {
55 | const validMessage = this.isValidMessage(msg);
56 | const hasHandler = this.canHandleMessage(msg);
57 |
58 | if (validMessage && hasHandler) {
59 | // console.log("incoming", { msg: msg.data });
60 |
61 | const handler = this.handlers.get(msg.data.method);
62 | try {
63 | // @ts-expect-error Handler existence is checked in this.canHandleMessage
64 | const response = await handler(msg);
65 |
66 | // If response is not returned, it means the response will be send somewhere else
67 | if (typeof response !== "undefined") {
68 | this.send(response, msg.data.id);
69 | }
70 | } catch (err: any) {
71 | this.send(err.message, msg.data.id, true);
72 | }
73 | }
74 | };
75 |
76 | clear = (): void => {
77 | window.removeEventListener("message", this.handleIncomingMessage);
78 | };
79 | }
80 |
81 | const useAppCommunicator = (iframeRef: MutableRefObject): AppCommunicator | undefined => {
82 | const [communicator, setCommunicator] = useState(undefined);
83 | useEffect(() => {
84 | let communicatorInstance: AppCommunicator;
85 | const initCommunicator = (iframeRef: MutableRefObject) => {
86 | communicatorInstance = new AppCommunicator(iframeRef);
87 | setCommunicator(communicatorInstance);
88 | };
89 |
90 | initCommunicator(iframeRef as MutableRefObject);
91 |
92 | return () => {
93 | communicatorInstance?.clear();
94 | };
95 | }, [iframeRef]);
96 |
97 | return communicator;
98 | };
99 |
100 | export { useAppCommunicator };
101 |
--------------------------------------------------------------------------------
/packages/react-app/src/components/ERC20Balance.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 |
3 | import { Spin } from "antd";
4 |
5 | import {
6 | getDisplayNumberWithDecimals,
7 | getTokenBalance,
8 | getInverseDecimalCorrectedAmountNumber,
9 | } from "../helpers/ERC20Helper";
10 |
11 | import { getTokenPrice } from "../helpers/LiFiTokenPriceHelper";
12 |
13 | const { BigNumber } = require("ethers");
14 |
15 | export default function ERC20Balance({
16 | targetNetwork,
17 | token,
18 | rpcURL,
19 | size,
20 | address,
21 | dollarMode,
22 | setDollarMode,
23 | balance,
24 | setBalance,
25 | price,
26 | setPrice,
27 | }) {
28 | const [loading, setLoading] = useState(true);
29 |
30 | const [displayedNumber, setDisplayedNumber] = useState();
31 |
32 | useEffect(() => {
33 | // Price 0 means that there was an error when fetching token price
34 | if (price === 0) {
35 | setDollarMode(false);
36 | }
37 |
38 | if (!balance || (!price && price !== 0)) {
39 | return;
40 | }
41 |
42 | let displayNumber;
43 |
44 | const balanceNumber = getInverseDecimalCorrectedAmountNumber(BigNumber.from(balance), token.decimals);
45 |
46 | if (!dollarMode) {
47 | displayNumber = balanceNumber;
48 | } else {
49 | displayNumber = balanceNumber * price;
50 | }
51 |
52 | if (Math.floor(displayNumber) === displayNumber && displayNumber !== 0) {
53 | setDisplayedNumber(displayNumber);
54 | return;
55 | }
56 |
57 | setDisplayedNumber(getDisplayNumberWithDecimals(displayNumber, dollarMode));
58 | }, [balance, price, dollarMode]);
59 |
60 | // ToDo: Get rid of the error (when switching networks qickly):
61 | // Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
62 | // https://medium.com/doctolib/react-stop-checking-if-your-component-is-mounted-3bb2568a4934
63 |
64 | useEffect(() => {
65 | if (price === 0) {
66 | return;
67 | }
68 |
69 | async function getPrice() {
70 | setPrice(await getTokenPrice(targetNetwork.chainId, token.address));
71 | }
72 |
73 | getPrice();
74 | }, [targetNetwork, token]);
75 |
76 | useEffect(() => {
77 | if (price !== null) {
78 | return;
79 | }
80 |
81 | async function getPrice() {
82 | setPrice(await getTokenPrice(targetNetwork.chainId, token.address));
83 | }
84 |
85 | getPrice();
86 | }, [price]);
87 |
88 | useEffect(() => {
89 | async function getBalance() {
90 | if (!address) {
91 | return;
92 | }
93 |
94 | setLoading(true);
95 |
96 | try {
97 | const balanceBigNumber = await getTokenBalance(token, rpcURL, address);
98 |
99 | setBalance(balanceBigNumber.toHexString());
100 | } catch (error) {
101 | console.error("Coudn't fetch balance", error);
102 | }
103 |
104 | setLoading(false);
105 | }
106 |
107 | getBalance();
108 | }, [address, token, rpcURL]);
109 |
110 | return (
111 |
112 | {
115 | if (price) {
116 | setDollarMode(!dollarMode);
117 | }
118 | }}
119 | >
120 | {!displayedNumber || loading ? : }
121 |
122 |
123 | );
124 | }
125 |
126 | const Display = ({ displayedNumber, dollarMode }) => {
127 | if (dollarMode) {
128 | return "$" + displayedNumber;
129 | } else {
130 | return displayedNumber;
131 | }
132 | };
133 |
--------------------------------------------------------------------------------
/packages/react-app/src/components/TransactionHistory.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Button, Divider, List } from 'antd';
3 |
4 | import InfiniteScroll from 'react-infinite-scroll-component';
5 |
6 | import { TransactionResponseDisplay, TransactionDisplay } from "./";
7 |
8 | import { appendDeletedOrderIdToLocalStorage, getChainId } from "../helpers/MoneriumHelper";
9 |
10 | import { NETWORKS } from "../constants";
11 |
12 | const getDate = (transactionResponse) => {
13 | return transactionResponse?.date ? transactionResponse.date : transactionResponse?.meta?.placedAt;
14 | }
15 |
16 | export default function TransactionHistory({ transactionResponsesArray, transactionManager, blockExplorer, useInfiniteScroll = false }) {
17 | const sortedTransactionResponsesArray = transactionResponsesArray.sort(
18 | (a, b) => {
19 | return new Date(getDate(b)) - new Date(getDate(a));
20 | }
21 | )
22 |
23 | const transactionList = (
24 | (
28 |
29 | {
43 | appendDeletedOrderIdToLocalStorage(transactionResponse.id);
44 | }
45 | }
46 | />
47 | :
48 |
53 | }
54 | />
55 |
56 | )}
57 | />
58 | );
59 |
60 | const onClearAllTransactions = () => {
61 | transactionResponsesArray.forEach(
62 | (transactionResponse) => {
63 | if (transactionResponse?.id) {
64 | appendDeletedOrderIdToLocalStorage(transactionResponse.id);
65 | }
66 | else {
67 | transactionManager.removeTransactionResponse(transactionResponse)
68 | }
69 |
70 | }
71 | );
72 | }
73 |
74 | const clearAllButton = (
75 | 🗑 🗑 🗑
76 | );
77 |
78 | return (
79 |
80 | {useInfiniteScroll ?
81 |
90 |
95 | {transactionList}
96 |
97 |
98 | {(transactionResponsesArray.length > 2) && clearAllButton}
99 |
100 | :
101 | <>
102 | {transactionList}
103 | {(transactionResponsesArray.length > 1) && clearAllButton }
104 |
105 | >
106 | }
107 |
108 | );
109 | }
110 |
--------------------------------------------------------------------------------
/packages/react-app/src/helpers/PolygonTransferWithAuthorizationHelper.js:
--------------------------------------------------------------------------------
1 | // USDC implements EIP-3009
2 | // https://github.com/centrehq/centre-tokens/blob/0d3cab14ebd133a83fc834dbd48d0468bdf0b391/contracts/v2/EIP3009.sol
3 | // On Polygon a slightly different version is deployed, where EIP_712 version is "1" and the domain uses salt instead of the chainId
4 | // https://polygonscan.com/address/0x2791bca1f2de4661ed88a30c99a7a9449aa84174#readProxyContract
5 |
6 | // Instead of calling the transfer function on the token contract and pay for the transaction fee,
7 | // we can sign the transferWithAuthorization message, and execute it
8 | // from a different "relayer account", who has MATIC to pay for the transaction.
9 |
10 | // PolygonNativeMetaTransaction should work for USDC as well, USDC contract handles nonces differently than DAI and USDT though
11 | // USDC has the nonces method instead of getNonce
12 |
13 | import { createEthersWallet } from "./EIP1559Helper";
14 | import { getAmount } from "./ERC20Helper";
15 | import { sendTransactionViaRelayerAccount } from "./PolygonRelayerAccountHelper";
16 | import { NETWORKS, POLYGON_USDC_ADDRESS } from "../constants";
17 |
18 | const { ethers, BigNumber } = require("ethers");
19 |
20 | const ABI = [
21 | "function transferWithAuthorization (address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)"
22 | ];
23 |
24 | export const transferWithAuthorization = async (token, to, value) => {
25 | value = getAmount(value, token.decimals);
26 |
27 | const validAfter = "0x0000000000000000000000000000000000000000000000000000000000000001"; // signature valid from the beginning
28 |
29 | const provider = new ethers.providers.JsonRpcProvider(NETWORKS.polygon.rpcUrl);
30 | const timestamp = (await provider.getBlock()).timestamp + 60; // signature valid till a minute
31 | const validBefore = ethers.utils.hexZeroPad(BigNumber.from(timestamp).toHexString(), 32);
32 |
33 | const nonce = ethers.utils.hexlify(ethers.utils.randomBytes(32)); // eip-3009 uses Unique Random Nonces
34 |
35 | const signerWallet = createEthersWallet();
36 | const transferWithAuthorizationSignature = await getTransferWithAuthorizationSignature(signerWallet, to, value, validAfter, validBefore, nonce);
37 |
38 | const contract = new ethers.Contract(POLYGON_USDC_ADDRESS, ABI, provider);
39 |
40 | const txParams =
41 | await contract.populateTransaction.transferWithAuthorization(
42 | signerWallet.address, to, value, validAfter, validBefore, nonce, transferWithAuthorizationSignature.v, transferWithAuthorizationSignature.r, transferWithAuthorizationSignature.s);
43 |
44 | console.log("txParams", JSON.parse(JSON.stringify(txParams)));
45 |
46 | txParams.chainId = NETWORKS.polygon.chainId;
47 |
48 | return sendTransactionViaRelayerAccount(txParams, signerWallet.address, provider);
49 | }
50 |
51 | const getTransferWithAuthorizationSignature = async (signer, to, value, validAfter, validBefore, nonce) => {
52 | const name = "USD Coin (PoS)";
53 | const version = "1";
54 | const verifyingContract = POLYGON_USDC_ADDRESS;
55 | const salt = ethers.utils.hexZeroPad(ethers.utils.hexlify(NETWORKS.polygon.chainId), 32);
56 |
57 | return ethers.utils.splitSignature(
58 | await signer._signTypedData(
59 | {
60 | name,
61 | version,
62 | verifyingContract,
63 | salt
64 | },
65 | {
66 | TransferWithAuthorization: [
67 | {
68 | name: "from",
69 | type: "address",
70 | },
71 | {
72 | name: "to",
73 | type: "address",
74 | },
75 | {
76 | name: "value",
77 | type: "uint256",
78 | },
79 | {
80 | name: "validAfter",
81 | type: "uint256",
82 | },
83 | {
84 | name: "validBefore",
85 | type: "uint256",
86 | },
87 | {
88 | name: "nonce",
89 | type: "bytes32",
90 | }
91 | ],
92 | },
93 | {
94 | from: signer.address,
95 | to,
96 | value,
97 | validAfter,
98 | validBefore,
99 | nonce,
100 | }
101 | )
102 | )
103 | }
--------------------------------------------------------------------------------
/packages/react-app/src/helpers/ERC20Helper.js:
--------------------------------------------------------------------------------
1 | import { NETWORKS } from "../constants";
2 |
3 | const { ethers, BigNumber, utils } = require("ethers");
4 |
5 | // https://docs.ethers.org/v5/single-page/#/v5/api/contract/example/-%23-example-erc-20-contract--connecting-to-a-contract
6 | // A Human-Readable ABI; for interacting with the contract, we
7 | // must include any fragment we wish to use
8 | const abi = [
9 | "function balanceOf(address owner) view returns (uint256)",
10 | "function decimals() view returns (uint8)",
11 | "function symbol() view returns (string)",
12 | "function transfer(address to, uint amount) returns (bool)",
13 | ];
14 |
15 | export class ERC20Helper {
16 | constructor(tokenAddress, wallet, rpcURL) {
17 | this.tokenAddress = tokenAddress;
18 | this.wallet = wallet;
19 |
20 | let provider;
21 |
22 | if (!wallet && rpcURL) {
23 | provider = new ethers.providers.StaticJsonRpcProvider(rpcURL);
24 | }
25 |
26 | let signerOrProvider = wallet ? wallet : provider ? provider : undefined;
27 |
28 | this.contract = new ethers.Contract(tokenAddress, abi, signerOrProvider);
29 | }
30 |
31 | balanceOf = address => this.contract.balanceOf(address);
32 |
33 | symbol = () => this.contract.symbol();
34 |
35 | decimals = () => this.contract.decimals();
36 |
37 | transfer = (toAddress, amount) => this.contract.transfer(toAddress, amount);
38 |
39 | transferPopulateTransaction = (toAddress, amount) => this.contract.populateTransaction.transfer(toAddress, amount);
40 | }
41 |
42 | export const getAmount = (amount, decimals) => {
43 | if (utils.isHexString(amount)) {
44 | return amount;
45 | }
46 |
47 | return getDecimalCorrectedAmountBigNumber(amount, decimals).toHexString();
48 | };
49 |
50 | export const getDecimalCorrectedAmountBigNumber = (amountNumber, decimals) =>
51 | utils.parseUnits(amountNumber.toString(), decimals);
52 |
53 | export const getInverseDecimalCorrectedAmountNumber = (amountBigNumber, decimals) =>
54 | Number(utils.formatUnits(amountBigNumber.toString(), decimals));
55 |
56 | export const getDisplayNumberWithDecimals = (number, dollarMode) => {
57 | let decimals = 2;
58 |
59 | if (!dollarMode && number < 1) {
60 | decimals = 4;
61 | }
62 |
63 | return number.toFixed(decimals);
64 | };
65 |
66 | export const getTokenBalance = async (token, rpcURL, address) => {
67 | const erc20Helper = new ERC20Helper(token.address, null, rpcURL);
68 |
69 | const balanceBigNumber = await erc20Helper.balanceOf(address);
70 |
71 | return balanceBigNumber;
72 | };
73 |
74 | export const getTransferTxParams = (token, to, amount) => {
75 | const erc20Helper = new ERC20Helper(token.address);
76 |
77 | return erc20Helper.transferPopulateTransaction(to, getAmount(amount, token.decimals));
78 | };
79 |
80 | const checkBalance = async (token, rpcURL, address, balance, setBalance, intervalId) => {
81 | try {
82 | const balanceBigNumber = BigNumber.from(balance);
83 |
84 | const currentBalanceBigNumber = await getTokenBalance(token, rpcURL, address);
85 |
86 | if (!balanceBigNumber.eq(currentBalanceBigNumber)) {
87 | setBalance(currentBalanceBigNumber.toHexString());
88 |
89 | clearInterval(intervalId);
90 | }
91 | } catch (error) {
92 | console.error("Couldn't check balance", error);
93 | }
94 | };
95 |
96 | export const monitorBalance = (token, rpcURL, address, balance, setBalance) => {
97 | if (!token || !balance || !address) {
98 | return;
99 | }
100 |
101 | let intervalId;
102 |
103 | const checkBalanceAndUpdate = () => {
104 | checkBalance(token, rpcURL, address, balance, setBalance, intervalId);
105 | };
106 |
107 | intervalId = setInterval(checkBalanceAndUpdate, 1000);
108 |
109 | setTimeout(() => {
110 | clearInterval(intervalId);
111 | }, 30 * 1000);
112 | };
113 |
114 | const isTokenAddressMainnetWETH = tokenAddress => {
115 | const tokens = NETWORKS?.ethereum?.erc20Tokens;
116 |
117 | if (!tokens) {
118 | return false;
119 | }
120 |
121 | let tokenAddressWETH = undefined;
122 |
123 | for (const token of tokens) {
124 | if (token.name === "WETH") {
125 | tokenAddressWETH = token.address;
126 | break;
127 | }
128 | }
129 |
130 | if (tokenAddress == tokenAddressWETH) {
131 | return true;
132 | }
133 |
134 | return false;
135 | };
136 |
--------------------------------------------------------------------------------
/packages/react-app/src/components/QRPunkBlockie.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import QR from "qrcode.react";
3 | import { message } from "antd";
4 | import { build } from "eth-url-parser";
5 |
6 | import { Punk, PunkBlockie } from ".";
7 |
8 | import { copy } from "../helpers/EditorHelper";
9 |
10 | const { ethers } = require("ethers");
11 |
12 | export const BLOCKIES_DEFAULT_SIZE = 8;
13 |
14 | export default function QRPunkBlockie({
15 | address,
16 | showAddress,
17 | withQr,
18 | receiveMode,
19 | scale,
20 | chainId,
21 | amount,
22 | selectedErc20Token,
23 | }) {
24 | let displayValue = address;
25 | let paymentLink;
26 |
27 | if (receiveMode) {
28 | try {
29 | const eip681Data = {
30 | scheme: "ethereum",
31 | prefix: null,
32 | target_address: selectedErc20Token ? selectedErc20Token.address : address,
33 | chain_id: chainId,
34 | function_name: selectedErc20Token ? "transfer" : null,
35 | parameters: {},
36 | };
37 |
38 | if (selectedErc20Token) {
39 | eip681Data.parameters.address = address;
40 | }
41 |
42 | if (amount) {
43 | let amountString;
44 |
45 | try {
46 | const decimals = selectedErc20Token ? selectedErc20Token.decimals : 18;
47 |
48 | amountString = ethers.utils
49 | .parseUnits(typeof amount === "string" ? amount : amount.toFixed(decimals).toString(), decimals)
50 | .toString();
51 | } catch (error) {
52 | console.error("Couldn't parse amount", amount, error);
53 | }
54 |
55 | if (amountString) {
56 | if (selectedErc20Token) {
57 | eip681Data.parameters.uint256 = amountString;
58 | } else {
59 | eip681Data.parameters.value = amountString;
60 | }
61 | }
62 | }
63 |
64 | const eip681 = build(eip681Data);
65 |
66 | displayValue = eip681;
67 |
68 | paymentLink = window.location.href + displayValue;
69 | } catch (error) {
70 | console.error("Couldn't create EIP-681 QR value", error);
71 | }
72 | }
73 |
74 | const hardcodedSizeForNow = 380;
75 | let blockieScale = 11.5;
76 |
77 | if (scale) {
78 | blockieScale *= scale;
79 | }
80 |
81 | const punkSize = blockieScale * BLOCKIES_DEFAULT_SIZE; // Make punk image the same size as the blockie, from https://github.com/ethereum/blockies: width/height of the icon in blocks, default: 8
82 |
83 | return (
84 |
86 | copy(paymentLink || displayValue, () =>
87 | message.success(
88 |
89 | {!receiveMode ? "Copied Address" : "Copied Payment Link"}
90 |
93 | ,
94 | ),
95 | )
96 | }
97 | >
98 |
108 | {withQr && (
109 |
110 |
117 |
118 | )}
119 |
120 |
123 |
124 |
125 | {showAddress && (
126 |
137 | {displayValue}
138 |
139 | )}
140 |
141 | );
142 | }
143 |
--------------------------------------------------------------------------------