From 2c7c09d7723a23e933fa1e7ff60a67c0f3f07bb3 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi Date: Wed, 8 Jan 2025 23:02:27 -0700 Subject: [PATCH 01/48] chore: remove hardcoded chainNames --- .../groups/components/groupControls.tsx | 9 +- components/groups/components/myGroups.tsx | 2 +- .../groups/forms/groups/ConfirmationForm.tsx | 6 +- components/groups/modals/voteModal.tsx | 8 +- config/defaults.ts | 104 ------------------ config/index.ts | 1 - pages/_app.tsx | 13 ++- 7 files changed, 22 insertions(+), 121 deletions(-) delete mode 100644 config/defaults.ts diff --git a/components/groups/components/groupControls.tsx b/components/groups/components/groupControls.tsx index a3be34c..8bbdc07 100644 --- a/components/groups/components/groupControls.tsx +++ b/components/groups/components/groupControls.tsx @@ -4,7 +4,6 @@ import { useTallyCount, useVotesByProposal, useMultipleTallyCounts, - ExtendedQueryGroupsByMemberResponseSDKType, ExtendedGroupType, } from '@/hooks/useQueries'; import { ProposalSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/types'; @@ -15,16 +14,14 @@ import { useRouter } from 'next/router'; import VoteDetailsModal from '@/components/groups/modals/voteDetailsModal'; import { useGroupsByMember } from '@/hooks/useQueries'; import { useChain } from '@cosmos-kit/react'; -import { - MemberSDKType, - GroupInfoSDKType, -} from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/types'; +import { MemberSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/types'; import { ArrowRightIcon } from '@/components/icons'; import ProfileAvatar from '@/utils/identicon'; import { HistoryBox, TransactionGroup } from '@/components'; import { TokenList } from '@/components'; import { CombinedBalanceInfo, ExtendedMetadataSDKType } from '@/utils'; import DenomList from '@/components/factory/components/DenomList'; +import env from '@/config/env'; type GroupControlsProps = { policyAddress: string; @@ -226,7 +223,7 @@ export default function GroupControls({ .trim(); } - const { address } = useChain('manifest'); + const { address } = useChain(env.chain); const { groupByMemberData } = useGroupsByMember(address ?? ''); useEffect(() => { diff --git a/components/groups/components/myGroups.tsx b/components/groups/components/myGroups.tsx index 1b91597..5563631 100644 --- a/components/groups/components/myGroups.tsx +++ b/components/groups/components/myGroups.tsx @@ -100,7 +100,7 @@ export function YourGroups({ const [selectedGroupName, setSelectedGroupName] = useState('Untitled Group'); const router = useRouter(); - const { address } = useChain('manifest'); + const { address } = useChain(env.chain); const filteredGroups = groups.groups.filter(group => { try { diff --git a/components/groups/forms/groups/ConfirmationForm.tsx b/components/groups/forms/groups/ConfirmationForm.tsx index c5dce76..93de035 100644 --- a/components/groups/forms/groups/ConfirmationForm.tsx +++ b/components/groups/forms/groups/ConfirmationForm.tsx @@ -6,7 +6,7 @@ import { cosmos } from '@liftedinit/manifestjs'; import { ThresholdDecisionPolicy } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/types'; import { Duration } from '@liftedinit/manifestjs/dist/codegen/google/protobuf/duration'; import { secondsToHumanReadable } from '@/utils/string'; - +import env from '@/config/env'; export default function ConfirmationForm({ nextStep, prevStep, @@ -30,8 +30,8 @@ export default function ConfirmationForm({ // Convert the object to a JSON string const jsonString = JSON.stringify(groupMetadata); - const { tx, isSigning, setIsSigning } = useTx('manifest'); - const { estimateFee } = useFeeEstimation('manifest'); + const { tx, isSigning, setIsSigning } = useTx(env.chain); + const { estimateFee } = useFeeEstimation(env.chain); const minExecutionPeriod: Duration = { seconds: BigInt(0), diff --git a/components/groups/modals/voteModal.tsx b/components/groups/modals/voteModal.tsx index d19e13a..c9070a0 100644 --- a/components/groups/modals/voteModal.tsx +++ b/components/groups/modals/voteModal.tsx @@ -4,6 +4,8 @@ import { cosmos } from '@liftedinit/manifestjs'; import { useChain } from '@cosmos-kit/react'; import React, { useState } from 'react'; import { CloseIcon } from '@/components/icons'; +import env from '@/config/env'; + function VotingPopup({ proposalId, refetch, @@ -13,9 +15,9 @@ function VotingPopup({ refetch: () => void; setIsSigning: (isSigning: boolean) => void; }) { - const { estimateFee } = useFeeEstimation('manifest'); - const { tx } = useTx('manifest'); - const { address } = useChain('manifest'); + const { estimateFee } = useFeeEstimation(env.chain); + const { tx } = useTx(env.chain); + const { address } = useChain(env.chain); const [error, setError] = useState(null); const { vote } = cosmos.group.v1.MessageComposer.withTypeUrl; diff --git a/config/defaults.ts b/config/defaults.ts deleted file mode 100644 index 7bf5d29..0000000 --- a/config/defaults.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { AssetList, Chain } from '@chain-registry/types'; -import env from './env'; - -export const manifestChain: Chain = { - chain_name: env.chain, - status: 'live', - network_type: env.chainTier, - website: '', - pretty_name: 'Manifest Testnet', - chain_id: env.chainId, - bech32_prefix: 'manifest', - daemon_name: 'manifest', - node_home: '$HOME/.manifest', - slip44: 118, - apis: { - rpc: [ - { - address: env.rpcUrl, - }, - ], - rest: [ - { - address: env.apiUrl, - }, - ], - }, - fees: { - fee_tokens: [ - { - denom: 'umfx', - fixed_min_gas_price: 0.02, - low_gas_price: 0.01, - average_gas_price: 0.022, - high_gas_price: 0.034, - }, - ], - }, - staking: { - staking_tokens: [ - { - denom: 'upoa', - }, - ], - }, - codebase: { - git_repo: 'github.com/liftedinit/manifest-ledger', - recommended_version: 'v0.0.1-alpha.4', - compatible_versions: ['v0.0.1-alpha.4'], - binaries: { - 'linux/amd64': - 'https://github.com/liftedinit/manifest-ledger/releases/download/v0.0.1-alpha.4/manifest-ledger_0.0.1-alpha.4_linux_amd64.tar.gz', - }, - versions: [ - { - name: 'v1', - recommended_version: 'v0.0.1-alpha.4', - compatible_versions: ['v0.0.1-alpha.4'], - }, - ], - genesis: { - genesis_url: - 'https://github.com/liftedinit/manifest-ledger/blob/main/network/manifest-1/manifest-1_genesis.json', - }, - }, -}; -export const manifestAssets: AssetList = { - chain_name: env.chain, - assets: [ - { - description: 'Manifest testnet native token', - denom_units: [ - { - denom: 'umfx', - exponent: 0, - }, - { - denom: 'mfx', - exponent: 6, - }, - ], - base: 'umfx', - name: 'Manifest Testnet Token', - display: 'mfx', - symbol: 'MFX', - }, - { - description: 'Proof of Authority token for the Manifest testnet', - denom_units: [ - { - denom: 'upoa', - exponent: 0, - }, - { - denom: 'poa', - exponent: 6, - }, - ], - base: 'upoa', - name: 'Manifest Testnet Token', - display: 'poa', - symbol: 'POA', - }, - ], -}; diff --git a/config/index.ts b/config/index.ts index 34b53e4..c1532d6 100644 --- a/config/index.ts +++ b/config/index.ts @@ -1,2 +1 @@ -export * from './defaults'; export * from './env'; diff --git a/pages/_app.tsx b/pages/_app.tsx index de6b8b1..5f8c863 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -9,7 +9,14 @@ import { createPortal } from 'react-dom'; import { makeWeb3AuthWallets, SignData } from '@cosmos-kit/web3auth'; import { useEffect, useMemo, useState } from 'react'; import SignModal from '@/components/react/authSignerModal'; -import { manifestAssets, manifestChain } from '@/config'; +import { + assets as manifestAssets, + chain as manifestChain, +} from 'chain-registry/testnet/manifesttestnet'; +import { + assets as osmosisAssets, + chain as osmosisChain, +} from 'chain-registry/testnet/osmosistestnet'; import { SignerOptions, wallets } from 'cosmos-kit'; import { wallets as cosmosExtensionWallets } from '@cosmos-kit/cosmos-extension-metamask'; @@ -184,8 +191,8 @@ function ManifestApp({ Component, pageProps }: ManifestAppProps) { { Date: Thu, 9 Jan 2025 00:05:10 -0700 Subject: [PATCH 02/48] feat: add osmosis balances --- components/bank/components/sendBox.tsx | 20 +++- components/bank/forms/ibcSendForm.tsx | 156 ++++++++++++++++++++++--- config/env.ts | 1 + hooks/useLcdQueryClient.ts | 16 +++ hooks/useQueries.ts | 30 ++++- pages/bank.tsx | 1 + public/osmosis.png | Bin 731617 -> 0 bytes public/osmosis.svg | 118 +++++++++++++++++++ utils/ibc.ts | 5 + utils/yupExtensions.ts | 2 +- 10 files changed, 322 insertions(+), 27 deletions(-) delete mode 100644 public/osmosis.png create mode 100644 public/osmosis.svg diff --git a/components/bank/components/sendBox.tsx b/components/bank/components/sendBox.tsx index 62950b3..cfa6ed7 100644 --- a/components/bank/components/sendBox.tsx +++ b/components/bank/components/sendBox.tsx @@ -33,12 +33,19 @@ export default function SendBox({ admin?: string; }) { const [activeTab, setActiveTab] = useState<'send' | 'cross-chain'>('send'); - const [selectedChain, setSelectedChain] = useState(''); + const [selectedFromChain, setSelectedFromChain] = useState(''); + const [selectedToChain, setSelectedToChain] = useState(''); const ibcChains: IbcChain[] = [ + { + id: 'manifest', + name: 'Manifest', + icon: '/logo.svg', + prefix: 'manifest', + }, { id: 'osmosis', name: 'Osmosis', - icon: 'https://osmosis.zone/assets/icons/osmo-logo-icon.svg', + icon: '/osmosis.svg', prefix: 'osmo', }, ]; @@ -79,16 +86,17 @@ export default function SendBox({ ) : ( void; isIbcTransfer: boolean; ibcChains: IbcChain[]; - selectedChain: string; - setSelectedChain: (selectedChain: string) => void; + selectedFromChain: string; + setSelectedFromChain: (selectedChain: string) => void; + selectedToChain: string; + setSelectedToChain: (selectedChain: string) => void; selectedDenom?: string; - isGroup?: boolean; }>) { const [isSending, setIsSending] = useState(false); const [searchTerm, setSearchTerm] = useState(''); @@ -52,6 +62,10 @@ export default function IbcSendForm({ const { transfer } = ibc.applications.transfer.v1.MessageComposer.withTypeUrl; const [isContactsOpen, setIsContactsOpen] = useState(false); + const { address: osmosisAddress } = useChain('osmosistestnet'); + const { balances: osmosisBalances, isBalancesLoading: isOsmosisBalancesLoading } = + useTokenBalancesOsmosis(osmosisAddress ?? ''); + // Adjusted filter logic to handle undefined metadata const filteredBalances = useMemo( () => @@ -115,7 +129,7 @@ export default function IbcSendForm({ const { source_port, source_channel } = getIbcInfo(env.chain, destinationChain ?? ''); const token = { - denom: values.selectedToken.coreDenom, + denom: getIbcDenom(selectedToChain, values.selectedToken.coreDenom) ?? '', amount: amountInBaseUnits, }; @@ -171,10 +185,105 @@ export default function IbcSendForm({ {({ isValid, dirty, setFieldValue, values, errors }) => (
+ {/* From Chain (Manifest) */} +
+
+ + From Chain + + +
+ + +
+ {/* To Chain (Osmosis) */}
- Chain + To Chain
- -
+ {/* To Chain (Osmosis) */} +
+
+ + To Chain + + +
+ +
+ aria-label={chain.name} + > + {chain.name} + {chain.name} + + + ))} + +
-
@@ -238,6 +248,10 @@ export function TokenList(props: Readonly) { isGroup={isGroup} admin={admin} refetchProposals={refetchProposals} + osmosisBalances={osmosisBalances ?? []} + isOsmosisBalancesLoading={isOsmosisBalancesLoading ?? false} + refetchOsmosisBalances={refetchOsmosisBalances ?? (() => {})} + resolveOsmosisRefetch={resolveOsmosisRefetch ?? (() => {})} /> ); diff --git a/components/bank/forms/ibcSendForm.tsx b/components/bank/forms/ibcSendForm.tsx index 5ce0ba6..72f6c15 100644 --- a/components/bank/forms/ibcSendForm.tsx +++ b/components/bank/forms/ibcSendForm.tsx @@ -1,4 +1,4 @@ -import React, { useState, useMemo } from 'react'; +import React, { useState, useMemo, useEffect } from 'react'; import { useFeeEstimation, useOsmosisTokenBalancesResolved, @@ -6,7 +6,7 @@ import { useTokenBalancesOsmosis, useTx, } from '@/hooks'; -import { ibc } from '@liftedinit/manifestjs'; +import { cosmos, ibc } from '@liftedinit/manifestjs'; import { MsgTransfer } from '@liftedinit/manifestjs/dist/codegen/ibc/applications/transfer/v1/tx'; import { getIbcInfo, @@ -15,6 +15,7 @@ import { truncateString, getIbcDenom, OSMOSIS_TOKEN_DATA, + denomToAsset, } from '@/utils'; import { PiCaretDownBold } from 'react-icons/pi'; import { MdContacts } from 'react-icons/md'; @@ -30,6 +31,8 @@ import { SearchIcon, TransferIcon } from '@/components/icons'; import { TailwindModal } from '@/components/react/modal'; import env from '@/config/env'; import { useChain } from '@cosmos-kit/react'; +import { useSearchParams } from 'next/navigation'; +import { Any } from 'cosmjs-types/google/protobuf/any'; //TODO: switch to main-net names export default function IbcSendForm({ @@ -46,6 +49,13 @@ export default function IbcSendForm({ selectedToChain, setSelectedToChain, selectedDenom, + isGroup, + osmosisBalances, + isOsmosisBalancesLoading, + refetchOsmosisBalances, + resolveOsmosisRefetch, + refetchProposals, + admin, }: Readonly<{ address: string; destinationChain: string; @@ -55,12 +65,20 @@ export default function IbcSendForm({ refetchHistory: () => void; isIbcTransfer: boolean; ibcChains: IbcChain[]; + isGroup?: boolean; selectedFromChain: string; setSelectedFromChain: (selectedChain: string) => void; selectedToChain: string; setSelectedToChain: (selectedChain: string) => void; selectedDenom?: string; + osmosisBalances: CombinedBalanceInfo[]; + isOsmosisBalancesLoading: boolean; + refetchOsmosisBalances: () => void; + resolveOsmosisRefetch: () => void; + refetchProposals?: () => void; + admin?: string; }>) { + const { address: osmosisAddress } = useChain(env.osmosisTestnetChain); const [isSending, setIsSending] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [feeWarning, setFeeWarning] = useState(''); @@ -72,63 +90,40 @@ export default function IbcSendForm({ ); const { transfer } = ibc.applications.transfer.v1.MessageComposer.withTypeUrl; const [isContactsOpen, setIsContactsOpen] = useState(false); - - const { address: osmosisAddress } = useChain(env.osmosisTestnetChain); - const { balances: osmosisBalances, isBalancesLoading: isOsmosisBalancesLoading } = - useTokenBalancesOsmosis(osmosisAddress ?? ''); - const { - balances: resolvedOsmosisBalances, - isBalancesLoading: resolvedOsmosisLoading, - refetchBalances: resolveOsmosisRefetch, - } = useOsmosisTokenBalancesResolved(osmosisAddress ?? ''); - - const { metadatas: osmosisMetadatas, isMetadatasLoading: isOsmosisMetadatasLoading } = - useOsmosisTokenFactoryDenomsMetadata(); - - // Add this combined balances memo for Osmosis tokens - const combinedOsmosisBalances = useMemo(() => { - if (!osmosisBalances || !resolvedOsmosisBalances || !osmosisMetadatas) { - return []; + const [isIconRotated, setIsIconRotated] = useState(false); + const searchParams = useSearchParams(); + const policyAddress = searchParams.get('policyAddress'); + const { submitProposal } = cosmos.group.v1.MessageComposer.withTypeUrl; + useEffect(() => { + if (policyAddress) { + setSelectedFromChain(env.chain); } + }, [policyAddress, setSelectedFromChain]); - const combined = osmosisBalances.map((coreBalance): CombinedBalanceInfo => { - const resolvedBalance = resolvedOsmosisBalances.find( - rb => rb.denom === coreBalance.denom || rb.denom === coreBalance.denom.split('/').pop() - ); - - return { - denom: resolvedBalance?.denom || coreBalance.denom, - coreDenom: coreBalance.denom, - amount: coreBalance.amount, - metadata: OSMOSIS_TOKEN_DATA, - }; - }); - - return combined; - }, [osmosisBalances, resolvedOsmosisBalances, osmosisMetadatas]); + // Add this combined balances memo for Osmosis tokens - // Update the filtered balances logic to use the appropriate balance source + // Update the filtered balances logic to use passed props instead of hooks const filteredBalances = useMemo(() => { const sourceBalances = - selectedFromChain === env.osmosisTestnetChain ? combinedOsmosisBalances : balances; + selectedFromChain === env.osmosisTestnetChain ? osmosisBalances : balances; return sourceBalances?.filter(token => { const displayName = token.metadata?.display ?? token.denom; return displayName.toLowerCase().includes(searchTerm.toLowerCase()); }); - }, [balances, combinedOsmosisBalances, searchTerm, selectedFromChain]); + }, [balances, osmosisBalances, searchTerm, selectedFromChain]); // Update initialSelectedToken to consider the chain const initialSelectedToken = useMemo(() => { const sourceBalances = - selectedFromChain === env.osmosisTestnetChain ? combinedOsmosisBalances : balances; + selectedFromChain === env.osmosisTestnetChain ? osmosisBalances : balances; return ( sourceBalances?.find(token => token.coreDenom === selectedDenom) || sourceBalances?.[0] || null ); - }, [balances, combinedOsmosisBalances, selectedDenom, selectedFromChain]); + }, [balances, osmosisBalances, selectedDenom, selectedFromChain]); // Update the loading check if ( @@ -153,14 +148,23 @@ export default function IbcSendForm({ const balance = parseFloat(selectedToken.amount) / Math.pow(10, exponent); return value <= balance; }) - .test('leave-for-fees', 'Insufficient balance for fees', function (value) { + .test('leave-for-fees', '', function (value) { const { selectedToken } = this.parent; if (!selectedToken || !value || selectedToken.denom !== 'umfx') return true; const exponent = selectedToken.metadata?.denom_units[1]?.exponent ?? 6; const balance = parseFloat(selectedToken.amount) / Math.pow(10, exponent); - return value <= balance - 0.09; + const MIN_FEE_BUFFER = 0.09; + const hasInsufficientBuffer = value > balance - MIN_FEE_BUFFER; + + if (hasInsufficientBuffer) { + setFeeWarning('Remember to leave tokens for fees!'); + } else { + setFeeWarning(''); + } + + return !hasInsufficientBuffer; }), selectedToken: Yup.object().required('Please select a token'), memo: Yup.string().max(255, 'Memo must be less than 255 characters'), @@ -180,7 +184,7 @@ export default function IbcSendForm({ try { const exponent = values.selectedToken.metadata?.denom_units[1]?.exponent ?? 6; const amountInBaseUnits = parseNumberToBigInt(values.amount, exponent).toString(); - console.log(selectedFromChain, selectedToChain); + const { source_port, source_channel } = getIbcInfo(selectedFromChain, selectedToChain); const token = { @@ -191,11 +195,14 @@ export default function IbcSendForm({ const stamp = Date.now(); const timeoutInNanos = (stamp + 1.2e6) * 1e6; - const msg = transfer({ + const transferMsg = transfer({ sourcePort: source_port, sourceChannel: source_channel, - sender: - selectedFromChain === env.osmosisTestnetChain ? (osmosisAddress ?? '') : (address ?? ''), + sender: admin + ? admin + : selectedFromChain === env.osmosisTestnetChain + ? (osmosisAddress ?? '') + : (address ?? ''), receiver: values.recipient ?? '', token, timeoutHeight: { @@ -205,6 +212,23 @@ export default function IbcSendForm({ timeoutTimestamp: BigInt(timeoutInNanos), }); + const msg = isGroup + ? submitProposal({ + groupPolicyAddress: admin!, + messages: [ + Any.fromPartial({ + typeUrl: MsgTransfer.typeUrl, + value: MsgTransfer.encode(transferMsg.value).finish(), + }), + ], + metadata: '', + proposers: [address], + title: `IBC Transfer`, + summary: `This proposal will send ${values.amount} ${values.selectedToken.metadata?.display} to ${values.recipient} via IBC transfer`, + exec: 0, + }) + : transferMsg; + const fee = await estimateFee( selectedFromChain === env.osmosisTestnetChain ? (osmosisAddress ?? '') : (address ?? ''), [msg] @@ -216,6 +240,9 @@ export default function IbcSendForm({ onSuccess: () => { refetchBalances(); refetchHistory(); + refetchOsmosisBalances(); + resolveOsmosisRefetch(); + refetchProposals?.(); }, }); } catch (error) { @@ -246,121 +273,161 @@ export default function IbcSendForm({
- {/* From Chain (Manifest) */} -
-
- - From Chain - -
{/* Switch Button */}
{ e.preventDefault(); + setIsIconRotated(!isIconRotated); setSelectedFromChain(selectedToChain); setSelectedToChain(selectedFromChain); }} - className="absolute top-[calc(50%-16px)] right-0 w-full flex justify-center items-center cursor-pointer z-10" + className={`absolute top-[calc(50%-16px)] right-0 w-full justify-center items-center cursor-pointer z-10 ${ + isGroup === true ? 'hidden' : 'flex' + }`} >
{/* To Chain (Osmosis) */} @@ -505,7 +572,9 @@ export default function IbcSendForm({ return tokenDisplayName.startsWith('factory') ? tokenDisplayName.split('/').pop()?.toUpperCase() - : truncateString(tokenDisplayName, 10).toUpperCase(); + : tokenDisplayName.startsWith('u') + ? tokenDisplayName.slice(1).toUpperCase() + : truncateString(tokenDisplayName, 10).toUpperCase(); })()} @@ -553,7 +622,9 @@ export default function IbcSendForm({ return tokenDisplayName.startsWith('factory') ? tokenDisplayName.split('/').pop()?.toUpperCase() - : truncateString(tokenDisplayName, 10).toUpperCase(); + : tokenDisplayName.startsWith('u') + ? tokenDisplayName.slice(1).toUpperCase() + : truncateString(tokenDisplayName, 10).toUpperCase(); })()} @@ -589,7 +660,9 @@ export default function IbcSendForm({ return tokenDisplayName.startsWith('factory') ? tokenDisplayName.split('/').pop()?.toUpperCase() - : truncateString(tokenDisplayName, 10).toUpperCase(); + : tokenDisplayName.startsWith('u') + ? tokenDisplayName.slice(1).toUpperCase() + : truncateString(tokenDisplayName, 10).toUpperCase(); })()}
{ const denomUnits = [ diff --git a/components/groups/components/myGroups.tsx b/components/groups/components/myGroups.tsx index 49570e6..630f482 100644 --- a/components/groups/components/myGroups.tsx +++ b/components/groups/components/myGroups.tsx @@ -11,6 +11,7 @@ import { import ProfileAvatar from '@/utils/identicon'; import { CombinedBalanceInfo, + denomToAsset, ExtendedMetadataSDKType, MFX_TOKEN_DATA, truncateString, @@ -260,6 +261,32 @@ export function YourGroups({ ); const metadata = metadatas.metadatas.find(m => m.base === coreBalance.denom); + if (coreBalance.denom.startsWith('ibc/')) { + const assetInfo = denomToAsset(env.chain, coreBalance.denom); + + const baseDenom = assetInfo?.traces?.[1]?.counterparty?.base_denom; + + return { + denom: baseDenom ?? '', // normalized denom (e.g., 'umfx') + coreDenom: coreBalance.denom, // full IBC trace + amount: coreBalance.amount, + metadata: { + description: assetInfo?.description ?? '', + denom_units: + assetInfo?.denom_units?.map(unit => ({ + ...unit, + aliases: unit.aliases || [], + })) ?? [], + base: assetInfo?.base ?? '', + display: assetInfo?.display ?? '', + name: assetInfo?.name ?? '', + symbol: assetInfo?.symbol ?? '', + uri: assetInfo?.logo_URIs?.svg ?? assetInfo?.logo_URIs?.png ?? '', + uri_hash: assetInfo?.logo_URIs?.svg ?? assetInfo?.logo_URIs?.png ?? '', + }, + }; + } + return { denom: resolvedBalance?.denom || coreBalance.denom, coreDenom: coreBalance.denom, diff --git a/hooks/useQueries.ts b/hooks/useQueries.ts index 3114d4a..f85693f 100644 --- a/hooks/useQueries.ts +++ b/hooks/useQueries.ts @@ -4,7 +4,7 @@ import { QueryGroupsByMemberResponseSDKType } from '@liftedinit/manifestjs/dist/ import { useLcdQueryClient, useOsmosisLcdQueryClient } from './useLcdQueryClient'; import { usePoaLcdQueryClient } from './usePoaLcdQueryClient'; -import { getLogoUrls } from '@/utils'; +import { getLogoUrls, normalizeIBCDenom } from '@/utils'; import { useManifestLcdQueryClient } from './useManifestLcdQueryClient'; diff --git a/pages/_app.tsx b/pages/_app.tsx index 2b0ef0c..89e8027 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -44,12 +44,15 @@ import { import MobileNav from '@/components/react/mobileNav'; import { OPENLOGIN_NETWORK_TYPE } from '@toruslabs/openlogin-utils'; +import { AssetList } from '@chain-registry/types'; type ManifestAppProps = AppProps & { Component: AppProps['Component']; pageProps: AppProps['pageProps']; }; +// TODO: remove asset list injections when chain registry is updated + function ManifestApp({ Component, pageProps }: ManifestAppProps) { const [isDrawerVisible, setIsDrawerVisible] = useState(() => { // Initialize from localStorage if available, otherwise default to true @@ -201,6 +204,7 @@ function ManifestApp({ Component, pageProps }: ManifestAppProps) { m.base === coreBalance.denom); + if (coreBalance.denom.startsWith('ibc/')) { + const assetInfo = denomToAsset(env.chain, coreBalance.denom); + + const baseDenom = assetInfo?.traces?.[1]?.counterparty?.base_denom; + + return { + denom: baseDenom ?? '', // normalized denom (e.g., 'umfx') + coreDenom: coreBalance.denom, // full IBC trace + amount: coreBalance.amount, + metadata: { + description: assetInfo?.description ?? '', + denom_units: + assetInfo?.denom_units?.map(unit => ({ + ...unit, + aliases: unit.aliases || [], + })) ?? [], + base: assetInfo?.base ?? '', + display: assetInfo?.display ?? '', + name: assetInfo?.name ?? '', + symbol: assetInfo?.symbol ?? '', + uri: assetInfo?.logo_URIs?.svg ?? assetInfo?.logo_URIs?.png ?? '', + uri_hash: assetInfo?.logo_URIs?.svg ?? assetInfo?.logo_URIs?.png ?? '', + }, + }; + } + return { denom: resolvedBalance?.denom || coreBalance.denom, coreDenom: coreBalance.denom, @@ -127,6 +155,80 @@ export default function Bank() { return mfxCombinedBalance ? [mfxCombinedBalance, ...otherBalances] : otherBalances; }, [balances, resolvedBalances, metadatas]); + const { address: osmosisAddress } = useChain(env.osmosisTestnetChain); + const { + balances: osmosisBalances, + isBalancesLoading: isOsmosisBalancesLoading, + refetchBalances: refetchOsmosisBalances, + } = useTokenBalancesOsmosis(osmosisAddress ?? ''); + const { + balances: resolvedOsmosisBalances, + isBalancesLoading: resolvedOsmosisLoading, + refetchBalances: resolveOsmosisRefetch, + } = useOsmosisTokenBalancesResolved(osmosisAddress ?? ''); + + const { + metadatas: osmosisMetadatas, + isMetadatasLoading: isOsmosisMetadatasLoading, + refetchMetadatas: refetchOsmosisMetadatas, + } = useOsmosisTokenFactoryDenomsMetadata(); + + const combinedOsmosisBalances = useMemo(() => { + if (!osmosisBalances || !resolvedOsmosisBalances || !osmosisMetadatas) { + return []; + } + + const combined = osmosisBalances.map((coreBalance): CombinedBalanceInfo => { + // Handle OSMO token specifically + if (coreBalance.denom === 'uosmo') { + return { + denom: 'uosmo', + coreDenom: coreBalance.denom, + amount: coreBalance.amount, + metadata: OSMOSIS_TOKEN_DATA, + }; + } + + // Handle IBC tokens + if (coreBalance.denom.startsWith('ibc/')) { + const assetInfo = denomToAsset(env.osmosisTestnetChain, coreBalance.denom); + + const baseDenom = assetInfo?.traces?.[1]?.counterparty?.base_denom; + + return { + denom: baseDenom ?? '', // normalized denom (e.g., 'umfx') + coreDenom: coreBalance.denom, // full IBC trace + amount: coreBalance.amount, + metadata: { + description: assetInfo?.description ?? '', + denom_units: + assetInfo?.denom_units?.map(unit => ({ + ...unit, + aliases: unit.aliases || [], + })) ?? [], + base: assetInfo?.base ?? '', + display: assetInfo?.display ?? '', + name: assetInfo?.name ?? '', + symbol: assetInfo?.symbol ?? '', + uri: assetInfo?.logo_URIs?.svg ?? assetInfo?.logo_URIs?.png ?? '', + uri_hash: assetInfo?.logo_URIs?.svg ?? assetInfo?.logo_URIs?.png ?? '', + }, + }; + } + + // Handle all other tokens + const metadata = osmosisMetadatas.metadatas?.find(m => m.base === coreBalance.denom); + return { + denom: coreBalance.denom, + coreDenom: coreBalance.denom, + amount: coreBalance.amount, + metadata: metadata || null, + }; + }); + + return combined; + }, [osmosisBalances, resolvedOsmosisBalances, osmosisMetadatas]); + const isLoading = isBalancesLoading || resolvedLoading || isMetadatasLoading; const [searchTerm, setSearchTerm] = useState(''); @@ -152,17 +254,18 @@ export default function Bank() { > Bank - -
- setSearchTerm(e.target.value)} - /> - -
+ {combinedBalances.length > 0 && ( +
+ setSearchTerm(e.target.value)} + /> + +
+ )}
{activeTab === 'assets' && - (combinedBalances.length === 0 && !isLoading ? ( + (combinedBalances.length === 0 ? ( ) : ( ))} {activeTab === 'history' && - (totalCount === 0 && !txLoading ? ( + (sendTxs.length === 0 ? ( ) : ( group.policies[0].address) ?? []; diff --git a/utils/ibc.ts b/utils/ibc.ts index f1b4b3a..3f7ae33 100644 --- a/utils/ibc.ts +++ b/utils/ibc.ts @@ -1,9 +1,123 @@ -import { asset_lists as assetLists } from '@chain-registry/assets'; import { Asset, AssetList, IBCInfo } from '@chain-registry/types'; -import { assets, ibc } from 'chain-registry'; + import { Coin } from '@liftedinit/manifestjs/dist/codegen/cosmos/base/v1beta1/coin'; import { shiftDigits } from './maths'; +import { assets as manifestAssets } from 'chain-registry/testnet/manifesttestnet'; +import { assets as osmosisAssets } from 'chain-registry/testnet/osmosistestnet'; + +//TODO: use chain-registry when the package is updated + +const manifestAssetList: AssetList = { + chain_name: 'manifesttestnet', + assets: [ + ...manifestAssets.assets, + { + description: 'Osmosis token on Manifest Ledger Testnet', + denom_units: [ + { + denom: 'ibc/ED07A3391A112B175915CD8FAF43A2DA8E4790EDE12566649D0C2F97716B8518', + exponent: 0, + aliases: ['uosmo'], + }, + { + denom: 'osmo', + exponent: 6, + }, + ], + type_asset: 'ics20', + base: 'ibc/ED07A3391A112B175915CD8FAF43A2DA8E4790EDE12566649D0C2F97716B8518', + name: 'Osmosis', + display: 'osmo', + symbol: 'OSMO', + traces: [ + { + type: 'ibc', + counterparty: { + chain_name: 'osmosistestnet', + base_denom: 'uosmo', + channel_id: 'channel-10016', + }, + chain: { + channel_id: 'channel-0', + path: 'transfer/channel-0/uosmo', + }, + }, + ], + images: [ + { + image_sync: { + chain_name: 'osmosistestnet', + base_denom: 'uosmo', + }, + svg: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/osmosis/images/osmo.svg', + png: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/osmosis/images/osmo.png', + }, + ], + logo_URIs: { + png: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/osmosis/images/osmo.png', + svg: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/osmosis/images/osmo.svg', + }, + }, + ], +}; + +const osmosisAssetList: AssetList = { + chain_name: 'osmosistestnet', + assets: [ + ...osmosisAssets.assets, + { + description: 'Manifest Testnet Token', + denom_units: [ + { + denom: 'ibc/8402769A51AEE1CDF35223998D284E937EBF03F4A2CE43EC10BB028BB5AD29C8', + exponent: 0, + aliases: ['umfx'], + }, + { + denom: 'mfx', + exponent: 6, + }, + ], + type_asset: 'ics20', + base: 'ibc/8402769A51AEE1CDF35223998D284E937EBF03F4A2CE43EC10BB028BB5AD29C8', + name: 'Manifest Testnet', + display: 'mfx', + symbol: 'MFX', + traces: [ + { + type: 'ibc', + counterparty: { + chain_name: 'manifesttestnet', + base_denom: 'umfx', + channel_id: 'channel-0', + }, + chain: { + channel_id: 'channel-10016', + path: 'transfer/channel-10016/umfx', + }, + }, + ], + logo_URIs: { + png: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/testnets/manifesttestnet/images/mfx.png', + svg: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/testnets/manifesttestnet/images/mfx.svg', + }, + images: [ + { + image_sync: { + chain_name: 'manifesttestnet', + base_denom: 'umfx', + }, + png: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/testnets/manifesttestnet/images/mfx.png', + svg: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/testnets/manifesttestnet/images/mfx.svg', + }, + ], + }, + ], +}; + +const assets = [manifestAssetList, osmosisAssetList]; +const assetLists = [manifestAssetList, osmosisAssetList]; //TODO: use chain-registry when the package is updated export const truncateDenom = (denom: string) => { @@ -14,7 +128,7 @@ const filterAssets = (chainName: string, assetList: AssetList[]): Asset[] => { return ( assetList .find(({ chain_name }) => chain_name === chainName) - ?.assets?.filter(({ type_asset }) => type_asset !== 'ics20') || [] + ?.assets?.filter(({ type_asset }) => type_asset === 'ics20' || !type_asset) || [] ); }; @@ -26,7 +140,22 @@ const getAllAssets = (chainName: string) => { }; export const denomToAsset = (chainName: string, denom: string) => { - return getAllAssets(chainName).find(asset => asset.base === denom); + const allAssets = getAllAssets(chainName); + // Only handle IBC hashes + if (denom.startsWith('ibc/')) { + // Find the asset that has this IBC hash as its base + const asset = allAssets.find(asset => asset.base === denom); + if (asset?.traces?.[0]?.counterparty?.base_denom) { + // Return the original denom from the counterparty chain + return { + ...asset, + base: asset.traces[0].counterparty.base_denom, + }; + } + } + + // Return original asset if not an IBC hash + return allAssets.find(asset => asset.base === denom); }; export const denomToExponent = (chainName: string, denom: string) => { @@ -110,3 +239,15 @@ export const getIbcDenom = (chainName: string, denom: string) => { const asset = denomToAsset(chainName, denom); return asset?.base; }; + +export const normalizeIBCDenom = (chainName: string, denom: string) => { + const asset = denomToAsset(chainName, denom); + if (asset) { + return { + denom: asset.base, + }; + } + return { denom }; +}; + +export type ResolvedIBCDenom = ReturnType; From 03619b3592fa697cc705956c3ca7c63ceb88336f Mon Sep 17 00:00:00 2001 From: Joseph Chalabi Date: Fri, 10 Jan 2025 17:38:41 -0700 Subject: [PATCH 08/48] fix: show ibc assets in history --- components/bank/components/historyBox.tsx | 47 ++++++++++++++++++++--- components/bank/modals/txInfo.tsx | 30 ++++++++++----- utils/ibc.ts | 1 + 3 files changed, 64 insertions(+), 14 deletions(-) diff --git a/components/bank/components/historyBox.tsx b/components/bank/components/historyBox.tsx index e5f923a..cc93b6e 100644 --- a/components/bank/components/historyBox.tsx +++ b/components/bank/components/historyBox.tsx @@ -1,12 +1,13 @@ import React, { useMemo, useState, useEffect } from 'react'; import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; import TxInfoModal from '../modals/txInfo'; -import { shiftDigits, truncateString } from '@/utils'; +import { denomToAsset, shiftDigits, truncateString } from '@/utils'; import { BurnIcon, DenomImage, formatDenom, MintIcon } from '@/components'; import { HistoryTxType, useTokenFactoryDenomsMetadata } from '@/hooks'; import { ReceiveIcon, SendIcon } from '@/components/icons'; import useIsMobile from '@/hooks/useIsMobile'; +import env from '@/config/env'; interface Transaction { tx_type: HistoryTxType; @@ -216,7 +217,26 @@ export function HistoryBox({
{tx.data.amount.map((amt, index) => { - const metadata = metadatas?.metadatas.find(m => m.base === amt.denom); + const assetInfo = denomToAsset(env.chain, amt.denom); + let metadata = metadatas?.metadatas.find(m => m.base === amt.denom); + + if (amt.denom.startsWith('ibc/')) { + metadata = { + description: assetInfo?.description ?? '', + denom_units: + assetInfo?.denom_units?.map(unit => ({ + ...unit, + aliases: unit.aliases || [], + })) ?? [], + base: assetInfo?.base ?? '', + display: assetInfo?.display ?? '', + name: assetInfo?.name ?? '', + symbol: assetInfo?.symbol ?? '', + uri: assetInfo?.logo_URIs?.svg ?? assetInfo?.logo_URIs?.png ?? '', + uri_hash: assetInfo?.logo_URIs?.svg ?? assetInfo?.logo_URIs?.png ?? '', + }; + } + return ; })} @@ -228,8 +248,16 @@ export function HistoryBox({

{tx.data.amount.map((amt, index) => { const metadata = metadatas?.metadatas.find(m => m.base === amt.denom); - const display = metadata?.display ?? metadata?.symbol ?? ''; - return metadata?.display.startsWith('factory') + let display = metadata?.display ?? metadata?.symbol ?? ''; + + if (amt.denom.startsWith('ibc/')) { + const assetInfo = denomToAsset(env.chain, amt.denom); + if (assetInfo?.traces?.[0]?.counterparty?.base_denom) { + display = assetInfo.traces[0].counterparty.base_denom.slice(1); + } + } + + return metadata?.display?.startsWith('factory') ? metadata?.display?.split('/').pop()?.toUpperCase() : display.length > 4 ? display.slice(0, 4).toUpperCase() + '...' @@ -269,7 +297,16 @@ export function HistoryBox({ const metadata = metadatas?.metadatas.find(m => m.base === amt.denom); const exponent = Number(metadata?.denom_units[1]?.exponent) || 6; const amount = Number(shiftDigits(amt.amount, -exponent)); - return `${formatLargeNumber(amount)} ${formatDenom(amt.denom)}`; + let baseDenom = formatDenom(amt.denom); + + if (amt.denom.startsWith('ibc/')) { + const assetInfo = denomToAsset(env.chain, amt.denom); + if (assetInfo?.traces?.[0]?.counterparty?.base_denom) { + baseDenom = assetInfo.traces[0].counterparty.base_denom.slice(1); + } + } + + return `${formatLargeNumber(amount)} ${baseDenom.toUpperCase()}`; }) .join(', ')}

diff --git a/components/bank/modals/txInfo.tsx b/components/bank/modals/txInfo.tsx index 3c7b1c6..53d8ada 100644 --- a/components/bank/modals/txInfo.tsx +++ b/components/bank/modals/txInfo.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; import { formatDenom, TransactionGroup } from '@/components'; import { FaExternalLinkAlt } from 'react-icons/fa'; -import { shiftDigits } from '@/utils'; +import { denomToAsset, shiftDigits } from '@/utils'; import env from '@/config/env'; interface TxInfoModalProps { @@ -75,14 +75,26 @@ export default function TxInfoModal({ tx, modalId }: TxInfoModalProps) { VALUE

- {tx?.data?.amount.map((amt, index) => ( -

- {Number(shiftDigits(amt.amount, -6)).toLocaleString(undefined, { - maximumFractionDigits: 6, - })}{' '} - {formatDenom(amt.denom)} -

- ))} + {tx?.data?.amount.map((amt, index) => { + const amount = Number(shiftDigits(amt.amount, -6)); + let displayDenom = formatDenom(amt.denom); + + if (amt.denom.startsWith('ibc/')) { + const assetInfo = denomToAsset(env.chain, amt.denom); + if (assetInfo?.traces?.[0]?.counterparty?.base_denom) { + displayDenom = assetInfo.traces[0].counterparty.base_denom.slice(1); + } + } + + return ( +

+ {Number(amount).toLocaleString(undefined, { + maximumFractionDigits: 6, + })}{' '} + {displayDenom.toUpperCase()} +

+ ); + })}
diff --git a/utils/ibc.ts b/utils/ibc.ts index 3f7ae33..dcfee6a 100644 --- a/utils/ibc.ts +++ b/utils/ibc.ts @@ -141,6 +141,7 @@ const getAllAssets = (chainName: string) => { export const denomToAsset = (chainName: string, denom: string) => { const allAssets = getAllAssets(chainName); + // Only handle IBC hashes if (denom.startsWith('ibc/')) { // Find the asset that has this IBC hash as its base From dc163e0cc6fb151a9e4b2385d3aac465786066e6 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi Date: Fri, 10 Jan 2025 19:47:43 -0700 Subject: [PATCH 09/48] chore: add back uppercase --- components/bank/components/tokenList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/bank/components/tokenList.tsx b/components/bank/components/tokenList.tsx index 8d0482c..78190e9 100644 --- a/components/bank/components/tokenList.tsx +++ b/components/bank/components/tokenList.tsx @@ -117,7 +117,7 @@ export function TokenList(props: Readonly) {

- {truncateString(balance.metadata?.display ?? '', 12)} + {truncateString(balance.metadata?.display ?? '', 12).toUpperCase()}

{balance.metadata?.denom_units[0]?.denom.startsWith('ibc') From aa9301fc2b2e0f6c9ea94125f11ddf498f3ea562 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi Date: Fri, 10 Jan 2025 20:34:19 -0700 Subject: [PATCH 10/48] chore: code rabbit suggestions --- .gitignore | 4 +- components/bank/components/sendBox.tsx | 15 +- components/bank/forms/ibcSendForm.tsx | 11 +- scripts/ibcTransferAll.ts | 232 +++++++++++++++++++++++++ 4 files changed, 255 insertions(+), 7 deletions(-) create mode 100644 scripts/ibcTransferAll.ts diff --git a/.gitignore b/.gitignore index 0add2f2..669983e 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,6 @@ next-env.d.ts .idea/ -certificates \ No newline at end of file +certificates + +/scripts/demons.json \ No newline at end of file diff --git a/components/bank/components/sendBox.tsx b/components/bank/components/sendBox.tsx index 3240b55..974bd99 100644 --- a/components/bank/components/sendBox.tsx +++ b/components/bank/components/sendBox.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import SendForm from '../forms/sendForm'; import IbcSendForm from '../forms/ibcSendForm'; import env from '@/config/env'; @@ -61,6 +61,18 @@ export default function SendBox({ [] ); + useEffect(() => { + if (selectedFromChain && selectedToChain && selectedFromChain === selectedToChain) { + // If chains match, switch the destination chain to the other available chain + const otherChain = ibcChains.find(chain => chain.id !== selectedFromChain)?.id || ''; + setSelectedToChain(otherChain); + } + }, [selectedFromChain, selectedToChain, ibcChains]); + + const getAvailableToChains = useMemo(() => { + return ibcChains.filter(chain => chain.id !== selectedFromChain); + }, [ibcChains, selectedFromChain]); + return (

@@ -115,6 +127,7 @@ export default function SendBox({ isOsmosisBalancesLoading={isOsmosisBalancesLoading} refetchOsmosisBalances={refetchOsmosisBalances} resolveOsmosisRefetch={resolveOsmosisRefetch} + availableToChains={getAvailableToChains} /> ) : ( void; refetchProposals?: () => void; admin?: string; + availableToChains: IbcChain[]; }>) { const { address: osmosisAddress } = useChain(env.osmosisTestnetChain); const [isSending, setIsSending] = useState(false); @@ -91,14 +93,13 @@ export default function IbcSendForm({ const { transfer } = ibc.applications.transfer.v1.MessageComposer.withTypeUrl; const [isContactsOpen, setIsContactsOpen] = useState(false); const [isIconRotated, setIsIconRotated] = useState(false); - const searchParams = useSearchParams(); - const policyAddress = searchParams.get('policyAddress'); + const { submitProposal } = cosmos.group.v1.MessageComposer.withTypeUrl; useEffect(() => { - if (policyAddress) { + if (isGroup) { setSelectedFromChain(env.chain); } - }, [policyAddress, setSelectedFromChain]); + }, [isGroup, setSelectedFromChain]); // Add this combined balances memo for Osmosis tokens @@ -472,7 +473,7 @@ export default function IbcSendForm({ role="listbox" className="dropdown-content z-[100] menu p-2 shadow bg-base-300 rounded-lg w-full mt-1 dark:text-[#FFFFFF] text-[#161616]" > - {ibcChains.map(chain => ( + {availableToChains.map(chain => (
  • { diff --git a/scripts/ibcTransferAll.ts b/scripts/ibcTransferAll.ts new file mode 100644 index 0000000..7324d6a --- /dev/null +++ b/scripts/ibcTransferAll.ts @@ -0,0 +1,232 @@ +import { DirectSecp256k1HdWallet } from '@cosmjs/proto-signing'; +import { SigningStargateClient } from '@cosmjs/stargate'; +import { cosmos, ibc } from '@liftedinit/manifestjs'; +import { MsgTransfer } from '@liftedinit/manifestjs/dist/codegen/ibc/applications/transfer/v1/tx'; +import axios from 'axios'; +import * as fs from 'fs'; +import * as path from 'path'; + +// Environment Configuration +const env = { + rpcUrl: 'https://nodes.liftedinit.tech/manifest/testnet/rpc', + osmosisTestnetRpcUrl: 'https://rpc.osmotest5.osmosis.zone', + chain: 'manifest-testnet', + osmosisTestnetChain: 'osmo-test-5', +}; + +// IBC Configuration +const getIbcInfo = (fromChain: string, toChain: string) => { + if (fromChain === 'manifest-testnet' && toChain === 'osmo-test-5') { + return { + source_port: 'transfer', + source_channel: 'channel-0', + }; + } + throw new Error(`Unsupported chain combination: ${fromChain} -> ${toChain}`); +}; + +// Configuration +const MANIFEST_RPC = env.rpcUrl; +const OSMOSIS_RPC = env.osmosisTestnetRpcUrl; +const SOURCE_CHAIN = env.chain; +const TARGET_CHAIN = env.osmosisTestnetChain; + +// Helper function to format token info for asset list +function formatTokenForAssetList(ibcDenom: string, denomTrace: any, originalDenom: string) { + const tokenName = originalDenom.split('/').pop()?.replace('u', '') || ''; + const displayName = tokenName.toUpperCase(); + + return { + description: `${displayName} Token`, + denom_units: [ + { + denom: ibcDenom, + exponent: 0, + aliases: [originalDenom], + }, + { + denom: displayName, + exponent: 6, + }, + ], + type_asset: 'ics20', + base: ibcDenom, + name: displayName, + display: displayName, + symbol: displayName, + traces: [ + { + type: 'ibc', + counterparty: { + chain_name: 'manifesttestnet', + base_denom: originalDenom, + channel_id: 'channel-0', + }, + chain: { + channel_id: 'channel-10016', + path: denomTrace.path, + }, + }, + ], + logo_URIs: { + png: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/testnets/manifesttestnet/images/mfx.png', + svg: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/testnets/manifesttestnet/images/mfx.svg', + }, + images: [ + { + image_sync: { + chain_name: 'manifesttestnet', + base_denom: originalDenom, + }, + png: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/testnets/manifesttestnet/images/mfx.png', + svg: 'https://raw.githubusercontent.com/cosmos/chain-registry/master/testnets/manifesttestnet/images/mfx.svg', + }, + ], + }; +} + +// Helper function to query denom trace +async function getDenomTrace(hash: string) { + try { + const response = await axios.get( + `${OSMOSIS_RPC.replace('rpc', 'rest')}/ibc/apps/transfer/v1/denom_traces/${hash}` + ); + return response.data.denom_trace; + } catch (error) { + console.error('Error fetching denom trace:', error); + return null; + } +} + +async function main() { + // Get mnemonic from environment or argument + const mnemonic = process.env.WALLET_MNEMONIC; + if (!mnemonic) { + throw new Error('Please provide WALLET_MNEMONIC environment variable'); + } + + // Setup wallets for both chains + const manifestWallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { + prefix: 'manifest', + }); + const osmosisWallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { + prefix: 'osmo', + }); + + // Get addresses + const [manifestAccount] = await manifestWallet.getAccounts(); + const [osmosisAccount] = await osmosisWallet.getAccounts(); + + console.log('Manifest address:', manifestAccount.address); + console.log('Osmosis address:', osmosisAccount.address); + + // Create signing clients + const manifestClient = await SigningStargateClient.connectWithSigner( + MANIFEST_RPC, + manifestWallet + ); + const osmosisClient = await SigningStargateClient.connectWithSigner(OSMOSIS_RPC, osmosisWallet); + + // Query balances on Manifest chain + const balances = await manifestClient.getAllBalances(manifestAccount.address); + console.log('\nManifest chain balances:', balances); + + // Get IBC info + const { source_port, source_channel } = getIbcInfo(SOURCE_CHAIN, TARGET_CHAIN); + + // Filter and create IBC transfer messages for each token + const messages = balances + .filter(token => token.denom.startsWith('factory/')) + .map(token => { + const timeoutInNanos = (Date.now() + 1.2e6) * 1e6; + + return { + typeUrl: MsgTransfer.typeUrl, + value: { + sourcePort: source_port, + sourceChannel: source_channel, + sender: manifestAccount.address, + receiver: osmosisAccount.address, + token: { + denom: token.denom, + amount: '1', + }, + timeoutHeight: { + revisionNumber: BigInt(0), + revisionHeight: BigInt(0), + }, + timeoutTimestamp: BigInt(timeoutInNanos), + }, + }; + }); + + // Execute transfers + if (messages.length > 0) { + try { + const fee = { + amount: [{ denom: 'umfx', amount: '5500' }], + gas: '5000000', + }; + + console.log('\nExecuting IBC transfers...'); + console.log(`Attempting to transfer 1 token each for ${messages.length} different denoms...`); + + const result = await manifestClient.signAndBroadcast(manifestAccount.address, messages, fee); + + if (result.code !== 0) { + throw new Error(`Transaction failed with code ${result.code}. Logs: ${result.rawLog}`); + } + + console.log('Transfer result:', { + code: result.code, + hash: result.transactionHash, + }); + } catch (error) { + console.error('Error during transfer:', error); + process.exit(1); // Exit with error code + } + } else { + console.log('No tokens to transfer'); + } + + // Wait a bit for the transfers to complete + console.log('\nWaiting 1 minute for transfers to complete...'); + await new Promise(resolve => setTimeout(resolve, 60000)); + + // Query final balances on Osmosis + const osmosisBalances = await osmosisClient.getAllBalances(osmosisAccount.address); + console.log('\nOsmosis chain balances:', osmosisBalances); + + // Query IBC denom traces for each IBC token and format them + const ibcTokens = osmosisBalances.filter(token => token.denom.startsWith('ibc/')); + const formattedTokens = []; + + if (ibcTokens.length > 0) { + console.log('\nIBC Denom Traces:'); + for (const token of ibcTokens) { + try { + const hash = token.denom.split('/')[1]; + const denomTrace = await getDenomTrace(hash); + console.log(`${token.denom}:`, denomTrace); + + // Find original denom from balances + const originalDenom = balances.find( + b => denomTrace?.base_denom === b.denom || denomTrace?.path.includes(b.denom) + )?.denom; + + if (originalDenom && denomTrace) { + formattedTokens.push(formatTokenForAssetList(token.denom, denomTrace, originalDenom)); + } + } catch (error) { + console.error(`Error querying denom trace for ${token.denom}:`, error); + } + } + + // Save formatted tokens to file + const outputPath = path.join(__dirname, 'denoms.json'); + fs.writeFileSync(outputPath, JSON.stringify({ tokens: formattedTokens }, null, 2)); + console.log(`\nToken information saved to ${outputPath}`); + } +} + +main().catch(console.error); From 6f565daa02b377d1c5cb6dda74a802c031b27b59 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi Date: Sat, 11 Jan 2025 00:35:04 -0700 Subject: [PATCH 11/48] chore: code rabbit suggestions --- components/bank/forms/ibcSendForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/bank/forms/ibcSendForm.tsx b/components/bank/forms/ibcSendForm.tsx index d01c9f8..507da09 100644 --- a/components/bank/forms/ibcSendForm.tsx +++ b/components/bank/forms/ibcSendForm.tsx @@ -473,7 +473,7 @@ export default function IbcSendForm({ role="listbox" className="dropdown-content z-[100] menu p-2 shadow bg-base-300 rounded-lg w-full mt-1 dark:text-[#FFFFFF] text-[#161616]" > - {availableToChains.map(chain => ( + {availableToChains?.map(chain => (
  • { From 5a0f4c10e688174b4fd46db1a0c24a647c9611c6 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi Date: Tue, 14 Jan 2025 00:01:15 -0700 Subject: [PATCH 12/48] chore: add osmosis related .env variables to readme --- README.md | 8 ++++++++ config/env.ts | 1 + 2 files changed, 9 insertions(+) diff --git a/README.md b/README.md index b06d153..be4b270 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,10 @@ NEXT_PUBLIC_RPC_URL= NEXT_PUBLIC_API_URL= NEXT_PUBLIC_EXPLORER_URL= NEXT_PUBLIC_INDEXER_URL= +NEXT_PUBLIC_OSMOSIS_TESTNET_CHAIN_ID= +NEXT_PUBLIC_OSMOSIS_TESTNET_RPC_URL= +NEXT_PUBLIC_OSMOSIS_TESTNET_API_URL= +NEXT_PUBLIC_OSMOSIS_TESTNET_EXPLORER_URL= ``` where @@ -45,6 +49,10 @@ where - `NEXT_PUBLIC_API_URL` is the chain API URL - `NEXT_PUBLIC_EXPLORER_URL` is the block explorer URL - `NEXT_PUBLIC_INDEXER_URL` is the YACI indexer URL +- `NEXT_PUBLIC_OSMOSIS_TESTNET_CHAIN_ID` is the osmosis testnet chain ID +- `NEXT_PUBLIC_OSMOSIS_TESTNET_RPC_URL` is the osmosis testnet RPC URL +- `NEXT_PUBLIC_OSMOSIS_TESTNET_API_URL` is the osmosis testnet API URL +- `NEXT_PUBLIC_OSMOSIS_TESTNET_EXPLORER_URL` is the osmosis testnet block explorer URL ### Development diff --git a/config/env.ts b/config/env.ts index c626f92..5bef3ad 100644 --- a/config/env.ts +++ b/config/env.ts @@ -3,6 +3,7 @@ const env = { rpcUrl: process.env.NEXT_PUBLIC_RPC_URL ?? '', explorerUrl: process.env.NEXT_PUBLIC_EXPLORER_URL ?? '', osmosisTestnetExplorerUrl: process.env.NEXT_PUBLIC_OSMOSIS_TESTNET_EXPLORER_URL ?? '', + osmosisTestnetChainId: process.env.NEXT_PUBLIC_OSMOSIS_TESTNET_CHAIN_ID ?? '', web3AuthClientId: process.env.NEXT_PUBLIC_WEB3AUTH_CLIENT_ID ?? '', walletConnectKey: process.env.NEXT_PUBLIC_WALLETCONNECT_KEY ?? '', web3AuthNetwork: process.env.NEXT_PUBLIC_WEB3AUTH_NETWORK ?? '', From f7ce6726bf5ba786f0c84df586fbb29e20a8f6a0 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi Date: Tue, 14 Jan 2025 00:03:59 -0700 Subject: [PATCH 13/48] fix: re-add proper rendering logic to bank page --- pages/bank.tsx | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/pages/bank.tsx b/pages/bank.tsx index e6a8767..b7e7661 100644 --- a/pages/bank.tsx +++ b/pages/bank.tsx @@ -254,18 +254,17 @@ export default function Bank() { > Bank - {combinedBalances.length > 0 && ( -
    - setSearchTerm(e.target.value)} - /> - -
    - )} + +
    + setSearchTerm(e.target.value)} + /> + +
  • {activeTab === 'assets' && - (combinedBalances.length === 0 ? ( + (combinedBalances.length === 0 && !isLoading ? ( ) : ( ))} {activeTab === 'history' && - (sendTxs.length === 0 ? ( + (totalPages === 0 ? ( ) : ( Date: Tue, 14 Jan 2025 12:35:35 -0700 Subject: [PATCH 14/48] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Félix C. Morency <1102868+fmorency@users.noreply.github.com> Signed-off-by: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index be4b270..aeaff15 100644 --- a/README.md +++ b/README.md @@ -31,10 +31,10 @@ NEXT_PUBLIC_RPC_URL= NEXT_PUBLIC_API_URL= NEXT_PUBLIC_EXPLORER_URL= NEXT_PUBLIC_INDEXER_URL= -NEXT_PUBLIC_OSMOSIS_TESTNET_CHAIN_ID= -NEXT_PUBLIC_OSMOSIS_TESTNET_RPC_URL= -NEXT_PUBLIC_OSMOSIS_TESTNET_API_URL= -NEXT_PUBLIC_OSMOSIS_TESTNET_EXPLORER_URL= +NEXT_PUBLIC_OSMOSIS_CHAIN_ID= +NEXT_PUBLIC_OSMOSIS_RPC_URL= +NEXT_PUBLIC_OSMOSIS_API_URL= +NEXT_PUBLIC_OSMOSIS_EXPLORER_URL= ``` where From fb9cfff4c6d0b4decfb77146796d5d251386644c Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:35:47 -0700 Subject: [PATCH 15/48] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Félix C. Morency <1102868+fmorency@users.noreply.github.com> Signed-off-by: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index aeaff15..d25f2be 100644 --- a/README.md +++ b/README.md @@ -49,10 +49,10 @@ where - `NEXT_PUBLIC_API_URL` is the chain API URL - `NEXT_PUBLIC_EXPLORER_URL` is the block explorer URL - `NEXT_PUBLIC_INDEXER_URL` is the YACI indexer URL -- `NEXT_PUBLIC_OSMOSIS_TESTNET_CHAIN_ID` is the osmosis testnet chain ID -- `NEXT_PUBLIC_OSMOSIS_TESTNET_RPC_URL` is the osmosis testnet RPC URL -- `NEXT_PUBLIC_OSMOSIS_TESTNET_API_URL` is the osmosis testnet API URL -- `NEXT_PUBLIC_OSMOSIS_TESTNET_EXPLORER_URL` is the osmosis testnet block explorer URL +- `NEXT_PUBLIC_OSMOSIS_CHAIN_ID` is the osmosis chain ID +- `NEXT_PUBLIC_OSMOSIS_RPC_URL` is the osmosis RPC URL +- `NEXT_PUBLIC_OSMOSIS_API_URL` is the osmosis API URL +- `NEXT_PUBLIC_OSMOSIS_EXPLORER_URL` is the osmosis block explorer URL ### Development From 09bf7059dbb77c347a76467624fc71cd36afe232 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:35:57 -0700 Subject: [PATCH 16/48] Update config/env.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Félix C. Morency <1102868+fmorency@users.noreply.github.com> Signed-off-by: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> --- config/env.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/env.ts b/config/env.ts index 5bef3ad..70cd771 100644 --- a/config/env.ts +++ b/config/env.ts @@ -2,8 +2,8 @@ const env = { chainId: process.env.NEXT_PUBLIC_CHAIN_ID ?? '', rpcUrl: process.env.NEXT_PUBLIC_RPC_URL ?? '', explorerUrl: process.env.NEXT_PUBLIC_EXPLORER_URL ?? '', - osmosisTestnetExplorerUrl: process.env.NEXT_PUBLIC_OSMOSIS_TESTNET_EXPLORER_URL ?? '', - osmosisTestnetChainId: process.env.NEXT_PUBLIC_OSMOSIS_TESTNET_CHAIN_ID ?? '', + osmosisExplorerUrl: process.env.NEXT_PUBLIC_OSMOSIS_EXPLORER_URL ?? '', + osmosisChainId: process.env.NEXT_PUBLIC_OSMOSIS_CHAIN_ID ?? '', web3AuthClientId: process.env.NEXT_PUBLIC_WEB3AUTH_CLIENT_ID ?? '', walletConnectKey: process.env.NEXT_PUBLIC_WALLETCONNECT_KEY ?? '', web3AuthNetwork: process.env.NEXT_PUBLIC_WEB3AUTH_NETWORK ?? '', From 7b1a642b6cd64baac255fa644a8bda347a5c9191 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi Date: Tue, 14 Jan 2025 12:34:53 -0700 Subject: [PATCH 17/48] chore: add coderabbit changes --- components/bank/components/historyBox.tsx | 54 +++++++++++++++-------- components/bank/forms/ibcSendForm.tsx | 17 ++++--- components/groups/components/myGroups.tsx | 5 ++- 3 files changed, 52 insertions(+), 24 deletions(-) diff --git a/components/bank/components/historyBox.tsx b/components/bank/components/historyBox.tsx index b5cb321..127cdd7 100644 --- a/components/bank/components/historyBox.tsx +++ b/components/bank/components/historyBox.tsx @@ -221,20 +221,34 @@ export function HistoryBox({ let metadata = metadatas?.metadatas.find(m => m.base === amt.denom); if (amt.denom.startsWith('ibc/')) { - metadata = { - description: assetInfo?.description ?? '', - denom_units: - assetInfo?.denom_units?.map(unit => ({ - ...unit, - aliases: unit.aliases || [], - })) ?? [], - base: assetInfo?.base ?? '', - display: assetInfo?.display ?? '', - name: assetInfo?.name ?? '', - symbol: assetInfo?.symbol ?? '', - uri: assetInfo?.logo_URIs?.svg ?? assetInfo?.logo_URIs?.png ?? '', - uri_hash: assetInfo?.logo_URIs?.svg ?? assetInfo?.logo_URIs?.png ?? '', - }; + if (assetInfo) { + metadata = { + description: assetInfo?.description ?? '', + denom_units: + assetInfo?.denom_units?.map(unit => ({ + ...unit, + aliases: unit.aliases || [], + })) ?? [], + base: assetInfo?.base ?? '', + display: assetInfo?.display ?? '', + name: assetInfo?.name ?? '', + symbol: assetInfo?.symbol ?? '', + uri: assetInfo?.logo_URIs?.svg ?? assetInfo?.logo_URIs?.png ?? '', + uri_hash: assetInfo?.logo_URIs?.svg ?? assetInfo?.logo_URIs?.png ?? '', + }; + } else { + // assetInfo is undefined + metadata = { + description: '', + denom_units: [], + base: '', + display: '', + name: '', + symbol: '', + uri: '', + uri_hash: '', + }; + } } return ; @@ -252,8 +266,10 @@ export function HistoryBox({ if (amt.denom.startsWith('ibc/')) { const assetInfo = denomToAsset(env.chain, amt.denom); - if (assetInfo?.traces?.[0]?.counterparty?.base_denom) { - display = assetInfo.traces[0].counterparty.base_denom.slice(1); + if (assetInfo?.traces && assetInfo.traces.length > 0) { + if (assetInfo.traces[0].counterparty?.base_denom) { + display = assetInfo.traces[0].counterparty.base_denom.slice(1); + } } } @@ -301,8 +317,10 @@ export function HistoryBox({ if (amt.denom.startsWith('ibc/')) { const assetInfo = denomToAsset(env.chain, amt.denom); - if (assetInfo?.traces?.[0]?.counterparty?.base_denom) { - baseDenom = assetInfo.traces[0].counterparty.base_denom.slice(1); + if (assetInfo?.traces && assetInfo.traces.length > 0) { + if (assetInfo.traces[0].counterparty?.base_denom) { + baseDenom = assetInfo.traces[0].counterparty.base_denom.slice(1); + } } } diff --git a/components/bank/forms/ibcSendForm.tsx b/components/bank/forms/ibcSendForm.tsx index 507da09..321159b 100644 --- a/components/bank/forms/ibcSendForm.tsx +++ b/components/bank/forms/ibcSendForm.tsx @@ -81,6 +81,17 @@ export default function IbcSendForm({ availableToChains: IbcChain[]; }>) { const { address: osmosisAddress } = useChain(env.osmosisTestnetChain); + + const formatTokenDisplayName = (displayName: string) => { + if (displayName.startsWith('factory')) { + return displayName.split('/').pop()?.toUpperCase(); + } + if (displayName.startsWith('u')) { + return displayName.slice(1).toUpperCase(); + } + return truncateString(displayName, 10).toUpperCase(); + }; + const [isSending, setIsSending] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [feeWarning, setFeeWarning] = useState(''); @@ -571,11 +582,7 @@ export default function IbcSendForm({ values.selectedToken?.denom ?? 'Select'; - return tokenDisplayName.startsWith('factory') - ? tokenDisplayName.split('/').pop()?.toUpperCase() - : tokenDisplayName.startsWith('u') - ? tokenDisplayName.slice(1).toUpperCase() - : truncateString(tokenDisplayName, 10).toUpperCase(); + return formatTokenDisplayName(tokenDisplayName); })()} diff --git a/components/groups/components/myGroups.tsx b/components/groups/components/myGroups.tsx index e5756a6..91204c4 100644 --- a/components/groups/components/myGroups.tsx +++ b/components/groups/components/myGroups.tsx @@ -264,7 +264,10 @@ export function YourGroups({ if (coreBalance.denom.startsWith('ibc/')) { const assetInfo = denomToAsset(env.chain, coreBalance.denom); - const baseDenom = assetInfo?.traces?.[1]?.counterparty?.base_denom; + let baseDenom = ''; + if (assetInfo?.traces && assetInfo.traces.length > 1) { + baseDenom = assetInfo.traces[1]?.counterparty?.base_denom ?? ''; + } return { denom: baseDenom ?? '', // normalized denom (e.g., 'umfx') From 6d570b0f88687459fb12e21d8fd29545e6ff71cc Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:36:56 -0700 Subject: [PATCH 18/48] Update config/env.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Félix C. Morency <1102868+fmorency@users.noreply.github.com> Signed-off-by: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> --- config/env.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/env.ts b/config/env.ts index 70cd771..731e3ea 100644 --- a/config/env.ts +++ b/config/env.ts @@ -8,7 +8,7 @@ const env = { walletConnectKey: process.env.NEXT_PUBLIC_WALLETCONNECT_KEY ?? '', web3AuthNetwork: process.env.NEXT_PUBLIC_WEB3AUTH_NETWORK ?? '', chain: process.env.NEXT_PUBLIC_CHAIN ?? '', - osmosisTestnetChain: process.env.NEXT_PUBLIC_OSMOSIS_TESTNET_CHAIN ?? '', + osmosisChain: process.env.NEXT_PUBLIC_OSMOSIS_CHAIN ?? '', chainTier: process.env.NEXT_PUBLIC_CHAIN_TIER ?? '', apiUrl: process.env.NEXT_PUBLIC_API_URL ?? '', indexerUrl: process.env.NEXT_PUBLIC_INDEXER_URL ?? '', From bd0319f188108ef98289d320962906d7fde723a2 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:37:05 -0700 Subject: [PATCH 19/48] Update config/env.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Félix C. Morency <1102868+fmorency@users.noreply.github.com> Signed-off-by: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> --- config/env.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/env.ts b/config/env.ts index 731e3ea..65a47c5 100644 --- a/config/env.ts +++ b/config/env.ts @@ -12,8 +12,8 @@ const env = { chainTier: process.env.NEXT_PUBLIC_CHAIN_TIER ?? '', apiUrl: process.env.NEXT_PUBLIC_API_URL ?? '', indexerUrl: process.env.NEXT_PUBLIC_INDEXER_URL ?? '', - osmosisTestnetApiUrl: process.env.NEXT_PUBLIC_OSMOSIS_TESTNET_API_URL ?? '', - osmosisTestnetRpcUrl: process.env.NEXT_PUBLIC_OSMOSIS_TESTNET_RPC_URL ?? '', + osmosisApiUrl: process.env.NEXT_PUBLIC_OSMOSIS_API_URL ?? '', + osmosisRpcUrl: process.env.NEXT_PUBLIC_OSMOSIS_RPC_URL ?? '', }; export default env; From 958a27ffa474825a35d913c3c5b4136a10201a89 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:37:20 -0700 Subject: [PATCH 20/48] Update components/bank/components/sendBox.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Félix C. Morency <1102868+fmorency@users.noreply.github.com> Signed-off-by: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> --- components/bank/components/sendBox.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/bank/components/sendBox.tsx b/components/bank/components/sendBox.tsx index 974bd99..b3e49e5 100644 --- a/components/bank/components/sendBox.tsx +++ b/components/bank/components/sendBox.tsx @@ -52,7 +52,7 @@ export default function SendBox({ prefix: 'manifest', }, { - id: env.osmosisTestnetChain, + id: env.osmosisChain, name: 'Osmosis', icon: '/osmosis.svg', prefix: 'osmo', From de95589424f74635f9435b8fd9397ec32168d372 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:37:29 -0700 Subject: [PATCH 21/48] Update pages/_app.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Félix C. Morency <1102868+fmorency@users.noreply.github.com> Signed-off-by: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> --- pages/_app.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/_app.tsx b/pages/_app.tsx index 813c90a..068650d 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -190,8 +190,8 @@ function ManifestApp({ Component, pageProps }: ManifestAppProps) { rest: [env.apiUrl], }, ['osmosistestnet']: { - rpc: [env.osmosisTestnetRpcUrl], - rest: [env.osmosisTestnetApiUrl], + rpc: [env.osmosisRpcUrl], + rest: [env.osmosisApiUrl], }, }, }; From 1e474cf33c2610f6992736a6734887629435bc50 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:37:39 -0700 Subject: [PATCH 22/48] Update pages/bank.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Félix C. Morency <1102868+fmorency@users.noreply.github.com> Signed-off-by: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> --- pages/bank.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/bank.tsx b/pages/bank.tsx index b7e7661..7d6f59f 100644 --- a/pages/bank.tsx +++ b/pages/bank.tsx @@ -155,7 +155,7 @@ export default function Bank() { return mfxCombinedBalance ? [mfxCombinedBalance, ...otherBalances] : otherBalances; }, [balances, resolvedBalances, metadatas]); - const { address: osmosisAddress } = useChain(env.osmosisTestnetChain); + const { address: osmosisAddress } = useChain(env.osmosisChain); const { balances: osmosisBalances, isBalancesLoading: isOsmosisBalancesLoading, From 036be3f4677493408ed8b140b4b6391496d3c38f Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:37:46 -0700 Subject: [PATCH 23/48] Update scripts/ibcTransferAll.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Félix C. Morency <1102868+fmorency@users.noreply.github.com> Signed-off-by: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> --- scripts/ibcTransferAll.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/ibcTransferAll.ts b/scripts/ibcTransferAll.ts index 7324d6a..1a88de9 100644 --- a/scripts/ibcTransferAll.ts +++ b/scripts/ibcTransferAll.ts @@ -27,9 +27,9 @@ const getIbcInfo = (fromChain: string, toChain: string) => { // Configuration const MANIFEST_RPC = env.rpcUrl; -const OSMOSIS_RPC = env.osmosisTestnetRpcUrl; +const OSMOSIS_RPC = env.osmosisRpcUrl; const SOURCE_CHAIN = env.chain; -const TARGET_CHAIN = env.osmosisTestnetChain; +const TARGET_CHAIN = env.osmosisChain; // Helper function to format token info for asset list function formatTokenForAssetList(ibcDenom: string, denomTrace: any, originalDenom: string) { From 65dbb12f49f6b763efe075264ae6be6cf3dc6623 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:37:57 -0700 Subject: [PATCH 24/48] Update scripts/ibcTransferAll.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Félix C. Morency <1102868+fmorency@users.noreply.github.com> Signed-off-by: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> --- scripts/ibcTransferAll.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/ibcTransferAll.ts b/scripts/ibcTransferAll.ts index 1a88de9..73185a9 100644 --- a/scripts/ibcTransferAll.ts +++ b/scripts/ibcTransferAll.ts @@ -9,9 +9,9 @@ import * as path from 'path'; // Environment Configuration const env = { rpcUrl: 'https://nodes.liftedinit.tech/manifest/testnet/rpc', - osmosisTestnetRpcUrl: 'https://rpc.osmotest5.osmosis.zone', + osmosisRpcUrl: 'https://rpc.osmotest5.osmosis.zone', chain: 'manifest-testnet', - osmosisTestnetChain: 'osmo-test-5', + osmosisChain: 'osmo-test-5', }; // IBC Configuration From f0e7e8d00dc3773f54e5e57b7e947c0c88d0ce96 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:38:21 -0700 Subject: [PATCH 25/48] Update components/bank/forms/ibcSendForm.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Félix C. Morency <1102868+fmorency@users.noreply.github.com> Signed-off-by: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> --- components/bank/forms/ibcSendForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/bank/forms/ibcSendForm.tsx b/components/bank/forms/ibcSendForm.tsx index 321159b..5f91be4 100644 --- a/components/bank/forms/ibcSendForm.tsx +++ b/components/bank/forms/ibcSendForm.tsx @@ -80,7 +80,7 @@ export default function IbcSendForm({ admin?: string; availableToChains: IbcChain[]; }>) { - const { address: osmosisAddress } = useChain(env.osmosisTestnetChain); + const { address: osmosisAddress } = useChain(env.osmosisChain); const formatTokenDisplayName = (displayName: string) => { if (displayName.startsWith('factory')) { From 52672ce29640817d9215256402b625bce2492354 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:38:34 -0700 Subject: [PATCH 26/48] Update components/bank/forms/ibcSendForm.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Félix C. Morency <1102868+fmorency@users.noreply.github.com> Signed-off-by: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> --- components/bank/forms/ibcSendForm.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/bank/forms/ibcSendForm.tsx b/components/bank/forms/ibcSendForm.tsx index 5f91be4..3cd3bd1 100644 --- a/components/bank/forms/ibcSendForm.tsx +++ b/components/bank/forms/ibcSendForm.tsx @@ -96,10 +96,10 @@ export default function IbcSendForm({ const [searchTerm, setSearchTerm] = useState(''); const [feeWarning, setFeeWarning] = useState(''); const { tx } = useTx( - selectedFromChain === env.osmosisTestnetChain ? env.osmosisTestnetChain : env.chain + selectedFromChain === env.osmosisChain ? env.osmosisChain : env.chain ); const { estimateFee } = useFeeEstimation( - selectedFromChain === env.osmosisTestnetChain ? env.osmosisTestnetChain : env.chain + selectedFromChain === env.osmosisChain ? env.osmosisChain : env.chain ); const { transfer } = ibc.applications.transfer.v1.MessageComposer.withTypeUrl; const [isContactsOpen, setIsContactsOpen] = useState(false); From d7bcb839a147ae4a3b15eb36c26ea71da40c23be Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:38:50 -0700 Subject: [PATCH 27/48] Update components/bank/forms/ibcSendForm.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Félix C. Morency <1102868+fmorency@users.noreply.github.com> Signed-off-by: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> --- components/bank/forms/ibcSendForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/bank/forms/ibcSendForm.tsx b/components/bank/forms/ibcSendForm.tsx index 3cd3bd1..ac1acda 100644 --- a/components/bank/forms/ibcSendForm.tsx +++ b/components/bank/forms/ibcSendForm.tsx @@ -117,7 +117,7 @@ export default function IbcSendForm({ // Update the filtered balances logic to use passed props instead of hooks const filteredBalances = useMemo(() => { const sourceBalances = - selectedFromChain === env.osmosisTestnetChain ? osmosisBalances : balances; + selectedFromChain === env.osmosisChain ? osmosisBalances : balances; return sourceBalances?.filter(token => { const displayName = token.metadata?.display ?? token.denom; From 3e1c22fa11ec07fcd5fe7fe7121d6888b77188bf Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:39:42 -0700 Subject: [PATCH 28/48] Update components/bank/forms/ibcSendForm.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Félix C. Morency <1102868+fmorency@users.noreply.github.com> Signed-off-by: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> --- components/bank/forms/ibcSendForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/bank/forms/ibcSendForm.tsx b/components/bank/forms/ibcSendForm.tsx index ac1acda..2816d4a 100644 --- a/components/bank/forms/ibcSendForm.tsx +++ b/components/bank/forms/ibcSendForm.tsx @@ -128,7 +128,7 @@ export default function IbcSendForm({ // Update initialSelectedToken to consider the chain const initialSelectedToken = useMemo(() => { const sourceBalances = - selectedFromChain === env.osmosisTestnetChain ? osmosisBalances : balances; + selectedFromChain === env.osmosisChain ? osmosisBalances : balances; return ( sourceBalances?.find(token => token.coreDenom === selectedDenom) || From 38129b10cc21af8d74aed91ca60c6f196ecd8270 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:39:55 -0700 Subject: [PATCH 29/48] Update components/bank/forms/ibcSendForm.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Félix C. Morency <1102868+fmorency@users.noreply.github.com> Signed-off-by: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> --- components/bank/forms/ibcSendForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/bank/forms/ibcSendForm.tsx b/components/bank/forms/ibcSendForm.tsx index 2816d4a..c15c677 100644 --- a/components/bank/forms/ibcSendForm.tsx +++ b/components/bank/forms/ibcSendForm.tsx @@ -139,7 +139,7 @@ export default function IbcSendForm({ // Update the loading check if ( - (selectedFromChain === env.osmosisTestnetChain + (selectedFromChain === env.osmosisChain ? isOsmosisBalancesLoading : isBalancesLoading) || !initialSelectedToken From 16da549bbf81faef782e0a4f5f6b41de40d3f3c1 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:40:18 -0700 Subject: [PATCH 30/48] Update components/bank/forms/ibcSendForm.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Félix C. Morency <1102868+fmorency@users.noreply.github.com> Signed-off-by: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> --- components/bank/forms/ibcSendForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/bank/forms/ibcSendForm.tsx b/components/bank/forms/ibcSendForm.tsx index c15c677..add21c1 100644 --- a/components/bank/forms/ibcSendForm.tsx +++ b/components/bank/forms/ibcSendForm.tsx @@ -212,7 +212,7 @@ export default function IbcSendForm({ sourceChannel: source_channel, sender: admin ? admin - : selectedFromChain === env.osmosisTestnetChain + : selectedFromChain === env.osmosisChain ? (osmosisAddress ?? '') : (address ?? ''), receiver: values.recipient ?? '', From ed1b3ee056739641efd427e12e2f66ce4fed8c4d Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:40:35 -0700 Subject: [PATCH 31/48] Update components/bank/forms/ibcSendForm.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Félix C. Morency <1102868+fmorency@users.noreply.github.com> Signed-off-by: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> --- components/bank/forms/ibcSendForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/bank/forms/ibcSendForm.tsx b/components/bank/forms/ibcSendForm.tsx index add21c1..910290a 100644 --- a/components/bank/forms/ibcSendForm.tsx +++ b/components/bank/forms/ibcSendForm.tsx @@ -242,7 +242,7 @@ export default function IbcSendForm({ : transferMsg; const fee = await estimateFee( - selectedFromChain === env.osmosisTestnetChain ? (osmosisAddress ?? '') : (address ?? ''), + selectedFromChain === env.osmosisChain ? (osmosisAddress ?? '') : (address ?? ''), [msg] ); From c748bc0c17504e4f02ec1308afbe62312961cadc Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:40:50 -0700 Subject: [PATCH 32/48] Update hooks/useLcdQueryClient.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Félix C. Morency <1102868+fmorency@users.noreply.github.com> Signed-off-by: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> --- hooks/useLcdQueryClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hooks/useLcdQueryClient.ts b/hooks/useLcdQueryClient.ts index 4c0774a..5bcde93 100644 --- a/hooks/useLcdQueryClient.ts +++ b/hooks/useLcdQueryClient.ts @@ -22,7 +22,7 @@ export const useLcdQueryClient = () => { export const useOsmosisLcdQueryClient = () => { const lcdQueryClient = useQuery({ - queryKey: ['lcdQueryClientOsmosis', env.osmosisTestnetApiUrl], + queryKey: ['lcdQueryClientOsmosis', env.osmosisApiUrl], queryFn: () => createLcdQueryClient({ restEndpoint: env.osmosisTestnetApiUrl, From 3ff70b4cb601269a3d0c719bdfd5bf208266320a Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:41:09 -0700 Subject: [PATCH 33/48] Update hooks/useLcdQueryClient.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Félix C. Morency <1102868+fmorency@users.noreply.github.com> Signed-off-by: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> --- hooks/useLcdQueryClient.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hooks/useLcdQueryClient.ts b/hooks/useLcdQueryClient.ts index 5bcde93..510f3ff 100644 --- a/hooks/useLcdQueryClient.ts +++ b/hooks/useLcdQueryClient.ts @@ -25,9 +25,9 @@ export const useOsmosisLcdQueryClient = () => { queryKey: ['lcdQueryClientOsmosis', env.osmosisApiUrl], queryFn: () => createLcdQueryClient({ - restEndpoint: env.osmosisTestnetApiUrl, + restEndpoint: env.osmosisApiUrl, }), - enabled: !!env.osmosisTestnetApiUrl, + enabled: !!env.osmosisApiUrl, staleTime: Infinity, }); From e50afae16adf1d3d9edff7a91aecfb15523a8589 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:41:19 -0700 Subject: [PATCH 34/48] Update hooks/useTx.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Félix C. Morency <1102868+fmorency@users.noreply.github.com> Signed-off-by: Joseph Chalabi <100090645+chalabi2@users.noreply.github.com> --- hooks/useTx.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hooks/useTx.tsx b/hooks/useTx.tsx index 8b91602..b181092 100644 --- a/hooks/useTx.tsx +++ b/hooks/useTx.tsx @@ -47,7 +47,7 @@ export const useTx = (chainName: string) => { const { setToastMessage } = useToast(); const [isSigning, setIsSigning] = useState(false); const explorerUrl = - chainName === env.osmosisTestnetChain ? env.osmosisTestnetExplorerUrl : env.explorerUrl; + chainName === env.osmosisChain ? env.osmosisExplorerUrl : env.explorerUrl; const tx = async (msgs: Msg[], options: TxOptions) => { if (!address) { From 891c4b675cc4f70ef9de3d295957253f1b9333e2 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi Date: Tue, 14 Jan 2025 12:46:49 -0700 Subject: [PATCH 35/48] chore: add transfer script instructions --- components/bank/forms/ibcSendForm.tsx | 14 ++++---------- package.json | 3 ++- pages/bank.tsx | 2 +- scripts/ibcTransferAll.ts | 5 ++++- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/components/bank/forms/ibcSendForm.tsx b/components/bank/forms/ibcSendForm.tsx index 910290a..79ee0af 100644 --- a/components/bank/forms/ibcSendForm.tsx +++ b/components/bank/forms/ibcSendForm.tsx @@ -95,9 +95,7 @@ export default function IbcSendForm({ const [isSending, setIsSending] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [feeWarning, setFeeWarning] = useState(''); - const { tx } = useTx( - selectedFromChain === env.osmosisChain ? env.osmosisChain : env.chain - ); + const { tx } = useTx(selectedFromChain === env.osmosisChain ? env.osmosisChain : env.chain); const { estimateFee } = useFeeEstimation( selectedFromChain === env.osmosisChain ? env.osmosisChain : env.chain ); @@ -116,8 +114,7 @@ export default function IbcSendForm({ // Update the filtered balances logic to use passed props instead of hooks const filteredBalances = useMemo(() => { - const sourceBalances = - selectedFromChain === env.osmosisChain ? osmosisBalances : balances; + const sourceBalances = selectedFromChain === env.osmosisChain ? osmosisBalances : balances; return sourceBalances?.filter(token => { const displayName = token.metadata?.display ?? token.denom; @@ -127,8 +124,7 @@ export default function IbcSendForm({ // Update initialSelectedToken to consider the chain const initialSelectedToken = useMemo(() => { - const sourceBalances = - selectedFromChain === env.osmosisChain ? osmosisBalances : balances; + const sourceBalances = selectedFromChain === env.osmosisChain ? osmosisBalances : balances; return ( sourceBalances?.find(token => token.coreDenom === selectedDenom) || @@ -139,9 +135,7 @@ export default function IbcSendForm({ // Update the loading check if ( - (selectedFromChain === env.osmosisChain - ? isOsmosisBalancesLoading - : isBalancesLoading) || + (selectedFromChain === env.osmosisChain ? isOsmosisBalancesLoading : isBalancesLoading) || !initialSelectedToken ) { return null; diff --git a/package.json b/package.json index d0d7f70..febb42d 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "update-deps": "bunx npm-check-updates --root --format group -i", "test:coverage": "bun test --coverage", "test:coverage:lcov": "bun run test:coverage --coverage-reporter=lcov --coverage-dir ./coverage", - "coverage:upload": "codecov" + "coverage:upload": "codecov", + "ibc-transfer": "tsx scripts/ibcTransferAll.ts" }, "author": "The Lifted Initiative", "license": "MIT", diff --git a/pages/bank.tsx b/pages/bank.tsx index 7d6f59f..c89ccd2 100644 --- a/pages/bank.tsx +++ b/pages/bank.tsx @@ -191,7 +191,7 @@ export default function Bank() { // Handle IBC tokens if (coreBalance.denom.startsWith('ibc/')) { - const assetInfo = denomToAsset(env.osmosisTestnetChain, coreBalance.denom); + const assetInfo = denomToAsset(env.osmosisChain, coreBalance.denom); const baseDenom = assetInfo?.traces?.[1]?.counterparty?.base_denom; diff --git a/scripts/ibcTransferAll.ts b/scripts/ibcTransferAll.ts index 73185a9..c87219c 100644 --- a/scripts/ibcTransferAll.ts +++ b/scripts/ibcTransferAll.ts @@ -1,6 +1,9 @@ +// This script is used to transfer 1/6th of all IBC tokens (factory tokens included) from Manifest to Osmosis +// you can run this script by providing a mnemonic as an environment variable: `WALLET_MNEMONIC="..." bun run ibc-transfer` + import { DirectSecp256k1HdWallet } from '@cosmjs/proto-signing'; import { SigningStargateClient } from '@cosmjs/stargate'; -import { cosmos, ibc } from '@liftedinit/manifestjs'; + import { MsgTransfer } from '@liftedinit/manifestjs/dist/codegen/ibc/applications/transfer/v1/tx'; import axios from 'axios'; import * as fs from 'fs'; From 2cd2f92017858399d200b964672150ac19809e38 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi Date: Tue, 14 Jan 2025 12:49:55 -0700 Subject: [PATCH 36/48] chore: prettier --- hooks/useTx.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hooks/useTx.tsx b/hooks/useTx.tsx index b181092..e87598f 100644 --- a/hooks/useTx.tsx +++ b/hooks/useTx.tsx @@ -46,8 +46,7 @@ export const useTx = (chainName: string) => { const { address, getSigningStargateClient, estimateFee } = useChain(chainName); const { setToastMessage } = useToast(); const [isSigning, setIsSigning] = useState(false); - const explorerUrl = - chainName === env.osmosisChain ? env.osmosisExplorerUrl : env.explorerUrl; + const explorerUrl = chainName === env.osmosisChain ? env.osmosisExplorerUrl : env.explorerUrl; const tx = async (msgs: Msg[], options: TxOptions) => { if (!address) { From 5fef3a428286d2ba1cf6261a52f673bad404b0a9 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi Date: Tue, 14 Jan 2025 13:15:59 -0700 Subject: [PATCH 37/48] fix: fix form tests --- .../components/__tests__/sendBox.test.tsx | 66 ++++++++++++------- components/bank/components/sendBox.tsx | 4 +- .../bank/forms/__tests__/ibcSendForm.test.tsx | 13 ++-- components/bank/forms/ibcSendForm.tsx | 4 +- tests/mock.ts | 21 ++++++ tests/render.tsx | 4 +- 6 files changed, 78 insertions(+), 34 deletions(-) diff --git a/components/bank/components/__tests__/sendBox.test.tsx b/components/bank/components/__tests__/sendBox.test.tsx index 6853172..a405039 100644 --- a/components/bank/components/__tests__/sendBox.test.tsx +++ b/components/bank/components/__tests__/sendBox.test.tsx @@ -1,7 +1,7 @@ import { test, expect, afterEach, describe, mock, jest } from 'bun:test'; import React from 'react'; import matchers from '@testing-library/jest-dom/matchers'; -import { screen, cleanup, waitFor, fireEvent } from '@testing-library/react'; +import { screen, cleanup, waitFor, fireEvent, within } from '@testing-library/react'; import SendBox from '@/components/bank/components/sendBox'; import { mockBalances } from '@/tests/mock'; import { renderWithChainProvider } from '@/tests/render'; @@ -17,6 +17,14 @@ mock.module('next/router', () => ({ }), })); +// Add this mock before your tests +mock.module('next/image', () => ({ + default: (props: any) => { + // eslint-disable-next-line @next/next/no-img-element + return {props.alt; + }, +})); + const renderWithProps = (props = {}) => { const defaultProps = { address: 'test_address', @@ -40,42 +48,56 @@ describe('SendBox', () => { test('toggles between Send and Cross-Chain Transfer', async () => { renderWithProps(); - expect(screen.getByText('Amount')).toBeInTheDocument(); + // Check initial send form + expect(screen.getByPlaceholderText('0.00')).toBeInTheDocument(); + expect(screen.queryByLabelText('to-chain-selector')).not.toBeInTheDocument(); - fireEvent.click(screen.getByText('Cross-Chain Transfer')); - await waitFor(() => expect(screen.getByText('Chain')).toBeInTheDocument()); - }); + // Switch to cross-chain transfer + fireEvent.click(screen.getByLabelText('cross-chain-transfer-tab')); - test('displays chain selection dropdown when in Cross-Chain Transfer mode', async () => { - renderWithProps(); - fireEvent.click(screen.getByText('Cross-Chain Transfer')); - await waitFor(() => expect(screen.getByText('Chain')).toBeInTheDocument()); + // Verify cross-chain elements are present + await waitFor(() => { + expect(screen.getByLabelText('from-chain-selector')).toBeInTheDocument(); + expect(screen.getByLabelText('to-chain-selector')).toBeInTheDocument(); + }); }); - test('selects a chain in Cross-Chain Transfer mode', async () => { + test('displays chain selection dropdowns in Cross-Chain Transfer mode', async () => { renderWithProps(); - const crossChainBtn = screen.getByLabelText('cross-chain-transfer-tab'); - fireEvent.click(crossChainBtn); + fireEvent.click(screen.getByLabelText('cross-chain-transfer-tab')); await waitFor(() => { - const chainSelector = screen.getByLabelText('chain-selector'); - expect(chainSelector).toBeTruthy(); + expect(screen.getByLabelText('from-chain-selector')).toBeInTheDocument(); + expect(screen.getByLabelText('to-chain-selector')).toBeInTheDocument(); }); + }); - const chainSelector = screen.getByLabelText('chain-selector'); - fireEvent.click(chainSelector); + test('selects chains in Cross-Chain Transfer mode', async () => { + renderWithProps(); + fireEvent.click(screen.getByLabelText('cross-chain-transfer-tab')); + // Select destination chain await waitFor(() => { - const osmosisOption = screen.getByText('Osmosis'); - expect(osmosisOption).toBeTruthy(); + const toChainSelector = screen.getByLabelText('to-chain-selector'); + expect(toChainSelector).toBeInTheDocument(); + fireEvent.click(toChainSelector); }); - const osmosisOption = screen.getByText('Osmosis'); - fireEvent.click(osmosisOption); + // Select Osmosis as destination + await waitFor(() => { + const toChainDropdown = screen.getByLabelText('to-chain-selector').closest('.dropdown'); + expect(toChainDropdown).toBeInTheDocument(); + + // Find and click the Osmosis option within the dropdown + const osmosisOption = within(toChainDropdown!).getByText('Osmosis'); + fireEvent.click(osmosisOption); + }); + // Verify selection using text content instead of complex matchers await waitFor(() => { - const updatedChainSelector = screen.getByLabelText('chain-selector'); - expect(updatedChainSelector.textContent).toContain('Osmosis'); + const selectedChain = screen.getByLabelText('to-chain-selector'); + const chainText = selectedChain.textContent; + expect(chainText?.includes('Osmosis')).toBe(true); }); }); }); diff --git a/components/bank/components/sendBox.tsx b/components/bank/components/sendBox.tsx index b3e49e5..8668ba3 100644 --- a/components/bank/components/sendBox.tsx +++ b/components/bank/components/sendBox.tsx @@ -48,13 +48,13 @@ export default function SendBox({ { id: env.chain, name: 'Manifest', - icon: '/logo.svg', + icon: 'logo.svg', prefix: 'manifest', }, { id: env.osmosisChain, name: 'Osmosis', - icon: '/osmosis.svg', + icon: 'osmosis.svg', prefix: 'osmo', }, ], diff --git a/components/bank/forms/__tests__/ibcSendForm.test.tsx b/components/bank/forms/__tests__/ibcSendForm.test.tsx index 7941702..f8e76a4 100644 --- a/components/bank/forms/__tests__/ibcSendForm.test.tsx +++ b/components/bank/forms/__tests__/ibcSendForm.test.tsx @@ -19,7 +19,7 @@ mock.module('next/router', () => ({ function renderWithProps(props = {}) { const defaultProps = { address: 'manifest1address', - destinationChain: 'osmosis', + destinationChain: 'osmosistestnet', balances: mockBalances, isBalancesLoading: false, refetchBalances: jest.fn(), @@ -27,13 +27,13 @@ function renderWithProps(props = {}) { setIsIbcTransfer: jest.fn(), ibcChains: [ { - id: 'osmosis', + id: 'osmosistestnet', name: 'Osmosis', icon: 'https://osmosis.zone/assets/icons/osmo-logo-icon.svg', prefix: 'osmo', }, ], - selectedChain: 'osmosis', + selectedChain: 'osmosistestnet', setSelectedChain: jest.fn(), }; @@ -45,9 +45,10 @@ describe('IbcSendForm Component', () => { test('renders form with correct details', () => { renderWithProps(); + expect(screen.getByText('From Chain')).toBeInTheDocument(); + expect(screen.getByText('To Chain')).toBeInTheDocument(); expect(screen.getByText('Amount')).toBeInTheDocument(); expect(screen.getByText('Send To')).toBeInTheDocument(); - expect(screen.getByText('Chain')).toBeInTheDocument(); }); test('empty balances', async () => { @@ -73,9 +74,9 @@ describe('IbcSendForm Component', () => { test('updates chain selector correctly', () => { renderWithProps(); - const chainSelector = screen.getByLabelText('chain-selector'); + const chainSelector = screen.getByRole('combobox', { name: 'to-chain-selector' }); fireEvent.click(chainSelector); - expect(chainSelector).toHaveTextContent('Osmosis'); + expect(screen.getByText('Osmosis')).toBeInTheDocument(); }); test('updates amount input correctly', () => { diff --git a/components/bank/forms/ibcSendForm.tsx b/components/bank/forms/ibcSendForm.tsx index 79ee0af..6002d54 100644 --- a/components/bank/forms/ibcSendForm.tsx +++ b/components/bank/forms/ibcSendForm.tsx @@ -446,13 +446,13 @@ export default function IbcSendForm({
    From a47b3d285bf7eb9e729b8d1f60056c0a3580ec17 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi Date: Tue, 14 Jan 2025 14:14:00 -0700 Subject: [PATCH 42/48] fix: proper module mock path for ibcSendForm in sendbox test --- .../components/__tests__/sendBox.test.tsx | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/components/bank/components/__tests__/sendBox.test.tsx b/components/bank/components/__tests__/sendBox.test.tsx index d7c1052..455e333 100644 --- a/components/bank/components/__tests__/sendBox.test.tsx +++ b/components/bank/components/__tests__/sendBox.test.tsx @@ -26,7 +26,7 @@ mock.module('next/image', () => ({ })); // Add this mock at the top of your test file -mock.module('../forms/ibcSendForm', () => ({ +mock.module('@/components/bank/forms/ibcSendForm', () => ({ default: (props: any) => { return (
    @@ -81,26 +81,19 @@ describe('SendBox', () => { fireEvent.click(screen.getByLabelText('cross-chain-transfer-tab')); // Verify cross-chain elements are present - await waitFor( - () => { - expect(screen.getByLabelText('from-chain-selector')).toBeInTheDocument(); - expect(screen.getByLabelText('to-chain-selector')).toBeInTheDocument(); - }, - { timeout: 2000 } - ); + await waitFor(() => { + expect(screen.getByLabelText('from-chain-selector')).toBeInTheDocument(); + expect(screen.getByLabelText('to-chain-selector')).toBeInTheDocument(); + }); }); - test( - 'displays chain selection dropdowns in Cross-Chain Transfer mode', - async () => { - renderWithProps(); - fireEvent.click(screen.getByLabelText('cross-chain-transfer-tab')); + test('displays chain selection dropdowns in Cross-Chain Transfer mode', async () => { + renderWithProps(); + fireEvent.click(screen.getByLabelText('cross-chain-transfer-tab')); - await waitFor(() => { - expect(screen.getByLabelText('from-chain-selector')).toBeInTheDocument(); - expect(screen.getByLabelText('to-chain-selector')).toBeInTheDocument(); - }); - }, - { timeout: 2000 } - ); + await waitFor(() => { + expect(screen.getByLabelText('from-chain-selector')).toBeInTheDocument(); + expect(screen.getByLabelText('to-chain-selector')).toBeInTheDocument(); + }); + }); }); From c09f5a51ef706b50fe1ac70da1dafa7b9fb1596d Mon Sep 17 00:00:00 2001 From: Joseph Chalabi Date: Thu, 30 Jan 2025 22:53:58 -0700 Subject: [PATCH 43/48] add skip go --- components/bank/forms/ibcSendForm.tsx | 5 ++ contexts/skipGoContext.tsx | 29 +++++++++++ pages/_app.tsx | 71 ++++++++++++++------------- 3 files changed, 71 insertions(+), 34 deletions(-) create mode 100644 contexts/skipGoContext.tsx diff --git a/components/bank/forms/ibcSendForm.tsx b/components/bank/forms/ibcSendForm.tsx index 6002d54..d015f93 100644 --- a/components/bank/forms/ibcSendForm.tsx +++ b/components/bank/forms/ibcSendForm.tsx @@ -33,6 +33,7 @@ import env from '@/config/env'; import { useChain } from '@cosmos-kit/react'; import { useSearchParams } from 'next/navigation'; import { Any } from 'cosmjs-types/google/protobuf/any'; +import { useSkipClient } from '@/contexts/skipGoContext'; //TODO: switch to main-net names export default function IbcSendForm({ @@ -99,6 +100,7 @@ export default function IbcSendForm({ const { estimateFee } = useFeeEstimation( selectedFromChain === env.osmosisChain ? env.osmosisChain : env.chain ); + const skipClient = useSkipClient(); const { transfer } = ibc.applications.transfer.v1.MessageComposer.withTypeUrl; const [isContactsOpen, setIsContactsOpen] = useState(false); const [isIconRotated, setIsIconRotated] = useState(false); @@ -193,6 +195,9 @@ export default function IbcSendForm({ const { source_port, source_channel } = getIbcInfo(selectedFromChain, selectedToChain); + const testnetChains = await skipClient.chains({}); + + console.log(testnetChains); const token = { denom: values.selectedToken.coreDenom, amount: amountInBaseUnits, diff --git a/contexts/skipGoContext.tsx b/contexts/skipGoContext.tsx new file mode 100644 index 0000000..2cae678 --- /dev/null +++ b/contexts/skipGoContext.tsx @@ -0,0 +1,29 @@ +import React, { createContext, useContext } from 'react'; +import { SkipClient } from '@skip-go/client'; + +// Create the context +interface SkipContextType { + skipClient: SkipClient; +} + +const SkipContext = createContext(undefined); + +// Create the provider component +interface SkipProviderProps { + children: React.ReactNode; +} + +export function SkipProvider({ children }: SkipProviderProps) { + const skipClient = new SkipClient({}); + + return {children}; +} + +// Create a custom hook to use the Skip client +export function useSkipClient() { + const context = useContext(SkipContext); + if (context === undefined) { + throw new Error('useSkipClient must be used within a SkipProvider'); + } + return context.skipClient; +} diff --git a/pages/_app.tsx b/pages/_app.tsx index 068650d..2529507 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -44,7 +44,8 @@ import { import MobileNav from '@/components/react/mobileNav'; import { OPENLOGIN_NETWORK_TYPE } from '@toruslabs/openlogin-utils'; -import { AssetList } from '@chain-registry/types'; + +import { SkipProvider } from '@/contexts/skipGoContext'; type ManifestAppProps = AppProps & { Component: AppProps['Component']; @@ -225,43 +226,45 @@ function ManifestApp({ Component, pageProps }: ManifestAppProps) { // @ts-ignore walletModal={TailwindModal} > - - -
    -
    - -
    + + + +
    +
    + +
    -
    -
    - +
    +
    + +
    +
    + +
    -
    - -
    -
    - {/* Web3auth signing modal */} - {isBrowser && - createPortal( - web3AuthPrompt?.resolve(false)} - data={web3AuthPrompt?.signData ?? ({} as SignData)} - approve={() => web3AuthPrompt?.resolve(true)} - reject={() => web3AuthPrompt?.resolve(false)} - />, - document.body - )} - - + {/* Web3auth signing modal */} + {isBrowser && + createPortal( + web3AuthPrompt?.resolve(false)} + data={web3AuthPrompt?.signData ?? ({} as SignData)} + approve={() => web3AuthPrompt?.resolve(true)} + reject={() => web3AuthPrompt?.resolve(false)} + />, + document.body + )} + + + } From 8b410498c743af0736931c634041e3ced525ce21 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi Date: Thu, 30 Jan 2025 23:21:53 -0700 Subject: [PATCH 44/48] check chain tier when rendering cross chain transfers --- components/bank/components/sendBox.tsx | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/components/bank/components/sendBox.tsx b/components/bank/components/sendBox.tsx index 8668ba3..feca025 100644 --- a/components/bank/components/sendBox.tsx +++ b/components/bank/components/sendBox.tsx @@ -87,17 +87,19 @@ export default function SendBox({ > Send - + {env.chainTier === 'testnet' && ( + + )}
    @@ -105,7 +107,7 @@ export default function SendBox({
    ) : ( <> - {activeTab === 'cross-chain' ? ( + {activeTab === 'cross-chain' && env.chainTier === 'testnet' ? ( Date: Sat, 1 Feb 2025 20:13:17 -0700 Subject: [PATCH 45/48] add skip routes and messages --- bun.lockb | Bin 544188 -> 601940 bytes components/bank/components/sendBox.tsx | 21 +- components/bank/components/tokenList.tsx | 4 + components/bank/forms/ibcSendForm.tsx | 234 ++++++++++++++--------- components/bank/modals/sendModal.tsx | 3 + package.json | 1 + pages/bank.tsx | 27 ++- utils/ibc.ts | 10 +- 8 files changed, 193 insertions(+), 107 deletions(-) diff --git a/bun.lockb b/bun.lockb index db2f05581bc4f24407d24fa4e73e971f64254333..eeb222942dbdfa81161700fcbf99be0aff1d23b8 100755 GIT binary patch delta 138547 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 $?tBV!i=6ml!_MECSlY>svqt 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 HKZR^BqbLIRO-jq|&Q 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 Ue+?L>lE)3Z9yPs!S_c^Z2aMGE`~Uy| diff --git a/components/bank/components/sendBox.tsx b/components/bank/components/sendBox.tsx index feca025..8107694 100644 --- a/components/bank/components/sendBox.tsx +++ b/components/bank/components/sendBox.tsx @@ -3,6 +3,7 @@ import SendForm from '../forms/sendForm'; import IbcSendForm from '../forms/ibcSendForm'; import env from '@/config/env'; import { CombinedBalanceInfo } from '@/utils/types'; +import { ChainContext } from '@cosmos-kit/core'; export interface IbcChain { id: string; @@ -25,6 +26,7 @@ export default function SendBox({ isOsmosisBalancesLoading, refetchOsmosisBalances, resolveOsmosisRefetch, + chains, }: { address: string; balances: CombinedBalanceInfo[]; @@ -39,10 +41,8 @@ export default function SendBox({ isOsmosisBalancesLoading: boolean; refetchOsmosisBalances: () => void; resolveOsmosisRefetch: () => void; + chains: Record; }) { - const [activeTab, setActiveTab] = useState<'send' | 'cross-chain'>('send'); - const [selectedFromChain, setSelectedFromChain] = useState(''); - const [selectedToChain, setSelectedToChain] = useState(''); const ibcChains = useMemo( () => [ { @@ -50,27 +50,34 @@ export default function SendBox({ name: 'Manifest', icon: 'logo.svg', prefix: 'manifest', + chainID: env.chainId, }, { id: env.osmosisChain, name: 'Osmosis', icon: 'osmosis.svg', prefix: 'osmo', + chainID: env.osmosisChainId, }, ], [] ); + const [activeTab, setActiveTab] = useState<'send' | 'cross-chain'>('send'); + const [selectedFromChain, setSelectedFromChain] = useState(ibcChains[0]); + const [selectedToChain, setSelectedToChain] = useState(ibcChains[1]); useEffect(() => { - if (selectedFromChain && selectedToChain && selectedFromChain === selectedToChain) { + if (selectedFromChain && selectedToChain && selectedFromChain.id === selectedToChain.id) { // If chains match, switch the destination chain to the other available chain - const otherChain = ibcChains.find(chain => chain.id !== selectedFromChain)?.id || ''; - setSelectedToChain(otherChain); + const otherChain = ibcChains.find(chain => chain.id !== selectedFromChain.id); + if (otherChain) { + setSelectedToChain(otherChain); + } } }, [selectedFromChain, selectedToChain, ibcChains]); const getAvailableToChains = useMemo(() => { - return ibcChains.filter(chain => chain.id !== selectedFromChain); + return ibcChains.filter(chain => chain.id !== selectedFromChain.id); }, [ibcChains, selectedFromChain]); return ( diff --git a/components/bank/components/tokenList.tsx b/components/bank/components/tokenList.tsx index 78190e9..2f1ffdd 100644 --- a/components/bank/components/tokenList.tsx +++ b/components/bank/components/tokenList.tsx @@ -4,6 +4,7 @@ import { shiftDigits, truncateString } from '@/utils'; import { CombinedBalanceInfo } from '@/utils/types'; import { SendTxIcon, QuestionIcon } from '@/components/icons'; import SendModal from '@/components/bank/modals/sendModal'; +import { ChainContext } from '@cosmos-kit/core'; interface TokenListProps { balances: CombinedBalanceInfo[] | undefined; @@ -20,6 +21,7 @@ interface TokenListProps { isOsmosisBalancesLoading?: boolean; refetchOsmosisBalances?: () => void; resolveOsmosisRefetch?: () => void; + chains: Record; } export function TokenList(props: Readonly) { @@ -38,6 +40,7 @@ export function TokenList(props: Readonly) { isOsmosisBalancesLoading, refetchOsmosisBalances, resolveOsmosisRefetch, + chains, } = props; const [selectedDenom, setSelectedDenom] = useState(null); const [isSendModalOpen, setIsSendModalOpen] = useState(false); @@ -252,6 +255,7 @@ export function TokenList(props: Readonly) { isOsmosisBalancesLoading={isOsmosisBalancesLoading ?? false} refetchOsmosisBalances={refetchOsmosisBalances ?? (() => {})} resolveOsmosisRefetch={resolveOsmosisRefetch ?? (() => {})} + chains={chains} />
    ); diff --git a/components/bank/forms/ibcSendForm.tsx b/components/bank/forms/ibcSendForm.tsx index d015f93..fb3864f 100644 --- a/components/bank/forms/ibcSendForm.tsx +++ b/components/bank/forms/ibcSendForm.tsx @@ -24,16 +24,19 @@ import { DenomImage } from '@/components/factory'; import { Formik, Form } from 'formik'; import Yup from '@/utils/yupExtensions'; import { TextInput } from '@/components/react/inputs'; -import { IbcChain } from '@/components'; + import Image from 'next/image'; import { SearchIcon, TransferIcon } from '@/components/icons'; import { TailwindModal } from '@/components/react/modal'; import env from '@/config/env'; -import { useChain } from '@cosmos-kit/react'; +import { useChains } from '@cosmos-kit/react'; import { useSearchParams } from 'next/navigation'; import { Any } from 'cosmjs-types/google/protobuf/any'; import { useSkipClient } from '@/contexts/skipGoContext'; +import { SkipClient } from '@skip-go/client'; +import { IbcChain } from '@/components'; +import { ChainContext } from '@cosmos-kit/core'; //TODO: switch to main-net names export default function IbcSendForm({ @@ -58,9 +61,10 @@ export default function IbcSendForm({ refetchProposals, admin, availableToChains, + chains, }: Readonly<{ address: string; - destinationChain: string; + destinationChain: IbcChain; balances: CombinedBalanceInfo[]; isBalancesLoading: boolean; refetchBalances: () => void; @@ -68,10 +72,10 @@ export default function IbcSendForm({ isIbcTransfer: boolean; ibcChains: IbcChain[]; isGroup?: boolean; - selectedFromChain: string; - setSelectedFromChain: (selectedChain: string) => void; - selectedToChain: string; - setSelectedToChain: (selectedChain: string) => void; + selectedFromChain: IbcChain; + setSelectedFromChain: (selectedChain: IbcChain) => void; + selectedToChain: IbcChain; + setSelectedToChain: (selectedChain: IbcChain) => void; selectedDenom?: string; osmosisBalances: CombinedBalanceInfo[]; isOsmosisBalancesLoading: boolean; @@ -80,9 +84,8 @@ export default function IbcSendForm({ refetchProposals?: () => void; admin?: string; availableToChains: IbcChain[]; + chains: Record; }>) { - const { address: osmosisAddress } = useChain(env.osmosisChain); - const formatTokenDisplayName = (displayName: string) => { if (displayName.startsWith('factory')) { return displayName.split('/').pop()?.toUpperCase(); @@ -96,19 +99,21 @@ export default function IbcSendForm({ const [isSending, setIsSending] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [feeWarning, setFeeWarning] = useState(''); - const { tx } = useTx(selectedFromChain === env.osmosisChain ? env.osmosisChain : env.chain); + const { tx } = useTx(selectedFromChain.name === env.osmosisChain ? env.osmosisChain : env.chain); const { estimateFee } = useFeeEstimation( - selectedFromChain === env.osmosisChain ? env.osmosisChain : env.chain + selectedFromChain.name === env.osmosisChain ? env.osmosisChain : env.chain ); - const skipClient = useSkipClient(); + const { transfer } = ibc.applications.transfer.v1.MessageComposer.withTypeUrl; const [isContactsOpen, setIsContactsOpen] = useState(false); const [isIconRotated, setIsIconRotated] = useState(false); + const skipClient = new SkipClient({}); + const { submitProposal } = cosmos.group.v1.MessageComposer.withTypeUrl; useEffect(() => { if (isGroup) { - setSelectedFromChain(env.chain); + setSelectedFromChain(ibcChains.find(chain => chain.id === env.chain) ?? ibcChains[0]); } }, [isGroup, setSelectedFromChain]); @@ -116,7 +121,7 @@ export default function IbcSendForm({ // Update the filtered balances logic to use passed props instead of hooks const filteredBalances = useMemo(() => { - const sourceBalances = selectedFromChain === env.osmosisChain ? osmosisBalances : balances; + const sourceBalances = selectedFromChain.name === env.osmosisChain ? osmosisBalances : balances; return sourceBalances?.filter(token => { const displayName = token.metadata?.display ?? token.denom; @@ -126,7 +131,7 @@ export default function IbcSendForm({ // Update initialSelectedToken to consider the chain const initialSelectedToken = useMemo(() => { - const sourceBalances = selectedFromChain === env.osmosisChain ? osmosisBalances : balances; + const sourceBalances = selectedFromChain.name === env.osmosisChain ? osmosisBalances : balances; return ( sourceBalances?.find(token => token.coreDenom === selectedDenom) || @@ -137,7 +142,7 @@ export default function IbcSendForm({ // Update the loading check if ( - (selectedFromChain === env.osmosisChain ? isOsmosisBalancesLoading : isBalancesLoading) || + (selectedFromChain.name === env.osmosisChain ? isOsmosisBalancesLoading : isBalancesLoading) || !initialSelectedToken ) { return null; @@ -193,11 +198,13 @@ export default function IbcSendForm({ const exponent = values.selectedToken.metadata?.denom_units[1]?.exponent ?? 6; const amountInBaseUnits = parseNumberToBigInt(values.amount, exponent).toString(); - const { source_port, source_channel } = getIbcInfo(selectedFromChain, selectedToChain); + const { source_port, source_channel } = getIbcInfo(selectedFromChain.id, selectedToChain.id); - const testnetChains = await skipClient.chains({}); + const skipChains = await skipClient.chains({ + onlyTestnets: true, + }); + console.log('Available Skip chains:', skipChains); - console.log(testnetChains); const token = { denom: values.selectedToken.coreDenom, amount: amountInBaseUnits, @@ -206,56 +213,99 @@ export default function IbcSendForm({ const stamp = Date.now(); const timeoutInNanos = (stamp + 1.2e6) * 1e6; - const transferMsg = transfer({ - sourcePort: source_port, - sourceChannel: source_channel, - sender: admin - ? admin - : selectedFromChain === env.osmosisChain - ? (osmosisAddress ?? '') - : (address ?? ''), - receiver: values.recipient ?? '', - token, - timeoutHeight: { - revisionNumber: BigInt(0), - revisionHeight: BigInt(0), - }, - timeoutTimestamp: BigInt(timeoutInNanos), + const ibcDenom = getIbcDenom(selectedToChain.name, values.selectedToken.coreDenom); + + console.log({ + fromChain: selectedFromChain, + toChain: selectedToChain, + sourceDenom: values.selectedToken.coreDenom, + ibcDenom, + }); + + const route = await skipClient.route({ + amountIn: amountInBaseUnits, + sourceAssetDenom: values.selectedToken.coreDenom, + sourceAssetChainID: 'manifest-ledger-testnet', + destAssetDenom: ibcDenom ?? values.selectedToken.coreDenom, + destAssetChainID: 'osmo-test-5', + cumulativeAffiliateFeeBPS: '0', }); - const msg = isGroup - ? submitProposal({ - groupPolicyAddress: admin!, - messages: [ - Any.fromPartial({ - typeUrl: MsgTransfer.typeUrl, - value: MsgTransfer.encode(transferMsg.value).finish(), - }), - ], - metadata: '', - proposers: [address], - title: `IBC Transfer`, - summary: `This proposal will send ${values.amount} ${values.selectedToken.metadata?.display} to ${values.recipient} via IBC transfer`, - exec: 0, - }) - : transferMsg; - - const fee = await estimateFee( - selectedFromChain === env.osmosisChain ? (osmosisAddress ?? '') : (address ?? ''), - [msg] - ); - - await tx([msg], { - memo: values.memo, - fee, - onSuccess: () => { - refetchBalances(); - refetchHistory(); - refetchOsmosisBalances(); - resolveOsmosisRefetch(); - refetchProposals?.(); - }, + const userAddresses = route.requiredChainAddresses.map(chainID => ({ + address: + Object.values(chains).find(chain => chain.chain.chain_id === chainID)?.address ?? '', + })); + console.log(userAddresses); + + // Log the validation result + + await skipClient.messages({ + sourceAssetDenom: values.selectedToken.coreDenom, + sourceAssetChainID: selectedFromChain.id, + destAssetDenom: ibcDenom ?? values.selectedToken.coreDenom, + destAssetChainID: selectedToChain.id, + amountIn: amountInBaseUnits, + amountOut: route.estimatedAmountOut ?? '', + addressList: userAddresses.map(user => user.address), + operations: route.operations, + estimatedAmountOut: route.estimatedAmountOut ?? '', + slippageTolerancePercent: '1', + affiliates: [], + chainIDsToAffiliates: {}, + postRouteHandler: undefined, + enableGasWarnings: false, }); + + console.log('skipClient.messages', skipClient.messages); + + // const transferMsg = transfer({ + // sourcePort: source_port, + // sourceChannel: source_channel, + // sender: admin + // ? admin + // : selectedFromChain === env.osmosisChain + // ? (osmosisAddress ?? '') + // : (address ?? ''), + // receiver: values.recipient ?? '', + // token, + // timeoutHeight: { + // revisionNumber: BigInt(0), + // revisionHeight: BigInt(0), + // }, + // timeoutTimestamp: BigInt(timeoutInNanos), + // }); + + // const msg = submitProposal({ + // groupPolicyAddress: admin!, + // messages: [ + // Any.fromPartial({ + // typeUrl: MsgTransfer.typeUrl, + // value: MsgTransfer.encode(transferMsg.value).finish(), + // }), + // ], + // metadata: '', + // proposers: [address], + // title: `IBC Transfer`, + // summary: `This proposal will send ${values.amount} ${values.selectedToken.metadata?.display} to ${values.recipient} via IBC transfer`, + // exec: 0, + // }); + + // const fee = await estimateFee( + // selectedFromChain === env.osmosisChain ? (osmosisAddress ?? '') : (address ?? ''), + // [msg] + // ); + + // await tx([msg], { + // memo: values.memo, + // fee, + // onSuccess: () => { + // refetchBalances(); + // refetchHistory(); + // refetchOsmosisBalances(); + // resolveOsmosisRefetch(); + // refetchProposals?.(); + // }, + // }); } catch (error) { console.error('Error during sending:', error); } finally { @@ -331,12 +381,12 @@ export default function IbcSendForm({ {selectedFromChain && ( chain.id === selectedFromChain)?.icon || - '' + ibcChains.find(chain => chain.id === selectedFromChain.id) + ?.icon || '' } alt={ - ibcChains.find(chain => chain.id === selectedFromChain)?.name || - '' + ibcChains.find(chain => chain.id === selectedFromChain.id) + ?.name || '' } width={24} height={24} @@ -344,7 +394,7 @@ export default function IbcSendForm({ /> )} - {ibcChains.find(chain => chain.id === selectedFromChain)?.name ?? + {ibcChains.find(chain => chain.id === selectedFromChain.id)?.name ?? 'Select Chain'} @@ -361,14 +411,14 @@ export default function IbcSendForm({
  • { - if (chain.id === selectedFromChain) { + if (chain.id === selectedFromChain.id) { return; } - setSelectedFromChain(chain.id); + setSelectedFromChain(chain); // Get the dropdown element and remove focus const dropdown = (e.target as HTMLElement).closest('.dropdown'); if (dropdown) { @@ -380,10 +430,10 @@ export default function IbcSendForm({ onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); - if (chain.id === selectedFromChain) { + if (chain.id === selectedFromChain.id) { return; } - setSelectedFromChain(chain.id); + setSelectedFromChain(chain); // Get the dropdown element and remove focus const dropdown = (e.target as HTMLElement).closest('.dropdown'); if (dropdown) { @@ -395,12 +445,12 @@ export default function IbcSendForm({ }} tabIndex={0} className={`flex items-center ${ - chain.id === selectedFromChain + chain.id === selectedFromChain.id ? 'opacity-50 cursor-not-allowed' : '' }`} style={ - chain.id === selectedFromChain + chain.id === selectedFromChain.id ? { pointerEvents: 'none' } : undefined } @@ -462,15 +512,19 @@ export default function IbcSendForm({ {selectedToChain && ( chain.id === selectedToChain)?.icon || ''} - alt={ibcChains.find(chain => chain.id === selectedToChain)?.name || ''} + src={ + ibcChains.find(chain => chain.id === selectedToChain.id)?.icon || '' + } + alt={ + ibcChains.find(chain => chain.id === selectedToChain.id)?.name || '' + } width={24} height={24} className="mr-2" /> )} - {ibcChains.find(chain => chain.id === selectedToChain)?.name ?? + {ibcChains.find(chain => chain.id === selectedToChain.id)?.name ?? 'Select Chain'} @@ -484,13 +538,17 @@ export default function IbcSendForm({ className="dropdown-content z-[100] menu p-2 shadow bg-base-300 rounded-lg w-full mt-1 dark:text-[#FFFFFF] text-[#161616]" > {availableToChains?.map(chain => ( -
  • +
  • { - if (chain.id === selectedToChain) { + if (chain.id === selectedToChain.id) { return; } - setSelectedToChain(chain.id); + setSelectedToChain(chain); // Get the dropdown element and remove focus const dropdown = (e.target as HTMLElement).closest('.dropdown'); if (dropdown) { @@ -502,10 +560,10 @@ export default function IbcSendForm({ onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); - if (chain.id === selectedToChain) { + if (chain.id === selectedToChain.id) { return; } - setSelectedToChain(chain.id); + setSelectedToChain(chain); // Get the dropdown element and remove focus const dropdown = (e.target as HTMLElement).closest('.dropdown'); if (dropdown) { @@ -517,10 +575,10 @@ export default function IbcSendForm({ }} tabIndex={0} className={`flex items-center ${ - chain.id === selectedToChain ? 'opacity-50 cursor-not-allowed' : '' + chain.id === selectedToChain.id ? 'opacity-50 cursor-not-allowed' : '' }`} style={ - chain.id === selectedToChain ? { pointerEvents: 'none' } : undefined + chain.id === selectedToChain.id ? { pointerEvents: 'none' } : undefined } aria-label={chain.name} > diff --git a/components/bank/modals/sendModal.tsx b/components/bank/modals/sendModal.tsx index eb7f14f..005d147 100644 --- a/components/bank/modals/sendModal.tsx +++ b/components/bank/modals/sendModal.tsx @@ -3,6 +3,7 @@ import SendBox from '../components/sendBox'; import { CombinedBalanceInfo } from '@/utils/types'; import { useEffect } from 'react'; import { createPortal } from 'react-dom'; +import { ChainContext } from '@cosmos-kit/core'; interface SendModalProps { modalId: string; @@ -21,6 +22,7 @@ interface SendModalProps { isOsmosisBalancesLoading: boolean; refetchOsmosisBalances: () => void; resolveOsmosisRefetch: () => void; + chains: Record; } export default function SendModal({ @@ -40,6 +42,7 @@ export default function SendModal({ isOsmosisBalancesLoading, refetchOsmosisBalances, resolveOsmosisRefetch, + chains, }: SendModalProps) { const handleClose = () => { if (setOpen) { diff --git a/package.json b/package.json index febb42d..7e04d7c 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@liftedinit/manifestjs": "0.0.1-rc.1", "@react-three/drei": "^9.114.0", "@react-three/fiber": "^8.17.8", + "@skip-go/client": "^0.16.7", "@tanstack/react-query": "^5.55.0", "@tanstack/react-query-devtools": "^5.55.0", "@types/file-saver": "^2.0.7", diff --git a/pages/bank.tsx b/pages/bank.tsx index c89ccd2..97c20b7 100644 --- a/pages/bank.tsx +++ b/pages/bank.tsx @@ -10,7 +10,7 @@ import { useTokenBalancesResolved, useTokenFactoryDenomsMetadata, } from '@/hooks'; -import { useChain } from '@cosmos-kit/react'; +import { useChain, useChains } from '@cosmos-kit/react'; import React, { useMemo, useState, useCallback, useEffect } from 'react'; import { BankIcon } from '@/components/icons'; @@ -29,13 +29,20 @@ interface PageSizeConfig { } export default function Bank() { - const { address, isWalletConnected } = useChain(env.chain); - const { balances, isBalancesLoading, refetchBalances } = useTokenBalances(address ?? ''); + const chains = useChains([env.chain, env.osmosisChain]); + const isWalletConnected = Object.values(chains).every(chain => chain.isWalletConnected); + if (!isWalletConnected) { + Object.values(chains).forEach(chain => chain.connect()); + } + + const { balances, isBalancesLoading, refetchBalances } = useTokenBalances( + chains.manifesttestnet.address ?? '' + ); const { balances: resolvedBalances, isBalancesLoading: resolvedLoading, refetchBalances: resolveRefetch, - } = useTokenBalancesResolved(address ?? ''); + } = useTokenBalancesResolved(chains.manifesttestnet.address ?? ''); const { metadatas, isMetadatasLoading } = useTokenFactoryDenomsMetadata(); const [currentPage, setCurrentPage] = useState(1); @@ -86,7 +93,7 @@ export default function Bank() { refetch: refetchHistory, } = useGetFilteredTxAndSuccessfulProposals( env.indexerUrl, - address ?? '', + chains.manifesttestnet.address ?? '', currentPage, historyPageSize ); @@ -155,17 +162,16 @@ export default function Bank() { return mfxCombinedBalance ? [mfxCombinedBalance, ...otherBalances] : otherBalances; }, [balances, resolvedBalances, metadatas]); - const { address: osmosisAddress } = useChain(env.osmosisChain); const { balances: osmosisBalances, isBalancesLoading: isOsmosisBalancesLoading, refetchBalances: refetchOsmosisBalances, - } = useTokenBalancesOsmosis(osmosisAddress ?? ''); + } = useTokenBalancesOsmosis(chains.osmosistestnet.address ?? ''); const { balances: resolvedOsmosisBalances, isBalancesLoading: resolvedOsmosisLoading, refetchBalances: resolveOsmosisRefetch, - } = useOsmosisTokenBalancesResolved(osmosisAddress ?? ''); + } = useOsmosisTokenBalancesResolved(chains.osmosistestnet.address ?? ''); const { metadatas: osmosisMetadatas, @@ -308,9 +314,10 @@ export default function Bank() { isLoading={isLoading} balances={combinedBalances} refetchHistory={refetchHistory} - address={address ?? ''} + address={chains.manifesttestnet.address ?? ''} pageSize={tokenListPageSize} searchTerm={searchTerm} + chains={chains} /> ))} {activeTab === 'history' && @@ -320,7 +327,7 @@ export default function Bank() { { }; export const getIbcDenom = (chainName: string, denom: string) => { - const asset = denomToAsset(chainName, denom); - return asset?.base; + const allAssets = getAllAssets(chainName); + + // Find the asset that has this denom as its counterparty base_denom + const ibcAsset = allAssets.find(asset => asset.traces?.[0]?.counterparty?.base_denom === denom); + + // Return the IBC hash (base) if found + return ibcAsset?.base; }; export const normalizeIBCDenom = (chainName: string, denom: string) => { From a26855dd2f7dc9501ef8b07459f5e113db94bf4a Mon Sep 17 00:00:00 2001 From: Joseph Chalabi Date: Sat, 1 Feb 2025 20:23:42 -0700 Subject: [PATCH 46/48] add skip routes and messages --- components/bank/components/sendBox.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/components/bank/components/sendBox.tsx b/components/bank/components/sendBox.tsx index 8107694..8e870db 100644 --- a/components/bank/components/sendBox.tsx +++ b/components/bank/components/sendBox.tsx @@ -137,6 +137,7 @@ export default function SendBox({ refetchOsmosisBalances={refetchOsmosisBalances} resolveOsmosisRefetch={resolveOsmosisRefetch} availableToChains={getAvailableToChains} + chains={chains} /> ) : ( Date: Sun, 2 Feb 2025 01:05:05 -0700 Subject: [PATCH 47/48] pass cosmos kit signers to skipClient context provider --- components/bank/components/sendBox.tsx | 1 + components/bank/forms/ibcSendForm.tsx | 75 ++++++++++++++++++++------ components/bank/modals/sendModal.tsx | 1 + components/react/modal.tsx | 24 ++++++++- components/wallet.tsx | 53 +++++++++++++++++- contexts/skipGoContext.tsx | 28 +++++++--- pages/bank.tsx | 9 ++-- 7 files changed, 160 insertions(+), 31 deletions(-) diff --git a/components/bank/components/sendBox.tsx b/components/bank/components/sendBox.tsx index 8e870db..4e4910a 100644 --- a/components/bank/components/sendBox.tsx +++ b/components/bank/components/sendBox.tsx @@ -10,6 +10,7 @@ export interface IbcChain { name: string; icon: string; prefix: string; + chainID: string; } export default function SendBox({ diff --git a/components/bank/forms/ibcSendForm.tsx b/components/bank/forms/ibcSendForm.tsx index fb3864f..e52bdd3 100644 --- a/components/bank/forms/ibcSendForm.tsx +++ b/components/bank/forms/ibcSendForm.tsx @@ -34,7 +34,7 @@ import { useChains } from '@cosmos-kit/react'; import { useSearchParams } from 'next/navigation'; import { Any } from 'cosmjs-types/google/protobuf/any'; import { useSkipClient } from '@/contexts/skipGoContext'; -import { SkipClient } from '@skip-go/client'; + import { IbcChain } from '@/components'; import { ChainContext } from '@cosmos-kit/core'; @@ -108,7 +108,13 @@ export default function IbcSendForm({ const [isContactsOpen, setIsContactsOpen] = useState(false); const [isIconRotated, setIsIconRotated] = useState(false); - const skipClient = new SkipClient({}); + const getCosmosSigner = async () => { + const signer = chains[selectedFromChain.name].getOfflineSignerAmino(); + return signer; + }; + const skipClient = useSkipClient({ + getCosmosSigner: getCosmosSigner, + }); const { submitProposal } = cosmos.group.v1.MessageComposer.withTypeUrl; useEffect(() => { @@ -121,7 +127,7 @@ export default function IbcSendForm({ // Update the filtered balances logic to use passed props instead of hooks const filteredBalances = useMemo(() => { - const sourceBalances = selectedFromChain.name === env.osmosisChain ? osmosisBalances : balances; + const sourceBalances = selectedFromChain.id === env.osmosisChain ? osmosisBalances : balances; return sourceBalances?.filter(token => { const displayName = token.metadata?.display ?? token.denom; @@ -131,7 +137,7 @@ export default function IbcSendForm({ // Update initialSelectedToken to consider the chain const initialSelectedToken = useMemo(() => { - const sourceBalances = selectedFromChain.name === env.osmosisChain ? osmosisBalances : balances; + const sourceBalances = selectedFromChain.id === env.osmosisChain ? osmosisBalances : balances; return ( sourceBalances?.find(token => token.coreDenom === selectedDenom) || @@ -142,7 +148,7 @@ export default function IbcSendForm({ // Update the loading check if ( - (selectedFromChain.name === env.osmosisChain ? isOsmosisBalancesLoading : isBalancesLoading) || + (selectedFromChain.id === env.osmosisChain ? isOsmosisBalancesLoading : isBalancesLoading) || !initialSelectedToken ) { return null; @@ -213,7 +219,7 @@ export default function IbcSendForm({ const stamp = Date.now(); const timeoutInNanos = (stamp + 1.2e6) * 1e6; - const ibcDenom = getIbcDenom(selectedToChain.name, values.selectedToken.coreDenom); + const ibcDenom = getIbcDenom(selectedToChain.id, values.selectedToken.coreDenom); console.log({ fromChain: selectedFromChain, @@ -223,30 +229,37 @@ export default function IbcSendForm({ }); const route = await skipClient.route({ - amountIn: amountInBaseUnits, sourceAssetDenom: values.selectedToken.coreDenom, - sourceAssetChainID: 'manifest-ledger-testnet', - destAssetDenom: ibcDenom ?? values.selectedToken.coreDenom, - destAssetChainID: 'osmo-test-5', - cumulativeAffiliateFeeBPS: '0', + sourceAssetChainID: selectedFromChain.chainID, + destAssetChainID: selectedToChain.chainID, + destAssetDenom: ibcDenom ?? '', + amountIn: amountInBaseUnits, }); + console.log('route', route); + + const addressList = route.requiredChainAddresses.map(chainID => ({ + address: + Object.values(chains).find(chain => chain.chain.chain_id === chainID)?.address ?? '', + })); + const userAddresses = route.requiredChainAddresses.map(chainID => ({ address: Object.values(chains).find(chain => chain.chain.chain_id === chainID)?.address ?? '', + chainID: chainID, })); console.log(userAddresses); // Log the validation result - await skipClient.messages({ + const messages = await skipClient.messages({ sourceAssetDenom: values.selectedToken.coreDenom, - sourceAssetChainID: selectedFromChain.id, + sourceAssetChainID: selectedFromChain.chainID, destAssetDenom: ibcDenom ?? values.selectedToken.coreDenom, - destAssetChainID: selectedToChain.id, + destAssetChainID: selectedToChain.chainID, amountIn: amountInBaseUnits, amountOut: route.estimatedAmountOut ?? '', - addressList: userAddresses.map(user => user.address), + addressList: addressList.map(user => user.address), operations: route.operations, estimatedAmountOut: route.estimatedAmountOut ?? '', slippageTolerancePercent: '1', @@ -256,7 +269,34 @@ export default function IbcSendForm({ enableGasWarnings: false, }); - console.log('skipClient.messages', skipClient.messages); + await skipClient.executeRoute({ + route, + userAddresses, + simulate: true, + // Executes after all of the operations triggered by a user's signature complete. + // For multi-tx routes that require multiple user signatures, this will be called once for each tx in sequence + onTransactionCompleted: async (chainID, txHash, status) => { + console.log(`Route completed with tx hash: ${txHash} & status: ${status.state}`); + }, + // called after the transaction that the user signs gets broadcast on chain + onTransactionBroadcast: async ({ txHash, chainID }) => { + console.log(`Transaction broadcasted with tx hash: ${txHash}`); + }, + // called after the transaction that the user signs is successfully registered for tracking + onTransactionTracked: async ({ txHash, chainID }) => { + console.log(`Transaction tracked with tx hash: ${txHash}`); + }, + // called after the user signs a transaction + onTransactionSigned: async ({ chainID }) => { + console.log(`Transaction signed with chain ID: ${chainID}`); + }, + // validate gas balance on each chain + onValidateGasBalance: async ({ chainID, txIndex, status }) => { + console.log(`Validating gas balance for chain ${chainID}...`); + }, + }); + + console.log('skipClient.messages', messages); // const transferMsg = transfer({ // sourcePort: source_port, @@ -823,3 +863,6 @@ export default function IbcSendForm({
  • ); } +function useSkip(getCosmosSigner: () => Promise) { + throw new Error('Function not implemented.'); +} diff --git a/components/bank/modals/sendModal.tsx b/components/bank/modals/sendModal.tsx index 005d147..396df1b 100644 --- a/components/bank/modals/sendModal.tsx +++ b/components/bank/modals/sendModal.tsx @@ -113,6 +113,7 @@ export default function SendModal({ isOsmosisBalancesLoading={isOsmosisBalancesLoading} refetchOsmosisBalances={refetchOsmosisBalances} resolveOsmosisRefetch={resolveOsmosisRefetch} + chains={chains} />
    (State.Init); const [qrMessage, setQrMessage] = useState(''); - const current = walletRepo?.current; + const chains = useChains([env.chain, env.osmosisChain]); + + const chainStates = useMemo(() => { + return Object.values(chains).map(chain => ({ + connect: chain.connect, + openView: chain.openView, + status: chain.status, + username: chain.username, + address: chain.address, + disconnect: chain.disconnect, + })); + }, [chains]); + + const disconnect = async () => { + await Promise.all(chainStates.map(chain => chain.disconnect())); + }; + + const current = chains?.manifesttestnet?.walletRepo?.current; + const currentWalletData = current?.walletInfo; const walletStatus = current?.walletStatus || WalletStatus.Disconnected; const currentWalletName = current?.walletName; @@ -444,7 +464,7 @@ export const TailwindModal: React.FC< setCurrentView(ModalView.WalletList)} - disconnect={() => current?.disconnect()} + disconnect={() => disconnect()} name={currentWalletData?.prettyName!} logo={currentWalletData?.logo!.toString() ?? ''} username={current?.username} diff --git a/components/wallet.tsx b/components/wallet.tsx index cdd18dc..eec6220 100644 --- a/components/wallet.tsx +++ b/components/wallet.tsx @@ -2,7 +2,7 @@ import React, { MouseEventHandler, useEffect, useMemo, useState, useRef } from ' import { ArrowDownTrayIcon, ArrowPathIcon } from '@heroicons/react/24/outline'; import { ArrowUpIcon, CopyIcon } from './icons'; -import { useChain } from '@cosmos-kit/react'; +import { useChain, useChains } from '@cosmos-kit/react'; import { WalletStatus } from 'cosmos-kit'; import { MdWallet } from 'react-icons/md'; import env from '@/config/env'; @@ -36,7 +36,56 @@ interface WalletSectionProps { } export const WalletSection: React.FC = ({ chainName }) => { - const { connect, openView, status, username, address } = useChain(chainName); + const chains = useChains([env.chain, env.osmosisChain]); + + const chainStates = useMemo(() => { + return Object.values(chains).map(chain => ({ + connect: chain.connect, + openView: chain.openView, + status: chain.status, + username: chain.username, + address: chain.address, + })); + }, [chains]); + + const connect = async () => { + await Promise.all(chainStates.map(chain => chain.connect())); + }; + + const openView = () => { + chainStates[0]?.openView(); + }; + + const status = useMemo(() => { + if (chainStates.some(chain => chain.status === WalletStatus.Connecting)) { + return WalletStatus.Connecting; + } + if (chainStates.some(chain => chain.status === WalletStatus.Error)) { + return WalletStatus.Error; + } + if (chainStates.every(chain => chain.status === WalletStatus.Connected)) { + return WalletStatus.Connected; + } + return WalletStatus.Disconnected; + }, [chainStates]); + + const username = useMemo( + () => + chainStates + .map(chain => chain.username) + .filter(Boolean) + .join(' / ') || undefined, + [chainStates] + ); + + const address = useMemo( + () => + chainStates + .map(chain => chain.address) + .filter(Boolean) + .join(' / ') || undefined, + [chainStates] + ); const [localStatus, setLocalStatus] = useState(status); const timeoutRef = useRef>(); diff --git a/contexts/skipGoContext.tsx b/contexts/skipGoContext.tsx index 2cae678..8ae401c 100644 --- a/contexts/skipGoContext.tsx +++ b/contexts/skipGoContext.tsx @@ -1,9 +1,9 @@ -import React, { createContext, useContext } from 'react'; +import React, { createContext, useContext, useMemo } from 'react'; import { SkipClient } from '@skip-go/client'; // Create the context interface SkipContextType { - skipClient: SkipClient; + createClient: (options: any) => SkipClient; } const SkipContext = createContext(undefined); @@ -14,16 +14,30 @@ interface SkipProviderProps { } export function SkipProvider({ children }: SkipProviderProps) { - const skipClient = new SkipClient({}); + const createClient = useMemo(() => { + return (options: any) => new SkipClient(options); + }, []); - return {children}; + return {children}; } -// Create a custom hook to use the Skip client -export function useSkipClient() { +// Update the hook to accept getCosmosSigner +interface UseSkipClientOptions { + getCosmosSigner: () => Promise; +} + +export function useSkipClient(options: UseSkipClientOptions) { const context = useContext(SkipContext); if (context === undefined) { throw new Error('useSkipClient must be used within a SkipProvider'); } - return context.skipClient; + + // Create a new client with the provided options + const skipClient = useMemo(() => { + return context.createClient({ + getCosmosSigner: options.getCosmosSigner, + }); + }, [context.createClient, options.getCosmosSigner]); + + return skipClient; } diff --git a/pages/bank.tsx b/pages/bank.tsx index 97c20b7..91f12bc 100644 --- a/pages/bank.tsx +++ b/pages/bank.tsx @@ -30,10 +30,11 @@ interface PageSizeConfig { export default function Bank() { const chains = useChains([env.chain, env.osmosisChain]); - const isWalletConnected = Object.values(chains).every(chain => chain.isWalletConnected); - if (!isWalletConnected) { - Object.values(chains).forEach(chain => chain.connect()); - } + + const isWalletConnected = useMemo( + () => Object.values(chains).every(chain => chain.isWalletConnected), + [chains] + ); const { balances, isBalancesLoading, refetchBalances } = useTokenBalances( chains.manifesttestnet.address ?? '' From 09ece7bf63b132a0f259fdd00fac7d9539e303b3 Mon Sep 17 00:00:00 2001 From: Joseph Chalabi Date: Sun, 2 Feb 2025 01:49:37 -0700 Subject: [PATCH 48/48] bring to most recent --- components/bank/forms/ibcSendForm.tsx | 52 +++++++++++---------------- 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/components/bank/forms/ibcSendForm.tsx b/components/bank/forms/ibcSendForm.tsx index e52bdd3..836aecc 100644 --- a/components/bank/forms/ibcSendForm.tsx +++ b/components/bank/forms/ibcSendForm.tsx @@ -221,13 +221,6 @@ export default function IbcSendForm({ const ibcDenom = getIbcDenom(selectedToChain.id, values.selectedToken.coreDenom); - console.log({ - fromChain: selectedFromChain, - toChain: selectedToChain, - sourceDenom: values.selectedToken.coreDenom, - ibcDenom, - }); - const route = await skipClient.route({ sourceAssetDenom: values.selectedToken.coreDenom, sourceAssetChainID: selectedFromChain.chainID, @@ -236,8 +229,6 @@ export default function IbcSendForm({ amountIn: amountInBaseUnits, }); - console.log('route', route); - const addressList = route.requiredChainAddresses.map(chainID => ({ address: Object.values(chains).find(chain => chain.chain.chain_id === chainID)?.address ?? '', @@ -248,9 +239,6 @@ export default function IbcSendForm({ Object.values(chains).find(chain => chain.chain.chain_id === chainID)?.address ?? '', chainID: chainID, })); - console.log(userAddresses); - - // Log the validation result const messages = await skipClient.messages({ sourceAssetDenom: values.selectedToken.coreDenom, @@ -296,15 +284,13 @@ export default function IbcSendForm({ }, }); - console.log('skipClient.messages', messages); - // const transferMsg = transfer({ // sourcePort: source_port, // sourceChannel: source_channel, // sender: admin // ? admin - // : selectedFromChain === env.osmosisChain - // ? (osmosisAddress ?? '') + // : selectedFromChain.id === env.osmosisChain + // ? (chains?.osmosistestnet?.address ?? '') // : (address ?? ''), // receiver: values.recipient ?? '', // token, @@ -315,23 +301,27 @@ export default function IbcSendForm({ // timeoutTimestamp: BigInt(timeoutInNanos), // }); - // const msg = submitProposal({ - // groupPolicyAddress: admin!, - // messages: [ - // Any.fromPartial({ - // typeUrl: MsgTransfer.typeUrl, - // value: MsgTransfer.encode(transferMsg.value).finish(), - // }), - // ], - // metadata: '', - // proposers: [address], - // title: `IBC Transfer`, - // summary: `This proposal will send ${values.amount} ${values.selectedToken.metadata?.display} to ${values.recipient} via IBC transfer`, - // exec: 0, - // }); + // const msg = isGroup + // ? submitProposal({ + // groupPolicyAddress: admin!, + // messages: [ + // Any.fromPartial({ + // typeUrl: MsgTransfer.typeUrl, + // value: MsgTransfer.encode(transferMsg.value).finish(), + // }), + // ], + // metadata: '', + // proposers: [address], + // title: `IBC Transfer`, + // summary: `This proposal will send ${values.amount} ${values.selectedToken.metadata?.display} to ${values.recipient} via IBC transfer`, + // exec: 0, + // }) + // : transferMsg; // const fee = await estimateFee( - // selectedFromChain === env.osmosisChain ? (osmosisAddress ?? '') : (address ?? ''), + // selectedFromChain.id === env.osmosisChain + // ? (chains.osmosistestnet.address ?? '') + // : (address ?? ''), // [msg] // );