bridge_ui: more context, don't autoconnect wallets

Change-Id: Ib4048193874f73ec413d4bcc8ac636964767018d
This commit is contained in:
Evan Gray 2021-08-06 01:28:15 -04:00 committed by Hendrik Hofstadt
parent 012c30b30b
commit 266daa228e
11 changed files with 267 additions and 130 deletions

View File

@ -1,33 +1,46 @@
import { Typography } from "@material-ui/core";
import { useEffect, useState } from "react";
import { Button, Tooltip, Typography } from "@material-ui/core";
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
const EthereumSignerKey = () => {
const provider = useEthereumProvider();
const [pk, setPk] = useState("");
// TODO: should this be moved to the context?
useEffect(() => {
let mounted = true;
provider
?.getSigner()
.getAddress()
.then((pk) => {
if (mounted) {
setPk(pk);
}
})
.catch(() => {
console.error("Failed to get signer address");
});
return () => {
mounted = false;
};
}, [provider]);
if (!pk) return null;
const { connect, disconnect, signerAddress, providerError } =
useEthereumProvider();
return (
<Typography>
{pk.substring(0, 6)}...{pk.substr(pk.length - 4)}
</Typography>
<>
{signerAddress ? (
<>
<Tooltip title={signerAddress}>
<Typography>
{signerAddress.substring(0, 6)}...
{signerAddress.substr(signerAddress.length - 4)}
</Typography>
</Tooltip>
<Button
color="secondary"
variant="contained"
size="small"
onClick={disconnect}
style={{ width: "100%", textTransform: "none" }}
>
Disconnect
</Button>
</>
) : (
<Button
color="primary"
variant="contained"
size="small"
onClick={connect}
style={{ width: "100%", textTransform: "none" }}
>
Connect
</Button>
)}
{providerError ? (
<Typography variant="body2" color="error">
{providerError}
</Typography>
) : null}
</>
);
};

View File

