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 '@nomiclabs/hardhat-waffle'; |
||||||
|
import { ethers } from 'ethers'; |
||||||
import { extendEnvironment } from 'hardhat/config'; |
import { extendEnvironment } from 'hardhat/config'; |
||||||
import { lazyObject } from "hardhat/plugins"; |
import { lazyObject } from "hardhat/plugins"; |
||||||
import { TestAbacusDeploy } from './src/TestAbacusDeploy' |
import 'hardhat/types/runtime'; |
||||||
export { TestAbacusDeploy } from './src/TestAbacusDeploy' |
import { TestCoreDeploy } from './src/TestCoreDeploy'; |
||||||
export { TestRouterDeploy } from './src/TestRouterDeploy' |
|
||||||
|
|
||||||
|
|
||||||
import "hardhat/types/runtime"; |
|
||||||
|
|
||||||
declare module 'hardhat/types/runtime' { |
declare module 'hardhat/types/runtime' { |
||||||
interface HardhatRuntimeEnvironment { |
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
|
// HardhatRuntimeEnvironment
|
||||||
extendEnvironment((hre) => { |
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