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..8bbda7b2 --- /dev/null +++ b/wallet/src/components/IbcTransfer.tsx @@ -0,0 +1,535 @@ +/* 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 { 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 { 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, + Withdrawal, +} + +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)', + }, +}; + +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 agoricExplorerPath = 'agoric'; + +const useRemoteChainAccount = (brandPetname?: Petname) => { + const ibcAsset = + typeof brandPetname === 'string' ? ibcAssets[brandPetname] : undefined; + + const [remoteChainAddress, setRemoteChainAddress] = useState(''); + const [remoteChainSigner, setRemoteChainSigner] = useState(null); + const [remoteChainBalance, setRemoteChainBalance] = useState( + null, + ); + + 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(); + 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); + }; + + const isRemoteChainAddressValid = useMemo(() => { + if (!remoteChainAddress) return false; + + try { + const { prefix } = fromBech32(remoteChainAddress); + return prefix === ibcAsset?.chainInfo.addressPrefix; + } catch { + return false; + } + }, [remoteChainAddress, ibcAsset]); + + useEffect(() => { + setRemoteChainBalance(null); + if (!isRemoteChainAddressValid || !ibcAsset) return; + + let isCancelled = false; + + const loadRemoteChainBalance = 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 loadRemoteChainBalance(); + + return () => { + isCancelled = true; + }; + }, [remoteChainAddress, isRemoteChainAddressValid, ibcAsset]); + + return { + remoteChainBalance, + isRemoteChainAddressValid, + connectWithKeplr, + remoteChainAddress, + remoteChainSigner, + setRemoteChainAddress: (address: string) => { + setRemoteChainAddress(address); + setRemoteChainSigner(null); + }, + }; +}; + +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, + 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 { showSnackbar, handleCloseSnackbar, isSnackbarOpen, snackbarMessage } = + useSnackbar(); + + const { + connectWithKeplr, + setRemoteChainAddress, + isRemoteChainAddressValid, + remoteChainBalance, + remoteChainAddress, + remoteChainSigner, + } = useRemoteChainAccount(purse?.brandPetname); + + const handleAmountChange = e => { + setAmount(e.target.value); + }; + + 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 = () => { + setInProgress(false); + setError(''); + setAmount(''); + setRemoteChainAddress(''); + handleClose(); + }; + + const send = async () => { + setError(''); + + let val: string; + try { + assert(ibcAsset); + val = String( + parseAsValue( + amount, + purse?.displayInfo?.assetKind, + purse?.displayInfo?.decimalPlaces, + ), + ); + } catch (e) { + setError(String(e)); + return; + } + + 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, + gas, + ); + + close(); + showSnackbar( + isDeliverTxSuccess(res), + explorerPath, + res.transactionHash, + ); + } else if (direction === IbcDirection.Withdrawal) { + const { withdraw } = ibcAsset; + + const res = await withdrawIbcTokens( + withdraw, + val, + keplrConnection.address, + remoteChainAddress, + ); + + close(); + showSnackbar( + isDeliverTxSuccess(res), + agoricExplorerPath, + res.transactionHash, + ); + } else { + throw new Error('Unrecognized IBC transfer direction', direction); + } + } catch (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={!isRemoteChainAddressValid} + helperText={ + isRemoteChainAddressValid ? ( + remoteChainBalance !== null ? ( + <> + Balance Available:{' '} + {purse?.currentAmount && + purse?.displayInfo && + stringifyPurseValue({ + value: remoteChainBalance, + displayInfo: purse?.displayInfo, + })} + + ) : ( + 'Fetching balance...' + ) + ) : ( + 'Invalid Address' + ) + } + InputProps={{ + type: 'text', + autoComplete: 'off', + endAdornment: !isRemoteChainAddressValid && ( + + + + ), + }} + /> + ); + + 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/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 65% rename from wallet/src/components/Petname.tsx rename to wallet/src/components/PetnameSpan.tsx index e5be20e2..9a6043ee 100644 --- a/wallet/src/components/Petname.tsx +++ b/wallet/src/components/PetnameSpan.tsx @@ -1,8 +1,9 @@ import clsx from 'clsx'; +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 ( @@ -17,10 +18,10 @@ const Petname = ({ name, plural = false, color = true }) => { return ( - {name} + {wellKnownPetnames[name] ?? name} {plural && 's'} ); }; -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 7fb8d226..f737450f 100644 --- a/wallet/src/components/PurseAmount.tsx +++ b/wallet/src/components/PurseAmount.tsx @@ -1,11 +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 } from '@agoric/smart-wallet/src/types'; interface Props { - brandPetname: string; + brandPetname: Petname; pursePetname: string; value: any; displayInfo: AdditionalDisplayInfo; @@ -21,7 +22,7 @@ const PurseAmount = ({
- + { @@ -201,7 +201,7 @@ const PurseValue = ({ value, displayInfo, brandPetname }) => { value, displayInfo, })}{' '} - + ); 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..1b9ec63a 100644 --- a/wallet/src/components/Purses.tsx +++ b/wallet/src/components/Purses.tsx @@ -1,33 +1,61 @@ 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 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, - pendingTransfers, previewEnabled, -}: any) => { - const [openPurse, setOpenPurse] = useState(null); + keplrConnection, +}: Props) => { + const [transferPurse, setTransferPurse] = useState({}); - const handleClickOpen = purse => { - setOpenPurse(purse); + const handleClickDeposit = purse => { + setTransferPurse({ purse, direction: IbcDirection.Deposit }); + }; + + const handleClickWithdraw = purse => { + setTransferPurse({ purse, direction: IbcDirection.Withdrawal }); }; const handleClose = () => { - setOpenPurse(null); + setTransferPurse({}); }; 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]; + return (
@@ -40,21 +68,24 @@ export const PursesWithoutContext = ({ />
- {previewEnabled && ( + {shouldShowIbcTransferButtons && (
- {pendingTransfers.has(purse.id) ? ( -
- -
- ) : ( - - )} + +
)}
@@ -83,13 +114,16 @@ export const PursesWithoutContext = ({ {purseItems} - +
); }; export default withApplicationContext(PursesWithoutContext, context => ({ purses: context.purses, - pendingTransfers: context.pendingTransfers, previewEnabled: context.previewEnabled, })); diff --git a/wallet/src/components/tests/Purses.test.tsx b/wallet/src/components/tests/Purses.test.tsx index 8910fbf0..d65e907e 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,39 +88,17 @@ 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( - + , ); 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]); -}); 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/WalletBackendAdapter.ts b/wallet/src/util/WalletBackendAdapter.ts index da03d56f..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,13 +245,15 @@ 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, }; 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..7c0d1b83 --- /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; + gas: string; +}; + +export type AssetInfo = { + sourcePort: string; + sourceChannel: string; + denom: string; +}; + +export type IbcAsset = { + chainInfo: ChainInfo; + deposit: AssetInfo; + withdraw: AssetInfo; +}; + +type IbcAssets = Record; + +export const ibcAssets: IbcAssets = { + 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', + }, + withdraw: { + sourcePort: 'transfer', + sourceChannel: 'channel-5', + // XXX This will be redundant once `agoricNames.vbankAssets` is published. + denom: + 'ibc/BA313C4A19DFBF943586C0387E6B11286F9E416B4DD27574E6909CABE0E342FA', + }, + }, +}; diff --git a/wallet/src/util/ibcTransfer.ts b/wallet/src/util/ibcTransfer.ts new file mode 100644 index 00000000..25ec40f4 --- /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, + }, + 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, + ); +}; 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', +}; 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})