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 {
|
||||
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(
|
|||
</div>
|
||||
<div>
|
||||
<Typography variant="body1">{option.token}</Typography>
|
||||
{option.balance ? (
|
||||
<Typography variant="h6">
|
||||
{formatTerraNativeBalance(option.balance)}
|
||||
</Typography>
|
||||
) : null}
|
||||
</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;
|
||||
token: string;
|
||||
icon: string;
|
||||
balance?: string; // populated by native tokens, could move to a type that extends this
|
||||
};
|
||||
|
||||
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 { 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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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<WormholeWrappedInfo> {
|
||||
if (isNativeDenom(wrappedAddress)) {
|
||||
return {
|
||||
isWrapped: false,
|
||||
chainId: CHAIN_ID_TERRA,
|
||||
assetAddress: buildNativeId(wrappedAddress),
|
||||
};
|
||||
}
|
||||
try {
|
||||
const result: {
|
||||
asset_address: string;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue