diff --git a/typescript/helloworld/src/multiProtocolApp/evmAdapter.ts b/typescript/helloworld/src/multiProtocolApp/evmAdapter.ts index 2f8c5bc621..43a7dc6fa2 100644 --- a/typescript/helloworld/src/multiProtocolApp/evmAdapter.ts +++ b/typescript/helloworld/src/multiProtocolApp/evmAdapter.ts @@ -2,8 +2,8 @@ import { ChainName, EthersV5Transaction, EvmRouterAdapter, + MultiProtocolProvider, ProviderType, - RouterAddress, } from '@hyperlane-xyz/sdk'; import { Address } from '@hyperlane-xyz/utils'; @@ -13,19 +13,27 @@ import { HelloWorld, HelloWorld__factory } from '../types'; import { IHelloWorldAdapter } from './types'; export class EvmHelloWorldAdapter - extends EvmRouterAdapter + extends EvmRouterAdapter implements IHelloWorldAdapter { + constructor( + public readonly chainName: ChainName, + public readonly multiProvider: MultiProtocolProvider, + public readonly addresses: { router: Address; mailbox: Address }, + ) { + super(chainName, multiProvider, addresses); + } + async populateSendHelloTx( - origin: ChainName, destination: ChainName, message: string, value: string, ): Promise { - const contract = this.getConnectedContract(origin); + const contract = this.getConnectedContract(); const toDomain = this.multiProvider.getDomainId(destination); - const { transactionOverrides } = - this.multiProvider.getChainMetadata(origin); + const { transactionOverrides } = this.multiProvider.getChainMetadata( + this.chainName, + ); // apply gas buffer due to https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/634 const estimated = await contract.estimateGas.sendHelloWorld( @@ -48,23 +56,27 @@ export class EvmHelloWorldAdapter } async channelStats( - origin: ChainName, destination: ChainName, + destinationMailbox: Address, ): Promise { - const originDomain = this.multiProvider.getDomainId(origin); + const originDomain = this.multiProvider.getDomainId(this.chainName); const destinationDomain = this.multiProvider.getDomainId(destination); - const sent = await this.getConnectedContract(origin).sentTo( - destinationDomain, - ); - const received = await this.getConnectedContract(destination).sentTo( - originDomain, + const originContract = this.getConnectedContract(); + const sent = await originContract.sentTo(destinationDomain); + const destinationProvider = + this.multiProvider.getEthersV5Provider(destination); + const destinationContract = HelloWorld__factory.connect( + destinationMailbox, + destinationProvider, ); + const received = await destinationContract.sentTo(originDomain); return { sent: sent.toNumber(), received: received.toNumber() }; } - override getConnectedContract(chain: ChainName): HelloWorld { - const address = this.multiProvider.getChainMetadata(chain).router; - const provider = this.multiProvider.getEthersV5Provider(chain); - return HelloWorld__factory.connect(address, provider); + override getConnectedContract(): HelloWorld { + return HelloWorld__factory.connect( + this.addresses.router, + this.getProvider(), + ); } } diff --git a/typescript/helloworld/src/multiProtocolApp/multiProtocolApp.ts b/typescript/helloworld/src/multiProtocolApp/multiProtocolApp.ts index 22b7004ffe..f969acbcd2 100644 --- a/typescript/helloworld/src/multiProtocolApp/multiProtocolApp.ts +++ b/typescript/helloworld/src/multiProtocolApp/multiProtocolApp.ts @@ -14,8 +14,8 @@ import { SealevelHelloWorldAdapter } from './sealevelAdapter'; import { IHelloWorldAdapter } from './types'; export class HelloMultiProtocolApp extends MultiProtocolRouterApp< - RouterAddress & { mailbox: Address }, - IHelloWorldAdapter + IHelloWorldAdapter, + RouterAddress & { mailbox: Address } > { override protocolToAdapter(protocol: ProtocolType) { if (protocol === ProtocolType.Ethereum) return EvmHelloWorldAdapter; @@ -31,7 +31,6 @@ export class HelloMultiProtocolApp extends MultiProtocolRouterApp< sender: Address, ): Promise { return this.adapter(origin).populateSendHelloTx( - origin, destination, message, value, @@ -40,7 +39,10 @@ export class HelloMultiProtocolApp extends MultiProtocolRouterApp< } channelStats(origin: ChainName, destination: ChainName): Promise { - return this.adapter(origin).channelStats(origin, destination); + return this.adapter(origin).channelStats( + destination, + this.addresses[destination].mailbox, + ); } async stats(): Promise>> { diff --git a/typescript/helloworld/src/multiProtocolApp/sealevelAdapter.ts b/typescript/helloworld/src/multiProtocolApp/sealevelAdapter.ts index 132f9f0305..22bea23e59 100644 --- a/typescript/helloworld/src/multiProtocolApp/sealevelAdapter.ts +++ b/typescript/helloworld/src/multiProtocolApp/sealevelAdapter.ts @@ -9,14 +9,15 @@ import { import { deserializeUnchecked, serialize } from 'borsh'; import { - BaseSealevelAdapter, ChainName, + MultiProtocolProvider, ProviderType, - RouterAddress, SEALEVEL_SPL_NOOP_ADDRESS, SealevelAccountDataWrapper, SealevelCoreAdapter, SealevelInstructionWrapper, + SealevelInterchainGasPaymasterConfig, + SealevelInterchainGasPaymasterConfigSchema, SealevelRouterAdapter, SolanaWeb3Transaction, getSealevelAccountDataSchema, @@ -28,11 +29,18 @@ import { StatCounts } from '../app/types'; import { IHelloWorldAdapter } from './types'; export class SealevelHelloWorldAdapter - extends SealevelRouterAdapter + extends SealevelRouterAdapter implements IHelloWorldAdapter { + constructor( + public readonly chainName: ChainName, + public readonly multiProvider: MultiProtocolProvider, + public readonly addresses: { router: Address; mailbox: Address }, + ) { + super(chainName, multiProvider, addresses); + } + async populateSendHelloTx( - origin: ChainName, destination: ChainName, message: string, value: string, @@ -40,14 +48,13 @@ export class SealevelHelloWorldAdapter ): Promise { this.logger( 'Creating sendHelloWorld tx for sealevel', - origin, + this.chainName, destination, message, value, ); - const { mailbox, router: programId } = - this.multiProvider.getChainMetadata(origin); + const { mailbox, router: programId } = this.addresses; const mailboxPubKey = new PublicKey(mailbox); const senderPubKey = new PublicKey(sender); const programPubKey = new PublicKey(programId); @@ -75,7 +82,7 @@ export class SealevelHelloWorldAdapter data: Buffer.from(serializedData), }); - const connection = this.multiProvider.getSolanaWeb3Provider(origin); + const connection = this.getProvider(); const recentBlockhash = (await connection.getLatestBlockhash('finalized')) .blockhash; // @ts-ignore Workaround for bug in the web3 lib, sometimes uses recentBlockhash and sometimes uses blockhash @@ -151,23 +158,20 @@ export class SealevelHelloWorldAdapter // Should match https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/dd7ff727b0d3d393a159afa5f0a364775bde3a58/rust/sealevel/programs/helloworld/src/processor.rs#L44 deriveProgramStoragePDA(programId: string | PublicKey): PublicKey { - return BaseSealevelAdapter.derivePda( + return this.derivePda( ['hello_world', '-', 'handle', '-', 'storage'], programId, ); } - async channelStats( - origin: ChainName, - _destination: ChainName, - ): Promise { - const data = await this.getAccountInfo(origin); + async channelStats(_destination: ChainName): Promise { + const data = await this.getAccountInfo(); return { sent: data.sent, received: data.received }; } - async getAccountInfo(chain: ChainName): Promise { - const address = this.multiProvider.getChainMetadata(chain).router; - const connection = this.multiProvider.getSolanaWeb3Provider(chain); + async getAccountInfo(): Promise { + const address = this.addresses.router; + const connection = this.getProvider(); const msgRecipientPda = this.deriveMessageRecipientPda(address); const accountInfo = await connection.getAccountInfo(msgRecipientPda); @@ -290,14 +294,7 @@ export const HelloWorldDataSchema = new Map([ 'igp', { kind: 'option', - type: { - kind: 'struct', - fields: [ - ['program_id', [32]], - ['type', 'u8'], - ['igp_account', [32]], - ], - }, + type: SealevelInterchainGasPaymasterConfig, }, ], ['owner', { kind: 'option', type: [32] }], @@ -309,4 +306,8 @@ export const HelloWorldDataSchema = new Map([ ], }, ], + [ + SealevelInterchainGasPaymasterConfig, + SealevelInterchainGasPaymasterConfigSchema, + ], ]); diff --git a/typescript/helloworld/src/multiProtocolApp/types.ts b/typescript/helloworld/src/multiProtocolApp/types.ts index 1e8f525aed..2f9eb5e5c3 100644 --- a/typescript/helloworld/src/multiProtocolApp/types.ts +++ b/typescript/helloworld/src/multiProtocolApp/types.ts @@ -1,25 +1,24 @@ import { ChainName, IRouterAdapter, - RouterAddress, TypedTransaction, } from '@hyperlane-xyz/sdk'; import { Address } from '@hyperlane-xyz/utils'; import { StatCounts } from '../app/types'; -export interface IHelloWorldAdapter - extends IRouterAdapter { +export interface IHelloWorldAdapter extends IRouterAdapter { populateSendHelloTx: ( - origin: ChainName, destination: ChainName, message: string, value: string, sender: Address, ) => Promise; + // TODO break apart into separate origin + destination methods to + // handle case where origin/dest protocols differ channelStats: ( - origin: ChainName, destination: ChainName, + destinationMailbox: Address, ) => Promise; } diff --git a/typescript/infra/scripts/helloworld/utils.ts b/typescript/infra/scripts/helloworld/utils.ts index 50288e470e..63276048a9 100644 --- a/typescript/infra/scripts/helloworld/utils.ts +++ b/typescript/infra/scripts/helloworld/utils.ts @@ -17,7 +17,7 @@ import { igpFactories, } from '@hyperlane-xyz/sdk'; import { hyperlaneEnvironmentsWithSealevel } from '@hyperlane-xyz/sdk/src'; -import { ProtocolType } from '@hyperlane-xyz/utils'; +import { ProtocolType, objMerge } from '@hyperlane-xyz/utils'; import { Contexts } from '../../config/contexts'; import { EnvironmentConfig } from '../../src/config'; @@ -74,20 +74,20 @@ export async function getHelloWorldMultiProtocolApp( if (!multiProtocolProvider.getKnownChainNames().includes('solanadevnet')) { multiProtocolProvider.addChain(chainMetadata.solanadevnet); } - // Add the helloWorld contract addresses to the metadata - const mpWithHelloWorld = multiProtocolProvider.extendChainMetadata( - helloworldConfig.addresses, - ); const core = MultiProtocolCore.fromAddressesMap( hyperlaneEnvironmentsWithSealevel[sdkEnvName], multiProtocolProvider, ); - // Extend the MP with mailbox addresses because the sealevel - // adapter needs that to function - const mpWithMailbox = mpWithHelloWorld.extendChainMetadata(core.chainMap); - const app = new HelloMultiProtocolApp(mpWithMailbox); + const routersAndMailboxes = objMerge( + core.chainMap, + helloworldConfig.addresses, + ); + const app = new HelloMultiProtocolApp( + multiProtocolProvider, + routersAndMailboxes, + ); // TODO we need a MultiProtocolIgp // Using an standard IGP for just evm chains for now diff --git a/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts b/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts index 96965b314f..f3c7ccb12c 100644 --- a/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts +++ b/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts @@ -1,4 +1,4 @@ -import { Connection } from '@solana/web3.js'; +import { SystemProgram } from '@solana/web3.js'; import { ethers } from 'ethers'; import { Gauge, Registry } from 'prom-client'; import yargs from 'yargs'; @@ -8,7 +8,12 @@ import { SealevelHypCollateralAdapter, TokenType, } from '@hyperlane-xyz/hyperlane-token'; -import { ChainMap, ChainName, MultiProvider } from '@hyperlane-xyz/sdk'; +import { + ChainMap, + ChainName, + MultiProtocolProvider, + MultiProvider, +} from '@hyperlane-xyz/sdk'; import { ProtocolType, debug, @@ -92,11 +97,15 @@ async function checkBalance( ethers.utils.formatUnits(collateralBalance, token.decimals), ); } else { - const connection = new Connection(multiprovider.getRpcUrl(chain)); const adapter = new SealevelHypCollateralAdapter( - connection, - token.hypCollateralAddress, - token.address, + chain, + MultiProtocolProvider.fromMultiProvider(multiprovider), + { + token: token.address, + warpRouter: token.hypCollateralAddress, + // Mailbox only required for transfers, using system as placeholder + mailbox: SystemProgram.programId.toBase58(), + }, token.isSpl2022, ); const collateralBalance = ethers.BigNumber.from( diff --git a/typescript/sdk/src/app/MultiProtocolApp.test.ts b/typescript/sdk/src/app/MultiProtocolApp.test.ts index 17e3fc60db..4239bfa89d 100644 --- a/typescript/sdk/src/app/MultiProtocolApp.test.ts +++ b/typescript/sdk/src/app/MultiProtocolApp.test.ts @@ -6,12 +6,13 @@ import { Chains } from '../consts/chains'; import { MultiProtocolProvider } from '../providers/MultiProtocolProvider'; import { + BaseAppAdapter, BaseEvmAdapter, BaseSealevelAdapter, MultiProtocolApp, } from './MultiProtocolApp'; -class TestMultiProtocolApp extends MultiProtocolApp { +class TestMultiProtocolApp extends MultiProtocolApp { override protocolToAdapter(protocol: ProtocolType) { if (protocol === ProtocolType.Ethereum) return BaseEvmAdapter; if (protocol === ProtocolType.Sealevel) return BaseSealevelAdapter; @@ -23,9 +24,11 @@ describe('MultiProtocolApp', () => { describe('constructs', () => { const multiProvider = new MultiProtocolProvider(); it('creates an app class and gleans types from generic', async () => { - const app = new TestMultiProtocolApp(multiProvider); + const app = new TestMultiProtocolApp(multiProvider, {}); expect(app).to.be.instanceOf(MultiProtocolApp); - expect(app.adapter(Chains.ethereum).protocol).to.eql(Chains.ethereum); + expect(app.adapter(Chains.ethereum).protocol).to.eql( + ProtocolType.Ethereum, + ); }); }); }); diff --git a/typescript/sdk/src/app/MultiProtocolApp.ts b/typescript/sdk/src/app/MultiProtocolApp.ts index 6e66104635..1a027924fa 100644 --- a/typescript/sdk/src/app/MultiProtocolApp.ts +++ b/typescript/sdk/src/app/MultiProtocolApp.ts @@ -1,10 +1,20 @@ import { PublicKey } from '@solana/web3.js'; import debug from 'debug'; -import { ProtocolType, objMap, promiseObjAll } from '@hyperlane-xyz/utils'; +import { + Address, + ProtocolType, + objMap, + promiseObjAll, +} from '@hyperlane-xyz/utils'; import { ChainMetadata } from '../metadata/chainMetadataTypes'; import { MultiProtocolProvider } from '../providers/MultiProtocolProvider'; +import { + EthersV5Provider, + SolanaWeb3Provider, + TypedProvider, +} from '../providers/ProviderType'; import { ChainMap, ChainName } from '../types'; import { MultiGeneric } from '../utils/MultiGeneric'; @@ -14,29 +24,38 @@ import { MultiGeneric } from '../utils/MultiGeneric'; * E.g. EvmRouterAdapter implements EVM-specific router functionality * whereas SealevelRouterAdapter implements the same logic for Solana */ -export abstract class BaseAppAdapter { +export abstract class BaseAppAdapter { public abstract readonly protocol: ProtocolType; constructor( - public readonly multiProvider: MultiProtocolProvider, + public readonly chainName: ChainName, + public readonly multiProvider: MultiProtocolProvider, + public readonly addresses: Record, public readonly logger = debug(`hyperlane:AppAdapter`), ) {} } -export type AdapterClassType = new ( - multiProvider: MultiProtocolProvider, +export type AdapterClassType = new ( + chainName: ChainName, + multiProvider: MultiProtocolProvider, + addresses: any, + ...args: any ) => API; -export class BaseEvmAdapter< - ContractAddrs = {}, -> extends BaseAppAdapter { +export class BaseEvmAdapter extends BaseAppAdapter { public readonly protocol: ProtocolType = ProtocolType.Ethereum; + + public getProvider(): EthersV5Provider['provider'] { + return this.multiProvider.getEthersV5Provider(this.chainName); + } } -export class BaseSealevelAdapter< - ContractAddrs = {}, -> extends BaseAppAdapter { +export class BaseSealevelAdapter extends BaseAppAdapter { public readonly protocol: ProtocolType = ProtocolType.Sealevel; + public getProvider(): SolanaWeb3Provider['provider'] { + return this.multiProvider.getSolanaWeb3Provider(this.chainName); + } + static derivePda( seeds: Array, programId: string | PublicKey, @@ -47,6 +66,14 @@ export class BaseSealevelAdapter< ); return pda; } + + // An dynamic alias for static method above for convenience + derivePda( + seeds: Array, + programId: string | PublicKey, + ): PublicKey { + return BaseSealevelAdapter.derivePda(seeds, programId); + } } /** @@ -67,31 +94,26 @@ export class BaseSealevelAdapter< * @override protocolToAdapter - This should return an Adapter class for a given protocol type */ export abstract class MultiProtocolApp< - ContractAddrs = {}, - IAdapterApi extends BaseAppAdapter = BaseAppAdapter, -> extends MultiGeneric> { + IAdapterApi extends BaseAppAdapter, + ContractAddrs extends Record = {}, +> extends MultiGeneric { constructor( - public readonly multiProvider: MultiProtocolProvider, + public readonly multiProvider: MultiProtocolProvider, + public readonly addresses: ChainMap, public readonly logger = debug('hyperlane:MultiProtocolApp'), ) { super(multiProvider.metadata); } - // Subclasses should override this with more specific adapters + // Subclasses must implement this with their specific adapters abstract protocolToAdapter( protocol: ProtocolType, - ): AdapterClassType; - - metadata(chain: ChainName): ChainMetadata { - return this.get(chain); - } + ): AdapterClassType; + // Subclasses may want to override this to provide adapters more arguments adapter(chain: ChainName): IAdapterApi { - const metadata = this.metadata(chain); - const Adapter = this.protocolToAdapter(metadata.protocol); - if (!Adapter) - throw new Error(`No adapter for protocol ${metadata.protocol}`); - return new Adapter(this.multiProvider); + const Adapter = this.protocolToAdapter(this.protocol(chain)); + return new Adapter(chain, this.multiProvider, this.addresses[chain]); } adapters(): ChainMap { @@ -103,4 +125,16 @@ export abstract class MultiProtocolApp< ): Promise> { return promiseObjAll(objMap(this.adapters(), fn)); } + + metadata(chain: ChainName): ChainMetadata { + return this.get(chain); + } + + protocol(chain: ChainName): ProtocolType { + return this.metadata(chain).protocol; + } + + provider(chain: ChainName): TypedProvider { + return this.multiProvider.getProvider(chain); + } } diff --git a/typescript/sdk/src/consts/chainMetadata.ts b/typescript/sdk/src/consts/chainMetadata.ts index 5e4a20d591..78aa6c1c1a 100644 --- a/typescript/sdk/src/consts/chainMetadata.ts +++ b/typescript/sdk/src/consts/chainMetadata.ts @@ -316,10 +316,7 @@ export const sepolia: ChainMetadata = { protocol: ProtocolType.Ethereum, displayName: 'Sepolia', nativeToken: etherToken, - rpcUrls: [ - { http: 'https://endpoints.omniatech.io/v1/eth/sepolia/public' }, - { http: 'https://rpc.sepolia.org' }, - ], + rpcUrls: [{ http: 'https://endpoints.omniatech.io/v1/eth/sepolia/public' }], blockExplorers: [ { name: 'Etherscan', @@ -668,9 +665,9 @@ export const solana: ChainMetadata = { rpcUrls: [{ http: 'https://api.mainnet-beta.solana.com' }], blockExplorers: [ { - name: 'SolScan', - url: 'https://solscan.io', - apiUrl: 'https://public-api.solscan.io', + name: 'Solana Explorer', + url: 'https://explorer.solana.com', + apiUrl: 'https://explorer.solana.com', family: ExplorerFamily.Other, }, ], @@ -690,6 +687,14 @@ export const solanatestnet: ChainMetadata = { displayNameShort: 'Sol Testnet', nativeToken: solToken, rpcUrls: [{ http: 'https://api.testnet.solana.com' }], + blockExplorers: [ + { + name: 'Solana Explorer', + url: 'https://explorer.solana.com', + apiUrl: 'https://explorer.solana.com', + family: ExplorerFamily.Other, + }, + ], blocks: { confirmations: 1, reorgPeriod: 0, @@ -707,6 +712,14 @@ export const solanadevnet: ChainMetadata = { displayNameShort: 'Sol Devnet', nativeToken: solToken, rpcUrls: [{ http: 'https://api.devnet.solana.com' }], + blockExplorers: [ + { + name: 'Solana Explorer', + url: 'https://explorer.solana.com', + apiUrl: 'https://explorer.solana.com', + family: ExplorerFamily.Other, + }, + ], blocks: { confirmations: 1, reorgPeriod: 0, @@ -744,8 +757,9 @@ export const chainMetadata: ChainMap = { test1, test2, test3, - solanadevnet, solana, + solanatestnet, + solanadevnet, nautilus, }; @@ -762,3 +776,9 @@ export const mainnetChainsMetadata: Array = Mainnets.map( export const testnetChainsMetadata: Array = Testnets.map( (chainName) => chainMetadata[chainName], ); + +export const solanaChainToClusterName: ChainMap = { + solana: 'mainnet-beta', + solanatestnet: 'testnet', + solanadevnet: 'devnet', +}; diff --git a/typescript/sdk/src/core/MultiProtocolCore.test.ts b/typescript/sdk/src/core/MultiProtocolCore.test.ts index ac38b267d0..ddb2a07b13 100644 --- a/typescript/sdk/src/core/MultiProtocolCore.test.ts +++ b/typescript/sdk/src/core/MultiProtocolCore.test.ts @@ -7,20 +7,22 @@ import { MultiProtocolProvider } from '../providers/MultiProtocolProvider'; import { MultiProtocolCore } from './MultiProtocolCore'; import { EvmCoreAdapter } from './adapters/EvmCoreAdapter'; -import { CoreAddresses } from './contracts'; describe('MultiProtocolCore', () => { describe('constructs', () => { it('with constructor', async () => { - const multiProvider = new MultiProtocolProvider({ + const multiProvider = new MultiProtocolProvider({ ethereum: { ...ethereum, + }, + }); + const core = new MultiProtocolCore(multiProvider, { + ethereum: { validatorAnnounce: ethers.constants.AddressZero, proxyAdmin: ethers.constants.AddressZero, mailbox: ethers.constants.AddressZero, }, }); - const core = new MultiProtocolCore(multiProvider); expect(core).to.be.instanceOf(MultiProtocolCore); const ethAdapter = core.adapter(Chains.ethereum); expect(ethAdapter).to.be.instanceOf(EvmCoreAdapter); diff --git a/typescript/sdk/src/core/MultiProtocolCore.ts b/typescript/sdk/src/core/MultiProtocolCore.ts index 44ff0bfcf9..971394f54d 100644 --- a/typescript/sdk/src/core/MultiProtocolCore.ts +++ b/typescript/sdk/src/core/MultiProtocolCore.ts @@ -17,14 +17,15 @@ import { ICoreAdapter } from './adapters/types'; import { CoreAddresses } from './contracts'; export class MultiProtocolCore extends MultiProtocolApp< - CoreAddresses, - ICoreAdapter + ICoreAdapter, + CoreAddresses > { constructor( - public readonly multiProvider: MultiProtocolProvider, + public readonly multiProvider: MultiProtocolProvider, + public readonly addresses: ChainMap, public readonly logger = debug('hyperlane:MultiProtocolCore'), ) { - super(multiProvider, logger); + super(multiProvider, addresses, logger); } static fromEnvironment( @@ -42,15 +43,15 @@ export class MultiProtocolCore extends MultiProtocolApp< addressesMap: ChainMap, multiProvider: MultiProtocolProvider, ): MultiProtocolCore { - const mpWithAddresses = multiProvider - .intersect(Object.keys(addressesMap)) - .result.extendChainMetadata(addressesMap); - return new MultiProtocolCore(mpWithAddresses); + return new MultiProtocolCore( + multiProvider.intersect(Object.keys(addressesMap)).result, + addressesMap, + ); } override protocolToAdapter( protocol: ProtocolType, - ): AdapterClassType { + ): AdapterClassType { if (protocol === ProtocolType.Ethereum) return EvmCoreAdapter; if (protocol === ProtocolType.Sealevel) return SealevelCoreAdapter; throw new Error(`No adapter for protocol ${protocol}`); diff --git a/typescript/sdk/src/core/adapters/EvmCoreAdapter.ts b/typescript/sdk/src/core/adapters/EvmCoreAdapter.ts index b1553639b3..4eb00a3801 100644 --- a/typescript/sdk/src/core/adapters/EvmCoreAdapter.ts +++ b/typescript/sdk/src/core/adapters/EvmCoreAdapter.ts @@ -1,16 +1,8 @@ -import { - Address, - HexString, - ProtocolType, - objMap, - pick, -} from '@hyperlane-xyz/utils'; +import { Mailbox__factory } from '@hyperlane-xyz/core'; +import { Address, HexString } from '@hyperlane-xyz/utils'; import { BaseEvmAdapter } from '../../app/MultiProtocolApp'; -import { - attachContractsMap, - filterAddressesToProtocol, -} from '../../contracts/contracts'; +import { HyperlaneContractsMap } from '../../contracts/types'; import { MultiProtocolProvider } from '../../providers/MultiProtocolProvider'; import { ProviderType, @@ -18,40 +10,30 @@ import { } from '../../providers/ProviderType'; import { ChainName } from '../../types'; import { HyperlaneCore } from '../HyperlaneCore'; -import { CoreAddresses, coreFactories } from '../contracts'; +import { CoreFactories } from '../contracts'; import { ICoreAdapter } from './types'; -// Explicitly omit timelockController b.c. most chains don't have it in SDK artifacts -type CoreAddressKeys = keyof Omit; - // This adapter just routes to the HyperlaneCore // Which implements the needed functionality for EVM chains -export class EvmCoreAdapter - extends BaseEvmAdapter - implements ICoreAdapter -{ +// TODO deprecate HyperlaneCore and replace all evm-specific classes with adapters +export class EvmCoreAdapter extends BaseEvmAdapter implements ICoreAdapter { core: HyperlaneCore; constructor( - public readonly multiProvider: MultiProtocolProvider, + public readonly chainName: ChainName, + public readonly multiProvider: MultiProtocolProvider, + public readonly addresses: { mailbox: Address }, ) { - super(multiProvider); - - // Pick out the addresses from the metadata in the multiProvider - // Reminder: MultiProtocol Apps expect the addresses to be in the metadata - const contractNames = Object.keys(coreFactories) as Array; - const addresses = objMap(multiProvider.metadata, (_, m) => - pick(m, contractNames), - ); - // Then filter it to just the addresses for Ethereum chains - // Otherwise the factory creators will throw - const filteredAddresses = filterAddressesToProtocol( - addresses, - ProtocolType.Ethereum, - multiProvider, - ); - const contractsMap = attachContractsMap(filteredAddresses, coreFactories); + super(chainName, multiProvider, addresses); + const contractsMap = { + [chainName]: { + mailbox: Mailbox__factory.connect( + addresses.mailbox, + multiProvider.getEthersV5Provider(chainName), + ), + }, + } as HyperlaneContractsMap; // Core only uses mailbox so cast to keep adapter interface simple this.core = new HyperlaneCore( contractsMap, multiProvider.toMultiProvider(), diff --git a/typescript/sdk/src/core/adapters/SealevelCoreAdapter.ts b/typescript/sdk/src/core/adapters/SealevelCoreAdapter.ts index be5ee19e7d..027774d901 100644 --- a/typescript/sdk/src/core/adapters/SealevelCoreAdapter.ts +++ b/typescript/sdk/src/core/adapters/SealevelCoreAdapter.ts @@ -1,14 +1,14 @@ import { PublicKey } from '@solana/web3.js'; -import { HexString, pollAsync } from '@hyperlane-xyz/utils'; +import { Address, HexString, pollAsync } from '@hyperlane-xyz/utils'; import { BaseSealevelAdapter } from '../../app/MultiProtocolApp'; +import { MultiProtocolProvider } from '../../providers/MultiProtocolProvider'; import { ProviderType, TypedTransactionReceipt, } from '../../providers/ProviderType'; import { ChainName } from '../../types'; -import { CoreAddresses } from '../contracts'; import { ICoreAdapter } from './types'; @@ -16,9 +16,17 @@ import { ICoreAdapter } from './types'; const MESSAGE_DISPATCH_LOG_REGEX = /Dispatched message to (.*), ID (.*)/; export class SealevelCoreAdapter - extends BaseSealevelAdapter + extends BaseSealevelAdapter implements ICoreAdapter { + constructor( + public readonly chainName: ChainName, + public readonly multiProvider: MultiProtocolProvider, + public readonly addresses: { mailbox: Address }, + ) { + super(chainName, multiProvider, addresses); + } + extractMessageIds( sourceTx: TypedTransactionReceipt, ): Array<{ messageId: HexString; destination: ChainName }> { @@ -45,10 +53,8 @@ export class SealevelCoreAdapter delayMs?: number, maxAttempts?: number, ): Promise { - const destinationMailbox = - this.multiProvider.getChainMetadata(destination).mailbox; const pda = SealevelCoreAdapter.deriveMailboxMessageProcessedPda( - destinationMailbox, + this.addresses.mailbox, messageId, ); const connection = this.multiProvider.getSolanaWeb3Provider(destination); diff --git a/typescript/sdk/src/core/adapters/types.ts b/typescript/sdk/src/core/adapters/types.ts index 1efb577a63..da97bc36dc 100644 --- a/typescript/sdk/src/core/adapters/types.ts +++ b/typescript/sdk/src/core/adapters/types.ts @@ -3,9 +3,8 @@ import type { HexString } from '@hyperlane-xyz/utils'; import type { BaseAppAdapter } from '../../app/MultiProtocolApp'; import type { TypedTransactionReceipt } from '../../providers/ProviderType'; import type { ChainName } from '../../types'; -import type { CoreAddresses } from '../contracts'; -export interface ICoreAdapter extends BaseAppAdapter { +export interface ICoreAdapter extends BaseAppAdapter { extractMessageIds( r: TypedTransactionReceipt, ): Array<{ messageId: HexString; destination: ChainName }>; diff --git a/typescript/sdk/src/core/contracts.ts b/typescript/sdk/src/core/contracts.ts index c22dc7fd95..334d2c60a8 100644 --- a/typescript/sdk/src/core/contracts.ts +++ b/typescript/sdk/src/core/contracts.ts @@ -13,11 +13,11 @@ export const coreFactories = { timelockController: new TimelockController__factory(), }; -export interface CoreAddresses { +export type CoreAddresses = { validatorAnnounce: Address; proxyAdmin: Address; mailbox: Address; timelockController?: Address; -} +}; export type CoreFactories = typeof coreFactories; diff --git a/typescript/sdk/src/gas/adapters/SealevelIgpAdapter.ts b/typescript/sdk/src/gas/adapters/SealevelIgpAdapter.ts new file mode 100644 index 0000000000..95f4df4b34 --- /dev/null +++ b/typescript/sdk/src/gas/adapters/SealevelIgpAdapter.ts @@ -0,0 +1,58 @@ +import { PublicKey } from '@solana/web3.js'; +import { deserializeUnchecked } from 'borsh'; + +import { Address } from '@hyperlane-xyz/utils'; + +import { BaseSealevelAdapter } from '../../app/MultiProtocolApp'; +import { MultiProtocolProvider } from '../../providers/MultiProtocolProvider'; +import { ChainName } from '../../types'; +import { SealevelAccountDataWrapper } from '../../utils/sealevelSerialization'; + +import { + SealevelOverheadIgpData, + SealevelOverheadIgpDataSchema, +} from './serialization'; + +export class SealevelOverheadIgpAdapter extends BaseSealevelAdapter { + constructor( + public readonly chainName: ChainName, + public readonly multiProvider: MultiProtocolProvider, + public readonly addresses: { igp: Address }, + ) { + super(chainName, multiProvider, addresses); + } + + async getAccountInfo(): Promise { + const address = this.addresses.igp; + const connection = this.getProvider(); + + const accountInfo = await connection.getAccountInfo(new PublicKey(address)); + if (!accountInfo) throw new Error(`No account info found for ${address}}`); + + const accountData = deserializeUnchecked( + SealevelOverheadIgpDataSchema, + SealevelAccountDataWrapper, + accountInfo.data, + ); + return accountData.data as SealevelOverheadIgpData; + } + + // https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/sealevel/programs/hyperlane-sealevel-igp/src/pda_seeds.rs#L7 + static deriveIgpProgramPda(igpProgramId: string | PublicKey): PublicKey { + return super.derivePda( + ['hyperlane_igp', '-', 'program_data'], + igpProgramId, + ); + } + + // https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/sealevel/programs/hyperlane-sealevel-igp/src/pda_seeds.rs#L62 + static deriveGasPaymentPda( + igpProgramId: string | PublicKey, + randomWalletPubKey: PublicKey, + ): PublicKey { + return super.derivePda( + ['hyperlane_igp', '-', 'gas_payment', '-', randomWalletPubKey.toBuffer()], + igpProgramId, + ); + } +} diff --git a/typescript/sdk/src/gas/adapters/serialization.ts b/typescript/sdk/src/gas/adapters/serialization.ts new file mode 100644 index 0000000000..6d4fbb3b29 --- /dev/null +++ b/typescript/sdk/src/gas/adapters/serialization.ts @@ -0,0 +1,92 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import { PublicKey } from '@solana/web3.js'; + +import { Domain } from '@hyperlane-xyz/utils'; + +import { + SealevelAccountDataWrapper, + getSealevelAccountDataSchema, +} from '../../utils/sealevelSerialization'; + +// Should match https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/sealevel/programs/hyperlane-sealevel-igp/src/accounts.rs#L24 +export enum SealevelInterchainGasPaymasterType { + // An IGP with gas oracles and that receives lamports as payment. + Igp = 0, + // An overhead IGP that points to an inner IGP and imposes a gas overhead for each destination domain. + OverheadIgp = 1, +} + +/** + * IGP Config Borsh Schema + */ + +// Config schema, e.g. for use in token data +export class SealevelInterchainGasPaymasterConfig { + program_id!: Uint8Array; + program_id_pubkey!: PublicKey; + type!: SealevelInterchainGasPaymasterType; + igp_account?: Uint8Array; + igp_account_pub_key?: PublicKey; + + constructor(public readonly fields: any) { + Object.assign(this, fields); + this.program_id_pubkey = new PublicKey(this.program_id); + this.igp_account_pub_key = this.igp_account + ? new PublicKey(this.igp_account) + : undefined; + } +} + +export const SealevelInterchainGasPaymasterConfigSchema = { + kind: 'struct', + fields: [ + ['program_id', [32]], + ['type', 'u8'], + ['igp_account', [32]], + ], +}; + +/** + * IGP Program Data Borsh Schema + */ + +// Should match https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/sealevel/programs/hyperlane-sealevel-igp/src/accounts.rs#L91 +export class SealevelOverheadIgpData { + /// The bump seed for this PDA. + bump!: number; + /// The salt used to derive the overhead IGP PDA. + salt!: Uint8Array; + /// The owner of the overhead IGP. + owner?: Uint8Array; + owner_pub_key?: PublicKey; + /// The inner IGP account. + inner!: Uint8Array; + inner_pub_key!: PublicKey; + /// The gas overheads to impose on gas payments to each destination domain. + gas_overheads!: Map; + constructor(public readonly fields: any) { + Object.assign(this, fields); + this.owner_pub_key = this.owner ? new PublicKey(this.owner) : undefined; + this.inner_pub_key = new PublicKey(this.inner); + } +} + +export const SealevelOverheadIgpDataSchema = new Map([ + [ + SealevelAccountDataWrapper, + getSealevelAccountDataSchema(SealevelOverheadIgpData, [8]), + ], + [ + SealevelOverheadIgpData, + { + kind: 'struct', + fields: [ + ['bump', 'u8'], + ['salt', [32]], + ['owner', { kind: 'option', type: [32] }], + ['inner', [32]], + ['gas_overheads', { kind: 'map', key: 'u32', value: 'u64' }], + ], + }, + ], +]); diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index 6a562085fe..5926197fad 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -10,6 +10,7 @@ export { chainIdToMetadata, chainMetadata, mainnetChainsMetadata, + solanaChainToClusterName, testnetChainsMetadata, } from './consts/chainMetadata'; export { @@ -87,6 +88,14 @@ export * as verificationUtils from './deploy/verify/utils'; export { HyperlaneIgp } from './gas/HyperlaneIgp'; export { HyperlaneIgpChecker } from './gas/HyperlaneIgpChecker'; export { HyperlaneIgpDeployer } from './gas/HyperlaneIgpDeployer'; +export { SealevelOverheadIgpAdapter } from './gas/adapters/SealevelIgpAdapter'; +export { + SealevelInterchainGasPaymasterConfig, + SealevelInterchainGasPaymasterConfigSchema, + SealevelInterchainGasPaymasterType, + SealevelOverheadIgpData, + SealevelOverheadIgpDataSchema, +} from './gas/adapters/serialization'; export { IgpFactories, igpFactories } from './gas/contracts'; export { CoinGeckoTokenPriceGetter } from './gas/token-prices'; export { @@ -253,18 +262,6 @@ export { RouterConfig, proxiedFactories, } from './router/types'; -export { - SealevelAccountDataWrapper, - SealevelInstructionWrapper, - getSealevelAccountDataSchema, -} from './sealevel/serialization'; -export { - SealevelHypTokenInstruction, - SealevelHyperlaneTokenData, - SealevelHyperlaneTokenDataSchema, - SealevelTransferRemoteInstruction, - SealevelTransferRemoteSchema, -} from './sealevel/tokenSerialization'; export { createRouterConfigMap, deployTestIgpsAndGetRouterConfig, @@ -279,4 +276,9 @@ export { export { MultiGeneric } from './utils/MultiGeneric'; export { filterByChains } from './utils/filter'; export { multisigIsmVerificationCost } from './utils/ism'; +export { + SealevelAccountDataWrapper, + SealevelInstructionWrapper, + getSealevelAccountDataSchema, +} from './utils/sealevelSerialization'; export { chainMetadataToWagmiChain, wagmiChainMetadata } from './utils/wagmi'; diff --git a/typescript/sdk/src/metadata/ChainMetadataManager.ts b/typescript/sdk/src/metadata/ChainMetadataManager.ts index 3f80e7a8cf..9731482686 100644 --- a/typescript/sdk/src/metadata/ChainMetadataManager.ts +++ b/typescript/sdk/src/metadata/ChainMetadataManager.ts @@ -1,8 +1,11 @@ import { Debugger, debug } from 'debug'; -import { exclude, isNumeric, pick } from '@hyperlane-xyz/utils'; +import { ProtocolType, exclude, isNumeric, pick } from '@hyperlane-xyz/utils'; -import { chainMetadata as defaultChainMetadata } from '../consts/chainMetadata'; +import { + chainMetadata as defaultChainMetadata, + solanaChainToClusterName, +} from '../consts/chainMetadata'; import { ChainMap, ChainName } from '../types'; import { @@ -210,9 +213,17 @@ export class ChainMetadataManager { * Get a block explorer URL for a given chain name, chain id, or domain id */ tryGetExplorerUrl(chainNameOrId: ChainName | number): string | null { - const explorers = this.tryGetChainMetadata(chainNameOrId)?.blockExplorers; - if (!explorers?.length) return null; - return explorers[0].url; + const metadata = this.tryGetChainMetadata(chainNameOrId); + if (!metadata?.blockExplorers?.length) return null; + const url = new URL(metadata.blockExplorers[0].url); + // TODO move handling of these chain/protocol specific quirks to ChainMetadata + if ( + metadata.protocol === ProtocolType.Sealevel && + solanaChainToClusterName[metadata.name] + ) { + url.searchParams.set('cluster', solanaChainToClusterName[metadata.name]); + } + return url.toString(); } /** @@ -229,9 +240,11 @@ export class ChainMetadataManager { * Get a block explorer's API URL for a given chain name, chain id, or domain id */ tryGetExplorerApiUrl(chainNameOrId: ChainName | number): string | null { - const explorers = this.tryGetChainMetadata(chainNameOrId)?.blockExplorers; - if (!explorers?.length || !explorers[0].apiUrl) return null; - const { apiUrl, apiKey } = explorers[0]; + const metadata = this.tryGetChainMetadata(chainNameOrId); + const { protocol, blockExplorers } = metadata || {}; + if (protocol !== ProtocolType.Ethereum) return null; + if (!blockExplorers?.length || !blockExplorers[0].apiUrl) return null; + const { apiUrl, apiKey } = blockExplorers[0]; if (!apiKey) return apiUrl; const url = new URL(apiUrl); url.searchParams.set('apikey', apiKey); @@ -256,7 +269,43 @@ export class ChainMetadataManager { response: { hash: string }, ): string | null { const baseUrl = this.tryGetExplorerUrl(chainNameOrId); - return baseUrl ? `${baseUrl}/tx/${response.hash}` : null; + if (!baseUrl) return null; + const chainName = this.getChainName(chainNameOrId); + const urlPathStub = ['nautilus', 'proteustestnet'].includes(chainName) + ? 'transaction' + : 'tx'; + const url = new URL(baseUrl); + url.pathname += `/${urlPathStub}/${response.hash}`; + return url.toString(); + } + + /** + * Get a block explorer URL for given chain's address + */ + async tryGetExplorerAddressUrl( + chainNameOrId: ChainName | number, + address?: string, + ): Promise { + if (!address) return null; + const baseUrl = this.tryGetExplorerUrl(chainNameOrId); + if (!baseUrl) return null; + const url = new URL(baseUrl); + url.pathname += `/address/${address}`; + return url.toString(); + } + + /** + * Get a block explorer URL for given chain's address + * @throws if address or the chain's block explorer data has no been set + */ + async getExplorerAddressUrl( + chainNameOrId: ChainName | number, + address?: string, + ): Promise { + const url = await this.tryGetExplorerAddressUrl(chainNameOrId, address); + if (!url) + throw new Error(`Missing data for address url for ${chainNameOrId}`); + return url; } /** diff --git a/typescript/sdk/src/providers/MultiProtocolProvider.ts b/typescript/sdk/src/providers/MultiProtocolProvider.ts index 179dfe4365..559d6049df 100644 --- a/typescript/sdk/src/providers/MultiProtocolProvider.ts +++ b/typescript/sdk/src/providers/MultiProtocolProvider.ts @@ -1,6 +1,6 @@ import { Debugger, debug } from 'debug'; -import { objFilter, objMap, pick } from '@hyperlane-xyz/utils'; +import { ProtocolType, objFilter, objMap, pick } from '@hyperlane-xyz/utils'; import { chainMetadata as defaultChainMetadata } from '../consts/chainMetadata'; import { ChainMetadataManager } from '../metadata/ChainMetadataManager'; @@ -21,6 +21,13 @@ import { defaultProviderBuilderMap, } from './providerBuilders'; +export const PROTOCOL_DEFAULT_PROVIDER_TYPE: Partial< + Record +> = { + [ProtocolType.Ethereum]: ProviderType.EthersV5, + [ProtocolType.Sealevel]: ProviderType.SolanaWeb3, +}; + export interface MultiProtocolProviderOptions { loggerName?: string; providers?: ChainMap>; @@ -107,11 +114,13 @@ export class MultiProtocolProvider< tryGetProvider( chainNameOrId: ChainName | number, - type: ProviderType, + type?: ProviderType, ): TypedProvider | null { const metadata = this.tryGetChainMetadata(chainNameOrId); if (!metadata) return null; - const { name, chainId, rpcUrls } = metadata; + const { protocol, name, chainId, rpcUrls } = metadata; + type = type || PROTOCOL_DEFAULT_PROVIDER_TYPE[protocol]; + if (!type) return null; if (this.providers[name]?.[type]) return this.providers[name][type]!; @@ -126,7 +135,7 @@ export class MultiProtocolProvider< getProvider( chainNameOrId: ChainName | number, - type: ProviderType, + type?: ProviderType, ): TypedProvider { const provider = this.tryGetProvider(chainNameOrId, type); if (!provider) diff --git a/typescript/sdk/src/providers/MultiProvider.ts b/typescript/sdk/src/providers/MultiProvider.ts index 206d644d5c..8c9a8ba013 100644 --- a/typescript/sdk/src/providers/MultiProvider.ts +++ b/typescript/sdk/src/providers/MultiProvider.ts @@ -264,30 +264,17 @@ export class MultiProvider extends ChainMetadataManager { /** * Get a block explorer URL for given chain's address */ - async tryGetExplorerAddressUrl( + override async tryGetExplorerAddressUrl( chainNameOrId: ChainName | number, address?: string, ): Promise { - const baseUrl = this.tryGetExplorerUrl(chainNameOrId); - if (!baseUrl) return null; - if (address) return `${baseUrl}/address/${address}`; + if (address) return super.tryGetExplorerAddressUrl(chainNameOrId, address); const signer = this.tryGetSigner(chainNameOrId); - if (!signer) return null; - return `${baseUrl}/address/${await signer.getAddress()}`; - } - - /** - * Get a block explorer URL for given chain's address - * @throws if chain's metadata, signer, or block explorer data has no been set - */ - async getExplorerAddressUrl( - chainNameOrId: ChainName | number, - address?: string, - ): Promise { - const url = await this.tryGetExplorerAddressUrl(chainNameOrId, address); - if (!url) - throw new Error(`Missing data for address url for ${chainNameOrId}`); - return url; + if (signer) { + const signerAddr = await signer.getAddress(); + return super.tryGetExplorerAddressUrl(chainNameOrId, signerAddr); + } + return null; } /** diff --git a/typescript/sdk/src/router/MultiProtocolRouterApps.test.ts b/typescript/sdk/src/router/MultiProtocolRouterApps.test.ts index ae185c48ac..c075da32f9 100644 --- a/typescript/sdk/src/router/MultiProtocolRouterApps.test.ts +++ b/typescript/sdk/src/router/MultiProtocolRouterApps.test.ts @@ -1,4 +1,5 @@ import { expect } from 'chai'; +import { ethers } from 'ethers'; import { Chains } from '../consts/chains'; import { MultiProtocolProvider } from '../providers/MultiProtocolProvider'; @@ -11,7 +12,9 @@ describe('MultiProtocolRouterApp', () => { describe('constructs', () => { const multiProvider = new MultiProtocolProvider(); it('creates an app class', async () => { - const app = new MultiProtocolRouterApp(multiProvider); + const app = new MultiProtocolRouterApp(multiProvider, { + ethereum: { router: ethers.constants.AddressZero }, + }); expect(app).to.be.instanceOf(MultiProtocolRouterApp); const ethAdapter = app.adapter(Chains.ethereum); expect(ethAdapter).to.be.instanceOf(EvmRouterAdapter); diff --git a/typescript/sdk/src/router/MultiProtocolRouterApps.ts b/typescript/sdk/src/router/MultiProtocolRouterApps.ts index 32fcfb6591..5b7248d956 100644 --- a/typescript/sdk/src/router/MultiProtocolRouterApps.ts +++ b/typescript/sdk/src/router/MultiProtocolRouterApps.ts @@ -17,12 +17,12 @@ import { RouterAddress } from './types'; export { Router } from '@hyperlane-xyz/core'; export class MultiProtocolRouterApp< - ContractAddrs extends RouterAddress = RouterAddress, IAdapterApi extends IRouterAdapter = IRouterAdapter, -> extends MultiProtocolApp { + ContractAddrs extends RouterAddress = RouterAddress, +> extends MultiProtocolApp { override protocolToAdapter( protocol: ProtocolType, - ): AdapterClassType { + ): AdapterClassType { // Casts are required here to allow for default adapters while still // enabling extensible generic types if (protocol === ProtocolType.Ethereum) return EvmRouterAdapter as any; @@ -31,33 +31,31 @@ export class MultiProtocolRouterApp< } router(chain: ChainName): Address { - return this.metadata(chain).router; + return this.addresses[chain].router; } interchainSecurityModules(): Promise> { - return this.adapterMap((chain, adapter) => - adapter.interchainSecurityModule(chain), - ); + return this.adapterMap((_, adapter) => adapter.interchainSecurityModule()); } owners(): Promise> { - return this.adapterMap((chain, adapter) => adapter.owner(chain)); + return this.adapterMap((_, adapter) => adapter.owner()); } remoteRouters( origin: ChainName, ): Promise> { - return this.adapter(origin).remoteRouters(origin); + return this.adapter(origin).remoteRouters(); } } export class MultiProtocolGasRouterApp< - ContractAddrs extends RouterAddress = RouterAddress, IAdapterApi extends IGasRouterAdapter = IGasRouterAdapter, -> extends MultiProtocolRouterApp { + ContractAddrs extends RouterAddress = RouterAddress, +> extends MultiProtocolRouterApp { override protocolToAdapter( protocol: ProtocolType, - ): AdapterClassType { + ): AdapterClassType { // Casts are required here to allow for default adapters while still // enabling extensible generic types if (protocol === ProtocolType.Ethereum) return EvmGasRouterAdapter as any; @@ -70,6 +68,6 @@ export class MultiProtocolGasRouterApp< origin: ChainName, destination: ChainName, ): Promise { - return this.adapter(origin).quoteGasPayment(origin, destination); + return this.adapter(origin).quoteGasPayment(destination); } } diff --git a/typescript/sdk/src/router/adapters/EvmRouterAdapter.ts b/typescript/sdk/src/router/adapters/EvmRouterAdapter.ts index 64bbdc7a44..1a39d96e35 100644 --- a/typescript/sdk/src/router/adapters/EvmRouterAdapter.ts +++ b/typescript/sdk/src/router/adapters/EvmRouterAdapter.ts @@ -7,76 +7,68 @@ import { import { Address, Domain, bytes32ToAddress } from '@hyperlane-xyz/utils'; import { BaseEvmAdapter } from '../../app/MultiProtocolApp'; +import { MultiProtocolProvider } from '../../providers/MultiProtocolProvider'; import { ChainName } from '../../types'; -import { RouterAddress } from '../types'; import { IGasRouterAdapter, IRouterAdapter } from './types'; -export class EvmRouterAdapter< - ContractAddrs extends RouterAddress = RouterAddress, - > - extends BaseEvmAdapter - implements IRouterAdapter -{ - interchainSecurityModule(chain: ChainName): Promise
{ - return this.getConnectedContract(chain).interchainSecurityModule(); +export class EvmRouterAdapter extends BaseEvmAdapter implements IRouterAdapter { + constructor( + public readonly chainName: ChainName, + public readonly multiProvider: MultiProtocolProvider, + public readonly addresses: { router: Address }, + ) { + super(chainName, multiProvider, addresses); + } + + interchainSecurityModule(): Promise
{ + return this.getConnectedContract().interchainSecurityModule(); } - owner(chain: ChainName): Promise
{ - return this.getConnectedContract(chain).owner(); + owner(): Promise
{ + return this.getConnectedContract().owner(); } - remoteDomains(originChain: ChainName): Promise { - return this.getConnectedContract(originChain).domains(); + remoteDomains(): Promise { + return this.getConnectedContract().domains(); } - async remoteRouter( - originChain: ChainName, - remoteDomain: Domain, - ): Promise
{ - const routerAddressesAsBytes32 = await this.getConnectedContract( - originChain, - ).routers(remoteDomain); + async remoteRouter(remoteDomain: Domain): Promise
{ + const routerAddressesAsBytes32 = await this.getConnectedContract().routers( + remoteDomain, + ); return bytes32ToAddress(routerAddressesAsBytes32); } - async remoteRouters( - originChain: ChainName, - ): Promise> { - const domains = await this.remoteDomains(originChain); + async remoteRouters(): Promise> { + const domains = await this.remoteDomains(); const routers: Address[] = await Promise.all( - domains.map((d) => this.remoteRouter(originChain, d)), + domains.map((d) => this.remoteRouter(d)), ); return domains.map((d, i) => ({ domain: d, address: routers[i] })); } - getConnectedContract(chain: ChainName): Router { - const address = this.multiProvider.getChainMetadata(chain).router; - const provider = this.multiProvider.getEthersV5Provider(chain); - return Router__factory.connect(address, provider); + getConnectedContract(): Router { + return Router__factory.connect(this.addresses.router, this.getProvider()); } } -export class EvmGasRouterAdapter< - ContractAddrs extends RouterAddress = RouterAddress, - > - extends EvmRouterAdapter - implements IGasRouterAdapter +export class EvmGasRouterAdapter + extends EvmRouterAdapter + implements IGasRouterAdapter { - async quoteGasPayment( - origin: ChainName, - destination: ChainName, - ): Promise { + async quoteGasPayment(destination: ChainName): Promise { const destDomain = this.multiProvider.getDomainId(destination); - const amount = await this.getConnectedContract(origin).quoteGasPayment( + const amount = await this.getConnectedContract().quoteGasPayment( destDomain, ); return amount.toString(); } - override getConnectedContract(chain: ChainName): GasRouter { - const address = this.multiProvider.getChainMetadata(chain).router; - const provider = this.multiProvider.getEthersV5Provider(chain); - return GasRouter__factory.connect(address, provider); + override getConnectedContract(): GasRouter { + return GasRouter__factory.connect( + this.addresses.router, + this.getProvider(), + ); } } diff --git a/typescript/sdk/src/router/adapters/SealevelRouterAdapter.test.ts b/typescript/sdk/src/router/adapters/SealevelRouterAdapter.test.ts deleted file mode 100644 index 819d21c958..0000000000 --- a/typescript/sdk/src/router/adapters/SealevelRouterAdapter.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { deserializeUnchecked } from 'borsh'; -import { expect } from 'chai'; - -import { SealevelAccountDataWrapper } from '../../sealevel/serialization'; -import { - SealevelHyperlaneTokenData, - SealevelHyperlaneTokenDataSchema, -} from '../../sealevel/tokenSerialization'; - -// Copied from the warp token router program on Solana devnet -const RAW_ACCOUNT_INFO = - '01ff3a280e8466d26bc4e1a5d3d17e73f7b307c082156dd0ffbf8c5f9ae75506d6f14aed87b9d3a2bb5effdbdcd1af363555ff8b6c1311a93c495e6bc722284d2574fb0612012cbc3cc37a2d2e8aaa301fac7e032fbe5d3140f8a12d7445e7fc69f80f60105800000200000061000000a009010000000000c2570100e0ab000000000000020000006100000000000000000000000000000031b5234a896fbc4b3e2f7237592d054716762131c257010000000000000000000000000034a9af13c5555bad0783c220911b9ef59cfdbcef06ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9e92839550965ffd4d64acaaf46d45df7318e5b4f57c90c487d60625d829b837b256d8b6f7c1f678a52ef123ddc35c248fcc1e1895e5b8c6d5e6dd381f8090a48fffe00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'; - -const OWNER_PUB_KEY = '41dRB2nrYY8Ymjctq4HNa3uF7gRG829pswAjbDtsj6vK'; - -describe('SealevelRouterAdapter', () => { - describe('account info', () => { - it('correctly deserializes router account info', () => { - const rawData = Buffer.from(RAW_ACCOUNT_INFO, 'hex'); - const wrappedData = deserializeUnchecked( - SealevelHyperlaneTokenDataSchema, - SealevelAccountDataWrapper, - rawData, - ); - expect(wrappedData.initialized).to.eql(1); - const data = wrappedData.data as SealevelHyperlaneTokenData; - expect(data.decimals).to.eql(6); - expect(data.owner_pub_key?.toBase58()).to.eql(OWNER_PUB_KEY); - expect(data.remote_router_pubkeys.size).to.eql(2); - }); - }); -}); diff --git a/typescript/sdk/src/router/adapters/SealevelRouterAdapter.ts b/typescript/sdk/src/router/adapters/SealevelRouterAdapter.ts index 44435030c9..a5ceed5bd9 100644 --- a/typescript/sdk/src/router/adapters/SealevelRouterAdapter.ts +++ b/typescript/sdk/src/router/adapters/SealevelRouterAdapter.ts @@ -1,50 +1,46 @@ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { PublicKey } from '@solana/web3.js'; -import { deserializeUnchecked } from 'borsh'; import { Address, Domain } from '@hyperlane-xyz/utils'; import { BaseSealevelAdapter } from '../../app/MultiProtocolApp'; -import { SealevelAccountDataWrapper } from '../../sealevel/serialization'; -import { - SealevelHyperlaneTokenData, - SealevelHyperlaneTokenDataSchema, -} from '../../sealevel/tokenSerialization'; +import { MultiProtocolProvider } from '../../providers/MultiProtocolProvider'; import { ChainName } from '../../types'; -import { RouterAddress } from '../types'; import { IGasRouterAdapter, IRouterAdapter } from './types'; -export class SealevelRouterAdapter< - ContractAddrs extends RouterAddress = RouterAddress, - > - extends BaseSealevelAdapter - implements IRouterAdapter +export class SealevelRouterAdapter + extends BaseSealevelAdapter + implements IRouterAdapter { - async interchainSecurityModule(chain: ChainName): Promise
{ - const routerAccountInfo = await this.getRouterAccountInfo(chain); + constructor( + public readonly chainName: ChainName, + public readonly multiProvider: MultiProtocolProvider, + public readonly addresses: { router: Address }, + ) { + super(chainName, multiProvider, addresses); + } + + async interchainSecurityModule(): Promise
{ + const routerAccountInfo = await this.getRouterAccountInfo(); if (!routerAccountInfo.interchain_security_module_pubkey) - throw new Error(`No ism found for router on ${chain}`); + throw new Error(`No ism found for router on ${this.chainName}`); return routerAccountInfo.interchain_security_module_pubkey.toBase58(); } - async owner(chain: ChainName): Promise
{ - const routerAccountInfo = await this.getRouterAccountInfo(chain); + async owner(): Promise
{ + const routerAccountInfo = await this.getRouterAccountInfo(); if (!routerAccountInfo.owner_pub_key) - throw new Error(`No owner found for router on ${chain}`); + throw new Error(`No owner found for router on ${this.chainName}`); return routerAccountInfo.owner_pub_key.toBase58(); } - async remoteDomains(originChain: ChainName): Promise { - const routers = await this.remoteRouters(originChain); + async remoteDomains(): Promise { + const routers = await this.remoteRouters(); return routers.map((router) => router.domain); } - async remoteRouter( - originChain: ChainName, - remoteDomain: Domain, - ): Promise
{ - const routers = await this.remoteRouters(originChain); + async remoteRouter(remoteDomain: Domain): Promise
{ + const routers = await this.remoteRouters(); const addr = routers.find( (router) => router.domain === remoteDomain, )?.address; @@ -52,10 +48,8 @@ export class SealevelRouterAdapter< return addr; } - async remoteRouters( - originChain: ChainName, - ): Promise> { - const routerAccountInfo = await this.getRouterAccountInfo(originChain); + async remoteRouters(): Promise> { + const routerAccountInfo = await this.getRouterAccountInfo(); const domainToPubKey = routerAccountInfo.remote_router_pubkeys; return Array.from(domainToPubKey.entries()).map(([domain, pubKey]) => ({ domain, @@ -63,54 +57,29 @@ export class SealevelRouterAdapter< })); } - // TODO this incorrectly assumes all sealevel routers will have the TokenRouter's data schema - // This will need to change when other types of routers are supported - async getRouterAccountInfo( - chain: ChainName, - ): Promise { - const address = this.multiProvider.getChainMetadata(chain).router; - const connection = this.multiProvider.getSolanaWeb3Provider(chain); - - const msgRecipientPda = this.deriveMessageRecipientPda(address); - const accountInfo = await connection.getAccountInfo(msgRecipientPda); - if (!accountInfo) - throw new Error( - `No account info found for ${msgRecipientPda.toBase58()}}`, - ); - const accountData = deserializeUnchecked( - SealevelHyperlaneTokenDataSchema, - SealevelAccountDataWrapper, - accountInfo.data, - ); - return accountData.data as SealevelHyperlaneTokenData; + getRouterAccountInfo(): Promise<{ + owner_pub_key?: PublicKey; + interchain_security_module?: Uint8Array; + interchain_security_module_pubkey?: PublicKey; + remote_router_pubkeys: Map; + }> { + throw new Error('TODO getRouterAccountInfo not yet implemented'); } // Should match https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/sealevel/libraries/hyperlane-sealevel-token/src/processor.rs deriveMessageRecipientPda(routerAddress: Address | PublicKey): PublicKey { - const [pda] = PublicKey.findProgramAddressSync( - [ - Buffer.from('hyperlane_message_recipient'), - Buffer.from('-'), - Buffer.from('handle'), - Buffer.from('-'), - Buffer.from('account_metas'), - ], - new PublicKey(routerAddress), + return super.derivePda( + ['hyperlane_message_recipient', '-', 'handle', '-', 'account_metas'], + routerAddress, ); - return pda; } } -export class SealevelGasRouterAdapter< - ContractAddrs extends RouterAddress = RouterAddress, - > - extends SealevelRouterAdapter - implements IGasRouterAdapter +export class SealevelGasRouterAdapter + extends SealevelRouterAdapter + implements IGasRouterAdapter { - async quoteGasPayment( - _origin: ChainName, - _destination: ChainName, - ): Promise { + async quoteGasPayment(_destination: ChainName): Promise { throw new Error('Gas payments not yet supported for sealevel'); } } diff --git a/typescript/sdk/src/router/adapters/types.ts b/typescript/sdk/src/router/adapters/types.ts index e78e3b9ffa..3384d25f3e 100644 --- a/typescript/sdk/src/router/adapters/types.ts +++ b/typescript/sdk/src/router/adapters/types.ts @@ -2,28 +2,15 @@ import { Address, Domain } from '@hyperlane-xyz/utils'; import { BaseAppAdapter } from '../../app/MultiProtocolApp'; import { ChainName } from '../../types'; -import { RouterAddress } from '../types'; -export interface IRouterAdapter< - ContractAddrs extends RouterAddress = RouterAddress, -> extends BaseAppAdapter { - interchainSecurityModule(chain: ChainName): Promise
; - owner: (chain: ChainName) => Promise
; - remoteDomains(originChain: ChainName): Promise; - remoteRouter: ( - originChain: ChainName, - remoteDomain: Domain, - ) => Promise
; - remoteRouters: ( - originChain: ChainName, - ) => Promise>; +export interface IRouterAdapter extends BaseAppAdapter { + interchainSecurityModule(): Promise
; + owner: () => Promise
; + remoteDomains(): Promise; + remoteRouter: (remoteDomain: Domain) => Promise
; + remoteRouters: () => Promise>; } -export interface IGasRouterAdapter< - ContractAddrs extends RouterAddress = RouterAddress, -> extends IRouterAdapter { - quoteGasPayment: ( - origin: ChainName, - destination: ChainName, - ) => Promise; +export interface IGasRouterAdapter extends IRouterAdapter { + quoteGasPayment: (destination: ChainName) => Promise; } diff --git a/typescript/sdk/src/sealevel/serialization.ts b/typescript/sdk/src/utils/sealevelSerialization.ts similarity index 74% rename from typescript/sdk/src/sealevel/serialization.ts rename to typescript/sdk/src/utils/sealevelSerialization.ts index 6d4dd7e0ad..13b3dcacd3 100644 --- a/typescript/sdk/src/sealevel/serialization.ts +++ b/typescript/sdk/src/utils/sealevelSerialization.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ export class SealevelInstructionWrapper { instruction!: number; data!: Instr; @@ -9,6 +8,7 @@ export class SealevelInstructionWrapper { export class SealevelAccountDataWrapper { initialized!: boolean; + discriminator?: unknown; data!: T; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types constructor(public readonly fields: any) { @@ -16,11 +16,15 @@ export class SealevelAccountDataWrapper { } } -export function getSealevelAccountDataSchema(DataClass: T) { +export function getSealevelAccountDataSchema( + DataClass: T, + discriminator?: any, +) { return { kind: 'struct', fields: [ ['initialized', 'u8'], + ...(discriminator ? [['discriminator', discriminator]] : []), ['data', DataClass], ], }; diff --git a/typescript/token/src/adapters/EvmTokenAdapter.ts b/typescript/token/src/adapters/EvmTokenAdapter.ts new file mode 100644 index 0000000000..d2ab167541 --- /dev/null +++ b/typescript/token/src/adapters/EvmTokenAdapter.ts @@ -0,0 +1,212 @@ +import { BigNumber, PopulatedTransaction } from 'ethers'; + +import { + BaseEvmAdapter, + ChainName, + MultiProtocolProvider, +} from '@hyperlane-xyz/sdk'; +import { + Address, + Domain, + addressToByteHexString, + addressToBytes32, + bytes32ToAddress, + strip0x, +} from '@hyperlane-xyz/utils'; + +import { MinimalTokenMetadata } from '../config'; +import { + ERC20, + ERC20__factory, + HypERC20, + HypERC20Collateral__factory, + HypERC20__factory, +} from '../types'; + +import { + IHypTokenAdapter, + ITokenAdapter, + TransferParams, + TransferRemoteParams, +} from './ITokenAdapter'; + +// Interacts with native currencies +export class EvmNativeTokenAdapter + extends BaseEvmAdapter + implements ITokenAdapter +{ + async getBalance(address: Address): Promise { + const balance = await this.getProvider().getBalance(address); + return balance.toString(); + } + + async getMetadata(): Promise { + // TODO get metadata from chainMetadata config + throw new Error('Metadata not available to native tokens'); + } + + async populateApproveTx( + _params: TransferParams, + ): Promise { + throw new Error('Approve not required for native tokens'); + } + + async populateTransferTx({ + weiAmountOrId, + recipient, + }: TransferParams): Promise { + const value = BigNumber.from(weiAmountOrId); + return { value, to: recipient }; + } +} + +// Interacts with ERC20/721 contracts +export class EvmTokenAdapter + extends EvmNativeTokenAdapter + implements ITokenAdapter +{ + public readonly contract: T; + + constructor( + public readonly chainName: ChainName, + public readonly multiProvider: MultiProtocolProvider, + public readonly addresses: { token: Address }, + public readonly contractFactory: any = ERC20__factory, + ) { + super(chainName, multiProvider, addresses); + this.contract = contractFactory.connect( + addresses.token, + this.getProvider(), + ); + } + + override async getBalance(address: Address): Promise { + const balance = await this.contract.balanceOf(address); + return balance.toString(); + } + + override async getMetadata(isNft?: boolean): Promise { + const [decimals, symbol, name] = await Promise.all([ + isNft ? 0 : this.contract.decimals(), + this.contract.symbol(), + this.contract.name(), + ]); + return { decimals, symbol, name }; + } + + override populateApproveTx({ + weiAmountOrId, + recipient, + }: TransferParams): Promise { + return this.contract.populateTransaction.approve(recipient, weiAmountOrId); + } + + override populateTransferTx({ + weiAmountOrId, + recipient, + }: TransferParams): Promise { + return this.contract.populateTransaction.transfer(recipient, weiAmountOrId); + } +} + +// Interacts with Hyp Synthetic token contracts (aka 'HypTokens') +export class EvmHypSyntheticAdapter + extends EvmTokenAdapter + implements IHypTokenAdapter +{ + constructor( + public readonly chainName: ChainName, + public readonly multiProvider: MultiProtocolProvider, + public readonly addresses: { token: Address }, + public readonly contractFactory: any = HypERC20__factory, + ) { + super(chainName, multiProvider, addresses, contractFactory); + } + + getDomains(): Promise { + return this.contract.domains(); + } + + async getRouterAddress(domain: Domain): Promise { + const routerAddressesAsBytes32 = await this.contract.routers(domain); + // Evm addresses will be padded with 12 bytes + if (routerAddressesAsBytes32.startsWith('0x000000000000000000000000')) { + return Buffer.from( + strip0x(bytes32ToAddress(routerAddressesAsBytes32)), + 'hex', + ); + // Otherwise leave the address unchanged + } else { + return Buffer.from(strip0x(routerAddressesAsBytes32), 'hex'); + } + } + + async getAllRouters(): Promise> { + const domains = await this.getDomains(); + const routers: Buffer[] = await Promise.all( + domains.map((d) => this.getRouterAddress(d)), + ); + return domains.map((d, i) => ({ domain: d, address: routers[i] })); + } + + async quoteGasPayment(destination: Domain): Promise { + const gasPayment = await this.contract.quoteGasPayment(destination); + return gasPayment.toString(); + } + + populateTransferRemoteTx({ + weiAmountOrId, + destination, + recipient, + txValue, + }: TransferRemoteParams): Promise { + const recipBytes32 = addressToBytes32(addressToByteHexString(recipient)); + return this.contract.populateTransaction.transferRemote( + destination, + recipBytes32, + weiAmountOrId, + { + // Note, typically the value is the gas payment as quoted by IGP + value: txValue, + }, + ); + } +} + +// Interacts with HypCollateral and HypNative contracts +export class EvmHypCollateralAdapter + extends EvmHypSyntheticAdapter + implements IHypTokenAdapter +{ + constructor( + public readonly chainName: ChainName, + public readonly multiProvider: MultiProtocolProvider, + public readonly addresses: { token: Address }, + public readonly contractFactory: any = HypERC20Collateral__factory, + ) { + super(chainName, multiProvider, addresses, contractFactory); + } + + override getMetadata(): Promise { + // TODO pass through metadata from wrapped token or chainMetadata config + throw new Error( + 'Metadata not available for HypCollateral/HypNative contract.', + ); + } + + override populateApproveTx( + _params: TransferParams, + ): Promise { + throw new Error( + 'Approve not applicable to HypCollateral/HypNative contract.', + ); + } + + override populateTransferTx( + _params: TransferParams, + ): Promise { + throw new Error( + 'Local transfer not supported for HypCollateral/HypNative contract.', + ); + } +} diff --git a/typescript/token/src/adapters/ITokenAdapter.ts b/typescript/token/src/adapters/ITokenAdapter.ts index e78da71790..b41c42ebae 100644 --- a/typescript/token/src/adapters/ITokenAdapter.ts +++ b/typescript/token/src/adapters/ITokenAdapter.ts @@ -1,8 +1,6 @@ import { Address, Domain } from '@hyperlane-xyz/utils'; -import { ERC20Metadata } from '../config'; - -export type MinimalTokenMetadata = Omit; +import { MinimalTokenMetadata } from '../config'; export interface TransferParams { weiAmountOrId: string | number; @@ -12,7 +10,6 @@ export interface TransferParams { // Included here optionally to keep Adapter types simple fromTokenAccount?: Address; fromAccountOwner?: Address; - mailbox?: Address; } export interface TransferRemoteParams extends TransferParams { @@ -21,7 +18,7 @@ export interface TransferRemoteParams extends TransferParams { } export interface ITokenAdapter { - getBalance(address?: Address): Promise; + getBalance(address: Address): Promise; getMetadata(isNft?: boolean): Promise; populateApproveTx(TransferParams: TransferParams): unknown | Promise; populateTransferTx( diff --git a/typescript/token/src/adapters/SealevelTokenAdapter.ts b/typescript/token/src/adapters/SealevelTokenAdapter.ts index 45e382bd51..f88758d0ba 100644 --- a/typescript/token/src/adapters/SealevelTokenAdapter.ts +++ b/typescript/token/src/adapters/SealevelTokenAdapter.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, @@ -7,7 +6,6 @@ import { } from '@solana/spl-token'; import { AccountMeta, - Connection, Keypair, PublicKey, SystemProgram, @@ -18,41 +16,47 @@ import BigNumber from 'bignumber.js'; import { deserializeUnchecked, serialize } from 'borsh'; import { + BaseSealevelAdapter, + ChainName, + MultiProtocolProvider, SEALEVEL_SPL_NOOP_ADDRESS, SealevelAccountDataWrapper, - SealevelHypTokenInstruction, - SealevelHyperlaneTokenData, - SealevelHyperlaneTokenDataSchema, SealevelInstructionWrapper, - SealevelTransferRemoteInstruction, - SealevelTransferRemoteSchema, + SealevelInterchainGasPaymasterType, + SealevelOverheadIgpAdapter, } from '@hyperlane-xyz/sdk'; import { Address, Domain, addressToBytes, + eqAddress, isZeroishAddress, } from '@hyperlane-xyz/utils'; +import { MinimalTokenMetadata } from '../config'; + import { IHypTokenAdapter, ITokenAdapter, - MinimalTokenMetadata, TransferParams, TransferRemoteParams, } from './ITokenAdapter'; +import { + SealevelHypTokenInstruction, + SealevelHyperlaneTokenData, + SealevelHyperlaneTokenDataSchema, + SealevelTransferRemoteInstruction, + SealevelTransferRemoteSchema, +} from './serialization'; // author @tkporter @jmrossy // Interacts with native currencies -export class SealevelNativeTokenAdapter implements ITokenAdapter { - constructor( - public readonly connection: Connection, - public readonly signerAddress?: Address, - ) {} - - async getBalance(address?: Address): Promise { - const pubKey = resolveAddress(address, this.signerAddress); - const balance = await this.connection.getBalance(pubKey); +export class SealevelNativeTokenAdapter + extends BaseSealevelAdapter + implements ITokenAdapter +{ + async getBalance(address: Address): Promise { + const balance = await this.getProvider().getBalance(new PublicKey(address)); return balance.toString(); } @@ -69,10 +73,11 @@ export class SealevelNativeTokenAdapter implements ITokenAdapter { recipient, fromAccountOwner, }: TransferParams): Transaction { - const fromPubkey = resolveAddress(fromAccountOwner, this.signerAddress); + if (!fromAccountOwner) + throw new Error('fromAccountOwner required for Sealevel'); return new Transaction().add( SystemProgram.transfer({ - fromPubkey, + fromPubkey: new PublicKey(fromAccountOwner), toPubkey: new PublicKey(recipient), lamports: new BigNumber(weiAmountOrId).toNumber(), }), @@ -81,25 +86,31 @@ export class SealevelNativeTokenAdapter implements ITokenAdapter { } // Interacts with SPL token programs -export class SealevelTokenAdapter implements ITokenAdapter { +export class SealevelTokenAdapter + extends BaseSealevelAdapter + implements ITokenAdapter +{ public readonly tokenProgramPubKey: PublicKey; constructor( - public readonly connection: Connection, - public readonly tokenProgramId: Address, + public readonly chainName: ChainName, + public readonly multiProvider: MultiProtocolProvider, + public readonly addresses: { token: Address }, public readonly isSpl2022: boolean = false, - public readonly signerAddress?: Address, ) { - this.tokenProgramPubKey = new PublicKey(tokenProgramId); + super(chainName, multiProvider, addresses); + this.tokenProgramPubKey = new PublicKey(addresses.token); } async getBalance(owner: Address): Promise { const tokenPubKey = this.deriveAssociatedTokenAccount(new PublicKey(owner)); - const response = await this.connection.getTokenAccountBalance(tokenPubKey); + const response = await this.getProvider().getTokenAccountBalance( + tokenPubKey, + ); return response.value.amount; } - async getMetadata(isNft?: boolean): Promise { + async getMetadata(_isNft?: boolean): Promise { // TODO solana support return { decimals: 9, symbol: 'SPL', name: 'SPL Token' }; } @@ -114,16 +125,15 @@ export class SealevelTokenAdapter implements ITokenAdapter { fromAccountOwner, fromTokenAccount, }: TransferParams): Transaction { - if (!fromTokenAccount) throw new Error('No fromTokenAccount provided'); - const fromWalletPubKey = resolveAddress( - fromAccountOwner, - this.signerAddress, - ); + if (!fromTokenAccount) + throw new Error('fromTokenAccount required for Sealevel'); + if (!fromAccountOwner) + throw new Error('fromAccountOwner required for Sealevel'); return new Transaction().add( createTransferInstruction( new PublicKey(fromTokenAccount), new PublicKey(recipient), - fromWalletPubKey, + new PublicKey(fromAccountOwner), new BigNumber(weiAmountOrId).toNumber(), ), ); @@ -148,32 +158,41 @@ export abstract class SealevelHypTokenAdapter implements IHypTokenAdapter { public readonly warpProgramPubKey: PublicKey; + protected cachedTokenAccountData: SealevelHyperlaneTokenData | undefined; constructor( - public readonly connection: Connection, - public readonly warpRouteProgramId: Address, - public readonly tokenProgramId: Address, + public readonly chainName: ChainName, + public readonly multiProvider: MultiProtocolProvider, + public readonly addresses: { + token: Address; + warpRouter: Address; + mailbox: Address; + }, public readonly isSpl2022: boolean = false, - public readonly signerAddress?: Address, ) { // Pass in placeholder address to avoid errors for native token addresses (which as represented here as 0s) - const superTokenProgramId = isZeroishAddress(tokenProgramId) + const superTokenProgramId = isZeroishAddress(addresses.token) ? SystemProgram.programId.toBase58() - : tokenProgramId; - super(connection, superTokenProgramId, isSpl2022, signerAddress); - this.warpProgramPubKey = new PublicKey(warpRouteProgramId); + : addresses.token; + super(chainName, multiProvider, { token: superTokenProgramId }, isSpl2022); + this.warpProgramPubKey = new PublicKey(addresses.warpRouter); } async getTokenAccountData(): Promise { - const tokenPda = this.deriveHypTokenAccount(); - const accountInfo = await this.connection.getAccountInfo(tokenPda); - if (!accountInfo) throw new Error(`No account info found for ${tokenPda}`); - const wrappedData = deserializeUnchecked( - SealevelHyperlaneTokenDataSchema, - SealevelAccountDataWrapper, - accountInfo.data, - ); - return wrappedData.data as SealevelHyperlaneTokenData; + if (!this.cachedTokenAccountData) { + const tokenPda = this.deriveHypTokenAccount(); + const accountInfo = await this.getProvider().getAccountInfo(tokenPda); + if (!accountInfo) + throw new Error(`No account info found for ${tokenPda}`); + const wrappedData = deserializeUnchecked( + SealevelHyperlaneTokenDataSchema, + SealevelAccountDataWrapper, + accountInfo.data, + ); + this.cachedTokenAccountData = + wrappedData.data as SealevelHyperlaneTokenData; + } + return this.cachedTokenAccountData; } override async getMetadata(): Promise { @@ -207,7 +226,7 @@ export abstract class SealevelHypTokenAdapter })); } - async quoteGasPayment(destination: Domain): Promise { + async quoteGasPayment(_destination: Domain): Promise { // TODO Solana support return '0'; } @@ -217,20 +236,19 @@ export abstract class SealevelHypTokenAdapter destination, recipient, fromAccountOwner, - mailbox, }: TransferRemoteParams): Promise { - if (!mailbox) throw new Error('No mailbox provided'); - const fromWalletPubKey = resolveAddress( - fromAccountOwner, - this.signerAddress, - ); + if (!fromAccountOwner) + throw new Error('fromAccountOwner required for Sealevel'); const randomWallet = Keypair.generate(); - const mailboxPubKey = new PublicKey(mailbox); - const keys = this.getTransferInstructionKeyList( - fromWalletPubKey, - mailboxPubKey, - randomWallet.publicKey, - ); + const fromWalletPubKey = new PublicKey(fromAccountOwner); + const mailboxPubKey = new PublicKey(this.addresses.mailbox); + + const keys = this.getTransferInstructionKeyList({ + sender: fromWalletPubKey, + mailbox: mailboxPubKey, + randomWallet: randomWallet.publicKey, + igp: await this.getIgpKeys(), + }); const value = new SealevelInstructionWrapper({ instruction: SealevelHypTokenInstruction.TransferRemote, @@ -254,7 +272,7 @@ export abstract class SealevelHypTokenAdapter }); const recentBlockhash = ( - await this.connection.getLatestBlockhash('finalized') + await this.getProvider().getLatestBlockhash('finalized') ).blockhash; // @ts-ignore Workaround for bug in the web3 lib, sometimes uses recentBlockhash and sometimes uses blockhash const tx = new Transaction({ @@ -266,12 +284,44 @@ export abstract class SealevelHypTokenAdapter return tx; } - getTransferInstructionKeyList( - sender: PublicKey, - mailbox: PublicKey, - randomWallet: PublicKey, - ): Array { - return [ + async getIgpKeys() { + const tokenData = await this.getTokenAccountData(); + if (!tokenData.interchain_gas_paymaster) return undefined; + const igpConfig = tokenData.interchain_gas_paymaster; + if (igpConfig.type === SealevelInterchainGasPaymasterType.Igp) { + return { + programId: igpConfig.program_id_pubkey, + }; + } else if ( + igpConfig.type === SealevelInterchainGasPaymasterType.OverheadIgp + ) { + if (!igpConfig.igp_account_pub_key) { + throw new Error('igpAccount field expected for Sealevel Overhead IGP'); + } + const overheadAdapter = new SealevelOverheadIgpAdapter( + this.chainName, + this.multiProvider, + { igp: igpConfig.igp_account_pub_key.toBase58() }, + ); + const overheadAccountInfo = await overheadAdapter.getAccountInfo(); + return { + programId: igpConfig.program_id_pubkey, + igpAccount: igpConfig.igp_account_pub_key, + innerIgpAccount: overheadAccountInfo.inner_pub_key, + }; + } else { + throw new Error(`Unsupported IGP type ${igpConfig.type}`); + } + } + + // Should match https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/sealevel/libraries/hyperlane-sealevel-token/src/processor.rs#L257-L274 + getTransferInstructionKeyList({ + sender, + mailbox, + randomWallet, + igp, + }: KeyListParams): Array { + let keys = [ // 0. [executable] The system program. { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, // 1. [executable] The spl_noop program. @@ -305,31 +355,75 @@ export abstract class SealevelHypTokenAdapter // 7. [signer] Unique message account. { pubkey: randomWallet, isSigner: true, isWritable: false }, // 8. [writeable] Message storage PDA. - // prettier-ignore - { pubkey: this.deriveMsgStorageAccount(mailbox, randomWallet), isSigner: false, isWritable: true, }, + { + pubkey: this.deriveMsgStorageAccount(mailbox, randomWallet), + isSigner: false, + isWritable: true, + }, ]; + if (igp) { + keys = [ + ...keys, + // 9. [executable] The IGP program. + { pubkey: igp.programId, isSigner: false, isWritable: false }, + // 10. [writeable] The IGP program data. + { + pubkey: SealevelOverheadIgpAdapter.deriveIgpProgramPda(igp.programId), + isSigner: false, + isWritable: true, + }, + // 11. [writeable] Gas payment PDA. + { + pubkey: SealevelOverheadIgpAdapter.deriveGasPaymentPda( + igp.programId, + randomWallet, + ), + isSigner: false, + isWritable: true, + }, + ]; + if (igp.igpAccount && igp.innerIgpAccount) { + keys = [ + ...keys, + // 12. [] OPTIONAL - The Overhead IGP account, if the configured IGP is an Overhead IGP + { + pubkey: igp.igpAccount, + isSigner: false, + isWritable: false, + }, + // 13. [writeable] The Overhead's inner IGP account + { + pubkey: igp.innerIgpAccount, + isSigner: false, + isWritable: true, + }, + ]; + } else { + keys = [ + ...keys, + // 12. [writeable] The IGP account. + { + pubkey: igp.programId, + isSigner: false, + isWritable: true, + }, + ]; + } + } + return keys; } // https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/sealevel/programs/mailbox/src/pda_seeds.rs#L19 deriveMailboxOutboxAccount(mailbox: PublicKey): PublicKey { - const [pda] = PublicKey.findProgramAddressSync( - [Buffer.from('hyperlane'), Buffer.from('-'), Buffer.from('outbox')], - mailbox, - ); - return pda; + return super.derivePda(['hyperlane', '-', 'outbox'], mailbox); } // https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/sealevel/programs/mailbox/src/pda_seeds.rs#L57 deriveMessageDispatchAuthorityAccount(): PublicKey { - const [pda] = PublicKey.findProgramAddressSync( - [ - Buffer.from('hyperlane_dispatcher'), - Buffer.from('-'), - Buffer.from('dispatch_authority'), - ], + return super.derivePda( + ['hyperlane_dispatcher', '-', 'dispatch_authority'], this.warpProgramPubKey, ); - return pda; } // https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/sealevel/programs/mailbox/src/pda_seeds.rs#L33-L37 @@ -337,32 +431,24 @@ export abstract class SealevelHypTokenAdapter mailbox: PublicKey, randomWalletPubKey: PublicKey, ): PublicKey { - const [pda] = PublicKey.findProgramAddressSync( + return super.derivePda( [ - Buffer.from('hyperlane'), - Buffer.from('-'), - Buffer.from('dispatched_message'), - Buffer.from('-'), + 'hyperlane', + '-', + 'dispatched_message', + '-', randomWalletPubKey.toBuffer(), ], mailbox, ); - return pda; } // Should match https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/sealevel/libraries/hyperlane-sealevel-token/src/processor.rs#LL49C1-L53C30 deriveHypTokenAccount(): PublicKey { - const [pda] = PublicKey.findProgramAddressSync( - [ - Buffer.from('hyperlane_message_recipient'), - Buffer.from('-'), - Buffer.from('handle'), - Buffer.from('-'), - Buffer.from('account_metas'), - ], + return super.derivePda( + ['hyperlane_message_recipient', '-', 'handle', '-', 'account_metas'], this.warpProgramPubKey, ); - return pda; } } @@ -371,22 +457,20 @@ export class SealevelHypNativeAdapter extends SealevelHypTokenAdapter { public readonly wrappedNative: SealevelNativeTokenAdapter; constructor( - public readonly connection: Connection, - public readonly warpRouteProgramId: Address, - public readonly tokenProgramId: Address, + public readonly chainName: ChainName, + public readonly multiProvider: MultiProtocolProvider, + public readonly addresses: { + token: Address; + warpRouter: Address; + mailbox: Address; + }, public readonly isSpl2022: boolean = false, - public readonly signerAddress?: Address, ) { - super( - connection, - warpRouteProgramId, - tokenProgramId, - isSpl2022, - signerAddress, - ); + super(chainName, multiProvider, addresses, isSpl2022); this.wrappedNative = new SealevelNativeTokenAdapter( - connection, - signerAddress, + chainName, + multiProvider, + {}, ); } @@ -398,13 +482,9 @@ export class SealevelHypNativeAdapter extends SealevelHypTokenAdapter { return this.wrappedNative.getMetadata(); } - getTransferInstructionKeyList( - sender: PublicKey, - mailbox: PublicKey, - randomWallet: PublicKey, - ): Array { + getTransferInstructionKeyList(params: KeyListParams): Array { return [ - ...super.getTransferInstructionKeyList(sender, mailbox, randomWallet), + ...super.getTransferInstructionKeyList(params), // 9. [executable] The system program. { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, // 10. [writeable] The native token collateral PDA account. @@ -418,15 +498,10 @@ export class SealevelHypNativeAdapter extends SealevelHypTokenAdapter { // https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/sealevel/programs/hyperlane-sealevel-token-native/src/plugin.rs#L26 deriveNativeTokenCollateralAccount(): PublicKey { - const [pda] = PublicKey.findProgramAddressSync( - [ - Buffer.from('hyperlane_token'), - Buffer.from('-'), - Buffer.from('native_collateral'), - ], + return super.derivePda( + ['hyperlane_token', '-', 'native_collateral'], this.warpProgramPubKey, ); - return pda; } } @@ -437,9 +512,9 @@ export class SealevelHypCollateralAdapter extends SealevelHypTokenAdapter { // This is because collateral warp routes don't hold escrowed collateral // tokens in their associated token account - instead, they hold them in // the escrow account. - if (owner === this.warpRouteProgramId) { + if (eqAddress(owner, this.addresses.warpRouter)) { const collateralAccount = this.deriveEscrowAccount(); - const response = await this.connection.getTokenAccountBalance( + const response = await this.getProvider().getTokenAccountBalance( collateralAccount, ); return response.value.amount; @@ -449,19 +524,17 @@ export class SealevelHypCollateralAdapter extends SealevelHypTokenAdapter { } override getTransferInstructionKeyList( - sender: PublicKey, - mailbox: PublicKey, - randomWallet: PublicKey, + params: KeyListParams, ): Array { return [ - ...super.getTransferInstructionKeyList(sender, mailbox, randomWallet), + ...super.getTransferInstructionKeyList(params), /// 9. [executable] The SPL token program for the mint. { pubkey: this.getTokenProgramId(), isSigner: false, isWritable: false }, /// 10. [writeable] The mint. { pubkey: this.tokenProgramPubKey, isSigner: false, isWritable: true }, /// 11. [writeable] The token sender's associated token account, from which tokens will be sent. { - pubkey: this.deriveAssociatedTokenAccount(sender), + pubkey: this.deriveAssociatedTokenAccount(params.sender), isSigner: false, isWritable: true, }, @@ -471,23 +544,20 @@ export class SealevelHypCollateralAdapter extends SealevelHypTokenAdapter { } deriveEscrowAccount(): PublicKey { - const [pda] = PublicKey.findProgramAddressSync( - [Buffer.from('hyperlane_token'), Buffer.from('-'), Buffer.from('escrow')], + return super.derivePda( + ['hyperlane_token', '-', 'escrow'], this.warpProgramPubKey, ); - return pda; } } // Interacts with Hyp Synthetic token programs (aka 'HypTokens') export class SealevelHypSyntheticAdapter extends SealevelHypTokenAdapter { override getTransferInstructionKeyList( - sender: PublicKey, - mailbox: PublicKey, - randomWallet: PublicKey, + params: KeyListParams, ): Array { return [ - ...super.getTransferInstructionKeyList(sender, mailbox, randomWallet), + ...super.getTransferInstructionKeyList(params), /// 9. [executable] The spl_token_2022 program. { pubkey: TOKEN_2022_PROGRAM_ID, isSigner: false, isWritable: false }, /// 10. [writeable] The mint / mint authority PDA account. @@ -498,7 +568,7 @@ export class SealevelHypSyntheticAdapter extends SealevelHypTokenAdapter { }, /// 11. [writeable] The token sender's associated token account, from which tokens will be burned. { - pubkey: this.deriveAssociatedTokenAccount(sender), + pubkey: this.deriveAssociatedTokenAccount(params.sender), isSigner: false, isWritable: true, }, @@ -507,16 +577,17 @@ export class SealevelHypSyntheticAdapter extends SealevelHypTokenAdapter { override async getBalance(owner: Address): Promise { const tokenPubKey = this.deriveAssociatedTokenAccount(new PublicKey(owner)); - const response = await this.connection.getTokenAccountBalance(tokenPubKey); + const response = await this.getProvider().getTokenAccountBalance( + tokenPubKey, + ); return response.value.amount; } deriveMintAuthorityAccount(): PublicKey { - const [pda] = PublicKey.findProgramAddressSync( - [Buffer.from('hyperlane_token'), Buffer.from('-'), Buffer.from('mint')], + return super.derivePda( + ['hyperlane_token', '-', 'mint'], this.warpProgramPubKey, ); - return pda; } override deriveAssociatedTokenAccount(owner: PublicKey): PublicKey { @@ -529,8 +600,13 @@ export class SealevelHypSyntheticAdapter extends SealevelHypTokenAdapter { } } -function resolveAddress(address1?: Address, address2?: Address): PublicKey { - if (address1) return new PublicKey(address1); - else if (address2) return new PublicKey(address2); - else throw new Error('No address provided'); +interface KeyListParams { + sender: PublicKey; + mailbox: PublicKey; + randomWallet: PublicKey; + igp?: { + programId: PublicKey; + igpAccount?: PublicKey; + innerIgpAccount?: PublicKey; + }; } diff --git a/typescript/sdk/src/sealevel/tokenSerialization.ts b/typescript/token/src/adapters/serialization.ts similarity index 86% rename from typescript/sdk/src/sealevel/tokenSerialization.ts rename to typescript/token/src/adapters/serialization.ts index fd0a3b5637..8ff30424df 100644 --- a/typescript/sdk/src/sealevel/tokenSerialization.ts +++ b/typescript/token/src/adapters/serialization.ts @@ -1,17 +1,13 @@ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { PublicKey } from '@solana/web3.js'; -import { Domain } from '@hyperlane-xyz/utils'; - import { SealevelAccountDataWrapper, SealevelInstructionWrapper, + SealevelInterchainGasPaymasterConfig, + SealevelInterchainGasPaymasterConfigSchema, getSealevelAccountDataSchema, -} from './serialization'; - -// TODO move this code to the token package -// after we've defined more accurate data schemas for Routers. -// Currently the RouterAdapters use this schema as a placeholder +} from '@hyperlane-xyz/sdk'; +import { Domain } from '@hyperlane-xyz/utils'; /** * Hyperlane Token Borsh Schema @@ -39,11 +35,7 @@ export class SealevelHyperlaneTokenData { interchain_security_module?: Uint8Array; interchain_security_module_pubkey?: PublicKey; // The interchain gas paymaster - interchain_gas_paymaster?: { - program_id: Uint8Array; - type: number; - account: Uint8Array; - }; + interchain_gas_paymaster?: SealevelInterchainGasPaymasterConfig; interchain_gas_paymaster_pubkey?: PublicKey; interchain_gas_paymaster_account_pubkey?: PublicKey; // Gas amounts by destination @@ -64,8 +56,8 @@ export class SealevelHyperlaneTokenData { ? new PublicKey(this.interchain_gas_paymaster.program_id) : undefined; this.interchain_gas_paymaster_account_pubkey = this.interchain_gas_paymaster - ?.account - ? new PublicKey(this.interchain_gas_paymaster.account) + ?.igp_account + ? new PublicKey(this.interchain_gas_paymaster.igp_account) : undefined; this.remote_router_pubkeys = new Map(); if (this.remote_routers) { @@ -98,14 +90,7 @@ export const SealevelHyperlaneTokenDataSchema = new Map([ 'interchain_gas_paymaster', { kind: 'option', - type: { - kind: 'struct', - fields: [ - ['program_id', [32]], - ['type', 'u8'], - ['account', [32]], - ], - }, + type: SealevelInterchainGasPaymasterConfig, }, ], ['destination_gas', { kind: 'map', key: 'u32', value: 'u64' }], @@ -113,6 +98,10 @@ export const SealevelHyperlaneTokenDataSchema = new Map([ ], }, ], + [ + SealevelInterchainGasPaymasterConfig, + SealevelInterchainGasPaymasterConfigSchema, + ], ]); /** diff --git a/typescript/token/src/app.ts b/typescript/token/src/app.ts deleted file mode 100644 index 03d0b63c5e..0000000000 --- a/typescript/token/src/app.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { BigNumberish } from 'ethers'; - -import { ChainName, HyperlaneContracts, RouterApp } from '@hyperlane-xyz/sdk'; -import { Address } from '@hyperlane-xyz/utils'; - -import { - HypERC20Factories, - HypERC721Factories, - TokenFactories, -} from './contracts'; -import { TokenRouter } from './types'; - -class HyperlaneTokenApp< - Factories extends TokenFactories, -> extends RouterApp { - router(contracts: HyperlaneContracts): TokenRouter { - return contracts.router; - } - - async transfer( - origin: ChainName, - destination: ChainName, - recipient: Address, - amountOrId: BigNumberish, - ) { - const originRouter = this.getContracts(origin).router; - const destProvider = this.multiProvider.getProvider(destination); - const destinationNetwork = await destProvider.getNetwork(); - const gasPayment = await originRouter.quoteGasPayment( - destinationNetwork.chainId, - ); - return this.multiProvider.handleTx( - origin, - originRouter.transferRemote( - destinationNetwork.chainId, - recipient, - amountOrId, - { - value: gasPayment, - }, - ), - ); - } -} - -export class HypERC20App extends HyperlaneTokenApp { - async transfer( - origin: ChainName, - destination: ChainName, - recipient: Address, - amount: BigNumberish, - ) { - const originRouter = this.getContracts(origin).router; - const signerAddress = await this.multiProvider.getSignerAddress(origin); - const balance = await originRouter.balanceOf(signerAddress); - if (balance.lt(amount)) - console.warn( - `Signer ${signerAddress} has insufficient balance ${balance}, needs ${amount} on ${origin}`, - ); - return super.transfer(origin, destination, recipient, amount); - } -} - -export class HypERC721App extends HyperlaneTokenApp { - async transfer( - origin: ChainName, - destination: ChainName, - recipient: Address, - tokenId: BigNumberish, - ) { - const originRouter = this.getContracts(origin).router; - const signerAddress = await this.multiProvider.getSignerAddress(origin); - const owner = await originRouter.ownerOf(tokenId); - if (signerAddress != owner) - console.warn( - `Signer ${signerAddress} not owner of token ${tokenId} on ${origin}`, - ); - return super.transfer(origin, destination, recipient, tokenId); - } -} diff --git a/typescript/token/src/config.ts b/typescript/token/src/config.ts index cd17e00356..6212ca5ea8 100644 --- a/typescript/token/src/config.ts +++ b/typescript/token/src/config.ts @@ -22,6 +22,7 @@ export type TokenDecimals = { }; export type ERC20Metadata = TokenMetadata & TokenDecimals; +export type MinimalTokenMetadata = Omit; export const isTokenMetadata = (metadata: any): metadata is TokenMetadata => metadata.name && metadata.symbol && metadata.totalSupply !== undefined; // totalSupply can be 0 diff --git a/typescript/token/src/index.ts b/typescript/token/src/index.ts index 12013e9f82..3025af926c 100644 --- a/typescript/token/src/index.ts +++ b/typescript/token/src/index.ts @@ -1,5 +1,30 @@ -export { SealevelHypCollateralAdapter } from './adapters/SealevelTokenAdapter'; -export { HypERC20App, HypERC721App } from './app'; +export { + EvmHypCollateralAdapter, + EvmHypSyntheticAdapter, + EvmNativeTokenAdapter, + EvmTokenAdapter, +} from './adapters/EvmTokenAdapter'; +export { + IHypTokenAdapter, + ITokenAdapter, + TransferParams, + TransferRemoteParams, +} from './adapters/ITokenAdapter'; +export { + SealevelHypCollateralAdapter, + SealevelHypNativeAdapter, + SealevelHypSyntheticAdapter, + SealevelHypTokenAdapter, + SealevelNativeTokenAdapter, + SealevelTokenAdapter, +} from './adapters/SealevelTokenAdapter'; +export { + SealevelHypTokenInstruction, + SealevelHyperlaneTokenData, + SealevelHyperlaneTokenDataSchema, + SealevelTransferRemoteInstruction, + SealevelTransferRemoteSchema, +} from './adapters/serialization'; export { CollateralConfig, ERC20Metadata, @@ -10,6 +35,7 @@ export { HypERC721CollateralConfig, HypERC721Config, HypNativeConfig, + MinimalTokenMetadata, NativeConfig, SyntheticConfig, TokenConfig, diff --git a/typescript/utils/index.ts b/typescript/utils/index.ts index 0a1f1752d7..e0b66e099e 100644 --- a/typescript/utils/index.ts +++ b/typescript/utils/index.ts @@ -5,6 +5,7 @@ export { addressToBytesEvm, addressToBytesSol, bytes32ToAddress, + bytesToProtocolAddress, capitalizeAddress, convertToProtocolAddress, ensure0x, @@ -28,6 +29,7 @@ export { strip0x, } from './src/addresses'; export { + convertDecimals, eqAmountApproximate, fromWei, fromWeiRounded, @@ -48,7 +50,6 @@ export { BigNumberMax, BigNumberMin, bigToFixed, - convertDecimalValue, fixedToBig, isBigNumberish, isZeroish, @@ -96,7 +97,6 @@ export { Address, AddressBytes32, CallData, - ChainCaip19Id, ChainCaip2Id, Checkpoint, Domain, @@ -112,6 +112,7 @@ export { S3Checkpoint, S3CheckpointWithId, SignatureLike, + TokenCaip19Id, } from './src/types'; export { assert } from './src/validation'; export { BaseValidator, Validator } from './src/validator'; diff --git a/typescript/utils/src/addresses.ts b/typescript/utils/src/addresses.ts index 12cb1b35a4..f415677275 100644 --- a/typescript/utils/src/addresses.ts +++ b/typescript/utils/src/addresses.ts @@ -202,6 +202,19 @@ export function addressToByteHexString( return '0x' + Buffer.from(addressToBytes(address, protocol)).toString('hex'); } +export function bytesToProtocolAddress( + bytes: Buffer, + toProtocol: ProtocolType, +) { + if (toProtocol === ProtocolType.Sealevel) { + return new PublicKey(bytes).toBase58(); + } else if (toProtocol === ProtocolType.Ethereum) { + return bytes32ToAddress(bytes.toString('hex')); + } else { + throw new Error(`Unsupported protocol for address ${toProtocol}`); + } +} + export function convertToProtocolAddress( address: string, protocol: ProtocolType, diff --git a/typescript/utils/src/amount.ts b/typescript/utils/src/amount.ts index 8d5cc2691a..b4c115b86e 100644 --- a/typescript/utils/src/amount.ts +++ b/typescript/utils/src/amount.ts @@ -2,7 +2,7 @@ import { formatUnits, parseUnits } from '@ethersproject/units'; import BigNumber from 'bignumber.js'; const DEFAULT_MIN_ROUNDED_VALUE = 0.00001; -const DEFAULT_DISPLAY_DECIMALS = 5; +const DEFAULT_DISPLAY_DECIMALS = 4; const DEFAULT_TOKEN_DECIMALS = 18; type NumberT = BigNumber.Value; @@ -35,10 +35,11 @@ export function fromWeiRounded( // If amount is less than min value if (amount.lt(DEFAULT_MIN_ROUNDED_VALUE)) { if (roundDownIfSmall) return '0'; - else return DEFAULT_MIN_ROUNDED_VALUE.toString(); + return amount.toString(10); } - return amount.toFixed(DEFAULT_DISPLAY_DECIMALS).toString(); + const displayDecimals = amount.gte(10000) ? 2 : DEFAULT_DISPLAY_DECIMALS; + return amount.toFixed(displayDecimals).toString(); } export function toWei( @@ -46,7 +47,11 @@ export function toWei( decimals = DEFAULT_TOKEN_DECIMALS, ): BigNumber { if (!value) return new BigNumber(0); - const valueString = value.toString().trim(); + // First convert to a BigNumber, and then call `toString` with the + // explicit radix 10 such that the result is formatted as a base-10 string + // and not in scientific notation. + const valueBN = new BigNumber(value); + const valueString = valueBN.toString(10).trim(); const components = valueString.split('.'); if (components.length === 1) { return new BigNumber(parseUnits(valueString, decimals).toString()); @@ -83,3 +88,32 @@ export function eqAmountApproximate( // Is difference btwn amount and balance less than min amount shown for token return amountInWei1.minus(amountInWei2).abs().lt(minValueWei); } + +/** + * Converts a value with `fromDecimals` decimals to a value with `toDecimals` decimals. + * Incurs a loss of precision when `fromDecimals` > `toDecimals`. + * @param fromDecimals The number of decimals `value` has. + * @param toDecimals The number of decimals to convert `value` to. + * @param value The value to convert. + * @returns `value` represented with `toDecimals` decimals. + */ +export function convertDecimals( + fromDecimals: number, + toDecimals: number, + value: NumberT, +) { + const amount = new BigNumber(value); + + if (fromDecimals === toDecimals) return amount; + else if (fromDecimals > toDecimals) { + const difference = fromDecimals - toDecimals; + return amount + .div(new BigNumber(10).pow(difference)) + .integerValue(BigNumber.ROUND_FLOOR); + } + // fromDecimals < toDecimals + else { + const difference = toDecimals - fromDecimals; + return amount.times(new BigNumber(10).pow(difference)); + } +} diff --git a/typescript/utils/src/big-numbers.ts b/typescript/utils/src/big-numbers.ts index 7462ca0d32..4e8b6dd733 100644 --- a/typescript/utils/src/big-numbers.ts +++ b/typescript/utils/src/big-numbers.ts @@ -63,29 +63,6 @@ export function mulBigAndFixed( return fixedToBig(fixed.mulUnsafe(bigToFixed(big)), ceil); } -/** - * Converts a value with `fromDecimals` decimals to a value with `toDecimals` decimals. - * Incurs a loss of precision when `fromDecimals` > `toDecimals`. - * @param value The value to convert. - * @param fromDecimals The number of decimals `value` has. - * @param toDecimals The number of decimals to convert `value` to. - * @returns `value` represented with `toDecimals` decimals. - */ -export function convertDecimalValue( - value: BigNumber, - fromDecimals: number, - toDecimals: number, -): BigNumber { - if (fromDecimals === toDecimals) { - return value; - } else if (fromDecimals > toDecimals) { - return value.div(10 ** (fromDecimals - toDecimals)); - } else { - // if (fromDecimals < toDecimals) - return value.mul(10 ** (toDecimals - fromDecimals)); - } -} - export function BigNumberMin(bn1: BigNumber, bn2: BigNumber) { return bn1.gte(bn2) ? bn2 : bn1; } diff --git a/typescript/utils/src/types.ts b/typescript/utils/src/types.ts index d46a0723fd..525f26feb2 100644 --- a/typescript/utils/src/types.ts +++ b/typescript/utils/src/types.ts @@ -18,7 +18,7 @@ export type Domain = number; export type Address = string; export type AddressBytes32 = string; export type ChainCaip2Id = `${string}:${string}`; // e.g. ethereum:1 or solana:mainnet-beta -export type ChainCaip19Id = `${string}:${string}/${string}:${string}`; // e.g. ethereum:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f +export type TokenCaip19Id = `${string}:${string}/${string}:${string}`; // e.g. ethereum:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f export type HexString = string; // copied from node_modules/@ethersproject/bytes/src.ts/index.ts