Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: button to add erc20 token to wallet #63

Merged
merged 3 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion web/src/components/DepositCard/DepositCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import { Dec, DecUtils } from "@keplr-wallet/unit";
import AnimatedArrowSpacer from "components/AnimatedDownArrowSpacer/AnimatedDownArrowSpacer";
import Dropdown from "components/Dropdown/Dropdown";
import { useConfig } from "config";
import { useEvmChainSelection } from "features/EthWallet";
import {
AddERC20ToWalletButton,
useEvmChainSelection,
} from "features/EthWallet";
import {
padDecimal,
sendIbcTransfer,
Expand Down Expand Up @@ -411,6 +414,11 @@ export default function DepositCard(): React.ReactElement {
Balance: <i className="fas fa-spinner fa-pulse" />
</p>
)}
{selectedEvmCurrencyOption?.value?.erc20ContractAddress && (
<AddERC20ToWalletButton
evmCurrency={selectedEvmCurrencyOption.value}
/>
)}
</div>
)}
{recipientAddressOverride && !isRecipientAddressEditable && (
Expand Down
4 changes: 1 addition & 3 deletions web/src/components/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ interface DropdownProps<T> {
valueOverride?: DropdownOption<T> | null;
}

function Dropdown<T>({
export default function Dropdown<T>({
options,
onSelect,
placeholder = "Select an option",
Expand Down Expand Up @@ -199,5 +199,3 @@ function Dropdown<T>({
</div>
);
}

export default Dropdown;
10 changes: 7 additions & 3 deletions web/src/components/WithdrawCard/WithdrawCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useConfig } from "config";
import AnimatedArrowSpacer from "components/AnimatedDownArrowSpacer/AnimatedDownArrowSpacer";
import Dropdown from "components/Dropdown/Dropdown";
import {
AddERC20ToWalletButton,
getAstriaWithdrawerService,
useEthWallet,
useEvmChainSelection,
Expand All @@ -15,7 +16,7 @@ import { NotificationType, useNotifications } from "features/Notifications";
export default function WithdrawCard(): React.ReactElement {
const { evmChains, ibcChains } = useConfig();
const { addNotification } = useNotifications();
const { selectedWallet } = useEthWallet();
const { provider } = useEthWallet();

const {
evmAccountAddress: fromAddress,
Expand Down Expand Up @@ -163,7 +164,7 @@ export default function WithdrawCard(): React.ReactElement {
}

const recipientAddress = recipientAddressOverride || ibcAccountAddress;
if (!selectedWallet || !fromAddress || !recipientAddress) {
if (!provider || !fromAddress || !recipientAddress) {
addNotification({
toastOpts: {
toastType: NotificationType.WARNING,
Expand Down Expand Up @@ -194,7 +195,7 @@ export default function WithdrawCard(): React.ReactElement {
selectedEvmCurrency.nativeTokenWithdrawerContractAddress ||
"";
const withdrawerSvc = getAstriaWithdrawerService(
selectedWallet.provider,
provider,
contractAddress,
Boolean(selectedEvmCurrency.erc20ContractAddress),
);
Expand Down Expand Up @@ -334,6 +335,9 @@ export default function WithdrawCard(): React.ReactElement {
Balance: <i className="fas fa-spinner fa-pulse" />
</p>
)}
{selectedEvmCurrency?.erc20ContractAddress && (
<AddERC20ToWalletButton evmCurrency={selectedEvmCurrency} />
)}
</div>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { EvmCurrency } from "config";

import { useEthWallet } from "../../hooks/useEthWallet";

interface AddERC20ToWalletButtonProps {
evmCurrency: EvmCurrency;
buttonClassNameOverride?: string;
}

export default function AddERC20ToWalletButton({
evmCurrency,
buttonClassNameOverride,
}: AddERC20ToWalletButtonProps) {
const { provider } = useEthWallet();
const buttonClassName =
buttonClassNameOverride ?? "p-0 is-size-7 has-text-light is-ghost";

const addCoinToWallet = async () => {
if (!provider) {
return;
}
try {
const wasAdded = await provider.send("wallet_watchAsset", {
type: "ERC20",
options: {
address: evmCurrency.erc20ContractAddress,
symbol: evmCurrency.coinDenom,
decimals: evmCurrency.coinDecimals,
},
});

if (wasAdded) {
console.debug("ERC20 token added: ", evmCurrency.erc20ContractAddress);
} else {
console.debug(
"User declined to add ERC20 token: ",
evmCurrency.erc20ContractAddress,
);
}
} catch (error) {
console.error(error);
}
};

return (
<>
<button
type="button"
key={evmCurrency.coinMinimalDenom}
onClick={() => addCoinToWallet()}
className={`button is-underlined ${buttonClassName}`}
>
Add ERC20 to wallet
</button>
</>
);
}
11 changes: 3 additions & 8 deletions web/src/features/EthWallet/contexts/EthWalletContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ import { formatBalance } from "features/EthWallet/utils/utils";

export interface EthWalletContextProps {
providers: EIP6963ProviderDetail[];
selectedWallet: EIP6963ProviderDetail | undefined; // TODO - refactor to be an ethers.Provider to make things easier?
selectedWallet: EIP6963ProviderDetail | undefined;
provider: ethers.BrowserProvider | undefined;
userAccount: UserAccount | undefined;
selectedChain: EvmChainInfo | undefined;
provider: ethers.BrowserProvider | undefined;
signer: ethers.Signer | undefined;
handleConnect: (
providerWithInfo: EIP6963ProviderDetail,
defaultChain: EvmChainInfo,
Expand All @@ -32,7 +31,6 @@ export const EthWalletContextProvider: React.FC<{ children: ReactNode }> = ({
const [selectedProvider, setSelectedProvider] =
useState<ethers.BrowserProvider>();
const [userAccount, setUserAccount] = useState<UserAccount>();
const [signer, setSigner] = useState<ethers.Signer>();
const providers = useSyncWalletProviders();
const [selectedChain, setSelectedChain] = useState<
EvmChainInfo | undefined
Expand Down Expand Up @@ -138,13 +136,11 @@ export const EthWalletContextProvider: React.FC<{ children: ReactNode }> = ({
// use first account
const address = accounts[0];

// create an ethers provider and signer
// create an ethers provider from eip1193 provider
const ethersProvider = new ethers.BrowserProvider(
providerWithInfo.provider,
);
setSelectedProvider(ethersProvider);
const ethersSigner = await ethersProvider.getSigner();
setSigner(ethersSigner);

// get balance using ethers
const balance = await ethersProvider.getBalance(address);
Expand Down Expand Up @@ -178,7 +174,6 @@ export const EthWalletContextProvider: React.FC<{ children: ReactNode }> = ({
selectedWallet,
userAccount,
selectedChain,
signer,
handleConnect,
}}
>
Expand Down
18 changes: 9 additions & 9 deletions web/src/features/EthWallet/hooks/useEvmChainSelection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { formatBalance } from "features/EthWallet/utils/utils";

export function useEvmChainSelection(evmChains: EvmChains) {
const { addNotification } = useNotifications();
const { selectedWallet, userAccount } = useEthWallet();
const { provider, userAccount } = useEthWallet();

const [selectedEvmChain, setSelectedEvmChain] = useState<EvmChainInfo | null>(
null,
Expand All @@ -52,7 +52,7 @@ export function useEvmChainSelection(evmChains: EvmChains) {
useEffect(() => {
async function getAndSetBalance() {
if (
!selectedWallet ||
!provider ||
!userAccount ||
!selectedEvmChain ||
!selectedEvmCurrency ||
Expand All @@ -70,7 +70,7 @@ export function useEvmChainSelection(evmChains: EvmChains) {
selectedEvmCurrency.nativeTokenWithdrawerContractAddress ||
"";
const withdrawerSvc = getAstriaWithdrawerService(
selectedWallet.provider,
provider,
contractAddress,
Boolean(selectedEvmCurrency.erc20ContractAddress),
);
Expand Down Expand Up @@ -98,7 +98,7 @@ export function useEvmChainSelection(evmChains: EvmChains) {
}, [
selectedEvmChain,
selectedEvmCurrency,
selectedWallet,
provider,
userAccount,
evmAccountAddress,
]);
Expand Down Expand Up @@ -175,7 +175,7 @@ export function useEvmChainSelection(evmChains: EvmChains) {
// create refs to hold the latest state values
const latestState = useRef({
userAccount,
selectedWallet,
provider,
evmAccountAddress,
selectedEvmChain,
});
Expand All @@ -184,11 +184,11 @@ export function useEvmChainSelection(evmChains: EvmChains) {
useEffect(() => {
latestState.current = {
userAccount,
selectedWallet,
provider,
evmAccountAddress,
selectedEvmChain,
};
}, [userAccount, selectedWallet, evmAccountAddress, selectedEvmChain]);
}, [userAccount, provider, evmAccountAddress, selectedEvmChain]);

const connectEVMWallet = async () => {
if (!selectedEvmChain) {
Expand All @@ -206,8 +206,8 @@ export function useEvmChainSelection(evmChains: EvmChains) {
const currentState = latestState.current;
setEvmAccountAddress("");
setSelectedEvmChain(null);
if (currentState.selectedWallet) {
currentState.selectedWallet = undefined;
if (currentState.provider) {
currentState.provider = undefined;
}
},
onConfirm: () => {
Expand Down
2 changes: 2 additions & 0 deletions web/src/features/EthWallet/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import AddERC20ToWalletButton from "./components/AddERC20ToWalletButton/AddERC20ToWalletButton";
import EthWalletConnector from "./components/EthWalletConnector/EthWalletConnector";
import { EthWalletContextProvider } from "./contexts/EthWalletContext";
import { useEthWallet } from "./hooks/useEthWallet";
Expand All @@ -6,6 +7,7 @@ import { getAstriaWithdrawerService } from "./services/AstriaWithdrawerService/A

export {
getAstriaWithdrawerService,
AddERC20ToWalletButton,
EthWalletConnector,
EthWalletContextProvider,
useEthWallet,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@ describe("AstriaWithdrawerService and AstriaErc20WithdrawerService", () => {
describe("AstriaWithdrawerService", () => {
it("should create a singleton instance", () => {
const service1 = getAstriaWithdrawerService(
{} as ethers.Eip1193Provider,
{} as ethers.BrowserProvider,
mockContractAddress,
);
const service2 = getAstriaWithdrawerService(
{} as ethers.Eip1193Provider,
{} as ethers.BrowserProvider,
mockContractAddress,
);

Expand All @@ -63,8 +63,8 @@ describe("AstriaWithdrawerService and AstriaErc20WithdrawerService", () => {
});

it("should update provider when a new one is supplied", () => {
const initialProvider = {} as ethers.Eip1193Provider;
const newProvider = {} as ethers.Eip1193Provider;
const initialProvider = {} as ethers.BrowserProvider;
const newProvider = {} as ethers.BrowserProvider;

const service1 = getAstriaWithdrawerService(
initialProvider,
Expand All @@ -76,17 +76,13 @@ describe("AstriaWithdrawerService and AstriaErc20WithdrawerService", () => {
);

expect(service1).toBe(service2);
expect(ethers.BrowserProvider).toHaveBeenCalledTimes(2);
expect(ethers.BrowserProvider).toHaveBeenNthCalledWith(
1,
initialProvider,
);
expect(ethers.BrowserProvider).toHaveBeenNthCalledWith(2, newProvider);
});

it("should call withdrawToIbcChain with correct parameters", async () => {
const provider = new ethers.BrowserProvider({} as ethers.Eip1193Provider);

const service = getAstriaWithdrawerService(
{} as ethers.Eip1193Provider,
provider,
mockContractAddress,
) as AstriaWithdrawerService;

Expand All @@ -113,12 +109,12 @@ describe("AstriaWithdrawerService and AstriaErc20WithdrawerService", () => {
describe("AstriaErc20WithdrawerService", () => {
it("should create a singleton instance", () => {
const service1 = getAstriaWithdrawerService(
{} as ethers.Eip1193Provider,
{} as ethers.BrowserProvider,
mockContractAddress,
true,
);
const service2 = getAstriaWithdrawerService(
{} as ethers.Eip1193Provider,
{} as ethers.BrowserProvider,
mockContractAddress,
true,
);
Expand All @@ -128,8 +124,8 @@ describe("AstriaWithdrawerService and AstriaErc20WithdrawerService", () => {
});

it("should update provider when a new one is supplied", () => {
const initialProvider = {} as ethers.Eip1193Provider;
const newProvider = {} as ethers.Eip1193Provider;
const initialProvider = {} as ethers.BrowserProvider;
const newProvider = {} as ethers.BrowserProvider;

const service1 = getAstriaWithdrawerService(
initialProvider,
Expand All @@ -143,17 +139,13 @@ describe("AstriaWithdrawerService and AstriaErc20WithdrawerService", () => {
);

expect(service1).toBe(service2);
expect(ethers.BrowserProvider).toHaveBeenCalledTimes(2);
expect(ethers.BrowserProvider).toHaveBeenNthCalledWith(
1,
initialProvider,
);
expect(ethers.BrowserProvider).toHaveBeenNthCalledWith(2, newProvider);
});

it("should call withdrawToIbcChain with correct parameters", async () => {
it("should call withdrawToIbcChain for erc20 with correct parameters", async () => {
const provider = new ethers.BrowserProvider({} as ethers.Eip1193Provider);

const service = getAstriaWithdrawerService(
{} as ethers.Eip1193Provider,
provider,
mockContractAddress,
true,
) as AstriaErc20WithdrawerService;
Expand All @@ -179,7 +171,7 @@ describe("AstriaWithdrawerService and AstriaErc20WithdrawerService", () => {
describe("getAstriaWithdrawerService", () => {
it("should return AstriaWithdrawerService when isErc20 is false", () => {
const service = getAstriaWithdrawerService(
{} as ethers.Eip1193Provider,
{} as ethers.BrowserProvider,
mockContractAddress,
false,
);
Expand All @@ -188,7 +180,7 @@ describe("AstriaWithdrawerService and AstriaErc20WithdrawerService", () => {

it("should return AstriaErc20WithdrawerService when isErc20 is true", () => {
const service = getAstriaWithdrawerService(
{} as ethers.Eip1193Provider,
{} as ethers.BrowserProvider,
mockContractAddress,
true,
);
Expand Down
Loading