Improve ergonomics of app mixins (#330)
Co-authored-by: Yorke Rhodes <yorke@useabacus.network> Co-authored-by: Asa Oines <asa@useabacus.network>pull/340/head
parent
c2206c814c
commit
9cc2a4388b
@ -0,0 +1,15 @@ |
||||
{ |
||||
"env": { |
||||
"browser": true, |
||||
"node": true, |
||||
"es2021": true |
||||
}, |
||||
"extends": "eslint:recommended", |
||||
"parserOptions": { |
||||
"ecmaVersion": 12, |
||||
"sourceType": "module" |
||||
}, |
||||
"rules": { |
||||
"no-undef": "off" |
||||
} |
||||
} |
@ -0,0 +1,8 @@ |
||||
node_modules/ |
||||
cache/ |
||||
artifacts/ |
||||
types/ |
||||
dist/ |
||||
coverage/ |
||||
coverage.json |
||||
.env |
@ -0,0 +1,18 @@ |
||||
{ |
||||
"tabWidth": 2, |
||||
"singleQuote": true, |
||||
"trailingComma": "all", |
||||
"overrides": [ |
||||
{ |
||||
"files": "*.sol", |
||||
"options": { |
||||
"printWidth": 80, |
||||
"tabWidth": 4, |
||||
"useTabs": false, |
||||
"singleQuote": false, |
||||
"bracketSpacing": false, |
||||
"explicitTypes": "always" |
||||
} |
||||
} |
||||
] |
||||
} |
@ -0,0 +1,3 @@ |
||||
module.exports = { |
||||
skipFiles: ["test"], |
||||
}; |
@ -0,0 +1,10 @@ |
||||
{ |
||||
"extends": "solhint:recommended", |
||||
"rules": { |
||||
"compiler-version": ["error", "^0.6.11"], |
||||
"func-visibility": ["warn", {"ignoreConstructors":true}], |
||||
"not-rely-on-time": "off", |
||||
"avoid-low-level-calls": "off", |
||||
"no-inline-assembly": "off" |
||||
} |
||||
} |
@ -0,0 +1,47 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.6.11; |
||||
|
||||
import "../Router.sol"; |
||||
|
||||
contract TestRouter is Router { |
||||
function initialize(address _xAppConnectionManager) external initializer { |
||||
__Router_initialize(_xAppConnectionManager); |
||||
} |
||||
|
||||
function _handle( |
||||
uint32, |
||||
bytes32, |
||||
bytes memory |
||||
) internal pure override {} |
||||
|
||||
function isRemoteRouter(uint32 _domain, bytes32 _potentialRemoteRouter) |
||||
external |
||||
view |
||||
returns (bool) |
||||
{ |
||||
return _isRemoteRouter(_domain, _potentialRemoteRouter); |
||||
} |
||||
|
||||
function mustHaveRemoteRouter(uint32 _domain) |
||||
external |
||||
view |
||||
returns (bytes32) |
||||
{ |
||||
return _mustHaveRemoteRouter(_domain); |
||||
} |
||||
|
||||
function dispatchToRemoteRouter(uint32 _destination, bytes calldata _msg) |
||||
external |
||||
returns (uint256) |
||||
{ |
||||
return _dispatchToRemoteRouter(_destination, _msg); |
||||
} |
||||
|
||||
function dispatchToRemoteRouterWithGas( |
||||
uint32 _destination, |
||||
bytes calldata _msg, |
||||
uint256 _gasPayment |
||||
) external { |
||||
return _dispatchToRemoteRouterWithGas(_destination, _msg, _gasPayment); |
||||
} |
||||
} |
@ -0,0 +1,32 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.6.11; |
||||
import {IInterchainGasPaymaster} from "@abacus-network/core/interfaces/IInterchainGasPaymaster.sol"; |
||||
import {IOutbox} from "@abacus-network/core/interfaces/IOutbox.sol"; |
||||
|
||||
import "../XAppConnectionClient.sol"; |
||||
|
||||
contract TestXAppConnectionClient is XAppConnectionClient { |
||||
function initialize(address _xAppConnectionManager) external initializer { |
||||
__XAppConnectionClient_initialize(_xAppConnectionManager); |
||||
} |
||||
|
||||
function outbox() external view returns (IOutbox) { |
||||
return _outbox(); |
||||
} |
||||
|
||||
function interchainGasPaymaster() |
||||
external |
||||
view |
||||
returns (IInterchainGasPaymaster) |
||||
{ |
||||
return _interchainGasPaymaster(); |
||||
} |
||||
|
||||
function isInbox(address _potentialInbox) external view returns (bool) { |
||||
return _isInbox(_potentialInbox); |
||||
} |
||||
|
||||
function localDomain() external view returns (uint32) { |
||||
return _localDomain(); |
||||
} |
||||
} |
@ -0,0 +1,35 @@ |
||||
import '@abacus-network/hardhat'; |
||||
import '@nomiclabs/hardhat-waffle'; |
||||
import '@typechain/hardhat'; |
||||
import 'hardhat-gas-reporter'; |
||||
import 'solidity-coverage'; |
||||
|
||||
/** |
||||
* @type import('hardhat/config').HardhatUserConfig |
||||
*/ |
||||
module.exports = { |
||||
solidity: { |
||||
compilers: [ |
||||
{ |
||||
version: '0.8.13', |
||||
}, |
||||
{ |
||||
version: '0.7.6', |
||||
settings: { |
||||
optimizer: { |
||||
enabled: true, |
||||
runs: 999999, |
||||
}, |
||||
}, |
||||
}, |
||||
], |
||||
}, |
||||
gasReporter: { |
||||
currency: 'USD', |
||||
}, |
||||
typechain: { |
||||
outDir: './types', |
||||
target: 'ethers-v5', |
||||
alwaysGenerateOverloads: false, // should overloads with full signatures like deposit(uint256) be generated always, even if there are no overloads?
|
||||
}, |
||||
}; |
@ -0,0 +1,43 @@ |
||||
{ |
||||
"name": "@abacus-network/app", |
||||
"devDependencies": { |
||||
"@nomiclabs/hardhat-ethers": "^2.0.1", |
||||
"@nomiclabs/hardhat-waffle": "^2.0.1", |
||||
"@typechain/ethers-v5": "~7.0.0", |
||||
"@typechain/hardhat": "^2.0.1", |
||||
"@types/mocha": "^9.1.0", |
||||
"chai": "^4.3.0", |
||||
"eslint": "^7.20.0", |
||||
"ethereum-waffle": "^3.2.2", |
||||
"ethers": "^5.4.4", |
||||
"hardhat": "^2.8.3", |
||||
"hardhat-gas-reporter": "^1.0.7", |
||||
"prettier": "^2.2.1", |
||||
"prettier-plugin-solidity": "^1.0.0-beta.5", |
||||
"solhint": "^3.3.2", |
||||
"solhint-plugin-prettier": "^0.0.5", |
||||
"solidity-coverage": "^0.7.14", |
||||
"ts-node": "^10.1.0", |
||||
"typechain": "^5.0.0", |
||||
"typescript": "^4.3.5" |
||||
}, |
||||
"version": "0.0.3", |
||||
"main": "dist/index.js", |
||||
"types": "dist/index.d.ts", |
||||
"directories": { |
||||
"test": "test" |
||||
}, |
||||
"scripts": { |
||||
"prettier": "prettier --write ./contracts ./test", |
||||
"build": "hardhat compile && hardhat typechain && npm run prettier && tsc && npm run copy-types", |
||||
"copy-types": "cp types/*.d.ts dist/", |
||||
"coverage": "hardhat coverage", |
||||
"test": "hardhat test" |
||||
}, |
||||
"license": "MIT OR Apache-2.0", |
||||
"dependencies": { |
||||
"@abacus-network/core": "^0.0.6", |
||||
"@abacus-network/utils": "^0.0.8", |
||||
"@openzeppelin/contracts-upgradeable": "^4.5.0" |
||||
} |
||||
} |
@ -0,0 +1,105 @@ |
||||
import { ethers } from 'hardhat'; |
||||
import { expect } from 'chai'; |
||||
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; |
||||
import { |
||||
Outbox__factory, |
||||
XAppConnectionManager, |
||||
XAppConnectionManager__factory, |
||||
} from '@abacus-network/core'; |
||||
import { utils } from '@abacus-network/utils'; |
||||
|
||||
import { TestRouter, TestRouter__factory } from '../types'; |
||||
|
||||
const ONLY_OWNER_REVERT_MSG = 'Ownable: caller is not the owner'; |
||||
const origin = 1; |
||||
const destination = 2; |
||||
const message = '0xdeadbeef'; |
||||
|
||||
describe('Router', async () => { |
||||
let router: TestRouter, |
||||
connectionManager: XAppConnectionManager, |
||||
signer: SignerWithAddress, |
||||
nonOwner: SignerWithAddress; |
||||
|
||||
before(async () => { |
||||
[signer, nonOwner] = await ethers.getSigners(); |
||||
}); |
||||
|
||||
beforeEach(async () => { |
||||
const connectionManagerFactory = new XAppConnectionManager__factory(signer); |
||||
connectionManager = await connectionManagerFactory.deploy(); |
||||
|
||||
const routerFactory = new TestRouter__factory(signer); |
||||
router = await routerFactory.deploy(); |
||||
await router.initialize(connectionManager.address); |
||||
}); |
||||
|
||||
it('Cannot be initialized twice', async () => { |
||||
await expect( |
||||
router.initialize(ethers.constants.AddressZero), |
||||
).to.be.revertedWith('Initializable: contract is already initialized'); |
||||
}); |
||||
|
||||
it('accepts message from enrolled inbox and router', async () => { |
||||
await connectionManager.enrollInbox(origin, signer.address); |
||||
const remote = utils.addressToBytes32(nonOwner.address); |
||||
await router.enrollRemoteRouter(origin, remote); |
||||
// Does not revert.
|
||||
await router.handle(origin, remote, message); |
||||
}); |
||||
|
||||
it('rejects message from unenrolled inbox', async () => { |
||||
await expect( |
||||
router.handle(origin, utils.addressToBytes32(nonOwner.address), message), |
||||
).to.be.revertedWith('!inbox'); |
||||
}); |
||||
|
||||
it('rejects message from unenrolled router', async () => { |
||||
await connectionManager.enrollInbox(origin, signer.address); |
||||
await expect( |
||||
router.handle(origin, utils.addressToBytes32(nonOwner.address), message), |
||||
).to.be.revertedWith('!router'); |
||||
}); |
||||
|
||||
it('owner can enroll remote router', async () => { |
||||
const remote = nonOwner.address; |
||||
const remoteBytes = utils.addressToBytes32(nonOwner.address); |
||||
expect(await router.isRemoteRouter(origin, remoteBytes)).to.equal(false); |
||||
await expect(router.mustHaveRemoteRouter(origin)).to.be.revertedWith( |
||||
'!router', |
||||
); |
||||
await router.enrollRemoteRouter(origin, utils.addressToBytes32(remote)); |
||||
expect(await router.isRemoteRouter(origin, remoteBytes)).to.equal(true); |
||||
expect(await router.mustHaveRemoteRouter(origin)).to.equal(remoteBytes); |
||||
}); |
||||
|
||||
it('non-owner cannot enroll remote router', async () => { |
||||
await expect( |
||||
router |
||||
.connect(nonOwner) |
||||
.enrollRemoteRouter(origin, utils.addressToBytes32(nonOwner.address)), |
||||
).to.be.revertedWith(ONLY_OWNER_REVERT_MSG); |
||||
}); |
||||
|
||||
it('dispatches message to enrolled remote router', async () => { |
||||
const outboxFactory = new Outbox__factory(signer); |
||||
const outbox = await outboxFactory.deploy(origin); |
||||
await connectionManager.setOutbox(outbox.address); |
||||
|
||||
const remote = nonOwner.address; |
||||
await router.enrollRemoteRouter( |
||||
destination, |
||||
utils.addressToBytes32(remote), |
||||
); |
||||
await expect(router.dispatchToRemoteRouter(destination, message)).to.emit( |
||||
outbox, |
||||
'Dispatch', |
||||
); |
||||
}); |
||||
|
||||
it('reverts when dispatching message to unenrolled remote router', async () => { |
||||
await expect( |
||||
router.dispatchToRemoteRouter(destination, message), |
||||
).to.be.revertedWith('!router'); |
||||
}); |
||||
}); |
@ -0,0 +1,96 @@ |
||||
import { ethers } from 'hardhat'; |
||||
import { expect } from 'chai'; |
||||
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; |
||||
import { |
||||
Outbox__factory, |
||||
XAppConnectionManager, |
||||
XAppConnectionManager__factory, |
||||
} from '@abacus-network/core'; |
||||
|
||||
import { |
||||
TestXAppConnectionClient, |
||||
TestXAppConnectionClient__factory, |
||||
} from '../types'; |
||||
|
||||
const ONLY_OWNER_REVERT_MSG = 'Ownable: caller is not the owner'; |
||||
|
||||
describe('XAppConnectionClient', async () => { |
||||
let connectionClient: TestXAppConnectionClient, |
||||
connectionManager: XAppConnectionManager, |
||||
signer: SignerWithAddress, |
||||
nonOwner: SignerWithAddress; |
||||
|
||||
before(async () => { |
||||
[signer, nonOwner] = await ethers.getSigners(); |
||||
}); |
||||
|
||||
beforeEach(async () => { |
||||
const connectionManagerFactory = new XAppConnectionManager__factory(signer); |
||||
connectionManager = await connectionManagerFactory.deploy(); |
||||
|
||||
const connectionClientFactory = new TestXAppConnectionClient__factory( |
||||
signer, |
||||
); |
||||
connectionClient = await connectionClientFactory.deploy(); |
||||
await connectionClient.initialize(connectionManager.address); |
||||
}); |
||||
|
||||
it('Cannot be initialized twice', async () => { |
||||
await expect( |
||||
connectionClient.initialize(ethers.constants.AddressZero), |
||||
).to.be.revertedWith('Initializable: contract is already initialized'); |
||||
}); |
||||
|
||||
it('owner can set connection manager', async () => { |
||||
const newConnectionManager = signer.address; |
||||
expect(await connectionClient.xAppConnectionManager()).to.not.equal( |
||||
newConnectionManager, |
||||
); |
||||
await connectionClient.setXAppConnectionManager(newConnectionManager); |
||||
expect(await connectionClient.xAppConnectionManager()).to.equal( |
||||
newConnectionManager, |
||||
); |
||||
}); |
||||
|
||||
it('non-owner cannot set connection manager', async () => { |
||||
await expect( |
||||
connectionClient |
||||
.connect(nonOwner) |
||||
.setXAppConnectionManager(signer.address), |
||||
).to.be.revertedWith(ONLY_OWNER_REVERT_MSG); |
||||
}); |
||||
|
||||
it('returns outbox from connection manager', async () => { |
||||
const outbox = nonOwner.address; |
||||
expect(await connectionClient.outbox()).to.equal( |
||||
ethers.constants.AddressZero, |
||||
); |
||||
await connectionManager.setOutbox(outbox); |
||||
expect(await connectionClient.outbox()).to.equal(outbox); |
||||
}); |
||||
|
||||
it('returns paymaster from connection manager', async () => { |
||||
const paymaster = nonOwner.address; |
||||
expect(await connectionClient.interchainGasPaymaster()).to.equal( |
||||
ethers.constants.AddressZero, |
||||
); |
||||
await connectionManager.setInterchainGasPaymaster(paymaster); |
||||
expect(await connectionClient.interchainGasPaymaster()).to.equal(paymaster); |
||||
}); |
||||
|
||||
it('returns inbox from connection manager', async () => { |
||||
const inbox = nonOwner.address; |
||||
const domain = 1; |
||||
expect(await connectionClient.isInbox(inbox)).to.equal(false); |
||||
await connectionManager.enrollInbox(domain, inbox); |
||||
expect(await connectionClient.isInbox(inbox)).to.equal(true); |
||||
}); |
||||
|
||||
it('returns local domain from outbox', async () => { |
||||
const localDomain = 3; |
||||
const outboxFactory = new Outbox__factory(signer); |
||||
const outbox = await outboxFactory.deploy(localDomain); |
||||
await connectionManager.setOutbox(outbox.address); |
||||
expect(await connectionClient.localDomain()).to.equal(localDomain); |
||||
}); |
||||
}); |
@ -0,0 +1,16 @@ |
||||
{ |
||||
"compilerOptions": { |
||||
"outDir": "./dist/", |
||||
"rootDir": "./types/" |
||||
}, |
||||
"exclude": [ |
||||
"./node_modules/", |
||||
"./dist/", |
||||
"./types/hardhat.d.ts" |
||||
], |
||||
"extends": "../../tsconfig.package.json", |
||||
"include": [ |
||||
"./types/*.ts", |
||||
"./types/factories/*.ts" |
||||
] |
||||
} |
@ -0,0 +1,12 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.6.11; |
||||
|
||||
import {IInterchainGasPaymaster} from "./IInterchainGasPaymaster.sol"; |
||||
import {IOutbox} from "./IOutbox.sol"; |
||||
|
||||
interface IXAppConnectionManager { |
||||
function outbox() external view returns (IOutbox); |
||||
function interchainGasPaymaster() external view returns (IInterchainGasPaymaster); |
||||
function isInbox(address _inbox) external view returns (bool); |
||||
function localDomain() external view returns (uint32); |
||||
} |
@ -1 +1,2 @@ |
||||
dist/ |
||||
cache/ |
||||
|
@ -0,0 +1,29 @@ |
||||
import "@typechain/hardhat"; |
||||
import "@nomiclabs/hardhat-ethers"; |
||||
import "@nomiclabs/hardhat-waffle"; |
||||
|
||||
/** |
||||
* @type import('hardhat/config').HardhatUserConfig |
||||
*/ |
||||
module.exports = { |
||||
solidity: { |
||||
version: "0.7.6", |
||||
settings: { |
||||
optimizer: { |
||||
enabled: true, |
||||
runs: 999999, |
||||
}, |
||||
}, |
||||
}, |
||||
gasReporter: { |
||||
currency: "USD", |
||||
}, |
||||
typechain: { |
||||
outDir: "./types", |
||||
target: "ethers-v5", |
||||
alwaysGenerateOverloads: false, // should overloads with full signatures like deposit(uint256) be generated always, even if there are no overloads?
|
||||
}, |
||||
mocha: { |
||||
bail: true, |
||||
}, |
||||
}; |
@ -0,0 +1,72 @@ |
||||
import { ethers } from "hardhat"; |
||||
import { expect } from "chai"; |
||||
|
||||
import { TestRecipient__factory } from "@abacus-network/core"; |
||||
import { utils } from "@abacus-network/utils"; |
||||
|
||||
import { TestAbacusDeploy } from ".."; |
||||
|
||||
const localDomain = 1000; |
||||
const remoteDomain = 2000; |
||||
const domains = [localDomain, remoteDomain]; |
||||
const message = "0xdeadbeef"; |
||||
|
||||
describe("TestAbacusDeploy", async () => { |
||||
let abacus: TestAbacusDeploy; |
||||
|
||||
beforeEach(async () => { |
||||
abacus = new TestAbacusDeploy({ signer: {} }); |
||||
const [signer] = await ethers.getSigners(); |
||||
await abacus.deploy(domains, signer); |
||||
|
||||
const recipient = await new TestRecipient__factory(signer).deploy(); |
||||
const localOutbox = abacus.outbox(localDomain); |
||||
await expect( |
||||
localOutbox.dispatch( |
||||
remoteDomain, |
||||
utils.addressToBytes32(recipient.address), |
||||
message |
||||
) |
||||
).to.emit(localOutbox, "Dispatch"); |
||||
const remoteOutbox = abacus.outbox(remoteDomain); |
||||
await expect( |
||||
remoteOutbox.dispatch( |
||||
localDomain, |
||||
utils.addressToBytes32(recipient.address), |
||||
message |
||||
) |
||||
).to.emit(remoteOutbox, "Dispatch"); |
||||
}); |
||||
|
||||
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 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); |
||||
}); |
||||
}); |
Loading…
Reference in new issue