Implement WarpCore and Token classes (#3272)
### Description Implements the WarpCore and auxiliary classes. Summary of changes: - Add `WarpCore`, `Token`, and `TokenAmount` classes - Define TokenStandard enum and TokenConnection type - Improve IGP quote handling in token adapters - Define `ChainNameOrId` type, also use in MultiProvider - Add optional `denom` field to chain metadata native token ### Related issues Fixes https://github.com/hyperlane-xyz/hyperlane-warp-ui-template/issues/129 ### Backward compatibility No: The params to the `IHypTokenAdapter` `populateTransferRemoteTx` method have changed. `txValue` has been replaced with `interchainGas` ### Testing - Created new tests - Integrated into Warp UIinjective-ism-fix
parent
65fa0daae8
commit
aea9e1438a
@ -0,0 +1,8 @@ |
||||
--- |
||||
'@hyperlane-xyz/utils': minor |
||||
'@hyperlane-xyz/sdk': minor |
||||
--- |
||||
|
||||
Add `WarpCore`, `Token`, and `TokenAmount` classes for interacting with Warp Route instances. |
||||
|
||||
_Breaking change_: The params to the `IHypTokenAdapter` `populateTransferRemoteTx` method have changed. `txValue` has been replaced with `interchainGas`. |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
@ -0,0 +1,88 @@ |
||||
import { z } from 'zod'; |
||||
|
||||
import { Address, Numberish, ProtocolType } from '@hyperlane-xyz/utils'; |
||||
|
||||
import { ZChainName, ZUint } from '../metadata/customZodTypes'; |
||||
import type { MultiProtocolProvider } from '../providers/MultiProtocolProvider'; |
||||
import type { ChainName } from '../types'; |
||||
|
||||
import type { TokenAmount } from './TokenAmount'; |
||||
import { |
||||
type TokenConnection, |
||||
TokenConnectionConfigSchema, |
||||
} from './TokenConnection'; |
||||
import { TokenStandard } from './TokenStandard'; |
||||
import type { IHypTokenAdapter, ITokenAdapter } from './adapters/ITokenAdapter'; |
||||
|
||||
export const TokenConfigSchema = z.object({ |
||||
chainName: ZChainName.describe( |
||||
'The name of the chain, must correspond to a chain in the multiProvider chainMetadata', |
||||
), |
||||
standard: z |
||||
.nativeEnum(TokenStandard) |
||||
.describe('The type of token. See TokenStandard for valid values.'), |
||||
decimals: ZUint.lt(256).describe('The decimals value (e.g. 18 for Eth)'), |
||||
symbol: z.string().min(1).describe('The symbol of the token'), |
||||
name: z.string().min(1).describe('The name of the token'), |
||||
addressOrDenom: z |
||||
.string() |
||||
.min(1) |
||||
.or(z.null()) |
||||
.describe('The address or denom, or null for native tokens'), |
||||
collateralAddressOrDenom: z |
||||
.string() |
||||
.min(1) |
||||
.optional() |
||||
.describe('The address or denom of the collateralized token'), |
||||
igpTokenAddressOrDenom: z |
||||
.string() |
||||
.min(1) |
||||
.optional() |
||||
.describe('The address or denom of the token for IGP payments'), |
||||
logoURI: z.string().optional().describe('The URI of the token logo'), |
||||
connections: z |
||||
.array(TokenConnectionConfigSchema) |
||||
.optional() |
||||
.describe('The list of token connections (e.g. warp or IBC)'), |
||||
}); |
||||
|
||||
export type TokenArgs = Omit< |
||||
z.infer<typeof TokenConfigSchema>, |
||||
'addressOrDenom' | 'connections' |
||||
> & { |
||||
addressOrDenom: Address | string; |
||||
connections?: Array<TokenConnection>; |
||||
}; |
||||
|
||||
export interface IToken extends TokenArgs { |
||||
protocol: ProtocolType; |
||||
|
||||
getAdapter(multiProvider: MultiProtocolProvider): ITokenAdapter<unknown>; |
||||
getHypAdapter( |
||||
multiProvider: MultiProtocolProvider<{ mailbox?: Address }>, |
||||
destination?: ChainName, |
||||
): IHypTokenAdapter<unknown>; |
||||
|
||||
getBalance( |
||||
multiProvider: MultiProtocolProvider, |
||||
address: Address, |
||||
): Promise<TokenAmount>; |
||||
|
||||
amount(amount: Numberish): TokenAmount; |
||||
|
||||
isNft(): boolean; |
||||
isNative(): boolean; |
||||
isHypToken(): boolean; |
||||
isIbcToken(): boolean; |
||||
isMultiChainToken(): boolean; |
||||
|
||||
getConnections(): TokenConnection[]; |
||||
|
||||
getConnectionForChain(chain: ChainName): TokenConnection | undefined; |
||||
addConnection(connection: TokenConnection): IToken; |
||||
removeConnection(token: IToken): IToken; |
||||
|
||||
equals(token: IToken): boolean; |
||||
|
||||
collateralizes(token: IToken): boolean; |
||||
} |
@ -0,0 +1,176 @@ |
||||
/* eslint-disable no-console */ |
||||
import { expect } from 'chai'; |
||||
import { ethers } from 'ethers'; |
||||
|
||||
import { Address, ProtocolType } from '@hyperlane-xyz/utils'; |
||||
|
||||
import { chainMetadata } from '../consts/chainMetadata'; |
||||
import { Chains } from '../consts/chains'; |
||||
import { MultiProtocolProvider } from '../providers/MultiProtocolProvider'; |
||||
|
||||
import { TokenArgs } from './IToken'; |
||||
import { Token } from './Token'; |
||||
import { TokenStandard } from './TokenStandard'; |
||||
|
||||
// null values represent TODOs here, ideally all standards should be tested
|
||||
const STANDARD_TO_TOKEN: Record<TokenStandard, TokenArgs | null> = { |
||||
// EVM
|
||||
[TokenStandard.ERC20]: { |
||||
chainName: Chains.ethereum, |
||||
standard: TokenStandard.ERC20, |
||||
addressOrDenom: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', |
||||
decimals: 6, |
||||
symbol: 'USDC', |
||||
name: 'USDC', |
||||
}, |
||||
[TokenStandard.ERC721]: null, |
||||
[TokenStandard.EvmNative]: Token.FromChainMetadataNativeToken( |
||||
chainMetadata.optimism, |
||||
), |
||||
[TokenStandard.EvmHypNative]: { |
||||
chainName: Chains.inevm, |
||||
standard: TokenStandard.EvmHypNative, |
||||
addressOrDenom: '0x26f32245fCF5Ad53159E875d5Cae62aEcf19c2d4', |
||||
decimals: 18, |
||||
symbol: 'INJ', |
||||
name: 'Injective Coin', |
||||
}, |
||||
[TokenStandard.EvmHypCollateral]: { |
||||
chainName: Chains.goerli, |
||||
standard: TokenStandard.EvmHypCollateral, |
||||
addressOrDenom: '0x145de8760021c4ac6676376691b78038d3DE9097', |
||||
collateralAddressOrDenom: '0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', |
||||
decimals: 18, |
||||
symbol: 'WETH', |
||||
name: 'Weth', |
||||
}, |
||||
[TokenStandard.EvmHypSynthetic]: { |
||||
chainName: Chains.inevm, |
||||
standard: TokenStandard.EvmHypSynthetic, |
||||
addressOrDenom: '0x8358D8291e3bEDb04804975eEa0fe9fe0fAfB147', |
||||
decimals: 6, |
||||
symbol: 'USDC', |
||||
name: 'USDC', |
||||
}, |
||||
|
||||
// Sealevel
|
||||
[TokenStandard.SealevelSpl]: { |
||||
chainName: Chains.solana, |
||||
standard: TokenStandard.SealevelSpl, |
||||
addressOrDenom: 'So11111111111111111111111111111111111111112', |
||||
decimals: 9, |
||||
symbol: 'Wrapped SOL', |
||||
name: 'SOL', |
||||
}, |
||||
[TokenStandard.SealevelSpl2022]: { |
||||
chainName: Chains.solana, |
||||
standard: TokenStandard.SealevelSpl2022, |
||||
addressOrDenom: '21zHSATJqhNkcpoNkhFzPJW9LARSmoinLEeDtdygGuWh', |
||||
decimals: 6, |
||||
symbol: 'SOLMAX', |
||||
name: 'Solana Maxi', |
||||
}, |
||||
[TokenStandard.SealevelNative]: Token.FromChainMetadataNativeToken( |
||||
chainMetadata.solana, |
||||
), |
||||
[TokenStandard.SealevelHypNative]: null, |
||||
[TokenStandard.SealevelHypCollateral]: null, |
||||
[TokenStandard.SealevelHypSynthetic]: null, |
||||
|
||||
// Cosmos
|
||||
[TokenStandard.CosmosIcs20]: null, |
||||
[TokenStandard.CosmosIcs721]: null, |
||||
[TokenStandard.CosmosNative]: Token.FromChainMetadataNativeToken( |
||||
chainMetadata.neutron, |
||||
), |
||||
[TokenStandard.CosmosIbc]: { |
||||
chainName: Chains.neutron, |
||||
standard: TokenStandard.CosmosIbc, |
||||
addressOrDenom: |
||||
'ibc/773B4D0A3CD667B2275D5A4A7A2F0909C0BA0F4059C0B9181E680DDF4965DCC7', |
||||
decimals: 6, |
||||
symbol: 'TIA', |
||||
name: 'TIA', |
||||
}, |
||||
[TokenStandard.CW20]: null, |
||||
[TokenStandard.CWNative]: { |
||||
chainName: Chains.neutron, |
||||
standard: TokenStandard.CWNative, |
||||
addressOrDenom: |
||||
'ibc/5751B8BCDA688FD0A8EC0B292EEF1CDEAB4B766B63EC632778B196D317C40C3A', |
||||
decimals: 6, |
||||
symbol: 'ASTRO', |
||||
name: 'ASTRO', |
||||
}, |
||||
[TokenStandard.CW721]: null, |
||||
[TokenStandard.CwHypNative]: { |
||||
chainName: Chains.injective, |
||||
standard: TokenStandard.CwHypNative, |
||||
addressOrDenom: 'inj1mv9tjvkaw7x8w8y9vds8pkfq46g2vcfkjehc6k', |
||||
igpTokenAddressOrDenom: 'inj', |
||||
decimals: 18, |
||||
symbol: 'INJ', |
||||
name: 'Injective Coin', |
||||
}, |
||||
[TokenStandard.CwHypCollateral]: { |
||||
chainName: Chains.neutron, |
||||
standard: TokenStandard.CwHypCollateral, |
||||
addressOrDenom: |
||||
'neutron1jyyjd3x0jhgswgm6nnctxvzla8ypx50tew3ayxxwkrjfxhvje6kqzvzudq', |
||||
collateralAddressOrDenom: |
||||
'ibc/773B4D0A3CD667B2275D5A4A7A2F0909C0BA0F4059C0B9181E680DDF4965DCC7', |
||||
decimals: 6, |
||||
symbol: 'TIA.n', |
||||
name: 'TIA.n', |
||||
}, |
||||
[TokenStandard.CwHypSynthetic]: null, |
||||
|
||||
// Fuel
|
||||
[TokenStandard.FuelNative]: null, |
||||
}; |
||||
|
||||
const PROTOCOL_TO_ADDRESS: Partial<Record<ProtocolType, Address>> = { |
||||
[ProtocolType.Ethereum]: ethers.constants.AddressZero, |
||||
[ProtocolType.Cosmos]: |
||||
'neutron13we0myxwzlpx8l5ark8elw5gj5d59dl6cjkzmt80c5q5cv5rt54qvzkv2a', |
||||
[ProtocolType.Fuel]: '', |
||||
}; |
||||
|
||||
const STANDARD_TO_ADDRESS: Partial<Record<TokenStandard, Address>> = { |
||||
[TokenStandard.SealevelSpl]: 'HVSZJ2juJnMxd6yCNarTL56YmgUqzfUiwM7y7LtTXKHR', |
||||
[TokenStandard.SealevelSpl2022]: |
||||
'EK6cs8jNnu2d9pmKTGf1Bvre9oW2xNhcCKNdLKx6t74w', |
||||
[TokenStandard.SealevelNative]: |
||||
'EK6cs8jNnu2d9pmKTGf1Bvre9oW2xNhcCKNdLKx6t74w', |
||||
[TokenStandard.CwHypNative]: 'inj1fl48vsnmsdzcv85q5d2q4z5ajdha8yu3lj7tt0', |
||||
}; |
||||
|
||||
describe('Token', () => { |
||||
it('Handles all standards', async () => { |
||||
const multiProvider = new MultiProtocolProvider(); |
||||
for (const tokenArgs of Object.values(STANDARD_TO_TOKEN)) { |
||||
if (!tokenArgs) continue; |
||||
console.debug('Testing token standard', tokenArgs.standard); |
||||
const token = new Token(tokenArgs); |
||||
expect(token.standard).to.eql(tokenArgs.standard); |
||||
const adapter = token.getAdapter(multiProvider); |
||||
const adddress = |
||||
STANDARD_TO_ADDRESS[token.standard] ?? |
||||
PROTOCOL_TO_ADDRESS[token.protocol]; |
||||
if (!adddress) |
||||
throw new Error(`No address for standard ${tokenArgs.standard}`); |
||||
const balance = await adapter.getBalance(adddress); |
||||
expect(typeof balance).to.eql('bigint'); |
||||
} |
||||
}) |
||||
.timeout(120_000) |
||||
.retries(3); |
||||
|
||||
it('Constructs from ChainMetadata', () => { |
||||
for (const metadata of Object.values(chainMetadata)) { |
||||
if (!metadata.nativeToken) continue; |
||||
const token = Token.FromChainMetadataNativeToken(metadata); |
||||
expect(token.symbol).to.eql(metadata.nativeToken.symbol); |
||||
} |
||||
}); |
||||
}); |
@ -0,0 +1,394 @@ |
||||
/* eslint-disable @typescript-eslint/no-empty-interface */ |
||||
import { MsgTransferEncodeObject } from '@cosmjs/stargate'; |
||||
|
||||
import { |
||||
Address, |
||||
Numberish, |
||||
ProtocolType, |
||||
assert, |
||||
eqAddress, |
||||
} from '@hyperlane-xyz/utils'; |
||||
|
||||
import { ChainMetadata } from '../metadata/chainMetadataTypes'; |
||||
import { MultiProtocolProvider } from '../providers/MultiProtocolProvider'; |
||||
import { ChainName } from '../types'; |
||||
|
||||
import type { IToken, TokenArgs } from './IToken'; |
||||
import { TokenAmount } from './TokenAmount'; |
||||
import { TokenConnection, TokenConnectionType } from './TokenConnection'; |
||||
import { |
||||
PROTOCOL_TO_NATIVE_STANDARD, |
||||
TOKEN_COLLATERALIZED_STANDARDS, |
||||
TOKEN_HYP_STANDARDS, |
||||
TOKEN_MULTI_CHAIN_STANDARDS, |
||||
TOKEN_NFT_STANDARDS, |
||||
TOKEN_STANDARD_TO_PROTOCOL, |
||||
TokenStandard, |
||||
} from './TokenStandard'; |
||||
import { |
||||
CwHypCollateralAdapter, |
||||
CwHypNativeAdapter, |
||||
CwHypSyntheticAdapter, |
||||
CwNativeTokenAdapter, |
||||
CwTokenAdapter, |
||||
} from './adapters/CosmWasmTokenAdapter'; |
||||
import { |
||||
CosmIbcToWarpTokenAdapter, |
||||
CosmIbcTokenAdapter, |
||||
CosmNativeTokenAdapter, |
||||
} from './adapters/CosmosTokenAdapter'; |
||||
import { |
||||
EvmHypCollateralAdapter, |
||||
EvmHypNativeAdapter, |
||||
EvmHypSyntheticAdapter, |
||||
EvmNativeTokenAdapter, |
||||
EvmTokenAdapter, |
||||
} from './adapters/EvmTokenAdapter'; |
||||
import type { IHypTokenAdapter, ITokenAdapter } from './adapters/ITokenAdapter'; |
||||
import { |
||||
SealevelHypCollateralAdapter, |
||||
SealevelHypNativeAdapter, |
||||
SealevelHypSyntheticAdapter, |
||||
SealevelNativeTokenAdapter, |
||||
SealevelTokenAdapter, |
||||
} from './adapters/SealevelTokenAdapter'; |
||||
|
||||
// Declaring the interface in addition to class allows
|
||||
// Typescript to infer the members vars from TokenArgs
|
||||
export interface Token extends TokenArgs {} |
||||
|
||||
export class Token implements IToken { |
||||
public readonly protocol: ProtocolType; |
||||
|
||||
constructor(args: TokenArgs) { |
||||
Object.assign(this, args); |
||||
this.protocol = TOKEN_STANDARD_TO_PROTOCOL[this.standard]; |
||||
} |
||||
|
||||
static FromChainMetadataNativeToken(chainMetadata: ChainMetadata): Token { |
||||
const { protocol, name: chainName, nativeToken, logoURI } = chainMetadata; |
||||
assert( |
||||
nativeToken, |
||||
`ChainMetadata for ${chainMetadata.name} missing nativeToken`, |
||||
); |
||||
|
||||
return new Token({ |
||||
chainName, |
||||
standard: PROTOCOL_TO_NATIVE_STANDARD[protocol], |
||||
addressOrDenom: nativeToken.denom ?? '', |
||||
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. |
||||
* @throws If token is an NFT (TODO NFT Adapter support) |
||||
*/ |
||||
getAdapter(multiProvider: MultiProtocolProvider): ITokenAdapter<unknown> { |
||||
const { standard, chainName, addressOrDenom } = this; |
||||
|
||||
assert(!this.isNft(), 'NFT adapters not yet supported'); |
||||
assert( |
||||
multiProvider.tryGetChainMetadata(chainName), |
||||
`Token chain ${chainName} not found in multiProvider`, |
||||
); |
||||
|
||||
if (standard === TokenStandard.ERC20) { |
||||
return new EvmTokenAdapter(chainName, multiProvider, { |
||||
token: addressOrDenom, |
||||
}); |
||||
} else if (standard === TokenStandard.EvmNative) { |
||||
return new EvmNativeTokenAdapter(chainName, multiProvider, {}); |
||||
} else if (standard === TokenStandard.SealevelSpl) { |
||||
return new SealevelTokenAdapter( |
||||
chainName, |
||||
multiProvider, |
||||
{ token: addressOrDenom }, |
||||
false, |
||||
); |
||||
} else if (standard === TokenStandard.SealevelSpl2022) { |
||||
return new SealevelTokenAdapter( |
||||
chainName, |
||||
multiProvider, |
||||
{ token: addressOrDenom }, |
||||
true, |
||||
); |
||||
} else if (standard === TokenStandard.SealevelNative) { |
||||
return new SealevelNativeTokenAdapter(chainName, multiProvider, {}); |
||||
} else if (standard === TokenStandard.CosmosIcs20) { |
||||
throw new Error('Cosmos ICS20 token adapter not yet supported'); |
||||
} else if (standard === TokenStandard.CosmosNative) { |
||||
return new CosmNativeTokenAdapter( |
||||
chainName, |
||||
multiProvider, |
||||
{}, |
||||
{ ibcDenom: addressOrDenom }, |
||||
); |
||||
} else if (standard === TokenStandard.CW20) { |
||||
return new CwTokenAdapter(chainName, multiProvider, { |
||||
token: addressOrDenom, |
||||
}); |
||||
} else if (standard === TokenStandard.CWNative) { |
||||
return new CwNativeTokenAdapter( |
||||
chainName, |
||||
multiProvider, |
||||
{}, |
||||
addressOrDenom, |
||||
); |
||||
} else if (this.isHypToken()) { |
||||
return this.getHypAdapter(multiProvider); |
||||
} else if (this.isIbcToken()) { |
||||
// Passing in a stub connection here because it's not required
|
||||
// for an IBC adapter to fulfill the ITokenAdapter interface
|
||||
return this.getIbcAdapter(multiProvider, { |
||||
token: this, |
||||
sourcePort: 'transfer', |
||||
sourceChannel: 'channel-0', |
||||
type: TokenConnectionType.Ibc, |
||||
}); |
||||
} else { |
||||
throw new Error(`No adapter found for token standard: ${standard}`); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Returns a HypTokenAdapter for the token and multiProvider |
||||
* @throws If not applicable to this token's standard. |
||||
* @throws If multiProvider does not contain this token's chain. |
||||
* @throws If token is an NFT (TODO NFT Adapter support) |
||||
*/ |
||||
getHypAdapter( |
||||
multiProvider: MultiProtocolProvider<{ mailbox?: Address }>, |
||||
destination?: ChainName, |
||||
): IHypTokenAdapter<unknown> { |
||||
const { |
||||
protocol, |
||||
standard, |
||||
chainName, |
||||
addressOrDenom, |
||||
collateralAddressOrDenom, |
||||
} = this; |
||||
const chainMetadata = multiProvider.tryGetChainMetadata(chainName); |
||||
const mailbox = chainMetadata?.mailbox; |
||||
|
||||
assert( |
||||
this.isMultiChainToken(), |
||||
`Token standard ${standard} not applicable to hyp adapter`, |
||||
); |
||||
assert(!this.isNft(), 'NFT adapters not yet supported'); |
||||
assert( |
||||
chainMetadata, |
||||
`Token chain ${chainName} not found in multiProvider`, |
||||
); |
||||
|
||||
let sealevelAddresses; |
||||
if (protocol === ProtocolType.Sealevel) { |
||||
assert(mailbox, `Mailbox required for Sealevel hyp tokens`); |
||||
assert( |
||||
collateralAddressOrDenom, |
||||
`collateralAddressOrDenom required for Sealevel hyp tokens`, |
||||
); |
||||
sealevelAddresses = { |
||||
warpRouter: addressOrDenom, |
||||
token: collateralAddressOrDenom, |
||||
mailbox, |
||||
}; |
||||
} |
||||
if (standard === TokenStandard.EvmHypNative) { |
||||
return new EvmHypNativeAdapter(chainName, multiProvider, { |
||||
token: addressOrDenom, |
||||
}); |
||||
} else if (standard === TokenStandard.EvmHypCollateral) { |
||||
return new EvmHypCollateralAdapter(chainName, multiProvider, { |
||||
token: addressOrDenom, |
||||
}); |
||||
} else if (standard === TokenStandard.EvmHypSynthetic) { |
||||
return new EvmHypSyntheticAdapter(chainName, multiProvider, { |
||||
token: addressOrDenom, |
||||
}); |
||||
} else if (standard === TokenStandard.SealevelHypNative) { |
||||
return new SealevelHypNativeAdapter( |
||||
chainName, |
||||
multiProvider, |
||||
sealevelAddresses!, |
||||
false, |
||||
); |
||||
} else if (standard === TokenStandard.SealevelHypCollateral) { |
||||
return new SealevelHypCollateralAdapter( |
||||
chainName, |
||||
multiProvider, |
||||
sealevelAddresses!, |
||||
false, |
||||
); |
||||
} else if (standard === TokenStandard.SealevelHypSynthetic) { |
||||
return new SealevelHypSyntheticAdapter( |
||||
chainName, |
||||
multiProvider, |
||||
sealevelAddresses!, |
||||
false, |
||||
); |
||||
} else if (standard === TokenStandard.CwHypNative) { |
||||
return new CwHypNativeAdapter(chainName, multiProvider, { |
||||
warpRouter: addressOrDenom, |
||||
}); |
||||
} else if (standard === TokenStandard.CwHypCollateral) { |
||||
assert( |
||||
collateralAddressOrDenom, |
||||
'collateralAddressOrDenom required for CwHypCollateral', |
||||
); |
||||
return new CwHypCollateralAdapter(chainName, multiProvider, { |
||||
warpRouter: addressOrDenom, |
||||
token: collateralAddressOrDenom, |
||||
}); |
||||
} else if (standard === TokenStandard.CwHypSynthetic) { |
||||
assert( |
||||
collateralAddressOrDenom, |
||||
'collateralAddressOrDenom required for CwHypSyntheticAdapter', |
||||
); |
||||
return new CwHypSyntheticAdapter(chainName, multiProvider, { |
||||
warpRouter: addressOrDenom, |
||||
token: collateralAddressOrDenom, |
||||
}); |
||||
} else if (standard === TokenStandard.CosmosIbc) { |
||||
assert(destination, 'destination required for IBC token adapters'); |
||||
const connection = this.getConnectionForChain(destination); |
||||
assert(connection, `No connection found for chain ${destination}`); |
||||
return this.getIbcAdapter(multiProvider, connection); |
||||
} else { |
||||
throw new Error(`No hyp adapter found for token standard: ${standard}`); |
||||
} |
||||
} |
||||
|
||||
protected getIbcAdapter( |
||||
multiProvider: MultiProtocolProvider, |
||||
connection: TokenConnection, |
||||
): IHypTokenAdapter<MsgTransferEncodeObject> { |
||||
if (connection.type === TokenConnectionType.Ibc) { |
||||
const { sourcePort, sourceChannel } = connection; |
||||
return new CosmIbcTokenAdapter( |
||||
this.chainName, |
||||
multiProvider, |
||||
{}, |
||||
{ ibcDenom: this.addressOrDenom, sourcePort, sourceChannel }, |
||||
); |
||||
} else if (connection.type === TokenConnectionType.IbcHyperlane) { |
||||
const { |
||||
sourcePort, |
||||
sourceChannel, |
||||
intermediateChainName, |
||||
intermediateIbcDenom, |
||||
intermediateRouterAddress, |
||||
} = connection; |
||||
const destinationRouterAddress = connection.token.addressOrDenom; |
||||
return new CosmIbcToWarpTokenAdapter( |
||||
this.chainName, |
||||
multiProvider, |
||||
{ |
||||
intermediateRouterAddress, |
||||
destinationRouterAddress, |
||||
}, |
||||
{ |
||||
ibcDenom: this.addressOrDenom, |
||||
sourcePort, |
||||
sourceChannel, |
||||
intermediateIbcDenom, |
||||
intermediateChainName, |
||||
}, |
||||
); |
||||
} else { |
||||
throw new Error(`Unsupported IBC connection type: ${connection.type}`); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 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 { |
||||
return new TokenAmount(amount, this); |
||||
} |
||||
|
||||
isNft(): boolean { |
||||
return TOKEN_NFT_STANDARDS.includes(this.standard); |
||||
} |
||||
|
||||
isNative(): boolean { |
||||
return Object.values(PROTOCOL_TO_NATIVE_STANDARD).includes(this.standard); |
||||
} |
||||
|
||||
isHypToken(): boolean { |
||||
return TOKEN_HYP_STANDARDS.includes(this.standard); |
||||
} |
||||
|
||||
isIbcToken(): boolean { |
||||
return this.standard === TokenStandard.CosmosIbc; |
||||
} |
||||
|
||||
isMultiChainToken(): boolean { |
||||
return TOKEN_MULTI_CHAIN_STANDARDS.includes(this.standard); |
||||
} |
||||
|
||||
getConnections(): TokenConnection[] { |
||||
return this.connections || []; |
||||
} |
||||
|
||||
getConnectionForChain(chain: ChainName): TokenConnection | undefined { |
||||
// A token cannot have > 1 connected token for the same chain
|
||||
return this.getConnections().filter((t) => t.token.chainName === chain)[0]; |
||||
} |
||||
|
||||
addConnection(connection: TokenConnection): Token { |
||||
this.connections = [...(this.connections || []), connection]; |
||||
return this; |
||||
} |
||||
|
||||
removeConnection(token: Token): Token { |
||||
const index = this.connections?.findIndex((t) => t.token.equals(token)); |
||||
if (index && index >= 0) this.connections?.splice(index, 1); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Returns true if tokens refer to the same asset |
||||
*/ |
||||
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.collateralAddressOrDenom?.toLowerCase() === |
||||
token.collateralAddressOrDenom?.toLowerCase() |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Checks if this token is both: |
||||
* 1) Of a TokenStandard that uses other tokens as collateral (eg. EvmHypCollateral) |
||||
* 2) Has a collateralAddressOrDenom address that matches the given token |
||||
* E.g. ERC20 Token ABC, EvmHypCollateral DEF that wraps ABC, DEF.collateralizes(ABC) === true |
||||
*/ |
||||
collateralizes(token: Token): boolean { |
||||
if (token.chainName !== this.chainName) return false; |
||||
if (!TOKEN_COLLATERALIZED_STANDARDS.includes(this.standard)) return false; |
||||
const isCollateralWrapper = |
||||
this.collateralAddressOrDenom && |
||||
eqAddress(this.collateralAddressOrDenom, token.addressOrDenom); |
||||
const isNativeWrapper = !this.collateralAddressOrDenom && token.isNative(); |
||||
return isCollateralWrapper || isNativeWrapper; |
||||
} |
||||
} |
@ -0,0 +1,48 @@ |
||||
import { expect } from 'chai'; |
||||
import { ethers } from 'ethers'; |
||||
|
||||
import { chainMetadata } from '../consts/chainMetadata'; |
||||
import { Chains } from '../consts/chains'; |
||||
|
||||
import { Token } from './Token'; |
||||
import { TokenAmount } from './TokenAmount'; |
||||
import { TokenStandard } from './TokenStandard'; |
||||
|
||||
const token1 = new Token({ |
||||
chainName: Chains.ethereum, |
||||
standard: TokenStandard.ERC20, |
||||
addressOrDenom: ethers.constants.AddressZero, |
||||
decimals: 4, |
||||
symbol: 'FAKE', |
||||
name: 'Fake Token', |
||||
}); |
||||
const token2 = Token.FromChainMetadataNativeToken( |
||||
chainMetadata[Chains.neutron], |
||||
); |
||||
|
||||
describe('TokenAmount', () => { |
||||
let tokenAmount1: TokenAmount; |
||||
let tokenAmount2: TokenAmount; |
||||
|
||||
it('Constructs', () => { |
||||
tokenAmount1 = new TokenAmount(123456789, token1); |
||||
tokenAmount2 = new TokenAmount('1', token2); |
||||
expect(!!tokenAmount1).to.eq(true); |
||||
expect(!!tokenAmount2).to.eq(true); |
||||
}); |
||||
|
||||
it('Formats human readable string', () => { |
||||
expect(tokenAmount1.getDecimalFormattedAmount()).to.eq(12345.6789); |
||||
expect(tokenAmount2.getDecimalFormattedAmount()).to.eq(0.000001); |
||||
}); |
||||
|
||||
it('Does arithmetic', () => { |
||||
expect(tokenAmount1.plus(1).amount).to.eq(123456790n); |
||||
expect(tokenAmount2.minus(1).amount).to.eq(0n); |
||||
}); |
||||
|
||||
it('Checks equality', () => { |
||||
expect(tokenAmount1.equals(tokenAmount2)).to.be.false; |
||||
expect(tokenAmount1.equals(new TokenAmount(123456789n, token1))).to.true; |
||||
}); |
||||
}); |
@ -0,0 +1,29 @@ |
||||
import { Numberish, fromWei } from '@hyperlane-xyz/utils'; |
||||
|
||||
import type { IToken } from './IToken'; |
||||
|
||||
export class TokenAmount { |
||||
public readonly amount: bigint; |
||||
|
||||
constructor(_amount: Numberish, public readonly token: IToken) { |
||||
this.amount = BigInt(_amount); |
||||
} |
||||
|
||||
getDecimalFormattedAmount(): number { |
||||
return Number(fromWei(this.amount.toString(), this.token.decimals)); |
||||
} |
||||
|
||||
plus(amount: Numberish): TokenAmount { |
||||
return new TokenAmount(this.amount + BigInt(amount), this.token); |
||||
} |
||||
|
||||
minus(amount: Numberish): TokenAmount { |
||||
return new TokenAmount(this.amount - BigInt(amount), this.token); |
||||
} |
||||
|
||||
equals(tokenAmount: TokenAmount): boolean { |
||||
return ( |
||||
this.amount === tokenAmount.amount && this.token.equals(tokenAmount.token) |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,73 @@ |
||||
import { z } from 'zod'; |
||||
|
||||
import { Address } from '@hyperlane-xyz/utils'; |
||||
|
||||
import { ZChainName } from '../metadata/customZodTypes'; |
||||
import { ChainName } from '../types'; |
||||
|
||||
import type { IToken } from './IToken'; |
||||
|
||||
export enum TokenConnectionType { |
||||
Hyperlane = 'hyperlane', |
||||
Ibc = 'ibc', |
||||
IbcHyperlane = 'ibc-hyperlane', // a.k.a. one-click two-hop
|
||||
} |
||||
|
||||
interface TokenConnectionBase { |
||||
type?: TokenConnectionType; |
||||
token: IToken; // the token that is being connected to
|
||||
} |
||||
|
||||
export interface HyperlaneTokenConnection extends TokenConnectionBase { |
||||
type?: TokenConnectionType.Hyperlane; |
||||
} |
||||
|
||||
export interface IbcTokenConnection extends TokenConnectionBase { |
||||
type: TokenConnectionType.Ibc; |
||||
sourcePort: string; |
||||
sourceChannel: string; |
||||
} |
||||
|
||||
export interface IbcToHyperlaneTokenConnection extends TokenConnectionBase { |
||||
type: TokenConnectionType.IbcHyperlane; |
||||
sourcePort: string; |
||||
sourceChannel: string; |
||||
intermediateChainName: ChainName; |
||||
intermediateIbcDenom: string; |
||||
intermediateRouterAddress: Address; |
||||
} |
||||
|
||||
export type TokenConnection = |
||||
| HyperlaneTokenConnection |
||||
| IbcTokenConnection |
||||
| IbcToHyperlaneTokenConnection; |
||||
|
||||
const TokenConnectionRegex = /^(.+)|(.+)|(.+)$/; |
||||
|
||||
// Distinct from type above in that it uses a
|
||||
// serialized representation of the tokens instead
|
||||
// of the possibly recursive Token references
|
||||
export const TokenConnectionConfigSchema = z |
||||
.object({ |
||||
type: z.literal(TokenConnectionType.Hyperlane).optional(), |
||||
token: z.string().regex(TokenConnectionRegex), |
||||
}) |
||||
.or( |
||||
z.object({ |
||||
type: z.literal(TokenConnectionType.Ibc), |
||||
token: z.string().regex(TokenConnectionRegex), |
||||
sourcePort: z.string(), |
||||
sourceChannel: z.string(), |
||||
}), |
||||
) |
||||
.or( |
||||
z.object({ |
||||
type: z.literal(TokenConnectionType.IbcHyperlane), |
||||
token: z.string().regex(TokenConnectionRegex), |
||||
sourcePort: z.string(), |
||||
sourceChannel: z.string(), |
||||
intermediateChainName: ZChainName, |
||||
intermediateIbcDenom: z.string(), |
||||
intermediateRouterAddress: z.string(), |
||||
}), |
||||
); |
@ -0,0 +1,137 @@ |
||||
import { ProtocolType, objMap } from '@hyperlane-xyz/utils'; |
||||
|
||||
import { |
||||
PROTOCOL_TO_DEFAULT_PROVIDER_TYPE, |
||||
ProviderType, |
||||
} from '../providers/ProviderType'; |
||||
|
||||
export enum TokenStandard { |
||||
// EVM
|
||||
ERC20 = 'ERC20', |
||||
ERC721 = 'ERC721', |
||||
EvmNative = 'EvmNative', |
||||
EvmHypNative = 'EvmHypNative', |
||||
EvmHypCollateral = 'EvmHypCollateral', |
||||
EvmHypSynthetic = 'EvmHypSynthetic', |
||||
|
||||
// Sealevel (Solana)
|
||||
SealevelSpl = 'SealevelSpl', |
||||
SealevelSpl2022 = 'SealevelSpl2022', |
||||
SealevelNative = 'SealevelNative', |
||||
SealevelHypNative = 'SealevelHypNative', |
||||
SealevelHypCollateral = 'SealevelHypCollateral', |
||||
SealevelHypSynthetic = 'SealevelHypSynthetic', |
||||
|
||||
// Cosmos
|
||||
CosmosIcs20 = 'CosmosIcs20', |
||||
CosmosIcs721 = 'CosmosIcs721', |
||||
CosmosNative = 'CosmosNative', |
||||
CosmosIbc = 'CosmosIbc', |
||||
|
||||
// CosmWasm
|
||||
CW20 = 'CW20', |
||||
CWNative = 'CWNative', |
||||
CW721 = 'CW721', |
||||
CwHypNative = 'CwHypNative', |
||||
CwHypCollateral = 'CwHypCollateral', |
||||
CwHypSynthetic = 'CwHypSynthetic', |
||||
|
||||
// Fuel (TODO)
|
||||
FuelNative = 'FuelNative', |
||||
} |
||||
|
||||
// Allows for omission of protocol field in token args
|
||||
export const TOKEN_STANDARD_TO_PROTOCOL: Record<TokenStandard, ProtocolType> = { |
||||
// EVM
|
||||
ERC20: ProtocolType.Ethereum, |
||||
ERC721: ProtocolType.Ethereum, |
||||
EvmNative: ProtocolType.Ethereum, |
||||
EvmHypNative: ProtocolType.Ethereum, |
||||
EvmHypCollateral: ProtocolType.Ethereum, |
||||
EvmHypSynthetic: ProtocolType.Ethereum, |
||||
|
||||
// Sealevel (Solana)
|
||||
SealevelSpl: ProtocolType.Sealevel, |
||||
SealevelSpl2022: ProtocolType.Sealevel, |
||||
SealevelNative: ProtocolType.Sealevel, |
||||
SealevelHypNative: ProtocolType.Sealevel, |
||||
SealevelHypCollateral: ProtocolType.Sealevel, |
||||
SealevelHypSynthetic: ProtocolType.Sealevel, |
||||
|
||||
// Cosmos
|
||||
CosmosIcs20: ProtocolType.Cosmos, |
||||
CosmosIcs721: ProtocolType.Cosmos, |
||||
CosmosNative: ProtocolType.Cosmos, |
||||
CosmosIbc: ProtocolType.Cosmos, |
||||
|
||||
// CosmWasm
|
||||
CW20: ProtocolType.Cosmos, |
||||
CWNative: ProtocolType.Cosmos, |
||||
CW721: ProtocolType.Cosmos, |
||||
CwHypNative: ProtocolType.Cosmos, |
||||
CwHypCollateral: ProtocolType.Cosmos, |
||||
CwHypSynthetic: ProtocolType.Cosmos, |
||||
|
||||
// Fuel (TODO)
|
||||
FuelNative: ProtocolType.Fuel, |
||||
}; |
||||
|
||||
export const TOKEN_STANDARD_TO_PROVIDER_TYPE: Record< |
||||
TokenStandard, |
||||
ProviderType |
||||
> = objMap(TOKEN_STANDARD_TO_PROTOCOL, (k, v) => { |
||||
if (k.startsWith('Cosmos')) return ProviderType.CosmJs; |
||||
return PROTOCOL_TO_DEFAULT_PROVIDER_TYPE[v]; |
||||
}); |
||||
|
||||
export const TOKEN_NFT_STANDARDS = [ |
||||
TokenStandard.ERC721, |
||||
TokenStandard.CosmosIcs721, |
||||
TokenStandard.CW721, |
||||
// TODO solana here
|
||||
]; |
||||
|
||||
export const TOKEN_COLLATERALIZED_STANDARDS = [ |
||||
TokenStandard.EvmHypCollateral, |
||||
TokenStandard.EvmHypNative, |
||||
TokenStandard.SealevelHypCollateral, |
||||
TokenStandard.SealevelHypNative, |
||||
TokenStandard.CwHypCollateral, |
||||
TokenStandard.CwHypNative, |
||||
]; |
||||
|
||||
export const TOKEN_HYP_STANDARDS = [ |
||||
TokenStandard.EvmHypNative, |
||||
TokenStandard.EvmHypCollateral, |
||||
TokenStandard.EvmHypSynthetic, |
||||
TokenStandard.SealevelHypNative, |
||||
TokenStandard.SealevelHypCollateral, |
||||
TokenStandard.SealevelHypSynthetic, |
||||
TokenStandard.CwHypNative, |
||||
TokenStandard.CwHypCollateral, |
||||
TokenStandard.CwHypSynthetic, |
||||
]; |
||||
|
||||
export const TOKEN_MULTI_CHAIN_STANDARDS = [ |
||||
...TOKEN_HYP_STANDARDS, |
||||
TokenStandard.CosmosIbc, |
||||
]; |
||||
|
||||
// Useful for differentiating from norma Cosmos standards
|
||||
// (e.g. for determining the appropriate cosmos client)
|
||||
export const TOKEN_COSMWASM_STANDARDS = [ |
||||
TokenStandard.CW20, |
||||
TokenStandard.CWNative, |
||||
TokenStandard.CW721, |
||||
TokenStandard.CwHypNative, |
||||
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, |
||||
}; |
@ -1,37 +1,42 @@ |
||||
import { Address, Domain } from '@hyperlane-xyz/utils'; |
||||
import { Address, Domain, Numberish } from '@hyperlane-xyz/utils'; |
||||
|
||||
import { MinimalTokenMetadata } from '../config'; |
||||
|
||||
export interface TransferParams { |
||||
weiAmountOrId: string | number; |
||||
weiAmountOrId: Numberish; |
||||
recipient: Address; |
||||
|
||||
// Solana-specific params
|
||||
// Included here optionally to keep Adapter types simple
|
||||
fromTokenAccount?: Address; |
||||
// Required for Cosmos + Solana
|
||||
fromAccountOwner?: Address; |
||||
// Required for Solana
|
||||
fromTokenAccount?: Address; |
||||
} |
||||
|
||||
export interface TransferRemoteParams extends TransferParams { |
||||
destination: Domain; |
||||
txValue?: string; |
||||
interchainGas?: InterchainGasQuote; |
||||
} |
||||
|
||||
export interface InterchainGasQuote { |
||||
addressOrDenom?: string; // undefined values represent default native tokens
|
||||
amount: bigint; |
||||
} |
||||
|
||||
export interface ITokenAdapter { |
||||
getBalance(address: Address): Promise<string>; |
||||
export interface ITokenAdapter<Tx> { |
||||
getBalance(address: Address): Promise<bigint>; |
||||
getMetadata(isNft?: boolean): Promise<MinimalTokenMetadata>; |
||||
populateApproveTx(TransferParams: TransferParams): unknown | Promise<unknown>; |
||||
populateTransferTx( |
||||
TransferParams: TransferParams, |
||||
): unknown | Promise<unknown>; |
||||
isApproveRequired( |
||||
owner: Address, |
||||
spender: Address, |
||||
weiAmountOrId: Numberish, |
||||
): Promise<boolean>; |
||||
populateApproveTx(params: TransferParams): Promise<Tx>; |
||||
populateTransferTx(params: TransferParams): Promise<Tx>; |
||||
} |
||||
|
||||
export interface IHypTokenAdapter extends ITokenAdapter { |
||||
export interface IHypTokenAdapter<Tx> extends ITokenAdapter<Tx> { |
||||
getDomains(): Promise<Domain[]>; |
||||
getRouterAddress(domain: Domain): Promise<Buffer>; |
||||
getAllRouters(): Promise<Array<{ domain: Domain; address: Buffer }>>; |
||||
quoteGasPayment(destination: Domain): Promise<string>; |
||||
populateTransferRemoteTx( |
||||
TransferParams: TransferRemoteParams, |
||||
): unknown | Promise<unknown>; |
||||
quoteGasPayment(destination: Domain): Promise<InterchainGasQuote>; |
||||
populateTransferRemoteTx(p: TransferRemoteParams): Promise<Tx>; |
||||
} |
||||
|
@ -0,0 +1,264 @@ |
||||
import { expect } from 'chai'; |
||||
import { ethers } from 'ethers'; |
||||
import fs from 'fs'; |
||||
import path from 'path'; |
||||
import sinon from 'sinon'; |
||||
import { parse as yamlParse } from 'yaml'; |
||||
|
||||
import { chainMetadata } from '../consts/chainMetadata'; |
||||
import { Chains } from '../consts/chains'; |
||||
import { MultiProtocolProvider } from '../providers/MultiProtocolProvider'; |
||||
import { ProviderType } from '../providers/ProviderType'; |
||||
import { Token } from '../token/Token'; |
||||
import { TokenStandard } from '../token/TokenStandard'; |
||||
import { InterchainGasQuote } from '../token/adapters/ITokenAdapter'; |
||||
import { ChainName } from '../types'; |
||||
|
||||
import { WarpCore } from './WarpCore'; |
||||
import { WarpTxCategory } from './types'; |
||||
|
||||
const MOCK_QUOTE = { amount: 20_000n }; |
||||
const TRANSFER_AMOUNT = BigInt('1000000000000000000'); // 1 units @ 18 decimals
|
||||
const BIG_TRANSFER_AMOUNT = BigInt('100000000000000000000'); // 100 units @ 18 decimals
|
||||
const MOCK_BALANCE = BigInt('10000000000000000000'); // 10 units @ 18 decimals
|
||||
|
||||
describe('WarpCore', () => { |
||||
const multiProvider = new MultiProtocolProvider(); |
||||
let warpCore: WarpCore; |
||||
let evmHypNative: Token; |
||||
let evmHypSynthetic: Token; |
||||
let sealevelHypSynthetic: Token; |
||||
let cwHypCollateral: Token; |
||||
let cw20: Token; |
||||
let cosmosIbc: Token; |
||||
|
||||
it('Constructs', () => { |
||||
const fromArgs = new WarpCore(multiProvider, [ |
||||
Token.FromChainMetadataNativeToken(chainMetadata[Chains.ethereum]), |
||||
]); |
||||
const exampleConfig = yamlParse( |
||||
fs.readFileSync( |
||||
path.join(__dirname, './example-warp-core-config.yaml'), |
||||
'utf-8', |
||||
), |
||||
); |
||||
const fromConfig = WarpCore.FromConfig(multiProvider, exampleConfig); |
||||
expect(fromArgs).to.be.instanceOf(WarpCore); |
||||
expect(fromConfig).to.be.instanceOf(WarpCore); |
||||
expect(fromConfig.tokens.length).to.equal(exampleConfig.tokens.length); |
||||
|
||||
warpCore = fromConfig; |
||||
[ |
||||
evmHypNative, |
||||
evmHypSynthetic, |
||||
sealevelHypSynthetic, |
||||
cwHypCollateral, |
||||
cw20, |
||||
cosmosIbc, |
||||
] = warpCore.tokens; |
||||
}); |
||||
|
||||
it('Finds tokens', () => { |
||||
expect( |
||||
warpCore.findToken(Chains.ethereum, evmHypNative.addressOrDenom), |
||||
).to.be.instanceOf(Token); |
||||
expect( |
||||
warpCore.findToken(Chains.ethereum, sealevelHypSynthetic.addressOrDenom), |
||||
).to.be.null; |
||||
expect( |
||||
warpCore.findToken(Chains.neutron, cw20.addressOrDenom), |
||||
).to.be.instanceOf(Token); |
||||
}); |
||||
|
||||
it('Gets transfer gas quote', async () => { |
||||
const stubs = warpCore.tokens.map((t) => |
||||
sinon.stub(t, 'getHypAdapter').returns({ |
||||
quoteGasPayment: () => Promise.resolve(MOCK_QUOTE), |
||||
} as any), |
||||
); |
||||
|
||||
const testQuote = async ( |
||||
token: Token, |
||||
chain: ChainName, |
||||
standard: TokenStandard, |
||||
quote: InterchainGasQuote = MOCK_QUOTE, |
||||
) => { |
||||
const result = await warpCore.getTransferGasQuote(token, chain); |
||||
expect( |
||||
result.token.standard, |
||||
`token standard check for ${token.chainName} to ${chain}`, |
||||
).equals(standard); |
||||
expect( |
||||
result.amount, |
||||
`token amount check for ${token.chainName} to ${chain}`, |
||||
).to.equal(quote.amount); |
||||
}; |
||||
|
||||
await testQuote(evmHypNative, Chains.arbitrum, TokenStandard.EvmNative); |
||||
await testQuote(evmHypNative, Chains.neutron, TokenStandard.EvmNative); |
||||
await testQuote(evmHypNative, Chains.solana, TokenStandard.EvmNative); |
||||
await testQuote(evmHypSynthetic, Chains.ethereum, TokenStandard.EvmNative); |
||||
await testQuote( |
||||
sealevelHypSynthetic, |
||||
Chains.ethereum, |
||||
TokenStandard.SealevelNative, |
||||
); |
||||
await testQuote(cosmosIbc, Chains.arbitrum, TokenStandard.CosmosNative); |
||||
// Note, this route uses an igp quote const config
|
||||
await testQuote( |
||||
cwHypCollateral, |
||||
Chains.arbitrum, |
||||
TokenStandard.CosmosNative, |
||||
{ |
||||
amount: 1n, |
||||
addressOrDenom: 'untrn', |
||||
}, |
||||
); |
||||
|
||||
stubs.forEach((s) => s.restore()); |
||||
}); |
||||
|
||||
it('Checks for destination collateral', async () => { |
||||
const stubs = warpCore.tokens.map((t) => |
||||
sinon.stub(t, 'getHypAdapter').returns({ |
||||
getBalance: () => Promise.resolve(MOCK_BALANCE), |
||||
} as any), |
||||
); |
||||
|
||||
const testCollateral = async ( |
||||
token: Token, |
||||
chain: ChainName, |
||||
expectedBigResult = true, |
||||
) => { |
||||
const smallResult = await warpCore.isDestinationCollateralSufficient( |
||||
token.amount(TRANSFER_AMOUNT), |
||||
chain, |
||||
); |
||||
expect( |
||||
smallResult, |
||||
`small collateral check for ${token.chainName} to ${chain}`, |
||||
).to.be.true; |
||||
const bigResult = await warpCore.isDestinationCollateralSufficient( |
||||
token.amount(BIG_TRANSFER_AMOUNT), |
||||
chain, |
||||
); |
||||
expect( |
||||
bigResult, |
||||
`big collateral check for ${token.chainName} to ${chain}`, |
||||
).to.equal(expectedBigResult); |
||||
}; |
||||
|
||||
await testCollateral(evmHypNative, Chains.arbitrum); |
||||
await testCollateral(evmHypNative, Chains.neutron, false); |
||||
await testCollateral(evmHypNative, Chains.solana); |
||||
await testCollateral(cwHypCollateral, Chains.arbitrum); |
||||
|
||||
stubs.forEach((s) => s.restore()); |
||||
}); |
||||
|
||||
it('Validates transfers', async () => { |
||||
const balanceStubs = warpCore.tokens.map((t) => |
||||
sinon |
||||
.stub(t, 'getBalance') |
||||
.returns(Promise.resolve({ amount: MOCK_BALANCE } as any)), |
||||
); |
||||
const quoteStubs = warpCore.tokens.map((t) => |
||||
sinon.stub(t, 'getHypAdapter').returns({ |
||||
quoteGasPayment: () => Promise.resolve(MOCK_QUOTE), |
||||
} as any), |
||||
); |
||||
|
||||
const validResult = await warpCore.validateTransfer( |
||||
evmHypNative.amount(TRANSFER_AMOUNT), |
||||
Chains.arbitrum, |
||||
ethers.constants.AddressZero, |
||||
ethers.constants.AddressZero, |
||||
); |
||||
expect(validResult).to.be.null; |
||||
|
||||
const invalidChain = await warpCore.validateTransfer( |
||||
evmHypNative.amount(TRANSFER_AMOUNT), |
||||
'fakechain', |
||||
ethers.constants.AddressZero, |
||||
ethers.constants.AddressZero, |
||||
); |
||||
expect(Object.keys(invalidChain || {})[0]).to.equal('destination'); |
||||
|
||||
const invalidRecipient = await warpCore.validateTransfer( |
||||
evmHypNative.amount(TRANSFER_AMOUNT), |
||||
Chains.neutron, |
||||
ethers.constants.AddressZero, |
||||
ethers.constants.AddressZero, |
||||
); |
||||
expect(Object.keys(invalidRecipient || {})[0]).to.equal('recipient'); |
||||
|
||||
const invalidAmount = await warpCore.validateTransfer( |
||||
evmHypNative.amount(-10), |
||||
Chains.arbitrum, |
||||
ethers.constants.AddressZero, |
||||
ethers.constants.AddressZero, |
||||
); |
||||
expect(Object.keys(invalidAmount || {})[0]).to.equal('amount'); |
||||
|
||||
const insufficientBalance = await warpCore.validateTransfer( |
||||
evmHypNative.amount(BIG_TRANSFER_AMOUNT), |
||||
Chains.arbitrum, |
||||
ethers.constants.AddressZero, |
||||
ethers.constants.AddressZero, |
||||
); |
||||
expect(Object.keys(insufficientBalance || {})[0]).to.equal('amount'); |
||||
|
||||
balanceStubs.forEach((s) => s.restore()); |
||||
quoteStubs.forEach((s) => s.restore()); |
||||
}); |
||||
|
||||
it('Gets transfer remote txs', async () => { |
||||
const coreStub = sinon |
||||
.stub(warpCore, 'isApproveRequired') |
||||
.returns(Promise.resolve(false)); |
||||
|
||||
const adapterStubs = warpCore.tokens.map((t) => |
||||
sinon.stub(t, 'getHypAdapter').returns({ |
||||
quoteGasPayment: () => Promise.resolve(MOCK_QUOTE), |
||||
populateTransferRemoteTx: () => Promise.resolve({}), |
||||
} as any), |
||||
); |
||||
|
||||
const testGetTxs = async ( |
||||
token: Token, |
||||
chain: ChainName, |
||||
providerType = ProviderType.EthersV5, |
||||
) => { |
||||
const result = await warpCore.getTransferRemoteTxs( |
||||
token.amount(TRANSFER_AMOUNT), |
||||
chain, |
||||
ethers.constants.AddressZero, |
||||
ethers.constants.AddressZero, |
||||
); |
||||
expect(result.length).to.equal(1); |
||||
expect( |
||||
result[0], |
||||
`transfer tx for ${token.chainName} to ${chain}`, |
||||
).to.eql({ |
||||
category: WarpTxCategory.Transfer, |
||||
transaction: {}, |
||||
type: providerType, |
||||
}); |
||||
}; |
||||
|
||||
await testGetTxs(evmHypNative, Chains.arbitrum); |
||||
await testGetTxs(evmHypNative, Chains.neutron); |
||||
await testGetTxs(evmHypNative, Chains.solana); |
||||
await testGetTxs(evmHypSynthetic, Chains.ethereum); |
||||
await testGetTxs( |
||||
sealevelHypSynthetic, |
||||
Chains.ethereum, |
||||
ProviderType.SolanaWeb3, |
||||
); |
||||
await testGetTxs(cwHypCollateral, Chains.arbitrum, ProviderType.CosmJsWasm); |
||||
await testGetTxs(cosmosIbc, Chains.arbitrum, ProviderType.CosmJs); |
||||
|
||||
coreStub.restore(); |
||||
adapterStubs.forEach((s) => s.restore()); |
||||
}); |
||||
}); |
@ -0,0 +1,445 @@ |
||||
import debug, { Debugger } from 'debug'; |
||||
|
||||
import { |
||||
Address, |
||||
ProtocolType, |
||||
assert, |
||||
convertDecimals, |
||||
isValidAddress, |
||||
} from '@hyperlane-xyz/utils'; |
||||
|
||||
import { MultiProtocolProvider } from '../providers/MultiProtocolProvider'; |
||||
import { IToken } from '../token/IToken'; |
||||
import { Token } from '../token/Token'; |
||||
import { TokenAmount } from '../token/TokenAmount'; |
||||
import { |
||||
TOKEN_COLLATERALIZED_STANDARDS, |
||||
TOKEN_STANDARD_TO_PROVIDER_TYPE, |
||||
} from '../token/TokenStandard'; |
||||
import { ChainName, ChainNameOrId } from '../types'; |
||||
|
||||
import { |
||||
IgpQuoteConstants, |
||||
RouteBlacklist, |
||||
WarpCoreConfigSchema, |
||||
WarpTxCategory, |
||||
WarpTypedTransaction, |
||||
} from './types'; |
||||
|
||||
export interface WarpCoreOptions { |
||||
loggerName?: string; |
||||
igpQuoteConstants?: IgpQuoteConstants; |
||||
routeBlacklist?: RouteBlacklist; |
||||
} |
||||
|
||||
export class WarpCore { |
||||
public readonly multiProvider: MultiProtocolProvider<{ mailbox?: Address }>; |
||||
public readonly tokens: Token[]; |
||||
public readonly igpQuoteConstants: IgpQuoteConstants; |
||||
public readonly routeBlacklist: RouteBlacklist; |
||||
public readonly logger: Debugger; |
||||
|
||||
constructor( |
||||
multiProvider: MultiProtocolProvider<{ mailbox?: Address }>, |
||||
tokens: Token[], |
||||
options?: WarpCoreOptions, |
||||
) { |
||||
this.multiProvider = multiProvider; |
||||
this.tokens = tokens; |
||||
this.igpQuoteConstants = options?.igpQuoteConstants || []; |
||||
this.routeBlacklist = options?.routeBlacklist || []; |
||||
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: unknown, |
||||
): WarpCore { |
||||
// Validate and parse config data
|
||||
const parsedConfig = WarpCoreConfigSchema.parse(config); |
||||
// Instantiate all tokens
|
||||
const tokens = parsedConfig.tokens.map( |
||||
(t) => |
||||
new Token({ |
||||
...t, |
||||
addressOrDenom: t.addressOrDenom || '', |
||||
connections: undefined, |
||||
}), |
||||
); |
||||
// Connect tokens together
|
||||
parsedConfig.tokens.forEach((config, i) => { |
||||
for (const connection of config.connections || []) { |
||||
const token1 = tokens[i]; |
||||
// TODO see https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/3298
|
||||
const [_protocol, chainName, addrOrDenom] = connection.token.split('|'); |
||||
const token2 = tokens.find( |
||||
(t) => t.chainName === chainName && t.addressOrDenom === addrOrDenom, |
||||
); |
||||
assert( |
||||
token2, |
||||
`Connected token not found: ${chainName} ${addrOrDenom}`, |
||||
); |
||||
token1.addConnection({ |
||||
...connection, |
||||
token: token2, |
||||
}); |
||||
} |
||||
}); |
||||
// Create new Warp
|
||||
return new WarpCore(multiProvider, tokens, { |
||||
igpQuoteConstants: parsedConfig.options?.igpQuoteConstants, |
||||
routeBlacklist: parsedConfig.options?.routeBlacklist, |
||||
}); |
||||
} |
||||
|
||||
async getTransferGasQuote( |
||||
originToken: IToken, |
||||
destination: ChainNameOrId, |
||||
): Promise<TokenAmount> { |
||||
const { chainName: originName } = originToken; |
||||
const destinationName = this.multiProvider.getChainName(destination); |
||||
|
||||
let gasAmount: bigint; |
||||
let gasAddressOrDenom: string | undefined; |
||||
// Check constant quotes first
|
||||
const defaultQuote = this.igpQuoteConstants.find( |
||||
(q) => q.origin === originName && q.destination === destinationName, |
||||
); |
||||
if (defaultQuote) { |
||||
gasAmount = BigInt(defaultQuote.amount.toString()); |
||||
gasAddressOrDenom = defaultQuote.addressOrDenom; |
||||
} else { |
||||
// Otherwise, compute IGP quote via the adapter
|
||||
const hypAdapter = originToken.getHypAdapter( |
||||
this.multiProvider, |
||||
destinationName, |
||||
); |
||||
const destinationDomainId = this.multiProvider.getDomainId(destination); |
||||
const quote = await hypAdapter.quoteGasPayment(destinationDomainId); |
||||
gasAmount = BigInt(quote.amount); |
||||
gasAddressOrDenom = quote.addressOrDenom; |
||||
} |
||||
|
||||
let igpToken: Token; |
||||
if (!gasAddressOrDenom) { |
||||
// An empty/undefined addressOrDenom indicates the native token
|
||||
igpToken = Token.FromChainMetadataNativeToken( |
||||
this.multiProvider.getChainMetadata(originName), |
||||
); |
||||
} else { |
||||
const searchResult = this.findToken(originName, gasAddressOrDenom); |
||||
assert(searchResult, `IGP token ${gasAddressOrDenom} is unknown`); |
||||
igpToken = searchResult; |
||||
} |
||||
|
||||
this.logger(`Quoted igp gas payment: ${gasAmount} ${igpToken.symbol}`); |
||||
return new TokenAmount(gasAmount, igpToken); |
||||
} |
||||
|
||||
async getTransferRemoteTxs( |
||||
originTokenAmount: TokenAmount, |
||||
destination: ChainNameOrId, |
||||
sender: Address, |
||||
recipient: Address, |
||||
): Promise<Array<WarpTypedTransaction>> { |
||||
const transactions: Array<WarpTypedTransaction> = []; |
||||
|
||||
const { token, amount } = originTokenAmount; |
||||
const destinationName = this.multiProvider.getChainName(destination); |
||||
const destinationDomainId = this.multiProvider.getDomainId(destination); |
||||
const providerType = TOKEN_STANDARD_TO_PROVIDER_TYPE[token.standard]; |
||||
const hypAdapter = token.getHypAdapter(this.multiProvider, destinationName); |
||||
|
||||
if (await this.isApproveRequired(originTokenAmount, sender)) { |
||||
this.logger(`Approval required for transfer of ${token.symbol}`); |
||||
const approveTxReq = await hypAdapter.populateApproveTx({ |
||||
weiAmountOrId: amount.toString(), |
||||
recipient: token.addressOrDenom, |
||||
}); |
||||
this.logger(`Approval tx for ${token.symbol} populated`); |
||||
|
||||
const approveTx = { |
||||
category: WarpTxCategory.Approval, |
||||
type: providerType, |
||||
transaction: approveTxReq, |
||||
} as WarpTypedTransaction; |
||||
transactions.push(approveTx); |
||||
} |
||||
|
||||
const interchainGasAmount = await this.getTransferGasQuote( |
||||
token, |
||||
destination, |
||||
); |
||||
|
||||
const transferTxReq = await hypAdapter.populateTransferRemoteTx({ |
||||
weiAmountOrId: amount.toString(), |
||||
destination: destinationDomainId, |
||||
fromAccountOwner: sender, |
||||
recipient, |
||||
interchainGas: { |
||||
amount: interchainGasAmount.amount, |
||||
addressOrDenom: interchainGasAmount.token.addressOrDenom, |
||||
}, |
||||
}); |
||||
this.logger(`Remote transfer tx for ${token.symbol} populated`); |
||||
|
||||
const transferTx = { |
||||
category: WarpTxCategory.Transfer, |
||||
type: providerType, |
||||
transaction: transferTxReq, |
||||
} as WarpTypedTransaction; |
||||
transactions.push(transferTx); |
||||
|
||||
return transactions; |
||||
} |
||||
|
||||
/** |
||||
* Checks if destination chain's collateral is sufficient to cover the transfer |
||||
*/ |
||||
async isDestinationCollateralSufficient( |
||||
originTokenAmount: TokenAmount, |
||||
destination: ChainNameOrId, |
||||
): Promise<boolean> { |
||||
const { token: originToken, amount } = originTokenAmount; |
||||
const destinationName = this.multiProvider.getChainName(destination); |
||||
this.logger( |
||||
`Checking collateral for ${originToken.symbol} to ${destination}`, |
||||
); |
||||
|
||||
const destinationToken = |
||||
originToken.getConnectionForChain(destinationName)?.token; |
||||
assert(destinationToken, `No connection found for ${destinationName}`); |
||||
|
||||
if (!TOKEN_COLLATERALIZED_STANDARDS.includes(destinationToken.standard)) { |
||||
this.logger(`${destinationToken.symbol} is not collateralized, skipping`); |
||||
return true; |
||||
} |
||||
|
||||
const adapter = destinationToken.getAdapter(this.multiProvider); |
||||
const destinationBalance = await adapter.getBalance( |
||||
destinationToken.addressOrDenom, |
||||
); |
||||
const destinationBalanceInOriginDecimals = convertDecimals( |
||||
destinationToken.decimals, |
||||
originToken.decimals, |
||||
destinationBalance.toString(), |
||||
); |
||||
|
||||
const isSufficient = BigInt(destinationBalanceInOriginDecimals) >= amount; |
||||
this.logger( |
||||
`${originTokenAmount.token.symbol} to ${destination} has ${ |
||||
isSufficient ? 'sufficient' : 'INSUFFICIENT' |
||||
} collateral`,
|
||||
); |
||||
return isSufficient; |
||||
} |
||||
|
||||
/** |
||||
* Checks if a token transfer requires an approval tx first |
||||
*/ |
||||
async isApproveRequired( |
||||
originTokenAmount: TokenAmount, |
||||
owner: Address, |
||||
): Promise<boolean> { |
||||
const { token, amount } = originTokenAmount; |
||||
const adapter = token.getAdapter(this.multiProvider); |
||||
const isRequired = await adapter.isApproveRequired( |
||||
owner, |
||||
token.addressOrDenom, |
||||
amount, |
||||
); |
||||
this.logger( |
||||
`Approval is${isRequired ? '' : ' not'} required for transfer of ${ |
||||
token.symbol |
||||
}`,
|
||||
); |
||||
return isRequired; |
||||
} |
||||
|
||||
/** |
||||
* Ensure the remote token transfer would be valid for the given chains, amount, sender, and recipient |
||||
*/ |
||||
async validateTransfer( |
||||
originTokenAmount: TokenAmount, |
||||
destination: ChainNameOrId, |
||||
sender: Address, |
||||
recipient: Address, |
||||
): Promise<Record<string, string> | null> { |
||||
const chainError = this.validateChains( |
||||
originTokenAmount.token.chainName, |
||||
destination, |
||||
); |
||||
if (chainError) return chainError; |
||||
|
||||
const recipientError = this.validateRecipient(recipient, destination); |
||||
if (recipientError) return recipientError; |
||||
|
||||
const amountError = this.validateAmount(originTokenAmount); |
||||
if (amountError) return amountError; |
||||
|
||||
const balancesError = await this.validateTokenBalances( |
||||
originTokenAmount, |
||||
destination, |
||||
sender, |
||||
); |
||||
if (balancesError) return balancesError; |
||||
|
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* Ensure the origin and destination chains are valid and known by this WarpCore |
||||
*/ |
||||
protected validateChains( |
||||
origin: ChainNameOrId, |
||||
destination: ChainNameOrId, |
||||
): Record<string, string> | null { |
||||
if (!origin) return { origin: 'Origin chain required' }; |
||||
if (!destination) return { destination: 'Destination chain required' }; |
||||
const originMetadata = this.multiProvider.tryGetChainMetadata(origin); |
||||
const destinationMetadata = |
||||
this.multiProvider.tryGetChainMetadata(destination); |
||||
if (!originMetadata) return { origin: 'Origin chain metadata missing' }; |
||||
if (!destinationMetadata) |
||||
return { destination: 'Destination chain metadata missing' }; |
||||
if ( |
||||
this.routeBlacklist.some( |
||||
(bl) => |
||||
bl.origin === originMetadata.name && |
||||
bl.destination === destinationMetadata.name, |
||||
) |
||||
) { |
||||
return { destination: 'Route is not currently allowed' }; |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* Ensure recipient address is valid for the destination chain |
||||
*/ |
||||
protected validateRecipient( |
||||
recipient: Address, |
||||
destination: ChainNameOrId, |
||||
): Record<string, string> | null { |
||||
const destinationMetadata = |
||||
this.multiProvider.getChainMetadata(destination); |
||||
const { protocol, bech32Prefix } = destinationMetadata; |
||||
// Ensure recip address is valid for the destination chain's protocol
|
||||
if (!isValidAddress(recipient, protocol)) |
||||
return { recipient: 'Invalid recipient' }; |
||||
// Also ensure the address denom is correct if the dest protocol is Cosmos
|
||||
if (protocol === ProtocolType.Cosmos) { |
||||
if (!bech32Prefix) { |
||||
this.logger(`No bech32 prefix found for chain ${destination}`); |
||||
return { destination: 'Invalid chain data' }; |
||||
} else if (!recipient.startsWith(bech32Prefix)) { |
||||
this.logger(`Recipient prefix should be ${bech32Prefix}`); |
||||
return { recipient: `Invalid recipient prefix` }; |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* Ensure token amount is valid |
||||
*/ |
||||
protected validateAmount( |
||||
originTokenAmount: TokenAmount, |
||||
): Record<string, string> | null { |
||||
if (!originTokenAmount.amount || originTokenAmount.amount < 0n) { |
||||
const isNft = originTokenAmount.token.isNft(); |
||||
return { amount: isNft ? 'Invalid Token Id' : 'Invalid amount' }; |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* Ensure the sender has sufficient balances for transfer and interchain gas |
||||
*/ |
||||
protected async validateTokenBalances( |
||||
originTokenAmount: TokenAmount, |
||||
destination: ChainNameOrId, |
||||
sender: Address, |
||||
): Promise<Record<string, string> | null> { |
||||
const { token, amount } = originTokenAmount; |
||||
const { amount: senderBalance } = await token.getBalance( |
||||
this.multiProvider, |
||||
sender, |
||||
); |
||||
|
||||
// First check basic token balance
|
||||
if (amount > senderBalance) return { amount: 'Insufficient balance' }; |
||||
|
||||
// Next, ensure balances can cover IGP fees
|
||||
const igpQuote = await this.getTransferGasQuote(token, destination); |
||||
if (token.equals(igpQuote.token) || token.collateralizes(igpQuote.token)) { |
||||
const total = amount + igpQuote.amount; |
||||
if (senderBalance < total) |
||||
return { amount: 'Insufficient balance for gas and transfer' }; |
||||
} else { |
||||
const igpTokenBalance = await igpQuote.token.getBalance( |
||||
this.multiProvider, |
||||
sender, |
||||
); |
||||
if (igpTokenBalance.amount < igpQuote.amount) |
||||
return { amount: `Insufficient ${igpQuote.token.symbol} for gas` }; |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* Search through token list to find token with matching chain and address |
||||
*/ |
||||
findToken( |
||||
chainName: ChainName, |
||||
addressOrDenom?: Address | string, |
||||
): Token | null { |
||||
if (!addressOrDenom) return null; |
||||
|
||||
const results = this.tokens.filter( |
||||
(token) => |
||||
token.chainName === chainName && |
||||
token.addressOrDenom.toLowerCase() === addressOrDenom.toLowerCase(), |
||||
); |
||||
|
||||
if (results.length === 1) return results[0]; |
||||
|
||||
if (results.length > 1) |
||||
throw new Error(`Ambiguous token search results for ${addressOrDenom}`); |
||||
|
||||
// If the token is not found, check to see if it matches the denom of chain's native token
|
||||
// This is a convenience so WarpConfigs don't need to include definitions for native tokens
|
||||
const chainMetadata = this.multiProvider.getChainMetadata(chainName); |
||||
if (chainMetadata.nativeToken?.denom === addressOrDenom) { |
||||
return Token.FromChainMetadataNativeToken(chainMetadata); |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* Get the list of chains referenced by the tokens in this WarpCore |
||||
*/ |
||||
getTokenChains(): ChainName[] { |
||||
return [...new Set(this.tokens.map((t) => t.chainName)).values()]; |
||||
} |
||||
|
||||
/** |
||||
* Get the subset of tokens whose chain matches the given chainName |
||||
*/ |
||||
getTokensForChain(chainName: ChainName): Token[] { |
||||
return this.tokens.filter((t) => t.chainName === chainName); |
||||
} |
||||
|
||||
/** |
||||
* Get the subset of tokens whose chain matches the given chainName |
||||
* and which are connected to a token on the given destination chain |
||||
*/ |
||||
getTokensForRoute(origin: ChainName, destination: ChainName): Token[] { |
||||
return this.tokens.filter( |
||||
(t) => t.chainName === origin && t.getConnectionForChain(destination), |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,73 @@ |
||||
# An example Warp Core config |
||||
# Contains the token + route data needed to create a Warp Core |
||||
--- |
||||
tokens: |
||||
# Eth Mainnet HypNative token |
||||
- chainName: ethereum |
||||
standard: EvmHypNative |
||||
decimals: 18 |
||||
symbol: ETH |
||||
name: Ether |
||||
addressOrDenom: '0x1234567890123456789012345678901234567890' |
||||
connections: |
||||
- { token: ethereum|arbitrum|0x9876543210987654321098765432109876543210 } |
||||
- { token: cosmos|neutron|neutron1abcdefghijklmnopqrstuvwxyz1234567890ab } |
||||
- { token: sealevel|solana|s0LaBcEeFgHiJkLmNoPqRsTuVwXyZ456789012345678 } |
||||
# Arbitrum HypSynthetic token |
||||
- chainName: arbitrum |
||||
standard: EvmHypSynthetic |
||||
decimals: 18 |
||||
symbol: ETH |
||||
name: Ether |
||||
addressOrDenom: '0x9876543210987654321098765432109876543210' |
||||
connections: |
||||
- { token: ethereum|ethereum|0x1234567890123456789012345678901234567890 } |
||||
- { token: cosmos|neutron|neutron1abcdefghijklmnopqrstuvwxyz1234567890ab } |
||||
# Solana HypSynthetic |
||||
- chainName: solana |
||||
standard: SealevelHypSynthetic |
||||
decimals: 9 |
||||
symbol: ETH.sol |
||||
name: Ether on Solana |
||||
addressOrDenom: s0LaBcEeFgHiJkLmNoPqRsTuVwXyZ456789012345678 |
||||
connections: |
||||
- { token: ethereum|ethereum|0x1234567890123456789012345678901234567890 } |
||||
# Cosmos Neutron HypCollateral token |
||||
- chainName: neutron |
||||
standard: CwHypCollateral |
||||
decimals: 18 |
||||
symbol: ETH.in |
||||
name: Ether on Neutron |
||||
addressOrDenom: neutron1abcdefghijklmnopqrstuvwxyz1234567890ab |
||||
collateralAddressOrDenom: neutron1c0ll4t3r4lc0ll4t3r4lc0ll4t3r4lc0ll4t3r |
||||
connections: |
||||
- { token: ethereum|ethereum|0x1234567890123456789012345678901234567890 } |
||||
- { token: ethereum|arbitrum|0x9876543210987654321098765432109876543210 } |
||||
# Cosmos Neutron Collateralized token |
||||
- chainName: neutron |
||||
standard: CW20 |
||||
decimals: 18 |
||||
symbol: ETH.in |
||||
name: Ether on Neutron |
||||
addressOrDenom: neutron1c0ll4t3r4lc0ll4t3r4lc0ll4t3r4lc0ll4t3r |
||||
# Cosmos Injective token with IBC two-hop |
||||
- chainName: injective |
||||
standard: CosmosIbc |
||||
decimals: 18 |
||||
symbol: INJ |
||||
name: Injective |
||||
addressOrDenom: inj |
||||
connections: |
||||
- token: ethereum|arbitrum|0x9876543210987654321098765432109876543210 |
||||
type: ibc |
||||
sourcePort: transfer |
||||
sourceChannel: channel-1 |
||||
intermediateChainName: neutron |
||||
intermediateIbcDenom: untrn |
||||
intermediateRouterAddress: neutron1abcdefghijklmnopqrstuvwxyz1234567890ab |
||||
options: |
||||
igpQuoteConstants: |
||||
- origin: neutron |
||||
destination: arbitrum |
||||
amount: 1 |
||||
addressOrDenom: untrn |
@ -0,0 +1,62 @@ |
||||
import { z } from 'zod'; |
||||
|
||||
import { ZChainName } from '../metadata/customZodTypes'; |
||||
import { TypedTransaction } from '../providers/ProviderType'; |
||||
import { TokenConfigSchema } from '../token/IToken'; |
||||
import { ChainName } from '../types'; |
||||
|
||||
// Map of protocol to either quote constant or to a map of chain name to quote constant
|
||||
export type IgpQuoteConstants = Array<{ |
||||
origin: ChainName; |
||||
destination: ChainName; |
||||
amount: string | number | bigint; |
||||
addressOrDenom?: string; |
||||
}>; |
||||
|
||||
// List of chain pairs to blacklist for warp routes
|
||||
export type RouteBlacklist = Array<{ |
||||
origin: ChainName; |
||||
destination: ChainName; |
||||
}>; |
||||
|
||||
// Transaction types for warp core remote transfers
|
||||
export enum WarpTxCategory { |
||||
Approval = 'approval', |
||||
Transfer = 'transfer', |
||||
} |
||||
|
||||
export type WarpTypedTransaction = TypedTransaction & { |
||||
category: WarpTxCategory; |
||||
}; |
||||
|
||||
/** |
||||
* Configuration used for instantiating a WarpCore |
||||
* Contains the relevant tokens and their connections |
||||
*/ |
||||
export const WarpCoreConfigSchema = z.object({ |
||||
tokens: z.array(TokenConfigSchema), |
||||
options: z |
||||
.object({ |
||||
igpQuoteConstants: z |
||||
.array( |
||||
z.object({ |
||||
origin: ZChainName, |
||||
destination: ZChainName, |
||||
amount: z.union([z.string(), z.number(), z.bigint()]), |
||||
addressOrDenom: z.string().optional(), |
||||
}), |
||||
) |
||||
.optional(), |
||||
routeBlacklist: z |
||||
.array( |
||||
z.object({ |
||||
origin: ZChainName, |
||||
destination: ZChainName, |
||||
}), |
||||
) |
||||
.optional(), |
||||
}) |
||||
.optional(), |
||||
}); |
||||
|
||||
export type WarpCoreConfig = z.infer<typeof WarpCoreConfigSchema>; |
Loading…
Reference in new issue