diff --git a/bridge_ui/src/components/TokenSelectors/TerraSourceTokenSelector.tsx b/bridge_ui/src/components/TokenSelectors/TerraSourceTokenSelector.tsx
index 55ce4bbb..df1c845b 100644
--- a/bridge_ui/src/components/TokenSelectors/TerraSourceTokenSelector.tsx
+++ b/bridge_ui/src/components/TokenSelectors/TerraSourceTokenSelector.tsx
@@ -1,3 +1,4 @@
+import { isNativeDenom } from "@certusone/wormhole-sdk";
import {
CircularProgress,
createStyles,
@@ -14,12 +15,19 @@ import {
import { formatUnits } from "ethers/lib/utils";
import React, { useCallback, useMemo, useState } from "react";
import { createParsedTokenAccount } from "../../hooks/useGetSourceParsedTokenAccounts";
+import useTerraNativeBalances from "../../hooks/useTerraNativeBalances";
import useTerraTokenMap, {
TerraTokenMetadata,
} from "../../hooks/useTerraTokenMap";
import { ParsedTokenAccount } from "../../store/transferSlice";
import { TERRA_HOST } from "../../utils/consts";
import { shortenAddress } from "../../utils/solana";
+import {
+ formatNativeDenom,
+ formatTerraNativeBalance,
+ getNativeTerraIcon,
+ NATIVE_TERRA_DECIMALS,
+} from "../../utils/terra";
import OffsetButton from "./OffsetButton";
import RefreshButtonWrapper from "./RefreshButtonWrapper";
@@ -126,11 +134,25 @@ export default function TerraSourceTokenSelector(
const isLoading = tokenMap?.isFetching || false;
+ const { balances } = useTerraNativeBalances(terraWallet?.walletAddress);
+
const terraTokenArray = useMemo(() => {
+ const balancesItems = balances
+ ? Object.keys(balances).map(
+ (denom) =>
+ ({
+ protocol: "native",
+ symbol: formatNativeDenom(denom),
+ token: denom,
+ icon: getNativeTerraIcon(formatNativeDenom(denom)),
+ balance: balances[denom],
+ } as TerraTokenMetadata)
+ )
+ : [];
const values = tokenMap.data?.mainnet;
- const items = Object.values(values || {});
- return items || [];
- }, [tokenMap]);
+ const tokenMapItems = Object.values(values || {}) || [];
+ return [...balancesItems, ...tokenMapItems];
+ }, [balances, tokenMap]);
const valueToOption = (fromProps: ParsedTokenAccount | undefined | null) => {
if (!fromProps) return null;
@@ -154,15 +176,32 @@ export default function TerraSourceTokenSelector(
return;
}
setAdvancedModeError("");
- lookupTerraAddress(address, terraWallet).then(
- (result) => {
- onChange(result);
- },
- (error) => {
+ if (isNativeDenom(address)) {
+ if (balances && balances[address]) {
+ onChange(
+ createParsedTokenAccount(
+ terraWallet.walletAddress,
+ address,
+ balances[address],
+ NATIVE_TERRA_DECIMALS,
+ Number(formatUnits(balances[address], NATIVE_TERRA_DECIMALS)),
+ formatUnits(balances[address], NATIVE_TERRA_DECIMALS),
+ formatNativeDenom(address)
+ )
+ );
+ } else {
setAdvancedModeError("Unable to retrieve that address.");
}
- );
- setAdvancedModeError("");
+ } else {
+ lookupTerraAddress(address, terraWallet).then(
+ (result) => {
+ onChange(result);
+ },
+ (error) => {
+ setAdvancedModeError("Unable to retrieve that address.");
+ }
+ );
+ }
};
const filterConfig = createFilterOptions({
@@ -191,6 +230,11 @@ export default function TerraSourceTokenSelector(
{option.token}
+ {option.balance ? (
+
+ {formatTerraNativeBalance(option.balance)}
+
+ ) : null}
);
diff --git a/bridge_ui/src/hooks/useTerraNativeBalances.ts b/bridge_ui/src/hooks/useTerraNativeBalances.ts
new file mode 100644
index 00000000..70e1d878
--- /dev/null
+++ b/bridge_ui/src/hooks/useTerraNativeBalances.ts
@@ -0,0 +1,43 @@
+import { LCDClient } from "@terra-money/terra.js";
+import { useEffect, useMemo, useState } from "react";
+import { TERRA_HOST } from "../utils/consts";
+
+export interface TerraNativeBalances {
+ [index: string]: string;
+}
+
+export default function useTerraNativeBalances(walletAddress?: string) {
+ const [isLoading, setIsLoading] = useState(true);
+ const [balances, setBalances] = useState({});
+ useEffect(() => {
+ if (walletAddress) {
+ setIsLoading(true);
+ setBalances(undefined);
+ const lcd = new LCDClient(TERRA_HOST);
+ lcd.bank
+ .balance(walletAddress)
+ .then((coins) => {
+ // coins doesn't support reduce
+ const balancePairs = coins.map(({ amount, denom }) => [
+ denom,
+ amount,
+ ]);
+ const balance = balancePairs.reduce((obj, current) => {
+ obj[current[0].toString()] = current[1].toString();
+ return obj;
+ }, {} as TerraNativeBalances);
+ setIsLoading(false);
+ setBalances(balance);
+ })
+ .catch((e) => {
+ setIsLoading(false);
+ setBalances(undefined);
+ });
+ } else {
+ setIsLoading(false);
+ setBalances(undefined);
+ }
+ }, [walletAddress]);
+ const value = useMemo(() => ({ isLoading, balances }), [isLoading, balances]);
+ return value;
+}
diff --git a/bridge_ui/src/hooks/useTerraTokenMap.ts b/bridge_ui/src/hooks/useTerraTokenMap.ts
index fdf872d5..61233028 100644
--- a/bridge_ui/src/hooks/useTerraTokenMap.ts
+++ b/bridge_ui/src/hooks/useTerraTokenMap.ts
@@ -16,6 +16,7 @@ export type TerraTokenMetadata = {
symbol: string;
token: string;
icon: string;
+ balance?: string; // populated by native tokens, could move to a type that extends this
};
export type TerraTokenMap = {
diff --git a/bridge_ui/src/utils/terra.ts b/bridge_ui/src/utils/terra.ts
index effa1454..138d275f 100644
--- a/bridge_ui/src/utils/terra.ts
+++ b/bridge_ui/src/utils/terra.ts
@@ -1,7 +1,29 @@
+import { isNativeTerra } from "@certusone/wormhole-sdk";
+import { formatUnits } from "@ethersproject/units";
import { LCDClient } from "@terra-money/terra.js";
import { TxResult } from "@terra-money/wallet-provider";
+// import { TerraTokenMetadata } from "../hooks/useTerraTokenMap";
import { TERRA_HOST } from "./consts";
+export const NATIVE_TERRA_DECIMALS = 6;
+
+export const getNativeTerraIcon = (symbol = "") =>
+ `https://assets.terra.money/icon/60/${symbol}.png`;
+
+// inspired by https://github.com/terra-money/station/blob/dca7de43958ce075c6e46605622203b9859b0e14/src/lib/utils/format.ts#L38
+export const formatNativeDenom = (denom = ""): string => {
+ const unit = denom.slice(1).toUpperCase();
+ const isValidTerra = isNativeTerra(denom);
+ return denom === "uluna"
+ ? "Luna"
+ : isValidTerra
+ ? unit.slice(0, 2) + "T"
+ : "";
+};
+
+export const formatTerraNativeBalance = (balance = ""): string =>
+ formatUnits(balance, 6);
+
export async function waitForTerraExecution(transaction: TxResult) {
const lcd = new LCDClient(TERRA_HOST);
let info;
diff --git a/sdk/js/src/terra/address.ts b/sdk/js/src/terra/address.ts
index 03a0452c..e7730f7f 100644
--- a/sdk/js/src/terra/address.ts
+++ b/sdk/js/src/terra/address.ts
@@ -1,3 +1,4 @@
+import { zeroPad } from "@ethersproject/bytes";
import { bech32 } from "bech32";
export function canonicalAddress(humanAddress: string) {
@@ -6,3 +7,21 @@ export function canonicalAddress(humanAddress: string) {
export function humanAddress(canonicalAddress: Uint8Array) {
return bech32.encode("terra", bech32.toWords(canonicalAddress));
}
+
+// from https://github.com/terra-money/station/blob/dca7de43958ce075c6e46605622203b9859b0e14/src/lib/utils/is.ts#L12
+export const isNativeTerra = (string = "") =>
+ string.startsWith("u") && string.length === 4;
+
+// from https://github.com/terra-money/station/blob/dca7de43958ce075c6e46605622203b9859b0e14/src/lib/utils/is.ts#L20
+export const isNativeDenom = (string = "") =>
+ isNativeTerra(string) || string === "uluna";
+
+export function buildNativeId(denom: string): Uint8Array {
+ const bytes = [];
+ for (let i = 0; i < denom.length; i++) {
+ bytes.push(denom.charCodeAt(i));
+ }
+ const padded = zeroPad(new Uint8Array(bytes), 32);
+ padded[0] = 1;
+ return padded;
+}
diff --git a/sdk/js/src/token_bridge/attest.ts b/sdk/js/src/token_bridge/attest.ts
index b46cb65b..08c7227f 100644
--- a/sdk/js/src/token_bridge/attest.ts
+++ b/sdk/js/src/token_bridge/attest.ts
@@ -5,6 +5,7 @@ import { getBridgeFeeIx, ixFromRust } from "../solana";
import { createNonce } from "../utils/createNonce";
import { ConnectedWallet as TerraConnectedWallet } from "@terra-money/wallet-provider";
import { MsgExecuteContract } from "@terra-money/terra.js";
+import { isNativeDenom } from "..";
export async function attestFromEth(
tokenBridgeAddress: string,
@@ -20,9 +21,10 @@ export async function attestFromEth(
export async function attestFromTerra(
tokenBridgeAddress: string,
wallet: TerraConnectedWallet,
- asset: string,
+ asset: string
) {
const nonce = Math.round(Math.random() * 100000);
+ const isNativeAsset = isNativeDenom(asset);
return await wallet.post({
msgs: [
new MsgExecuteContract(
@@ -30,7 +32,15 @@ export async function attestFromTerra(
tokenBridgeAddress,
{
create_asset_meta: {
- asset_address: asset,
+ asset_info: isNativeAsset
+ ? {
+ native_token: { denom: asset },
+ }
+ : {
+ token: {
+ contract_addr: asset,
+ },
+ },
nonce: nonce,
},
},
diff --git a/sdk/js/src/token_bridge/getOriginalAsset.ts b/sdk/js/src/token_bridge/getOriginalAsset.ts
index bde4d019..318fdc3a 100644
--- a/sdk/js/src/token_bridge/getOriginalAsset.ts
+++ b/sdk/js/src/token_bridge/getOriginalAsset.ts
@@ -10,7 +10,7 @@ import {
} from "../utils";
import { getIsWrappedAssetEth } from "./getIsWrappedAsset";
import { LCDClient } from "@terra-money/terra.js";
-import { canonicalAddress } from "../terra";
+import { buildNativeId, canonicalAddress, isNativeDenom } from "../terra";
export interface WormholeWrappedInfo {
isWrapped: boolean;
@@ -59,6 +59,13 @@ export async function getOriginalAssetTerra(
client: LCDClient,
wrappedAddress: string
): Promise {
+ if (isNativeDenom(wrappedAddress)) {
+ return {
+ isWrapped: false,
+ chainId: CHAIN_ID_TERRA,
+ assetAddress: buildNativeId(wrappedAddress),
+ };
+ }
try {
const result: {
asset_address: string;
diff --git a/sdk/js/src/token_bridge/transfer.ts b/sdk/js/src/token_bridge/transfer.ts
index fab5640a..ba7b0cae 100644
--- a/sdk/js/src/token_bridge/transfer.ts
+++ b/sdk/js/src/token_bridge/transfer.ts
@@ -7,7 +7,8 @@ import {
Transaction,
} from "@solana/web3.js";
import { MsgExecuteContract } from "@terra-money/terra.js";
-import { ethers } from "ethers";
+import { BigNumber, ethers } from "ethers";
+import { isNativeDenom } from "..";
import {
Bridge__factory,
TokenImplementation__factory,
@@ -90,24 +91,10 @@ export async function transferFromTerra(
recipientAddress: Uint8Array
) {
const nonce = Math.round(Math.random() * 100000);
- const isNativeAsset = ["uluna"].includes(tokenAddress);
- return [
- new MsgExecuteContract(
- walletAddress,
- tokenAddress,
- {
- increase_allowance: {
- spender: tokenBridgeAddress,
- amount: amount,
- expires: {
- never: {},
- },
- },
- },
- { uluna: 10000 }
- ),
- isNativeAsset
- ? new MsgExecuteContract(
+ const isNativeAsset = isNativeDenom(tokenAddress);
+ return isNativeAsset
+ ? [
+ new MsgExecuteContract(
walletAddress,
tokenBridgeAddress,
{
@@ -126,9 +113,29 @@ export async function transferFromTerra(
nonce: nonce,
},
},
- { uluna: 10000, [tokenAddress]: amount }
- )
- : new MsgExecuteContract(
+ {
+ uluna: BigNumber.from("10000")
+ .add(BigNumber.from(amount))
+ .toString(),
+ }
+ ),
+ ]
+ : [
+ new MsgExecuteContract(
+ walletAddress,
+ tokenAddress,
+ {
+ increase_allowance: {
+ spender: tokenBridgeAddress,
+ amount: amount,
+ expires: {
+ never: {},
+ },
+ },
+ },
+ { uluna: 10000 }
+ ),
+ new MsgExecuteContract(
walletAddress,
tokenBridgeAddress,
{
@@ -149,7 +156,7 @@ export async function transferFromTerra(
},
{ uluna: 10000 }
),
- ];
+ ];
}
export async function transferNativeSol(
diff --git a/sdk/js/src/utils/array.ts b/sdk/js/src/utils/array.ts
index c5bbf596..3fb3132c 100644
--- a/sdk/js/src/utils/array.ts
+++ b/sdk/js/src/utils/array.ts
@@ -6,8 +6,11 @@ import {
} from "./consts";
import { humanAddress } from "../terra";
import { PublicKey } from "@solana/web3.js";
-import { hexValue, hexZeroPad } from "ethers/lib/utils";
+import { hexValue, hexZeroPad, stripZeros } from "ethers/lib/utils";
+export const isHexNativeTerra = (h: string) => h.startsWith("01");
+export const nativeTerraHexToDenom = (h: string) =>
+ Buffer.from(stripZeros(hexToUint8Array(h.substr(2)))).toString("ascii");
export const uint8ArrayToHex = (a: Uint8Array) =>
Buffer.from(a).toString("hex");
export const hexToUint8Array = (h: string) =>
@@ -21,7 +24,9 @@ export const hexToNativeString = (h: string | undefined, c: ChainId) => {
: c === CHAIN_ID_ETH
? hexZeroPad(hexValue(hexToUint8Array(h)), 20)
: c === CHAIN_ID_TERRA
- ? humanAddress(hexToUint8Array(h.substr(24))) // terra expects 20 bytes, not 32
+ ? isHexNativeTerra(h)
+ ? nativeTerraHexToDenom(h)
+ : humanAddress(hexToUint8Array(h.substr(24))) // terra expects 20 bytes, not 32
: h;
} catch (e) {}
return undefined;