bridge_ui, sdk/js: support native terra
Change-Id: I1125030b0f09b29567c19ca9adec7866695e2262
This commit is contained in:
parent
a9e98247bc
commit
77bf3620c6
|
@ -1,3 +1,4 @@
|
||||||
|
import { isNativeDenom } from "@certusone/wormhole-sdk";
|
||||||
import {
|
import {
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
createStyles,
|
createStyles,
|
||||||
|
@ -14,12 +15,19 @@ import {
|
||||||
import { formatUnits } from "ethers/lib/utils";
|
import { formatUnits } from "ethers/lib/utils";
|
||||||
import React, { useCallback, useMemo, useState } from "react";
|
import React, { useCallback, useMemo, useState } from "react";
|
||||||
import { createParsedTokenAccount } from "../../hooks/useGetSourceParsedTokenAccounts";
|
import { createParsedTokenAccount } from "../../hooks/useGetSourceParsedTokenAccounts";
|
||||||
|
import useTerraNativeBalances from "../../hooks/useTerraNativeBalances";
|
||||||
import useTerraTokenMap, {
|
import useTerraTokenMap, {
|
||||||
TerraTokenMetadata,
|
TerraTokenMetadata,
|
||||||
} from "../../hooks/useTerraTokenMap";
|
} from "../../hooks/useTerraTokenMap";
|
||||||
import { ParsedTokenAccount } from "../../store/transferSlice";
|
import { ParsedTokenAccount } from "../../store/transferSlice";
|
||||||
import { TERRA_HOST } from "../../utils/consts";
|
import { TERRA_HOST } from "../../utils/consts";
|
||||||
import { shortenAddress } from "../../utils/solana";
|
import { shortenAddress } from "../../utils/solana";
|
||||||
|
import {
|
||||||
|
formatNativeDenom,
|
||||||
|
formatTerraNativeBalance,
|
||||||
|
getNativeTerraIcon,
|
||||||
|
NATIVE_TERRA_DECIMALS,
|
||||||
|
} from "../../utils/terra";
|
||||||
import OffsetButton from "./OffsetButton";
|
import OffsetButton from "./OffsetButton";
|
||||||
import RefreshButtonWrapper from "./RefreshButtonWrapper";
|
import RefreshButtonWrapper from "./RefreshButtonWrapper";
|
||||||
|
|
||||||
|
@ -126,11 +134,25 @@ export default function TerraSourceTokenSelector(
|
||||||
|
|
||||||
const isLoading = tokenMap?.isFetching || false;
|
const isLoading = tokenMap?.isFetching || false;
|
||||||
|
|
||||||
|
const { balances } = useTerraNativeBalances(terraWallet?.walletAddress);
|
||||||
|
|
||||||
const terraTokenArray = useMemo(() => {
|
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 values = tokenMap.data?.mainnet;
|
||||||
const items = Object.values(values || {});
|
const tokenMapItems = Object.values(values || {}) || [];
|
||||||
return items || [];
|
return [...balancesItems, ...tokenMapItems];
|
||||||
}, [tokenMap]);
|
}, [balances, tokenMap]);
|
||||||
|
|
||||||
const valueToOption = (fromProps: ParsedTokenAccount | undefined | null) => {
|
const valueToOption = (fromProps: ParsedTokenAccount | undefined | null) => {
|
||||||
if (!fromProps) return null;
|
if (!fromProps) return null;
|
||||||
|
@ -154,6 +176,23 @@ export default function TerraSourceTokenSelector(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setAdvancedModeError("");
|
setAdvancedModeError("");
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
lookupTerraAddress(address, terraWallet).then(
|
lookupTerraAddress(address, terraWallet).then(
|
||||||
(result) => {
|
(result) => {
|
||||||
onChange(result);
|
onChange(result);
|
||||||
|
@ -162,7 +201,7 @@ export default function TerraSourceTokenSelector(
|
||||||
setAdvancedModeError("Unable to retrieve that address.");
|
setAdvancedModeError("Unable to retrieve that address.");
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
setAdvancedModeError("");
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterConfig = createFilterOptions({
|
const filterConfig = createFilterOptions({
|
||||||
|
@ -191,6 +230,11 @@ export default function TerraSourceTokenSelector(
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Typography variant="body1">{option.token}</Typography>
|
<Typography variant="body1">{option.token}</Typography>
|
||||||
|
{option.balance ? (
|
||||||
|
<Typography variant="h6">
|
||||||
|
{formatTerraNativeBalance(option.balance)}
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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<TerraNativeBalances | undefined>({});
|
||||||
|
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;
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ export type TerraTokenMetadata = {
|
||||||
symbol: string;
|
symbol: string;
|
||||||
token: string;
|
token: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
|
balance?: string; // populated by native tokens, could move to a type that extends this
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TerraTokenMap = {
|
export type TerraTokenMap = {
|
||||||
|
|
|
@ -1,7 +1,29 @@
|
||||||
|
import { isNativeTerra } from "@certusone/wormhole-sdk";
|
||||||
|
import { formatUnits } from "@ethersproject/units";
|
||||||
import { LCDClient } from "@terra-money/terra.js";
|
import { LCDClient } from "@terra-money/terra.js";
|
||||||
import { TxResult } from "@terra-money/wallet-provider";
|
import { TxResult } from "@terra-money/wallet-provider";
|
||||||
|
// import { TerraTokenMetadata } from "../hooks/useTerraTokenMap";
|
||||||
import { TERRA_HOST } from "./consts";
|
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) {
|
export async function waitForTerraExecution(transaction: TxResult) {
|
||||||
const lcd = new LCDClient(TERRA_HOST);
|
const lcd = new LCDClient(TERRA_HOST);
|
||||||
let info;
|
let info;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { zeroPad } from "@ethersproject/bytes";
|
||||||
import { bech32 } from "bech32";
|
import { bech32 } from "bech32";
|
||||||
|
|
||||||
export function canonicalAddress(humanAddress: string) {
|
export function canonicalAddress(humanAddress: string) {
|
||||||
|
@ -6,3 +7,21 @@ export function canonicalAddress(humanAddress: string) {
|
||||||
export function humanAddress(canonicalAddress: Uint8Array) {
|
export function humanAddress(canonicalAddress: Uint8Array) {
|
||||||
return bech32.encode("terra", bech32.toWords(canonicalAddress));
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { getBridgeFeeIx, ixFromRust } from "../solana";
|
||||||
import { createNonce } from "../utils/createNonce";
|
import { createNonce } from "../utils/createNonce";
|
||||||
import { ConnectedWallet as TerraConnectedWallet } from "@terra-money/wallet-provider";
|
import { ConnectedWallet as TerraConnectedWallet } from "@terra-money/wallet-provider";
|
||||||
import { MsgExecuteContract } from "@terra-money/terra.js";
|
import { MsgExecuteContract } from "@terra-money/terra.js";
|
||||||
|
import { isNativeDenom } from "..";
|
||||||
|
|
||||||
export async function attestFromEth(
|
export async function attestFromEth(
|
||||||
tokenBridgeAddress: string,
|
tokenBridgeAddress: string,
|
||||||
|
@ -20,9 +21,10 @@ export async function attestFromEth(
|
||||||
export async function attestFromTerra(
|
export async function attestFromTerra(
|
||||||
tokenBridgeAddress: string,
|
tokenBridgeAddress: string,
|
||||||
wallet: TerraConnectedWallet,
|
wallet: TerraConnectedWallet,
|
||||||
asset: string,
|
asset: string
|
||||||
) {
|
) {
|
||||||
const nonce = Math.round(Math.random() * 100000);
|
const nonce = Math.round(Math.random() * 100000);
|
||||||
|
const isNativeAsset = isNativeDenom(asset);
|
||||||
return await wallet.post({
|
return await wallet.post({
|
||||||
msgs: [
|
msgs: [
|
||||||
new MsgExecuteContract(
|
new MsgExecuteContract(
|
||||||
|
@ -30,7 +32,15 @@ export async function attestFromTerra(
|
||||||
tokenBridgeAddress,
|
tokenBridgeAddress,
|
||||||
{
|
{
|
||||||
create_asset_meta: {
|
create_asset_meta: {
|
||||||
asset_address: asset,
|
asset_info: isNativeAsset
|
||||||
|
? {
|
||||||
|
native_token: { denom: asset },
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
token: {
|
||||||
|
contract_addr: asset,
|
||||||
|
},
|
||||||
|
},
|
||||||
nonce: nonce,
|
nonce: nonce,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import { getIsWrappedAssetEth } from "./getIsWrappedAsset";
|
import { getIsWrappedAssetEth } from "./getIsWrappedAsset";
|
||||||
import { LCDClient } from "@terra-money/terra.js";
|
import { LCDClient } from "@terra-money/terra.js";
|
||||||
import { canonicalAddress } from "../terra";
|
import { buildNativeId, canonicalAddress, isNativeDenom } from "../terra";
|
||||||
|
|
||||||
export interface WormholeWrappedInfo {
|
export interface WormholeWrappedInfo {
|
||||||
isWrapped: boolean;
|
isWrapped: boolean;
|
||||||
|
@ -59,6 +59,13 @@ export async function getOriginalAssetTerra(
|
||||||
client: LCDClient,
|
client: LCDClient,
|
||||||
wrappedAddress: string
|
wrappedAddress: string
|
||||||
): Promise<WormholeWrappedInfo> {
|
): Promise<WormholeWrappedInfo> {
|
||||||
|
if (isNativeDenom(wrappedAddress)) {
|
||||||
|
return {
|
||||||
|
isWrapped: false,
|
||||||
|
chainId: CHAIN_ID_TERRA,
|
||||||
|
assetAddress: buildNativeId(wrappedAddress),
|
||||||
|
};
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const result: {
|
const result: {
|
||||||
asset_address: string;
|
asset_address: string;
|
||||||
|
|
|
@ -7,7 +7,8 @@ import {
|
||||||
Transaction,
|
Transaction,
|
||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import { MsgExecuteContract } from "@terra-money/terra.js";
|
import { MsgExecuteContract } from "@terra-money/terra.js";
|
||||||
import { ethers } from "ethers";
|
import { BigNumber, ethers } from "ethers";
|
||||||
|
import { isNativeDenom } from "..";
|
||||||
import {
|
import {
|
||||||
Bridge__factory,
|
Bridge__factory,
|
||||||
TokenImplementation__factory,
|
TokenImplementation__factory,
|
||||||
|
@ -90,24 +91,10 @@ export async function transferFromTerra(
|
||||||
recipientAddress: Uint8Array
|
recipientAddress: Uint8Array
|
||||||
) {
|
) {
|
||||||
const nonce = Math.round(Math.random() * 100000);
|
const nonce = Math.round(Math.random() * 100000);
|
||||||
const isNativeAsset = ["uluna"].includes(tokenAddress);
|
const isNativeAsset = isNativeDenom(tokenAddress);
|
||||||
return [
|
return isNativeAsset
|
||||||
|
? [
|
||||||
new MsgExecuteContract(
|
new MsgExecuteContract(
|
||||||
walletAddress,
|
|
||||||
tokenAddress,
|
|
||||||
{
|
|
||||||
increase_allowance: {
|
|
||||||
spender: tokenBridgeAddress,
|
|
||||||
amount: amount,
|
|
||||||
expires: {
|
|
||||||
never: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ uluna: 10000 }
|
|
||||||
),
|
|
||||||
isNativeAsset
|
|
||||||
? new MsgExecuteContract(
|
|
||||||
walletAddress,
|
walletAddress,
|
||||||
tokenBridgeAddress,
|
tokenBridgeAddress,
|
||||||
{
|
{
|
||||||
|
@ -126,9 +113,29 @@ export async function transferFromTerra(
|
||||||
nonce: nonce,
|
nonce: nonce,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ uluna: 10000, [tokenAddress]: amount }
|
{
|
||||||
)
|
uluna: BigNumber.from("10000")
|
||||||
: new MsgExecuteContract(
|
.add(BigNumber.from(amount))
|
||||||
|
.toString(),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
new MsgExecuteContract(
|
||||||
|
walletAddress,
|
||||||
|
tokenAddress,
|
||||||
|
{
|
||||||
|
increase_allowance: {
|
||||||
|
spender: tokenBridgeAddress,
|
||||||
|
amount: amount,
|
||||||
|
expires: {
|
||||||
|
never: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ uluna: 10000 }
|
||||||
|
),
|
||||||
|
new MsgExecuteContract(
|
||||||
walletAddress,
|
walletAddress,
|
||||||
tokenBridgeAddress,
|
tokenBridgeAddress,
|
||||||
{
|
{
|
||||||
|
|
|
@ -6,8 +6,11 @@ import {
|
||||||
} from "./consts";
|
} from "./consts";
|
||||||
import { humanAddress } from "../terra";
|
import { humanAddress } from "../terra";
|
||||||
import { PublicKey } from "@solana/web3.js";
|
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) =>
|
export const uint8ArrayToHex = (a: Uint8Array) =>
|
||||||
Buffer.from(a).toString("hex");
|
Buffer.from(a).toString("hex");
|
||||||
export const hexToUint8Array = (h: string) =>
|
export const hexToUint8Array = (h: string) =>
|
||||||
|
@ -21,7 +24,9 @@ export const hexToNativeString = (h: string | undefined, c: ChainId) => {
|
||||||
: c === CHAIN_ID_ETH
|
: c === CHAIN_ID_ETH
|
||||||
? hexZeroPad(hexValue(hexToUint8Array(h)), 20)
|
? hexZeroPad(hexValue(hexToUint8Array(h)), 20)
|
||||||
: c === CHAIN_ID_TERRA
|
: 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;
|
: h;
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|
Loading…
Reference in New Issue