-
-
-
- Chain
-
-
-
@@ -365,7 +747,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();
})()}
diff --git a/components/factory/forms/TokenDetailsForm.tsx b/components/factory/forms/TokenDetailsForm.tsx
index 85c34ffe..0a802fae 100644
--- a/components/factory/forms/TokenDetailsForm.tsx
+++ b/components/factory/forms/TokenDetailsForm.tsx
@@ -39,6 +39,7 @@ export default function TokenDetails({
formData.isGroup && formData.groupPolicyAddress ? formData.groupPolicyAddress : address;
const fullDenom = `factory/${effectiveAddress}/u${formData.subdenom}`;
+
// Automatically set denom units
React.useEffect(() => {
const denomUnits = [
diff --git a/components/factory/modals/updateDenomMetadata.tsx b/components/factory/modals/updateDenomMetadata.tsx
index b01d15d9..89e53dec 100644
--- a/components/factory/modals/updateDenomMetadata.tsx
+++ b/components/factory/modals/updateDenomMetadata.tsx
@@ -97,11 +97,10 @@ export default function UpdateDenomMetadataModal({
sender: admin,
metadata: {
description: values.description || formData.description,
- denomUnits:
- [
- { denom: fullDenom, exponent: 0, aliases: [symbol] },
- { denom: symbol, exponent: 6, aliases: [fullDenom] },
- ] || formData.denomUnits,
+ denomUnits: [
+ { denom: fullDenom, exponent: 0, aliases: [symbol] },
+ { denom: symbol, exponent: 6, aliases: [fullDenom] },
+ ],
base: fullDenom,
display: symbol,
name: values.name || formData.name,
@@ -122,11 +121,10 @@ export default function UpdateDenomMetadataModal({
sender: address,
metadata: {
description: values.description || formData.description,
- denomUnits:
- [
- { denom: fullDenom, exponent: 0, aliases: [symbol] },
- { denom: symbol, exponent: 6, aliases: [fullDenom] },
- ] || formData.denomUnits,
+ denomUnits: [
+ { denom: fullDenom, exponent: 0, aliases: [symbol] },
+ { denom: symbol, exponent: 6, aliases: [fullDenom] },
+ ],
base: fullDenom,
display: symbol,
name: values.name || formData.name,
diff --git a/components/groups/components/groupControls.tsx b/components/groups/components/groupControls.tsx
index cdbc5d91..ca8774ff 100644
--- a/components/groups/components/groupControls.tsx
+++ b/components/groups/components/groupControls.tsx
@@ -13,7 +13,7 @@ import { useRouter } from 'next/router';
import VoteDetailsModal from '@/components/groups/modals/voteDetailsModal';
import { useGroupsByMember } from '@/hooks/useQueries';
-import { useChain } from '@cosmos-kit/react';
+import { useChain, useChains } from '@cosmos-kit/react';
import { MemberSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/types';
import { ArrowRightIcon } from '@/components/icons';
import ProfileAvatar from '@/utils/identicon';
@@ -230,6 +230,7 @@ export default function GroupControls({
}
const { address } = useChain(env.chain);
+ const chains = useChains([env.chain, env.osmosisChain, env.axelarChain]);
const { groupByMemberData } = useGroupsByMember(address ?? '');
useEffect(() => {
@@ -603,6 +604,7 @@ export default function GroupControls({
isGroup={true}
admin={policyAddress}
refetchProposals={refetchProposals}
+ chains={chains}
/>
)}
diff --git a/components/groups/components/myGroups.tsx b/components/groups/components/myGroups.tsx
index 4f9a7aa4..1582296c 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,35 @@ 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);
+
+ let baseDenom = '';
+ if (assetInfo?.traces && assetInfo.traces.length > 1) {
+ 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/components/react/modal.tsx b/components/react/modal.tsx
index 9b416e8a..fe4b61ca 100644
--- a/components/react/modal.tsx
+++ b/components/react/modal.tsx
@@ -36,6 +36,8 @@ import { Web3AuthClient, Web3AuthWallet } from '@cosmos-kit/web3auth';
import { useDeviceDetect } from '@/hooks';
import { State } from '@cosmos-kit/core';
import { ExpiredError } from '@cosmos-kit/core';
+import env from '@/config/env';
+import { useChains } from '@cosmos-kit/react';
export enum ModalView {
WalletList,
@@ -92,7 +94,25 @@ export const TailwindModal: React.FC<
const [qrState, setQRState] = useState
(State.Init);
const [qrMessage, setQrMessage] = useState('');
- const current = walletRepo?.current;
+ const chains = useChains([env.chain, env.osmosisChain, env.axelarChain]);
+
+ 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 54db3e10..fee320fd 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';
@@ -37,7 +37,51 @@ interface WalletSectionProps {
}
export const WalletSection: React.FC = ({ chainName }) => {
- const { connect, openView, status, username, address, wallet } = useChain(chainName);
+ const chains = useChains([env.chain, env.osmosisChain, env.axelarChain]);
+
+ const chainStates = useMemo(() => {
+ return Object.values(chains).map(chain => ({
+ connect: chain.connect,
+ openView: chain.openView,
+ status: chain.status,
+ username: chain.username,
+ address: chain.address,
+ wallet: chain.wallet,
+ }));
+ }, [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.find(chain => chain.username)?.username || undefined,
+ [chainStates]
+ );
+
+ const wallet = useMemo(() => chainStates.find(chain => chain.wallet)?.wallet, [chainStates]);
+
+ const address = useMemo(
+ () => chainStates.find(chain => chain.address)?.address || undefined,
+ [chainStates]
+ );
const [localStatus, setLocalStatus] = useState(status);
const timeoutRef = useRef>();
diff --git a/config/defaults.ts b/config/defaults.ts
deleted file mode 100644
index 7bf5d290..00000000
--- 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/env.ts b/config/env.ts
index 543750c5..c3b5d146 100644
--- a/config/env.ts
+++ b/config/env.ts
@@ -1,14 +1,36 @@
const env = {
- chainId: process.env.NEXT_PUBLIC_CHAIN_ID ?? '',
- rpcUrl: process.env.NEXT_PUBLIC_RPC_URL ?? '',
- explorerUrl: process.env.NEXT_PUBLIC_EXPLORER_URL ?? '',
- web3AuthClientId: process.env.NEXT_PUBLIC_WEB3AUTH_CLIENT_ID ?? '',
+ // Wallet
walletConnectKey: process.env.NEXT_PUBLIC_WALLETCONNECT_KEY ?? '',
web3AuthNetwork: process.env.NEXT_PUBLIC_WEB3AUTH_NETWORK ?? '',
+ web3AuthClientId: process.env.NEXT_PUBLIC_WEB3AUTH_CLIENT_ID ?? '',
+
+ // Chains
chain: process.env.NEXT_PUBLIC_CHAIN ?? '',
+ osmosisChain: process.env.NEXT_PUBLIC_OSMOSIS_CHAIN ?? '',
+ axelarChain: process.env.NEXT_PUBLIC_AXELAR_CHAIN ?? '',
+ chainId: process.env.NEXT_PUBLIC_CHAIN_ID ?? '',
+ osmosisChainId: process.env.NEXT_PUBLIC_OSMOSIS_CHAIN_ID ?? '',
+ axelarChainId: process.env.NEXT_PUBLIC_AXELAR_CHAIN_ID ?? '',
+
+ // Ops
chainTier: process.env.NEXT_PUBLIC_CHAIN_TIER ?? '',
+
+ // Explorer URLs
+ explorerUrl: process.env.NEXT_PUBLIC_EXPLORER_URL ?? '',
+ osmosisExplorerUrl: process.env.NEXT_PUBLIC_OSMOSIS_EXPLORER_URL ?? '',
+ axelarExplorerUrl: process.env.NEXT_PUBLIC_AXELAR_EXPLORER_URL ?? '',
+ // RPC and API URLs
+ rpcUrl: process.env.NEXT_PUBLIC_RPC_URL ?? '',
apiUrl: process.env.NEXT_PUBLIC_API_URL ?? '',
indexerUrl: process.env.NEXT_PUBLIC_INDEXER_URL ?? '',
+
+ // Osmosis RPC URLs
+ osmosisApiUrl: process.env.NEXT_PUBLIC_OSMOSIS_API_URL ?? '',
+ osmosisRpcUrl: process.env.NEXT_PUBLIC_OSMOSIS_RPC_URL ?? '',
+
+ // Axelar RPC URLs
+ axelarApiUrl: process.env.NEXT_PUBLIC_AXELAR_API_URL ?? '',
+ axelarRpcUrl: process.env.NEXT_PUBLIC_AXELAR_RPC_URL ?? '',
};
export default env;
diff --git a/config/index.ts b/config/index.ts
index 34b53e46..c1532d6d 100644
--- a/config/index.ts
+++ b/config/index.ts
@@ -1,2 +1 @@
-export * from './defaults';
export * from './env';
diff --git a/contexts/skipGoContext.tsx b/contexts/skipGoContext.tsx
new file mode 100644
index 00000000..ffadd0dc
--- /dev/null
+++ b/contexts/skipGoContext.tsx
@@ -0,0 +1,47 @@
+import React, { createContext, useContext, useMemo } from 'react';
+import { SkipClient, SkipClientOptions } from '@skip-go/client';
+import { OfflineDirectSigner } from '@cosmjs/proto-signing';
+import { OfflineAminoSigner } from '@cosmjs/amino';
+
+// Create the context
+interface SkipContextType {
+ createClient: (options: SkipClientOptions) => SkipClient;
+}
+
+const SkipContext = createContext(undefined);
+
+// Create the provider component
+interface SkipProviderProps {
+ children: React.ReactNode;
+}
+
+export function SkipProvider({ children }: SkipProviderProps) {
+ const createClient = useMemo(() => {
+ return (options: SkipClientOptions) => new SkipClient(options);
+ }, []);
+
+ return {children};
+}
+
+// Update the hook to accept getCosmosSigner
+interface UseSkipClientOptions {
+ getCosmosSigner: () => Promise<
+ OfflineAminoSigner | OfflineDirectSigner | (OfflineAminoSigner & OfflineDirectSigner)
+ >;
+}
+
+export function useSkipClient(options: UseSkipClientOptions) {
+ const context = useContext(SkipContext);
+ if (context === undefined) {
+ throw new Error('useSkipClient must be used within a SkipProvider');
+ }
+
+ // 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/hooks/useFeeEstimation.ts b/hooks/useFeeEstimation.ts
index 37421bf8..ff204f5f 100644
--- a/hooks/useFeeEstimation.ts
+++ b/hooks/useFeeEstimation.ts
@@ -2,15 +2,11 @@ import { EncodeObject } from '@cosmjs/proto-signing';
import { GasPrice, calculateFee } from '@cosmjs/stargate';
import { useChain } from '@cosmos-kit/react';
-import { getCoin } from '@/utils';
-
export const useFeeEstimation = (chainName: string) => {
const { getSigningStargateClient, chain } = useChain(chainName);
const gasPrice = chain.fees?.fee_tokens[0].average_gas_price || 0.025;
- const coin = getCoin(chainName);
-
const estimateFee = async (
address: string,
messages: EncodeObject[],
@@ -29,7 +25,7 @@ export const useFeeEstimation = (chainName: string) => {
const fee = calculateFee(
Math.round(gasEstimation * (modifier || 1.5)),
- GasPrice.fromString(gasPrice + 'umfx')
+ GasPrice.fromString(`${gasPrice}${chainName === 'manifesttestnet' ? 'umfx' : 'uosmo'}`)
);
return fee;
diff --git a/hooks/useLcdQueryClient.ts b/hooks/useLcdQueryClient.ts
index 13b6074d..510f3ff9 100644
--- a/hooks/useLcdQueryClient.ts
+++ b/hooks/useLcdQueryClient.ts
@@ -19,3 +19,19 @@ export const useLcdQueryClient = () => {
lcdQueryClient: lcdQueryClient.data,
};
};
+
+export const useOsmosisLcdQueryClient = () => {
+ const lcdQueryClient = useQuery({
+ queryKey: ['lcdQueryClientOsmosis', env.osmosisApiUrl],
+ queryFn: () =>
+ createLcdQueryClient({
+ restEndpoint: env.osmosisApiUrl,
+ }),
+ enabled: !!env.osmosisApiUrl,
+ staleTime: Infinity,
+ });
+
+ return {
+ lcdQueryClient: lcdQueryClient.data,
+ };
+};
diff --git a/hooks/useQueries.ts b/hooks/useQueries.ts
index ca3f4825..86d383f5 100644
--- a/hooks/useQueries.ts
+++ b/hooks/useQueries.ts
@@ -2,9 +2,9 @@ import { useEffect, useState } from 'react';
import { useQueries, useQuery } from '@tanstack/react-query';
import { QueryGroupsByMemberResponseSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/query';
-import { useLcdQueryClient } from './useLcdQueryClient';
+import { useLcdQueryClient, useOsmosisLcdQueryClient } from './useLcdQueryClient';
import { usePoaLcdQueryClient } from './usePoaLcdQueryClient';
-import { getLogoUrls } from '@/utils';
+import { getLogoUrls, normalizeIBCDenom } from '@/utils';
import { useManifestLcdQueryClient } from './useManifestLcdQueryClient';
@@ -632,6 +632,32 @@ export const useTokenFactoryDenomsMetadata = () => {
};
};
+export const useOsmosisTokenFactoryDenomsMetadata = () => {
+ const { lcdQueryClient } = useOsmosisLcdQueryClient();
+
+ const fetchDenoms = async () => {
+ if (!lcdQueryClient) {
+ throw new Error('LCD Client not ready');
+ }
+
+ return await lcdQueryClient.cosmos.bank.v1beta1.denomsMetadata({});
+ };
+
+ const denomsQuery = useQuery({
+ queryKey: ['osmosisAllMetadatas'],
+ queryFn: fetchDenoms,
+ enabled: !!lcdQueryClient,
+ staleTime: Infinity,
+ });
+
+ return {
+ metadatas: denomsQuery.data,
+ isMetadatasLoading: denomsQuery.isLoading,
+ isMetadatasError: denomsQuery.isError,
+ refetchMetadatas: denomsQuery.refetch,
+ };
+};
+
export const useTokenBalances = (address: string) => {
const { lcdQueryClient } = useLcdQueryClient();
@@ -660,6 +686,34 @@ export const useTokenBalances = (address: string) => {
};
};
+export const useTokenBalancesOsmosis = (address: string) => {
+ const { lcdQueryClient } = useOsmosisLcdQueryClient();
+
+ const fetchBalances = async () => {
+ if (!lcdQueryClient) {
+ throw new Error('LCD Client not ready');
+ }
+ return await lcdQueryClient.cosmos.bank.v1beta1.allBalances({
+ address,
+ resolveDenom: false,
+ });
+ };
+
+ const balancesQuery = useQuery({
+ queryKey: ['osmosisBalances', address],
+ queryFn: fetchBalances,
+ enabled: !!lcdQueryClient && !!address,
+ staleTime: Infinity,
+ });
+
+ return {
+ balances: balancesQuery.data?.balances,
+ isBalancesLoading: balancesQuery.isLoading,
+ isBalancesError: balancesQuery.isError,
+ refetchBalances: balancesQuery.refetch,
+ };
+};
+
export const useTokenBalancesResolved = (address: string) => {
const { lcdQueryClient } = useLcdQueryClient();
@@ -688,6 +742,126 @@ export const useTokenBalancesResolved = (address: string) => {
};
};
+export const useOsmosisTokenBalancesResolved = (address: string) => {
+ const { lcdQueryClient } = useOsmosisLcdQueryClient();
+
+ const fetchBalances = async () => {
+ if (!lcdQueryClient) {
+ throw new Error('LCD Client not ready');
+ }
+ return await lcdQueryClient.cosmos.bank.v1beta1.allBalances({
+ address,
+ resolveDenom: true,
+ });
+ };
+
+ const balancesQuery = useQuery({
+ queryKey: ['osmosisBalances-resolved', address],
+ queryFn: fetchBalances,
+ enabled: !!lcdQueryClient && !!address,
+ staleTime: Infinity,
+ });
+
+ return {
+ balances: balancesQuery.data?.balances,
+ isBalancesLoading: balancesQuery.isLoading,
+ isBalancesError: balancesQuery.isError,
+ refetchBalances: balancesQuery.refetch,
+ };
+};
+
+interface TransactionAmount {
+ amount: string;
+ denom: string;
+}
+export enum HistoryTxType {
+ SEND,
+ MINT,
+ BURN,
+ PAYOUT,
+ BURN_HELD_BALANCE,
+}
+
+const _formatMessage = (
+ message: any,
+ address: string
+): {
+ data: {
+ tx_type: HistoryTxType;
+ from_address: string;
+ to_address: string;
+ amount: { amount: string; denom: string }[];
+ };
+}[] => {
+ switch (message['@type']) {
+ case `/cosmos.bank.v1beta1.MsgSend`:
+ return [
+ {
+ data: {
+ tx_type: HistoryTxType.SEND,
+ from_address: message.fromAddress,
+ to_address: message.toAddress,
+ amount: message.amount.map((amt: TransactionAmount) => ({
+ amount: amt.amount,
+ denom: amt.denom,
+ })),
+ },
+ },
+ ];
+ case `/osmosis.tokenfactory.v1beta1.MsgMint`:
+ return [
+ {
+ data: {
+ tx_type: HistoryTxType.MINT,
+ from_address: message.sender,
+ to_address: message.mintToAddress,
+ amount: [message.amount],
+ },
+ },
+ ];
+ case `/osmosis.tokenfactory.v1beta1.MsgBurn`:
+ return [
+ {
+ data: {
+ tx_type: HistoryTxType.BURN,
+ from_address: message.sender,
+ to_address: message.burnFromAddress,
+ amount: [message.amount],
+ },
+ },
+ ];
+ case `/liftedinit.manifest.v1.MsgPayout`:
+ return message.payoutPairs
+ .map((pair: { coin: TransactionAmount; address: string }) => {
+ if (message.authority === address || pair.address === address) {
+ return {
+ data: {
+ tx_type: HistoryTxType.PAYOUT,
+ from_address: message.authority,
+ to_address: pair.address,
+ amount: [{ amount: pair.coin.amount, denom: pair.coin.denom }],
+ },
+ };
+ }
+ return null;
+ })
+ .filter((msg: any) => msg !== null);
+ case `/lifted.init.manifest.v1.MsgBurnHeldBalance`:
+ return [
+ {
+ data: {
+ tx_type: HistoryTxType.BURN_HELD_BALANCE,
+ from_address: message.authority,
+ to_address: message.authority,
+ amount: message.burnCoins,
+ },
+ },
+ ];
+ default:
+ return [];
+ }
+};
+
export const useGetMessagesFromAddress = (
indexerUrl: string,
address: string,
diff --git a/hooks/useTx.tsx b/hooks/useTx.tsx
index 30d2ea2b..e87598f3 100644
--- a/hooks/useTx.tsx
+++ b/hooks/useTx.tsx
@@ -46,7 +46,7 @@ export const useTx = (chainName: string) => {
const { address, getSigningStargateClient, estimateFee } = useChain(chainName);
const { setToastMessage } = useToast();
const [isSigning, setIsSigning] = useState(false);
- const explorerUrl = env.explorerUrl;
+ const explorerUrl = chainName === env.osmosisChain ? env.osmosisExplorerUrl : env.explorerUrl;
const tx = async (msgs: Msg[], options: TxOptions) => {
if (!address) {
@@ -107,6 +107,7 @@ export const useTx = (chainName: string) => {
if (isDeliverTxSuccess(res)) {
if (options.onSuccess) options.onSuccess();
setIsSigning(false);
+
if (msgs.filter(msg => msg.typeUrl === '/cosmos.group.v1.MsgSubmitProposal').length > 0) {
const submitProposalEvent = res.events.find(
event => event.type === 'cosmos.group.v1.EventSubmitProposal'
diff --git a/package.json b/package.json
index 5d8487e2..fb4b6add 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"private": false,
"description": "An application to interact with the Manifest Chain",
"scripts": {
- "dev": "next dev -H 0.0.0.0",
+ "dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
@@ -13,7 +13,9 @@
"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",
+ "print-tokens": "tsx scripts/printAllTokens.ts"
},
"author": "The Lifted Initiative",
"license": "MIT",
@@ -45,6 +47,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.8",
"@tanstack/react-query": "^5.55.0",
"@tanstack/react-query-devtools": "^5.55.0",
"@types/file-saver": "^2.0.7",
@@ -52,7 +55,7 @@
"apexcharts": "^3.54.0",
"autoprefixer": "^10.4.20",
"babel-plugin-glsl": "^1.0.0",
- "chain-registry": "1.69.93",
+ "chain-registry": "^1.69.115",
"cosmjs-types": "^0.9.0",
"cosmos-kit": "2.23.9",
"country-flag-icons": "^1.5.13",
@@ -107,7 +110,7 @@
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"prettier": "^3.3.3",
- "typescript": "4.9.3"
+ "typescript": "5.7.3"
},
"files": [
"."
diff --git a/pages/_app.tsx b/pages/_app.tsx
index 42f5e266..d8b2bcf6 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -9,7 +9,15 @@ 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 { assets as axelarAssets, chain as axelarChain } from 'chain-registry/testnet/axelartestnet';
import { SignerOptions, wallets } from 'cosmos-kit';
import { wallets as cosmosExtensionWallets } from '@cosmos-kit/cosmos-extension-metamask';
@@ -31,15 +39,21 @@ import {
osmosisProtoRegistry,
cosmosAminoConverters,
cosmosProtoRegistry,
+ ibcAminoConverters,
+ ibcProtoRegistry,
} from '@liftedinit/manifestjs';
import MobileNav from '@/components/react/mobileNav';
import { WEB3AUTH_NETWORK_TYPE } from '@web3auth/auth';
+import { SkipProvider } from '@/contexts/skipGoContext';
+
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
@@ -63,11 +77,13 @@ function ManifestApp({ Component, pageProps }: ManifestAppProps) {
...osmosisProtoRegistry,
...strangeloveVenturesProtoRegistry,
...liftedinitProtoRegistry,
+ ...ibcProtoRegistry,
]);
const mergedAminoTypes = new AminoTypes({
...cosmosAminoConverters,
...liftedinitAminoConverters,
...osmosisAminoConverters,
+ ...ibcAminoConverters,
...strangeloveVenturesAminoConverters,
});
return {
@@ -174,6 +190,10 @@ function ManifestApp({ Component, pageProps }: ManifestAppProps) {
rpc: [env.rpcUrl],
rest: [env.apiUrl],
},
+ ['osmosistestnet']: {
+ rpc: [env.osmosisRpcUrl],
+ rest: [env.osmosisApiUrl],
+ },
},
};
@@ -183,8 +203,9 @@ function ManifestApp({ Component, pageProps }: ManifestAppProps) {
{
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
- {/* 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
+ )}
+
+
+
}
diff --git a/pages/bank.tsx b/pages/bank.tsx
index 0799d933..cbf6d97c 100644
--- a/pages/bank.tsx
+++ b/pages/bank.tsx
@@ -3,20 +3,24 @@ import { TokenList } from '@/components/bank/components/tokenList';
import {
useGetMessagesFromAddress,
useIsMobile,
+ useOsmosisTokenBalancesResolved,
+ useOsmosisTokenFactoryDenomsMetadata,
useTokenBalances,
+ useTokenBalancesOsmosis,
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';
import { CombinedBalanceInfo } from '@/utils/types';
-import { MFX_TOKEN_DATA } from '@/utils/constants';
+import { MFX_TOKEN_DATA, OSMOSIS_TOKEN_DATA } from '@/utils/constants';
import env from '@/config/env';
import { SEO } from '@/components';
import { useResponsivePageSize } from '@/hooks/useResponsivePageSize';
import Link from 'next/link';
+import { denomToAsset } from '@/utils';
interface PageSizeConfig {
tokenList: number;
@@ -25,13 +29,21 @@ 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, env.axelarChain]);
+
+ const isWalletConnected = useMemo(
+ () => Object.values(chains).every(chain => chain.isWalletConnected),
+ [chains]
+ );
+
+ 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);
@@ -81,7 +93,12 @@ export default function Bank() {
isError,
refetch: refetchHistory,
totalCount,
- } = useGetMessagesFromAddress(env.indexerUrl, address ?? '', currentPage, historyPageSize);
+ } = useGetMessagesFromAddress(
+ env.indexerUrl,
+ chains.manifesttestnet.address ?? '',
+ currentPage,
+ historyPageSize
+ );
const combinedBalances = useMemo(() => {
if (!balances || !resolvedBalances || !metadatas) return [];
@@ -109,6 +126,32 @@ export default function Bank() {
);
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,
@@ -121,6 +164,79 @@ export default function Bank() {
return mfxCombinedBalance ? [mfxCombinedBalance, ...otherBalances] : otherBalances;
}, [balances, resolvedBalances, metadatas]);
+ const {
+ balances: osmosisBalances,
+ isBalancesLoading: isOsmosisBalancesLoading,
+ refetchBalances: refetchOsmosisBalances,
+ } = useTokenBalancesOsmosis(chains.osmosistestnet.address ?? '');
+ const {
+ balances: resolvedOsmosisBalances,
+ isBalancesLoading: resolvedOsmosisLoading,
+ refetchBalances: resolveOsmosisRefetch,
+ } = useOsmosisTokenBalancesResolved(chains.osmosistestnet.address ?? '');
+
+ 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.osmosisChain, 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('');
@@ -193,24 +309,29 @@ export default function Bank() {
) : (
))}
{activeTab === 'history' &&
- (totalCount === 0 && !txLoading ? (
+ (totalPages === 0 ? (
) : (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scripts/ibcTransferAll.ts b/scripts/ibcTransferAll.ts
new file mode 100644
index 00000000..40e1ce68
--- /dev/null
+++ b/scripts/ibcTransferAll.ts
@@ -0,0 +1,289 @@
+// This script is used to transfer all tokens on manifest to destination chain then print out a list of tokens that can be used in the chain-registry
+// you can run this script by providing a mnemonic as an environment variable: `WALLET_MNEMONIC="..." bun run ibc-transfer`
+
+// ENV's:
+// DESTINATION_RPC_URL: the rpc url of the destination chain
+// DESTINATION_CHAIN: the name of the destination chain
+// DESTINATION_PREFIX: the prefix of the destination chain
+// SOURCE_CHANNEL: the channel id of the source chain
+// DESTINATION_CHANNEL: the channel id of the destination chain
+
+// You can provide the above env's in the command in the same fashion as the mnemonic or they will be set to default values
+// Axelar example:
+// WALLET_MNEMONIC="" DESTINATION_CHAIN="axelar-testnet-lisbon-3" DESTINATION_PREFIX="axelar" SOURCE_CHANNEL="channel-3" DESTINATION_CHANNEL="channel-591" DESTINATION_RPC_URL="https://axelar-testnet-rpc.polkachu.com/" bun run ibc-transfer
+// Axlear query only:
+// QUERY_ONLY=true WALLET_MNEMONIC="" DESTINATION_CHAIN="axelar-testnet-lisbon-3" DESTINATION_PREFIX="axelar" SOURCE_CHANNEL="channel-3" DESTINATION_CHANNEL="channel-591" DESTINATION_RPC_URL="https://axelar-testnet-rpc.polkachu.com/" QUERY_ONLY="true" bun run ibc-transfer
+
+import { DirectSecp256k1HdWallet } from '@cosmjs/proto-signing';
+import { SigningStargateClient } from '@cosmjs/stargate';
+
+import { MsgTransfer } from '@liftedinit/manifestjs/dist/codegen/ibc/applications/transfer/v1/tx';
+
+import * as fs from 'fs';
+import * as path from 'path';
+import { ibc } from '@liftedinit/manifestjs';
+
+// Environment Configuration
+const env = {
+ rpcUrl: 'https://nodes.liftedinit.tech/manifest/testnet/rpc',
+ destinationRpcUrl: process.env.DESTINATION_RPC_URL || 'https://rpc.osmotest5.osmosis.zone',
+ chain: 'manifest-testnet',
+ destinationChain: process.env.DESTINATION_CHAIN || 'osmo-test-5',
+ destinationPrefix: process.env.DESTINATION_PREFIX || 'osmo',
+ sourceChannel: process.env.SOURCE_CHANNEL || 'channel-0',
+ destinationChannel: process.env.DESTINATION_CHANNEL || 'channel-10016',
+};
+
+// Add option for query-only mode at the top with other constants
+const QUERY_ONLY = process.env.QUERY_ONLY === 'true';
+
+// IBC Configuration
+const getIbcInfo = (fromChain: string, toChain: string) => {
+ // Default configuration
+ return {
+ source_port: 'transfer',
+ source_channel: env.sourceChannel,
+ };
+};
+
+// Configuration
+const MANIFEST_RPC = env.rpcUrl;
+const DESTINATION_RPC = env.destinationRpcUrl;
+const SOURCE_CHAIN = env.chain;
+const TARGET_CHAIN = env.destinationChain;
+
+// 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 on Manifest Ledger Testnet`,
+ denom_units: [
+ {
+ denom: ibcDenom,
+ exponent: 0,
+ },
+ {
+ denom: tokenName,
+ exponent: 6,
+ },
+ ],
+ type_asset: 'ics20',
+ base: ibcDenom,
+ name: displayName,
+ display: tokenName,
+ symbol: displayName,
+ traces: [
+ {
+ type: 'ibc',
+ counterparty: {
+ chain_name: 'manifesttestnet',
+ base_denom: originalDenom,
+ channel_id: env.sourceChannel,
+ },
+ chain: {
+ channel_id: env.destinationChannel,
+ path: `${denomTrace.path}/${originalDenom}`,
+ },
+ },
+ ],
+ 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`,
+ },
+ ],
+ 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`,
+ },
+ };
+}
+
+// Update the getDenomTrace function
+async function getDenomTrace(hash: string) {
+ try {
+ const { createRPCQueryClient } = ibc.ClientFactory;
+ const client = await createRPCQueryClient({
+ rpcEndpoint: DESTINATION_RPC,
+ });
+
+ const response = await client.ibc.applications.transfer.v1.denomTrace({
+ hash: hash,
+ });
+
+ console.log('Denom trace response:', response);
+ return response.denomTrace;
+ } catch (error: any) {
+ console.error('Error fetching denom trace:', {
+ error: error.message,
+ hash: hash,
+ });
+ 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 destinationWallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, {
+ prefix: env.destinationPrefix,
+ });
+
+ // Get addresses
+ const [manifestAccount] = await manifestWallet.getAccounts();
+ const [destinationAccount] = await destinationWallet.getAccounts();
+
+ console.log('Manifest address:', manifestAccount.address);
+ console.log('Destination address:', destinationAccount.address);
+
+ // Create signing clients
+ const manifestClient = await SigningStargateClient.connectWithSigner(
+ MANIFEST_RPC,
+ manifestWallet
+ );
+ const destinationClient = await SigningStargateClient.connectWithSigner(
+ DESTINATION_RPC,
+ destinationWallet
+ );
+
+ // 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: destinationAccount.address,
+ token: {
+ denom: token.denom,
+ amount: '1',
+ },
+ timeoutHeight: {
+ revisionNumber: BigInt(0),
+ revisionHeight: BigInt(0),
+ },
+ timeoutTimestamp: BigInt(timeoutInNanos),
+ },
+ };
+ });
+
+ // Execute transfers only if not in query-only mode
+ if (!QUERY_ONLY && messages.length > 0) {
+ try {
+ const fee = {
+ amount: [{ denom: 'umfx', amount: '5500' }],
+ gas: '5000000',
+ };
+
+ console.log('\nExecuting IBC transfers...');
+ console.log(`Total tokens to transfer: ${messages.length}`);
+
+ // Process each message individually
+ for (let i = 0; i < messages.length; i++) {
+ const message = messages[i];
+ console.log(`\nProcessing transfer ${i + 1} of ${messages.length}`);
+ console.log(`Transferring token...`);
+
+ const result = await manifestClient.signAndBroadcast(
+ manifestAccount.address,
+ [message], // Send single message instead of batch
+ 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,
+ });
+
+ // Add a small delay between transfers to prevent rate limiting
+ if (i + 1 < messages.length) {
+ console.log('Waiting 5 seconds before next transfer...');
+ await new Promise(resolve => setTimeout(resolve, 5000));
+ }
+ }
+ } catch (error) {
+ console.error('Error during transfer:', error);
+ process.exit(1);
+ }
+ } else if (QUERY_ONLY) {
+ console.log('\nQuery-only mode - skipping transfers');
+ } else {
+ console.log('No tokens to transfer');
+ }
+
+ if (!QUERY_ONLY) {
+ // 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 Destination
+ console.log('\nQuerying Destination balances...');
+ const destinationBalances = await destinationClient.getAllBalances(destinationAccount.address);
+ console.log('Destination chain balances:', destinationBalances);
+
+ // Query IBC denom traces for each IBC token and format them
+ const ibcTokens = destinationBalances.filter(token => token.denom.startsWith('ibc/'));
+ const formattedTokens = [];
+
+ if (ibcTokens.length > 0) {
+ console.log('\nProcessing IBC Denom Traces:');
+ for (const token of ibcTokens) {
+ try {
+ const hash = token.denom.split('/')[1];
+ const denomTrace = await getDenomTrace(hash);
+ console.log(`Processing ${token.denom}:`, denomTrace);
+
+ if (denomTrace) {
+ // Extract original denom from the denom trace
+ const originalDenom = denomTrace.baseDenom;
+ formattedTokens.push(formatTokenForAssetList(token.denom, denomTrace, originalDenom));
+ }
+ } catch (error) {
+ console.error(`Error processing denom trace for ${token.denom}:`, error);
+ }
+ }
+
+ // Save formatted tokens to file
+ if (formattedTokens.length > 0) {
+ const outputPath = path.join(__dirname, 'chain-registry-tokens.json');
+ fs.writeFileSync(outputPath, JSON.stringify({ tokens: formattedTokens }, null, 2));
+ console.log(`\nChain Registry token information saved to ${outputPath}`);
+ }
+ } else {
+ console.log('No IBC tokens found in Destination balances');
+ }
+}
+
+main().catch(console.error);
diff --git a/scripts/printAllTokens.ts b/scripts/printAllTokens.ts
new file mode 100644
index 00000000..53c4aeb3
--- /dev/null
+++ b/scripts/printAllTokens.ts
@@ -0,0 +1,87 @@
+// This script prints all the tokens in the manifest testnet chain in a format to be used in the chain-registry
+
+import { cosmos } from '@liftedinit/manifestjs';
+import * as fs from 'fs';
+import * as path from 'path';
+
+// Environment Configuration
+const env = {
+ rpcUrl: 'https://nodes.liftedinit.tech/manifest/testnet/rpc',
+ chain: 'manifest-testnet',
+};
+
+async function getTokenMetadata(denom: string) {
+ const { createRPCQueryClient } = cosmos.ClientFactory;
+ const client = await createRPCQueryClient({ rpcEndpoint: env.rpcUrl });
+ try {
+ // Query token metadata using the bank module
+ const response = await client.cosmos.bank.v1beta1.denomMetadata({ denom });
+ return response.metadata;
+ } catch (error) {
+ console.error(`Error fetching metadata for ${denom}:`, error);
+ return null;
+ }
+}
+
+function formatTokenInfo(denom: string, metadata: any = null) {
+ const tokenName = denom.split('/').pop()?.replace('u', '') || '';
+ const displayName = tokenName.toUpperCase();
+
+ return {
+ description: metadata?.description || `${displayName} Token`,
+ denom_units: [
+ {
+ denom: denom,
+ exponent: 0,
+ },
+ {
+ denom: displayName.toLowerCase(),
+ exponent: 6,
+ },
+ ],
+ base: denom,
+ name: `${displayName} Token`,
+ display: displayName.toLowerCase(),
+ symbol: displayName,
+ 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: [
+ {
+ 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',
+ },
+ ],
+ type_asset: 'factory_token',
+ };
+}
+
+async function main() {
+ // Create client
+ const { createRPCQueryClient } = cosmos.ClientFactory;
+ const client = await createRPCQueryClient({ rpcEndpoint: env.rpcUrl });
+
+ // Query balances for the specified address
+ const address = 'manifest1hj5fveer5cjtn4wd6wstzugjfdxzl0xp8ws9ct';
+ const balances = await client.cosmos.bank.v1beta1.allBalances({ address, resolveDenom: false });
+
+ // Filter for factory tokens and format them
+ const factoryTokens = balances.balances.filter(token => token.denom.startsWith('factory/'));
+ const formattedTokens = [];
+
+ console.log(`Found ${factoryTokens.length} factory tokens`);
+
+ for (const token of factoryTokens) {
+ const metadata = await getTokenMetadata(token.denom);
+ const formattedToken = formatTokenInfo(token.denom, metadata);
+ formattedTokens.push(formattedToken);
+ }
+
+ // Save to file
+ const outputPath = path.join(__dirname, 'token_metadata.json');
+ fs.writeFileSync(outputPath, JSON.stringify({ tokens: formattedTokens }, null, 2));
+ console.log(`\nToken metadata saved to ${outputPath}`);
+}
+
+main().catch(console.error);
diff --git a/tests/mock.ts b/tests/mock.ts
index a5f9454c..553c437e 100644
--- a/tests/mock.ts
+++ b/tests/mock.ts
@@ -182,6 +182,42 @@ export const defaultAssetLists = [
},
];
+export const osmosisAssetList = [
+ {
+ chain_name: 'osmosistestnet',
+ assets: [
+ {
+ name: 'Osmosis Testnet Token',
+ display: 'uosmo',
+ base: 'uosmo',
+ symbol: 'uosmo',
+ denom_units: [{ denom: 'uosmo', exponent: 0, aliases: ['uosmo'] }],
+ },
+ ],
+ },
+];
+
+export const osmosisChain: Chain = {
+ chain_name: 'osmosistestnet',
+ chain_id: 'osmo-test-5',
+ status: 'live',
+ network_type: 'testnet',
+ pretty_name: 'Osmosis Testnet',
+ bech32_prefix: 'osmo',
+ slip44: 118,
+ fees: {
+ fee_tokens: [
+ {
+ denom: 'uosmo',
+ fixed_min_gas_price: 0.001,
+ low_gas_price: 0.001,
+ average_gas_price: 0.001,
+ high_gas_price: 0.001,
+ },
+ ],
+ },
+};
+
export const defaultChain: Chain = {
chain_name: 'manifest',
chain_id: 'manifest-1',
diff --git a/tests/render.tsx b/tests/render.tsx
index 20faad3b..344f9730 100644
--- a/tests/render.tsx
+++ b/tests/render.tsx
@@ -2,12 +2,22 @@ import React from 'react';
import { render } from '@testing-library/react';
import { ChainProvider } from '@cosmos-kit/react';
import { ToastProvider } from '@/contexts';
-import { defaultAssetLists, defaultChain } from '@/tests/mock';
+
+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 { assets as axelarAssets, chain as axelarChain } from 'chain-registry/testnet/axelartestnet';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { SkipProvider } from '@/contexts/skipGoContext';
const defaultOptions = {
- chains: [defaultChain],
- assetLists: defaultAssetLists,
+ chains: [manifestChain, osmosisChain, axelarChain],
+ assetLists: [manifestAssets, osmosisAssets, axelarAssets],
wallets: [],
};
@@ -17,7 +27,9 @@ export const renderWithChainProvider = (ui: React.ReactElement, options = {}) =>
return render(
- {ui}
+
+ {ui}
+
,
options
diff --git a/utils/constants.ts b/utils/constants.ts
index e9ab2771..64cef0ec 100644
--- a/utils/constants.ts
+++ b/utils/constants.ts
@@ -14,6 +14,20 @@ export const MFX_TOKEN_DATA: Omit = {
+ description: 'The native token of the Osmosis Chain',
+ denom_units: [
+ { denom: 'uosmo', exponent: 0, aliases: [] },
+ { denom: 'osmo', exponent: 6, aliases: [] },
+ ],
+ base: 'uosmo',
+ display: 'osmo',
+ name: 'Osmosis',
+ symbol: 'OSMO',
+ uri: '',
+ uri_hash: '',
+};
+
export const tokenExponents = [
{ exponent: 18, subdenom: 'atto', letter: 'a', description: 'Smallest unit, 10⁻¹⁸' },
{ exponent: 15, subdenom: 'femto', letter: 'f', description: '10⁻¹⁵' },
diff --git a/utils/format.ts b/utils/format.ts
index 24bc741a..6aa72dd7 100644
--- a/utils/format.ts
+++ b/utils/format.ts
@@ -1,5 +1,7 @@
import { MetadataSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/bank/v1beta1/bank';
import { shiftDigits } from '@/utils/maths';
+import { denomToAsset } from './ibc';
+import env from '@/config/env';
export function formatLargeNumber(num: number): string {
if (!Number.isFinite(num)) return 'Invalid number';
@@ -30,9 +32,17 @@ export function formatLargeNumber(num: number): string {
}
export function formatDenom(denom: string): string {
- const cleanDenom = denom.replace(/^factory\/[^/]+\//, '');
+ const assetInfo = denomToAsset(env.chain, denom);
- if (cleanDenom.startsWith('u')) {
+ // Fallback to cleaning the denom if no assetInfo
+ const cleanDenom = denom?.replace(/^factory\/[^/]+\//, '');
+
+ // Skip cleaning for IBC denoms as they should be resolved via assetInfo
+ if (cleanDenom.startsWith('ibc/')) {
+ return assetInfo?.display.toUpperCase() ?? '';
+ }
+
+ if (cleanDenom?.startsWith('u')) {
return cleanDenom.slice(1).toUpperCase();
}
diff --git a/utils/ibc.ts b/utils/ibc.ts
index 9a6e9cbd..638a0eba 100644
--- a/utils/ibc.ts
+++ b/utils/ibc.ts
@@ -1,9 +1,16 @@
-import { asset_lists as assetLists } from '@chain-registry/assets';
-import { Asset, AssetList } from '@chain-registry/types';
-import { assets, ibc } from 'chain-registry';
+import { Asset, AssetList, IBCInfo } from '@chain-registry/types';
+
import { Coin } from '@liftedinit/manifestjs/dist/codegen/cosmos/base/v1beta1/coin';
import { shiftDigits } from './maths';
+import {
+ assets as manifestAssets,
+ ibc as manifestIbc,
+} from 'chain-registry/testnet/manifesttestnet';
+import { assets as osmosisAssets, ibc as osmosisIbc } from 'chain-registry/testnet/osmosistestnet';
+import { assets as axelarAssets, ibc as axelarIbc } from 'chain-registry/testnet/axelartestnet';
+
+const assets: AssetList[] = [manifestAssets, osmosisAssets, axelarAssets];
export const truncateDenom = (denom: string) => {
return denom.slice(0, 10) + '...' + denom.slice(-6);
@@ -13,19 +20,35 @@ 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) || []
);
};
const getAllAssets = (chainName: string) => {
const nativeAssets = filterAssets(chainName, assets);
- const ibcAssets = filterAssets(chainName, assetLists);
+ const ibcAssets = filterAssets(chainName, assets);
return [...nativeAssets, ...ibcAssets];
};
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) => {
@@ -47,15 +70,17 @@ export const prettyBalance = (chainName: string, balance: Coin) => {
export type PrettyBalance = ReturnType;
+const ibcData: IBCInfo[] = [...manifestIbc, ...osmosisIbc, ...axelarIbc];
+
export const getIbcInfo = (fromChainName: string, toChainName: string) => {
let flipped = false;
- let ibcInfo = ibc.find(
+ let ibcInfo = ibcData.find(
i => i.chain_1.chain_name === fromChainName && i.chain_2.chain_name === toChainName
);
if (!ibcInfo) {
- ibcInfo = ibc.find(
+ ibcInfo = ibcData.find(
i => i.chain_1.chain_name === toChainName && i.chain_2.chain_name === fromChainName
);
flipped = true;
@@ -71,3 +96,25 @@ export const getIbcInfo = (fromChainName: string, toChainName: string) => {
return { source_port, source_channel };
};
+
+export const getIbcDenom = (chainName: string, denom: string) => {
+ 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) => {
+ const asset = denomToAsset(chainName, denom);
+ if (asset) {
+ return {
+ denom: asset.base,
+ };
+ }
+ return { denom };
+};
+
+export type ResolvedIBCDenom = ReturnType;
diff --git a/utils/yupExtensions.ts b/utils/yupExtensions.ts
index e2de151e..c6f3f029 100644
--- a/utils/yupExtensions.ts
+++ b/utils/yupExtensions.ts
@@ -89,7 +89,7 @@ Yup.addMethod(Yup.string, 'manifestAddress', function (message
}
const decoded = bech32.decode(value as `${string}1${string}`);
- const validPrefixes = ['manifest', 'manifestvaloper', 'manifestvalcons'];
+ const validPrefixes = ['manifest', 'manifestvaloper', 'manifestvalcons', 'osmo'];
if (!validPrefixes.includes(decoded.prefix)) {
return createError({
path,