Multi-protocol Kathy (#2559)

### Description

- Add support for multi-protocol to the HelloWorld app and Kathy service
- Create a new MultiProtocolCore

### Related issues

https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2503

### Backward compatibility

Yes

### Testing

- Tested new Kathy abstraction on existing EVM while we wait for
Sealevel Kathy to be unblocked
- Tested using local config stubs for both EVM and Sealevel
pull/2704/head
J M Rossy 1 year ago committed by GitHub
parent 2d8fced77a
commit f08ceb187f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      typescript/helloworld/src/app/app.ts
  2. 4
      typescript/helloworld/src/app/types.ts
  3. 4
      typescript/helloworld/src/index.ts
  4. 5
      typescript/helloworld/src/multiProtocolApp/README.md
  5. 70
      typescript/helloworld/src/multiProtocolApp/evmAdapter.ts
  6. 60
      typescript/helloworld/src/multiProtocolApp/multiProtocolApp.ts
  7. 312
      typescript/helloworld/src/multiProtocolApp/sealevelAdapter.ts
  8. 25
      typescript/helloworld/src/multiProtocolApp/types.ts
  9. 4
      typescript/infra/.gitignore
  10. 9
      typescript/infra/config/environments/mainnet2/index.ts
  11. 3
      typescript/infra/config/environments/test/index.ts
  12. 2
      typescript/infra/config/environments/testnet3/chains.ts
  13. 11
      typescript/infra/config/environments/testnet3/helloworld.ts
  14. 3
      typescript/infra/config/environments/testnet3/helloworld/rc/addresses.json
  15. 9
      typescript/infra/config/environments/testnet3/index.ts
  16. 2
      typescript/infra/scripts/check-deploy.ts
  17. 2
      typescript/infra/scripts/deploy.ts
  18. 2
      typescript/infra/scripts/funding/fund-keys-from-deployer.ts
  19. 4
      typescript/infra/scripts/helloworld/check.ts
  20. 263
      typescript/infra/scripts/helloworld/kathy.ts
  21. 72
      typescript/infra/scripts/helloworld/utils.ts
  22. 56
      typescript/infra/scripts/utils.ts
  23. 4
      typescript/infra/src/agents/aws/key.ts
  24. 7
      typescript/infra/src/agents/gcp.ts
  25. 2
      typescript/infra/src/agents/keys.ts
  26. 7
      typescript/infra/src/config/environment.ts
  27. 6
      typescript/infra/src/govern/HyperlaneCoreGovernor.ts
  28. 16
      typescript/sdk/src/app/MultiProtocolApp.test.ts
  29. 34
      typescript/sdk/src/app/MultiProtocolApp.ts
  30. 5
      typescript/sdk/src/consts/environments/index.ts
  31. 19
      typescript/sdk/src/consts/environments/testnet-sealevel.json
  32. 2
      typescript/sdk/src/consts/sealevel.ts
  33. 17
      typescript/sdk/src/contracts/contracts.ts
  34. 72
      typescript/sdk/src/core/HyperlaneCore.ts
  35. 36
      typescript/sdk/src/core/MultiProtocolCore.test.ts
  36. 78
      typescript/sdk/src/core/MultiProtocolCore.ts
  37. 89
      typescript/sdk/src/core/adapters/EvmCoreAdapter.ts
  38. 21
      typescript/sdk/src/core/adapters/SealevelCoreAdapter.test.ts
  39. 148
      typescript/sdk/src/core/adapters/SealevelCoreAdapter.ts
  40. 18
      typescript/sdk/src/core/adapters/types.ts
  41. 8
      typescript/sdk/src/core/contracts.ts
  42. 16
      typescript/sdk/src/core/types.ts
  43. 42
      typescript/sdk/src/index.ts
  44. 36
      typescript/sdk/src/metadata/ChainMetadataManager.ts
  45. 58
      typescript/sdk/src/providers/MultiProtocolProvider.ts
  46. 46
      typescript/sdk/src/providers/MultiProvider.ts
  47. 35
      typescript/sdk/src/providers/ProviderType.ts
  48. 5
      typescript/sdk/src/router/MultiProtocolRouterApps.test.ts
  49. 29
      typescript/sdk/src/router/MultiProtocolRouterApps.ts
  50. 7
      typescript/sdk/src/router/adapters/EvmRouterAdapter.ts
  51. 15
      typescript/sdk/src/router/adapters/SealevelRouterAdapter.test.ts
  52. 40
      typescript/sdk/src/router/adapters/SealevelRouterAdapter.ts
  53. 26
      typescript/sdk/src/utils/sealevel.ts
  54. 3
      typescript/utils/index.ts
  55. 4
      typescript/utils/src/addresses.ts
  56. 9
      typescript/utils/src/base58.ts
  57. 8
      typescript/utils/src/objects.ts

@ -14,11 +14,7 @@ import { debug } from '@hyperlane-xyz/utils';
import { HelloWorld } from '../types';
import { HelloWorldFactories } from './contracts';
type Counts = {
sent: number;
received: number;
};
import { StatCounts } from './types';
export class HelloWorldApp extends RouterApp<HelloWorldFactories> {
constructor(
@ -78,7 +74,7 @@ export class HelloWorldApp extends RouterApp<HelloWorldFactories> {
return this.core.waitForMessageProcessed(receipt);
}
async channelStats(from: ChainName, to: ChainName): Promise<Counts> {
async channelStats(from: ChainName, to: ChainName): Promise<StatCounts> {
const sent = await this.getContracts(from).router.sentTo(
this.multiProvider.getDomainId(to),
);
@ -89,8 +85,8 @@ export class HelloWorldApp extends RouterApp<HelloWorldFactories> {
return { sent: sent.toNumber(), received: received.toNumber() };
}
async stats(): Promise<ChainMap<ChainMap<Counts>>> {
const entries: Array<[ChainName, ChainMap<Counts>]> = await Promise.all(
async stats(): Promise<ChainMap<ChainMap<StatCounts>>> {
const entries: Array<[ChainName, ChainMap<StatCounts>]> = await Promise.all(
this.chains().map(async (source) => {
const destinationEntries = await Promise.all(
this.remoteChains(source).map(async (destination) => [

@ -0,0 +1,4 @@
export type StatCounts = {
sent: number | bigint;
received: number | bigint;
};

@ -3,4 +3,8 @@ export { HelloWorldFactories, helloWorldFactories } from './app/contracts';
export { HelloWorldChecker } from './deploy/check';
export { HelloWorldConfig } from './deploy/config';
export { HelloWorldDeployer } from './deploy/deploy';
export { EvmHelloWorldAdapter } from './multiProtocolApp/evmAdapter';
export { HelloMultiProtocolApp } from './multiProtocolApp/multiProtocolApp';
export { SealevelHelloWorldAdapter } from './multiProtocolApp/sealevelAdapter';
export { IHelloWorldAdapter } from './multiProtocolApp/types';
export * as types from './types';

@ -0,0 +1,5 @@
# HelloMultiProtocol
This folder contains an alternative version of the HelloWorld app that intended for use across multiple protocol types (e.g. Ethereum, Sealevel, etc.)
The simpler version in [../app](../app/) is recommended for applications that will only use evm-compatible chains.

@ -0,0 +1,70 @@
import {
ChainName,
EthersV5Transaction,
EvmRouterAdapter,
ProviderType,
RouterAddress,
} from '@hyperlane-xyz/sdk';
import { Address } from '@hyperlane-xyz/utils';
import { StatCounts } from '../app/types';
import { HelloWorld, HelloWorld__factory } from '../types';
import { IHelloWorldAdapter } from './types';
export class EvmHelloWorldAdapter
extends EvmRouterAdapter<RouterAddress & { mailbox: Address }>
implements IHelloWorldAdapter
{
async populateSendHelloTx(
origin: ChainName,
destination: ChainName,
message: string,
value: string,
): Promise<EthersV5Transaction> {
const contract = this.getConnectedContract(origin);
const toDomain = this.multiProvider.getDomainId(destination);
const { transactionOverrides } =
this.multiProvider.getChainMetadata(origin);
// apply gas buffer due to https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/634
const estimated = await contract.estimateGas.sendHelloWorld(
toDomain,
message,
{ ...transactionOverrides, value },
);
const gasLimit = estimated.mul(12).div(10);
const tx = await contract.populateTransaction.sendHelloWorld(
toDomain,
message,
{
...transactionOverrides,
gasLimit,
value,
},
);
return { transaction: tx, type: ProviderType.EthersV5 };
}
async channelStats(
origin: ChainName,
destination: ChainName,
): Promise<StatCounts> {
const originDomain = this.multiProvider.getDomainId(origin);
const destinationDomain = this.multiProvider.getDomainId(destination);
const sent = await this.getConnectedContract(origin).sentTo(
destinationDomain,
);
const received = await this.getConnectedContract(destination).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);
}
}

@ -0,0 +1,60 @@
import {
ChainMap,
ChainName,
MultiProtocolRouterApp,
RouterAddress,
TypedTransaction,
} from '@hyperlane-xyz/sdk';
import { Address, ProtocolType } from '@hyperlane-xyz/utils';
import { StatCounts } from '../app/types';
import { EvmHelloWorldAdapter } from './evmAdapter';
import { SealevelHelloWorldAdapter } from './sealevelAdapter';
import { IHelloWorldAdapter } from './types';
export class HelloMultiProtocolApp extends MultiProtocolRouterApp<
RouterAddress & { mailbox: Address },
IHelloWorldAdapter
> {
override protocolToAdapter(protocol: ProtocolType) {
if (protocol === ProtocolType.Ethereum) return EvmHelloWorldAdapter;
if (protocol === ProtocolType.Sealevel) return SealevelHelloWorldAdapter;
throw new Error(`No adapter for protocol ${protocol}`);
}
populateHelloWorldTx(
origin: ChainName,
destination: ChainName,
message: string,
value: string,
sender: Address,
): Promise<TypedTransaction> {
return this.adapter(origin).populateSendHelloTx(
origin,
destination,
message,
value,
sender,
);
}
channelStats(origin: ChainName, destination: ChainName): Promise<StatCounts> {
return this.adapter(origin).channelStats(origin, destination);
}
async stats(): Promise<ChainMap<ChainMap<StatCounts>>> {
const entries: Array<[ChainName, ChainMap<StatCounts>]> = await Promise.all(
this.chains().map(async (source) => {
const destinationEntries = await Promise.all(
this.remoteChains(source).map(async (destination) => [
destination,
await this.channelStats(source, destination),
]),
);
return [source, Object.fromEntries(destinationEntries)];
}),
);
return Object.fromEntries(entries);
}
}

@ -0,0 +1,312 @@
import {
AccountMeta,
Keypair,
PublicKey,
SystemProgram,
Transaction,
TransactionInstruction,
} from '@solana/web3.js';
import { deserializeUnchecked, serialize } from 'borsh';
import {
BaseSealevelAdapter,
ChainName,
ProviderType,
RouterAddress,
SEALEVEL_SPL_NOOP_ADDRESS,
SealevelAccountDataWrapper,
SealevelCoreAdapter,
SealevelInstructionWrapper,
SealevelRouterAdapter,
SolanaWeb3Transaction,
getSealevelAccountDataSchema,
} from '@hyperlane-xyz/sdk';
import { Address, Domain } from '@hyperlane-xyz/utils';
import { StatCounts } from '../app/types';
import { IHelloWorldAdapter } from './types';
export class SealevelHelloWorldAdapter
extends SealevelRouterAdapter<RouterAddress & { mailbox: Address }>
implements IHelloWorldAdapter
{
async populateSendHelloTx(
origin: ChainName,
destination: ChainName,
message: string,
value: string,
sender: Address,
): Promise<SolanaWeb3Transaction> {
this.logger(
'Creating sendHelloWorld tx for sealevel',
origin,
destination,
message,
value,
);
const { mailbox, router: programId } =
this.multiProvider.getChainMetadata(origin);
const mailboxPubKey = new PublicKey(mailbox);
const senderPubKey = new PublicKey(sender);
const programPubKey = new PublicKey(programId);
const randomWallet = Keypair.generate();
const keys = this.getSendHelloKeyList(
programPubKey,
mailboxPubKey,
senderPubKey,
randomWallet.publicKey,
);
const instructionData =
new SealevelInstructionWrapper<SendHelloWorldInstruction>({
instruction: HelloWorldInstruction.SendHelloWorld,
data: new SendHelloWorldInstruction({
destination: this.multiProvider.getDomainId(destination),
message,
}),
});
const serializedData = serialize(SendHelloWorldSchema, instructionData);
const txInstruction = new TransactionInstruction({
keys,
programId: programPubKey,
data: Buffer.from(serializedData),
});
const connection = this.multiProvider.getSolanaWeb3Provider(origin);
const recentBlockhash = (await connection.getLatestBlockhash('finalized'))
.blockhash;
// @ts-ignore Workaround for bug in the web3 lib, sometimes uses recentBlockhash and sometimes uses blockhash
const transaction = new Transaction({
feePayer: senderPubKey,
blockhash: recentBlockhash,
recentBlockhash,
}).add(txInstruction);
transaction.partialSign(randomWallet);
return { type: ProviderType.SolanaWeb3, transaction };
}
// Should match https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/dd7ff727b0d3d393a159afa5f0a364775bde3a58/rust/sealevel/programs/helloworld/src/processor.rs#L157
getSendHelloKeyList(
programId: PublicKey,
mailbox: PublicKey,
sender: PublicKey,
randomWallet: PublicKey,
): Array<AccountMeta> {
return [
// 0. [writable] Program storage.
{
pubkey: this.deriveProgramStoragePDA(programId),
isSigner: false,
isWritable: true,
},
// 1. [executable] The mailbox.
{ pubkey: mailbox, isSigner: false, isWritable: false },
// 2. [writeable] Outbox PDA
{
pubkey: SealevelCoreAdapter.deriveMailboxOutboxPda(mailbox),
isSigner: false,
isWritable: true,
},
// 3. [] Program's dispatch authority
{
pubkey:
SealevelCoreAdapter.deriveMailboxDispatchAuthorityPda(programId),
isSigner: false,
isWritable: false,
},
// 4. [executable] The system program.
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
// 5. [executable] The spl_noop program.
{
pubkey: new PublicKey(SEALEVEL_SPL_NOOP_ADDRESS),
isSigner: false,
isWritable: false,
},
// 6. [signer] Tx payer.
{ pubkey: sender, isSigner: true, isWritable: true },
// 7. [signer] Unique message account.
{ pubkey: randomWallet, isSigner: true, isWritable: false },
// 8. [writeable] Dispatched message PDA
{
pubkey: SealevelCoreAdapter.deriveMailboxDispatchedMessagePda(
mailbox,
randomWallet,
),
isSigner: false,
isWritable: true,
},
/// ---- if an IGP is configured ----
/// 9. [executable] The IGP program.
/// 10. [writeable] The IGP program data.
/// 11. [writeable] The gas payment PDA.
/// 12. [] OPTIONAL - The Overhead IGP program, if the configured IGP is an Overhead IGP.
/// 13. [writeable] The IGP account.
/// ---- end if an IGP is configured ----
];
}
// 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(
['hello_world', '-', 'handle', '-', 'storage'],
programId,
);
}
async channelStats(
origin: ChainName,
_destination: ChainName,
): Promise<StatCounts> {
const data = await this.getAccountInfo(origin);
return { sent: data.sent, received: data.received };
}
async getAccountInfo(chain: ChainName): Promise<HelloWorldData> {
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(
HelloWorldDataSchema,
SealevelAccountDataWrapper,
accountInfo.data,
);
return accountData.data as HelloWorldData;
}
}
/**
* Borsh Schema
*/
// Should match https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/dd7ff727b0d3d393a159afa5f0a364775bde3a58/rust/sealevel/programs/helloworld/src/instruction.rs#L40
export enum HelloWorldInstruction {
Init,
SendHelloWorld,
SetInterchainSecurityModule,
EnrollRemoteRouters,
}
export class SendHelloWorldInstruction {
destination!: number;
message!: string;
constructor(public readonly fields: any) {
Object.assign(this, fields);
}
}
export const SendHelloWorldSchema = new Map<any, any>([
[
SealevelInstructionWrapper,
{
kind: 'struct',
fields: [
['instruction', 'u8'],
['data', SendHelloWorldInstruction],
],
},
],
[
SendHelloWorldInstruction,
{
kind: 'struct',
fields: [
['destination', 'u32'],
['message', 'string'],
],
},
],
]);
// Should match https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/dd7ff727b0d3d393a159afa5f0a364775bde3a58/rust/sealevel/programs/helloworld/src/accounts.rs#L20
export class HelloWorldData {
local_domain!: Domain;
/// The address of the mailbox contract.
mailbox!: Uint8Array;
mailbox_pubkey!: PublicKey;
// The address of the ISM
ism?: Uint8Array;
ism_pubkey?: PublicKey;
// The address of the IGP
igp?: {
program_id: Uint8Array;
type: number;
igp_account: Uint8Array;
};
igp_pubkey?: PublicKey;
igp_account_pubkey?: PublicKey;
// The address of the owner
owner?: Uint8Array;
owner_pubkey?: PublicKey;
// A counter of how many messages have been sent from this contract.
sent!: bigint;
// A counter of how many messages have been received by this contract.
received!: bigint;
// Keyed by domain, a counter of how many messages that have been sent
// from this contract to the domain.
sent_to!: Map<Domain, bigint>;
// Keyed by domain, a counter of how many messages that have been received
// by this contract from the domain.
received_from!: Map<Domain, bigint>;
// Keyed by domain, the router for the remote domain.
routers!: Map<Domain, Uint8Array>;
constructor(public readonly fields: any) {
Object.assign(this, fields);
this.mailbox_pubkey = new PublicKey(this.mailbox);
this.ism_pubkey = this.ism ? new PublicKey(this.ism) : undefined;
this.igp_pubkey = this.igp?.program_id
? new PublicKey(this.igp.program_id)
: undefined;
this.igp_account_pubkey = this.igp?.igp_account
? new PublicKey(this.igp.igp_account)
: undefined;
this.owner_pubkey = this.owner ? new PublicKey(this.owner) : undefined;
}
}
export const HelloWorldDataSchema = new Map<any, any>([
[SealevelAccountDataWrapper, getSealevelAccountDataSchema(HelloWorldData)],
[
HelloWorldData,
{
kind: 'struct',
fields: [
['domain', 'u32'],
['mailbox', [32]],
['ism', { kind: 'option', type: [32] }],
[
'igp',
{
kind: 'option',
type: {
kind: 'struct',
fields: [
['program_id', [32]],
['type', 'u8'],
['igp_account', [32]],
],
},
},
],
['owner', { kind: 'option', type: [32] }],
['sent', 'u64'],
['received', 'u64'],
['sent_to', { kind: 'map', key: 'u32', value: 'u64' }],
['received_from', { kind: 'map', key: 'u32', value: 'u64' }],
['routers', { kind: 'map', key: 'u32', value: [32] }],
],
},
],
]);

@ -0,0 +1,25 @@
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<RouterAddress & { mailbox: Address }> {
populateSendHelloTx: (
origin: ChainName,
destination: ChainName,
message: string,
value: string,
sender: Address,
) => Promise<TypedTransaction>;
channelStats: (
origin: ChainName,
destination: ChainName,
) => Promise<StatCounts>;
}

@ -1,8 +1,8 @@
node_modules/
tmp.ts
dist/
.env
.env*
cache/
test/outputs
config/environments/test/core/
config/environments/test/igp/
config/environments/test/igp/

@ -1,6 +1,9 @@
import { AgentConnectionType } from '@hyperlane-xyz/sdk';
import { getMultiProviderForRole } from '../../../scripts/utils';
import {
getKeysForRole,
getMultiProviderForRole,
} from '../../../scripts/utils';
import { EnvironmentConfig } from '../../../src/config';
import { Role } from '../../../src/roles';
import { Contexts } from '../../contexts';
@ -33,6 +36,10 @@ export const environment: EnvironmentConfig = {
undefined,
connectionType,
),
getKeys: (
context: Contexts = Contexts.Hyperlane,
role: Role = Role.Deployer,
) => getKeysForRole(mainnetConfigs, environmentName, context, role),
agents,
core,
igp,

@ -30,5 +30,8 @@ export const environment: EnvironmentConfig = {
mp.setSharedSigner(signer);
return mp;
},
getKeys: async () => {
throw new Error('Not implemented');
},
storageGasOracleConfig,
};

@ -1,6 +1,6 @@
import { ChainMap, ChainMetadata, chainMetadata } from '@hyperlane-xyz/sdk';
import { AgentChainNames, AgentRole, Role } from '../../../src/roles';
import { AgentChainNames, Role } from '../../../src/roles';
export const testnetConfigs: ChainMap<ChainMetadata> = {
alfajores: chainMetadata.alfajores,

@ -1,14 +1,7 @@
import { HelloWorldConfig as HelloWorldContractsConfig } from '@hyperlane-xyz/helloworld';
import {
AgentConnectionType,
ChainMap,
RouterConfig,
} from '@hyperlane-xyz/sdk';
import { objMap } from '@hyperlane-xyz/utils';
import { AgentConnectionType } from '@hyperlane-xyz/sdk';
import { DeployEnvironment, HelloWorldConfig } from '../../../src/config';
import { HelloWorldConfig } from '../../../src/config';
import { HelloWorldKathyRunMode } from '../../../src/config/helloworld';
import { aggregationIsm } from '../../aggregationIsm';
import { Contexts } from '../../contexts';
import { environment } from './chains';

@ -25,5 +25,8 @@
},
"sepolia": {
"router": "0x6AD4DEBA8A147d000C09de6465267a9047d1c217"
},
"solanadevnet": {
"router": "DdTMkk9nuqH5LnD56HLkPiKMV3yB3BNEYSQfgmJHa5i7"
}
}

@ -1,6 +1,9 @@
import { AgentConnectionType } from '@hyperlane-xyz/sdk';
import { getMultiProviderForRole } from '../../../scripts/utils';
import {
getKeysForRole,
getMultiProviderForRole,
} from '../../../scripts/utils';
import { EnvironmentConfig } from '../../../src/config';
import { Role } from '../../../src/roles';
import { Contexts } from '../../contexts';
@ -34,6 +37,10 @@ export const environment: EnvironmentConfig = {
undefined,
connectionType,
),
getKeys: (
context: Contexts = Contexts.Hyperlane,
role: Role = Role.Deployer,
) => getKeysForRole(testnetConfigs, environmentName, context, role),
agents,
core,
igp,

@ -3,12 +3,12 @@ import {
HyperlaneCoreChecker,
HyperlaneIgp,
HyperlaneIgpChecker,
HyperlaneIsmFactory,
InterchainAccount,
InterchainAccountChecker,
InterchainQuery,
InterchainQueryChecker,
} from '@hyperlane-xyz/sdk';
import { HyperlaneIsmFactory } from '@hyperlane-xyz/sdk/dist/ism/HyperlaneIsmFactory';
import { deployEnvToSdkEnv } from '../src/config/environment';
import { HyperlaneAppGovernor } from '../src/govern/HyperlaneAppGovernor';

@ -64,7 +64,7 @@ async function main() {
let config: ChainMap<unknown> = {};
let deployer: HyperlaneDeployer<any, any>;
if (module === Modules.ISM_FACTORY) {
config = objMap(envConfig.core, (chain) => true);
config = objMap(envConfig.core, (_chain) => true);
deployer = new HyperlaneIsmFactoryDeployer(multiProvider);
} else if (module === Modules.CORE) {
config = envConfig.core;

@ -6,12 +6,12 @@ import { format } from 'util';
import {
AgentConnectionType,
AllChains,
ChainMap,
ChainName,
Chains,
HyperlaneIgp,
MultiProvider,
} from '@hyperlane-xyz/sdk';
import { ChainMap } from '@hyperlane-xyz/sdk/dist/types';
import { error, log, warn } from '@hyperlane-xyz/utils';
import { Contexts } from '../../config/contexts';

@ -9,13 +9,13 @@ import {
withContext,
} from '../utils';
import { getApp } from './utils';
import { getHelloWorldApp } from './utils';
async function main() {
const { environment, context } = await withContext(getArgs()).argv;
const coreConfig = getEnvironmentConfig(environment);
const multiProvider = await coreConfig.getMultiProvider();
const app = await getApp(
const app = await getHelloWorldApp(
coreConfig,
context,
Role.Deployer,

@ -1,17 +1,25 @@
import { BigNumber, ethers } from 'ethers';
import { Keypair, sendAndConfirmRawTransaction } from '@solana/web3.js';
import { BigNumber, Wallet, ethers } from 'ethers';
import { Counter, Gauge, Registry } from 'prom-client';
import { format } from 'util';
import { HelloWorldApp } from '@hyperlane-xyz/helloworld';
import { HelloMultiProtocolApp } from '@hyperlane-xyz/helloworld';
import {
AgentConnectionType,
ChainMap,
ChainName,
DispatchedMessage,
HyperlaneCore,
HyperlaneIgp,
MultiProtocolCore,
MultiProvider,
ProviderType,
TypedTransactionReceipt,
chainMetadata,
} from '@hyperlane-xyz/sdk';
import {
Address,
ProtocolType,
debug,
ensure0x,
error,
log,
retryAsync,
@ -19,13 +27,19 @@ import {
warn,
} from '@hyperlane-xyz/utils';
import { deployEnvToSdkEnv } from '../../src/config/environment';
import { Contexts } from '../../config/contexts';
import {
hyperlaneHelloworld,
releaseCandidateHelloworld,
} from '../../config/environments/testnet3/helloworld';
import { CloudAgentKey } from '../../src/agents/keys';
import { DeployEnvironment } from '../../src/config/environment';
import { Role } from '../../src/roles';
import { startMetricsServer } from '../../src/utils/metrics';
import { assertChain, diagonalize, sleep } from '../../src/utils/utils';
import { getArgs, getEnvironmentConfig, withContext } from '../utils';
import { getAddressesForKey, getArgs, withContext } from '../utils';
import { getApp } from './utils';
import { getHelloWorldMultiProtocolApp } from './utils';
const metricsRegister = new Registry();
// TODO rename counter names
@ -146,18 +160,19 @@ async function main(): Promise<boolean> {
startMetricsServer(metricsRegister);
debug('Starting up', { environment });
const coreConfig = getEnvironmentConfig(environment);
const app = await getApp(
coreConfig,
context,
Role.Kathy,
undefined,
connectionType,
);
const igp = HyperlaneIgp.fromEnvironment(
deployEnvToSdkEnv[coreConfig.environment],
app.multiProvider,
);
// TODO (Rossy) remove getCoreConfigStub and re-enable getEnvironmentConfig
// const coreConfig = getEnvironmentConfig(environment);
const coreConfig = getCoreConfigStub(environment);
const { app, core, igp, multiProvider, keys } =
await getHelloWorldMultiProtocolApp(
coreConfig,
context,
Role.Kathy,
undefined,
connectionType,
);
const appChains = app.chains();
// Ensure the specified chains to skip are actually valid for the app.
@ -228,7 +243,9 @@ async function main(): Promise<boolean> {
messageReceiptSeconds.labels({ origin, remote }).inc(0);
}
chains.map((chain) => updateWalletBalanceMetricFor(app, chain));
chains.map((chain) =>
updateWalletBalanceMetricFor(app, chain, coreConfig.owners[chain]),
);
// Incremented each time an entire cycle has occurred
let currentCycle = 0;
@ -327,6 +344,9 @@ async function main(): Promise<boolean> {
try {
await sendMessage(
app,
core,
keys,
multiProvider,
igp,
origin,
destination,
@ -343,12 +363,14 @@ async function main(): Promise<boolean> {
messagesSendCount.labels({ ...labels, status: 'failure' }).inc();
errorOccurred = true;
}
updateWalletBalanceMetricFor(app, origin).catch((e) => {
warn('Failed to update wallet balance for chain', {
chain: origin,
err: format(e),
});
});
updateWalletBalanceMetricFor(app, origin, coreConfig.owners[origin]).catch(
(e) => {
warn('Failed to update wallet balance for chain', {
chain: origin,
err: format(e),
});
},
);
// Break if we should stop sending messages
if (await nextMessage()) {
@ -359,7 +381,10 @@ async function main(): Promise<boolean> {
}
async function sendMessage(
app: HelloWorldApp,
app: HelloMultiProtocolApp,
core: MultiProtocolCore,
keys: ChainMap<CloudAgentKey>,
multiProvider: MultiProvider,
igp: HyperlaneIgp,
origin: ChainName,
destination: ChainName,
@ -370,27 +395,86 @@ async function sendMessage(
const msg = 'Hello!';
const expectedHandleGas = BigNumber.from(50_000);
const value = await retryAsync(
() =>
igp.quoteGasPaymentForDefaultIsmIgp(
origin,
destination,
expectedHandleGas,
),
2,
);
// TODO sealevel igp support here
let value: string;
if (app.metadata(origin).protocol == ProtocolType.Ethereum) {
const valueBn = await retryAsync(
() =>
igp.quoteGasPaymentForDefaultIsmIgp(
origin,
destination,
expectedHandleGas,
),
2,
);
value = valueBn.toString();
} else {
value = '0';
}
const metricLabels = { origin, remote: destination };
log('Sending message', {
origin,
destination,
interchainGasPayment: value.toString(),
interchainGasPayment: value,
});
const sendAndConfirmMsg = async () => {
const sender = getAddressesForKey(keys, origin, multiProvider);
const tx = await app.populateHelloWorldTx(
origin,
destination,
msg,
value,
sender,
);
let txReceipt: TypedTransactionReceipt;
if (tx.type == ProviderType.EthersV5) {
// Utilize the legacy evm-specific multiprovider utils to send the transaction
const receipt = await multiProvider.sendTransaction(
origin,
tx.transaction,
);
txReceipt = {
type: ProviderType.EthersV5,
receipt,
};
} else if (tx.type === ProviderType.SolanaWeb3) {
// Utilize the new multi-protocol provider for non-evm chains
// This could be done for EVM too but the legacy MP has tx formatting utils
// that have not yet been ported over
const connection = app.multiProvider.getSolanaWeb3Provider(origin);
const payer = Keypair.fromSeed(
Buffer.from(keys[origin].privateKey, 'hex'),
);
tx.transaction.partialSign(payer);
// Note, tx signature essentially tx means hash on sealevel
const txSignature = await sendAndConfirmRawTransaction(
connection,
tx.transaction.serialize(),
);
const receipt = await connection.getTransaction(txSignature, {
commitment: 'confirmed',
maxSupportedTransactionVersion: 0,
});
if (!receipt)
throw new Error(`Sealevel tx not found with signature ${txSignature}`);
txReceipt = {
type: ProviderType.SolanaWeb3,
receipt,
};
} else {
throw new Error(`Unsupported provider type for kathy send ${tx.type}`);
}
return txReceipt;
};
const receipt = await retryAsync(
() =>
timeout(
app.sendHelloWorld(origin, destination, msg, value),
sendAndConfirmMsg(),
messageSendTimeout,
'Timeout sending message',
),
@ -398,43 +482,17 @@ async function sendMessage(
);
messageSendSeconds.labels(metricLabels).inc((Date.now() - startTime) / 1000);
const [message] = app.core.getDispatchedMessages(receipt);
log('Message sent', {
log('Message sent, waiting for it to be processed', {
origin,
destination,
events: receipt.events,
logs: receipt.logs,
message,
receipt,
});
try {
await timeout(
app.waitForMessageProcessed(receipt),
messageReceiptTimeout,
'Timeout waiting for message to be received',
);
} catch (error) {
// If we weren't able to get the receipt for message processing,
// try to read the state to ensure it wasn't a transient provider issue
log('Checking if message was received despite timeout', {
message,
});
// Try a few times to see if the message has been processed --
// we've seen some intermittent issues when fetching state.
// This will throw if the message is found to have not been processed.
await retryAsync(async () => {
if (!(await messageIsProcessed(app.core, origin, destination, message))) {
throw error;
}
}, 3);
// Otherwise, the message has been processed
log(
'Did not receive event for message delivery even though it was delivered',
{ origin, destination, message },
);
}
await timeout(
core.waitForMessagesProcessed(origin, destination, receipt, 5000, 36),
messageReceiptTimeout,
'Timeout waiting for message to be received',
);
messageReceiptSeconds
.labels(metricLabels)
@ -445,24 +503,13 @@ async function sendMessage(
});
}
async function messageIsProcessed(
core: HyperlaneCore,
origin: ChainName,
destination: ChainName,
message: DispatchedMessage,
): Promise<boolean> {
const destinationMailbox = core.getContracts(destination).mailbox;
return destinationMailbox.delivered(message.id);
}
async function updateWalletBalanceMetricFor(
app: HelloWorldApp,
app: HelloMultiProtocolApp,
chain: ChainName,
signerAddress: Address,
): Promise<void> {
const provider = app.multiProvider.getProvider(chain);
const signerAddress = await app
.getContracts(chain)
.router.signer.getAddress();
if (app.metadata(chain).protocol !== ProtocolType.Ethereum) return;
const provider = app.multiProvider.getEthersV5Provider(chain);
const signerBalance = await provider.getBalance(signerAddress);
const balance = parseFloat(ethers.utils.formatEther(signerBalance));
walletBalance
@ -479,6 +526,50 @@ async function updateWalletBalanceMetricFor(
debug('Wallet balance updated for chain', { chain, signerAddress, balance });
}
// Get a core config intended for testing Kathy without secret access
export function getCoreConfigStub(environment: DeployEnvironment) {
const multiProvider = new MultiProvider({
// Desired chains here. Key must have funds on these chains
fuji: chainMetadata.fuji,
solanadevnet: chainMetadata.solanadevnet,
});
const privateKeyEvm = process.env.KATHY_PRIVATE_KEY_EVM;
if (!privateKeyEvm) throw new Error('KATHY_PRIVATE_KEY_EVM env var not set');
const evmSigner = new Wallet(privateKeyEvm);
console.log('evmSigner address', evmSigner.address);
multiProvider.setSharedSigner(evmSigner);
const privateKeySealevel = process.env.KATHY_PRIVATE_KEY_SEALEVEL;
if (!privateKeySealevel)
throw new Error('KATHY_PRIVATE_KEY_SEALEVEL env var not set');
const sealevelSigner = Keypair.fromSeed(
Buffer.from(privateKeySealevel, 'hex'),
);
console.log('sealevelSigner address', sealevelSigner.publicKey.toBase58());
return {
helloWorld: {
[Contexts.Hyperlane]: hyperlaneHelloworld,
[Contexts.ReleaseCandidate]: releaseCandidateHelloworld,
},
environment,
owners: {
fuji: evmSigner.address,
solanadevnet: sealevelSigner.publicKey.toBase58(),
},
getMultiProvider: () => multiProvider,
getKeys: () => ({
fuji: { address: evmSigner.address, privateKey: ensure0x(privateKeyEvm) },
solanadevnet: {
address: sealevelSigner.publicKey.toBase58(),
privateKey: privateKeySealevel,
},
}),
} as any;
}
main()
.then((errorOccurred: boolean) => {
log('Main exited');

@ -1,10 +1,23 @@
import { HelloWorldApp, helloWorldFactories } from '@hyperlane-xyz/helloworld';
import {
HelloMultiProtocolApp,
HelloWorldApp,
helloWorldFactories,
} from '@hyperlane-xyz/helloworld';
import {
AgentConnectionType,
HyperlaneCore,
HyperlaneIgp,
MultiProtocolCore,
MultiProtocolProvider,
MultiProvider,
attachContractsMap,
chainMetadata,
filterAddressesToProtocol,
hyperlaneEnvironments,
igpFactories,
} from '@hyperlane-xyz/sdk';
import { hyperlaneEnvironmentsWithSealevel } from '@hyperlane-xyz/sdk/src';
import { ProtocolType } from '@hyperlane-xyz/utils';
import { Contexts } from '../../config/contexts';
import { EnvironmentConfig } from '../../src/config';
@ -12,7 +25,7 @@ import { deployEnvToSdkEnv } from '../../src/config/environment';
import { HelloWorldConfig } from '../../src/config/helloworld';
import { Role } from '../../src/roles';
export async function getApp(
export async function getHelloWorldApp(
coreConfig: EnvironmentConfig,
context: Contexts,
keyRole: Role,
@ -36,6 +49,61 @@ export async function getApp(
return new HelloWorldApp(core, contracts, multiProvider);
}
export async function getHelloWorldMultiProtocolApp(
coreConfig: EnvironmentConfig,
context: Contexts,
keyRole: Role,
keyContext: Contexts = context,
connectionType: AgentConnectionType = AgentConnectionType.Http,
) {
const multiProvider: MultiProvider = await coreConfig.getMultiProvider(
keyContext,
keyRole,
connectionType,
);
const sdkEnvName = deployEnvToSdkEnv[coreConfig.environment];
const keys = await coreConfig.getKeys(keyContext, keyRole);
const helloworldConfig = getHelloWorldConfig(coreConfig, context);
const multiProtocolProvider =
MultiProtocolProvider.fromMultiProvider(multiProvider);
// Hacking around infra code limitations, we may need to add solana manually
// because the it's not in typescript/infra/config/environments/testnet3/chains.ts
// Adding it there breaks many things
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);
// TODO we need a MultiProtocolIgp
// Using an standard IGP for just evm chains for now
// Unfortunately this requires hacking surgically around certain addresses
const envAddresses = hyperlaneEnvironments[sdkEnvName];
const filteredAddresses = filterAddressesToProtocol(
envAddresses,
ProtocolType.Ethereum,
multiProtocolProvider,
);
const contractsMap = attachContractsMap(filteredAddresses, igpFactories);
const igp = new HyperlaneIgp(contractsMap, multiProvider);
return { app, core, igp, multiProvider, multiProtocolProvider, keys };
}
export function getHelloWorldConfig(
coreConfig: EnvironmentConfig,
context: Contexts,

@ -1,3 +1,5 @@
import { Keypair } from '@solana/web3.js';
import { Wallet } from 'ethers';
import path from 'path';
import yargs from 'yargs';
@ -6,17 +8,23 @@ import {
AllChains,
ChainMap,
ChainMetadata,
ChainMetadataManager,
ChainName,
Chains,
CoreConfig,
HyperlaneCore,
HyperlaneIgp,
MultiProvider,
ProxiedRouterConfig,
RouterConfig,
collectValidators,
} from '@hyperlane-xyz/sdk';
import { ProxiedRouterConfig } from '@hyperlane-xyz/sdk/dist/router/types';
import { ProtocolType, objMap, promiseObjAll } from '@hyperlane-xyz/utils';
import {
ProtocolType,
objMap,
promiseObjAll,
strip0x,
} from '@hyperlane-xyz/utils';
import { Contexts } from '../config/contexts';
import { environments } from '../config/environments';
@ -185,7 +193,7 @@ export async function getMultiProviderForRole(
const multiProvider = new MultiProvider(txConfigs);
await promiseObjAll(
objMap(txConfigs, async (chain, config) => {
objMap(txConfigs, async (chain, _) => {
const provider = await fetchProvider(environment, chain, connectionType);
const key = getKeyForRole(environment, context, chain, role, index);
const signer = await key.getSigner(provider);
@ -196,6 +204,48 @@ export async function getMultiProviderForRole(
return multiProvider;
}
// Note: this will only work for keystores that allow key's to be extracted.
// I.e. GCP will work but AWS HSMs will not.
export async function getKeysForRole(
txConfigs: ChainMap<ChainMetadata>,
environment: DeployEnvironment,
context: Contexts,
role: Role,
index?: number,
): Promise<ChainMap<CloudAgentKey>> {
if (process.env.CI === 'true') {
return {};
}
const keys = await promiseObjAll(
objMap(txConfigs, async (chain, _) => {
const key = getKeyForRole(environment, context, chain, role, index);
if (!key.privateKey)
throw new Error(`Key for ${chain} does not have private key`);
return key;
}),
);
return keys;
}
// Note: this will only work for keystores that allow key's to be extracted.
export function getAddressesForKey(
keys: ChainMap<CloudAgentKey>,
chain: ChainName,
manager: ChainMetadataManager<any>,
) {
const protocol = manager.getChainMetadata(chain).protocol;
if (protocol === ProtocolType.Ethereum) {
return new Wallet(keys[chain]).address;
} else if (protocol === ProtocolType.Sealevel) {
return Keypair.fromSeed(
Buffer.from(strip0x(keys[chain].privateKey), 'hex'),
).publicKey.toBase58();
} else {
throw Error(`Protocol ${protocol} not supported`);
}
}
export function getContractAddressesSdkFilepath() {
return path.join('../sdk/src/consts/environments');
}

@ -55,6 +55,10 @@ export class AgentAwsKey extends CloudAgentKey {
this.region = agentConfig.aws.region;
}
get privateKey(): string {
throw new Error('Private key unavailable for AWS keys');
}
async getClient(): Promise<KMSClient> {
if (this.client) {
return this.client;

@ -98,10 +98,9 @@ export class AgentGCPKey extends CloudAgentKey {
case ProtocolType.Ethereum:
return this.address;
case ProtocolType.Sealevel:
const keypair = Keypair.fromSeed(
Uint8Array.from(Buffer.from(strip0x(this.privateKey), 'hex')),
);
return keypair.publicKey.toBase58();
return Keypair.fromSeed(
Buffer.from(strip0x(this.privateKey), 'hex'),
).publicKey.toBase58();
default:
return undefined;
}

@ -63,6 +63,8 @@ export abstract class CloudAgentKey extends BaseCloudAgentKey {
provider?: ethers.providers.Provider,
): Promise<ethers.Signer>;
abstract privateKey: string;
serializeAsAddress() {
return {
identifier: this.identifier,

@ -6,14 +6,15 @@ import {
ChainName,
CoreConfig,
HookConfig,
HyperlaneEnvironment,
MultiProvider,
OverheadIgpConfig,
} from '@hyperlane-xyz/sdk';
import { HyperlaneEnvironment } from '@hyperlane-xyz/sdk/dist/consts/environments';
import { Address } from '@hyperlane-xyz/utils';
import { Contexts } from '../../config/contexts';
import { environments } from '../../config/environments';
import { CloudAgentKey } from '../agents/keys';
import { Role } from '../roles';
import { RootAgentConfig } from './agent';
@ -45,6 +46,10 @@ export type EnvironmentConfig = {
role?: Role,
connectionType?: AgentConnectionType,
) => Promise<MultiProvider>;
getKeys: (
context?: Contexts,
role?: Role,
) => Promise<ChainMap<CloudAgentKey>>;
helloWorld?: Partial<Record<Contexts, HelloWorldConfig>>;
keyFunderConfig?: KeyFunderConfig;
liquidityLayerConfig?: {

@ -4,13 +4,11 @@ import {
CoreViolationType,
HyperlaneCore,
HyperlaneCoreChecker,
MailboxViolation,
MailboxViolationType,
OwnerViolation,
ViolationType,
} from '@hyperlane-xyz/sdk';
import {
MailboxViolation,
MailboxViolationType,
} from '@hyperlane-xyz/sdk/dist/core/types';
import { Address } from '@hyperlane-xyz/utils';
import { HyperlaneAppGovernor } from '../govern/HyperlaneAppGovernor';

@ -1,11 +1,23 @@
import { expect } from 'chai';
import { ProtocolType } from '@hyperlane-xyz/utils';
import { Chains } from '../consts/chains';
import { MultiProtocolProvider } from '../providers/MultiProtocolProvider';
import { MultiProtocolApp } from './MultiProtocolApp';
import {
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;
throw new Error(`No adapter for protocol ${protocol}`);
}
}
describe('MultiProtocolApp', () => {
describe('constructs', () => {

@ -1,3 +1,4 @@
import { PublicKey } from '@solana/web3.js';
import debug from 'debug';
import { ProtocolType, objMap, promiseObjAll } from '@hyperlane-xyz/utils';
@ -17,6 +18,7 @@ export abstract class BaseAppAdapter<ContractAddrs = {}> {
public abstract readonly protocol: ProtocolType;
constructor(
public readonly multiProvider: MultiProtocolProvider<ContractAddrs>,
public readonly logger = debug(`hyperlane:AppAdapter`),
) {}
}
@ -24,10 +26,6 @@ export type AdapterClassType<ContractAddrs = {}, API = {}> = new (
multiProvider: MultiProtocolProvider<ContractAddrs>,
) => API;
export type AdapterProtocolMap<ContractAddrs = {}, API = {}> = Partial<
Record<ProtocolType, AdapterClassType<ContractAddrs, API>>
>;
export class BaseEvmAdapter<
ContractAddrs = {},
> extends BaseAppAdapter<ContractAddrs> {
@ -38,6 +36,17 @@ export class BaseSealevelAdapter<
ContractAddrs = {},
> extends BaseAppAdapter<ContractAddrs> {
public readonly protocol: ProtocolType = ProtocolType.Sealevel;
static derivePda(
seeds: Array<string | Buffer>,
programId: string | PublicKey,
): PublicKey {
const [pda] = PublicKey.findProgramAddressSync(
seeds.map((s) => Buffer.from(s)),
new PublicKey(programId),
);
return pda;
}
}
/**
@ -54,17 +63,13 @@ export class BaseSealevelAdapter<
* @param multiProvider - A MultiProtocolProvider instance that MUST include the app's
* contract addresses in its chain metadata
* @param logger - A logger instance
*
* @override protocolToAdapter - This should return an Adapter class for a given protocol type
*/
export abstract class MultiProtocolApp<
ContractAddrs = {},
IAdapterApi extends BaseAppAdapter = BaseAppAdapter,
> extends MultiGeneric<ChainMetadata<ContractAddrs>> {
// Subclasses should override this with more specific adapters
public readonly protocolToAdapter: AdapterProtocolMap<any, BaseAppAdapter> = {
[ProtocolType.Ethereum]: BaseEvmAdapter,
[ProtocolType.Sealevel]: BaseSealevelAdapter,
};
constructor(
public readonly multiProvider: MultiProtocolProvider<ContractAddrs>,
public readonly logger = debug('hyperlane:MultiProtocolApp'),
@ -72,15 +77,18 @@ export abstract class MultiProtocolApp<
super(multiProvider.metadata);
}
// Subclasses should override this with more specific adapters
abstract protocolToAdapter(
protocol: ProtocolType,
): AdapterClassType<ContractAddrs, IAdapterApi>;
metadata(chain: ChainName): ChainMetadata<ContractAddrs> {
return this.get(chain);
}
adapter(chain: ChainName): IAdapterApi {
const metadata = this.metadata(chain);
const Adapter = this.protocolToAdapter[
metadata.protocol
] as AdapterClassType<ContractAddrs, IAdapterApi>;
const Adapter = this.protocolToAdapter(metadata.protocol);
if (!Adapter)
throw new Error(`No adapter for protocol ${metadata.protocol}`);
return new Adapter(this.multiProvider);

@ -5,9 +5,14 @@ import { CoreChainName } from '../chains';
import mainnet from './mainnet.json';
import test from './test.json';
import testnetSealevel from './testnet-sealevel.json';
import testnet from './testnet.json';
export const hyperlaneEnvironments = { test, testnet, mainnet };
export const hyperlaneEnvironmentsWithSealevel = {
...hyperlaneEnvironments,
testnet: { ...testnet, ...testnetSealevel },
};
export type HyperlaneEnvironment = keyof typeof hyperlaneEnvironments;
export type HyperlaneEnvironmentChain<E extends HyperlaneEnvironment> = Extract<

@ -0,0 +1,19 @@
{
"solanadevnet": {
"storageGasOracle": "",
"validatorAnnounce": "",
"proxyAdmin": "",
"mailbox": "4v25Dz9RccqUrTzmfHzJMsjd1iVoNrWzeJ4o6GYuJrVn",
"interchainGasPaymaster": "",
"defaultIsmInterchainGasPaymaster": "",
"multisigIsm": "",
"testRecipient": "",
"interchainAccountIsm": "",
"aggregationIsmFactory": "",
"routingIsmFactory": "",
"interchainQueryRouter": "",
"interchainAccountRouter": "",
"merkleRootMultisigIsmFactory": "",
"messageIdMultisigIsmFactory": ""
}
}

@ -0,0 +1,2 @@
export const SEALEVEL_SPL_NOOP_ADDRESS =
'noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV';

@ -3,6 +3,7 @@ import { Contract } from 'ethers';
import { Ownable } from '@hyperlane-xyz/core';
import {
Address,
ProtocolType,
ValueOf,
objFilter,
objMap,
@ -10,6 +11,7 @@ import {
promiseObjAll,
} from '@hyperlane-xyz/utils';
import { ChainMetadataManager } from '../metadata/ChainMetadataManager';
import { MultiProvider } from '../providers/MultiProvider';
import { ChainMap, Connection } from '../types';
@ -63,6 +65,19 @@ export function filterAddressesMap<F extends HyperlaneFactories>(
);
}
export function filterAddressesToProtocol(
addresses: HyperlaneAddressesMap<any>,
protocolType: ProtocolType,
// Note, MultiProviders are passable here
metadataManager: ChainMetadataManager<any>,
): HyperlaneAddressesMap<any> {
return objFilter(
addresses,
(c, _addrs): _addrs is any =>
metadataManager.tryGetChainMetadata(c)?.protocol === protocolType,
);
}
export function attachContracts<F extends HyperlaneFactories>(
addresses: HyperlaneAddresses<F>,
factories: F,
@ -141,6 +156,6 @@ export function appFromAddressesMapHelper<F extends HyperlaneFactories>(
return {
contractsMap: filteredContractsMap,
multiProvider: intersection.multiProvider,
multiProvider: intersection.result,
};
}

@ -1,12 +1,7 @@
import { ethers } from 'ethers';
import { Mailbox, Mailbox__factory } from '@hyperlane-xyz/core';
import {
ParsedMessage,
messageId,
parseMessage,
pollAsync,
} from '@hyperlane-xyz/utils';
import { Mailbox__factory } from '@hyperlane-xyz/core';
import { messageId, parseMessage, pollAsync } from '@hyperlane-xyz/utils';
import { HyperlaneApp } from '../app/HyperlaneApp';
import {
@ -19,12 +14,7 @@ import { MultiProvider } from '../providers/MultiProvider';
import { ChainName } from '../types';
import { CoreFactories, coreFactories } from './contracts';
export type DispatchedMessage = {
id: string;
message: string;
parsed: ParsedMessage;
};
import { DispatchedMessage } from './types';
export class HyperlaneCore extends HyperlaneApp<CoreFactories> {
static fromEnvironment<Env extends HyperlaneEnvironment>(
@ -50,22 +40,16 @@ export class HyperlaneCore extends HyperlaneApp<CoreFactories> {
return new HyperlaneCore(helper.contractsMap, helper.multiProvider);
}
protected getDestination(message: DispatchedMessage): {
destinationChain: ChainName;
mailbox: Mailbox;
} {
const destinationChain = this.multiProvider.getChainName(
message.parsed.destination,
);
const mailbox = this.getContracts(destinationChain).mailbox;
return { destinationChain, mailbox };
protected getDestination(message: DispatchedMessage): ChainName {
return this.multiProvider.getChainName(message.parsed.destination);
}
protected waitForProcessReceipt(
message: DispatchedMessage,
): Promise<ethers.ContractReceipt> {
const id = messageId(message.message);
const { destinationChain, mailbox } = this.getDestination(message);
const destinationChain = this.getDestination(message);
const mailbox = this.contractsMap[destinationChain].mailbox;
const filter = mailbox.filters.ProcessId(id);
return new Promise<ethers.ContractReceipt>((resolve, reject) => {
@ -80,17 +64,27 @@ export class HyperlaneCore extends HyperlaneApp<CoreFactories> {
});
}
protected async waitForMessageWasProcessed(
message: DispatchedMessage,
async waitForMessageIdProcessed(
messageId: string,
destination: ChainName,
delayMs?: number,
maxAttempts?: number,
): Promise<void> {
const id = messageId(message.message);
const { mailbox } = this.getDestination(message);
await pollAsync(async () => {
const delivered = await mailbox.delivered(id);
if (!delivered) {
throw new Error(`Message ${id} not yet processed`);
}
});
await pollAsync(
async () => {
this.logger(`Checking if message ${messageId} was processed`);
const mailbox = this.contractsMap[destination].mailbox;
const delivered = await mailbox.delivered(messageId);
if (delivered) {
this.logger(`Message ${messageId} was processed`);
return;
} else {
throw new Error(`Message ${messageId} not yet processed`);
}
},
delayMs,
maxAttempts,
);
return;
}
@ -101,12 +95,22 @@ export class HyperlaneCore extends HyperlaneApp<CoreFactories> {
return Promise.all(messages.map((msg) => this.waitForProcessReceipt(msg)));
}
// TODO consider renaming this, all the waitForMessage* methods are confusing
async waitForMessageProcessed(
sourceTx: ethers.ContractReceipt,
delay?: number,
maxAttempts?: number,
): Promise<void> {
const messages = HyperlaneCore.getDispatchedMessages(sourceTx);
await Promise.all(
messages.map((msg) => this.waitForMessageWasProcessed(msg)),
messages.map((msg) =>
this.waitForMessageIdProcessed(
msg.id,
this.getDestination(msg),
delay,
maxAttempts,
),
),
);
}

@ -0,0 +1,36 @@
import { expect } from 'chai';
import { ethers } from 'ethers';
import { ethereum } from '../consts/chainMetadata';
import { Chains } from '../consts/chains';
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<CoreAddresses>({
ethereum: {
...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);
});
it('from environment', async () => {
const multiProvider = new MultiProtocolProvider();
const core = MultiProtocolCore.fromEnvironment('mainnet', multiProvider);
expect(core).to.be.instanceOf(MultiProtocolCore);
const ethAdapter = core.adapter(Chains.ethereum);
expect(ethAdapter).to.be.instanceOf(EvmCoreAdapter);
});
});
});

@ -0,0 +1,78 @@
import debug from 'debug';
import { ProtocolType } from '@hyperlane-xyz/utils';
import { AdapterClassType, MultiProtocolApp } from '../app/MultiProtocolApp';
import {
HyperlaneEnvironment,
hyperlaneEnvironments,
} from '../consts/environments';
import { MultiProtocolProvider } from '../providers/MultiProtocolProvider';
import { TypedTransactionReceipt } from '../providers/ProviderType';
import { ChainMap, ChainName } from '../types';
import { EvmCoreAdapter } from './adapters/EvmCoreAdapter';
import { SealevelCoreAdapter } from './adapters/SealevelCoreAdapter';
import { ICoreAdapter } from './adapters/types';
import { CoreAddresses } from './contracts';
export class MultiProtocolCore extends MultiProtocolApp<
CoreAddresses,
ICoreAdapter
> {
constructor(
public readonly multiProvider: MultiProtocolProvider<CoreAddresses>,
public readonly logger = debug('hyperlane:MultiProtocolCore'),
) {
super(multiProvider, logger);
}
static fromEnvironment<Env extends HyperlaneEnvironment>(
env: Env,
multiProvider: MultiProtocolProvider,
): MultiProtocolCore {
const envAddresses = hyperlaneEnvironments[env];
if (!envAddresses) {
throw new Error(`No addresses found for ${env}`);
}
return MultiProtocolCore.fromAddressesMap(envAddresses, multiProvider);
}
static fromAddressesMap(
addressesMap: ChainMap<CoreAddresses>,
multiProvider: MultiProtocolProvider,
): MultiProtocolCore {
const mpWithAddresses = multiProvider
.intersect(Object.keys(addressesMap))
.result.extendChainMetadata(addressesMap);
return new MultiProtocolCore(mpWithAddresses);
}
override protocolToAdapter(
protocol: ProtocolType,
): AdapterClassType<CoreAddresses, ICoreAdapter> {
if (protocol === ProtocolType.Ethereum) return EvmCoreAdapter;
if (protocol === ProtocolType.Sealevel) return SealevelCoreAdapter;
throw new Error(`No adapter for protocol ${protocol}`);
}
waitForMessagesProcessed(
origin: ChainName,
destination: ChainName,
sourceTx: TypedTransactionReceipt,
delayMs?: number,
maxAttempts?: number,
): Promise<void[]> {
const messages = this.adapter(origin).extractMessageIds(sourceTx);
return Promise.all(
messages.map((msg) =>
this.adapter(destination).waitForMessageProcessed(
msg.messageId,
msg.destination,
delayMs,
maxAttempts,
),
),
);
}
}

@ -0,0 +1,89 @@
import {
Address,
HexString,
ProtocolType,
objMap,
pick,
} from '@hyperlane-xyz/utils';
import { BaseEvmAdapter } from '../../app/MultiProtocolApp';
import {
attachContractsMap,
filterAddressesToProtocol,
} from '../../contracts/contracts';
import { MultiProtocolProvider } from '../../providers/MultiProtocolProvider';
import {
ProviderType,
TypedTransactionReceipt,
} from '../../providers/ProviderType';
import { ChainName } from '../../types';
import { HyperlaneCore } from '../HyperlaneCore';
import { CoreAddresses, 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<CoreAddresses, 'timelockController'>;
// This adapter just routes to the HyperlaneCore
// Which implements the needed functionality for EVM chains
export class EvmCoreAdapter
extends BaseEvmAdapter<CoreAddresses>
implements ICoreAdapter
{
core: HyperlaneCore;
constructor(
public readonly multiProvider: MultiProtocolProvider<CoreAddresses>,
) {
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<CoreAddressKeys>;
const addresses = objMap(multiProvider.metadata, (_, m) =>
pick<CoreAddressKeys, Address>(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);
this.core = new HyperlaneCore(
contractsMap,
multiProvider.toMultiProvider(),
);
}
extractMessageIds(
sourceTx: TypedTransactionReceipt,
): Array<{ messageId: string; destination: ChainName }> {
if (sourceTx.type !== ProviderType.EthersV5) {
throw new Error(
`Unsupported provider type for EvmCoreAdapter ${sourceTx.type}`,
);
}
const messages = this.core.getDispatchedMessages(sourceTx.receipt);
return messages.map(({ id, parsed }) => ({
messageId: id,
destination: this.multiProvider.getChainName(parsed.destination),
}));
}
waitForMessageProcessed(
messageId: HexString,
destination: ChainName,
delayMs?: number,
maxAttempts?: number,
): Promise<void> {
return this.core.waitForMessageIdProcessed(
messageId,
destination,
delayMs,
maxAttempts,
);
}
}

@ -0,0 +1,21 @@
import { expect } from 'chai';
import { SealevelCoreAdapter } from './SealevelCoreAdapter';
describe('SealevelCoreAdapter', () => {
describe('parses dispatch messages', () => {
it('finds message id', async () => {
expect(
SealevelCoreAdapter.parseMessageDispatchLogs([
'Dispatched message to 123, ID abc',
]),
).to.eql([{ destination: '123', messageId: 'abc' }]);
});
it('Skips invalid', async () => {
expect(SealevelCoreAdapter.parseMessageDispatchLogs([])).to.eql([]);
expect(
SealevelCoreAdapter.parseMessageDispatchLogs(['foo', 'bar']),
).to.eql([]);
});
});
});

@ -0,0 +1,148 @@
import { PublicKey } from '@solana/web3.js';
import { HexString, pollAsync } from '@hyperlane-xyz/utils';
import { BaseSealevelAdapter } from '../../app/MultiProtocolApp';
import {
ProviderType,
TypedTransactionReceipt,
} from '../../providers/ProviderType';
import { ChainName } from '../../types';
import { CoreAddresses } from '../contracts';
import { ICoreAdapter } from './types';
// https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/sealevel/programs/mailbox/src/processor.rs
const MESSAGE_DISPATCH_LOG_REGEX = /Dispatched message to (.*), ID (.*)/;
export class SealevelCoreAdapter
extends BaseSealevelAdapter<CoreAddresses>
implements ICoreAdapter
{
extractMessageIds(
sourceTx: TypedTransactionReceipt,
): Array<{ messageId: HexString; destination: ChainName }> {
if (sourceTx.type !== ProviderType.SolanaWeb3) {
throw new Error(
`Unsupported provider type for SealevelCoreAdapter ${sourceTx.type}`,
);
}
const receipt = sourceTx.receipt;
const logs = receipt.meta?.logMessages;
if (!logs)
throw new Error('Transaction logs required to check message delivery');
const parsedLogs = SealevelCoreAdapter.parseMessageDispatchLogs(logs);
if (!parsedLogs.length) throw new Error('Message dispatch log not found');
return parsedLogs.map(({ destination, messageId }) => ({
messageId: Buffer.from(messageId, 'hex').toString('hex'),
destination: this.multiProvider.getChainName(destination),
}));
}
async waitForMessageProcessed(
messageId: string,
destination: ChainName,
delayMs?: number,
maxAttempts?: number,
): Promise<void> {
const destinationMailbox =
this.multiProvider.getChainMetadata(destination).mailbox;
const pda = SealevelCoreAdapter.deriveMailboxMessageProcessedPda(
destinationMailbox,
messageId,
);
const connection = this.multiProvider.getSolanaWeb3Provider(destination);
await pollAsync(
async () => {
// If the PDA exists, then the message has been processed
// Checking existence by checking for account info
const accountInfo = await connection.getAccountInfo(pda);
if (accountInfo?.data?.length) return;
else throw new Error(`Message ${messageId} not yet processed`);
},
delayMs,
maxAttempts,
);
}
static parseMessageDispatchLogs(
logs: string[],
): Array<{ destination: string; messageId: string }> {
const result: Array<{ destination: string; messageId: string }> = [];
for (const log of logs) {
if (!MESSAGE_DISPATCH_LOG_REGEX.test(log)) continue;
const [, destination, messageId] = MESSAGE_DISPATCH_LOG_REGEX.exec(log)!;
if (destination && messageId) result.push({ destination, messageId });
}
return result;
}
/*
* Methods for deriving PDA addresses
* Should match https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/sealevel/programs/mailbox/src/pda_seeds.rs
*/
static deriveMailboxInboxPda(
mailboxProgramId: string | PublicKey,
): PublicKey {
return super.derivePda(['hyperlane', '-', 'inbox'], mailboxProgramId);
}
static deriveMailboxOutboxPda(
mailboxProgramId: string | PublicKey,
): PublicKey {
return super.derivePda(['hyperlane', '-', 'outbox'], mailboxProgramId);
}
static deriveMailboxDispatchedMessagePda(
mailboxProgramId: string | PublicKey,
uniqueMessageAccount: string | PublicKey,
): PublicKey {
return super.derivePda(
[
'hyperlane',
'-',
'dispatched_message',
'-',
new PublicKey(uniqueMessageAccount).toBuffer(),
],
mailboxProgramId,
);
}
static deriveMailboxDispatchAuthorityPda(
programId: string | PublicKey,
): PublicKey {
return super.derivePda(
['hyperlane_dispatcher', '-', 'dispatch_authority'],
programId,
);
}
static deriveMailboxProcessAuthorityPda(
mailboxProgramId: string | PublicKey,
recipient: string | PublicKey,
): PublicKey {
return super.derivePda(
[
'hyperlane',
'-',
'process_authority',
'-',
new PublicKey(recipient).toBuffer(),
],
mailboxProgramId,
);
}
static deriveMailboxMessageProcessedPda(
mailboxProgramId: string | PublicKey,
messageId: string,
): PublicKey {
return super.derivePda(
['hyperlane', '-', 'processed_message', Buffer.from(messageId, 'hex')],
mailboxProgramId,
);
}
}

@ -0,0 +1,18 @@
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<CoreAddresses> {
extractMessageIds(
r: TypedTransactionReceipt,
): Array<{ messageId: HexString; destination: ChainName }>;
waitForMessageProcessed(
messageId: HexString,
destination: ChainName,
delayMs?: number,
maxAttempts?: number,
): Promise<void>;
}

@ -4,6 +4,7 @@ import {
TimelockController__factory,
ValidatorAnnounce__factory,
} from '@hyperlane-xyz/core';
import { Address } from '@hyperlane-xyz/utils';
export const coreFactories = {
validatorAnnounce: new ValidatorAnnounce__factory(),
@ -12,4 +13,11 @@ export const coreFactories = {
timelockController: new TimelockController__factory(),
};
export interface CoreAddresses {
validatorAnnounce: Address;
proxyAdmin: Address;
mailbox: Address;
timelockController?: Address;
}
export type CoreFactories = typeof coreFactories;

@ -1,10 +1,10 @@
import { Mailbox } from '@hyperlane-xyz/core';
import type { Address } from '@hyperlane-xyz/utils';
import type { Mailbox } from '@hyperlane-xyz/core';
import type { Address, ParsedMessage } from '@hyperlane-xyz/utils';
import { UpgradeConfig } from '../deploy/proxy';
import type { UpgradeConfig } from '../deploy/proxy';
import type { CheckerViolation } from '../deploy/types';
import { IsmConfig } from '../ism/types';
import { ChainName } from '../types';
import type { IsmConfig } from '../ism/types';
import type { ChainName } from '../types';
export type CoreConfig = {
defaultIsm: IsmConfig;
@ -41,3 +41,9 @@ export interface ValidatorAnnounceViolation extends CheckerViolation {
actual: boolean;
expected: boolean;
}
export type DispatchedMessage = {
id: string;
message: string;
parsed: ParsedMessage;
};

@ -1,7 +1,6 @@
export { HyperlaneApp } from './app/HyperlaneApp';
export {
AdapterClassType,
AdapterProtocolMap,
BaseAppAdapter,
BaseEvmAdapter,
BaseSealevelAdapter,
@ -28,14 +27,18 @@ export {
HyperlaneEnvironmentChain,
hyperlaneContractAddresses,
hyperlaneEnvironments,
hyperlaneEnvironmentsWithSealevel,
} from './consts/environments';
export { defaultMultisigIsmConfigs } from './consts/multisigIsm';
export { SEALEVEL_SPL_NOOP_ADDRESS } from './consts/sealevel';
export {
attachContracts,
attachContractsMap,
connectContracts,
connectContractsMap,
filterAddressesMap,
filterAddressesToProtocol,
filterOwnableContracts,
serializeContracts,
serializeContractsMap,
} from './contracts/contracts';
@ -47,16 +50,21 @@ export {
HyperlaneContractsMap,
HyperlaneFactories,
} from './contracts/types';
export { DispatchedMessage, HyperlaneCore } from './core/HyperlaneCore';
export { HyperlaneCore } from './core/HyperlaneCore';
export { HyperlaneCoreChecker } from './core/HyperlaneCoreChecker';
export { HyperlaneCoreDeployer } from './core/HyperlaneCoreDeployer';
export { MultiProtocolCore } from './core/MultiProtocolCore';
export { TestCoreApp } from './core/TestCoreApp';
export { TestCoreDeployer } from './core/TestCoreDeployer';
export { EvmCoreAdapter } from './core/adapters/EvmCoreAdapter';
export { SealevelCoreAdapter } from './core/adapters/SealevelCoreAdapter';
export { ICoreAdapter } from './core/adapters/types';
export { CoreFactories, coreFactories } from './core/contracts';
export { HyperlaneLifecyleEvent } from './core/events';
export {
CoreConfig,
CoreViolationType,
DispatchedMessage,
MailboxMultisigIsmViolation,
MailboxViolation,
MailboxViolationType,
@ -79,6 +87,7 @@ export * as verificationUtils from './deploy/verify/utils';
export { HyperlaneIgp } from './gas/HyperlaneIgp';
export { HyperlaneIgpChecker } from './gas/HyperlaneIgpChecker';
export { HyperlaneIgpDeployer } from './gas/HyperlaneIgpDeployer';
export { IgpFactories, igpFactories } from './gas/contracts';
export { CoinGeckoTokenPriceGetter } from './gas/token-prices';
export {
GasOracleContractType,
@ -111,20 +120,24 @@ export {
RoutingIsmConfig,
} from './ism/types';
export {
ChainMetadataManager,
ChainMetadataManagerOptions,
} from './metadata/ChainMetadataManager';
export {
AgentChainMetadata,
AgentChainMetadataSchema,
AgentChainSetup,
AgentChainSetupBase,
AgentConfig,
AgentConfigSchema,
AgentConfigV2,
AgentConnection,
AgentConnectionType,
AgentLogFormat,
AgentLogLevel,
AgentSigner,
AgentSignerSchema,
AgentSignerV2,
AgentChainMetadata,
AgentChainMetadataSchema,
AgentConfigSchema,
AgentLogLevel,
AgentLogFormat,
AgentConfigV2,
buildAgentConfig,
buildAgentConfigDeprecated,
buildAgentConfigNew,
@ -132,10 +145,10 @@ export {
export {
ChainMetadata,
ChainMetadataSchema,
RpcUrlSchema,
RpcUrl,
ExplorerFamily,
ExplorerFamilyValue,
RpcUrl,
RpcUrlSchema,
getDomainId,
isValidChainMetadata,
} from './metadata/chainMetadataTypes';
@ -176,17 +189,21 @@ export {
EthersV5Contract,
EthersV5Provider,
EthersV5Transaction,
EthersV5TransactionReceipt,
ProviderMap,
ProviderType,
SolanaWeb3Contract,
SolanaWeb3Provider,
SolanaWeb3Transaction,
SolanaWeb3TransactionReceipt,
TypedContract,
TypedProvider,
TypedTransaction,
TypedTransactionReceipt,
ViemContract,
ViemProvider,
ViemTransaction,
ViemTransactionReceipt,
} from './providers/ProviderType';
export {
RetryJsonRpcProvider,
@ -251,4 +268,9 @@ export {
export { MultiGeneric } from './utils/MultiGeneric';
export { filterByChains } from './utils/filter';
export { multisigIsmVerificationCost } from './utils/ism';
export {
SealevelAccountDataWrapper,
SealevelInstructionWrapper,
getSealevelAccountDataSchema,
} from './utils/sealevel';
export { chainMetadataToWagmiChain, wagmiChainMetadata } from './utils/wagmi';

@ -1,6 +1,6 @@
import { Debugger, debug } from 'debug';
import { exclude, isNumeric } from '@hyperlane-xyz/utils';
import { exclude, isNumeric, pick } from '@hyperlane-xyz/utils';
import { chainMetadata as defaultChainMetadata } from '../consts/chainMetadata';
import { ChainMap, ChainName } from '../types';
@ -22,7 +22,7 @@ export interface ChainMetadataManagerOptions {
*/
export class ChainMetadataManager<MetaExt = {}> {
public readonly metadata: ChainMap<ChainMetadata<MetaExt>> = {};
protected readonly logger: Debugger;
public readonly logger: Debugger;
/**
* Create a new ChainMetadataManager with the given chainMetadata,
@ -286,4 +286,36 @@ export class ChainMetadataManager<MetaExt = {}> {
}
return new ChainMetadataManager(newMetadata);
}
/**
* Create a new instance from the intersection
* of current's chains and the provided chain list
*/
intersect(
chains: ChainName[],
throwIfNotSubset = false,
): {
intersection: ChainName[];
result: ChainMetadataManager<MetaExt>;
} {
const knownChains = this.getKnownChainNames();
const intersection: ChainName[] = [];
for (const chain of chains) {
if (knownChains.includes(chain)) intersection.push(chain);
else if (throwIfNotSubset)
throw new Error(`Known chains does not include ${chain}`);
}
if (!intersection.length) {
throw new Error(
`No chains shared between known chains and list (${knownChains} and ${chains})`,
);
}
const intersectionMetadata = pick(this.metadata, intersection);
const result = new ChainMetadataManager(intersectionMetadata);
return { intersection, result };
}
}

@ -1,13 +1,13 @@
import { Debugger, debug } from 'debug';
import { objMap } from '@hyperlane-xyz/utils';
import { objFilter, objMap, pick } from '@hyperlane-xyz/utils';
import { chainMetadata as defaultChainMetadata } from '../consts/chainMetadata';
import { ChainMetadataManager } from '../metadata/ChainMetadataManager';
import type { ChainMetadata } from '../metadata/chainMetadataTypes';
import type { ChainMap, ChainName } from '../types';
import type { MultiProvider } from './MultiProvider';
import { MultiProvider, MultiProviderOptions } from './MultiProvider';
import {
EthersV5Provider,
ProviderMap,
@ -23,6 +23,7 @@ import {
export interface MultiProtocolProviderOptions {
loggerName?: string;
providers?: ChainMap<ProviderMap<TypedProvider>>;
providerBuilders?: Partial<ProviderBuilderMap>;
}
@ -39,10 +40,12 @@ export interface MultiProtocolProviderOptions {
export class MultiProtocolProvider<
MetaExt = {},
> extends ChainMetadataManager<MetaExt> {
protected readonly providers: ChainMap<ProviderMap<TypedProvider>> = {};
// Chain name -> provider type -> provider
protected readonly providers: ChainMap<ProviderMap<TypedProvider>>;
// Chain name -> provider type -> signer
protected signers: ChainMap<ProviderMap<never>> = {}; // TODO signer support
protected readonly logger: Debugger;
protected readonly providerBuilders: Partial<ProviderBuilderMap>;
public readonly logger: Debugger;
constructor(
chainMetadata: ChainMap<
@ -54,6 +57,7 @@ export class MultiProtocolProvider<
this.logger = debug(
options?.loggerName || 'hyperlane:MultiProtocolProvider',
);
this.providers = options.providers || {};
this.providerBuilders =
options.providerBuilders || defaultProviderBuilderMap;
}
@ -63,19 +67,42 @@ export class MultiProtocolProvider<
options: MultiProtocolProviderOptions = {},
): MultiProtocolProvider<MetaExt> {
const newMp = new MultiProtocolProvider<MetaExt>(mp.metadata, options);
const typedProviders = objMap(mp.providers, (_, provider) => ({
type: ProviderType.EthersV5,
provider,
})) as ChainMap<TypedProvider>;
newMp.setProviders(typedProviders);
return newMp;
}
toMultiProvider(options?: MultiProviderOptions): MultiProvider<MetaExt> {
const newMp = new MultiProvider<MetaExt>(this.metadata, options);
const providers = objMap(
this.providers,
(_, typeToProviders) => typeToProviders[ProviderType.EthersV5]?.provider,
) as ChainMap<EthersV5Provider['provider'] | undefined>;
const filteredProviders = objFilter(
providers,
(_, p): p is EthersV5Provider['provider'] => !!p,
) as ChainMap<EthersV5Provider['provider']>;
newMp.setProviders(filteredProviders);
return newMp;
}
override extendChainMetadata<NewExt = {}>(
additionalMetadata: ChainMap<NewExt>,
): MultiProtocolProvider<MetaExt & NewExt> {
const newMetadata = super.extendChainMetadata(additionalMetadata).metadata;
return new MultiProtocolProvider(newMetadata, this.options);
const newMp = new MultiProtocolProvider(newMetadata, {
...this.options,
providers: this.providers,
});
return newMp;
}
tryGetProvider(
@ -119,14 +146,14 @@ export class MultiProtocolProvider<
// getEthersV6Provider(
// chainNameOrId: ChainName | number,
// ): EthersV6Provider['provider'] {
// const provider = this.getProvider(chainNameOrId, ProviderType.EthersV5);
// const provider = this.getProvider(chainNameOrId, ProviderType.EthersV6);
// if (provider.type !== ProviderType.EthersV6)
// throw new Error('Invalid provider type');
// return provider.provider;
// }
getViemProvider(chainNameOrId: ChainName | number): ViemProvider['provider'] {
const provider = this.getProvider(chainNameOrId, ProviderType.EthersV5);
const provider = this.getProvider(chainNameOrId, ProviderType.Viem);
if (provider.type !== ProviderType.Viem)
throw new Error('Invalid provider type');
return provider.provider;
@ -135,7 +162,7 @@ export class MultiProtocolProvider<
getSolanaWeb3Provider(
chainNameOrId: ChainName | number,
): SolanaWeb3Provider['provider'] {
const provider = this.getProvider(chainNameOrId, ProviderType.EthersV5);
const provider = this.getProvider(chainNameOrId, ProviderType.SolanaWeb3);
if (provider.type !== ProviderType.SolanaWeb3)
throw new Error('Invalid provider type');
return provider.provider;
@ -156,4 +183,19 @@ export class MultiProtocolProvider<
this.setProvider(chain, providers[chain]);
}
}
override intersect(
chains: ChainName[],
throwIfNotSubset = false,
): {
intersection: ChainName[];
result: MultiProtocolProvider<MetaExt>;
} {
const { intersection, result } = super.intersect(chains, throwIfNotSubset);
const multiProvider = new MultiProtocolProvider(result.metadata, {
...this.options,
providers: pick(this.providers, intersection),
});
return { intersection, result: multiProvider };
}
}

@ -26,7 +26,9 @@ type Provider = providers.Provider;
export interface MultiProviderOptions {
loggerName?: string;
providers?: ChainMap<Provider>;
providerBuilder?: ProviderBuilderFn<Provider>;
signers?: ChainMap<Signer>;
}
/**
@ -34,9 +36,9 @@ export interface MultiProviderOptions {
* @typeParam MetaExt - Extra metadata fields for chains (such as contract addresses)
*/
export class MultiProvider<MetaExt = {}> extends ChainMetadataManager<MetaExt> {
readonly providers: ChainMap<Provider> = {};
readonly providers: ChainMap<Provider>;
readonly providerBuilder: ProviderBuilderFn<Provider>;
signers: ChainMap<Signer> = {};
signers: ChainMap<Signer>;
useSharedSigner = false; // A single signer to be used for all chains
readonly logger: Debugger;
@ -50,7 +52,9 @@ export class MultiProvider<MetaExt = {}> extends ChainMetadataManager<MetaExt> {
) {
super(chainMetadata, options);
this.logger = debug(options?.loggerName || 'hyperlane:MultiProvider');
this.providers = options?.providers || {};
this.providerBuilder = options?.providerBuilder || defaultProviderBuilder;
this.signers = options?.signers || {};
}
override addChain(metadata: ChainMetadata<MetaExt>): void {
@ -241,40 +245,20 @@ export class MultiProvider<MetaExt = {}> extends ChainMetadataManager<MetaExt> {
* Create a new MultiProvider from the intersection
* of current's chains and the provided chain list
*/
intersect(
override intersect(
chains: ChainName[],
throwIfNotSubset = false,
): {
intersection: ChainName[];
multiProvider: MultiProvider<MetaExt>;
result: MultiProvider<MetaExt>;
} {
const ownChains = this.getKnownChainNames();
const intersection: ChainName[] = [];
for (const chain of chains) {
if (ownChains.includes(chain)) {
intersection.push(chain);
} else if (throwIfNotSubset) {
throw new Error(
`MultiProvider#intersect: chains specified ${chain}, but ownChains did not include it`,
);
}
}
if (!intersection.length) {
throw new Error(
`No chains shared between MultiProvider and list (${ownChains} and ${chains})`,
);
}
const intersectionMetadata = pick(this.metadata, intersection);
const intersectionProviders = pick(this.providers, intersection);
const intersectionSigners = pick(this.signers, intersection);
const multiProvider = new MultiProvider(intersectionMetadata);
multiProvider.setProviders(intersectionProviders);
multiProvider.setSigners(intersectionSigners);
return { intersection, multiProvider };
const { intersection, result } = super.intersect(chains, throwIfNotSubset);
const multiProvider = new MultiProvider(result.metadata, {
...this.options,
providers: pick(this.providers, intersection),
signers: pick(this.signers, intersection),
});
return { intersection, result: multiProvider };
}
/**

@ -1,6 +1,7 @@
import type {
Connection,
Transaction as SolTransaction,
VersionedTransactionResponse as SolTransactionReceipt,
} from '@solana/web3.js';
import type {
Contract as EV5Contract,
@ -12,6 +13,7 @@ import type {
GetContractReturnType,
PublicClient,
Transaction as VTransaction,
TransactionReceipt as VTransactionReceipt,
} from 'viem';
export enum ProviderType {
@ -123,7 +125,6 @@ export interface ViemTransaction extends TypedTransactionBase<VTransaction> {
export interface SolanaWeb3Transaction
extends TypedTransactionBase<SolTransaction> {
type: ProviderType.SolanaWeb3;
// Transaction concept doesn't exist in @solana/web3.js
transaction: SolTransaction;
}
@ -132,3 +133,35 @@ export type TypedTransaction =
// | EthersV6Transaction
| ViemTransaction
| SolanaWeb3Transaction;
/**
* Transaction receipt/response with discriminated union of provider type
*/
interface TypedTransactionReceiptBase<T> {
type: ProviderType;
receipt: T;
}
export interface EthersV5TransactionReceipt
extends TypedTransactionReceiptBase<EV5Providers.TransactionReceipt> {
type: ProviderType.EthersV5;
receipt: EV5Providers.TransactionReceipt;
}
export interface ViemTransactionReceipt
extends TypedTransactionReceiptBase<VTransactionReceipt> {
type: ProviderType.Viem;
receipt: VTransactionReceipt;
}
export interface SolanaWeb3TransactionReceipt
extends TypedTransactionReceiptBase<SolTransactionReceipt> {
type: ProviderType.SolanaWeb3;
receipt: SolTransactionReceipt;
}
export type TypedTransactionReceipt =
| EthersV5TransactionReceipt
| ViemTransactionReceipt
| SolanaWeb3TransactionReceipt;

@ -1,16 +1,15 @@
import { expect } from 'chai';
import { Address } from '@hyperlane-xyz/utils';
import { Chains } from '../consts/chains';
import { MultiProtocolProvider } from '../providers/MultiProtocolProvider';
import { MultiProtocolRouterApp } from './MultiProtocolRouterApps';
import { EvmRouterAdapter } from './adapters/EvmRouterAdapter';
import { RouterAddress } from './types';
describe('MultiProtocolRouterApp', () => {
describe('constructs', () => {
const multiProvider = new MultiProtocolProvider<{ router: Address }>();
const multiProvider = new MultiProtocolProvider<RouterAddress>();
it('creates an app class', async () => {
const app = new MultiProtocolRouterApp(multiProvider);
expect(app).to.be.instanceOf(MultiProtocolRouterApp);

@ -1,6 +1,6 @@
import { Address, Domain, ProtocolType } from '@hyperlane-xyz/utils';
import { MultiProtocolApp } from '../app/MultiProtocolApp';
import { AdapterClassType, MultiProtocolApp } from '../app/MultiProtocolApp';
import { ChainMap, ChainName } from '../types';
import {
@ -20,10 +20,15 @@ export class MultiProtocolRouterApp<
ContractAddrs extends RouterAddress = RouterAddress,
IAdapterApi extends IRouterAdapter = IRouterAdapter,
> extends MultiProtocolApp<ContractAddrs, IAdapterApi> {
public override readonly protocolToAdapter = {
[ProtocolType.Ethereum]: EvmRouterAdapter,
[ProtocolType.Sealevel]: SealevelRouterAdapter,
};
override protocolToAdapter(
protocol: ProtocolType,
): AdapterClassType<ContractAddrs, IAdapterApi> {
// Casts are required here to allow for default adapters while still
// enabling extensible generic types
if (protocol === ProtocolType.Ethereum) return EvmRouterAdapter as any;
if (protocol === ProtocolType.Sealevel) return SealevelRouterAdapter as any;
throw new Error(`No adapter for protocol ${protocol}`);
}
router(chain: ChainName): Address {
return this.metadata(chain).router;
@ -50,10 +55,16 @@ export class MultiProtocolGasRouterApp<
ContractAddrs extends RouterAddress = RouterAddress,
IAdapterApi extends IGasRouterAdapter = IGasRouterAdapter,
> extends MultiProtocolRouterApp<ContractAddrs, IAdapterApi> {
public override readonly protocolToAdapter = {
[ProtocolType.Ethereum]: EvmGasRouterAdapter,
[ProtocolType.Sealevel]: SealevelGasRouterAdapter,
};
override protocolToAdapter(
protocol: ProtocolType,
): AdapterClassType<ContractAddrs, IAdapterApi> {
// Casts are required here to allow for default adapters while still
// enabling extensible generic types
if (protocol === ProtocolType.Ethereum) return EvmGasRouterAdapter as any;
if (protocol === ProtocolType.Sealevel)
return SealevelGasRouterAdapter as any;
throw new Error(`No adapter for protocol ${protocol}`);
}
async quoteGasPayment(
origin: ChainName,

@ -7,7 +7,6 @@ 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';
@ -19,12 +18,6 @@ export class EvmRouterAdapter<
extends BaseEvmAdapter<ContractAddrs>
implements IRouterAdapter<ContractAddrs>
{
constructor(
public readonly multiProvider: MultiProtocolProvider<ContractAddrs>,
) {
super(multiProvider);
}
interchainSecurityModule(chain: ChainName): Promise<Address> {
return this.getConnectedContract(chain).interchainSecurityModule();
}

@ -1,8 +1,10 @@
import { deserializeUnchecked } from 'borsh';
import { expect } from 'chai';
import { SealevelAccountDataWrapper } from '../../utils/sealevel';
import {
SealevelAccountDataWrapper,
SealevelTokenData,
SealevelTokenDataSchema,
} from './SealevelRouterAdapter';
@ -16,15 +18,16 @@ describe('SealevelRouterAdapter', () => {
describe('account info', () => {
it('correctly deserializes router account info', () => {
const rawData = Buffer.from(RAW_ACCOUNT_INFO, 'hex');
const accountData = deserializeUnchecked(
const wrappedData = deserializeUnchecked(
SealevelTokenDataSchema,
SealevelAccountDataWrapper,
rawData,
);
expect(accountData.initialized).to.eql(1);
expect(accountData.data.decimals).to.eql(6);
expect(accountData.data.owner_pub_key?.toBase58()).to.eql(OWNER_PUB_KEY);
expect(accountData.data.remote_router_pubkeys.size).to.eql(2);
expect(wrappedData.initialized).to.eql(1);
const data = wrappedData.data as SealevelTokenData;
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);
});
});
});

@ -5,22 +5,16 @@ import { deserializeUnchecked } from 'borsh';
import { Address, Domain } from '@hyperlane-xyz/utils';
import { BaseSealevelAdapter } from '../../app/MultiProtocolApp';
import { MultiProtocolProvider } from '../../providers/MultiProtocolProvider';
import { ChainName } from '../../types';
import {
SealevelAccountDataWrapper,
getSealevelAccountDataSchema,
} from '../../utils/sealevel';
import { RouterAddress } from '../types';
import { IGasRouterAdapter, IRouterAdapter } from './types';
// Hyperlane Token Borsh Schema
export class SealevelAccountDataWrapper {
initialized!: boolean;
data!: SealevelTokenData;
constructor(public readonly fields: any) {
Object.assign(this, fields);
}
}
// Should match https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/trevor/sealevel-validator-rebase/rust/sealevel/libraries/hyperlane-sealevel-token/src/accounts.rs#L21
// Should match https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/rust/sealevel/libraries/hyperlane-sealevel-token/src/accounts.rs
export class SealevelTokenData {
/// The bump seed for this PDA.
bump!: number;
@ -62,17 +56,9 @@ export class SealevelTokenData {
}
}
// Hyperlane Token Borsh Schema
export const SealevelTokenDataSchema = new Map<any, any>([
[
SealevelAccountDataWrapper,
{
kind: 'struct',
fields: [
['initialized', 'u8'],
['data', SealevelTokenData],
],
},
],
[SealevelAccountDataWrapper, getSealevelAccountDataSchema(SealevelTokenData)],
[
SealevelTokenData,
{
@ -98,12 +84,6 @@ export class SealevelRouterAdapter<
extends BaseSealevelAdapter<ContractAddrs>
implements IRouterAdapter<ContractAddrs>
{
constructor(
public readonly multiProvider: MultiProtocolProvider<ContractAddrs>,
) {
super(multiProvider);
}
async interchainSecurityModule(chain: ChainName): Promise<Address> {
const routerAccountInfo = await this.getRouterAccountInfo(chain);
if (!routerAccountInfo.interchain_security_module_pubkey)
@ -163,11 +143,11 @@ export class SealevelRouterAdapter<
SealevelAccountDataWrapper,
accountInfo.data,
);
return accountData.data;
return accountData.data as SealevelTokenData;
}
// Should match https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/trevor/sealevel-validator-rebase/rust/sealevel/libraries/hyperlane-sealevel-token/src/processor.rs#LL49C1-L53C30
deriveMessageRecipientPda(routerAddress: Address): PublicKey {
// 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'),

@ -0,0 +1,26 @@
export class SealevelInstructionWrapper<Instr> {
instruction!: number;
data!: Instr;
constructor(public readonly fields: any) {
Object.assign(this, fields);
}
}
export class SealevelAccountDataWrapper<T> {
initialized!: boolean;
data!: T;
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
constructor(public readonly fields: any) {
Object.assign(this, fields);
}
}
export function getSealevelAccountDataSchema<T>(DataClass: T) {
return {
kind: 'struct',
fields: [
['initialized', 'u8'],
['data', DataClass],
],
};
}

@ -2,8 +2,8 @@ export {
addressToByteHexString,
addressToBytes,
addressToBytes32,
addressToBytesEvm,
addressToBytesSol,
adressToBytesEvm,
bytes32ToAddress,
capitalizeAddress,
convertToProtocolAddress,
@ -42,6 +42,7 @@ export {
sleep,
timeout,
} from './src/async';
export { base58ToBuffer, bufferToBase58 } from './src/base58';
export { fromBase64, toBase64 } from './src/base64';
export {
BigNumberMax,

@ -176,7 +176,7 @@ export function bytes32ToAddress(bytes32: HexString): Address {
return ethersUtils.getAddress(bytes32.slice(-40));
}
export function adressToBytesEvm(address: Address): Uint8Array {
export function addressToBytesEvm(address: Address): Uint8Array {
const addrBytes32 = addressToBytes32(address);
return Buffer.from(addrBytes32.substring(2), 'hex');
}
@ -187,7 +187,7 @@ export function addressToBytesSol(address: Address): Uint8Array {
export function addressToBytes(address: Address, protocol?: ProtocolType) {
return routeAddressUtil(
adressToBytesEvm,
addressToBytesEvm,
addressToBytesSol,
new Uint8Array(),
address,

@ -0,0 +1,9 @@
import { utils } from 'ethers';
export function base58ToBuffer(value: string) {
return Buffer.from(utils.base58.decode(value));
}
export function bufferToBase58(value: Buffer) {
return utils.base58.encode(value);
}

@ -6,10 +6,6 @@ export function deepEquals(v1: any, v2: any) {
return JSON.stringify(v1) === JSON.stringify(v2);
}
type MappedObject<M extends Record<any, any>, O> = {
[Property in keyof M]: O;
};
export type ValueOf<T> = T[keyof T];
export function objMapEntries<
@ -27,8 +23,8 @@ export function objMap<
K extends keyof M,
O,
I = ValueOf<M>,
>(obj: M, func: (k: K, v: I) => O): MappedObject<M, O> {
return Object.fromEntries<O>(objMapEntries(obj, func)) as MappedObject<M, O>;
>(obj: M, func: (k: K, v: I) => O): Record<K, O> {
return Object.fromEntries<O>(objMapEntries(obj, func)) as Record<K, O>;
}
export function objFilter<K extends string, I, O extends I>(

Loading…
Cancel
Save