UNPKG

@openzeppelin/defender-as-code

Version:
851 lines 72.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const prompt_1 = __importDefault(require("prompt")); const lodash_1 = __importDefault(require("lodash")); const logger_1 = __importDefault(require("../utils/logger")); const utils_1 = require("../utils"); const keccak256_1 = __importDefault(require("keccak256")); class DefenderDeploy { constructor(serverless, options, logging) { this.serverless = serverless; this.options = options; this.logging = logging; this.resources = this.serverless.service.resources; this.log = logger_1.default.getInstance(); this.hooks = { 'before:deploy:deploy': () => this.validateKeys(), 'deploy:deploy': this.requestConfirmation.bind(this), }; } validateKeys() { this.teamKey = (0, utils_1.getTeamAPIkeysOrThrow)(this.serverless); } async getSSOTDifference() { const difference = { monitors: [], actions: [], notifications: [], contracts: [], relayerApiKeys: [], relayerGroupApiKeys: [], secrets: [], blockExplorerApiKeys: [], forkedNetworks: [], privateNetworks: [], }; // Contracts const contracts = this.resources?.contracts ?? {}; const adminClient = (0, utils_1.getProposalClient)(this.teamKey); const dContracts = await adminClient.listContracts(); const contractDifference = lodash_1.default.differenceWith(dContracts, Object.entries(contracts), (a, b) => { const contractId = `${a.network}-${a.address}`; if ((0, utils_1.isDefenderId)(b[1])) { return contractId === b[1]; } return contractId === `${b[1].network}-${b[1].address}`; }); // Forked Networks const forkedNetworks = this.resources?.['forked-networks'] ?? {}; const networkClient = (0, utils_1.getNetworkClient)(this.teamKey); const forkedNetworkItems = await networkClient.listForkedNetworks(); const forkedNetworkDifference = lodash_1.default.differenceWith(forkedNetworkItems, Object.entries(forkedNetworks), (a, b) => { if ((0, utils_1.isDefenderId)(b[1])) { return a.tenantNetworkId === b[1]; } return a.stackResourceId === (0, utils_1.getResourceID)((0, utils_1.getStackName)(this.serverless), b[0]); }); // Private Networks const privateNetworks = this.resources?.['private-networks'] ?? {}; const privateNetworkItems = await networkClient.listPrivateNetworks(); const privateNetworkDifference = lodash_1.default.differenceWith(privateNetworkItems, Object.entries(privateNetworks), (a, b) => { if ((0, utils_1.isDefenderId)(b[1])) { return a.tenantNetworkId === b[1]; } return a.stackResourceId === (0, utils_1.getResourceID)((0, utils_1.getStackName)(this.serverless), b[0]); }); // Monitors const monitors = this.resources?.monitors ?? {}; const monitorClient = (0, utils_1.getMonitorClient)(this.teamKey); const monitorItems = (await monitorClient.list()).items; const monitorDifference = lodash_1.default.differenceWith(monitorItems, Object.entries(monitors), (a, b) => { if ((0, utils_1.isDefenderId)(b[1])) { return a.monitorId === b[1]; } return a.stackResourceId === (0, utils_1.getResourceID)((0, utils_1.getStackName)(this.serverless), b[0]); }); // Relayers const relayers = this.resources?.relayers ?? {}; const relayerClient = (0, utils_1.getRelayClient)(this.teamKey); const dRelayers = (await relayerClient.list()).items; // Relayers API keys await Promise.all(Object.entries(relayers).map(async ([id, relayer]) => { if ((0, utils_1.isDefenderId)(relayer)) return; const dRelayer = (0, utils_1.getEquivalentResourceByKey)((0, utils_1.getResourceID)((0, utils_1.getStackName)(this.serverless), id), dRelayers); if (dRelayer) { const dRelayerApiKeys = await relayerClient.listKeys(dRelayer.relayerId); const configuredKeys = relayer['api-keys'] ?? []; const relayerApiKeyDifference = lodash_1.default.differenceWith(dRelayerApiKeys, configuredKeys, (a, b) => a.stackResourceId === (0, utils_1.getResourceID)(dRelayer.stackResourceId, b)); difference.relayerApiKeys.push(...relayerApiKeyDifference); } })); // Relayer Groups const relayerGroups = this.resources?.['relayer-groups'] ?? {}; const relayerGroupClient = (0, utils_1.getRelayGroupClient)(this.teamKey); const dRelayerGroups = await relayerGroupClient.list(); // Relayer Group API keys await Promise.all(Object.entries(relayerGroups).map(async ([id, relayerGroup]) => { if ((0, utils_1.isDefenderId)(relayerGroup)) return; const dRelayerGroup = (0, utils_1.getEquivalentResourceByKey)((0, utils_1.getResourceID)((0, utils_1.getStackName)(this.serverless), id), dRelayerGroups); if (dRelayerGroup) { const dRelayerGroupApiKeys = await relayerGroupClient.listKeys(dRelayerGroup.relayerGroupId); const configuredKeys = relayerGroup['api-keys'] ?? []; const relayerGroupApiKeyDifference = lodash_1.default.differenceWith(dRelayerGroupApiKeys, configuredKeys, (a, b) => a.stackResourceId === (0, utils_1.getResourceID)(dRelayerGroup.stackResourceId, b)); difference.relayerGroupApiKeys.push(...relayerGroupApiKeyDifference); } })); // Notifications const notifications = this.resources?.notifications ?? {}; const dNotifications = await monitorClient.listNotificationChannels(); const notificationDifference = lodash_1.default.differenceWith(dNotifications, Object.entries(notifications), (a, b) => { if ((0, utils_1.isDefenderId)(b[1])) { return a.notificationId === b[1]; } return a.stackResourceId === (0, utils_1.getResourceID)((0, utils_1.getStackName)(this.serverless), b[0]); }); // Actions const actions = this.resources.actions ?? {}; const actionClient = (0, utils_1.getActionClient)(this.teamKey); const dActions = (await actionClient.list()).items; const actionDifference = lodash_1.default.differenceWith(dActions, Object.entries(actions), (a, b) => { if ((0, utils_1.isDefenderId)(b[1])) { return a.actionId === b[1]; } return a.stackResourceId === (0, utils_1.getResourceID)((0, utils_1.getStackName)(this.serverless), b[0]); }); // Secrets const allSecrets = (0, utils_1.getConsolidatedSecrets)(this.serverless, this.resources); const dSecrets = (await actionClient.listSecrets()).secretNames ?? []; const secretsDifference = lodash_1.default.differenceWith(dSecrets, Object.values(allSecrets).map((k, _) => Object.keys(k)[0] ?? ''), (a, b) => a === b); // Block Explorer Api Keys const blockExplorerApiKeys = this.resources?.['block-explorer-api-keys'] ?? {}; const deployClient = (0, utils_1.getDeployClient)(this.teamKey); const dBlockExplorerApiKeys = await deployClient.listBlockExplorerApiKeys(); const blockExplorerApiKeyDifference = lodash_1.default.differenceWith(dBlockExplorerApiKeys, Object.entries(blockExplorerApiKeys ?? []), (a, b) => { if ((0, utils_1.isDefenderId)(b[1])) { return a.blockExplorerApiKeyId === b[1]; } return a.stackResourceId === (0, utils_1.getResourceID)((0, utils_1.getStackName)(this.serverless), b[0]); }); difference.forkedNetworks = forkedNetworkDifference; difference.privateNetworks = privateNetworkDifference; difference.contracts = contractDifference; difference.monitors = monitorDifference; difference.notifications = notificationDifference; difference.actions = actionDifference; difference.secrets = secretsDifference; difference.blockExplorerApiKeys = blockExplorerApiKeyDifference; return difference; } async constructConfirmationMessage(withResources) { const start = `You have SSOT enabled. This might remove resources from Defender permanently.\nHaving SSOT enabled will interpret the resources defined in the serverless.yml file as the Single Source Of Truth, and will remove any existing Defender resource which is not defined in the YAML file (with the exception of Relayers).\nIf you continue, the following resources will be removed from Defender:`; const end = `Are you sure you wish to continue [y/n]?`; const formattedResources = { actions: withResources.actions.length > 0 ? withResources.actions.map((a) => `${a.stackResourceId ?? a.name} (${a.actionId})`) : undefined, monitors: withResources.monitors.length > 0 ? withResources.monitors.map((a) => `${a.stackResourceId ?? a.name} (${a.monitorId})`) : undefined, notifications: withResources.notifications.length > 0 ? withResources.notifications.map((a) => `${a.stackResourceId ?? a.name} (${a.notificationId})`) : undefined, contracts: withResources.contracts.length > 0 ? withResources.contracts.map((a) => `${a.network}-${a.address} (${a.name})`) : undefined, relayerApiKeys: 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 ? withResources.forkedNetworks.map((a) => `${a.stackResourceId ?? a.name} (${a.tenantNetworkId})`) : undefined, privateNetworks: withResources.privateNetworks.length > 0 ? withResources.privateNetworks.map((a) => `${a.stackResourceId ?? a.name} (${a.tenantNetworkId})`) : undefined, }; return `${start}\n${lodash_1.default.isEmpty((0, utils_1.validateTypesAndSanitise)(formattedResources)) ? 'None. No differences found.' : JSON.stringify(formattedResources, null, 2)}\n\n${end}`; } async requestConfirmation() { if ((0, utils_1.isSSOT)(this.serverless) && process.stdout.isTTY) { const properties = [ { name: 'confirm', validator: /^(y|n){1}$/i, warning: 'Confirmation must be `y` (yes) or `n` (no)', }, ]; this.log.progress('component-deploy', `Retrieving list of resources`); this.ssotDifference = await this.getSSOTDifference(); this.log.progress('component-deploy', `Awaiting confirmation from user`); prompt_1.default.start({ message: await this.constructConfirmationMessage(this.ssotDifference), }); const { confirm } = await prompt_1.default.get(properties); if (confirm.toString().toLowerCase() !== 'y') { this.log.error('Confirmation not acquired. Terminating command'); return; } this.log.success('Confirmation acquired'); } await this.deploy(); } async deploySecrets(output) { const allSecrets = (0, utils_1.getConsolidatedSecrets)(this.serverless, this.resources); const client = (0, utils_1.getActionClient)(this.teamKey); const retrieveExisting = () => client.listSecrets().then((r) => r.secretNames ?? []); await this.wrapper(this.serverless, 'Secrets', allSecrets, retrieveExisting, // on update async (secret, match) => { await client.createSecrets({ deletes: [], secrets: secret, }); return { name: `Secret`, id: `${match}`, success: true, response: secret, }; }, // on create async (secret, _) => { await client.createSecrets({ deletes: [], secrets: secret, }); return { name: `Secret`, id: `${Object.keys(secret)[0]}`, success: true, response: secret, }; }, // on remove async (secrets) => { await client.createSecrets({ deletes: secrets, secrets: {}, }); }, // overrideMatchDefinition (a, b) => !!b[a], output, this.ssotDifference?.secrets); } async deployContracts(output) { const contracts = this.resources?.contracts ?? {}; const client = (0, utils_1.getProposalClient)(this.teamKey); const retrieveExisting = () => client.listContracts(); await this.wrapper(this.serverless, 'Contracts', (0, utils_1.removeDefenderIdReferences)(contracts), retrieveExisting, // on update async (contract, match) => { const mappedMatch = { 'name': match.name, 'network': match.network, 'address': match.address, 'abi': match.abi && JSON.stringify(JSON.parse(match.abi)), 'nat-spec': match.natSpec ? match.natSpec : undefined, }; // in reality this will never be called as long as platform-sdk does not return ABI as part of the list response if (lodash_1.default.isEqual((0, utils_1.validateTypesAndSanitise)(contract), (0, utils_1.validateTypesAndSanitise)(mappedMatch))) { return { name: match.name, id: `${match.network}-${match.address}`, success: false, response: match, notice: `Skipped import - contract ${match.address} already exists on ${match.network}`, }; } this.log.notice(`Contracts will always update regardless of changes due to certain limitations in Defender SDK.`); const updatedContract = await client.addContract({ name: contract.name, network: match.network, address: match.address, abi: (0, utils_1.formatABI)(contract.abi), natSpec: contract['nat-spec'] ? contract['nat-spec'] : undefined, }); return { name: updatedContract.name, id: `${match.network}-${match.address}`, success: true, response: updatedContract, }; }, // on create async (contract, _stackResourceId) => { const importedContract = await client.addContract({ name: contract.name, network: contract.network, address: contract.address, abi: (0, utils_1.formatABI)(contract.abi), natSpec: contract['nat-spec'] ? contract['nat-spec'] : undefined, }); return { name: importedContract.name, id: `${importedContract.network}-${importedContract.address}`, success: true, response: importedContract, }; }, // on remove async (contracts) => { await Promise.all(contracts.map(async (c) => await client.deleteContract(`${c.network}-${c.address}`))); }, // overrideMatchDefinition (a, b) => { return a.address === b.address && a.network === b.network; }, output, this.ssotDifference?.contracts); } async deployRelayers(output) { const relayers = this.resources?.relayers ?? {}; const client = (0, utils_1.getRelayClient)(this.teamKey); const retrieveExisting = () => client.list().then((r) => r.items); await this.wrapper(this.serverless, 'Relayers', (0, utils_1.removeDefenderIdReferences)(relayers), retrieveExisting, // on update async (relayer, match) => { // Warn users when they try to change the relayer network if (match.network !== relayer.network) { this.log.warn(`Detected a network change from ${match.network} to ${relayer.network} for Relayer: ${match.stackResourceId}. Defender does not currently allow updates to the network once a Relayer is created. This change will be ignored. To enforce this change, remove this relayer and create a new one. Alternatively, you can change the unique identifier (stack resource ID), to force a new creation of the relayer. Note that this change might cause errors further in the deployment process for resources that have any dependencies to this relayer.`); relayer.network = match.network; } 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, }, // currently not supported by defender-sdk // paused: match.paused }; let updatedRelayer = undefined; if (!lodash_1.default.isEqual((0, utils_1.validateTypesAndSanitise)(lodash_1.default.omit(relayer, ['api-keys', 'address-from-relayer'])), (0, utils_1.validateTypesAndSanitise)(mappedMatch))) { updatedRelayer = await client.update(match.relayerId, { name: relayer.name, minBalance: relayer['min-balance'], policies: relayer.policy && { whitelistReceivers: relayer.policy['whitelist-receivers'], gasPriceCap: relayer.policy['gas-price-cap'], EIP1559Pricing: relayer.policy['eip1559-pricing'], privateTransactions: relayer.policy['private-transactions'], }, }); } // check existing keys and remove / create accordingly const existingRelayerKeys = await client.listKeys(match.relayerId); const configuredKeys = relayer['api-keys'] ?? []; const inDefender = lodash_1.default.differenceWith(existingRelayerKeys, configuredKeys, (a, b) => a.stackResourceId === (0, utils_1.getResourceID)(match.stackResourceId, b)); // delete key in Defender thats not defined in template if ((0, utils_1.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.relayerId, key.keyId))); this.log.success(`Removed resources from Defender`); output.relayerKeys.removed.push(...inDefender); } const inTemplate = lodash_1.default.differenceWith(configuredKeys, existingRelayerKeys, (a, b) => (0, utils_1.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 = (0, utils_1.getResourceID)(match.stackResourceId, key); const createdKey = await client.createKey(match.relayerId, { stackResourceId: keyStackResource, }); this.log.success(`Created API Key (${keyStackResource}) for Relayer (${match.relayerId})`); const keyPath = `${process.cwd()}/.defender/relayer-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.relayerKeys.created.push(createdKey); })); } return { name: match.stackResourceId, id: match.relayerId, success: !!updatedRelayer, response: updatedRelayer ?? match, notice: !updatedRelayer ? `Skipped ${match.stackResourceId} - no changes detected` : undefined, }; }, // on create async (relayer, stackResourceId) => { const relayers = this.resources?.relayers ?? {}; const existingRelayers = (await (0, utils_1.getRelayClient)(this.teamKey).list()).items; const maybeRelayer = (0, utils_1.getEquivalentResource)(this.serverless, relayer['address-from-relayer'], // typing address-from-relayer causes issues with schema generation due to circular dependancies so we cast it relayers, existingRelayers, 'Relayers'); const createdRelayer = await client.create({ name: relayer.name, network: relayer.network, minBalance: relayer['min-balance'], useAddressFromRelayerId: maybeRelayer?.relayerId, policies: relayer.policy && { whitelistReceivers: relayer.policy['whitelist-receivers'], gasPriceCap: relayer.policy['gas-price-cap'], EIP1559Pricing: relayer.policy['eip1559-pricing'], privateTransactions: relayer.policy['private-transactions'], }, stackResourceId, }); const relayerKeys = relayer['api-keys']; if (relayerKeys) { await Promise.all(relayerKeys.map(async (key) => { const keyStackResource = (0, utils_1.getResourceID)(stackResourceId, key); const createdKey = await client.createKey(createdRelayer.relayerId, { stackResourceId: keyStackResource, }); this.log.success(`Created API Key (${keyStackResource}) for Relayer (${createdRelayer.relayerId})`); const keyPath = `${process.cwd()}/.defender/relayer-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.relayerKeys.created.push(createdKey); })); } return { name: stackResourceId, id: createdRelayer.relayerId, success: true, response: createdRelayer, }; }, // on remove requires manual interaction undefined, undefined, output); } async deployRelayerGroups(output) { const relayerGroups = this.resources?.['relayer-groups'] ?? {}; const client = (0, utils_1.getRelayGroupClient)(this.teamKey); const retrieveExisting = () => client.list(); await this.wrapper(this.serverless, 'Relayer Groups', (0, utils_1.removeDefenderIdReferences)(relayerGroups), retrieveExisting, // on update async (relayerGroup, match) => { // 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 = (0, utils_1.getMonitorClient)(this.teamKey); const notifications = await monitorClient.listNotificationChannels(); const notificationChannelIds = relayerGroup['notification-channels']?.['notification-ids'] .map((notification) => { const maybeNotification = (0, utils_1.getEquivalentResource)(this.serverless, notification, this.resources?.notifications, notifications, 'Notifications'); return maybeNotification?.notificationId; }) .filter(utils_1.isResource); 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 (!lodash_1.default.isEqual((0, utils_1.validateTypesAndSanitise)(lodash_1.default.omit(relayerGroup, ['api-keys'])), (0, utils_1.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 = lodash_1.default.differenceWith(existingRelayerGroupKeys, configuredKeys, (a, b) => a.stackResourceId === (0, utils_1.getResourceID)(match.stackResourceId, b)); // delete key in Defender thats not defined in template if ((0, utils_1.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 = lodash_1.default.differenceWith(configuredKeys, existingRelayerGroupKeys, (a, b) => (0, utils_1.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 = (0, utils_1.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, stackResourceId) => { 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 = (0, utils_1.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); } async deployNotifications(output) { const notifications = this.resources?.notifications ?? {}; const client = (0, utils_1.getMonitorClient)(this.teamKey); const retrieveExisting = () => client.listNotificationChannels(); await this.wrapper(this.serverless, 'Notifications', (0, utils_1.removeDefenderIdReferences)(notifications), retrieveExisting, // on update async (notification, match) => { const mappedMatch = { type: match.type, name: match.name, config: match.config, paused: match.paused, }; if (lodash_1.default.isEqual((0, utils_1.validateTypesAndSanitise)(notification), (0, utils_1.validateTypesAndSanitise)(mappedMatch))) { return { name: match.stackResourceId, id: match.notificationId, success: false, response: match, notice: `Skipped ${match.stackResourceId} - no changes detected`, }; } const updatedNotification = await client.updateNotificationChannel(match.notificationId, { ...(0, utils_1.constructNotification)(notification, match.stackResourceId), notificationId: match.notificationId, }); return { name: updatedNotification.stackResourceId, id: updatedNotification.notificationId, success: true, response: updatedNotification, }; }, // on create async (notification, stackResourceId) => { const createdNotification = await client.createNotificationChannel((0, utils_1.constructNotification)(notification, stackResourceId)); return { name: stackResourceId, id: createdNotification.notificationId, success: true, response: createdNotification, }; }, // on remove async (notifications) => { await Promise.all(notifications.map(async (n) => await client.deleteNotificationChannel(n.notificationId, n.type))); }, undefined, output, this.ssotDifference?.notifications); } async deployMonitors(output) { try { const monitors = this.resources?.monitors ?? {}; const client = (0, utils_1.getMonitorClient)(this.teamKey); const actions = await (0, utils_1.getActionClient)(this.teamKey).list(); const notifications = await client.listNotificationChannels(); const contracts = await (0, utils_1.getProposalClient)(this.teamKey).listContracts({ includeAbi: true }); const retrieveExisting = () => client.list().then((r) => r.items); await this.wrapper(this.serverless, 'Monitors', (0, utils_1.removeDefenderIdReferences)(monitors), retrieveExisting, // on update async (monitor, match) => { const isForta = (o) => o.type === 'FORTA'; const isBlock = (o) => o.type === 'BLOCK'; // Warn users when they try to change the monitor network if (match.network !== monitor.network) { this.log.warn(`Detected a network change from ${match.network} to ${monitor.network} for Monitor: ${match.stackResourceId}. Defender does not currently allow updates to the network once a Monitor is created. This change will be ignored. To enforce this change, remove this monitor and create a new one. Alternatively, you can change the unique identifier (stack resource ID), to force a new creation of the monitor. Note that this change might cause errors further in the deployment process for resources that have any dependencies to this monitor.`); monitor.network = match.network; } // Warn users when they try to change the monitor type if (monitor.type !== match.type) { this.log.warn(`Detected a type change from ${match.type} to ${monitor.type} for Monitor: ${match.stackResourceId}. Defender does not currently allow updates to the type once a Monitor is created. This change will be ignored. To enforce this change, remove this monitor and create a new one. Alternatively, you can change the unique identifier (stack resource ID), to force a new creation of the monitor. Note that this change might cause errors further in the deployment process for resources that have any dependencies to this monitor.`); monitor.type = match.type; } let blockwatchersForNetwork = []; // Check if network is forked network if ((0, utils_1.isTenantNetwork)(monitor.network)) { blockwatchersForNetwork = (await client.listTenantBlockwatchers()).filter((b) => b.network === monitor.network); } else { blockwatchersForNetwork = (await client.listBlockwatchers()).filter((b) => b.network === monitor.network); } const newMonitor = (0, utils_1.constructMonitor)(this.serverless, this.resources, match.stackResourceId, monitor, notifications, actions.items, blockwatchersForNetwork, contracts); // Map match "response" object to that of a "create" object const addressRule = (isBlock(match) && match.addressRules.length > 0 && lodash_1.default.first(match.addressRules)) || undefined; const blockConditions = (addressRule && addressRule.conditions.length > 0 && addressRule.conditions) || undefined; const confirmLevel = (isBlock(match) && match.blockWatcherId.split('-').length > 0 && lodash_1.default.last(match.blockWatcherId.split('-'))) || undefined; const mappedMatch = { name: match.name, abi: addressRule && addressRule.abi, paused: match.paused, skipABIValidation: match.skipABIValidation, alertThreshold: match.alertThreshold, actionTrigger: match.notifyConfig?.actionId, alertTimeoutMs: match.notifyConfig?.timeoutMs, alertMessageBody: match.notifyConfig?.messageBody, alertMessageSubject: match.notifyConfig?.messageSubject, notificationChannels: match.notifyConfig?.notifications.map((n) => n.notificationId), severityLevel: match.notifyConfig?.severityLevel, type: match.type, stackResourceId: match.stackResourceId, network: match.network, confirmLevel: (confirmLevel && parseInt(confirmLevel)) || confirmLevel, eventConditions: blockConditions && blockConditions.flatMap((c) => c.eventConditions), functionConditions: blockConditions && blockConditions.flatMap((c) => c.functionConditions), txCondition: blockConditions && blockConditions[0].txConditions.length > 0 && blockConditions[0].txConditions[0].expression, privateFortaNodeId: (isForta(match) && match.privateFortaNodeId) || undefined, addresses: isBlock(match) ? addressRule && addressRule.addresses : match.fortaRule?.addresses, actionCondition: isBlock(match) ? addressRule && addressRule.actionCondition?.actionId : match.fortaRule?.actionCondition?.actionId, fortaLastProcessedTime: (isForta(match) && match.fortaLastProcessedTime) || undefined, agentIDs: (isForta(match) && match.fortaRule?.agentIDs) || undefined, fortaConditions: (isForta(match) && match.fortaRule.conditions) || undefined, riskCategory: match.riskCategory, }; if (lodash_1.default.isEqual((0, utils_1.validateTypesAndSanitise)(newMonitor), (0, utils_1.validateTypesAndSanitise)(mappedMatch))) { return { name: match.stackResourceId, id: match.monitorId, success: false, response: match, notice: `Skipped ${match.stackResourceId} - no changes detected`, }; } const updatedMonitor = await client.update(match.monitorId, { // Do not allow to update network of (existing) monitors ...lodash_1.default.omit(newMonitor, ['network']), monitorId: match.monitorId, }); return { name: updatedMonitor.stackResourceId, id: updatedMonitor.monitorId, success: true, response: updatedMonitor, }; }, // on create async (monitor, stackResourceId) => { let blockwatchersForNetwork = []; // Check if network is forked network if ((0, utils_1.isTenantNetwork)(monitor.network)) { blockwatchersForNetwork = (await client.listTenantBlockwatchers()).filter((b) => b.network === monitor.network); } else { blockwatchersForNetwork = (await client.listBlockwatchers()).filter((b) => b.network === monitor.network); } const createdMonitor = await client.create((0, utils_1.constructMonitor)(this.serverless, this.resources, stackResourceId, monitor, notifications, actions.items, blockwatchersForNetwork, contracts)); return { name: stackResourceId, id: createdMonitor.monitorId, success: true, response: createdMonitor, }; }, // on remove async (monitors) => { await Promise.all(monitors.map(async (s) => await client.delete(s.monitorId))); }, undefined, output, this.ssotDifference?.monitors); } catch (e) { this.log.tryLogDefenderError(e); } } async deployActions(output) { const actions = this.resources.actions ?? {}; const client = (0, utils_1.getActionClient)(this.teamKey); const retrieveExisting = () => client.list().then((r) => r.items); await this.wrapper(this.serverless, 'Actions', (0, utils_1.removeDefenderIdReferences)(actions), retrieveExisting, // on update async (action, match) => { const relayers = this.resources?.relayers ?? {}; const existingRelayers = (await (0, utils_1.getRelayClient)(this.teamKey).list()).items; const maybeRelayer = (0, utils_1.getEquivalentResource)(this.serverless, action.relayer, relayers, existingRelayers, 'Relayers'); // Get new code digest const code = await client.getEncodedZippedCodeFromFolder(action.path); const newDigest = client.getCodeDigest({ encodedZippedCode: code, }); const { codeDigest } = await client.get(match.actionId); const environmentVariables = await client.getEnvironmentVariables(match.actionId); const isSchedule = (o) => o.type === 'schedule'; const mappedMatch = { 'name': match.name, 'trigger': { type: match.trigger.type, frequency: (isSchedule(match.trigger) && match.trigger.frequencyMinutes) || undefined, cron: (isSchedule(match.trigger) && match.trigger.cron) || undefined, }, 'paused': match.paused, 'relayerId': match.relayerId, 'codeDigest': match.codeDigest, 'environment-variables': environmentVariables, }; if (lodash_1.default.isEqual((0, utils_1.validateTypesAndSanitise)({ ...lodash_1.default.omit(action, ['events', 'package', 'relayer', 'path']), relayerId: maybeRelayer?.relayerId, codeDigest: newDigest, }), (0, utils_1.validateTypesAndSanitise)(mappedMatch))) { return { name: match.stackResourceId, id: match.actionId, success: false, response: match, notice: `Skipped ${match.stackResourceId} - no changes detected`, }; } const updatesAction = await client.update({ actionId: match.actionId, name: action.name, paused: action.paused, trigger: { type: action.trigger.type, frequencyMinutes: action.trigger.frequency ?? undefined, cron: action.trigger.cron ?? undefined, }, relayerId: maybeRelayer?.relayerId, }); await client.updateEnvironmentVariables(match.actionId, { variables: action['environment-variables'] ?? {} }); if (newDigest === codeDigest) { return { name: match.stackResourceId, id: match.actionId, success: true, notice: `Skipped code upload - no code changes detected for ${match.stackResourceId}`, response: updatesAction, }; } else { await client.updateCodeFromFolder(match.actionId, { path: action.path, }); return { name: match.stackResourceId, id: match.actionId, success: true, response: updatesAction, }; } }, // on create async (action, stackResourceId) => { const actionRelayer = action.relayer; const relayers = this.resources?.relayers ?? {}; const existingRelayers = (await (0, utils_1.getRelayClient)(this.teamKey).list()).items; const maybeRelayer = (0, utils_1.getEquivalentResource)(this.serverless, actionRelayer, relayers, existingRelayers, 'Relayers'); const createdAction = await client.create({ name: action.name, trigger: { type: action.trigger.type, frequencyMinutes: action.trigger.frequency ?? undefined, cron: action.trigger.cron ?? undefined, }, encodedZippedCode: await client.getEncodedZippedCodeFromFolder(action.path), paused: action.paused, relayerId: maybeRelayer?.relayerId, stackResourceId: stackResourceId, environmentVariables: action['environment-variables'], }); return { name: stackResourceId, id: createdAction.actionId, success: true, response: createdAction, }; }, // on remove async (actions) => { await Promise.all(actions.map(async (a) => await client.delete(a.actionId))); }, undefined, output, this.ssotDifference?.actions); } async deployBlockExplorerApiKey(output) { const blockExplorerApiKeys = this.resources?.['block-explorer-api-keys'] ?? {}; const client = (0, utils_1.getDeployClient)(this.teamKey); const retrieveExisting = () => client.listBlockExplorerApiKeys(); await this.wrapper(this.serverless, 'Block Explorer Api Keys', (0, utils_1.removeDefenderIdRe