Skip to content

Commit

Permalink
Start WarpCore implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
jmrossy committed Feb 10, 2024
1 parent 0db8a6d commit 19462b2
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 27 deletions.
8 changes: 8 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@
"@typescript-eslint/no-explicit-any": ["off"],
"@typescript-eslint/no-non-null-assertion": ["off"],
"@typescript-eslint/no-require-imports": ["warn"],
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}
],
"jsx-a11y/alt-text": ["off"]
}
}
79 changes: 53 additions & 26 deletions src/features/core/Token.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
import {
ChainMetadata,
ChainName,
CosmIbcTokenAdapter,
CosmNativeTokenAdapter,
Expand All @@ -26,6 +27,7 @@ import { ProtocolType } from '@hyperlane-xyz/utils';
import { TokenAmount } from './TokenAmount';
import {
Numberish,
PROTOCOL_TO_NATIVE_STANDARD,
TOKEN_MULTI_CHAIN_STANDARDS,
TOKEN_NFT_STANDARDS,
TokenStandard,
Expand All @@ -36,12 +38,12 @@ export interface TokenArgs {
chainName: ChainName;
standard: TokenStandard;
addressOrDenom: Address | string;
collateralizedAddressOrDenom?: Address | string;
gasAddressOrDenom?: string;
collateralAddressOrDenom?: Address | string;
igpTokenAddressOrDenom?: string;
decimals: number;
symbol: string;
name: string;
logoUri?: string;
logoURI?: string;
connectedTokens?: Token[];

// Cosmos specific:
Expand All @@ -58,6 +60,23 @@ export class Token {
Object.assign(this, args);
}

static FromChainMetadataNativeToken(chainMetadata: ChainMetadata): Token {
if (!chainMetadata.nativeToken)
throw new Error(`ChainMetadata for ${chainMetadata.name} missing nativeToken`);

const { protocol, name: chainName, nativeToken, logoURI } = chainMetadata;
return new Token({
protocol,
chainName,
standard: PROTOCOL_TO_NATIVE_STANDARD[protocol],
addressOrDenom: '',
decimals: nativeToken.decimals,
symbol: nativeToken.symbol,
name: nativeToken.name,
logoURI,
});
}

/**
* Returns a TokenAdapter for the token and multiProvider
* @throws If multiProvider does not contain this token's chain.
Expand Down Expand Up @@ -110,8 +129,8 @@ export class Token {
standard,
chainName,
addressOrDenom,
collateralizedAddressOrDenom,
gasAddressOrDenom,
collateralAddressOrDenom,
igpTokenAddressOrDenom,
sourcePort,
sourceChannel,
} = this;
Expand All @@ -126,11 +145,11 @@ export class Token {
let sealevelAddresses;
if (protocol === ProtocolType.Sealevel) {
if (!mailbox) throw new Error('mailbox required for Sealevel hyp tokens');
if (!collateralizedAddressOrDenom)
throw new Error('collateralizedAddressOrDenom required for Sealevel hyp tokens');
if (!collateralAddressOrDenom)
throw new Error('collateralAddressOrDenom required for Sealevel hyp tokens');
sealevelAddresses = {
warpRouter: addressOrDenom,
token: collateralizedAddressOrDenom,
token: collateralAddressOrDenom,
mailbox,
};
}
Expand All @@ -150,25 +169,25 @@ export class Token {
chainName,
multiProvider,
{ warpRouter: addressOrDenom },
gasAddressOrDenom,
igpTokenAddressOrDenom,
);
} else if (standard === TokenStandard.CwHypCollateral) {
if (!collateralizedAddressOrDenom)
throw new Error('collateralizedAddressOrDenom required for CwHypCollateral');
if (!collateralAddressOrDenom)
throw new Error('collateralAddressOrDenom required for CwHypCollateral');
return new CwHypCollateralAdapter(
chainName,
multiProvider,
{ warpRouter: addressOrDenom, token: collateralizedAddressOrDenom },
gasAddressOrDenom,
{ warpRouter: addressOrDenom, token: collateralAddressOrDenom },
igpTokenAddressOrDenom,
);
} else if (standard === TokenStandard.CwHypSynthetic) {
if (!collateralizedAddressOrDenom)
throw new Error('collateralizedAddressOrDenom required for CwHypSyntheticAdapter');
if (!collateralAddressOrDenom)
throw new Error('collateralAddressOrDenom required for CwHypSyntheticAdapter');
return new CwHypSyntheticAdapter(
chainName,
multiProvider,
{ warpRouter: addressOrDenom, token: collateralizedAddressOrDenom },
gasAddressOrDenom,
{ warpRouter: addressOrDenom, token: collateralAddressOrDenom },
igpTokenAddressOrDenom,
);
} else if (standard === TokenStandard.CosmosIbc) {
if (!sourcePort || !sourceChannel)
Expand All @@ -184,13 +203,13 @@ export class Token {
}
}

getConnectedTokens(): Token[] {
return this.connectedTokens || [];
}

setConnectedTokens(tokens: Token[]): Token[] {
this.connectedTokens = tokens;
return tokens;
/**
* Convenience method to create an adapter and return an account balance
*/
async getBalance(multiProvider: MultiProtocolProvider, address: Address): Promise<TokenAmount> {
const adapter = this.getAdapter(multiProvider);
const balance = await adapter.getBalance(address);
return new TokenAmount(balance, this);
}

amount(amount: Numberish): TokenAmount {
Expand All @@ -205,15 +224,23 @@ export class Token {
return TOKEN_MULTI_CHAIN_STANDARDS.includes(this.standard);
}

getConnectedTokens(): Token[] {
return this.connectedTokens || [];
}

setConnectedTokens(tokens: Token[]): Token[] {
this.connectedTokens = tokens;
return tokens;
}

equals(token: Token): boolean {
return (
this.protocol === token.protocol &&
this.chainName === token.chainName &&
this.standard === token.standard &&
this.decimals === token.decimals &&
this.addressOrDenom.toLowerCase() === token.addressOrDenom.toLowerCase() &&
this.collateralizedAddressOrDenom?.toLowerCase() ===
token.collateralizedAddressOrDenom?.toLowerCase()
this.collateralAddressOrDenom?.toLowerCase() === token.collateralAddressOrDenom?.toLowerCase()
);
}
}
120 changes: 120 additions & 0 deletions src/features/core/WarpCore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import debug, { Debugger } from 'debug';

import { ChainName, MultiProtocolProvider } from '@hyperlane-xyz/sdk';
import { ProtocolType } from '@hyperlane-xyz/utils';

import { Token } from './Token';
import { TokenAmount } from './TokenAmount';
import { HyperlaneChainId, IgpQuoteConstants } from './types';

export interface WarpCoreOptions {
loggerName?: string;
igpQuoteConstants?: IgpQuoteConstants;
}

export class WarpCore {
public readonly multiProvider: MultiProtocolProvider<{ mailbox?: Address }>;
public readonly tokens: Token[];
public readonly igpQuoteConstants: IgpQuoteConstants;
public readonly logger: Debugger;

constructor(
multiProvider: MultiProtocolProvider<{ mailbox?: Address }>,
tokens: Token[],
options: WarpCoreOptions,
) {
this.multiProvider = multiProvider;
this.tokens = tokens;
this.igpQuoteConstants = options?.igpQuoteConstants || {};
this.logger = debug(options?.loggerName || 'hyperlane:WarpCore');
}

// Takes the serialized representation of a complete warp config and returns a WarpCore instance
static FromConfig(
_multiProvider: MultiProtocolProvider<{ mailbox?: Address }>,
_config: string,
): WarpCore {
throw new Error('TODO: method not implemented');
}

async getTransferGasQuote(
originToken: Token,
destination: HyperlaneChainId,
): Promise<TokenAmount> {
const { chainName: originName, protocol: originProtocol } = originToken;

// Step 1: Determine the amount

let gasAmount: bigint;
const defaultQuotes = this.igpQuoteConstants[originProtocol];
// Check constant quotes first
if (typeof defaultQuotes === 'string') {
gasAmount = BigInt(defaultQuotes);
} else if (defaultQuotes?.[originName]) {
gasAmount = BigInt(defaultQuotes[originName]);
} else {
// Otherwise, compute IGP quote via the adapter
const hypAdapter = originToken.getHypAdapter(this.multiProvider);
const destinationDomainId = this.multiProvider.getDomainId(destination);
gasAmount = BigInt(await hypAdapter.quoteGasPayment(destinationDomainId));
}

// Step 2: Determine the IGP token
// TODO, it would be more robust to determine this based on on-chain data
// rather than these janky heuristic

// If the token has an explicit IGP token address set, use that
let igpToken: Token;
if (originToken.igpTokenAddressOrDenom) {
const searchResult = this.findToken(originToken.igpTokenAddressOrDenom, originName);
if (!searchResult)
throw new Error(`IGP token ${originToken.igpTokenAddressOrDenom} is unknown`);
igpToken = searchResult;
} else if (originProtocol === ProtocolType.Cosmos) {
// If the protocol is cosmos, assume the token itself is used
igpToken = originToken;
} else {
// Otherwise use the plain old native token from the route origin
igpToken = Token.FromChainMetadataNativeToken(
this.multiProvider.getChainMetadata(originName),
);
}

return new TokenAmount(gasAmount, igpToken);
}

async validateTransfer(
originTokenAmount: TokenAmount,
destination: HyperlaneChainId,
recipient: Address,
): Promise<Record<string, string> | null> {
throw new Error('TODO');
}

async getTransferRemoteTxs(
originTokenAmount: TokenAmount,
destination: HyperlaneChainId,
recipient: Address,
): Promise<{ approveTx; transferTx }> {
throw new Error('TODO');
}

// Checks to ensure the destination chain's collateral is sufficient to cover the transfer
async isDestinationCollateralSufficient(
originTokenAmount: TokenAmount,
destination: HyperlaneChainId,
): Promise<boolean> {
throw new Error('TODO');
}

findToken(addressOrDenom: Address | string, chainName: ChainName): Token | null {
const results = this.tokens.filter(
(token) =>
token.chainName === chainName &&
token.addressOrDenom.toLowerCase() === addressOrDenom.toLowerCase(),
);
if (!results.length) return null;
if (results.length > 1) throw new Error(`Ambiguous token search results for ${addressOrDenom}`);
return results[0];
}
}
2 changes: 1 addition & 1 deletion src/features/core/WarpCoreSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export enum TokenStandard {

export class Token {
constructor({
protocol, chainName, standard, addressOrDenom, collateralizedAddressOrDenom,
protocol, chainName, standard, addressOrDenom, collateralAddressOrDenom,
symbol, decimals, name, logoUri, connectedTokens
}) {}

Expand Down
16 changes: 16 additions & 0 deletions src/features/core/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ChainName } from '@hyperlane-xyz/sdk';
import { ProtocolType } from '@hyperlane-xyz/utils';

export type HyperlaneChainId = ChainName | ChainId | DomainId;
export type Numberish = number | string | bigint;
Expand Down Expand Up @@ -34,6 +35,9 @@ export enum TokenStandard {
CwHypNative = 'CwHypNative',
CwHypCollateral = 'CwHypCollateral',
CwHypSynthetic = 'CwHypSynthetic',

// Fuel (TODO)
FuelNative = 'FuelNative',
}

export const TOKEN_NFT_STANDARDS = [
Expand All @@ -55,3 +59,15 @@ export const TOKEN_MULTI_CHAIN_STANDARDS = [
TokenStandard.CwHypCollateral,
TokenStandard.CwHypSynthetic,
];

export const PROTOCOL_TO_NATIVE_STANDARD: Record<ProtocolType, TokenStandard> = {
[ProtocolType.Ethereum]: TokenStandard.EvmNative,
[ProtocolType.Cosmos]: TokenStandard.CosmosNative,
[ProtocolType.Sealevel]: TokenStandard.SealevelNative,
[ProtocolType.Fuel]: TokenStandard.FuelNative,
};

// Map of protocol to either quote constant or to a map of chain name to quote constant
export type IgpQuoteConstants = Partial<
Record<ProtocolType, string | Record<string | number, string>>
>;

0 comments on commit 19462b2

Please sign in to comment.