Skip to content

Commit

Permalink
Introduce Forked Networks support (#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
shahnami authored Sep 19, 2023
1 parent 8b44cad commit 784eca9
Show file tree
Hide file tree
Showing 31 changed files with 966 additions and 745 deletions.
10 changes: 10 additions & 0 deletions examples/defender-test-project/serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -201,5 +201,15 @@ resources:
# optional
forta-last-processed-time: null

forked-networks:
mainnet-fork:
name: mainnet-fork # restricted to alphanumeric characters and dashes (no whitespaces)
forked-network: mainnet # must be of type SupportedNetwork
rpc-url: https://rpc.phalcon.xyz/rpc_123
# optional
block-explorer-url: https://scan.phalcon.xyz/fork_123
# optional
api-key: null

plugins:
- '@openzeppelin/defender-as-code'
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
"typescript": "^4.9.5"
},
"dependencies": {
"@openzeppelin/defender-sdk": "^1.0.0",
"@openzeppelin/defender-sdk": "^1.1.0",
"keccak256": "^1.0.6",
"lodash": "^4.17.21",
"prompt": "^1.3.0"
Expand Down
120 changes: 119 additions & 1 deletion src/cmd/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
getDeployClient,
formatABI,
constructMonitor,
getNetworkClient,
} from '../utils';
import {
DefenderAction,
Expand All @@ -49,6 +50,7 @@ import {
DefenderFortaMonitorResponse,
DefenderBlockMonitorResponse,
Resources,
DefenderForkedNetwork,
} from '../types';
import keccak256 from 'keccak256';
import {
Expand All @@ -60,12 +62,15 @@ import {
Category,
Contract,
Contracts,
ForkedNetworkRequest,
ForkedNetworks,
Monitor,
Monitors,
Notification,
Notifications,
Relayer,
Relayers,
SupportedNetwork,
} from '../types/types/resources.schema';

export default class DefenderDeploy {
Expand Down Expand Up @@ -107,6 +112,7 @@ export default class DefenderDeploy {
relayerApiKeys: [],
secrets: [],
blockExplorerApiKeys: [],
forkedNetworks: [],
};
// Contracts
const contracts: Contracts = this.resources?.contracts ?? {};
Expand All @@ -118,6 +124,17 @@ export default class DefenderDeploy {
(a: DefenderContract, b: [string, Contract]) => `${a.network}-${a.address}` === `${b[1].network}-${b[1].address}`,
);

// Forked Networks
const forkedNetworks: ForkedNetworks = this.resources?.['forked-networks'] ?? {};
const networkClient = getNetworkClient(this.teamKey!);
const forkedNetworkItems = await networkClient.listForkedNetworks();
const forkedNetworkDifference = _.differenceWith(
forkedNetworkItems,
Object.entries(forkedNetworks),
(a: DefenderForkedNetwork, b: [string, ForkedNetworkRequest]) =>
a.stackResourceId === getResourceID(getStackName(this.serverless), b[0]),
);

// Monitors
const monitors: Monitors = this.resources?.monitors ?? {};
const monitorClient = getMonitorClient(this.teamKey!);
Expand Down Expand Up @@ -205,6 +222,7 @@ export default class DefenderDeploy {
a.stackResourceId === getResourceID(getStackName(this.serverless), b[0]),
);

difference.forkedNetworks = forkedNetworkDifference;
difference.contracts = contractDifference;
difference.monitors = monitorDifference;
difference.notifications = notificationDifference;
Expand Down Expand Up @@ -1060,6 +1078,99 @@ export default class DefenderDeploy {
);
}

private async deployForkedNetworks(output: DeployOutput<DefenderForkedNetwork>) {
const forkedNetworks: ForkedNetworks = this.resources?.['forked-networks'] ?? {};
const client = getNetworkClient(this.teamKey!);
const retrieveExisting = () => client.listForkedNetworks();

await this.wrapper<ForkedNetworkRequest, DefenderForkedNetwork>(
this.serverless,
'Forked Networks',
forkedNetworks,
retrieveExisting,
// on update
async (forkedNetwork: ForkedNetworkRequest, match: DefenderForkedNetwork) => {
// Warn users when they try to change fields that are not allowed to be updated
if (match.name !== forkedNetwork.name) {
this.log.warn(
`Detected a name change from ${match.name} to ${forkedNetwork.name} for Forked Network: ${match.stackResourceId}. Defender does not currently allow updates to the name once a Forked Network is created. This change will be ignored. To enforce this change, remove this Forked Network and create a new one. Alternatively, you can change the unique identifier (stack resource ID), to force a new creation of the Forked Network. Note that this change might cause errors further in the deployment process for resources that have any dependencies to this network.`,
);
forkedNetwork.name = match.name;
}

if (match.forkedNetwork !== forkedNetwork['forked-network']) {
this.log.warn(
`Detected a change from ${match.forkedNetwork} to ${forkedNetwork['forked-network']} for Forked Network: ${match.stackResourceId}. Defender does not currently allow updates to the fork source once a Forked Network is created. This change will be ignored. To enforce this change, remove this Forked Network and create a new one. Alternatively, you can change the unique identifier (stack resource ID), to force a new creation of the Forked Network. Note that this change might cause errors further in the deployment process for resources that have any dependencies to this network.`,
);
forkedNetwork['forked-network'] = match.forkedNetwork as SupportedNetwork;
}

if (match.rpcUrl !== forkedNetwork['rpc-url']) {
this.log.warn(
`Detected a change from ${match.rpcUrl} to ${forkedNetwork['rpc-url']} for Forked Network: ${match.stackResourceId}. Defender does not currently allow updates to the RPC URL once a Forked Network is created. This change will be ignored. To enforce this change, remove this Forked Network and create a new one. Alternatively, you can change the unique identifier (stack resource ID), to force a new creation of the Forked Network. Note that this change might cause errors further in the deployment process for resources that have any dependencies to this network.`,
);
forkedNetwork['rpc-url'] = match.rpcUrl;
}

const mappedMatch = {
'name': match.name,
'forked-network': match.forkedNetwork,
'rpc-url': match.rpcUrl,
'api-key': match.apiKey === null || !!match.apiKey ? match.apiKey : undefined,
'block-explorer-url':
match.blockExplorerUrl === null || !!match.blockExplorerUrl ? match.blockExplorerUrl : undefined,
};

if (_.isEqual(validateTypesAndSanitise(forkedNetwork), validateTypesAndSanitise(mappedMatch))) {
return {
name: match.stackResourceId!,
id: match.forkedNetworkId,
success: false,
response: match,
notice: `Skipped ${match.stackResourceId} - no changes detected`,
};
}

const updatedForkedNetwork = await client.updateForkedNetwork(match.forkedNetworkId, {
apiKey: forkedNetwork['api-key'] ?? undefined,
blockExplorerUrl: forkedNetwork['block-explorer-url'],
stackResourceId: match.stackResourceId!,
});

return {
name: updatedForkedNetwork.stackResourceId!,
id: updatedForkedNetwork.forkedNetworkId,
success: true,
response: updatedForkedNetwork,
};
},
// on create
async (forkedNetwork: ForkedNetworkRequest, stackResourceId: string) => {
const createdForkedNetwork = await client.createForkedNetwork({
name: forkedNetwork.name,
forkedNetwork: forkedNetwork['forked-network'],
rpcUrl: forkedNetwork['rpc-url'],
blockExplorerUrl: forkedNetwork['block-explorer-url'] ?? undefined,
apiKey: forkedNetwork['api-key'] ?? undefined,
stackResourceId,
});
return {
name: stackResourceId,
id: createdForkedNetwork.forkedNetworkId,
success: true,
response: createdForkedNetwork,
};
},
// on remove
async (forkedNetworks: DefenderForkedNetwork[]) => {
await Promise.all(forkedNetworks.map(async (c) => await client.deleteForkedNetwork(c.forkedNetworkId)));
},
undefined,
output,
this.ssotDifference?.forkedNetworks,
);
}

private async wrapper<Y, D>(
context: Serverless,
resourceType: ResourceType,
Expand Down Expand Up @@ -1197,12 +1308,16 @@ export default class DefenderDeploy {
updated: [],
},
};

const blockExplorerApiKeys: DeployOutput<DefenderBlockExplorerApiKey> = {
removed: [],
created: [],
updated: [],
};
const forkedNetworks: DeployOutput<DefenderForkedNetwork> = {
removed: [],
created: [],
updated: [],
};

const stdOut = {
stack: stackName,
Expand All @@ -1215,7 +1330,10 @@ export default class DefenderDeploy {
categories,
secrets,
blockExplorerApiKeys,
forkedNetworks,
};

await this.deployForkedNetworks(stdOut.forkedNetworks);
await this.deploySecrets(stdOut.secrets);
await this.deployContracts(stdOut.contracts);
// Always deploy relayers before actions
Expand Down
26 changes: 25 additions & 1 deletion src/cmd/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
getConsolidatedSecrets,
getRelayClient,
getMonitorClient,
getNetworkClient,
getStackName,
getTeamAPIkeysOrThrow,
isTemplateResource,
Expand All @@ -27,8 +28,17 @@ import {
TeamKey,
YSecret,
Resources,
DefenderForkedNetwork,
} from '../types';
import { Action, Contract, Monitor, Relayer, Notification, Category } from '../types/types/resources.schema';
import {
Action,
Contract,
Monitor,
Relayer,
Notification,
Category,
ForkedNetworkRequest,
} from '../types/types/resources.schema';

export default class DefenderInfo {
serverless: Serverless;
Expand Down Expand Up @@ -109,7 +119,21 @@ export default class DefenderInfo {
notifications: [],
categories: [],
secrets: [],
forkedNetworks: [],
};

// Forked Networks
const listForkedNetworks = () => getNetworkClient(this.teamKey!).listForkedNetworks();

await this.wrapper<ForkedNetworkRequest, DefenderForkedNetwork>(
this.serverless,
'Forked Networks',
this.resources?.['forked-networks'],
listForkedNetworks,
(resource: DefenderForkedNetwork) => `${resource.stackResourceId}: ${resource.forkedNetworkId}`,
stdOut.forkedNetworks,
);

// Monitors
const listMonitors = () =>
getMonitorClient(this.teamKey!)
Expand Down
37 changes: 36 additions & 1 deletion src/cmd/remove.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
getConsolidatedSecrets,
getRelayClient,
getMonitorClient,
getNetworkClient,
getStackName,
getTeamAPIkeysOrThrow,
isTemplateResource,
Expand All @@ -28,8 +29,16 @@ import {
TeamKey,
YSecret,
Resources,
DefenderForkedNetwork,
} from '../types';
import { Action, Contract, Monitor, Relayer, Notification } from '../types/types/resources.schema';
import {
Action,
Contract,
Monitor,
Relayer,
Notification,
ForkedNetworkRequest,
} from '../types/types/resources.schema';

export default class DefenderRemove {
serverless: Serverless;
Expand Down Expand Up @@ -121,6 +130,7 @@ export default class DefenderRemove {
notifications: DefenderNotification[];
categories: DefenderCategory[];
secrets: string[];
forkedNetworks: DefenderForkedNetwork[];
} = {
stack: stackName,
monitors: [],
Expand All @@ -130,7 +140,32 @@ export default class DefenderRemove {
notifications: [],
categories: [],
secrets: [],
forkedNetworks: [],
};

// Forked Networks
const forkedNetworkClient = getNetworkClient(this.teamKey!);
const listForkedNetworks = () => forkedNetworkClient.listForkedNetworks();
await this.wrapper<ForkedNetworkRequest, DefenderForkedNetwork>(
this.serverless,
'Forked Networks',
this.resources?.['forked-networks'],
listForkedNetworks,
async (forkedNetworks: DefenderForkedNetwork[]) => {
await Promise.all(
forkedNetworks.map(async (e) => {
this.log.progress(
'component-remove-extra',
`Removing ${e.stackResourceId} (${e.forkedNetworkId}) from Defender`,
);
await forkedNetworkClient.deleteForkedNetwork(e.forkedNetworkId);
this.log.success(`Removed ${e.stackResourceId} (${e.forkedNetworkId})`);
}),
);
},
stdOut.forkedNetworks,
);

// Monitors
const monitorClient = getMonitorClient(this.teamKey!);
const listMonitors = () => monitorClient.list().then((i) => i.items);
Expand Down
Loading

0 comments on commit 784eca9

Please sign in to comment.