diff --git a/components/chainSelect/ChainSelect.tsx b/components/chainSelect/ChainSelect.tsx
index 91dce56..93abf97 100644
--- a/components/chainSelect/ChainSelect.tsx
+++ b/components/chainSelect/ChainSelect.tsx
@@ -62,6 +62,7 @@ const ChainSelect = () => {
const [tempDisplayDenomExponent, setDisplayDenomExponent] = useState(
state.chain.displayDenomExponent,
);
+ const [tempAssets, setAssets] = useState(state.chain.assets);
const [tempGasPrice, setGasPrice] = useState(state.chain.gasPrice);
const [tempChainName, setChainName] = useState(state.chain.chainDisplayName);
const [tempRegistryName, setRegistryName] = useState(state.chain.registryName);
@@ -110,6 +111,7 @@ const ChainSelect = () => {
setDenom(state.chain.denom);
setDisplayDenom(state.chain.displayDenom);
setDisplayDenomExponent(state.chain.displayDenomExponent);
+ setAssets(state.chain.assets);
setGasPrice(state.chain.gasPrice);
setChainName(state.chain.chainDisplayName);
setExplorerLink(state.chain.explorerLink);
@@ -133,25 +135,27 @@ const ChainSelect = () => {
try {
const chainData = await getChainFromRegistry(chainOption.path);
- const assets = await getAssetsFromRegistry(chainOption.path);
- const firstAsset = assets[0];
+ const registryAssets = await getAssetsFromRegistry(chainOption.path);
+ assert(registryAssets.length >= 1, "No assets found in registry");
+ const firstAsset = registryAssets[0];
const nodeAddress = await getNodeFromArray(chainData.apis.rpc);
const explorerLink = getExplorerFromArray(chainData.explorers);
- const denom = firstAsset.base || "";
- const displayDenom = firstAsset.symbol || "";
-
+ const firstAssetDenom = firstAsset.base;
+ const displayDenom = firstAsset.symbol;
const displayUnit = firstAsset.denom_units.find((u) => u.denom == firstAsset.display);
const displayDenomExponent = displayUnit?.exponent ?? 6;
- const feeToken = chainData.fees.fee_tokens.find((token) => token.denom == denom) ?? { denom };
+ const feeToken = chainData.fees.fee_tokens.find(
+ (token) => token.denom == firstAssetDenom,
+ ) ?? { denom: firstAssetDenom };
const gasPrice =
feeToken.average_gas_price ??
feeToken.low_gas_price ??
feeToken.high_gas_price ??
feeToken.fixed_min_gas_price ??
0.03;
- const formattedGasPrice = firstAsset ? `${gasPrice}${denom}` : "";
+ const formattedGasPrice = firstAsset ? `${gasPrice}${firstAssetDenom}` : "";
// change app state
dispatch({
@@ -163,10 +167,11 @@ const ChainSelect = () => {
chainDisplayName: chainData.pretty_name,
nodeAddress,
explorerLink,
- denom,
+ denom: firstAssetDenom,
displayDenom,
displayDenomExponent,
gasPrice: formattedGasPrice,
+ assets: registryAssets,
},
});
@@ -253,6 +258,7 @@ const ChainSelect = () => {
denom: tempDenom,
displayDenom: tempDisplayDenom,
displayDenomExponent: tempDisplayDenomExponent,
+ assets: tempAssets,
gasPrice: tempGasPrice,
chainId: tempChainId,
chainDisplayName: tempChainName,
@@ -367,14 +373,22 @@ const ChainSelect = () => {
}
label="Denom Exponent"
/>
+ ) =>
+ setAssets(JSON.parse(e.target.value))
+ }
+ label="Assets"
+ />
+
+
) => setGasPrice(e.target.value)}
label="Gas Price"
/>
-
-
{
+ if (!assets?.length) {
+ return [customDenomOption];
+ }
+
+ return [...assets.map((asset) => ({ label: asset.symbol, value: asset })), customDenomOption];
+};
+
interface MsgSendFormProps {
readonly fromAddress: string;
readonly setMsgGetter: (msgGetter: MsgGetter) => void;
@@ -19,60 +31,90 @@ const MsgSendForm = ({ fromAddress, setMsgGetter, deleteMsg }: MsgSendFormProps)
const { state } = useAppContext();
assert(state.chain.addressPrefix, "addressPrefix missing");
+ const denomOptions = getDenomOptions(state.chain.assets);
+
const [toAddress, setToAddress] = useState("");
+ const [selectedDenom, setSelectedDenom] = useState(denomOptions[0]);
+ const [customDenom, setCustomDenom] = useState("");
const [amount, setAmount] = useState("0");
const [toAddressError, setToAddressError] = useState("");
+ const [customDenomError, setCustomDenomError] = useState("");
const [amountError, setAmountError] = useState("");
useEffect(() => {
- try {
- assert(state.chain.denom, "denom missing");
+ assert(state.chain.denom, "denom missing");
- setToAddressError("");
- setAmountError("");
+ setToAddressError("");
+ setCustomDenomError("");
+ setAmountError("");
- const isMsgValid = (): boolean => {
- assert(state.chain.addressPrefix, "addressPrefix missing");
+ const isMsgValid = (): boolean => {
+ assert(state.chain.addressPrefix, "addressPrefix missing");
- const addressErrorMsg = checkAddress(toAddress, state.chain.addressPrefix);
- if (addressErrorMsg) {
- setToAddressError(
- `Invalid address for network ${state.chain.chainId}: ${addressErrorMsg}`,
- );
- return false;
+ const addressErrorMsg = checkAddress(toAddress, state.chain.addressPrefix);
+ if (addressErrorMsg) {
+ setToAddressError(`Invalid address for network ${state.chain.chainId}: ${addressErrorMsg}`);
+ return false;
+ }
+
+ if (selectedDenom.value === customDenomOption.value && !customDenom) {
+ setCustomDenomError("Custom denom must be set because of selection above");
+ return false;
+ }
+
+ if (!amount || Number(amount) <= 0) {
+ setAmountError("Amount must be greater than 0");
+ return false;
+ }
+
+ if (selectedDenom.value === customDenomOption.value && !Number.isInteger(Number(amount))) {
+ setAmountError("Amount cannot be decimal for custom denom");
+ return false;
+ }
+
+ return true;
+ };
+
+ const denom =
+ selectedDenom.value === customDenomOption.value ? customDenom : selectedDenom.value.symbol;
+
+ const amountInAtomics = (() => {
+ try {
+ if (selectedDenom.value === customDenomOption.value) {
+ return Decimal.fromUserInput(amount, 0).atomics;
}
- if (!amount || Number(amount) <= 0) {
- setAmountError("Amount must be greater than 0");
- return false;
- }
+ const foundAsset = state.chain.assets?.find((asset) => asset.symbol === denom);
+ const exponent =
+ foundAsset?.denom_units.find((unit) => unit.denom === foundAsset.symbol.toLowerCase())
+ ?.exponent ?? 0;
- return true;
- };
+ return Decimal.fromUserInput(amount, exponent).atomics;
+ } catch {
+ return "0";
+ }
+ })();
- const amountInAtomics = amount
- ? Decimal.fromUserInput(amount, Number(state.chain.displayDenomExponent)).atomics
- : "0";
+ const msgValue = MsgCodecs[MsgTypeUrls.Send].fromPartial({
+ fromAddress,
+ toAddress,
+ amount: [{ denom, amount: amountInAtomics }],
+ });
- const msgValue = MsgCodecs[MsgTypeUrls.Send].fromPartial({
- fromAddress,
- toAddress,
- amount: [{ amount: amountInAtomics, denom: state.chain.denom }],
- });
+ const msg: MsgSendEncodeObject = { typeUrl: MsgTypeUrls.Send, value: msgValue };
- const msg: MsgSendEncodeObject = { typeUrl: MsgTypeUrls.Send, value: msgValue };
-
- setMsgGetter({ isMsgValid, msg });
- } catch {}
+ setMsgGetter({ isMsgValid, msg });
}, [
amount,
+ customDenom,
fromAddress,
+ selectedDenom.value,
setMsgGetter,
state.chain.addressPrefix,
+ state.chain.assets,
state.chain.chainId,
state.chain.denom,
- state.chain.displayDenomExponent,
toAddress,
]);
@@ -92,10 +134,40 @@ const MsgSendForm = ({ fromAddress, setMsgGetter, deleteMsg }: MsgSendFormProps)
placeholder={`E.g. ${exampleAddress(0, state.chain.addressPrefix)}`}
/>
+
+
+
+
+ setCustomDenom(target.value)}
+ placeholder={
+ selectedDenom.value === customDenomOption.value
+ ? "Enter custom denom"
+ : "Select Custom denom above"
+ }
+ disabled={selectedDenom.value !== customDenomOption.value}
+ error={customDenomError}
+ />
+
setAmount(target.value)}
@@ -106,6 +178,15 @@ const MsgSendForm = ({ fromAddress, setMsgGetter, deleteMsg }: MsgSendFormProps)
.form-item {
margin-top: 1.5em;
}
+ .form-item label {
+ font-style: italic;
+ font-size: 12px;
+ }
+ .form-select {
+ display: flex;
+ flex-direction: column;
+ gap: 0.8em;
+ }
button.remove {
background: rgba(255, 255, 255, 0.2);
width: 30px;
diff --git a/context/AppContext.tsx b/context/AppContext.tsx
index f58e2ca..689b5ab 100644
--- a/context/AppContext.tsx
+++ b/context/AppContext.tsx
@@ -1,7 +1,6 @@
-import React, { useEffect, createContext, useContext, useReducer } from "react";
-
-import { AppReducer, ChangeChainAction, initialState } from "./AppReducer";
+import { createContext, useContext, useEffect, useReducer } from "react";
import { ChainInfo } from "../types";
+import { AppReducer, ChangeChainAction, initialState } from "./AppReducer";
export interface AppContextType {
chain: ChainInfo;
@@ -23,6 +22,7 @@ function getChainInfoFromUrl(): ChainInfo {
decodeURIComponent(params.get("displayDenomExponent") || ""),
10,
),
+ assets: JSON.parse(decodeURIComponent(params.get("assets") || "{}")),
gasPrice: decodeURIComponent(params.get("gasPrice") || ""),
chainId: decodeURIComponent(params.get("chainId") || ""),
chainDisplayName: decodeURIComponent(params.get("chainDisplayName") || ""),
@@ -37,11 +37,13 @@ function getChainInfoFromUrl(): ChainInfo {
function setChainInfoParams(chainInfo: ChainInfo) {
const params = new URLSearchParams();
- const keys = Object.keys(chainInfo) as Array;
-
- keys.forEach((value: keyof ChainInfo) => {
- params.set(value, encodeURIComponent(chainInfo[value] || ""));
- });
+ for (const [key, value] of Object.entries(chainInfo)) {
+ if (Array.isArray(value)) {
+ params.set(key, encodeURIComponent(JSON.stringify(value)));
+ } else {
+ params.set(key, encodeURIComponent(value ?? ""));
+ }
+ }
window.history.replaceState({}, "", `${location.pathname}?${params}`);
}
diff --git a/lib/displayHelpers.spec.ts b/lib/displayHelpers.spec.ts
index 385c1b0..9f04922 100644
--- a/lib/displayHelpers.spec.ts
+++ b/lib/displayHelpers.spec.ts
@@ -12,13 +12,37 @@ const testChainInfo: ChainInfo = {
denom: "ujunox",
displayDenom: "JUNOX",
displayDenomExponent: 6,
+ assets: [
+ {
+ description: "The native token of JUNO Chain",
+ denom_units: [
+ {
+ denom: "ujunox",
+ exponent: 0,
+ },
+ {
+ denom: "junox",
+ exponent: 6,
+ },
+ ],
+ base: "ujunox",
+ name: "Juno Testnet",
+ display: "junox",
+ symbol: "JUNOX",
+ logo_URIs: {
+ png: "https://raw.githubusercontent.com/cosmos/chain-registry/master/testnets/junotestnet/images/juno.png",
+ svg: "https://raw.githubusercontent.com/cosmos/chain-registry/master/testnets/junotestnet/images/juno.svg",
+ },
+ coingecko_id: "juno-network",
+ },
+ ],
gasPrice: "0.04ujunox",
};
const emptyCoin: Coin = { amount: "", denom: "" };
const coinInChain: Coin = { amount: "1000", denom: "ujunox" };
const coinNotInChainLeading: Coin = { amount: "20000", denom: "utest" };
-const coinNotInChainNotLeading: Coin = { amount: "300000", denom: "test" };
+const coinNotInChainNotLeading: Coin = { amount: "300000", denom: "TEST" };
const ibcCoin: Coin = {
amount: "4000000",
denom: "ibc/c4cff46fd6de35ca4cf4ce031e643c8fdc9ba4b99ae598e9b0ed98fe3a2319f9",
@@ -34,17 +58,17 @@ describe("printableCoin", () => {
});
it("works with coin not in ChainInfo with leading 'u'", () => {
- expect(printableCoin(coinNotInChainLeading, testChainInfo)).toEqual(`0.02${thinSpace}TEST`);
+ expect(printableCoin(coinNotInChainLeading, testChainInfo)).toEqual(`20000${thinSpace}UTEST`);
});
it("works with coin not in ChainInfo without leading 'u'", () => {
expect(printableCoin(coinNotInChainNotLeading, testChainInfo)).toEqual(
- `300000${thinSpace}test`,
+ `300000${thinSpace}TEST`,
);
});
it("works with IBC coin", () => {
- expect(printableCoin(ibcCoin, testChainInfo)).toEqual(`4000000${thinSpace}ibc/c4cff…319f9`);
+ expect(printableCoin(ibcCoin, testChainInfo)).toEqual(`4000000${thinSpace}ibc/C4CFF…319F9`);
});
});
@@ -67,7 +91,7 @@ describe("printableCoins", () => {
];
expect(printableCoins(coins, testChainInfo)).toEqual(
- `0.001${thinSpace}JUNOX, 0.02${thinSpace}TEST, 300000${thinSpace}test, 4000000 ibc/c4cff…319f9`,
+ `0.001${thinSpace}JUNOX, 20000${thinSpace}UTEST, 300000${thinSpace}TEST, 4000000 ibc/C4CFF…319F9`,
);
});
});
diff --git a/lib/displayHelpers.ts b/lib/displayHelpers.ts
index 34466b0..100bf61 100644
--- a/lib/displayHelpers.ts
+++ b/lib/displayHelpers.ts
@@ -37,32 +37,30 @@ const thinSpace = "\u202F";
const printableCoin = (coin: Coin, chainInfo: ChainInfo) => {
if (!coin.amount || !coin.denom) return "";
- // The display denom from configuration
- if (coin.denom === chainInfo.denom) {
- const exponent = Number(chainInfo.displayDenomExponent);
- const value = Decimal.fromAtomics(coin.amount ?? "0", exponent).toString();
- const ticker = chainInfo.displayDenom;
- return value + thinSpace + ticker;
- }
-
- // Auto-convert leading "u"s
- if (coin.denom.startsWith("u")) {
- const value = Decimal.fromAtomics(coin.amount ?? "0", 6).toString();
- const ticker = coin.denom.slice(1).toUpperCase();
- return value + thinSpace + ticker;
- }
-
// Ellide IBC tokens
if (coin.denom.startsWith("ibc/")) {
const value = coin.amount;
const hash = coin.denom.slice(4);
const ellidedHash = ellideMiddle(hash, 11);
- const ticker = `ibc/${ellidedHash}`;
+ const ticker = `ibc/${ellidedHash.toUpperCase()}`;
+ return value + thinSpace + ticker;
+ }
+
+ const foundAsset = chainInfo.assets?.find(
+ (asset) => coin.denom === asset.symbol || coin.denom === asset.base,
+ );
+ const foundExponent = foundAsset?.denom_units.find(
+ (unit) => unit.denom === foundAsset.symbol.toLowerCase(),
+ )?.exponent;
+
+ if (foundExponent) {
+ const value = Decimal.fromAtomics(coin.amount, foundExponent).toString();
+ const ticker = foundAsset.symbol;
return value + thinSpace + ticker;
}
// Fallback to plain coin display
- return coin.amount + thinSpace + coin.denom;
+ return coin.amount + thinSpace + coin.denom.toUpperCase();
};
const printableCoins = (coins: readonly Coin[], chainInfo: ChainInfo) =>
diff --git a/types/index.ts b/types/index.ts
index 16c5879..b01fb4d 100644
--- a/types/index.ts
+++ b/types/index.ts
@@ -1,5 +1,6 @@
import { StdFee } from "@cosmjs/amino";
import { EncodeObject } from "@cosmjs/proto-signing";
+import { RegistryAsset } from "../components/chainSelect/chainregistry";
declare global {
interface Window {
@@ -51,6 +52,7 @@ export interface ChainInfo {
denom?: string;
displayDenom?: string;
displayDenomExponent?: number;
+ assets?: readonly RegistryAsset[];
gasPrice?: string;
chainId?: string;
chainDisplayName?: string;