-
+
{
@@ -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})