@ -15,9 +15,10 @@ function KeyAndBalance({
tokenAddress?: string;
}) {
// TODO: more generic way to get balance
const provider = useEthereumProvider();
const ethBalance = useEthereumBalance(
const { provider, signerAddress } = useEthereumProvider();
const { uiAmountString: ethBalance } = useEthereumBalance(
tokenAddress,
signerAddress,
provider,
chainId === CHAIN_ID_ETH
);

View File

@ -1,14 +1,40 @@
import { Typography } from "@material-ui/core";
import { Button, Tooltip, Typography } from "@material-ui/core";
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
const SolanaWalletKey = () => {
const { wallet } = useSolanaWallet();
const pk = wallet?.publicKey?.toString();
if (!pk) return null;
const { connect, disconnect, connected, wallet } = useSolanaWallet();
const pk = wallet?.publicKey?.toString() || "";
return (
<Typography>
{pk.substring(0, 3)}...{pk.substr(pk.length - 3)}
</Typography>
<>
{connected ? (
<>
<Tooltip title={pk}>
<Typography>
{pk.substring(0, 3)}...{pk.substr(pk.length - 3)}
</Typography>
</Tooltip>
<Button
color="secondary"
variant="contained"
size="small"
onClick={disconnect}
style={{ width: "100%", textTransform: "none" }}
>
Disconnect
</Button>
</>
) : (
<Button
color="primary"
variant="contained"
size="small"
onClick={connect}
style={{ width: "100%", textTransform: "none" }}
>
Connect
</Button>
)}
</>
);
};

View File

@ -104,7 +104,14 @@ function Transfer() {
const handleAmountChange = useCallback((event) => {
setAmount(event.target.value);
}, []);
const provider = useEthereumProvider();
const { provider, signer, signerAddress } = useEthereumProvider();
const { decimals: ethDecimals, uiAmountString: ethBalance } =
useEthereumBalance(
assetAddress,
signerAddress,
provider,
fromChain === CHAIN_ID_ETH
);
const { wallet } = useSolanaWallet();
const solPK = wallet?.publicKey;
const {
@ -125,7 +132,7 @@ function Transfer() {
fromChain === CHAIN_ID_ETH &&
attestFrom[fromChain] === attestFromEth
) {
attestFromEth(provider, assetAddress);
attestFromEth(provider, signer, assetAddress);
}
if (
fromChain === CHAIN_ID_SOLANA &&
@ -134,7 +141,7 @@ function Transfer() {
attestFromSolana(wallet, solPK?.toString(), assetAddress, solDecimals);
}
}
}, [fromChain, provider, wallet, solPK, assetAddress, solDecimals]);
}, [fromChain, provider, signer, wallet, solPK, assetAddress, solDecimals]);
// TODO: dynamically get "to" wallet
const handleTransferClick = useCallback(() => {
// TODO: more generic way of calling these
@ -145,7 +152,9 @@ function Transfer() {
) {
transferFromEth(
provider,
signer,
assetAddress,
ethDecimals,
amount,
toChain,
solPK?.toBytes()
@ -162,7 +171,7 @@ function Transfer() {
assetAddress,
amount,
solDecimals,
provider,
signerAddress,
toChain
);
}
@ -170,20 +179,18 @@ function Transfer() {
}, [
fromChain,
provider,
signer,
signerAddress,
wallet,
solPK,
solTokenPK,
assetAddress,
amount,
ethDecimals,
solDecimals,
toChain,
]);
// update this as we develop, just setting expectations with the button state
const ethBalance = useEthereumBalance(
assetAddress,
provider,
fromChain === CHAIN_ID_ETH
);
const balance = Number(ethBalance) || solBalance;
const isAttestImplemented = !!attestFrom[fromChain];
const isTransferImplemented = !!transferFrom[fromChain];

View File

@ -1,52 +1,129 @@
import detectEthereumProvider from "@metamask/detect-provider";
import { ethers } from "ethers";
import React, { ReactChildren, useContext, useEffect, useState } from "react";
import React, {
ReactChildren,
useCallback,
useContext,
useMemo,
useState,
} from "react";
type Provider = ethers.providers.Web3Provider | undefined;
type Signer = ethers.Signer | undefined;
type Network = ethers.providers.Network | undefined;
const EthereumProviderContext = React.createContext<Provider>(undefined);
interface IEthereumProviderContext {
connect(): void;
disconnect(): void;
provider: Provider;
network: Network;
signer: Signer;
signerAddress: string | undefined;
providerError: string | null;
}
const EthereumProviderContext = React.createContext<IEthereumProviderContext>({
connect: () => {},
disconnect: () => {},
provider: undefined,
network: undefined,
signer: undefined,
signerAddress: undefined,
providerError: null,
});
export const EthereumProviderProvider = ({
children,
}: {
children: ReactChildren;
}) => {
const [providerError, setProviderError] = useState<string | null>(null);
const [provider, setProvider] = useState<Provider>(undefined);
useEffect(() => {
let mounted = true;
const [network, setNetwork] = useState<Network>(undefined);
const [signer, setSigner] = useState<Signer>(undefined);
const [signerAddress, setSignerAddress] = useState<string | undefined>(
undefined
);
const connect = useCallback(() => {
setProviderError(null);
detectEthereumProvider()
.then((detectedProvider) => {
if (detectedProvider) {
if (mounted) {
const ethersProvider = new ethers.providers.Web3Provider(
// @ts-ignore
detectedProvider
);
ethersProvider
.send("eth_requestAccounts", [])
.then(() => {
if (mounted) {
setProvider(ethersProvider);
}
})
.catch(() => {
console.error(
"An error occurred while requesting eth accounts"
);
});
}
const provider = new ethers.providers.Web3Provider(
// @ts-ignore
detectedProvider,
"any" //TODO: should we only allow homestead? env perhaps?
);
provider
.send("eth_requestAccounts", [])
.then(() => {
setProviderError(null);
setProvider(provider);
provider
.getNetwork()
.then((network) => {
setNetwork(network);
})
.catch(() => {
setProviderError(
"An error occurred while getting the network"
);
});
const signer = provider.getSigner();
setSigner(signer);
signer
.getAddress()
.then((address) => {
setSignerAddress(address);
})
.catch(() => {
setProviderError(
"An error occurred while getting the signer address"
);
});
})
.catch(() => {
setProviderError(
"An error occurred while requesting eth accounts"
);
});
} else {
console.log("Please install MetaMask");
setProviderError("Please install MetaMask");
}
})
.catch(() => console.log("Please install MetaMask"));
return () => {
mounted = false;
};
.catch(() => {
setProviderError("Please install MetaMask");
});
}, []);
const disconnect = useCallback(() => {
setProviderError(null);
setProvider(undefined);
setNetwork(undefined);
setSigner(undefined);
setSignerAddress(undefined);
}, []);
//TODO: useEffect provider.on("network") to refresh on network changes
//TODO: detect account change
const contextValue = useMemo(
() => ({
connect,
disconnect,
provider,
network,
signer,
signerAddress,
providerError,
}),
[
connect,
disconnect,
provider,
network,
signer,
signerAddress,
providerError,
]
);
return (
<EthereumProviderContext.Provider value={provider}>
<EthereumProviderContext.Provider value={contextValue}>
{children}
</EthereumProviderContext.Provider>
);

View File

@ -1,20 +1,23 @@
import Wallet from "@project-serum/sol-wallet-adapter";
import React, {
ReactChildren,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import Wallet from "@project-serum/sol-wallet-adapter";
import { SOLANA_HOST } from "../utils/consts";
interface ISolanaWalletContext {
connect(): void;
disconnect(): void;
connected: boolean;
wallet: Wallet | undefined;
}
const getDefaultWallet = () => new Wallet("https://www.sollet.io", SOLANA_HOST);
const SolanaWalletContext = React.createContext<ISolanaWalletContext>({
connect: () => {},
disconnect: () => {},
connected: false,
wallet: undefined,
});
@ -23,28 +26,27 @@ export const SolanaWalletProvider = ({
}: {
children: ReactChildren;
}) => {
const wallet = useMemo(getDefaultWallet, []);
const [wallet, setWallet] = useState<Wallet | undefined>(undefined);
const [connected, setConnected] = useState(false);
useEffect(() => {
const connect = useCallback(() => {
const wallet = new Wallet("https://www.sollet.io", SOLANA_HOST);
setWallet(wallet);
wallet.on("connect", () => {
setConnected(true);
console.log("Connected to wallet " + wallet.publicKey?.toBase58());
});
wallet.on("disconnect", () => {
console.log("disconnected");
setConnected(false);
console.log("Disconnected from wallet");
setWallet(undefined);
});
wallet.connect();
return () => {
wallet.disconnect();
};
}, []);
const disconnect = useCallback(() => {
wallet?.disconnect();
}, [wallet]);
console.log(`Connected state: ${connected}`);
//TODO: useEffect to refresh on network changes
// ensure users of the context refresh on connect state change
const contextValue = useMemo(
() => ({ connected, wallet }),
[wallet, connected]
() => ({ connect, disconnect, connected, wallet }),
[connect, disconnect, wallet, connected]
);
return (
<SolanaWalletContext.Provider value={contextValue}>

View File

@ -3,16 +3,30 @@ import { formatUnits } from "ethers/lib/utils";
import { useEffect, useState } from "react";
import { TokenImplementation__factory } from "../ethers-contracts";
// TODO: can this be shared with other balances
export interface Balance {
decimals: number;
uiAmountString: string;
}
function createBalance(decimals: number, uiAmountString: string) {
return {
decimals,
uiAmountString,
};
}
function useEthereumBalance(
address: string | undefined,
ownerAddress: string | undefined,
provider: ethers.providers.Web3Provider | undefined,
shouldCalculate?: boolean
) {
//TODO: should this check allowance too or subtract allowance?
const [balance, setBalance] = useState<string>("");
const [balance, setBalance] = useState<Balance>(createBalance(0, ""));
useEffect(() => {
if (!address || !provider || !shouldCalculate) {
setBalance("");
if (!address || !ownerAddress || !provider || !shouldCalculate) {
setBalance(createBalance(0, ""));
return;
}
let cancelled = false;
@ -20,26 +34,21 @@ function useEthereumBalance(
token
.decimals()
.then((decimals) => {
provider
?.getSigner()
.getAddress()
.then((pk) => {
token.balanceOf(pk).then((n) => {
if (!cancelled) {
setBalance(formatUnits(n, decimals));
}
});
});
token.balanceOf(ownerAddress).then((n) => {
if (!cancelled) {
setBalance(createBalance(decimals, formatUnits(n, decimals)));
}
});
})
.catch(() => {
if (!cancelled) {
setBalance("");
setBalance(createBalance(0, ""));
}
});
return () => {
cancelled = true;
};
}, [address, provider, shouldCalculate]);
}, [address, ownerAddress, provider, shouldCalculate]);
return balance;
}

View File

@ -40,12 +40,17 @@ function useSolanaBalance(
setBalance(createBalance(undefined, "", 0, 0, ""));
return;
}
let mint;
try {
mint = new PublicKey(tokenAddress);
} catch (e) {
setBalance(createBalance(undefined, "", 0, 0, ""));
return;
}
let cancelled = false;
const connection = new Connection(SOLANA_HOST, "finalized");
connection
.getParsedTokenAccountsByOwner(ownerAddress, {
mint: new PublicKey(tokenAddress),
})
.getParsedTokenAccountsByOwner(ownerAddress, { mint })
.then(({ value }) => {
if (!cancelled) {
if (value.length) {

View File

@ -19,16 +19,14 @@ import {
SOL_TOKEN_BRIDGE_ADDRESS,
} from "./consts";
// TODO: this should probably be extended from the context somehow so that the signatures match
// TODO: allow for / handle cancellation?
// TODO: overall better input checking and error handling
export function attestFromEth(
provider: ethers.providers.Web3Provider | undefined,
signer: ethers.Signer | undefined,
tokenAddress: string
) {
if (!provider) return;
const signer = provider.getSigner();
if (!signer) return;
if (!provider || !signer) return;
//TODO: more catches
(async () => {
const signerAddress = await signer.getAddress();

View File

@ -19,14 +19,18 @@ export async function getAttestedAssetEth(
ETH_TOKEN_BRIDGE_ADDRESS,
provider
);
// TODO: address conversion may be more complex than this
const originAssetBytes = zeroPad(
originChain === CHAIN_ID_SOLANA
? new PublicKey(originAsset).toBytes()
: arrayify(originAsset),
32
);
return await tokenBridge.wrappedAsset(originChain, originAssetBytes);
try {
// TODO: address conversion may be more complex than this
const originAssetBytes = zeroPad(
originChain === CHAIN_ID_SOLANA
? new PublicKey(originAsset).toBytes()
: arrayify(originAsset),
32
);
return await tokenBridge.wrappedAsset(originChain, originAssetBytes);
} catch (e) {
return ethers.constants.AddressZero;
}
}
export async function getAttestedAssetSol(

View File

@ -25,24 +25,22 @@ import {
SOL_TOKEN_BRIDGE_ADDRESS,
} from "./consts";
// TODO: this should probably be extended from the context somehow so that the signatures match
// TODO: allow for / handle cancellation?
// TODO: overall better input checking and error handling
export function transferFromEth(
provider: ethers.providers.Web3Provider | undefined,
// TODO: specify signer
signer: ethers.Signer | undefined,
tokenAddress: string,
decimals: number,
amount: string,
recipientChain: ChainId,
recipientAddress: Uint8Array | undefined
) {
if (!provider || !recipientAddress) return;
const signer = provider.getSigner();
if (!signer) return;
if (!provider || !signer || !recipientAddress) return;
//TODO: check if token attestation exists on the target chain
//TODO: don't hardcode, fetch decimals / share them with balance, how do we determine recipient chain?
//TODO: more catches
const amountParsed = parseUnits(amount, 18);
const amountParsed = parseUnits(amount, decimals);
(async () => {
const signerAddress = await signer.getAddress();
console.log("Signer:", signerAddress);
@ -63,7 +61,7 @@ export function transferFromEth(
const nonceBuffer = Buffer.alloc(4);
nonceBuffer.writeUInt32LE(nonceConst, 0);
console.log("Initiating transfer");
console.log("Amount:", formatUnits(amountParsed, 18));
console.log("Amount:", formatUnits(amountParsed, decimals));
console.log("To chain:", recipientChain);
console.log("To address:", recipientAddress);
console.log("Fees:", fee);
@ -108,7 +106,7 @@ export function transferFromSolana(
mintAddress: string,
amount: string,
decimals: number,
targetProvider: ethers.providers.Web3Provider | undefined,
targetAddressStr: string | undefined,
targetChain: ChainId
) {
if (
@ -116,13 +114,10 @@ export function transferFromSolana(
!wallet.publicKey ||
!payerAddress ||
!fromAddress ||
!targetProvider
!targetAddressStr
)
return;
const targetSigner = targetProvider.getSigner();
if (!targetSigner) return;
(async () => {
const targetAddressStr = await targetSigner.getAddress();
const targetAddress = zeroPad(arrayify(targetAddressStr), 32);
const nonceConst = Math.random() * 100000;
const nonceBuffer = Buffer.alloc(4);