From c483f92314bd997a4f851c5e6911c74adfbf5cdc Mon Sep 17 00:00:00 2001 From: samsiegart Date: Fri, 10 Mar 2023 14:20:18 -0800 Subject: [PATCH 01/11] feat: ibc transfers --- wallet/src/components/BrandIcon.tsx | 6 +- wallet/src/components/IbcTransfer.tsx | 491 ++++++++++++++++++++++++ wallet/src/components/Petname.tsx | 3 +- wallet/src/components/PurseAmount.tsx | 3 +- wallet/src/components/Purses.scss | 3 + wallet/src/components/Purses.tsx | 69 ++-- wallet/src/service/Offers.ts | 1 + wallet/src/util/Icons.ts | 1 + wallet/src/util/WalletBackendAdapter.ts | 1 + wallet/src/util/ibc-assets.ts | 47 +++ wallet/src/util/well-known-petnames.ts | 3 + 11 files changed, 596 insertions(+), 32 deletions(-) create mode 100644 wallet/src/components/IbcTransfer.tsx create mode 100644 wallet/src/util/ibc-assets.ts create mode 100644 wallet/src/util/well-known-petnames.ts diff --git a/wallet/src/components/BrandIcon.tsx b/wallet/src/components/BrandIcon.tsx index 52e03894..24674057 100644 --- a/wallet/src/components/BrandIcon.tsx +++ b/wallet/src/components/BrandIcon.tsx @@ -1,12 +1,14 @@ import Box from '@mui/material/Box'; import { icons, defaultIcon } from '../util/Icons'; +import type { Petname } from '@agoric/smart-wallet/src/types'; interface Props { - brandPetname: string; + brandPetname: Petname; } const BrandIcon = ({ brandPetname }: Props) => { - const src = icons[brandPetname] ?? defaultIcon; + const src = + (typeof brandPetname === 'string' && icons[brandPetname]) || defaultIcon; const icon = src ? ( icon diff --git a/wallet/src/components/IbcTransfer.tsx b/wallet/src/components/IbcTransfer.tsx new file mode 100644 index 00000000..a418d745 --- /dev/null +++ b/wallet/src/components/IbcTransfer.tsx @@ -0,0 +1,491 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { useEffect, useMemo, useState } from 'react'; +import Button from '@mui/material/Button'; +import TextField from '@mui/material/TextField'; +import InputAdornment from '@mui/material/InputAdornment'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import { parseAsValue } from '@agoric/ui-components'; +import { withApplicationContext } from '../contexts/Application'; +import Petname from './Petname'; +import { ibcAssets } from '../util/ibc-assets'; +import { stringifyPurseValue } from '@agoric/ui-components'; +import ArrowDownward from '@mui/icons-material/ArrowDownward'; +import { Box } from '@mui/system'; +import { fromBech32 } from '@cosmjs/encoding'; +import { queryBankBalances } from '../util/queryBankBalances'; +import type { PurseInfo } from '../service/Offers'; +import type { KeplrUtils } from '../contexts/Provider'; +import { SigningStargateClient } from '@cosmjs/stargate'; +import { CircularProgress, Link, Snackbar, Typography } from '@mui/material'; + +export enum IbcDirection { + Deposit, + Withdrawal, +} + +const titlePreposition = { + [IbcDirection.Deposit]: 'from', + [IbcDirection.Withdrawal]: 'to', +}; + +const agoricAddressLabel = { + [IbcDirection.Deposit]: 'To', + [IbcDirection.Withdrawal]: 'From', +}; + +const remoteChainAddressLabel = { + [IbcDirection.Deposit]: 'From', + [IbcDirection.Withdrawal]: 'To', +}; + +interface Params { + purse?: PurseInfo; + direction: IbcDirection; + handleClose: () => void; + keplrConnection: KeplrUtils; +} + +const secondsUntilTimeout = 300; + +const timeoutTimestampSeconds = () => + Math.round(Date.now() / 1000) + secondsUntilTimeout; + +const agoricChainId = 'agoric-3'; +const agoricRpc = 'https://agoric-rpc.stakely.io/'; + +// Exported for testing only. +export const IbcTransferInternal = ({ + purse, + handleClose, + direction, + keplrConnection, +}: Params) => { + const ibcAsset = + typeof purse?.brandPetname === 'string' + ? ibcAssets[purse.brandPetname] + : undefined; + + const purseBalance = purse?.currentAmount.value; + + const [inProgress, setInProgress] = useState(false); + const [error, setError] = useState(''); + const [amount, setAmount] = useState(''); + const [remoteChainAddress, setRemoteChainAddress] = useState(''); + const [remoteChainBalance, setRemoteChainBalance] = useState( + null, + ); + const [remoteChainSigner, setRemoteChainSigner] = useState(null); + + const [isSnackbarOpen, setIsSnackbarOpen] = useState(false); + const handleCloseSnackbar = _ => { + setIsSnackbarOpen(false); + }; + const [snackbarMessage, setSnackbarMessage] = useState(''); + const showSnackbar = msg => { + setSnackbarMessage(msg); + setIsSnackbarOpen(true); + }; + + const isRemoteChainAddressInvalid = useMemo(() => { + if (!remoteChainAddress) return true; + + try { + const { prefix } = fromBech32(remoteChainAddress); + return prefix !== ibcAsset?.chainInfo.addressPrefix; + } catch (e) { + return true; + } + }, [remoteChainAddress, ibcAsset]); + + useEffect(() => { + setRemoteChainBalance(null); + if (isRemoteChainAddressInvalid || !ibcAsset) return; + + let isCancelled = false; + + const doQuery = async () => { + const balances = await queryBankBalances( + remoteChainAddress, + /* @ts-expect-error rpc string */ + ibcAsset.chainInfo.rpc, + ); + if (isCancelled) return; + + const balance = balances.find( + ({ denom }) => denom === ibcAsset.deposit.denom, + ); + setRemoteChainBalance(balance ? BigInt(balance.amount) : 0n); + }; + + void doQuery(); + + return () => { + isCancelled = true; + }; + }, [remoteChainAddress, isRemoteChainAddressInvalid, ibcAsset]); + + const handleAmountChange = e => { + setAmount(e.target.value); + }; + + const fillFromKeplr = async () => { + assert(ibcAsset); + // @ts-expect-error window keys + const { keplr } = window; + const offlineSigner = await keplr.getOfflineSignerOnlyAmino( + ibcAsset.chainInfo.chainId, + ); + const accounts = await offlineSigner.getAccounts(); + setRemoteChainAddress(accounts[0].address); + setRemoteChainSigner(offlineSigner); + }; + + const isAmountInvalid = useMemo(() => { + switch (direction) { + case IbcDirection.Withdrawal: + try { + const val = parseAsValue( + amount, + purse?.displayInfo?.assetKind, + purse?.displayInfo?.decimalPlaces, + ); + return val === 0n || val > (purseBalance ?? 0n); + } catch { + return true; + } + case IbcDirection.Deposit: + try { + const val = parseAsValue( + amount, + purse?.displayInfo?.assetKind, + purse?.displayInfo?.decimalPlaces, + ); + return val === 0n || val > (remoteChainBalance ?? 0n); + } catch { + return true; + } + } + }, [amount, purse, purseBalance]); + + const close = () => { + setAmount(''); + setRemoteChainAddress(''); + setRemoteChainSigner(null); + setError(''); + handleClose(); + }; + + const send = async () => { + setError(''); + + let val; + try { + val = String( + parseAsValue( + amount, + purse?.displayInfo?.assetKind, + purse?.displayInfo?.decimalPlaces, + ), + ); + } catch (e) { + setError(String(e)); + } + + assert(ibcAsset); + + if (direction === IbcDirection.Deposit) { + assert(remoteChainSigner && keplrConnection); + const { chainInfo, deposit } = ibcAsset; + const { sourceChannel, sourcePort, denom, gas } = deposit; + assert(gas); + + setInProgress(true); + const signer = await SigningStargateClient.connectWithSigner( + chainInfo.rpc, + remoteChainSigner, + ); + try { + const res = await signer.sendIbcTokens( + remoteChainAddress, + keplrConnection.address, + { + amount: val, + denom: denom, + }, + sourcePort, + sourceChannel, + undefined, + timeoutTimestampSeconds(), + { + amount: [{ amount: '0', denom }], + gas, + }, + ); + close(); + showSnackbar( + <> + Successfully executed transaction{' '} + + ...{res.transactionHash.slice(res.transactionHash.length - 12)} + + , + ); + } catch (e) { + setError(String(e)); + } finally { + setInProgress(false); + } + } else { + /* Is Withdrawal */ + setInProgress(true); + const { + withdraw: { denom, sourceChannel, sourcePort }, + } = ibcAsset; + + // @ts-expect-error window keys + const { keplr } = window; + const offlineSigner = await keplr.getOfflineSignerOnlyAmino( + agoricChainId, + ); + const client = await SigningStargateClient.connectWithSigner( + agoricRpc, + offlineSigner, + ); + + try { + const res = await client.sendIbcTokens( + keplrConnection.address, + remoteChainAddress, + { + amount: val, + denom, + }, + sourcePort, + sourceChannel, + undefined, + timeoutTimestampSeconds(), + { + amount: [{ amount: '0', denom: 'uist' }], + gas: '300000', + }, + ); + close(); + showSnackbar( + <> + Successfully executed transaction{' '} + + ...{res.transactionHash.slice(res.transactionHash.length - 12)} + + , + ); + } catch (e) { + console.log('error', e); + setError(String(e)); + } finally { + setInProgress(false); + } + } + }; + + const agoricAddressInfo = ( + + Balance Available:{' '} + {purse?.currentAmount && + purse?.displayInfo && + stringifyPurseValue({ + value: purseBalance, + displayInfo: purse?.displayInfo, + })} + + } + /> + ); + + const remoteChainAddressInfo = remoteChainSigner ? ( + + Balance Available:{' '} + {purse?.currentAmount && + purse?.displayInfo && + stringifyPurseValue({ + value: remoteChainBalance, + displayInfo: purse?.displayInfo, + })} + + ) : ( + 'Fetching balance...' + ) + } + /> + ) : ( + + + + ); + + const sender = (() => { + switch (direction) { + case IbcDirection.Withdrawal: + return agoricAddressInfo; + case IbcDirection.Deposit: + return remoteChainAddressInfo; + } + })(); + + const remoteChainAddressInput = ( + setRemoteChainAddress(e.target.value)} + disabled={direction === IbcDirection.Deposit} + error={!!isRemoteChainAddressInvalid} + helperText={ + isRemoteChainAddressInvalid ? ( + 'Invalid Address' + ) : remoteChainBalance !== null ? ( + <> + Balance Available:{' '} + {purse?.currentAmount && + purse?.displayInfo && + stringifyPurseValue({ + value: remoteChainBalance, + displayInfo: purse?.displayInfo, + })} + + ) : ( + 'Fetching balance...' + ) + } + InputProps={{ + type: 'text', + autoComplete: 'off', + endAdornment: isRemoteChainAddressInvalid && ( + + + + ), + }} + /> + ); + + const recipient = (() => { + switch (direction) { + case IbcDirection.Deposit: + return agoricAddressInfo; + case IbcDirection.Withdrawal: + return remoteChainAddressInput; + } + })(); + + const isAmountInputDisabled = + direction === IbcDirection.Deposit && remoteChainBalance === null; + + return ( + <> + + + IBC Transfer {titlePreposition[direction]}{' '} + {ibcAsset?.chainInfo.chainName} + + + {sender} +
+ + + + ), + }} + /> +
+ + + + {recipient} + {error} +
+ + {inProgress ? ( + + ) : ( + <> + {/* @ts-expect-error 'cancel' is part of our theme */} + + + + )} + +
+ + + ); +}; + +export default withApplicationContext(IbcTransferInternal, context => ({ + keplrConnection: context.keplrConnection, +})); diff --git a/wallet/src/components/Petname.tsx b/wallet/src/components/Petname.tsx index e5be20e2..efdf77de 100644 --- a/wallet/src/components/Petname.tsx +++ b/wallet/src/components/Petname.tsx @@ -1,4 +1,5 @@ import clsx from 'clsx'; +import { wellKnownPetnames } from '../util/well-known-petnames'; import './Petname.scss'; @@ -17,7 +18,7 @@ const Petname = ({ name, plural = false, color = true }) => { return ( - {name} + {wellKnownPetnames[name] ?? name} {plural && 's'} ); diff --git a/wallet/src/components/PurseAmount.tsx b/wallet/src/components/PurseAmount.tsx index 7fb8d226..4e0f932f 100644 --- a/wallet/src/components/PurseAmount.tsx +++ b/wallet/src/components/PurseAmount.tsx @@ -3,9 +3,10 @@ import PurseValue from './PurseValue'; import BrandIcon from './BrandIcon'; import type { AdditionalDisplayInfo } from '@agoric/ertp/src/types'; +import type { Petname as PetnameType } from '@agoric/smart-wallet/src/types'; interface Props { - brandPetname: string; + brandPetname: PetnameType; pursePetname: string; value: any; displayInfo: AdditionalDisplayInfo; diff --git a/wallet/src/components/Purses.scss b/wallet/src/components/Purses.scss index f65cfdd5..5dd036ad 100644 --- a/wallet/src/components/Purses.scss +++ b/wallet/src/components/Purses.scss @@ -12,6 +12,9 @@ .Right { float: right; + display: flex; + flex-direction: row; + gap: 8px; } .PurseProgressWrapper { diff --git a/wallet/src/components/Purses.tsx b/wallet/src/components/Purses.tsx index 5d56af25..ea3fba25 100644 --- a/wallet/src/components/Purses.tsx +++ b/wallet/src/components/Purses.tsx @@ -1,30 +1,38 @@ import { useState } from 'react'; -import { CircularProgress } from '@mui/material'; +import ArrowDownward from '@mui/icons-material/ArrowDownward'; +import ArrowUpward from '@mui/icons-material/ArrowUpward'; import Button from '@mui/material/Button'; -import Transfer from './Transfer'; +import IbcTransfer, { IbcDirection } from './IbcTransfer'; import PurseAmount from './PurseAmount'; import { withApplicationContext } from '../contexts/Application'; import CardItem from './CardItem'; import Card from './Card'; import ErrorBoundary from './ErrorBoundary'; import Loading from './Loading'; +import { ibcAssets } from '../util/ibc-assets'; +import type { PurseInfo } from '../service/Offers'; import './Purses.scss'; +interface TransferPurse { + purse?: PurseInfo; + direction?: IbcDirection; +} + // Exported for testing only. -export const PursesWithoutContext = ({ - purses, - pendingTransfers, - previewEnabled, -}: any) => { - const [openPurse, setOpenPurse] = useState(null); +export const PursesWithoutContext = ({ purses }: any) => { + const [transferPurse, setTransferPurse] = useState({}); + + const handleClickDeposit = purse => { + setTransferPurse({ purse, direction: IbcDirection.Deposit }); + }; - const handleClickOpen = purse => { - setOpenPurse(purse); + const handleClickWithdraw = purse => { + setTransferPurse({ purse, direction: IbcDirection.Withdrawal }); }; const handleClose = () => { - setOpenPurse(null); + setTransferPurse({}); }; const Purse = purse => { @@ -40,21 +48,24 @@ export const PursesWithoutContext = ({ /> - {previewEnabled && ( + {ibcAssets[purse.brandPetname] && (
- {pendingTransfers.has(purse.id) ? ( -
- -
- ) : ( - - )} + +
)} @@ -83,13 +94,15 @@ export const PursesWithoutContext = ({ {purseItems} - + ); }; export default withApplicationContext(PursesWithoutContext, context => ({ purses: context.purses, - pendingTransfers: context.pendingTransfers, - previewEnabled: context.previewEnabled, })); diff --git a/wallet/src/service/Offers.ts b/wallet/src/service/Offers.ts index a7b0ae3f..6b437c7e 100644 --- a/wallet/src/service/Offers.ts +++ b/wallet/src/service/Offers.ts @@ -30,6 +30,7 @@ export type PurseInfo = { brandPetname?: Petname; pursePetname?: Petname; displayInfo?: DisplayInfo; + denom?: string; }; type GiveOrWantEntries = { diff --git a/wallet/src/util/Icons.ts b/wallet/src/util/Icons.ts index c2b7cc5b..9eadc669 100644 --- a/wallet/src/util/Icons.ts +++ b/wallet/src/util/Icons.ts @@ -2,6 +2,7 @@ export const icons = { IST: 'tokens/IST.png', BLD: 'tokens/BLD.svg', ATOM: 'tokens/cosmos.svg', + IbcATOM: 'tokens/cosmos.svg', LINK: 'tokens/chainlink.png', USDC: 'tokens/usdc.svg', USDC_grv: 'tokens/USDC_grv.webp', diff --git a/wallet/src/util/WalletBackendAdapter.ts b/wallet/src/util/WalletBackendAdapter.ts index da03d56f..9a5b704f 100644 --- a/wallet/src/util/WalletBackendAdapter.ts +++ b/wallet/src/util/WalletBackendAdapter.ts @@ -251,6 +251,7 @@ export const makeWalletBridgeFromFollowers = ( brandPetname: info.issuerName, pursePetname: info.issuerName, displayInfo: info.displayInfo, + denom, }; brandToPurse.set(info.brand, purseInfo); }); diff --git a/wallet/src/util/ibc-assets.ts b/wallet/src/util/ibc-assets.ts new file mode 100644 index 00000000..036233ac --- /dev/null +++ b/wallet/src/util/ibc-assets.ts @@ -0,0 +1,47 @@ +type ChainInfo = { + chainName: string; + chainId: string; + rpc: string; + addressPrefix: string; + explorerPath: string; +}; + +type AssetInfo = { + sourcePort: string; + sourceChannel: string; + denom: string; + gas?: string; +}; + +type IbcAssets = Record< + string, + { + chainInfo: ChainInfo; + deposit: AssetInfo; + withdraw: AssetInfo; + } +>; + +export const ibcAssets: IbcAssets = { + IbcATOM: { + chainInfo: { + chainName: 'Cosmos Hub', + chainId: 'cosmoshub-4', + rpc: 'https://cosmoshub-rpc.stakely.io/', + addressPrefix: 'cosmos', + explorerPath: 'cosmos', + }, + deposit: { + sourcePort: 'transfer', + sourceChannel: 'channel-405', + denom: 'uatom', + gas: '100000', + }, + withdraw: { + sourcePort: 'transfer', + sourceChannel: 'channel-5', + denom: + 'ibc/BA313C4A19DFBF943586C0387E6B11286F9E416B4DD27574E6909CABE0E342FA', + }, + }, +}; diff --git a/wallet/src/util/well-known-petnames.ts b/wallet/src/util/well-known-petnames.ts new file mode 100644 index 00000000..b7f2c5ac --- /dev/null +++ b/wallet/src/util/well-known-petnames.ts @@ -0,0 +1,3 @@ +export const wellKnownPetnames = { + IbcATOM: 'ATOM', +}; From 3b6a850e41dc748aaf662f5d238dc8a10f092d0d Mon Sep 17 00:00:00 2001 From: samsiegart Date: Mon, 13 Mar 2023 21:36:11 -0700 Subject: [PATCH 02/11] fix: remove old tests --- wallet/src/components/tests/Purses.test.tsx | 36 +-------------------- 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/wallet/src/components/tests/Purses.test.tsx b/wallet/src/components/tests/Purses.test.tsx index 8910fbf0..395c249d 100644 --- a/wallet/src/components/tests/Purses.test.tsx +++ b/wallet/src/components/tests/Purses.test.tsx @@ -58,20 +58,12 @@ const purses = [ }, ]; -const pendingTransfers = new Set([0]); - const withApplicationContext = (Component, _) => ({ ...props }) => { // Test the preview features props.previewEnabled = true; - return ( - - ); + return ; }; jest.mock('../../contexts/Application', () => { @@ -96,17 +88,6 @@ test('renders the purse amounts', () => { expect(component.find(PurseAmount)).toHaveLength(2); }); -test('renders a loading indicator over pending transfers', () => { - const component = mount( - - - , - ); - - expect(component.find(CircularProgress)).toHaveLength(1); - expect(component.find(Button)).toHaveLength(1); -}); - test('renders a loading indicator when purses is null', () => { const component = mount( @@ -117,18 +98,3 @@ test('renders a loading indicator when purses is null', () => { expect(component.find(Loading)).toHaveLength(1); expect(component.find(Button)).toHaveLength(0); }); - -test('opens the transfer dialog when the button is clicked', async () => { - const component = mount( - - - , - ); - - const firstSendButton = component.find(Button).get(0); - await act(async () => firstSendButton.props.onClick()); - component.update(); - - const transfer = component.find(Transfer); - expect(transfer.props().purse).toEqual(purses[1]); -}); From fb94499a56b197bdbec704efa17e13c124d17e4e Mon Sep 17 00:00:00 2001 From: samsiegart Date: Tue, 14 Mar 2023 11:50:05 -0700 Subject: [PATCH 03/11] feat: error toast, feature flag, and refactor --- wallet/src/components/IbcTransfer.tsx | 298 ++++++++++---------- wallet/src/components/Purses.tsx | 23 +- wallet/src/components/tests/Purses.test.tsx | 6 +- wallet/src/util/Icons.ts | 1 - wallet/src/util/WalletBackendAdapter.ts | 6 +- wallet/src/util/ibc-assets.ts | 23 +- wallet/src/util/ibcTransfer.ts | 64 +++++ 7 files changed, 256 insertions(+), 165 deletions(-) create mode 100644 wallet/src/util/ibcTransfer.ts diff --git a/wallet/src/components/IbcTransfer.tsx b/wallet/src/components/IbcTransfer.tsx index a418d745..e48728cc 100644 --- a/wallet/src/components/IbcTransfer.tsx +++ b/wallet/src/components/IbcTransfer.tsx @@ -9,23 +9,35 @@ import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; import { parseAsValue } from '@agoric/ui-components'; import { withApplicationContext } from '../contexts/Application'; -import Petname from './Petname'; import { ibcAssets } from '../util/ibc-assets'; import { stringifyPurseValue } from '@agoric/ui-components'; import ArrowDownward from '@mui/icons-material/ArrowDownward'; import { Box } from '@mui/system'; import { fromBech32 } from '@cosmjs/encoding'; import { queryBankBalances } from '../util/queryBankBalances'; +import { assertIsDeliverTxSuccess } from '@cosmjs/stargate'; +import { CircularProgress, Link, Snackbar, Typography } from '@mui/material'; +import Petname from './Petname'; import type { PurseInfo } from '../service/Offers'; import type { KeplrUtils } from '../contexts/Provider'; -import { SigningStargateClient } from '@cosmjs/stargate'; -import { CircularProgress, Link, Snackbar, Typography } from '@mui/material'; +import type { Petname as PetnameType } from '@agoric/smart-wallet/src/types'; +import { sendIbcTokens, withdrawIbcTokens } from '../util/ibcTransfer'; export enum IbcDirection { Deposit, Withdrawal, } +const unmodifiableAddressStyle = { + width: 420, + '& .Mui-disabled': { + color: 'rgba(0,0,0,0.6)', + }, + '& input.Mui-disabled': { + color: 'rgba(0,0,0,0.86)', + }, +}; + const titlePreposition = { [IbcDirection.Deposit]: 'from', [IbcDirection.Withdrawal]: 'to', @@ -48,45 +60,28 @@ interface Params { keplrConnection: KeplrUtils; } -const secondsUntilTimeout = 300; - -const timeoutTimestampSeconds = () => - Math.round(Date.now() / 1000) + secondsUntilTimeout; +const agoricExplorerPath = 'agoric'; -const agoricChainId = 'agoric-3'; -const agoricRpc = 'https://agoric-rpc.stakely.io/'; - -// Exported for testing only. -export const IbcTransferInternal = ({ - purse, - handleClose, - direction, - keplrConnection, -}: Params) => { +const useRemoteChainAccount = (brandPetname?: PetnameType) => { const ibcAsset = - typeof purse?.brandPetname === 'string' - ? ibcAssets[purse.brandPetname] - : undefined; + typeof brandPetname === 'string' ? ibcAssets[brandPetname] : undefined; - const purseBalance = purse?.currentAmount.value; - - const [inProgress, setInProgress] = useState(false); - const [error, setError] = useState(''); - const [amount, setAmount] = useState(''); const [remoteChainAddress, setRemoteChainAddress] = useState(''); + const [remoteChainSigner, setRemoteChainSigner] = useState(null); const [remoteChainBalance, setRemoteChainBalance] = useState( null, ); - const [remoteChainSigner, setRemoteChainSigner] = useState(null); - const [isSnackbarOpen, setIsSnackbarOpen] = useState(false); - const handleCloseSnackbar = _ => { - setIsSnackbarOpen(false); - }; - const [snackbarMessage, setSnackbarMessage] = useState(''); - const showSnackbar = msg => { - setSnackbarMessage(msg); - setIsSnackbarOpen(true); + const connectWithKeplr = async () => { + assert(ibcAsset); + // @ts-expect-error window keys + const { keplr } = window; + const offlineSigner = await keplr.getOfflineSignerOnlyAmino( + ibcAsset.chainInfo.chainId, + ); + const accounts = await offlineSigner.getAccounts(); + setRemoteChainAddress(accounts[0].address); + setRemoteChainSigner(offlineSigner); }; const isRemoteChainAddressInvalid = useMemo(() => { @@ -95,7 +90,7 @@ export const IbcTransferInternal = ({ try { const { prefix } = fromBech32(remoteChainAddress); return prefix !== ibcAsset?.chainInfo.addressPrefix; - } catch (e) { + } catch { return true; } }, [remoteChainAddress, ibcAsset]); @@ -106,7 +101,7 @@ export const IbcTransferInternal = ({ let isCancelled = false; - const doQuery = async () => { + const loadRemoteChainBalance = async () => { const balances = await queryBankBalances( remoteChainAddress, /* @ts-expect-error rpc string */ @@ -117,30 +112,86 @@ export const IbcTransferInternal = ({ const balance = balances.find( ({ denom }) => denom === ibcAsset.deposit.denom, ); + setRemoteChainBalance(balance ? BigInt(balance.amount) : 0n); }; - void doQuery(); + void loadRemoteChainBalance(); return () => { isCancelled = true; }; }, [remoteChainAddress, isRemoteChainAddressInvalid, ibcAsset]); - const handleAmountChange = e => { - setAmount(e.target.value); + return { + remoteChainBalance, + isRemoteChainAddressInvalid, + connectWithKeplr, + remoteChainAddress, + remoteChainSigner, + setRemoteChainAddress: (address: string) => { + setRemoteChainAddress(address); + setRemoteChainSigner(null); + }, }; +}; - const fillFromKeplr = async () => { - assert(ibcAsset); - // @ts-expect-error window keys - const { keplr } = window; - const offlineSigner = await keplr.getOfflineSignerOnlyAmino( - ibcAsset.chainInfo.chainId, +// Exported for testing only. +export const IbcTransferInternal = ({ + purse, + handleClose, + direction, + keplrConnection, +}: Params) => { + const ibcAsset = + typeof purse?.brandPetname === 'string' + ? ibcAssets[purse.brandPetname] + : undefined; + + const purseBalance = purse?.currentAmount.value; + + const [inProgress, setInProgress] = useState(false); + const [error, setError] = useState(''); + const [amount, setAmount] = useState(''); + const [isSnackbarOpen, setIsSnackbarOpen] = useState(false); + + const handleCloseSnackbar = _ => { + setIsSnackbarOpen(false); + }; + + const [snackbarMessage, setSnackbarMessage] = useState(<>); + + const showSnackbar = ( + isSuccess: boolean, + explorerPath: string, + transactionHash: string, + ) => { + setSnackbarMessage( + <> + Transaction {isSuccess ? 'succeeded' : 'failed'}:{' '} + + ...{transactionHash.slice(transactionHash.length - 12)} + + , ); - const accounts = await offlineSigner.getAccounts(); - setRemoteChainAddress(accounts[0].address); - setRemoteChainSigner(offlineSigner); + setIsSnackbarOpen(true); + }; + + const { + connectWithKeplr, + setRemoteChainAddress, + isRemoteChainAddressInvalid, + remoteChainBalance, + remoteChainAddress, + remoteChainSigner, + } = useRemoteChainAccount(purse?.brandPetname); + + const handleAmountChange = e => { + setAmount(e.target.value); }; const isAmountInvalid = useMemo(() => { @@ -171,18 +222,19 @@ export const IbcTransferInternal = ({ }, [amount, purse, purseBalance]); const close = () => { + setInProgress(false); + setError(''); setAmount(''); setRemoteChainAddress(''); - setRemoteChainSigner(null); - setError(''); handleClose(); }; const send = async () => { setError(''); - let val; + let val: string; try { + assert(ibcAsset); val = String( parseAsValue( amount, @@ -192,120 +244,73 @@ export const IbcTransferInternal = ({ ); } catch (e) { setError(String(e)); + return; } - assert(ibcAsset); - - if (direction === IbcDirection.Deposit) { - assert(remoteChainSigner && keplrConnection); - const { chainInfo, deposit } = ibcAsset; - const { sourceChannel, sourcePort, denom, gas } = deposit; - assert(gas); - - setInProgress(true); - const signer = await SigningStargateClient.connectWithSigner( - chainInfo.rpc, - remoteChainSigner, - ); - try { - const res = await signer.sendIbcTokens( + setInProgress(true); + try { + if (direction === IbcDirection.Deposit) { + assert(remoteChainSigner && keplrConnection); + const { + chainInfo: { rpc, gas, explorerPath }, + deposit, + } = ibcAsset; + + const res = await sendIbcTokens( + deposit, + rpc, + remoteChainSigner, + val, remoteChainAddress, keplrConnection.address, - { - amount: val, - denom: denom, - }, - sourcePort, - sourceChannel, - undefined, - timeoutTimestampSeconds(), - { - amount: [{ amount: '0', denom }], - gas, - }, + gas, ); + close(); - showSnackbar( - <> - Successfully executed transaction{' '} - - ...{res.transactionHash.slice(res.transactionHash.length - 12)} - - , - ); - } catch (e) { - setError(String(e)); - } finally { - setInProgress(false); - } - } else { - /* Is Withdrawal */ - setInProgress(true); - const { - withdraw: { denom, sourceChannel, sourcePort }, - } = ibcAsset; - - // @ts-expect-error window keys - const { keplr } = window; - const offlineSigner = await keplr.getOfflineSignerOnlyAmino( - agoricChainId, - ); - const client = await SigningStargateClient.connectWithSigner( - agoricRpc, - offlineSigner, - ); + try { + assertIsDeliverTxSuccess(res); + showSnackbar(true, explorerPath, res.transactionHash); + } catch { + showSnackbar(false, explorerPath, res.transactionHash); + } + } else { + // Is withdrawal. + const { withdraw } = ibcAsset; - try { - const res = await client.sendIbcTokens( + const res = await withdrawIbcTokens( + withdraw, + val, keplrConnection.address, remoteChainAddress, - { - amount: val, - denom, - }, - sourcePort, - sourceChannel, - undefined, - timeoutTimestampSeconds(), - { - amount: [{ amount: '0', denom: 'uist' }], - gas: '300000', - }, ); + close(); - showSnackbar( - <> - Successfully executed transaction{' '} - - ...{res.transactionHash.slice(res.transactionHash.length - 12)} - - , - ); - } catch (e) { - console.log('error', e); - setError(String(e)); - } finally { - setInProgress(false); + try { + assertIsDeliverTxSuccess(res); + showSnackbar(true, agoricExplorerPath, res.transactionHash); + } catch { + showSnackbar(false, agoricExplorerPath, res.transactionHash); + } } + } catch (e) { + setError(String(e)); + } finally { + setInProgress(false); } }; const agoricAddressInfo = ( Balance Available:{' '} @@ -322,12 +327,13 @@ export const IbcTransferInternal = ({ const remoteChainAddressInfo = remoteChainSigner ? ( @@ -346,7 +352,7 @@ export const IbcTransferInternal = ({ /> ) : ( - @@ -394,7 +400,7 @@ export const IbcTransferInternal = ({ autoComplete: 'off', endAdornment: isRemoteChainAddressInvalid && ( - diff --git a/wallet/src/components/Purses.tsx b/wallet/src/components/Purses.tsx index ea3fba25..d5b421f1 100644 --- a/wallet/src/components/Purses.tsx +++ b/wallet/src/components/Purses.tsx @@ -11,16 +11,28 @@ import ErrorBoundary from './ErrorBoundary'; import Loading from './Loading'; import { ibcAssets } from '../util/ibc-assets'; import type { PurseInfo } from '../service/Offers'; +import type { KeplrUtils } from '../contexts/Provider'; import './Purses.scss'; +import { agoricChainId } from '../util/ibcTransfer'; interface TransferPurse { purse?: PurseInfo; direction?: IbcDirection; } +interface Props { + purses: PurseInfo[] | null; + previewEnabled: boolean; + keplrConnection: KeplrUtils | null; +} + // Exported for testing only. -export const PursesWithoutContext = ({ purses }: any) => { +export const PursesWithoutContext = ({ + purses, + previewEnabled, + keplrConnection, +}: Props) => { const [transferPurse, setTransferPurse] = useState({}); const handleClickDeposit = purse => { @@ -36,6 +48,10 @@ export const PursesWithoutContext = ({ purses }: any) => { }; const Purse = purse => { + const shouldShowIbcTransferButtons = + (keplrConnection?.chainId === agoricChainId || previewEnabled) && + ibcAssets[purse.brandPetname]; + return (
@@ -48,7 +64,7 @@ export const PursesWithoutContext = ({ purses }: any) => { />
- {ibcAssets[purse.brandPetname] && ( + {shouldShowIbcTransferButtons && (
@@ -105,4 +121,5 @@ export const PursesWithoutContext = ({ purses }: any) => { export default withApplicationContext(PursesWithoutContext, context => ({ purses: context.purses, + previewEnabled: context.previewEnabled, })); diff --git a/wallet/src/components/tests/Purses.test.tsx b/wallet/src/components/tests/Purses.test.tsx index 395c249d..d65e907e 100644 --- a/wallet/src/components/tests/Purses.test.tsx +++ b/wallet/src/components/tests/Purses.test.tsx @@ -91,7 +91,11 @@ test('renders the purse amounts', () => { test('renders a loading indicator when purses is null', () => { const component = mount( - + , ); diff --git a/wallet/src/util/Icons.ts b/wallet/src/util/Icons.ts index 9eadc669..c2b7cc5b 100644 --- a/wallet/src/util/Icons.ts +++ b/wallet/src/util/Icons.ts @@ -2,7 +2,6 @@ export const icons = { IST: 'tokens/IST.png', BLD: 'tokens/BLD.svg', ATOM: 'tokens/cosmos.svg', - IbcATOM: 'tokens/cosmos.svg', LINK: 'tokens/chainlink.png', USDC: 'tokens/usdc.svg', USDC_grv: 'tokens/USDC_grv.webp', diff --git a/wallet/src/util/WalletBackendAdapter.ts b/wallet/src/util/WalletBackendAdapter.ts index 9a5b704f..493cf99c 100644 --- a/wallet/src/util/WalletBackendAdapter.ts +++ b/wallet/src/util/WalletBackendAdapter.ts @@ -31,6 +31,7 @@ import type { ValueFollowerElement } from '@agoric/casting/src/types'; import { queryBankBalances } from './queryBankBalances'; import type { Coin } from '@cosmjs/stargate'; import type { PurseInfo } from '../service/Offers'; +import { wellKnownPetnames } from './well-known-petnames'; const newId = kind => `${kind}${Math.random()}`; const POLL_INTERVAL_MS = 6000; @@ -244,12 +245,13 @@ export const makeWalletBridgeFromFollowers = ( // have any. This way it will show up on their asset list with the // deposit action available. const amount = bankMap.get(denom) ?? 0n; + const petname = wellKnownPetnames[info.issuerName] ?? info.issuerName; const purseInfo: PurseInfo = { brand: info.brand, currentAmount: AmountMath.make(info.brand, BigInt(amount)), - brandPetname: info.issuerName, - pursePetname: info.issuerName, + brandPetname: petname, + pursePetname: petname, displayInfo: info.displayInfo, denom, }; diff --git a/wallet/src/util/ibc-assets.ts b/wallet/src/util/ibc-assets.ts index 036233ac..675764e2 100644 --- a/wallet/src/util/ibc-assets.ts +++ b/wallet/src/util/ibc-assets.ts @@ -4,38 +4,37 @@ type ChainInfo = { rpc: string; addressPrefix: string; explorerPath: string; + gas: string; }; -type AssetInfo = { +export type AssetInfo = { sourcePort: string; sourceChannel: string; denom: string; - gas?: string; }; -type IbcAssets = Record< - string, - { - chainInfo: ChainInfo; - deposit: AssetInfo; - withdraw: AssetInfo; - } ->; +export type IbcAsset = { + chainInfo: ChainInfo; + deposit: AssetInfo; + withdraw: AssetInfo; +}; + +type IbcAssets = Record; export const ibcAssets: IbcAssets = { - IbcATOM: { + ATOM: { chainInfo: { chainName: 'Cosmos Hub', chainId: 'cosmoshub-4', rpc: 'https://cosmoshub-rpc.stakely.io/', addressPrefix: 'cosmos', explorerPath: 'cosmos', + gas: '100000', }, deposit: { sourcePort: 'transfer', sourceChannel: 'channel-405', denom: 'uatom', - gas: '100000', }, withdraw: { sourcePort: 'transfer', diff --git a/wallet/src/util/ibcTransfer.ts b/wallet/src/util/ibcTransfer.ts new file mode 100644 index 00000000..a2eb9b84 --- /dev/null +++ b/wallet/src/util/ibcTransfer.ts @@ -0,0 +1,64 @@ +import { OfflineSigner } from '@cosmjs/proto-signing'; +import { SigningStargateClient } from '@cosmjs/stargate'; +import { AssetInfo } from './ibc-assets'; + +const secondsUntilTimeout = 300; + +const timeoutTimestampSeconds = () => + Math.round(Date.now() / 1000) + secondsUntilTimeout; + +export const agoricChainId = 'agoric-3'; +const agoricRpc = 'https://agoric-rpc.stakely.io/'; +const agoricGas = '300000'; + +export const sendIbcTokens = async ( + assetInfo: AssetInfo, + rpc: string, + signer: OfflineSigner, + amount: string, + from: string, + to: string, + gas: string, +) => { + const { sourceChannel, sourcePort, denom } = assetInfo; + + const client = await SigningStargateClient.connectWithSigner(rpc, signer); + + return client.sendIbcTokens( + from, + to, + { + amount, + denom: denom, + }, + sourcePort, + sourceChannel, + undefined, + timeoutTimestampSeconds(), + { + amount: [{ amount: '0', denom }], + gas, + }, + ); +}; + +export const withdrawIbcTokens = async ( + assetInfo: AssetInfo, + amount: string, + from: string, + to: string, +) => { + // @ts-expect-error window keys + const { keplr } = window; + const signer = await keplr.getOfflineSignerOnlyAmino(agoricChainId); + + return sendIbcTokens( + assetInfo, + agoricRpc, + signer, + amount, + from, + to, + agoricGas, + ); +}; From 786a9ca91b98df11d9cc114aee2ccf03c5672535 Mon Sep 17 00:00:00 2001 From: samsiegart Date: Tue, 14 Mar 2023 15:24:49 -0700 Subject: [PATCH 04/11] fix: fix unmodifiable address color --- wallet/src/components/IbcTransfer.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/wallet/src/components/IbcTransfer.tsx b/wallet/src/components/IbcTransfer.tsx index e48728cc..97e7e0dc 100644 --- a/wallet/src/components/IbcTransfer.tsx +++ b/wallet/src/components/IbcTransfer.tsx @@ -35,6 +35,7 @@ const unmodifiableAddressStyle = { }, '& input.Mui-disabled': { color: 'rgba(0,0,0,0.86)', + '-webkit-text-fill-color': 'inherit', }, }; From f8c0e6223197fef3cc4d5ebf6ab032cf608ee364 Mon Sep 17 00:00:00 2001 From: samsiegart Date: Tue, 14 Mar 2023 15:38:35 -0700 Subject: [PATCH 05/11] chore: comments --- wallet/src/components/Purses.tsx | 4 ++++ wallet/src/util/ibc-assets.ts | 1 + 2 files changed, 5 insertions(+) diff --git a/wallet/src/components/Purses.tsx b/wallet/src/components/Purses.tsx index d5b421f1..1b9ec63a 100644 --- a/wallet/src/components/Purses.tsx +++ b/wallet/src/components/Purses.tsx @@ -48,6 +48,10 @@ export const PursesWithoutContext = ({ }; const Purse = purse => { + // Only enable IBC transfer when connected to mainnet since it only makes + // transactions on mainnet. Otherwise, you can force it to appear by + // typing setPreviewEnabled(true) in the console, but be cautious when + // signing transactions! const shouldShowIbcTransferButtons = (keplrConnection?.chainId === agoricChainId || previewEnabled) && ibcAssets[purse.brandPetname]; diff --git a/wallet/src/util/ibc-assets.ts b/wallet/src/util/ibc-assets.ts index 675764e2..7c0d1b83 100644 --- a/wallet/src/util/ibc-assets.ts +++ b/wallet/src/util/ibc-assets.ts @@ -39,6 +39,7 @@ export const ibcAssets: IbcAssets = { withdraw: { sourcePort: 'transfer', sourceChannel: 'channel-5', + // XXX This will be redundant once `agoricNames.vbankAssets` is published. denom: 'ibc/BA313C4A19DFBF943586C0387E6B11286F9E416B4DD27574E6909CABE0E342FA', }, From f82b930c5d338a6be604cea0bc1c9223de3d7127 Mon Sep 17 00:00:00 2001 From: samsiegart Date: Tue, 14 Mar 2023 15:42:11 -0700 Subject: [PATCH 06/11] chore: rename petname component --- wallet/src/components/IbcTransfer.tsx | 10 +++++----- wallet/src/components/Offer.tsx | 4 ++-- wallet/src/components/{Petname.tsx => PetnameSpan.tsx} | 4 ++-- wallet/src/components/Proposal.tsx | 10 +++++----- wallet/src/components/PurseAmount.tsx | 8 ++++---- wallet/src/components/PurseValue.tsx | 4 ++-- wallet/src/views/Issuers.tsx | 4 ++-- 7 files changed, 22 insertions(+), 22 deletions(-) rename wallet/src/components/{Petname.tsx => PetnameSpan.tsx} (83%) diff --git a/wallet/src/components/IbcTransfer.tsx b/wallet/src/components/IbcTransfer.tsx index 97e7e0dc..cb55a479 100644 --- a/wallet/src/components/IbcTransfer.tsx +++ b/wallet/src/components/IbcTransfer.tsx @@ -17,11 +17,11 @@ import { fromBech32 } from '@cosmjs/encoding'; import { queryBankBalances } from '../util/queryBankBalances'; import { assertIsDeliverTxSuccess } from '@cosmjs/stargate'; import { CircularProgress, Link, Snackbar, Typography } from '@mui/material'; -import Petname from './Petname'; +import PetnameSpan from './PetnameSpan'; +import { sendIbcTokens, withdrawIbcTokens } from '../util/ibcTransfer'; import type { PurseInfo } from '../service/Offers'; import type { KeplrUtils } from '../contexts/Provider'; -import type { Petname as PetnameType } from '@agoric/smart-wallet/src/types'; -import { sendIbcTokens, withdrawIbcTokens } from '../util/ibcTransfer'; +import type { Petname } from '@agoric/smart-wallet/src/types'; export enum IbcDirection { Deposit, @@ -63,7 +63,7 @@ interface Params { const agoricExplorerPath = 'agoric'; -const useRemoteChainAccount = (brandPetname?: PetnameType) => { +const useRemoteChainAccount = (brandPetname?: Petname) => { const ibcAsset = typeof brandPetname === 'string' ? ibcAssets[brandPetname] : undefined; @@ -452,7 +452,7 @@ export const IbcTransferInternal = ({ autoComplete: 'off', endAdornment: ( - + ), }} diff --git a/wallet/src/components/Offer.tsx b/wallet/src/components/Offer.tsx index dce8028e..a499a244 100644 --- a/wallet/src/components/Offer.tsx +++ b/wallet/src/components/Offer.tsx @@ -4,7 +4,7 @@ import CheckIcon from '@mui/icons-material/Check'; import CloseIcon from '@mui/icons-material/Close'; import { useState } from 'react'; import Request from './Request'; -import Petname from './Petname'; +import PetnameSpan from './PetnameSpan'; import { formatDateNow } from '../util/Date'; import { withApplicationContext } from '../contexts/Application'; import ErrorBoundary from './ErrorBoundary'; @@ -143,7 +143,7 @@ const OfferWithoutContext = ({ /> {formatDateNow(date)}
- + via {dappOrigin || origin}
diff --git a/wallet/src/components/Petname.tsx b/wallet/src/components/PetnameSpan.tsx similarity index 83% rename from wallet/src/components/Petname.tsx rename to wallet/src/components/PetnameSpan.tsx index efdf77de..9a6043ee 100644 --- a/wallet/src/components/Petname.tsx +++ b/wallet/src/components/PetnameSpan.tsx @@ -3,7 +3,7 @@ import { wellKnownPetnames } from '../util/well-known-petnames'; import './Petname.scss'; -const Petname = ({ name, plural = false, color = true }) => { +const PetnameSpan = ({ name, plural = false, color = true }) => { if (Array.isArray(name)) { return ( @@ -24,4 +24,4 @@ const Petname = ({ name, plural = false, color = true }) => { ); }; -export default Petname; +export default PetnameSpan; diff --git a/wallet/src/components/Proposal.tsx b/wallet/src/components/Proposal.tsx index 70facdc4..7eda79ea 100644 --- a/wallet/src/components/Proposal.tsx +++ b/wallet/src/components/Proposal.tsx @@ -4,7 +4,7 @@ import Link from '@mui/material/Link'; import Box from '@mui/material/Box'; import { Nat } from '@agoric/nat'; import { stringifyPurseValue } from '@agoric/ui-components'; -import Petname from './Petname'; +import PetnameSpan from './PetnameSpan'; import PurseValue from './PurseValue'; import { formatDateNow } from '../util/Date'; import { withApplicationContext } from '../contexts/Application'; @@ -36,7 +36,7 @@ const OfferEntryFromTemplate = ( displayInfo={purse.displayInfo} brandPetname={purse.brandPetname} /> - {type.move} + {type.move} @@ -59,7 +59,7 @@ const OfferEntryFromDisplayInfo = (type, [role, { amount, pursePetname }]) => { displayInfo={amount.displayInfo} brandPetname={amount.brand.petname} /> - {type.move} + {type.move} @@ -226,9 +226,9 @@ const Proposal = ({ offer, purses, swingsetParams, beansOwing }) => { value: fee.value, displayInfo: fee.displayInfo, })}{' '} - + - from + from diff --git a/wallet/src/components/PurseAmount.tsx b/wallet/src/components/PurseAmount.tsx index 4e0f932f..f737450f 100644 --- a/wallet/src/components/PurseAmount.tsx +++ b/wallet/src/components/PurseAmount.tsx @@ -1,12 +1,12 @@ -import Petname from './Petname'; +import PetnameSpan from './PetnameSpan'; import PurseValue from './PurseValue'; import BrandIcon from './BrandIcon'; import type { AdditionalDisplayInfo } from '@agoric/ertp/src/types'; -import type { Petname as PetnameType } from '@agoric/smart-wallet/src/types'; +import type { Petname } from '@agoric/smart-wallet/src/types'; interface Props { - brandPetname: PetnameType; + brandPetname: Petname; pursePetname: string; value: any; displayInfo: AdditionalDisplayInfo; @@ -22,7 +22,7 @@ const PurseAmount = ({
- + { @@ -201,7 +201,7 @@ const PurseValue = ({ value, displayInfo, brandPetname }) => { value, displayInfo, })}{' '} - + ); diff --git a/wallet/src/views/Issuers.tsx b/wallet/src/views/Issuers.tsx index e17a24fa..2455fbe3 100644 --- a/wallet/src/views/Issuers.tsx +++ b/wallet/src/views/Issuers.tsx @@ -10,7 +10,7 @@ import CardItem from '../components/CardItem'; import MakePurse from '../components/MakePurse'; import ImportIssuer from '../components/ImportIssuer'; import Loading from '../components/Loading'; -import Petname from '../components/Petname'; +import PetnameSpan from '../components/PetnameSpan'; import BrandIcon from '../components/BrandIcon'; import { withApplicationContext } from '../contexts/Application'; @@ -77,7 +77,7 @@ export const IssuersWithoutContext = ({
- +
Board ID: ({issuer.issuerBoardId})
From 2284911460c045ba73223546dd3a53db4303f7ff Mon Sep 17 00:00:00 2001 From: samsiegart Date: Tue, 14 Mar 2023 15:46:32 -0700 Subject: [PATCH 07/11] chore: clarify keplr api assumptions --- wallet/src/components/IbcTransfer.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/wallet/src/components/IbcTransfer.tsx b/wallet/src/components/IbcTransfer.tsx index cb55a479..5c3d6ede 100644 --- a/wallet/src/components/IbcTransfer.tsx +++ b/wallet/src/components/IbcTransfer.tsx @@ -80,7 +80,12 @@ const useRemoteChainAccount = (brandPetname?: Petname) => { const offlineSigner = await keplr.getOfflineSignerOnlyAmino( ibcAsset.chainInfo.chainId, ); + const accounts = await offlineSigner.getAccounts(); + if (accounts.length > 1) { + // Currently, Keplr extension manages only one address/public key pair. + console.warn('Got multiple accounts from Keplr. Using first of list.'); + } setRemoteChainAddress(accounts[0].address); setRemoteChainSigner(offlineSigner); }; From 7c54c9b9d877d73afa5db6732bd0685a0be904ca Mon Sep 17 00:00:00 2001 From: samsiegart Date: Tue, 14 Mar 2023 16:01:32 -0700 Subject: [PATCH 08/11] chore: reverse boolean and explicit conditional --- wallet/src/components/IbcTransfer.tsx | 55 ++++++++++++++------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/wallet/src/components/IbcTransfer.tsx b/wallet/src/components/IbcTransfer.tsx index 5c3d6ede..58768ba3 100644 --- a/wallet/src/components/IbcTransfer.tsx +++ b/wallet/src/components/IbcTransfer.tsx @@ -90,20 +90,20 @@ const useRemoteChainAccount = (brandPetname?: Petname) => { setRemoteChainSigner(offlineSigner); }; - const isRemoteChainAddressInvalid = useMemo(() => { - if (!remoteChainAddress) return true; + const isRemoteChainAddressValid = useMemo(() => { + if (!remoteChainAddress) return false; try { const { prefix } = fromBech32(remoteChainAddress); - return prefix !== ibcAsset?.chainInfo.addressPrefix; + return prefix === ibcAsset?.chainInfo.addressPrefix; } catch { - return true; + return false; } }, [remoteChainAddress, ibcAsset]); useEffect(() => { setRemoteChainBalance(null); - if (isRemoteChainAddressInvalid || !ibcAsset) return; + if (!isRemoteChainAddressValid || !ibcAsset) return; let isCancelled = false; @@ -127,11 +127,11 @@ const useRemoteChainAccount = (brandPetname?: Petname) => { return () => { isCancelled = true; }; - }, [remoteChainAddress, isRemoteChainAddressInvalid, ibcAsset]); + }, [remoteChainAddress, isRemoteChainAddressValid, ibcAsset]); return { remoteChainBalance, - isRemoteChainAddressInvalid, + isRemoteChainAddressValid, connectWithKeplr, remoteChainAddress, remoteChainSigner, @@ -190,7 +190,7 @@ export const IbcTransferInternal = ({ const { connectWithKeplr, setRemoteChainAddress, - isRemoteChainAddressInvalid, + isRemoteChainAddressValid, remoteChainBalance, remoteChainAddress, remoteChainSigner, @@ -279,8 +279,7 @@ export const IbcTransferInternal = ({ } catch { showSnackbar(false, explorerPath, res.transactionHash); } - } else { - // Is withdrawal. + } else if (direction === IbcDirection.Withdrawal) { const { withdraw } = ibcAsset; const res = await withdrawIbcTokens( @@ -297,6 +296,8 @@ export const IbcTransferInternal = ({ } catch { showSnackbar(false, agoricExplorerPath, res.transactionHash); } + } else { + throw new Error('Unrecognized IBC transfer direction', direction); } } catch (e) { setError(String(e)); @@ -383,28 +384,30 @@ export const IbcTransferInternal = ({ value={remoteChainAddress} onChange={e => setRemoteChainAddress(e.target.value)} disabled={direction === IbcDirection.Deposit} - error={!!isRemoteChainAddressInvalid} + error={!isRemoteChainAddressValid} helperText={ - isRemoteChainAddressInvalid ? ( - 'Invalid Address' - ) : remoteChainBalance !== null ? ( - <> - Balance Available:{' '} - {purse?.currentAmount && - purse?.displayInfo && - stringifyPurseValue({ - value: remoteChainBalance, - displayInfo: purse?.displayInfo, - })} - + isRemoteChainAddressValid ? ( + remoteChainBalance !== null ? ( + <> + Balance Available:{' '} + {purse?.currentAmount && + purse?.displayInfo && + stringifyPurseValue({ + value: remoteChainBalance, + displayInfo: purse?.displayInfo, + })} + + ) : ( + 'Fetching balance...' + ) ) : ( - 'Fetching balance...' + 'Invalid Address' ) } InputProps={{ type: 'text', autoComplete: 'off', - endAdornment: isRemoteChainAddressInvalid && ( + endAdornment: !isRemoteChainAddressValid && ( From 0b45a3326fbbfe2121ef59dcf6db6babc58d52c7 Mon Sep 17 00:00:00 2001 From: samsiegart Date: Tue, 14 Mar 2023 16:04:32 -0700 Subject: [PATCH 09/11] chore: redundant property key --- wallet/src/util/ibcTransfer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wallet/src/util/ibcTransfer.ts b/wallet/src/util/ibcTransfer.ts index a2eb9b84..25ec40f4 100644 --- a/wallet/src/util/ibcTransfer.ts +++ b/wallet/src/util/ibcTransfer.ts @@ -29,7 +29,7 @@ export const sendIbcTokens = async ( to, { amount, - denom: denom, + denom, }, sourcePort, sourceChannel, From 326a27c21b1895069a3fb6ad04322fc19747348b Mon Sep 17 00:00:00 2001 From: samsiegart Date: Tue, 14 Mar 2023 16:09:05 -0700 Subject: [PATCH 10/11] chore: use util function instead of assert --- wallet/src/components/IbcTransfer.tsx | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/wallet/src/components/IbcTransfer.tsx b/wallet/src/components/IbcTransfer.tsx index 58768ba3..ad47ecee 100644 --- a/wallet/src/components/IbcTransfer.tsx +++ b/wallet/src/components/IbcTransfer.tsx @@ -15,7 +15,7 @@ import ArrowDownward from '@mui/icons-material/ArrowDownward'; import { Box } from '@mui/system'; import { fromBech32 } from '@cosmjs/encoding'; import { queryBankBalances } from '../util/queryBankBalances'; -import { assertIsDeliverTxSuccess } from '@cosmjs/stargate'; +import { isDeliverTxSuccess } from '@cosmjs/stargate'; import { CircularProgress, Link, Snackbar, Typography } from '@mui/material'; import PetnameSpan from './PetnameSpan'; import { sendIbcTokens, withdrawIbcTokens } from '../util/ibcTransfer'; @@ -273,12 +273,11 @@ export const IbcTransferInternal = ({ ); close(); - try { - assertIsDeliverTxSuccess(res); - showSnackbar(true, explorerPath, res.transactionHash); - } catch { - showSnackbar(false, explorerPath, res.transactionHash); - } + showSnackbar( + isDeliverTxSuccess(res), + explorerPath, + res.transactionHash, + ); } else if (direction === IbcDirection.Withdrawal) { const { withdraw } = ibcAsset; @@ -290,12 +289,11 @@ export const IbcTransferInternal = ({ ); close(); - try { - assertIsDeliverTxSuccess(res); - showSnackbar(true, agoricExplorerPath, res.transactionHash); - } catch { - showSnackbar(false, agoricExplorerPath, res.transactionHash); - } + showSnackbar( + isDeliverTxSuccess(res), + agoricExplorerPath, + res.transactionHash, + ); } else { throw new Error('Unrecognized IBC transfer direction', direction); } From 5cd04af8fe7333a0094fd31638985d6af9877d43 Mon Sep 17 00:00:00 2001 From: samsiegart Date: Wed, 15 Mar 2023 21:49:24 -0700 Subject: [PATCH 11/11] fix: use snackbar hook and eliminate double render --- wallet/src/components/IbcTransfer.tsx | 87 ++++++++++++++++++--------- 1 file changed, 59 insertions(+), 28 deletions(-) diff --git a/wallet/src/components/IbcTransfer.tsx b/wallet/src/components/IbcTransfer.tsx index ad47ecee..8bbda7b2 100644 --- a/wallet/src/components/IbcTransfer.tsx +++ b/wallet/src/components/IbcTransfer.tsx @@ -17,11 +17,14 @@ import { fromBech32 } from '@cosmjs/encoding'; import { queryBankBalances } from '../util/queryBankBalances'; import { isDeliverTxSuccess } from '@cosmjs/stargate'; import { CircularProgress, Link, Snackbar, Typography } from '@mui/material'; +import IconButton from '@mui/material/IconButton'; +import CloseIcon from '@mui/icons-material/Close'; import PetnameSpan from './PetnameSpan'; import { sendIbcTokens, withdrawIbcTokens } from '../util/ibcTransfer'; import type { PurseInfo } from '../service/Offers'; import type { KeplrUtils } from '../contexts/Provider'; import type { Petname } from '@agoric/smart-wallet/src/types'; +import * as React from 'react'; export enum IbcDirection { Deposit, @@ -32,10 +35,10 @@ const unmodifiableAddressStyle = { width: 420, '& .Mui-disabled': { color: 'rgba(0,0,0,0.6)', + '-webkit-text-fill-color': 'inherit', }, '& input.Mui-disabled': { color: 'rgba(0,0,0,0.86)', - '-webkit-text-fill-color': 'inherit', }, }; @@ -142,6 +145,47 @@ const useRemoteChainAccount = (brandPetname?: Petname) => { }; }; +const useSnackbar = () => { + const [{ isSnackbarOpen, snackbarMessage }, setSnackbarState] = useState({ + isSnackbarOpen: false, + snackbarMessage: <>, + }); + + const handleCloseSnackbar = ( + _event: React.SyntheticEvent | Event, + reason?: string, + ) => { + if (reason === 'clickaway') { + return; + } + + setSnackbarState(state => ({ ...state, isSnackbarOpen: false })); + }; + + const showSnackbar = ( + isSuccess: boolean, + explorerPath: string, + transactionHash: string, + ) => + setSnackbarState({ + snackbarMessage: ( + <> + Transaction {isSuccess ? 'succeeded' : 'failed'}:{' '} + + ...{transactionHash.slice(transactionHash.length - 12)} + + + ), + isSnackbarOpen: true, + }); + + return { isSnackbarOpen, snackbarMessage, handleCloseSnackbar, showSnackbar }; +}; + // Exported for testing only. export const IbcTransferInternal = ({ purse, @@ -159,33 +203,8 @@ export const IbcTransferInternal = ({ const [inProgress, setInProgress] = useState(false); const [error, setError] = useState(''); const [amount, setAmount] = useState(''); - const [isSnackbarOpen, setIsSnackbarOpen] = useState(false); - - const handleCloseSnackbar = _ => { - setIsSnackbarOpen(false); - }; - - const [snackbarMessage, setSnackbarMessage] = useState(<>); - - const showSnackbar = ( - isSuccess: boolean, - explorerPath: string, - transactionHash: string, - ) => { - setSnackbarMessage( - <> - Transaction {isSuccess ? 'succeeded' : 'failed'}:{' '} - - ...{transactionHash.slice(transactionHash.length - 12)} - - , - ); - setIsSnackbarOpen(true); - }; + const { showSnackbar, handleCloseSnackbar, isSnackbarOpen, snackbarMessage } = + useSnackbar(); const { connectWithKeplr, @@ -494,6 +513,18 @@ export const IbcTransferInternal = ({ anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }} onClose={handleCloseSnackbar} message={snackbarMessage} + action={ + + + + + + } /> );