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 Sealevelpull/2704/head
parent
2d8fced77a
commit
f08ceb187f
@ -0,0 +1,4 @@ |
||||
export type StatCounts = { |
||||
sent: number | bigint; |
||||
received: number | bigint; |
||||
}; |
@ -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/ |
@ -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'; |
@ -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>; |
||||
} |
@ -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], |
||||
], |
||||
}; |
||||
} |
@ -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); |
||||
} |
Loading…
Reference in new issue