380 lines
12 KiB
TypeScript
380 lines
12 KiB
TypeScript
import { arrayify, zeroPad } from "@ethersproject/bytes";
|
|
import { PublicKey } from "@solana/web3.js";
|
|
import { hexValue, hexZeroPad, sha256, stripZeros } from "ethers/lib/utils";
|
|
import { Provider as NearProvider } from "near-api-js/lib/providers";
|
|
import { ethers } from "ethers";
|
|
import {
|
|
hexToNativeAssetStringAlgorand,
|
|
nativeStringToHexAlgorand,
|
|
uint8ArrayToNativeStringAlgorand,
|
|
} from "../algorand";
|
|
import { canonicalAddress, humanAddress } from "../cosmos";
|
|
import { buildTokenId } from "../cosmwasm/address";
|
|
import { isNativeDenom } from "../terra";
|
|
import {
|
|
ChainId,
|
|
ChainName,
|
|
CHAIN_ID_ALGORAND,
|
|
CHAIN_ID_APTOS,
|
|
CHAIN_ID_INJECTIVE,
|
|
CHAIN_ID_NEAR,
|
|
CHAIN_ID_OSMOSIS,
|
|
CHAIN_ID_PYTHNET,
|
|
CHAIN_ID_SOLANA,
|
|
CHAIN_ID_SUI,
|
|
CHAIN_ID_TERRA,
|
|
CHAIN_ID_TERRA2,
|
|
CHAIN_ID_WORMCHAIN,
|
|
CHAIN_ID_UNSET,
|
|
coalesceChainId,
|
|
isEVMChain,
|
|
isTerraChain,
|
|
CHAIN_ID_XPLA,
|
|
CHAIN_ID_SEI,
|
|
CHAIN_ID_BTC,
|
|
CHAIN_ID_COSMOSHUB,
|
|
CHAIN_ID_EVMOS,
|
|
CHAIN_ID_KUJIRA,
|
|
CHAIN_ID_NEUTRON,
|
|
CHAIN_ID_CELESTIA,
|
|
} from "./consts";
|
|
import { hashLookup } from "./near";
|
|
import { getExternalAddressFromType, isValidAptosType } from "./aptos";
|
|
import { isValidSuiAddress } from "@mysten/sui.js";
|
|
import { isValidSuiType } from "../sui";
|
|
|
|
/**
|
|
*
|
|
* Returns true iff the hex string represents a native Terra denom.
|
|
*
|
|
* Native assets on terra don't have an associated smart contract address, just
|
|
* like eth isn't an ERC-20 contract on Ethereum.
|
|
*
|
|
* The difference is that the EVM implementations of Portal don't support eth
|
|
* directly, and instead require swapping to an ERC-20 wrapped eth (WETH)
|
|
* contract first.
|
|
*
|
|
* The Terra implementation instead supports Terra-native denoms without
|
|
* wrapping to CW-20 token first. As these denoms don't have an address, they
|
|
* are encoded in the Portal payloads by the setting the first byte to 1. This
|
|
* encoding is safe, because the first 12 bytes of the 32-byte wormhole address
|
|
* space are not used on Terra otherwise, as cosmos addresses are 20 bytes wide.
|
|
*/
|
|
export const isHexNativeTerra = (h: string): boolean => h.startsWith("01");
|
|
|
|
const isLikely20ByteCosmwasm = (h: string): boolean =>
|
|
h.startsWith("000000000000000000000000");
|
|
|
|
export const nativeTerraHexToDenom = (h: string): string =>
|
|
Buffer.from(stripZeros(hexToUint8Array(h.substr(2)))).toString("ascii");
|
|
|
|
export const uint8ArrayToHex = (a: Uint8Array): string =>
|
|
Buffer.from(a).toString("hex");
|
|
|
|
export const hexToUint8Array = (h: string): Uint8Array => {
|
|
if (h.startsWith("0x")) h = h.slice(2);
|
|
return new Uint8Array(Buffer.from(h, "hex"));
|
|
};
|
|
|
|
/**
|
|
*
|
|
* Convert an address in a wormhole's 32-byte array representation into a chain's
|
|
* native string representation.
|
|
*
|
|
* @throws if address is not the right length for the given chain
|
|
*/
|
|
|
|
export const tryUint8ArrayToNative = (
|
|
a: Uint8Array,
|
|
chain: ChainId | ChainName
|
|
): string => {
|
|
const chainId = coalesceChainId(chain);
|
|
if (isEVMChain(chainId)) {
|
|
return hexZeroPad(hexValue(a), 20);
|
|
} else if (chainId === CHAIN_ID_SOLANA || chainId === CHAIN_ID_PYTHNET) {
|
|
return new PublicKey(a).toString();
|
|
} else if (isTerraChain(chainId)) {
|
|
const h = uint8ArrayToHex(a);
|
|
if (isHexNativeTerra(h)) {
|
|
return nativeTerraHexToDenom(h);
|
|
} else {
|
|
if (chainId === CHAIN_ID_TERRA2 && !isLikely20ByteCosmwasm(h)) {
|
|
// terra 2 has 32 byte addresses for contracts and 20 for wallets
|
|
return humanAddress("terra", a);
|
|
}
|
|
return humanAddress("terra", a.slice(-20));
|
|
}
|
|
} else if (chainId === CHAIN_ID_INJECTIVE) {
|
|
const h = uint8ArrayToHex(a);
|
|
return humanAddress("inj", isLikely20ByteCosmwasm(h) ? a.slice(-20) : a);
|
|
} else if (chainId === CHAIN_ID_ALGORAND) {
|
|
return uint8ArrayToNativeStringAlgorand(a);
|
|
} else if (chainId == CHAIN_ID_WORMCHAIN) {
|
|
const h = uint8ArrayToHex(a);
|
|
return humanAddress(
|
|
"wormhole",
|
|
isLikely20ByteCosmwasm(h) ? a.slice(-20) : a
|
|
);
|
|
} else if (chainId === CHAIN_ID_XPLA) {
|
|
const h = uint8ArrayToHex(a);
|
|
return humanAddress("xpla", isLikely20ByteCosmwasm(h) ? a.slice(-20) : a);
|
|
} else if (chainId === CHAIN_ID_SEI) {
|
|
const h = uint8ArrayToHex(a);
|
|
return humanAddress("sei", isLikely20ByteCosmwasm(h) ? a.slice(-20) : a);
|
|
} else if (chainId === CHAIN_ID_NEAR) {
|
|
throw Error("uint8ArrayToNative: Use tryHexToNativeStringNear instead.");
|
|
} else if (chainId === CHAIN_ID_OSMOSIS) {
|
|
throw Error("uint8ArrayToNative: Osmosis not supported yet.");
|
|
} else if (chainId === CHAIN_ID_COSMOSHUB) {
|
|
throw Error("uint8ArrayToNative: CosmosHub not supported yet.");
|
|
} else if (chainId === CHAIN_ID_EVMOS) {
|
|
throw Error("uint8ArrayToNative: Evmos not supported yet.");
|
|
} else if (chainId === CHAIN_ID_KUJIRA) {
|
|
throw Error("uint8ArrayToNative: Kujira not supported yet.");
|
|
} else if (chainId === CHAIN_ID_NEUTRON) {
|
|
throw Error("uint8ArrayToNative: Neutron not supported yet.");
|
|
} else if (chainId === CHAIN_ID_CELESTIA) {
|
|
throw Error("uint8ArrayToNative: Celestia not supported yet.");
|
|
} else if (chainId === CHAIN_ID_SUI) {
|
|
throw Error("uint8ArrayToNative: Sui not supported yet.");
|
|
} else if (chainId === CHAIN_ID_APTOS) {
|
|
throw Error("uint8ArrayToNative: Aptos not supported yet.");
|
|
} else if (chainId === CHAIN_ID_UNSET) {
|
|
throw Error("uint8ArrayToNative: Chain id unset");
|
|
} else if (chainId === CHAIN_ID_BTC) {
|
|
throw Error("uint8ArrayToNative: Btc not supported");
|
|
} else {
|
|
// This case is never reached
|
|
const _: never = chainId;
|
|
throw Error("Don't know how to convert address for chain " + chainId);
|
|
}
|
|
};
|
|
|
|
export const tryHexToNativeStringNear = async (
|
|
provider: NearProvider,
|
|
tokenBridge: string,
|
|
address: string
|
|
): Promise<string> => {
|
|
const { found, value } = await hashLookup(provider, tokenBridge, address);
|
|
if (!found) {
|
|
throw new Error("Address not found");
|
|
}
|
|
return value;
|
|
};
|
|
|
|
/**
|
|
*
|
|
* Convert an address in a wormhole's 32-byte hex representation into a chain's native
|
|
* string representation.
|
|
*
|
|
* @throws if address is not the right length for the given chain
|
|
*/
|
|
export const tryHexToNativeAssetString = (h: string, c: ChainId): string =>
|
|
c === CHAIN_ID_ALGORAND
|
|
? // Algorand assets are represented by their asset ids, not an address
|
|
hexToNativeAssetStringAlgorand(h)
|
|
: tryHexToNativeString(h, c);
|
|
|
|
/**
|
|
*
|
|
* Convert an address in a wormhole's 32-byte hex representation into a chain's native
|
|
* string representation.
|
|
*
|
|
* @deprecated since 0.3.0, use [[tryHexToNativeString]] instead.
|
|
*/
|
|
export const hexToNativeAssetString = (
|
|
h: string | undefined,
|
|
c: ChainId
|
|
): string | undefined => {
|
|
if (!h) {
|
|
return undefined;
|
|
}
|
|
try {
|
|
return tryHexToNativeAssetString(h, c);
|
|
} catch (e) {
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
/**
|
|
*
|
|
* Convert an address in a wormhole's 32-byte hex representation into a chain's native
|
|
* string representation.
|
|
*
|
|
* @throws if address is not the right length for the given chain
|
|
*/
|
|
export const tryHexToNativeString = (
|
|
h: string,
|
|
c: ChainId | ChainName
|
|
): string => tryUint8ArrayToNative(hexToUint8Array(h), c);
|
|
|
|
/**
|
|
*
|
|
* Convert an address in a wormhole's 32-byte hex representation into a chain's native
|
|
* string representation.
|
|
*
|
|
* @deprecated since 0.3.0, use [[tryHexToNativeString]] instead.
|
|
*/
|
|
export const hexToNativeString = (
|
|
h: string | undefined,
|
|
c: ChainId | ChainName
|
|
): string | undefined => {
|
|
if (!h) {
|
|
return undefined;
|
|
}
|
|
|
|
try {
|
|
return tryHexToNativeString(h, c);
|
|
} catch (e) {
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
/**
|
|
*
|
|
* Convert an address in a chain's native representation into a 32-byte hex string
|
|
* understood by wormhole.
|
|
*
|
|
* @throws if address is a malformed string for the given chain id
|
|
*/
|
|
export const tryNativeToHexString = (
|
|
address: string,
|
|
chain: ChainId | ChainName
|
|
): string => {
|
|
const chainId = coalesceChainId(chain);
|
|
if (isEVMChain(chainId)) {
|
|
return uint8ArrayToHex(zeroPad(arrayify(address), 32));
|
|
} else if (chainId === CHAIN_ID_SOLANA || chainId === CHAIN_ID_PYTHNET) {
|
|
return uint8ArrayToHex(zeroPad(new PublicKey(address).toBytes(), 32));
|
|
} else if (chainId === CHAIN_ID_TERRA) {
|
|
if (isNativeDenom(address)) {
|
|
return (
|
|
"01" +
|
|
uint8ArrayToHex(
|
|
zeroPad(new Uint8Array(Buffer.from(address, "ascii")), 31)
|
|
)
|
|
);
|
|
} else {
|
|
return uint8ArrayToHex(zeroPad(canonicalAddress(address), 32));
|
|
}
|
|
} else if (
|
|
chainId === CHAIN_ID_TERRA2 ||
|
|
chainId === CHAIN_ID_INJECTIVE ||
|
|
chainId === CHAIN_ID_XPLA ||
|
|
chainId === CHAIN_ID_SEI
|
|
) {
|
|
return buildTokenId(chainId, address);
|
|
} else if (chainId === CHAIN_ID_ALGORAND) {
|
|
return nativeStringToHexAlgorand(address);
|
|
} else if (chainId == CHAIN_ID_WORMCHAIN) {
|
|
return uint8ArrayToHex(zeroPad(canonicalAddress(address), 32));
|
|
} else if (chainId === CHAIN_ID_NEAR) {
|
|
return uint8ArrayToHex(arrayify(sha256(Buffer.from(address))));
|
|
} else if (chainId === CHAIN_ID_OSMOSIS) {
|
|
throw Error("nativeToHexString: Osmosis not supported yet.");
|
|
} else if (chainId === CHAIN_ID_COSMOSHUB) {
|
|
throw Error("nativeToHexString: CosmosHub not supported yet.");
|
|
} else if (chainId === CHAIN_ID_EVMOS) {
|
|
throw Error("nativeToHexString: Evmos not supported yet.");
|
|
} else if (chainId === CHAIN_ID_KUJIRA) {
|
|
throw Error("nativeToHexString: Kujira not supported yet.");
|
|
} else if (chainId === CHAIN_ID_NEUTRON) {
|
|
throw Error("nativeToHexString: Neutron not supported yet.");
|
|
} else if (chainId === CHAIN_ID_CELESTIA) {
|
|
throw Error("nativeToHexString: Celestia not supported yet.");
|
|
} else if (chainId === CHAIN_ID_SUI) {
|
|
if (!isValidSuiType(address) && isValidSuiAddress(address)) {
|
|
return uint8ArrayToHex(
|
|
zeroPad(arrayify(address, { allowMissingPrefix: true }), 32)
|
|
);
|
|
}
|
|
throw Error("nativeToHexString: Sui types not supported yet.");
|
|
} else if (chainId === CHAIN_ID_BTC) {
|
|
throw Error("nativeToHexString: Btc not supported yet.");
|
|
} else if (chainId === CHAIN_ID_APTOS) {
|
|
if (isValidAptosType(address)) {
|
|
return getExternalAddressFromType(address);
|
|
}
|
|
|
|
return uint8ArrayToHex(
|
|
zeroPad(arrayify(address, { allowMissingPrefix: true }), 32)
|
|
);
|
|
} else if (chainId === CHAIN_ID_UNSET) {
|
|
throw Error("nativeToHexString: Chain id unset");
|
|
} else {
|
|
// If this case is reached
|
|
const _: never = chainId;
|
|
throw Error("Don't know how to convert address from chain " + chainId);
|
|
}
|
|
};
|
|
|
|
/**
|
|
*
|
|
* Convert an address in a chain's native representation into a 32-byte hex string
|
|
* understood by wormhole.
|
|
*
|
|
* @deprecated since 0.3.0, use [[tryNativeToHexString]] instead.
|
|
* @throws if address is a malformed string for the given chain id
|
|
*/
|
|
export const nativeToHexString = (
|
|
address: string | undefined,
|
|
chain: ChainId | ChainName
|
|
): string | null => {
|
|
if (!address) {
|
|
return null;
|
|
}
|
|
return tryNativeToHexString(address, chain);
|
|
};
|
|
|
|
/**
|
|
*
|
|
* Convert an address in a chain's native representation into a 32-byte array
|
|
* understood by wormhole.
|
|
*
|
|
* @throws if address is a malformed string for the given chain id
|
|
*/
|
|
export function tryNativeToUint8Array(
|
|
address: string,
|
|
chain: ChainId | ChainName
|
|
): Uint8Array {
|
|
const chainId = coalesceChainId(chain);
|
|
return hexToUint8Array(tryNativeToHexString(address, chainId));
|
|
}
|
|
|
|
/**
|
|
*
|
|
* Convert an address in a chain's native representation into a 32-byte hex string
|
|
* understood by wormhole.
|
|
*
|
|
* @deprecated since 0.3.0, use [[tryUint8ArrayToNative]] instead.
|
|
* @throws if address is a malformed string for the given chain id
|
|
*/
|
|
export const uint8ArrayToNative = (a: Uint8Array, chainId: ChainId) =>
|
|
hexToNativeString(uint8ArrayToHex(a), chainId);
|
|
|
|
export function chunks<T>(array: T[], size: number): T[][] {
|
|
return Array.apply<number, T[], T[][]>(
|
|
0,
|
|
new Array(Math.ceil(array.length / size))
|
|
).map((_, index) => array.slice(index * size, (index + 1) * size));
|
|
}
|
|
|
|
export function textToHexString(name: string): string {
|
|
return Buffer.from(name, "binary").toString("hex");
|
|
}
|
|
|
|
export function textToUint8Array(name: string): Uint8Array {
|
|
return new Uint8Array(Buffer.from(name, "binary"));
|
|
}
|
|
|
|
export function hex(x: string): Buffer {
|
|
return Buffer.from(
|
|
ethers.utils.hexlify(x, { allowMissingPrefix: true }).substring(2),
|
|
"hex"
|
|
);
|
|
}
|
|
|
|
export function ensureHexPrefix(x: string): string {
|
|
return x.substring(0, 2) !== "0x" ? `0x${x}` : x;
|
|
}
|