Dedupe hardhat and core deploy (#449)
* Dedupe TestCoreDeploy and CoreDeploy * Simplify TestCoreApp * Remove checkpointing logic from test corepull/503/head
parent
4cd549b7fd
commit
b0475af057
@ -1,20 +1,40 @@ |
||||
import { MultiProvider, TestChainNames } from '@abacus-network/sdk'; |
||||
import '@nomiclabs/hardhat-waffle'; |
||||
import { ethers } from 'ethers'; |
||||
import { extendEnvironment } from 'hardhat/config'; |
||||
import { lazyObject } from "hardhat/plugins"; |
||||
import { TestAbacusDeploy } from './src/TestAbacusDeploy' |
||||
export { TestAbacusDeploy } from './src/TestAbacusDeploy' |
||||
export { TestRouterDeploy } from './src/TestRouterDeploy' |
||||
|
||||
|
||||
import "hardhat/types/runtime"; |
||||
import 'hardhat/types/runtime'; |
||||
import { TestCoreDeploy } from './src/TestCoreDeploy'; |
||||
|
||||
declare module 'hardhat/types/runtime' { |
||||
interface HardhatRuntimeEnvironment { |
||||
abacus: TestAbacusDeploy; |
||||
abacus: TestCoreDeploy; |
||||
} |
||||
} |
||||
|
||||
export function hardhatMultiProvider( |
||||
provider: ethers.providers.Provider, |
||||
signer?: ethers.Signer, |
||||
): MultiProvider<TestChainNames> { |
||||
return new MultiProvider<TestChainNames>({ |
||||
test1: { |
||||
provider, |
||||
signer, |
||||
}, |
||||
test2: { |
||||
provider, |
||||
signer, |
||||
}, |
||||
test3: { |
||||
provider, |
||||
signer, |
||||
}, |
||||
}); |
||||
} |
||||
|
||||
// HardhatRuntimeEnvironment
|
||||
extendEnvironment((hre) => { |
||||
hre.abacus = lazyObject(() => new TestAbacusDeploy({ signer: {} })); |
||||
hre.abacus = lazyObject( |
||||
() => new TestCoreDeploy(hardhatMultiProvider(hre.ethers.provider)), |
||||
); |
||||
}); |
||||
|
@ -1,213 +0,0 @@ |
||||
import { TestDeploy } from './TestDeploy'; |
||||
import { |
||||
AbacusConnectionManager, |
||||
AbacusConnectionManager__factory, |
||||
InterchainGasPaymaster, |
||||
InterchainGasPaymaster__factory, |
||||
Outbox, |
||||
Outbox__factory, |
||||
TestInbox, |
||||
TestInbox__factory, |
||||
UpgradeBeaconController, |
||||
UpgradeBeaconController__factory, |
||||
} from '@abacus-network/core'; |
||||
import { types } from '@abacus-network/utils'; |
||||
import { addressToBytes32 } from '@abacus-network/utils/dist/src/utils'; |
||||
import { ethers } from 'ethers'; |
||||
|
||||
export type TestAbacusConfig = { |
||||
signer: Record<types.Domain, ethers.Signer>; |
||||
}; |
||||
|
||||
// Outbox & inbox validator managers are not required for testing and are therefore omitted.
|
||||
export type TestAbacusInstance = { |
||||
outbox: Outbox; |
||||
abacusConnectionManager: AbacusConnectionManager; |
||||
upgradeBeaconController: UpgradeBeaconController; |
||||
inboxes: Record<types.Domain, TestInbox>; |
||||
interchainGasPaymaster: InterchainGasPaymaster; |
||||
}; |
||||
|
||||
export class TestAbacusDeploy extends TestDeploy< |
||||
TestAbacusInstance, |
||||
TestAbacusConfig |
||||
> { |
||||
async deploy(domains: types.Domain[], signer: ethers.Signer) { |
||||
// Clear previous deploy to support multiple tests.
|
||||
for (const domain of this.domains) { |
||||
delete this.config.signer[domain]; |
||||
delete this.instances[domain]; |
||||
} |
||||
for (const domain of domains) { |
||||
this.config.signer[domain] = signer; |
||||
} |
||||
for (const domain of domains) { |
||||
this.instances[domain] = await this.deployInstance(domain); |
||||
} |
||||
} |
||||
|
||||
async deployInstance(domain: types.Domain): Promise<TestAbacusInstance> { |
||||
const signer = this.config.signer[domain]; |
||||
|
||||
const upgradeBeaconControllerFactory = new UpgradeBeaconController__factory( |
||||
signer, |
||||
); |
||||
const upgradeBeaconController = |
||||
await upgradeBeaconControllerFactory.deploy(); |
||||
|
||||
const outboxFactory = new Outbox__factory(signer); |
||||
const outbox = await outboxFactory.deploy(domain); |
||||
// Outbox will require the validator manager to be a contract. We don't
|
||||
// actually make use of the validator manager, so just we pass in the
|
||||
// upgradeBeaconController as the validator manager to satisfy the contract
|
||||
// requirement and avoid deploying a new validator manager.
|
||||
await outbox.initialize(upgradeBeaconController.address); |
||||
|
||||
const abacusConnectionManagerFactory = new AbacusConnectionManager__factory( |
||||
signer, |
||||
); |
||||
const abacusConnectionManager = |
||||
await abacusConnectionManagerFactory.deploy(); |
||||
await abacusConnectionManager.setOutbox(outbox.address); |
||||
|
||||
const interchainGasPaymasterFactory = new InterchainGasPaymaster__factory( |
||||
signer, |
||||
); |
||||
const interchainGasPaymaster = await interchainGasPaymasterFactory.deploy(); |
||||
await abacusConnectionManager.setInterchainGasPaymaster( |
||||
interchainGasPaymaster.address, |
||||
); |
||||
|
||||
const inboxFactory = new TestInbox__factory(signer); |
||||
const inboxes: Record<types.Domain, TestInbox> = {}; |
||||
|
||||
// this.remotes reads this.instances which has not yet been set.
|
||||
const remotes = Object.keys(this.config.signer).map((d) => parseInt(d)); |
||||
const deploys = remotes.map(async (remote) => { |
||||
const inbox = await inboxFactory.deploy(domain); |
||||
// Inbox will require the validator manager to be a contract. We don't
|
||||
// actually make use of the validator manager, so we just pass in the
|
||||
// upgradeBeaconController as the validator manager to satisfy the contract
|
||||
// requirement and avoid deploying a new validator manager.
|
||||
await inbox.initialize( |
||||
remote, |
||||
upgradeBeaconController.address, |
||||
ethers.constants.HashZero, |
||||
0, |
||||
); |
||||
await abacusConnectionManager.enrollInbox(remote, inbox.address); |
||||
inboxes[remote] = inbox; |
||||
}); |
||||
await Promise.all(deploys); |
||||
|
||||
// dispatch a dummy event to allow a consumer to checkpoint/process a single message
|
||||
await outbox.dispatch( |
||||
remotes.find((_) => _ !== domain)!, |
||||
addressToBytes32(ethers.constants.AddressZero), |
||||
'0x', |
||||
); |
||||
return { |
||||
outbox, |
||||
abacusConnectionManager, |
||||
interchainGasPaymaster, |
||||
inboxes, |
||||
upgradeBeaconController, |
||||
}; |
||||
} |
||||
|
||||
async transferOwnership(domain: types.Domain, address: types.Address) { |
||||
await this.outbox(domain).transferOwnership(address); |
||||
await this.upgradeBeaconController(domain).transferOwnership(address); |
||||
await this.abacusConnectionManager(domain).transferOwnership(address); |
||||
for (const remote of this.remotes(domain)) { |
||||
await this.inbox(domain, remote).transferOwnership(address); |
||||
} |
||||
} |
||||
|
||||
outbox(domain: types.Domain): Outbox { |
||||
return this.instances[domain].outbox; |
||||
} |
||||
|
||||
upgradeBeaconController(domain: types.Domain): UpgradeBeaconController { |
||||
return this.instances[domain].upgradeBeaconController; |
||||
} |
||||
|
||||
inbox(origin: types.Domain, destination: types.Domain): TestInbox { |
||||
return this.instances[destination].inboxes[origin]; |
||||
} |
||||
|
||||
interchainGasPaymaster(domain: types.Domain): InterchainGasPaymaster { |
||||
return this.instances[domain].interchainGasPaymaster; |
||||
} |
||||
|
||||
abacusConnectionManager(domain: types.Domain): AbacusConnectionManager { |
||||
return this.instances[domain].abacusConnectionManager; |
||||
} |
||||
|
||||
async processMessages(): Promise< |
||||
Map<types.Domain, Map<types.Domain, ethers.providers.TransactionResponse[]>> |
||||
> { |
||||
const responses: Map< |
||||
types.Domain, |
||||
Map<types.Domain, ethers.providers.TransactionResponse[]> |
||||
> = new Map(); |
||||
for (const origin of this.domains) { |
||||
const outbound = await this.processOutboundMessages(origin); |
||||
responses.set(origin, new Map()); |
||||
this.domains.forEach((destination) => { |
||||
responses |
||||
.get(origin)! |
||||
.set(destination, outbound.get(destination) ?? []); |
||||
}); |
||||
} |
||||
return responses; |
||||
} |
||||
|
||||
async processOutboundMessages( |
||||
origin: types.Domain, |
||||
): Promise<Map<types.Domain, ethers.providers.TransactionResponse[]>> { |
||||
const responses: Map<types.Domain, ethers.providers.TransactionResponse[]> = |
||||
new Map(); |
||||
const outbox = this.outbox(origin); |
||||
const [root, index] = await outbox.latestCheckpoint(); |
||||
|
||||
// Find all unprocessed messages dispatched on the outbox since the previous checkpoint.
|
||||
const dispatchFilter = outbox.filters.Dispatch(); |
||||
const dispatches = await outbox.queryFilter(dispatchFilter); |
||||
for (const dispatch of dispatches) { |
||||
if (dispatch.args.leafIndex > index) { |
||||
// Message has not been checkpointed on the outbox
|
||||
break; |
||||
} |
||||
|
||||
const destination = dispatch.args.destination; |
||||
if (destination === origin) |
||||
throw new Error('Dispatched message to local domain'); |
||||
const inbox = this.inbox(origin, destination); |
||||
const status = await inbox.messages(dispatch.args.messageHash); |
||||
if (status !== types.MessageStatus.PROCESSED) { |
||||
if (dispatch.args.leafIndex.toNumber() == 0) { |
||||
// disregard the dummy message
|
||||
continue; |
||||
} |
||||
|
||||
const [, inboxCheckpointIndex] = await inbox.latestCheckpoint(); |
||||
if ( |
||||
inboxCheckpointIndex < dispatch.args.leafIndex && |
||||
inboxCheckpointIndex < index |
||||
) { |
||||
await inbox.setCheckpoint(root, index); |
||||
} |
||||
|
||||
const response = await inbox.testProcess( |
||||
dispatch.args.message, |
||||
dispatch.args.leafIndex.toNumber(), |
||||
); |
||||
let destinationResponses = responses.get(destination) || []; |
||||
destinationResponses.push(response); |
||||
responses.set(destination, destinationResponses); |
||||
} |
||||
} |
||||
return responses; |
||||
} |
||||
} |
@ -0,0 +1,90 @@ |
||||
import { |
||||
TestInbox, |
||||
TestInbox__factory, |
||||
TestOutbox__factory, |
||||
} from '@abacus-network/core'; |
||||
import { |
||||
AbacusCore, |
||||
chainMetadata, |
||||
DomainIdToChainName, |
||||
objMap, |
||||
TestChainNames, |
||||
} from '@abacus-network/sdk'; |
||||
import { types } from '@abacus-network/utils'; |
||||
import { ethers } from 'ethers'; |
||||
|
||||
export class TestCoreApp extends AbacusCore<TestChainNames> { |
||||
getContracts<Local extends TestChainNames>(chain: Local) { |
||||
const contracts = super.getContracts(chain); |
||||
return { |
||||
...contracts, |
||||
outbox: { |
||||
...contracts.outbox, |
||||
outbox: TestOutbox__factory.connect( |
||||
contracts.outbox.outbox.address, |
||||
contracts.outbox.outbox.signer, |
||||
), |
||||
}, |
||||
inboxes: objMap(contracts.inboxes, (_, inbox) => ({ |
||||
...inbox, |
||||
inbox: TestInbox__factory.connect( |
||||
inbox.inbox.address, |
||||
inbox.inbox.signer, |
||||
), |
||||
})), |
||||
}; |
||||
} |
||||
|
||||
async processMessages(): Promise< |
||||
Map< |
||||
TestChainNames, |
||||
Map<TestChainNames, ethers.providers.TransactionResponse[]> |
||||
> |
||||
> { |
||||
const responses = new Map(); |
||||
for (const origin of this.chains()) { |
||||
const outbound = await this.processOutboundMessages(origin); |
||||
const originResponses = new Map(); |
||||
this.remoteChains(origin).forEach((destination) => |
||||
originResponses.set(destination, outbound.get(destination)), |
||||
); |
||||
responses.set(origin, originResponses); |
||||
} |
||||
return responses; |
||||
} |
||||
|
||||
async processOutboundMessages<Local extends TestChainNames>(origin: Local) { |
||||
const responses = new Map(); |
||||
const contracts = this.getContracts(origin); |
||||
const outbox = contracts.outbox.outbox; |
||||
|
||||
const dispatchFilter = outbox.filters.Dispatch(); |
||||
const dispatches = await outbox.queryFilter(dispatchFilter); |
||||
for (const dispatch of dispatches) { |
||||
const destination = dispatch.args.destination; |
||||
if (destination === chainMetadata[origin].id) { |
||||
throw new Error('Dispatched message to local domain'); |
||||
} |
||||
const destinationChain = DomainIdToChainName[destination]; |
||||
const inbox: TestInbox = |
||||
// @ts-ignore
|
||||
this.getContracts(destinationChain).inboxes[origin].inbox; |
||||
const status = await inbox.messages(dispatch.args.messageHash); |
||||
if (status !== types.MessageStatus.PROCESSED) { |
||||
if (dispatch.args.leafIndex.toNumber() == 0) { |
||||
// disregard the dummy message
|
||||
continue; |
||||
} |
||||
|
||||
const response = await inbox.testProcess( |
||||
dispatch.args.message, |
||||
dispatch.args.leafIndex.toNumber(), |
||||
); |
||||
let destinationResponses = responses.get(destinationChain) || []; |
||||
destinationResponses.push(response); |
||||
responses.set(destinationChain, destinationResponses); |
||||
} |
||||
} |
||||
return responses; |
||||
} |
||||
} |
@ -0,0 +1,61 @@ |
||||
import { TestCoreApp } from './TestCoreApp'; |
||||
import { TestInbox__factory, TestOutbox__factory } from '@abacus-network/core'; |
||||
import { AbacusCoreDeployer, CoreConfig } from '@abacus-network/deploy'; |
||||
import { |
||||
chainMetadata, |
||||
CoreContractAddresses, |
||||
MultiProvider, |
||||
TestChainNames, |
||||
} from '@abacus-network/sdk'; |
||||
import { utils } from '@abacus-network/utils'; |
||||
import { ethers } from 'ethers'; |
||||
|
||||
// dummy config as TestInbox and TestOutbox do not use deployed ValidatorManager
|
||||
const testValidatorManagerConfig: CoreConfig = { |
||||
validatorManager: { |
||||
validators: [ethers.constants.AddressZero], |
||||
threshold: 1, |
||||
}, |
||||
}; |
||||
|
||||
export class TestCoreDeploy extends AbacusCoreDeployer<TestChainNames> { |
||||
constructor(public readonly multiProvider: MultiProvider<TestChainNames>) { |
||||
super(multiProvider, { |
||||
test1: testValidatorManagerConfig, |
||||
test2: testValidatorManagerConfig, |
||||
test3: testValidatorManagerConfig, |
||||
}); |
||||
} |
||||
|
||||
inboxFactoryBuilder = (signer: ethers.Signer) => |
||||
new TestInbox__factory(signer); |
||||
outboxFactoryBuilder = (signer: ethers.Signer) => |
||||
new TestOutbox__factory(signer); |
||||
|
||||
async deployContracts<LocalChain extends TestChainNames>( |
||||
local: LocalChain, |
||||
config: CoreConfig, |
||||
): Promise<CoreContractAddresses<TestChainNames, LocalChain>> { |
||||
const addresses = await super.deployContracts(local, config); |
||||
|
||||
const signer = this.multiProvider.getChainConnection(local).signer!; |
||||
const outbox = this.outboxFactoryBuilder(signer).attach( |
||||
addresses.outbox.proxy, |
||||
); |
||||
const remote = this.multiProvider.remoteChains(local)[0]; |
||||
|
||||
// dispatch a dummy event to allow a consumer to checkpoint/process a single message
|
||||
await outbox.dispatch( |
||||
chainMetadata[remote].id, |
||||
utils.addressToBytes32(ethers.constants.AddressZero), |
||||
'0x', |
||||
); |
||||
|
||||
return addresses; |
||||
} |
||||
|
||||
async deployCore() { |
||||
const result = await super.deploy(); |
||||
return new TestCoreApp(result, this.multiProvider); |
||||
} |
||||
} |
@ -1,19 +0,0 @@ |
||||
import { types } from '@abacus-network/utils'; |
||||
|
||||
export class TestDeploy<T, V> { |
||||
public readonly config: V; |
||||
public readonly instances: Record<types.Domain, T>; |
||||
|
||||
constructor(config: V) { |
||||
this.config = config; |
||||
this.instances = {}; |
||||
} |
||||
|
||||
get domains(): types.Domain[] { |
||||
return Object.keys(this.instances).map((d) => parseInt(d)); |
||||
} |
||||
|
||||
remotes(domain: types.Domain): types.Domain[] { |
||||
return this.domains.filter((d) => d !== domain); |
||||
} |
||||
} |
@ -1,30 +0,0 @@ |
||||
import { TestAbacusDeploy } from './TestAbacusDeploy'; |
||||
import { TestDeploy } from './TestDeploy'; |
||||
import { types, utils } from '@abacus-network/utils'; |
||||
|
||||
export interface Router { |
||||
address: types.Address; |
||||
enrollRemoteRouter(domain: types.Domain, router: types.Address): Promise<any>; |
||||
} |
||||
|
||||
export abstract class TestRouterDeploy<T, V> extends TestDeploy<T, V> { |
||||
async deploy(abacus: TestAbacusDeploy) { |
||||
for (const domain of abacus.domains) { |
||||
this.instances[domain] = await this.deployInstance(domain, abacus); |
||||
} |
||||
for (const local of this.domains) { |
||||
for (const remote of this.remotes(local)) { |
||||
await this.router(local).enrollRemoteRouter( |
||||
remote, |
||||
utils.addressToBytes32(this.router(remote).address), |
||||
); |
||||
} |
||||
} |
||||
} |
||||
|
||||
abstract deployInstance( |
||||
domain: types.Domain, |
||||
abacus: TestAbacusDeploy, |
||||
): Promise<T>; |
||||
abstract router(domain: types.Domain): Router; |
||||
} |
Loading…
Reference in new issue