bridge_ui, sdk/js: support native terra

Change-Id: I1125030b0f09b29567c19ca9adec7866695e2262
This commit is contained in:
Evan Gray 2021-10-04 11:52:11 +00:00 committed by David Paryente
parent a9e98247bc
commit 77bf3620c6
9 changed files with 196 additions and 38 deletions

View File

@ -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>
);

View File

@ -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;
}

View File

@ -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 = {

View File

@ -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;

View File

@ -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;
}

View File

@ -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,
},
},

View File

@ -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;

View File

@ -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(

View File

@ -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;