Support env subset deployments (#767)

- Support env subset deployements in AbacusCore and TestCore
- Harden MultiProvider against missing chain values
- Add `tryGet*` methods to MultiProvider and other convenience methods
- Add `getMultiProviderFromConfigAndProvider` and `getChainToOwnerMap` utils to SDK
- Make `enrollRemoteRouters` static in `AbacusRouterDeployer` 
- Create minimal envSubsetDeployer app in SDK to test subset chain deployments
- Reduce boilerplate needed for HelloWorld app
- Restructure HelloWorld check and deploy scripts to be more realistic
- Add `pick` util function for objects
- BREAKING CHANGE: Make order of generic types consistent across core classes
- BREAKING CHANGE: Replace `getMultiProviderFromSignerAndConfig` with `getTestMultiProvider`
pull/859/head
J M Rossy 2 years ago committed by GitHub
parent d24eaa4e32
commit 44df310300
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      typescript/helloworld/src/app/environments/test.json
  2. 4
      typescript/helloworld/src/deploy/check.ts
  3. 32
      typescript/helloworld/src/deploy/config.ts
  4. 4
      typescript/helloworld/src/deploy/deploy.ts
  5. 35
      typescript/helloworld/src/scripts/check.ts
  6. 28
      typescript/helloworld/src/scripts/deploy.ts
  7. 10
      typescript/helloworld/src/test/deploy.test.ts
  8. 10
      typescript/helloworld/src/test/helloworld.test.ts
  9. 4
      typescript/infra/config/environments/test/index.ts
  10. 6
      typescript/infra/hardhat.config.ts
  11. 7
      typescript/infra/test/core.test.ts
  12. 1
      typescript/sdk/package.json
  13. 6
      typescript/sdk/src/consts/chainConnectionConfigs.ts
  14. 34
      typescript/sdk/src/core/AbacusCore.ts
  15. 13
      typescript/sdk/src/core/TestCoreApp.ts
  16. 31
      typescript/sdk/src/core/TestCoreDeployer.ts
  17. 9
      typescript/sdk/src/core/testAbacusDeploy.hardhat-test.ts
  18. 2
      typescript/sdk/src/deploy/AbacusDeployer.ts
  19. 4
      typescript/sdk/src/deploy/core/AbacusCoreDeployer.ts
  20. 2
      typescript/sdk/src/deploy/router/AbacusRouterChecker.ts
  21. 6
      typescript/sdk/src/deploy/router/AbacusRouterDeployer.ts
  22. 39
      typescript/sdk/src/deploy/utils.ts
  23. 7
      typescript/sdk/src/index.ts
  24. 115
      typescript/sdk/src/providers/MultiProvider.ts
  25. 1
      typescript/sdk/src/test/.eslintrc
  26. 8
      typescript/sdk/src/test/envSubsetDeployer/README.md
  27. 97
      typescript/sdk/src/test/envSubsetDeployer/app.ts
  28. 55
      typescript/sdk/src/test/envSubsetDeployer/check-single-chain.ts
  29. 36
      typescript/sdk/src/test/envSubsetDeployer/deploy-single-chain.ts
  30. 96
      typescript/sdk/src/test/envSubsetDeployer/deploy.hardhat-test.ts
  31. 23
      typescript/sdk/src/test/envSubsetDeployer/utils.ts
  32. 6
      typescript/sdk/src/types.ts
  33. 65
      typescript/sdk/src/utils/MultiGeneric.ts
  34. 17
      typescript/sdk/src/utils/objects.ts
  35. 1
      yarn.lock

@ -1,11 +0,0 @@
{
"test1": {
"router": "0x5FbDB2315678afecb367f032d93F642f64180aa3"
},
"test2": {
"router": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0"
},
"test3": {
"router": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9"
}
}

@ -9,7 +9,7 @@ export class HelloWorldChecker<
Chain extends ChainName,
> extends AbacusRouterChecker<
Chain,
HelloWorldContracts,
HelloWorldApp<Chain>,
HelloWorldConfig
HelloWorldConfig,
HelloWorldContracts
> {}

@ -1,32 +1,8 @@
import {
ChainMap,
RouterConfig,
TestChainNames,
chainConnectionConfigs,
} from '@abacus-network/sdk';
import { RouterConfig, chainConnectionConfigs } from '@abacus-network/sdk';
export type HelloWorldConfig = RouterConfig;
// TODO reduce this config boilerplate
export const testConfigs = {
test1: chainConnectionConfigs.test1,
test2: chainConnectionConfigs.test2,
test3: chainConnectionConfigs.test3,
// SET DESIRED NETWORKS HERE
export const prodConfigs = {
alfajores: chainConnectionConfigs.alfajores,
};
export function getConfigMap(
signerAddress: string,
): ChainMap<TestChainNames, { owner: string }> {
return {
test1: {
owner: signerAddress,
},
test2: {
owner: signerAddress,
},
test3: {
owner: signerAddress,
},
};
}

@ -18,9 +18,9 @@ export class HelloWorldDeployer<
Chain extends ChainName,
> extends AbacusRouterDeployer<
Chain,
HelloWorldConfig,
HelloWorldContracts,
HelloWorldFactories,
HelloWorldConfig
HelloWorldFactories
> {
constructor(
multiProvider: MultiProvider<Chain>,

@ -1,38 +1,45 @@
import { ethers } from 'hardhat';
import {
AbacusCore,
ChainMap,
ChainName,
MultiProvider,
buildContracts,
getMultiProviderFromConfigAndSigner,
getChainToOwnerMap,
objMap,
} from '@abacus-network/sdk';
import { HelloWorldApp } from '../app/app';
import { HelloWorldContracts, helloWorldFactories } from '../app/contracts';
import testEnvironmentAddresses from '../app/environments/test.json';
import { HelloWorldChecker } from '../deploy/check';
import { getConfigMap, testConfigs } from '../deploy/config';
import { prodConfigs } from '../deploy/config';
// COPY FROM OUTPUT OF DEPLOYMENT SCRIPT OR IMPORT FROM ELSEWHERE
const deploymentAddresses = {};
// SET CONTRACT OWNER ADDRESS HERE
const ownerAddress = '0x123...';
async function check() {
const [signer] = await ethers.getSigners();
const multiProvider = getMultiProviderFromConfigAndSigner(
testConfigs,
signer,
);
console.info('Preparing utilities');
const chainProviders = objMap(prodConfigs, (_, config) => ({
provider: config.provider,
confirmations: config.confirmations,
overrides: config.overrides,
}));
const multiProvider = new MultiProvider(chainProviders);
const contractsMap = buildContracts(
testEnvironmentAddresses,
deploymentAddresses,
helloWorldFactories,
) as ChainMap<ChainName, HelloWorldContracts>;
const app = new HelloWorldApp(contractsMap, multiProvider);
const core = AbacusCore.fromEnvironment('test', multiProvider);
const core = AbacusCore.fromEnvironment('testnet2', multiProvider);
const config = core.extendWithConnectionClientConfig(
getConfigMap(signer.address),
getChainToOwnerMap(prodConfigs, ownerAddress),
);
console.info('Starting check');
const helloWorldChecker = new HelloWorldChecker(multiProvider, app, config);
await helloWorldChecker.check();
helloWorldChecker.expectEmpty();

@ -1,25 +1,31 @@
import '@nomiclabs/hardhat-ethers';
import { ethers } from 'hardhat';
import { Wallet } from 'ethers';
import {
AbacusCore,
getMultiProviderFromConfigAndSigner,
MultiProvider,
getChainToOwnerMap,
objMap,
serializeContracts,
} from '@abacus-network/sdk';
import { getConfigMap, testConfigs } from '../deploy/config';
import { prodConfigs } from '../deploy/config';
import { HelloWorldDeployer } from '../deploy/deploy';
async function main() {
const [signer] = await ethers.getSigners();
const multiProvider = getMultiProviderFromConfigAndSigner(
testConfigs,
signer,
);
console.info('Getting signer');
const signer = new Wallet('SET KEY HERE OR CREATE YOUR OWN SIGNER');
console.info('Preparing utilities');
const chainProviders = objMap(prodConfigs, (_, config) => ({
provider: config.provider,
confirmations: config.confirmations,
overrides: config.overrides,
}));
const multiProvider = new MultiProvider(chainProviders);
const core = AbacusCore.fromEnvironment('test', multiProvider);
const core = AbacusCore.fromEnvironment('testnet2', multiProvider);
const config = core.extendWithConnectionClientConfig(
getConfigMap(signer.address),
getChainToOwnerMap(prodConfigs, signer.address),
);
const deployer = new HelloWorldDeployer(multiProvider, config, core);

@ -7,13 +7,15 @@ import {
TestChainNames,
TestCoreApp,
TestCoreDeployer,
getMultiProviderFromConfigAndSigner,
getChainToOwnerMap,
getTestMultiProvider,
testChainConnectionConfigs,
} from '@abacus-network/sdk';
import { HelloWorldApp } from '../app/app';
import { HelloWorldContracts } from '../app/contracts';
import { HelloWorldChecker } from '../deploy/check';
import { HelloWorldConfig, getConfigMap, testConfigs } from '../deploy/config';
import { HelloWorldConfig } from '../deploy/config';
import { HelloWorldDeployer } from '../deploy/deploy';
describe('deploy', async () => {
@ -26,13 +28,13 @@ describe('deploy', async () => {
before(async () => {
const [signer] = await ethers.getSigners();
multiProvider = getMultiProviderFromConfigAndSigner(testConfigs, signer);
multiProvider = getTestMultiProvider(signer);
const coreDeployer = new TestCoreDeployer(multiProvider);
const coreContractsMaps = await coreDeployer.deploy();
core = new TestCoreApp(coreContractsMaps, multiProvider);
config = core.extendWithConnectionClientConfig(
getConfigMap(signer.address),
getChainToOwnerMap(testChainConnectionConfigs, signer.address),
);
deployer = new HelloWorldDeployer(multiProvider, config, core);
});

@ -9,10 +9,12 @@ import {
TestChainNames,
TestCoreApp,
TestCoreDeployer,
getMultiProviderFromConfigAndSigner,
getChainToOwnerMap,
getTestMultiProvider,
testChainConnectionConfigs,
} from '@abacus-network/sdk';
import { HelloWorldConfig, getConfigMap, testConfigs } from '../deploy/config';
import { HelloWorldConfig } from '../deploy/config';
import { HelloWorldDeployer } from '../deploy/deploy';
import { HelloWorld } from '../types';
@ -32,13 +34,13 @@ describe('HelloWorld', async () => {
before(async () => {
[signer] = await ethers.getSigners();
multiProvider = getMultiProviderFromConfigAndSigner(testConfigs, signer);
multiProvider = getTestMultiProvider(signer);
const coreDeployer = new TestCoreDeployer(multiProvider);
const coreContractsMaps = await coreDeployer.deploy();
coreApp = new TestCoreApp(coreContractsMaps, multiProvider);
config = coreApp.extendWithConnectionClientConfig(
getConfigMap(signer.address),
getChainToOwnerMap(testChainConnectionConfigs, signer.address),
);
});

@ -1,6 +1,6 @@
import { JsonRpcProvider } from '@ethersproject/providers';
import { getMultiProviderFromConfigAndSigner } from '@abacus-network/sdk';
import { getTestMultiProvider } from '@abacus-network/sdk';
import { CoreEnvironmentConfig } from '../../../src/config';
@ -19,6 +19,6 @@ export const environment: CoreEnvironmentConfig<TestChains> = {
getMultiProvider: async () => {
const provider = testConfigs.test1.provider! as JsonRpcProvider;
const signer = provider.getSigner(0);
return getMultiProviderFromConfigAndSigner(testConfigs, signer);
return getTestMultiProvider(signer, testConfigs);
},
};

@ -8,7 +8,7 @@ import {
AbacusCore,
ChainName,
ChainNameToDomainId,
getMultiProviderFromConfigAndSigner,
getTestMultiProvider,
} from '@abacus-network/sdk';
import { getCoreEnvironmentConfig } from './scripts/utils';
@ -63,9 +63,9 @@ task('kathy', 'Dispatches random abacus messages')
const interchainGasPayment = hre.ethers.utils.parseUnits('100', 'gwei');
const config = getCoreEnvironmentConfig(environment);
const [signer] = await hre.ethers.getSigners();
const multiProvider = getMultiProviderFromConfigAndSigner(
config.transactionConfigs,
const multiProvider = getTestMultiProvider(
signer,
config.transactionConfigs,
);
const core = AbacusCore.fromEnvironment(environment, multiProvider);

@ -12,7 +12,7 @@ import {
CoreConfig,
CoreContractsMap,
MultiProvider,
getMultiProviderFromConfigAndSigner,
getTestMultiProvider,
objMap,
serializeContracts,
} from '@abacus-network/sdk';
@ -35,10 +35,7 @@ describe('core', async () => {
beforeEach(async () => {
const [signer, owner] = await ethers.getSigners();
// This is kind of awkward and really these tests shouldn't live here
multiProvider = getMultiProviderFromConfigAndSigner(
testConfig.transactionConfigs,
signer,
);
multiProvider = getTestMultiProvider(signer, testConfig.transactionConfigs);
coreConfig = testConfig.core;
deployer = new AbacusCoreInfraDeployer(multiProvider, coreConfig);
owners = objMap(testConfig.transactionConfigs, () => owner.address);

@ -26,6 +26,7 @@
"mocha": "^9.2.2",
"prettier": "^2.4.1",
"sinon": "^13.0.2",
"ts-node": "^10.8.0",
"typescript": "^4.7.2"
},
"files": [

@ -189,3 +189,9 @@ export const chainConnectionConfigs: ChainMap<ChainName, IChainConnection> = {
test2,
test3,
};
export const testChainConnectionConfigs = {
test1,
test2,
test3,
};

@ -15,7 +15,7 @@ import { ChainConnection } from '../providers/ChainConnection';
import { MultiProvider } from '../providers/MultiProvider';
import { ConnectionClientConfig } from '../router';
import { ChainMap, ChainName, Remotes } from '../types';
import { objMap } from '../utils';
import { objMap, pick } from '../utils';
import { CoreContracts, coreFactories } from './contracts';
@ -46,15 +46,33 @@ export class AbacusCore<Chain extends ChainName = ChainName> extends AbacusApp<
super(contractsMap, multiProvider);
}
static fromEnvironment<Env extends CoreEnvironment>(
env: Env,
multiProvider: MultiProvider<CoreEnvironmentChain<Env>>,
): AbacusCore<CoreEnvironmentChain<Env>> {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
static fromEnvironment<
Env extends CoreEnvironment,
Chain extends ChainName = ChainName,
>(env: Env, multiProvider: MultiProvider<Chain>) {
const envConfig = environments[env];
if (!envConfig) {
throw new Error(`No default env config found for ${env}`);
}
type EnvChain = keyof typeof envConfig;
type IntersectionChain = EnvChain & Chain;
const envChains = Object.keys(envConfig) as IntersectionChain[];
const { intersection, multiProvider: intersectionProvider } =
multiProvider.intersect<IntersectionChain>(envChains);
const intersectionConfig = pick(
envConfig as ChainMap<Chain, any>,
intersection,
);
const contractsMap = buildContracts(
environments[env],
intersectionConfig,
coreFactories,
) as CoreContractsMap<CoreEnvironmentChain<Env>>;
return new AbacusCore(contractsMap, multiProvider);
) as CoreContractsMap<IntersectionChain>;
return new AbacusCore(contractsMap, intersectionProvider);
}
// override type to be derived from chain key

@ -32,18 +32,17 @@ export type TestCoreContracts<Local extends TestChainNames> = CoreContracts<
inboxes: ChainMap<Remotes<TestChainNames, Local>, TestInboxContracts>;
};
export class TestCoreApp extends AbacusCore<TestChainNames> {
getContracts<Local extends TestChainNames>(
export class TestCoreApp<
TestChain extends TestChainNames = TestChainNames,
> extends AbacusCore<TestChain> {
getContracts<Local extends TestChain>(
chain: Local,
): TestCoreContracts<Local> {
return super.getContracts(chain) as TestCoreContracts<Local>;
}
async processMessages(): Promise<
Map<
TestChainNames,
Map<TestChainNames, ethers.providers.TransactionResponse[]>
>
Map<TestChain, Map<TestChain, ethers.providers.TransactionResponse[]>>
> {
const responses = new Map();
for (const origin of this.chains()) {
@ -57,7 +56,7 @@ export class TestCoreApp extends AbacusCore<TestChainNames> {
return responses;
}
async processOutboundMessages<Local extends TestChainNames>(
async processOutboundMessages<Local extends TestChain>(
origin: Local,
): Promise<Map<ChainName, any>> {
const responses = new Map<ChainName, any>();

@ -7,7 +7,7 @@ import { AbacusCoreDeployer } from '../deploy/core/AbacusCoreDeployer';
import { CoreConfig, ValidatorManagerConfig } from '../deploy/core/types';
import { MultiProvider } from '../providers/MultiProvider';
import { ProxiedContract } from '../proxy';
import { Remotes, TestChainNames } from '../types';
import { ChainMap, Remotes, TestChainNames } from '../types';
import {
TestCoreApp,
@ -38,21 +38,26 @@ function mockProxy(contract: ethers.Contract) {
});
}
export class TestCoreDeployer extends AbacusCoreDeployer<TestChainNames> {
constructor(public readonly multiProvider: MultiProvider<TestChainNames>) {
super(
multiProvider,
{
export class TestCoreDeployer<
TestChain extends TestChainNames = TestChainNames,
> extends AbacusCoreDeployer<TestChain> {
constructor(
public readonly multiProvider: MultiProvider<TestChain>,
configMap?: ChainMap<TestChain, CoreConfig>,
) {
const configs =
configMap ??
({
test1: testValidatorManagerConfig,
test2: testValidatorManagerConfig,
test3: testValidatorManagerConfig,
},
testCoreFactories,
);
} as ChainMap<TestChain, CoreConfig>); // cast so param can be optional
super(multiProvider, configs, testCoreFactories);
}
// skip proxying
async deployOutbox<LocalChain extends TestChainNames>(
async deployOutbox<LocalChain extends TestChain>(
chain: LocalChain,
config: ValidatorManagerConfig,
): Promise<TestOutboxContracts> {
@ -74,9 +79,9 @@ export class TestCoreDeployer extends AbacusCoreDeployer<TestChainNames> {
}
// skip proxying
async deployInbox<LocalChain extends TestChainNames>(
async deployInbox<LocalChain extends TestChain>(
local: LocalChain,
remote: Remotes<TestChainNames, LocalChain>,
remote: Remotes<TestChain, LocalChain>,
config: ValidatorManagerConfig,
): Promise<TestInboxContracts> {
const localDomain = chainMetadata[local].id;
@ -96,7 +101,7 @@ export class TestCoreDeployer extends AbacusCoreDeployer<TestChainNames> {
} as TestInboxContracts;
}
async deployApp(): Promise<TestCoreApp> {
async deployApp(): Promise<TestCoreApp<TestChain>> {
return new TestCoreApp(await this.deploy(), this.multiProvider);
}
}

@ -8,7 +8,7 @@ import { TestOutbox, TestRecipient__factory } from '@abacus-network/core';
import { utils } from '@abacus-network/utils';
import { chainMetadata } from '../consts/chainMetadata';
import { getMultiProviderFromConfigAndSigner } from '../deploy/utils';
import { getTestMultiProvider } from '../deploy/utils';
import { TestCoreApp } from './TestCoreApp';
import { TestCoreDeployer } from './TestCoreDeployer';
@ -28,12 +28,7 @@ describe('TestCoreDeployer', async () => {
beforeEach(async () => {
const [signer] = await ethers.getSigners();
const config = {
test1: { provider: ethers.provider },
test2: { provider: ethers.provider },
test3: { provider: ethers.provider },
};
const multiProvider = getMultiProviderFromConfigAndSigner(config, signer);
const multiProvider = getTestMultiProvider(signer);
const deployer = new TestCoreDeployer(multiProvider);
abacus = await deployer.deployApp();

@ -28,8 +28,8 @@ export interface DeployerOptions {
export abstract class AbacusDeployer<
Chain extends ChainName,
Config,
Factories extends AbacusFactories,
Contracts extends AbacusContracts,
Factories extends AbacusFactories,
> {
public deployedContracts: Partial<Record<Chain, Contracts>> = {};

@ -24,8 +24,8 @@ import { CoreConfig, ValidatorManagerConfig } from './types';
export class AbacusCoreDeployer<Chain extends ChainName> extends AbacusDeployer<
Chain,
CoreConfig,
typeof coreFactories,
CoreContracts<Chain, Chain>
CoreContracts<Chain, Chain>,
typeof coreFactories
> {
startingBlockNumbers: ChainMap<Chain, number | undefined>;

@ -11,9 +11,9 @@ import { RouterConfig } from './types';
export class AbacusRouterChecker<
Chain extends ChainName,
Contracts extends RouterContracts,
App extends AbacusApp<Contracts, Chain>,
Config extends RouterConfig,
Contracts extends RouterContracts,
> extends AbacusAppChecker<Chain, App, Config> {
checkOwnership(chain: Chain): Promise<void> {
const owner = this.configMap[chain].owner;

@ -14,10 +14,10 @@ import { RouterConfig } from './types';
export abstract class AbacusRouterDeployer<
Chain extends ChainName,
Config extends RouterConfig,
Contracts extends RouterContracts,
Factories extends RouterFactories,
Config extends RouterConfig,
> extends AbacusDeployer<Chain, Config, Factories, Contracts> {
> extends AbacusDeployer<Chain, Config, Contracts, Factories> {
constructor(
multiProvider: MultiProvider<Chain>,
configMap: ChainMap<Chain, Config>,
@ -70,7 +70,7 @@ export abstract class AbacusRouterDeployer<
contractsMap: ChainMap<Chain, Contracts>,
): Promise<void> {
this.logger(`Enrolling deployed routers with each other...`);
// Make all routers aware of eachother.
// Make all routers aware of each other.
await promiseObjAll(
objMap(contractsMap, async (local, contracts) => {
const chainConnection = this.multiProvider.getChainConnection(local);

@ -1,20 +1,45 @@
import { ethers } from 'ethers';
import { Signer, providers } from 'ethers';
import { types } from '@abacus-network/utils';
import { testChainConnectionConfigs } from '../consts/chainConnectionConfigs';
import { MultiProvider } from '../providers/MultiProvider';
import { ChainName } from '../types';
import { ChainMap, ChainName, TestChainNames } from '../types';
import { objMap } from '../utils';
import { EnvironmentConfig } from './types';
export function getMultiProviderFromConfigAndSigner<Chain extends ChainName>(
environmentConfig: EnvironmentConfig<Chain>,
signer: ethers.Signer,
export function getTestMultiProvider<Chain extends TestChainNames>(
signerOrProvider: Signer | providers.Provider,
configs: EnvironmentConfig<Chain> = testChainConnectionConfigs,
): MultiProvider<Chain> {
const chainProviders = objMap(environmentConfig, (_, config) => ({
provider: signer.provider!,
let signer: Signer | undefined;
let provider: providers.Provider;
if (Signer.isSigner(signerOrProvider) && signerOrProvider.provider) {
signer = signerOrProvider;
provider = signerOrProvider.provider;
} else if (providers.Provider.isProvider(signerOrProvider)) {
provider = signerOrProvider;
} else {
throw new Error('signerOrProvider is invalid');
}
const chainProviders = objMap(configs, (_, config) => ({
signer,
provider,
confirmations: config.confirmations,
overrides: config.overrides,
}));
return new MultiProvider(chainProviders);
}
export function getChainToOwnerMap<Chain extends ChainName>(
configMap: ChainMap<Chain, any>,
owner: types.Address,
): ChainMap<Chain, { owner: string }> {
return objMap(configMap, () => {
return {
owner,
};
});
}

@ -1,6 +1,9 @@
export { AllChains, Chains } from './consts/chains';
export { chainMetadata } from './consts/chainMetadata';
export { chainConnectionConfigs } from './consts/chainConnectionConfigs';
export {
chainConnectionConfigs,
testChainConnectionConfigs,
} from './consts/chainConnectionConfigs';
export { environments as coreEnvironments } from './consts/environments';
export {
@ -99,7 +102,7 @@ export { UpgradeBeaconViolation } from './deploy/proxy';
export { AbacusRouterDeployer } from './deploy/router/AbacusRouterDeployer';
export { AbacusRouterChecker } from './deploy/router/AbacusRouterChecker';
export { RouterConfig } from './deploy/router/types';
export { getMultiProviderFromConfigAndSigner } from './deploy/utils';
export { getTestMultiProvider, getChainToOwnerMap } from './deploy/utils';
export { ContractVerifier } from './deploy/verify/ContractVerifier';
export {
ContractVerificationInput,

@ -1,5 +1,7 @@
import { ChainMap, ChainName, IChainConnection } from '../types';
import { MultiGeneric, objMap } from '../utils';
import { ethers } from 'ethers';
import { ChainMap, ChainName, IChainConnection, Remotes } from '../types';
import { MultiGeneric, objMap, pick } from '../utils';
import { ChainConnection } from './ChainConnection';
@ -14,9 +16,116 @@ export class MultiProvider<
),
);
}
getChainConnection(chain: Chain): ChainMap<Chain, ChainConnection>[Chain] {
/**
* Get chainConnection for a chain
* @throws if chain is invalid or has not been set
*/
getChainConnection(chain: Chain): ChainConnection {
return this.get(chain);
}
/**
* Get chainConnection for a chain
* @returns value or null if chain value has not been set
*/
tryGetChainConnection(chain: Chain): ChainConnection | null {
return this.tryGet(chain);
}
/**
* Set value for a chain
* @throws if chain is invalid or has not been set
*/
setChainConnection(
chain: Chain,
chainConnectionConfig: IChainConnection,
): ChainConnection {
const connection = new ChainConnection(chainConnectionConfig);
return this.set(chain, connection);
}
/**
* Get provider for a chain
* @throws if chain is invalid or has not been set
*/
getChainProvider(chain: Chain): ethers.providers.Provider {
const chainConnection = this.get(chain);
if (!chainConnection.provider) {
throw new Error(`No provider set for chain ${chain}`);
}
return chainConnection.provider;
}
/**
* Get provider for a chain
* @returns value or null if chain value has not been set
*/
tryGetChainProvider(chain: Chain): ethers.providers.Provider | null {
return this.tryGet(chain)?.provider ?? null;
}
/**
* Get signer for a chain
* @throws if chain is invalid or has not been set
*/
getChainSigner(chain: Chain): ethers.Signer {
const chainConnection = this.get(chain);
if (!chainConnection.signer) {
throw new Error(`No signer set for chain ${chain}`);
}
return chainConnection.signer;
}
/**
* Get signer for a chain
* @returns value or null if chain value has not been set
*/
tryGetChainSigner(chain: Chain): ethers.Signer | null {
return this.tryGet(chain)?.signer ?? null;
}
/**
* Create a new MultiProvider which includes the provided chain connection config
*/
extendWithChain<New extends Remotes<ChainName, Chain>>(
chain: New,
chainConnectionConfig: IChainConnection,
): MultiProvider<New & Chain> {
const connection = new ChainConnection(chainConnectionConfig);
return new MultiProvider<New & Chain>({
...this.chainMap,
[chain]: connection,
});
}
/**
* Create a new MultiProvider from the intersection
* of current's chains and the provided chain list
*/
intersect<IntersectionChain extends Chain>(
chains: ChainName[],
): {
intersection: IntersectionChain[];
multiProvider: MultiProvider<IntersectionChain>;
} {
const ownChains = this.chains();
const intersection = ownChains.filter((c) =>
chains.includes(c),
) as IntersectionChain[];
if (!intersection.length) {
throw new Error(`No chains shared between MultiProvider and list`);
}
const intersectionChainMap = pick(this.chainMap, intersection);
const multiProvider = new MultiProvider<IntersectionChain>({
...intersectionChainMap,
});
return { intersection, multiProvider };
}
// This doesn't work on hardhat providers so we skip for now
// ready() {
// return Promise.all(this.values().map((dc) => dc.provider!.ready));

@ -1,5 +1,6 @@
{
"rules": {
"no-console": ["off"],
"@typescript-eslint/explicit-module-boundary-types": ["off"]
}
}

@ -0,0 +1,8 @@
# Environment Subset Deployer App
A trivial app intended to test deployments to a subset of an environment's chains.
For example, test deploying to just alfajores from env `testnet2`.
To test deployment to a local hardhat network, run `yarn test:hardhat`
To test actual deployments to Alfajores run `MNEMONIC="your_mnemonic" yarn ts-node src/test/envSubsetDeployer/deploy-single-chain.ts`
To check run `yarn ts-node src/test/envSubsetDeployer/check-single-chain.ts`

@ -0,0 +1,97 @@
import { TestRouter__factory } from '@abacus-network/app';
import { AbacusApp } from '../../AbacusApp';
import { chainConnectionConfigs } from '../../consts/chainConnectionConfigs';
import { AbacusCore } from '../../core/AbacusCore';
import { AbacusDeployer } from '../../deploy/AbacusDeployer';
import { AbacusRouterChecker } from '../../deploy/router/AbacusRouterChecker';
import { AbacusRouterDeployer } from '../../deploy/router/AbacusRouterDeployer';
import { RouterConfig } from '../../deploy/router/types';
import { MultiProvider } from '../../providers/MultiProvider';
import { RouterContracts, RouterFactories } from '../../router';
import { ChainMap, ChainName } from '../../types';
import { objMap, promiseObjAll } from '../../utils';
export const fullEnvTestConfigs = {
test1: chainConnectionConfigs.test1,
test2: chainConnectionConfigs.test2,
test3: chainConnectionConfigs.test3,
};
export const subsetTestConfigs = {
test1: chainConnectionConfigs.test1,
test2: chainConnectionConfigs.test2,
};
export type SubsetChains = keyof typeof subsetTestConfigs;
export const alfajoresChainConfig = {
alfajores: chainConnectionConfigs.alfajores,
};
export class EnvSubsetApp<
Chain extends ChainName = ChainName,
> extends AbacusApp<RouterContracts, Chain> {}
export class EnvSubsetChecker<
Chain extends ChainName,
> extends AbacusRouterChecker<
Chain,
EnvSubsetApp<Chain>,
RouterConfig,
RouterContracts
> {}
export const envSubsetFactories: RouterFactories = {
router: new TestRouter__factory(),
};
export class EnvSubsetDeployer<
Chain extends ChainName,
> extends AbacusRouterDeployer<
Chain,
RouterConfig,
RouterContracts,
RouterFactories
> {
constructor(
multiProvider: MultiProvider<Chain>,
configMap: ChainMap<Chain, RouterConfig>,
protected core: AbacusCore<Chain>,
) {
super(multiProvider, configMap, envSubsetFactories, {});
}
// Consider moving this up to AbacusRouterDeployer
async initRouter(
contractsMap: ChainMap<Chain, RouterContracts>,
): Promise<void> {
this.logger(`Calling initialize on routers...`);
await promiseObjAll(
objMap(contractsMap, async (chain, contracts) => {
const chainConnection = this.multiProvider.getChainConnection(chain);
const acm = this.configMap[chain].abacusConnectionManager;
await chainConnection.handleTx(
// @ts-ignore
contracts.router.initialize(acm, chainConnection.overrides),
);
}),
);
}
async deploy(): Promise<ChainMap<Chain, RouterContracts>> {
const contractsMap = (await AbacusDeployer.prototype.deploy.apply(
this,
)) as Record<Chain, RouterContracts>;
await this.initRouter(contractsMap);
await this.enrollRemoteRouters(contractsMap);
return contractsMap;
}
async deployContracts(chain: Chain) {
const router = await this.deployContract(chain, 'router', []);
return {
router,
};
}
}

@ -0,0 +1,55 @@
import { buildContracts } from '../../contracts';
import { AbacusCore } from '../../core/AbacusCore';
import { getChainToOwnerMap } from '../../deploy/utils';
import { MultiProvider } from '../../providers/MultiProvider';
import { RouterContracts } from '../../router';
import { ChainMap, ChainName } from '../../types';
import {
EnvSubsetApp,
EnvSubsetChecker,
alfajoresChainConfig,
envSubsetFactories,
} from './app';
import { getAlfajoresProvider } from './utils';
// Copied from output of deploy-single-chain.ts script
const deploymentAddresses = {
alfajores: {
router: '0x0666AD4F636210B6a418f97790b7BAABAC54b9A4',
},
};
const ownerAddress = '0x35b74Ed5038bf0488Ff33bD9819b9D12D10A7560';
async function check() {
const provider = getAlfajoresProvider();
console.info('Preparing utilities');
const multiProvider = new MultiProvider({
alfajores: {
provider,
confirmations: alfajoresChainConfig.alfajores.confirmations,
overrides: alfajoresChainConfig.alfajores.overrides,
},
});
const contractsMap = buildContracts(
deploymentAddresses,
envSubsetFactories,
) as ChainMap<ChainName, RouterContracts>;
const app = new EnvSubsetApp(contractsMap, multiProvider);
const core = AbacusCore.fromEnvironment('testnet2', multiProvider);
const config = core.extendWithConnectionClientConfig(
getChainToOwnerMap(alfajoresChainConfig, ownerAddress),
);
const envSubsetChecker = new EnvSubsetChecker(multiProvider, app, config);
console.info('Starting check');
await envSubsetChecker.check();
envSubsetChecker.expectEmpty();
}
check()
.then(() => console.info('Check complete'))
.catch(console.error);

@ -0,0 +1,36 @@
import { serializeContracts } from '../../contracts';
import { AbacusCore } from '../../core/AbacusCore';
import { getChainToOwnerMap } from '../../deploy/utils';
import { MultiProvider } from '../../providers/MultiProvider';
import { EnvSubsetDeployer, alfajoresChainConfig } from './app';
import { getAlfajoresSigner } from './utils';
async function main() {
const signer = getAlfajoresSigner();
console.info('Preparing utilities');
const multiProvider = new MultiProvider({
alfajores: {
provider: signer.provider,
confirmations: alfajoresChainConfig.alfajores.confirmations,
overrides: alfajoresChainConfig.alfajores.overrides,
},
});
const core = AbacusCore.fromEnvironment('testnet2', multiProvider);
const config = core.extendWithConnectionClientConfig(
getChainToOwnerMap(alfajoresChainConfig, signer.address),
);
console.info('Starting deployment');
const deployer = new EnvSubsetDeployer(multiProvider, config, core);
const chainToContracts = await deployer.deploy();
const addresses = serializeContracts(chainToContracts);
console.info('===Contract Addresses===');
console.info(JSON.stringify(addresses));
}
main()
.then(() => console.info('Deploy complete'))
.catch(console.error);

@ -0,0 +1,96 @@
import '@nomiclabs/hardhat-waffle';
import { ethers } from 'hardhat';
import { TestCoreApp } from '../../core/TestCoreApp';
import { TestCoreDeployer } from '../../core/TestCoreDeployer';
import { RouterConfig } from '../../deploy/router/types';
import { EnvironmentConfig } from '../../deploy/types';
import { getChainToOwnerMap, getTestMultiProvider } from '../../deploy/utils';
import { MultiProvider } from '../../providers/MultiProvider';
import { RouterContracts } from '../../router';
import { ChainMap, TestChainNames } from '../../types';
import {
EnvSubsetApp,
EnvSubsetChecker,
EnvSubsetDeployer,
SubsetChains,
fullEnvTestConfigs,
subsetTestConfigs,
} from './app';
// Tests deploying the basic EnvSubsetApp to a local hardhat-based test env
describe('deploy app for full test env', async () => {
let multiProvider: MultiProvider<TestChainNames>;
let config: ChainMap<TestChainNames, RouterConfig>;
let deployer: EnvSubsetDeployer<TestChainNames>;
let contracts: Record<TestChainNames, RouterContracts>;
let app: EnvSubsetApp<TestChainNames>;
before(async () => {
const testEnv = await initTestEnv(fullEnvTestConfigs);
multiProvider = testEnv.multiProvider;
config = testEnv.config;
deployer = testEnv.deployer;
});
it('deploys', async () => {
contracts = await deployer.deploy();
});
it('builds app', async () => {
app = new EnvSubsetApp(contracts, multiProvider);
});
it('checks', async () => {
const checker = new EnvSubsetChecker(multiProvider, app, config);
await checker.check();
checker.expectEmpty();
});
});
// Tests same as above but only a subset of the full test env
describe('deploy app to test env subset', async () => {
let multiProvider: MultiProvider<SubsetChains>;
let config: ChainMap<SubsetChains, RouterConfig>;
let deployer: EnvSubsetDeployer<SubsetChains>;
let contracts: Record<SubsetChains, RouterContracts>;
let app: EnvSubsetApp<SubsetChains>;
before(async () => {
const testEnv = await initTestEnv(subsetTestConfigs);
multiProvider = testEnv.multiProvider;
config = testEnv.config;
deployer = testEnv.deployer;
});
it('deploys', async () => {
contracts = await deployer.deploy();
});
it('builds app', async () => {
app = new EnvSubsetApp(contracts, multiProvider);
});
it('checks', async () => {
const checker = new EnvSubsetChecker(multiProvider, app, config);
await checker.check();
checker.expectEmpty();
});
});
async function initTestEnv<Chain extends TestChainNames>(
environmentConfig: EnvironmentConfig<Chain>,
) {
const [signer] = await ethers.getSigners();
const multiProvider = getTestMultiProvider(signer, environmentConfig);
const coreDeployer = new TestCoreDeployer(multiProvider);
const coreContractsMaps = await coreDeployer.deploy();
const core = new TestCoreApp(coreContractsMaps, multiProvider);
const config = core.extendWithConnectionClientConfig(
getChainToOwnerMap(fullEnvTestConfigs, signer.address),
);
const deployer = new EnvSubsetDeployer(multiProvider, config, core);
return { multiProvider, config, deployer };
}

@ -0,0 +1,23 @@
import { Wallet } from 'ethers';
import { StaticCeloJsonRpcProvider } from '@abacus-network/celo-ethers-provider';
export const ALFAJORES_FORNO = 'https://alfajores-forno.celo-testnet.org';
export const CELO_DERIVATION_PATH = "m/44'/52752'/0'/0/0";
export function getAlfajoresSigner() {
console.info('Getting signer');
const provider = getAlfajoresProvider();
const mnemonic = process.env.MNEMONIC;
if (!mnemonic) throw new Error('No MNEMONIC provided in env');
const wallet = Wallet.fromMnemonic(mnemonic, CELO_DERIVATION_PATH).connect(
provider,
);
console.info('Signer and provider ready');
return wallet;
}
export function getAlfajoresProvider() {
console.info('Getting provider');
return new StaticCeloJsonRpcProvider(ALFAJORES_FORNO);
}

@ -2,9 +2,15 @@ import type { ethers } from 'ethers';
import type { Chains } from './consts/chains';
// A union type of the keys in the Chains enum
export type ChainName = keyof typeof Chains;
// A full object map of all chains to a value type
export type CompleteChainMap<Value> = Record<ChainName, Value>;
// A partial object map of some chains to a value type
export type PartialChainMap<Value> = Partial<CompleteChainMap<Value>>;
// A map of some specific subset of chains to a value type
export type ChainMap<Chain extends ChainName, Value> = Record<Chain, Value>;
// The names of test chains, should be kept up to date if new are added
export type TestChainNames = 'test1' | 'test2' | 'test3';
export type NameOrDomain = ChainName | number;

@ -1,25 +1,55 @@
import { AllChains } from '../consts/chains';
import { ChainMap, ChainName, Remotes } from '../types';
export class MultiGeneric<Chain extends ChainName, Value> {
constructor(protected readonly chainMap: ChainMap<Chain, Value>) {}
constructor(public readonly chainMap: ChainMap<Chain, Value>) {}
protected get(chain: Chain) {
return this.chainMap[chain];
/**
* Get value for a chain
* @throws if chain is invalid or has not been set
*/
protected get(chain: Chain): Value {
if (!chain || !AllChains.includes(chain)) {
throw new Error(`Invalid chain ${chain}`);
}
const value = this.chainMap[chain] ?? null;
if (!value) {
throw new Error(`No chain value found for ${chain}`);
}
return value;
}
/**
* Get value for a chain
* @returns value or null if chain value has not been set
*/
protected tryGet(chain: Chain): Value | null {
if (!chain || !AllChains.includes(chain)) {
return null;
}
return this.chainMap[chain] ?? null;
}
protected set(chain: Chain, value: Value) {
/**
* Set value for a chain
* @throws if chain is invalid or has not been set
*/
protected set(chain: Chain, value: Value): Value {
this.chainMap[chain] = value;
return value;
}
chains = () => Object.keys(this.chainMap) as Chain[];
chains(): Chain[] {
return Object.keys(this.chainMap) as Chain[];
}
apply(fn: (n: Chain, dc: Value) => void) {
forEach(fn: (n: Chain, dc: Value) => void): void {
for (const chain of this.chains()) {
fn(chain, this.chainMap[chain]);
}
}
map<Output>(fn: (n: Chain, dc: Value) => Output) {
map<Output>(fn: (n: Chain, dc: Value) => Output): Record<Chain, Output> {
const entries: [Chain, Output][] = [];
const chains = this.chains();
for (const chain of chains) {
@ -28,17 +58,26 @@ export class MultiGeneric<Chain extends ChainName, Value> {
return Object.fromEntries(entries) as Record<Chain, Output>;
}
remoteChains = <LocalChain extends Chain>(name: LocalChain) =>
this.chains().filter((key) => key !== name) as Remotes<Chain, LocalChain>[];
remoteChains<LocalChain extends Chain>(
name: LocalChain,
): Remotes<Chain, LocalChain>[] {
return this.chains().filter((key) => key !== name) as Remotes<
Chain,
LocalChain
>[];
}
extendWithChain = <New extends Remotes<ChainName, Chain>>(
extendWithChain<New extends Remotes<ChainName, Chain>>(
chain: New,
value: Value,
) =>
new MultiGeneric<New & Chain, Value>({
): MultiGeneric<New & Chain, Value> {
return new MultiGeneric<New & Chain, Value>({
...this.chainMap,
[chain]: value,
});
}
knownChain = (chain: ChainName) => chain in this.chainMap;
knownChain(chain: ChainName): boolean {
return chain in this.chainMap;
}
}

@ -19,11 +19,20 @@ export function objMap<K extends string, I = any, O = any>(
}
// promiseObjectAll :: {k: Promise a} -> Promise {k: a}
export const promiseObjAll = <K extends string, V>(object: {
export function promiseObjAll<K extends string, V>(obj: {
[key in K]: Promise<V>;
}): Promise<Record<K, V>> => {
const promiseList = Object.entries(object).map(([name, promise]) =>
}): Promise<Record<K, V>> {
const promiseList = Object.entries(obj).map(([name, promise]) =>
(promise as Promise<V>).then((result) => [name, result]),
);
return Promise.all(promiseList).then(Object.fromEntries);
};
}
// Get the subset of the object from key list
export function pick<K extends string, V = any>(obj: Record<K, V>, keys: K[]) {
const ret: Partial<Record<K, V>> = {};
for (const key of keys) {
ret[key] = obj[key];
}
return ret as Record<K, V>;
}

@ -175,6 +175,7 @@ __metadata:
mocha: ^9.2.2
prettier: ^2.4.1
sinon: ^13.0.2
ts-node: ^10.8.0
typescript: ^4.7.2
languageName: unknown
linkType: soft

Loading…
Cancel
Save