Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(relayer-groups): add support relayer groups #74

Merged
merged 6 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/defender-test-project/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
],
"license": "MIT",
"devDependencies": {
"@openzeppelin/defender-as-code": "^2.0.0",
"@openzeppelin/defender-as-code": "^3.0.0",
"serverless": "^3.20.0"
}
}
26 changes: 26 additions & 0 deletions examples/defender-test-project/serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,25 @@ resources:
# optional
policy: ${self:resources.policies.policy-1}

relayer-groups:
relayer-group-1:
name: 'Sepolia Relayer Group'
network: 'sepolia'
min-balance: 1000
# optional
relayers: 2
# optional
policy: ${self:resources.policies.policy-1}
# optional
api-keys:
- key1
# optional
notification-channels:
notification-ids:
- ${self:resources.notifications.webhook-1} # only webhooks are allowed here
events:
- 'pending'

notifications:
email-1:
type: 'email'
Expand All @@ -120,6 +139,13 @@ resources:
config:
url: ${self:custom.config.notifications.slack}
paused: false
webhook-1:
type: 'webhook'
name: 'Alert Webhook'
config:
url:
- ${self:custom.config.notifications.webhook}
paused: false

monitors:
# unique resource name
Expand Down
249 changes: 248 additions & 1 deletion src/cmd/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import {
isDefenderId,
removeDefenderIdReferences,
isTenantNetwork,
getRelayGroupClient,
isResource,
} from '../utils';
import {
DefenderAction,
Expand All @@ -53,6 +55,7 @@ import {
Resources,
DefenderTenantNetwork,
DefenderBlockWatcher,
DefenderRelayerGroup,
} from '../types';
import keccak256 from 'keccak256';
import {
Expand All @@ -69,10 +72,13 @@ import {
Monitor,
Monitors,
Notification,
NotificationOrDefenderID,
Notifications,
PrivateNetworkRequest,
PrivateNetworks,
Relayer,
RelayerGroup,
RelayerGroups,
RelayerOrDefenderID,
Relayers,
SupportedNetwork,
Expand Down Expand Up @@ -114,6 +120,7 @@ export default class DefenderDeploy {
notifications: [],
contracts: [],
relayerApiKeys: [],
relayerGroupApiKeys: [],
secrets: [],
blockExplorerApiKeys: [],
forkedNetworks: [],
Expand Down Expand Up @@ -205,6 +212,33 @@ export default class DefenderDeploy {
}),
);

// Relayer Groups
const relayerGroups: RelayerGroups = this.resources?.['relayer-groups'] ?? {};
const relayerGroupClient = getRelayGroupClient(this.teamKey!);
const dRelayerGroups = await relayerGroupClient.list();

// Relayer Group API keys
await Promise.all(
Object.entries(relayerGroups).map(async ([id, relayerGroup]: [string, RelayerGroup | DefenderID]) => {
if (isDefenderId(relayerGroup)) return;
const dRelayerGroup = getEquivalentResourceByKey<DefenderRelayerGroup>(
getResourceID(getStackName(this.serverless), id),
dRelayerGroups,
);
if (dRelayerGroup) {
const dRelayerGroupApiKeys = await relayerGroupClient.listKeys(dRelayerGroup.relayerGroupId);
const configuredKeys = relayerGroup['api-keys'] ?? [];
const relayerGroupApiKeyDifference = _.differenceWith(
dRelayerGroupApiKeys,
configuredKeys,
(a: DefenderRelayerApiKey, b: string) =>
a.stackResourceId === getResourceID(dRelayerGroup.stackResourceId!, b),
);
difference.relayerGroupApiKeys.push(...relayerGroupApiKeyDifference);
}
}),
);

// Notifications
const notifications: Notifications = this.resources?.notifications ?? {};
const dNotifications = await monitorClient.listNotificationChannels();
Expand Down Expand Up @@ -294,6 +328,10 @@ export default class DefenderDeploy {
withResources.relayerApiKeys.length > 0
? withResources.relayerApiKeys.map((a) => `${a.stackResourceId ?? a.apiKey} (${a.keyId})`)
: undefined,
relayerGroupApiKeys:
withResources.relayerGroupApiKeys.length > 0
? withResources.relayerGroupApiKeys.map((a) => `${a.stackResourceId ?? a.apiKey} (${a.keyId})`)
: undefined,
secrets: withResources.secrets.length > 0 ? withResources.secrets.map((a) => `${a}`) : undefined,
forkedNetworks:
withResources.forkedNetworks.length > 0
Expand Down Expand Up @@ -630,6 +668,201 @@ export default class DefenderDeploy {
);
}

private async deployRelayerGroups(
output: DeployOutput<DefenderRelayerGroup> & {
relayerGroupKeys: DeployOutput<DefenderRelayerApiKey>;
},
) {
const relayerGroups: RelayerGroups = this.resources?.['relayer-groups'] ?? {};
const client = getRelayGroupClient(this.teamKey!);
const retrieveExisting = () => client.list();
await this.wrapper<RelayerGroup, DefenderRelayerGroup>(
this.serverless,
'Relayer Groups',
removeDefenderIdReferences(relayerGroups),
retrieveExisting,
// on update
async (relayerGroup: RelayerGroup, match: DefenderRelayerGroup) => {
// Warn users when they try to change the relayer group network
if (match.network !== relayerGroup.network) {
this.log.warn(
`Detected a network change from ${match.network} to ${relayerGroup.network} for Relayer Group: ${match.stackResourceId}. Defender does not currently allow updates to the network once a Relayer Group is created. This change will be ignored. To enforce this change, remove this relayer group and create a new one. Alternatively, you can change the unique identifier (stack resource ID), to force a new creation of the relayer group. Note that this change might cause errors further in the deployment process for resources that have any dependencies to this relayer group.`,
);
relayerGroup.network = match.network!;
}
if (match.relayers.length !== relayerGroup.relayers) {
this.log.warn(
`Detected a change in the number of relayers from ${match.relayers.length} to ${relayerGroup.relayers} for Relayer Group: ${match.stackResourceId}. Defender does not currently allow updates to the number of relayers once a Relayer Group is created. This change will be ignored. To enforce this change, remove this relayer group and create a new one. Alternatively, you can change the unique identifier (stack resource ID), to force a new creation of the relayer group. Note that this change might cause errors further in the deployment process for resources that have any dependencies to this relayer group.`,
);
relayerGroup.relayers = match.relayers.length;
}

const monitorClient = getMonitorClient(this.teamKey!);
const notifications = await monitorClient.listNotificationChannels();

const notificationChannelIds = relayerGroup['notification-channels']?.['notification-ids']
.map((notification) => {
const maybeNotification = getEquivalentResource<NotificationOrDefenderID | undefined, DefenderNotification>(
this.serverless,
notification,
this.resources?.notifications,
notifications,
'Notifications',
);
return maybeNotification?.notificationId;
})
.filter(isResource) as string[];

if (relayerGroup['notification-channels']) {
relayerGroup['notification-channels'] = {
'events': relayerGroup['notification-channels']?.events,
'notification-ids': notificationChannelIds,
};
}

const mappedMatch = {
'name': match.name,
'network': match.network,
'min-balance': parseInt(match.minBalance.toString()),
'policy': {
'gas-price-cap': match.policies.gasPriceCap,
'whitelist-receivers': match.policies.whitelistReceivers,
'eip1559-pricing': match.policies.EIP1559Pricing,
'private-transactions': match.policies.privateTransactions,
},
'relayers': match.relayers.length,
'notification-channels': match.notificationChannels && {
'events': match.notificationChannels.events,
'notification-ids': match.notificationChannels.notificationIds,
},
// Not yet supported in SDK
// 'user-weight-caps': match.userWeightCaps,
};

let updatedRelayerGroup = undefined;
if (
!_.isEqual(
validateTypesAndSanitise(_.omit(relayerGroup, ['api-keys'])),
validateTypesAndSanitise(mappedMatch),
)
) {
updatedRelayerGroup = await client.update(match.relayerGroupId, {
name: relayerGroup.name,
minBalance: relayerGroup['min-balance'],
policies: relayerGroup.policy && {
whitelistReceivers: relayerGroup.policy['whitelist-receivers'],
gasPriceCap: relayerGroup.policy['gas-price-cap'],
EIP1559Pricing: relayerGroup.policy['eip1559-pricing'],
privateTransactions: relayerGroup.policy['private-transactions'],
},
notificationChannels: relayerGroup['notification-channels'] && {
events: relayerGroup['notification-channels'].events,
notificationIds: notificationChannelIds,
},
// Not yet supported in SDK
// userWeightCaps: relayerGroup['user-weight-caps'],
});
}

// check existing keys and remove / create accordingly
const existingRelayerGroupKeys = await client.listKeys(match.relayerGroupId);
const configuredKeys = relayerGroup['api-keys'] ?? [];
const inDefender = _.differenceWith(
existingRelayerGroupKeys,
configuredKeys,
(a: DefenderRelayerApiKey, b: string) => a.stackResourceId === getResourceID(match.stackResourceId!, b),
);

// delete key in Defender thats not defined in template
if (isSSOT(this.serverless) && inDefender.length > 0) {
this.log.info(`Unused resources found on Defender:`);
this.log.info(JSON.stringify(inDefender, null, 2));
this.log.progress('component-deploy-extra', `Removing resources from Defender`);
await Promise.all(inDefender.map(async (key) => await client.deleteKey(match.relayerGroupId, key.keyId)));
this.log.success(`Removed resources from Defender`);
output.relayerGroupKeys.removed.push(...inDefender);
}

const inTemplate = _.differenceWith(
configuredKeys,
existingRelayerGroupKeys,
(a: string, b: DefenderRelayerApiKey) => getResourceID(match.stackResourceId!, a) === b.stackResourceId,
);

// create key in Defender thats defined in template
if (inTemplate) {
await Promise.all(
inTemplate.map(async (key) => {
const keyStackResource = getResourceID(match.stackResourceId!, key);
const createdKey = await client.createKey(match.relayerGroupId, {
stackResourceId: keyStackResource,
});
this.log.success(`Created API Key (${keyStackResource}) for Relayer Group (${match.relayerGroupId})`);
const keyPath = `${process.cwd()}/.defender/relayer-group-keys/${keyStackResource}.json`;
await this.serverless.utils.writeFile(keyPath, JSON.stringify({ ...createdKey }, null, 2));
this.log.info(`API Key details stored in ${keyPath}`, 1);
output.relayerGroupKeys.created.push(createdKey);
}),
);
}

return {
name: match.stackResourceId!,
id: match.relayerGroupId,
success: !!updatedRelayerGroup,
response: updatedRelayerGroup ?? match,
notice: !updatedRelayerGroup ? `Skipped ${match.stackResourceId} - no changes detected` : undefined,
};
},
// on create
async (relayerGroup: RelayerGroup, stackResourceId: string) => {
const createdRelayerGroup = await client.create({
name: relayerGroup.name,
network: relayerGroup.network,
minBalance: relayerGroup['min-balance'],
policies: relayerGroup.policy && {
whitelistReceivers: relayerGroup.policy['whitelist-receivers'],
gasPriceCap: relayerGroup.policy['gas-price-cap'],
EIP1559Pricing: relayerGroup.policy['eip1559-pricing'],
privateTransactions: relayerGroup.policy['private-transactions'],
},
relayers: relayerGroup.relayers,
stackResourceId,
});

const relayerGroupKeys = relayerGroup['api-keys'];
if (relayerGroupKeys) {
await Promise.all(
relayerGroupKeys.map(async (key) => {
const keyStackResource = getResourceID(stackResourceId, key);
const createdKey = await client.createKey(createdRelayerGroup.relayerGroupId, {
stackResourceId: keyStackResource,
});
this.log.success(
`Created API Key (${keyStackResource}) for Relayer Group (${createdRelayerGroup.relayerGroupId})`,
);
const keyPath = `${process.cwd()}/.defender/relayer-group-keys/${keyStackResource}.json`;
await this.serverless.utils.writeFile(keyPath, JSON.stringify({ ...createdKey }, null, 2));
this.log.info(`API Key details stored in ${keyPath}`, 1);
output.relayerGroupKeys.created.push(createdKey);
}),
);
}

return {
name: stackResourceId,
id: createdRelayerGroup.relayerGroupId,
success: true,
response: createdRelayerGroup,
};
},
// on remove requires manual interaction
undefined,
undefined,
output,
);
}

private async deployNotifications(output: DeployOutput<DefenderNotification>) {
const notifications: Notifications = this.resources?.notifications ?? {};
const client = getMonitorClient(this.teamKey!);
Expand Down Expand Up @@ -1434,6 +1667,18 @@ export default class DefenderDeploy {
updated: [],
},
};
const relayerGroups: DeployOutput<DefenderRelayerGroup> & {
relayerGroupKeys: DeployOutput<DefenderRelayerApiKey>;
} = {
removed: [],
created: [],
updated: [],
relayerGroupKeys: {
removed: [],
created: [],
updated: [],
},
};
const blockExplorerApiKeys: DeployOutput<DefenderBlockExplorerApiKey> = {
removed: [],
created: [],
Expand All @@ -1458,6 +1703,7 @@ export default class DefenderDeploy {
actions: actions,
contracts,
relayers,
relayerGroups,
notifications,
secrets,
blockExplorerApiKeys,
Expand All @@ -1469,11 +1715,12 @@ export default class DefenderDeploy {
await this.deployPrivateNetworks(stdOut.privateNetworks);
await this.deploySecrets(stdOut.secrets);
await this.deployContracts(stdOut.contracts);
await this.deployNotifications(stdOut.notifications);
// Always deploy relayers before actions
await this.deployRelayers(stdOut.relayers);
await this.deployRelayerGroups(stdOut.relayerGroups);
await this.deployActions(stdOut.actions);
// Deploy notifications before monitors
await this.deployNotifications(stdOut.notifications);
await this.deployMonitors(stdOut.monitors);
await this.deployBlockExplorerApiKey(stdOut.blockExplorerApiKeys);

Expand Down
Loading
Loading