diff --git a/solidity/app/hardhat.config.ts b/solidity/app/hardhat.config.ts index 67a906b88..ab1f2039d 100644 --- a/solidity/app/hardhat.config.ts +++ b/solidity/app/hardhat.config.ts @@ -1,4 +1,3 @@ -import '@abacus-network/hardhat'; import '@nomiclabs/hardhat-waffle'; import '@typechain/hardhat'; import 'hardhat-gas-reporter'; diff --git a/typescript/hardhat/index.ts b/typescript/hardhat/index.ts index 61659eed9..4f8e17471 100644 --- a/typescript/hardhat/index.ts +++ b/typescript/hardhat/index.ts @@ -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 { + return new MultiProvider({ + 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)), + ); }); diff --git a/typescript/hardhat/package.json b/typescript/hardhat/package.json index 7e95937cf..ea58117a1 100644 --- a/typescript/hardhat/package.json +++ b/typescript/hardhat/package.json @@ -17,6 +17,7 @@ }, "dependencies": { "@abacus-network/core": "^0.1.1", + "@abacus-network/sdk": "^0.1.1", "@abacus-network/utils": "^0.1.1", "@nomiclabs/hardhat-ethers": "^2.0.5", "@nomiclabs/hardhat-waffle": "^2.0.2", diff --git a/typescript/hardhat/src/TestAbacusDeploy.ts b/typescript/hardhat/src/TestAbacusDeploy.ts deleted file mode 100644 index 7834cfedc..000000000 --- a/typescript/hardhat/src/TestAbacusDeploy.ts +++ /dev/null @@ -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; -}; - -// Outbox & inbox validator managers are not required for testing and are therefore omitted. -export type TestAbacusInstance = { - outbox: Outbox; - abacusConnectionManager: AbacusConnectionManager; - upgradeBeaconController: UpgradeBeaconController; - inboxes: Record; - 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 { - 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 = {}; - - // 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> - > { - const responses: Map< - types.Domain, - Map - > = 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> { - const responses: Map = - 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; - } -} diff --git a/typescript/hardhat/src/TestCoreApp.ts b/typescript/hardhat/src/TestCoreApp.ts new file mode 100644 index 000000000..d326f8907 --- /dev/null +++ b/typescript/hardhat/src/TestCoreApp.ts @@ -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 { + getContracts(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 + > + > { + 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(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; + } +} diff --git a/typescript/hardhat/src/TestCoreDeploy.ts b/typescript/hardhat/src/TestCoreDeploy.ts new file mode 100644 index 000000000..037380190 --- /dev/null +++ b/typescript/hardhat/src/TestCoreDeploy.ts @@ -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 { + constructor(public readonly multiProvider: MultiProvider) { + 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( + local: LocalChain, + config: CoreConfig, + ): Promise> { + 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); + } +} diff --git a/typescript/hardhat/src/TestDeploy.ts b/typescript/hardhat/src/TestDeploy.ts deleted file mode 100644 index b62c02761..000000000 --- a/typescript/hardhat/src/TestDeploy.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { types } from '@abacus-network/utils'; - -export class TestDeploy { - public readonly config: V; - public readonly instances: Record; - - 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); - } -} diff --git a/typescript/hardhat/src/TestRouterDeploy.ts b/typescript/hardhat/src/TestRouterDeploy.ts deleted file mode 100644 index 8c3a382c9..000000000 --- a/typescript/hardhat/src/TestRouterDeploy.ts +++ /dev/null @@ -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; -} - -export abstract class TestRouterDeploy extends TestDeploy { - 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; - abstract router(domain: types.Domain): Router; -} diff --git a/typescript/hardhat/test/testAbacusDeploy.test.ts b/typescript/hardhat/test/testAbacusDeploy.test.ts index 7c941669b..6369bd19f 100644 --- a/typescript/hardhat/test/testAbacusDeploy.test.ts +++ b/typescript/hardhat/test/testAbacusDeploy.test.ts @@ -1,24 +1,29 @@ -import { TestAbacusDeploy } from '..'; -import { TestRecipient__factory } from '@abacus-network/core'; +import { hardhatMultiProvider } from '../index'; +import { TestCoreApp } from '../src/TestCoreApp'; +import { TestCoreDeploy } from '../src/TestCoreDeploy'; +import { TestOutbox, TestRecipient__factory } from '@abacus-network/core'; +import { chainMetadata } from '@abacus-network/sdk'; import { utils } from '@abacus-network/utils'; import { expect } from 'chai'; import { ethers } from 'hardhat'; -const localDomain = 1000; -const remoteDomain = 2000; -const domains = [localDomain, remoteDomain]; +const localChain = 'test1'; +const localDomain = chainMetadata[localChain].id; +const remoteChain = 'test2'; +const remoteDomain = chainMetadata[remoteChain].id; const message = '0xdeadbeef'; -describe('TestAbacusDeploy', async () => { - let abacus: TestAbacusDeploy; +describe('TestCoreDeploy', async () => { + let abacus: TestCoreApp, localOutbox: TestOutbox, remoteOutbox: TestOutbox; beforeEach(async () => { - abacus = new TestAbacusDeploy({ signer: {} }); const [signer] = await ethers.getSigners(); - await abacus.deploy(domains, signer); + const multiProvider = hardhatMultiProvider(ethers.provider, signer); + const deployer = new TestCoreDeploy(multiProvider); + abacus = await deployer.deployCore(); const recipient = await new TestRecipient__factory(signer).deploy(); - const localOutbox = abacus.outbox(localDomain); + localOutbox = abacus.getContracts(localChain).outbox.outbox; await expect( localOutbox.dispatch( remoteDomain, @@ -26,7 +31,7 @@ describe('TestAbacusDeploy', async () => { message, ), ).to.emit(localOutbox, 'Dispatch'); - const remoteOutbox = abacus.outbox(remoteDomain); + remoteOutbox = abacus.getContracts(remoteChain).outbox.outbox; await expect( remoteOutbox.dispatch( localDomain, @@ -36,57 +41,21 @@ describe('TestAbacusDeploy', async () => { ).to.emit(remoteOutbox, 'Dispatch'); }); - describe('without a created checkpoint', () => { - it('does not process outbound messages', async () => { - const responses = await abacus.processOutboundMessages(localDomain); - expect(responses.get(remoteDomain)).to.be.undefined; - }); + it('processes outbound messages for a single domain', async () => { + const responses = await abacus.processOutboundMessages(localChain); + expect(responses.get(remoteChain)!.length).to.equal(1); }); - describe('with a checkpoint', () => { - beforeEach(async () => { - const localOutbox = abacus.outbox(localDomain); - const remoteOutbox = abacus.outbox(remoteDomain); - await localOutbox.checkpoint(); - await remoteOutbox.checkpoint(); - }); - - it('processes outbound messages for a single domain', async () => { - const responses = await abacus.processOutboundMessages(localDomain); - expect(responses.get(remoteDomain)!.length).to.equal(1); - const [_, index] = await abacus.outbox(localDomain).latestCheckpoint(); - expect(index).to.equal(1); - }); - - it('processes outbound messages for two domains', async () => { - const localResponses = await abacus.processOutboundMessages(localDomain); - expect(localResponses.get(remoteDomain)!.length).to.equal(1); - const [, localIndex] = await abacus - .outbox(localDomain) - .latestCheckpoint(); - expect(localIndex).to.equal(1); - const remoteResponses = await abacus.processOutboundMessages( - remoteDomain, - ); - expect(remoteResponses.get(localDomain)!.length).to.equal(1); - const [, remoteIndex] = await abacus - .outbox(remoteDomain) - .latestCheckpoint(); - expect(remoteIndex).to.equal(1); - }); + it('processes outbound messages for two domains', async () => { + const localResponses = await abacus.processOutboundMessages(localChain); + expect(localResponses.get(remoteChain)!.length).to.equal(1); + const remoteResponses = await abacus.processOutboundMessages(remoteChain); + expect(remoteResponses.get(localChain)!.length).to.equal(1); + }); - it('processes all messages', async () => { - const responses = await abacus.processMessages(); - expect(responses.get(localDomain)!.get(remoteDomain)!.length).to.equal(1); - expect(responses.get(remoteDomain)!.get(localDomain)!.length).to.equal(1); - const [, localIndex] = await abacus - .outbox(localDomain) - .latestCheckpoint(); - expect(localIndex).to.equal(1); - const [, remoteIndex] = await abacus - .outbox(remoteDomain) - .latestCheckpoint(); - expect(remoteIndex).to.equal(1); - }); + it('processes all messages', async () => { + const responses = await abacus.processMessages(); + expect(responses.get(localChain)!.get(remoteChain)!.length).to.equal(1); + expect(responses.get(remoteChain)!.get(localChain)!.length).to.equal(1); }); }); diff --git a/typescript/sdk/src/app.ts b/typescript/sdk/src/app.ts index 0c6dabe50..eb58e24da 100644 --- a/typescript/sdk/src/app.ts +++ b/typescript/sdk/src/app.ts @@ -10,7 +10,7 @@ export class AbacusApp< constructor( builder: ContractsBuilder, contractAddresses: ChainMap, - multiProvider: MultiProvider, + readonly multiProvider: MultiProvider, ) { super( objMap( diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index feb6f6da1..b75650e32 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -52,5 +52,6 @@ export { ProxiedAddress, RemoteChainMap, Remotes, + TestChainNames, } from './types'; export { objMap, objMapEntries, promiseObjAll, utils } from './utils'; diff --git a/yarn.lock b/yarn.lock index 1c2d48ae8..8d635cc29 100644 --- a/yarn.lock +++ b/yarn.lock @@ -90,6 +90,7 @@ __metadata: resolution: "@abacus-network/hardhat@workspace:typescript/hardhat" dependencies: "@abacus-network/core": ^0.1.1 + "@abacus-network/sdk": ^0.1.1 "@abacus-network/utils": ^0.1.1 "@nomiclabs/hardhat-ethers": ^2.0.5 "@nomiclabs/hardhat-waffle": ^2.0.2