From c281142c7439af36d91c264ac437606c7498d554 Mon Sep 17 00:00:00 2001 From: taitsengstock Date: Tue, 24 Feb 2026 15:16:05 +1100 Subject: [PATCH 1/3] feat: KEEP-1494 merge Safe plugin into Safe protocol integration --- components/workflow/config/action-config.tsx | 40 +++++- keeperhub/plugins/safe/index.ts | 124 ++++++++++--------- lib/workflow-executor.workflow.ts | 12 +- plugins/legacy-mappings.ts | 5 + specs/merge-safe-integrations.md | 66 ++++++++++ 5 files changed, 185 insertions(+), 62 deletions(-) create mode 100644 specs/merge-safe-integrations.md diff --git a/components/workflow/config/action-config.tsx b/components/workflow/config/action-config.tsx index 63cec1a8a..ab38cc9a3 100644 --- a/components/workflow/config/action-config.tsx +++ b/components/workflow/config/action-config.tsx @@ -580,10 +580,24 @@ function useCategoryData() { }; for (const [category, actions] of Object.entries(pluginCategories)) { - allCategories[category] = actions.map((a) => ({ - id: a.id, - label: a.label, - })); + // start custom keeperhub code // + // Deduplicate by slug within each category. When the same action is + // registered under two integrations (e.g. safe and safe-wallet both + // contribute "get-pending-transactions"), keep the first occurrence. + const seen = new Set(); + allCategories[category] = actions + .filter((a) => { + if (seen.has(a.slug)) { + return false; + } + seen.add(a.slug); + return true; + }) + .map((a) => ({ + id: a.id, + label: a.label, + })); + // end keeperhub code // } return allCategories; @@ -634,7 +648,23 @@ export function ActionConfig({ }: ActionConfigProps) { const actionType = (config?.actionType as string) || ""; const categories = useCategoryData(); - const integrations = useMemo(() => getAllIntegrations(), []); + // start custom keeperhub code // + // Deduplicate integrations by label for the Service dropdown. + // When two integrations share a label (e.g. "safe" and "safe-wallet" both + // labelled "Safe"), keep the one with more actions to avoid Radix Select + // duplicate-value collisions and present a single unified entry. + const integrations = useMemo(() => { + const all = getAllIntegrations(); + const byLabel = new Map(); + for (const i of all) { + const existing = byLabel.get(i.label); + if (!existing || i.actions.length > existing.actions.length) { + byLabel.set(i.label, i); + } + } + return Array.from(byLabel.values()); + }, []); + // end keeperhub code // const selectedCategory = actionType ? getCategoryForAction(actionType) : null; const [category, setCategory] = useState(selectedCategory || ""); diff --git a/keeperhub/plugins/safe/index.ts b/keeperhub/plugins/safe/index.ts index 92be6189a..2926f95b5 100644 --- a/keeperhub/plugins/safe/index.ts +++ b/keeperhub/plugins/safe/index.ts @@ -1,7 +1,73 @@ +import type { IntegrationType } from "@/lib/types/integration"; import type { IntegrationPlugin } from "@/plugins/registry"; -import { registerIntegration } from "@/plugins/registry"; +import { getIntegration, registerIntegration } from "@/plugins/registry"; import { SafeIcon } from "./icon"; +const getPendingTransactionsAction = { + slug: "get-pending-transactions", + label: "Get Pending Transactions", + description: + "Fetch pending multisig transactions from a Safe that have not been executed yet. Optionally filter for transactions a specific signer has not confirmed.", + category: "Safe", + stepFunction: "getPendingTransactionsStep", + stepImportPath: "get-pending-transactions", + requiresCredentials: true, + credentialIntegrationType: "safe" as string, + outputFields: [ + { + field: "success", + description: "Whether the request succeeded", + }, + { + field: "transactions", + description: + "Array of pending transactions with safeTxHash, to, value, data, operation, nonce, confirmations, confirmationsRequired, dataDecoded, and submissionDate", + }, + { + field: "count", + description: "Number of pending transactions returned", + }, + { field: "error", description: "Error message if failed" }, + ], + configFields: [ + { + key: "safeAddress", + label: "Safe Address", + type: "template-input" as const, + placeholder: "0x... or {{NodeName.address}}", + example: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + required: true, + }, + { + key: "network", + label: "Network", + type: "chain-select" as const, + chainTypeFilter: "evm", + placeholder: "Select network", + required: true, + }, + { + key: "signerAddress", + label: "Signer Address", + type: "template-input" as const, + placeholder: + "0x... filter for txs this address has not signed (optional)", + example: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + required: false, + }, + ], +}; + +// Inject get-pending-transactions into the safe-wallet protocol plugin +// so both on-chain reads and off-chain API actions appear under one "Safe" entry. +const safeWallet = getIntegration("safe-wallet" as IntegrationType); +if (safeWallet) { + safeWallet.actions.push(getPendingTransactionsAction); +} + +// Safe keeps its action for step registry generation (safe/get-pending-transactions). +// A legacy mapping alias creates the safe-wallet/get-pending-transactions entry. +// formFields/testConfig remain so the connection management UI can create credentials. const safePlugin: IntegrationPlugin = { type: "safe", label: "Safe", @@ -35,61 +101,7 @@ const safePlugin: IntegrationPlugin = { }, }, - actions: [ - { - slug: "get-pending-transactions", - label: "Get Pending Transactions", - description: - "Fetch pending multisig transactions from a Safe that have not been executed yet. Optionally filter for transactions a specific signer has not confirmed.", - category: "Safe", - stepFunction: "getPendingTransactionsStep", - stepImportPath: "get-pending-transactions", - requiresCredentials: true, - outputFields: [ - { - field: "success", - description: "Whether the request succeeded", - }, - { - field: "transactions", - description: - "Array of pending transactions with safeTxHash, to, value, data, operation, nonce, confirmations, confirmationsRequired, dataDecoded, and submissionDate", - }, - { - field: "count", - description: "Number of pending transactions returned", - }, - { field: "error", description: "Error message if failed" }, - ], - configFields: [ - { - key: "safeAddress", - label: "Safe Address", - type: "template-input", - placeholder: "0x... or {{NodeName.address}}", - example: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", - required: true, - }, - { - key: "network", - label: "Network", - type: "chain-select", - chainTypeFilter: "evm", - placeholder: "Select network", - required: true, - }, - { - key: "signerAddress", - label: "Signer Address", - type: "template-input", - placeholder: - "0x... filter for txs this address has not signed (optional)", - example: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", - required: false, - }, - ], - }, - ], + actions: [getPendingTransactionsAction], }; registerIntegration(safePlugin); diff --git a/lib/workflow-executor.workflow.ts b/lib/workflow-executor.workflow.ts index da9c464a5..2f3078ed7 100644 --- a/lib/workflow-executor.workflow.ts +++ b/lib/workflow-executor.workflow.ts @@ -37,6 +37,7 @@ import { getStepImporter, type StepImporter, } from "./step-registry"; +import { LEGACY_ACTION_MAPPINGS } from "@/plugins/legacy-mappings"; import type { StepContext } from "./steps/step-handler"; import { triggerStep } from "./steps/trigger"; import { deserializeEventTriggerData, getErrorMessageAsync } from "./utils"; @@ -363,7 +364,16 @@ async function executeActionStep(input: { } // Look up plugin action from the generated step registry - const stepImporter = getStepImporter(actionType); + let stepImporter = getStepImporter(actionType); + + // Fallback: check legacy action mappings (e.g. "safe/get-pending-transactions" -> "safe-wallet/get-pending-transactions") + if (!stepImporter) { + const mapped = LEGACY_ACTION_MAPPINGS[actionType]; + if (mapped) { + stepImporter = getStepImporter(mapped); + } + } + if (stepImporter) { const module = await stepImporter.importer(); const stepFunction = module[stepImporter.stepFunction]; diff --git a/plugins/legacy-mappings.ts b/plugins/legacy-mappings.ts index 1e638615d..9d2f8c675 100644 --- a/plugins/legacy-mappings.ts +++ b/plugins/legacy-mappings.ts @@ -31,6 +31,11 @@ export const LEGACY_ACTION_MAPPINGS: Record = { "Create Chat": "v0/create-chat", "Send Message": "v0/send-message", + // Safe: the get-pending-transactions action lives in the safe plugin but is + // injected into safe-wallet at runtime. This alias ensures the step registry + // has an entry for safe-wallet/get-pending-transactions. + "safe-wallet/get-pending-transactions": "safe/get-pending-transactions", + // Web3 "Check Balance": "web3/check-balance", "Transfer Funds": "web3/transfer-funds", diff --git a/specs/merge-safe-integrations.md b/specs/merge-safe-integrations.md new file mode 100644 index 000000000..2ab837093 --- /dev/null +++ b/specs/merge-safe-integrations.md @@ -0,0 +1,66 @@ +# Spec: Merge Safe Plugin into Safe Protocol + +## Problem + +Two separate integrations register with label "Safe" in the workflow builder UI: + +- `safe-wallet` protocol (type: `safe-wallet`) -- 6 on-chain read actions + event triggers +- `safe` plugin (type: `safe`) -- 1 off-chain API action (`get-pending-transactions`) + +Radix Select does not support duplicate `value` props. Both register as `value="Safe"`, causing the second integration's actions to collide with the first. Result: "Get Modules Paginated" disappears from the action dropdown. + +## Solution + +Inject the `safe` plugin's `get-pending-transactions` action into the `safe-wallet` protocol plugin at registration time. The `safe` plugin continues to exist for credential management (API key) but registers with zero actions. + +## Changes + +### 1. `keeperhub/plugins/safe/index.ts` + +- Import `getIntegration` from `@/plugins/registry` +- Find `safe-wallet` plugin via `getIntegration("safe-wallet")` +- Push `get-pending-transactions` action with `credentialIntegrationType: "safe"` so the connection picker uses the Safe API key +- Call `registerIntegration` on the `safe` plugin with `actions: []` + +### 2. `keeperhub/plugins/safe/steps/get-pending-transactions.ts` + +- Change `_integrationType` from `"safe"` to `"safe-wallet"` (line 295) +- Ensures `pnpm discover-plugins` generates `safe-wallet/get-pending-transactions` in the step registry + +### 3. `plugins/legacy-mappings.ts` + +- Add `"safe/get-pending-transactions": "safe-wallet/get-pending-transactions"` +- Existing workflows referencing the old ID continue to resolve in `findActionById` + +### 4. `lib/workflow-executor.workflow.ts` + +- After `getStepImporter(actionType)` returns undefined, check `LEGACY_ACTION_MAPPINGS` +- If found, retry with the mapped action type +- Ensures old workflows still execute after the step registry regeneration + +### 5. `components/workflow/config/action-config.tsx` + +- Filter `integrations` in the Service dropdown to exclude those with `actions.length === 0` +- Prevents the credential-only `safe` integration from appearing as a duplicate entry + +### 6. `pnpm discover-plugins` + +- Regenerates `lib/step-registry.ts` with `safe-wallet/get-pending-transactions` +- Regenerates `lib/types/integration.ts` (both types remain) + +## Backward Compatibility + +| Scenario | Action Type | Resolution | +|---|---|---| +| Old workflows | `safe/get-pending-transactions` | Legacy mapping in `findActionById` + executor fallback | +| New workflows | `safe-wallet/get-pending-transactions` | Direct step registry lookup | +| Safe credentials | type `safe` | Still registered, `credentialIntegrationType: "safe"` on injected action | + +## Verification + +1. Open "Module Installation Alert" workflow +2. Click "Read Modules" node -- one "Safe" entry in Service dropdown +3. Action dropdown shows all 7 actions including Get Modules Paginated and Get Pending Transactions +4. Select Get Pending Transactions -- connection picker shows Safe API key +5. Open "Safe Signing Alert" (uses old `safe/get-pending-transactions`) -- loads and executes correctly +6. `pnpm check && pnpm type-check` passes From 61d97ffc96d850ed7dadaa5c73415d6815ee5ff2 Mon Sep 17 00:00:00 2001 From: taitsengstock Date: Tue, 24 Feb 2026 16:08:49 +1100 Subject: [PATCH 2/3] refactor: rename safe-wallet protocol to safe Unify the safe-wallet protocol and safe plugin under a single "safe" integration type. The safe plugin now injects its get-pending-transactions action + credentials into the protocol-registered integration instead of registering a separate one. - Rename protocol slug, files, icon, and test - Add backward-compat legacy mappings for safe-wallet/* step IDs - Fix discover-plugins: register protocols before plugin imports, route non-protocol steps correctly under protocol integrations - Add DB migration to update existing workflow nodes - Merge safe-wallet docs into safe docs --- .claude/commands/add-protocol.md | 2 +- components/workflow/config/action-config.tsx | 8 +- docs/plugins/_meta.ts | 1 - docs/plugins/overview.md | 3 +- docs/plugins/safe-wallet.md | 165 ---------------- docs/plugins/safe.md | 178 +++++++++++++++++- drizzle/0024_rename-safe-wallet-to-safe.sql | 69 +++++++ drizzle/meta/_journal.json | 7 + keeperhub/plugins/safe/index.ts | 49 ++--- keeperhub/protocols/index.ts | 8 +- .../protocols/{safe-wallet.ts => safe.ts} | 4 +- lib/types/integration.ts | 3 +- lib/workflow-executor.workflow.ts | 2 +- plugins/legacy-mappings.ts | 11 +- .../protocols/{safe-wallet.png => safe.png} | Bin scripts/discover-plugins.ts | 16 +- ...e-wallet.test.ts => protocol-safe.test.ts} | 60 +++--- 17 files changed, 323 insertions(+), 263 deletions(-) delete mode 100644 docs/plugins/safe-wallet.md create mode 100644 drizzle/0024_rename-safe-wallet-to-safe.sql rename keeperhub/protocols/{safe-wallet.ts => safe.ts} (99%) rename public/protocols/{safe-wallet.png => safe.png} (100%) rename tests/unit/{protocol-safe-wallet.test.ts => protocol-safe.test.ts} (65%) diff --git a/.claude/commands/add-protocol.md b/.claude/commands/add-protocol.md index 139915926..94664455f 100644 --- a/.claude/commands/add-protocol.md +++ b/.claude/commands/add-protocol.md @@ -111,7 +111,7 @@ USER-SPECIFIED ADDRESS CONTRACTS: - Address format validation is skipped for these contracts (the reference addresses are informational) - At build time, `buildConfigFieldsFromAction()` automatically inserts a `contractAddress` template-input field at the top of the action config - At runtime, `protocol-read.ts` and `protocol-write.ts` read `input.contractAddress` instead of looking up from the fixed addresses map -- Reference: `keeperhub/protocols/safe-wallet.ts` (canonical example) +- Reference: `keeperhub/protocols/safe.ts` (canonical example) ICON HANDLING: - Icon is fully optional -- if omitted, a default square protocol icon (`ProtocolIcon`) is displayed everywhere (Hub, workflow builder, etc.) diff --git a/components/workflow/config/action-config.tsx b/components/workflow/config/action-config.tsx index ab38cc9a3..3962e4967 100644 --- a/components/workflow/config/action-config.tsx +++ b/components/workflow/config/action-config.tsx @@ -582,8 +582,7 @@ function useCategoryData() { for (const [category, actions] of Object.entries(pluginCategories)) { // start custom keeperhub code // // Deduplicate by slug within each category. When the same action is - // registered under two integrations (e.g. safe and safe-wallet both - // contribute "get-pending-transactions"), keep the first occurrence. + // registered under two integrations, keep the first occurrence. const seen = new Set(); allCategories[category] = actions .filter((a) => { @@ -650,9 +649,8 @@ export function ActionConfig({ const categories = useCategoryData(); // start custom keeperhub code // // Deduplicate integrations by label for the Service dropdown. - // When two integrations share a label (e.g. "safe" and "safe-wallet" both - // labelled "Safe"), keep the one with more actions to avoid Radix Select - // duplicate-value collisions and present a single unified entry. + // When two integrations share a label, keep the one with more actions + // to avoid Radix Select duplicate-value collisions. const integrations = useMemo(() => { const all = getAllIntegrations(); const byLabel = new Map(); diff --git a/docs/plugins/_meta.ts b/docs/plugins/_meta.ts index b78b3c72f..b4344849f 100644 --- a/docs/plugins/_meta.ts +++ b/docs/plugins/_meta.ts @@ -4,7 +4,6 @@ export default { code: "Code", math: "Math", safe: "Safe", - "safe-wallet": "Safe Wallet", ajna: "Ajna", sky: "Sky", discord: "Discord", diff --git a/docs/plugins/overview.md b/docs/plugins/overview.md index 095a5e82d..3f34013dc 100644 --- a/docs/plugins/overview.md +++ b/docs/plugins/overview.md @@ -14,8 +14,7 @@ Plugins provide the actions available in your workflows. Each plugin adds one or | [Web3](/plugins/web3) | Blockchain | Balance checks, contract reads/writes, transfers, calldata decoding, risk assessment | Wallet (for writes) | | [Code](/plugins/code) | Code | Execute custom JavaScript in a sandboxed VM | None | | [Math](/plugins/math) | Math | Aggregation operations (sum, count, average, median, min, max, product) | None | -| [Safe](/plugins/safe) | Security | Monitor pending Safe multisig transactions | API key | -| [Safe Wallet](/plugins/safe-wallet) | Protocol | Safe multisig owners, threshold, nonce, module status | None | +| [Safe](/plugins/safe) | Protocol | Safe multisig owners, threshold, nonce, module status, pending transactions | API key (for pending txs) | | [Ajna](/plugins/ajna) | Protocol | Liquidation keeper operations, vault rebalancing, buffer management | Wallet (for writes) | | [Sky](/plugins/sky) | Protocol | USDS savings, token balances, approvals, DAI/MKR converters | Wallet (for writes) | | [Discord](/plugins/discord) | Notifications | Send messages to channels | Webhook URL | diff --git a/docs/plugins/safe-wallet.md b/docs/plugins/safe-wallet.md deleted file mode 100644 index cd6004374..000000000 --- a/docs/plugins/safe-wallet.md +++ /dev/null @@ -1,165 +0,0 @@ ---- -title: "Safe Wallet" -description: "Read-only Safe multisig wallet actions -- owners, threshold, nonce, and module status on Ethereum, Base, Arbitrum, and Optimism." ---- - -# Safe Wallet - -Safe (formerly Gnosis Safe) is the most widely used multisig wallet on EVM chains. This plugin provides read-only actions for querying Safe multisig state: owner lists, confirmation thresholds, transaction nonces, and module status. - -Unlike other protocols with fixed contract addresses, Safe wallets are deployed at user-specific addresses. You provide your Safe address when configuring the workflow action. - -Supported chains: Ethereum, Base, Arbitrum, Optimism. All actions are read-only and require no credentials. - -## Actions - -| Action | Type | Credentials | Description | -|--------|------|-------------|-------------| -| Get Owners | Read | No | Get the list of owner addresses | -| Get Threshold | Read | No | Get the required confirmation count | -| Is Owner | Read | No | Check if an address is an owner | -| Get Nonce | Read | No | Get the current transaction nonce | -| Is Module Enabled | Read | No | Check if a module is enabled | - ---- - -## Get Owners - -Get the list of all owner addresses for a Safe multisig wallet. - -**Inputs:** - -| Input | Type | Description | -|-------|------|-------------| -| contractAddress | address | Safe Multisig Address | - -**Outputs:** - -| Output | Type | Description | -|--------|------|-------------| -| owners | address[] | Owner Addresses | - -**When to use:** Monitor ownership changes, verify signer lists, audit multisig configuration. - ---- - -## Get Threshold - -Get the number of required confirmations (M of N) for executing a Safe transaction. - -**Inputs:** - -| Input | Type | Description | -|-------|------|-------------| -| contractAddress | address | Safe Multisig Address | - -**Outputs:** - -| Output | Type | Description | -|--------|------|-------------| -| threshold | uint256 | Required Confirmations | - -**When to use:** Monitor threshold changes, verify security settings, alert if threshold drops below expected value. - ---- - -## Is Owner - -Check whether a specific address is an owner of the Safe multisig. - -**Inputs:** - -| Input | Type | Description | -|-------|------|-------------| -| contractAddress | address | Safe Multisig Address | -| owner | address | Address to Check | - -**Outputs:** - -| Output | Type | Description | -|--------|------|-------------| -| isOwner | bool | Is Owner | - -**When to use:** Verify address membership, monitor owner additions/removals, validate access control. - ---- - -## Get Nonce - -Get the current transaction nonce of the Safe multisig. Each executed transaction increments the nonce. - -**Inputs:** - -| Input | Type | Description | -|-------|------|-------------| -| contractAddress | address | Safe Multisig Address | - -**Outputs:** - -| Output | Type | Description | -|--------|------|-------------| -| nonce | uint256 | Current Nonce | - -**When to use:** Track transaction activity, detect new executed transactions, monitor Safe usage frequency. - ---- - -## Is Module Enabled - -Check whether a specific module is enabled on the Safe multisig. Modules can execute transactions without owner confirmations. - -**Inputs:** - -| Input | Type | Description | -|-------|------|-------------| -| contractAddress | address | Safe Multisig Address | -| module | address | Module Address | - -**Outputs:** - -| Output | Type | Description | -|--------|------|-------------| -| isEnabled | bool | Module Enabled | - -**When to use:** Audit enabled modules, verify module installation, monitor module changes for security. - ---- - -## Example Workflows - -### Monitor Safe Ownership Changes - -`Schedule (hourly) -> Safe: Get Owners -> Code (compare with previous) -> Condition (changed) -> Discord: Send Message` - -Periodically check the owner list and alert via Discord if any owners are added or removed. - -### Threshold Security Alert - -`Schedule (daily) -> Safe: Get Threshold -> Condition (< 2) -> SendGrid: Send Email` - -Monitor the confirmation threshold and send an email alert if it drops below a safe minimum. - -### Transaction Activity Tracker - -`Schedule (every 10 min) -> Safe: Get Nonce -> Condition (> previous nonce) -> Discord: Send Message` - -Track the Safe nonce to detect newly executed transactions and notify your team in real time. - -### Module Audit - -`Manual -> Safe: Is Module Enabled -> Condition (is true) -> Discord: Send Message` - -Check if a specific module is enabled on your Safe and alert if unexpected modules are found. - ---- - -## Supported Chains - -| Chain | Available | -|-------|-----------| -| Ethereum (1) | Safe Multisig | -| Base (8453) | Safe Multisig | -| Arbitrum (42161) | Safe Multisig | -| Optimism (10) | Safe Multisig | - -Safe wallets are deployed at unique, user-specified addresses on all four chains. Provide your Safe address when configuring each action. diff --git a/docs/plugins/safe.md b/docs/plugins/safe.md index ca8e8f5b7..65e0a34eb 100644 --- a/docs/plugins/safe.md +++ b/docs/plugins/safe.md @@ -1,25 +1,143 @@ --- -title: "Safe Plugin" -description: "Monitor pending Safe multisig transactions and verify them before signing." +title: "Safe" +description: "Safe multisig wallet actions -- read owners, threshold, nonce, module status, and monitor pending transactions." --- -# Safe Plugin +# Safe -Fetch pending transactions from Safe (formerly Gnosis Safe) multisig wallets. Use with existing Web3 decode and risk assessment actions to verify transactions before signing -- preventing attacks like the Bybit hack where malicious calldata was substituted into a legitimate-looking transaction. +Safe (formerly Gnosis Safe) is the most widely used multisig wallet on EVM chains. This plugin provides read-only on-chain actions for querying Safe multisig state (owner lists, confirmation thresholds, transaction nonces, module status) and off-chain actions for monitoring pending transactions via the Safe Transaction Service API. -## Actions +Unlike other protocols with fixed contract addresses, Safe wallets are deployed at user-specific addresses. You provide your Safe address when configuring the workflow action. -| Action | Description | -|--------|-------------| -| Get Pending Transactions | Fetch unexecuted multisig transactions from a Safe, optionally filtered by signer | +Supported chains for on-chain reads: Ethereum, Base, Arbitrum, Optimism. Pending transaction monitoring supports: Ethereum, Arbitrum, Optimism, Polygon, Base, BSC, Avalanche, Gnosis, Sepolia, Base Sepolia. ## Setup +On-chain read actions (Get Owners, Get Threshold, etc.) require no credentials. + +For **Get Pending Transactions**, you need a Safe Transaction Service API key: + 1. Go to [developer.safe.global](https://developer.safe.global/) and create an API project 2. Copy the JWT API key 3. In KeeperHub, go to **Connections > Add Connection > Safe** 4. Paste the API key and save +## Actions + +| Action | Type | Credentials | Description | +|--------|------|-------------|-------------| +| Get Owners | Read | No | Get the list of owner addresses | +| Get Threshold | Read | No | Get the required confirmation count | +| Is Owner | Read | No | Check if an address is an owner | +| Get Nonce | Read | No | Get the current transaction nonce | +| Is Module Enabled | Read | No | Check if a module is enabled | +| Get Modules Paginated | Read | No | Get paginated list of enabled modules | +| Get Pending Transactions | API | API key | Fetch unexecuted multisig transactions | + +--- + +## Get Owners + +Get the list of all owner addresses for a Safe multisig wallet. + +**Inputs:** + +| Input | Type | Description | +|-------|------|-------------| +| contractAddress | address | Safe Multisig Address | + +**Outputs:** + +| Output | Type | Description | +|--------|------|-------------| +| owners | address[] | Owner Addresses | + +**When to use:** Monitor ownership changes, verify signer lists, audit multisig configuration. + +--- + +## Get Threshold + +Get the number of required confirmations (M of N) for executing a Safe transaction. + +**Inputs:** + +| Input | Type | Description | +|-------|------|-------------| +| contractAddress | address | Safe Multisig Address | + +**Outputs:** + +| Output | Type | Description | +|--------|------|-------------| +| threshold | uint256 | Required Confirmations | + +**When to use:** Monitor threshold changes, verify security settings, alert if threshold drops below expected value. + +--- + +## Is Owner + +Check whether a specific address is an owner of the Safe multisig. + +**Inputs:** + +| Input | Type | Description | +|-------|------|-------------| +| contractAddress | address | Safe Multisig Address | +| owner | address | Address to Check | + +**Outputs:** + +| Output | Type | Description | +|--------|------|-------------| +| isOwner | bool | Is Owner | + +**When to use:** Verify address membership, monitor owner additions/removals, validate access control. + +--- + +## Get Nonce + +Get the current transaction nonce of the Safe multisig. Each executed transaction increments the nonce. + +**Inputs:** + +| Input | Type | Description | +|-------|------|-------------| +| contractAddress | address | Safe Multisig Address | + +**Outputs:** + +| Output | Type | Description | +|--------|------|-------------| +| nonce | uint256 | Current Nonce | + +**When to use:** Track transaction activity, detect new executed transactions, monitor Safe usage frequency. + +--- + +## Is Module Enabled + +Check whether a specific module is enabled on the Safe multisig. Modules can execute transactions without owner confirmations. + +**Inputs:** + +| Input | Type | Description | +|-------|------|-------------| +| contractAddress | address | Safe Multisig Address | +| module | address | Module Address | + +**Outputs:** + +| Output | Type | Description | +|--------|------|-------------| +| isEnabled | bool | Module Enabled | + +**When to use:** Audit enabled modules, verify module installation, monitor module changes for security. + +--- + ## Get Pending Transactions Fetch pending multisig transactions that have not been executed yet. Optionally filter for transactions a specific signer has not confirmed. @@ -32,9 +150,30 @@ Each transaction includes: `safeTxHash`, `to`, `value`, `data`, `operation` (0=C **When to use:** Monitor your Safe for new transactions awaiting your signature, verify transaction calldata before signing, detect suspicious proposals (DELEGATECALL, proxy upgrades, unknown targets). -**Supported networks:** Ethereum, Arbitrum, Optimism, Polygon, Base, BSC, Avalanche, Gnosis, Sepolia, Base Sepolia +--- + +## Example Workflows + +### Monitor Safe Ownership Changes + +`Schedule (hourly) -> Safe: Get Owners -> Code (compare with previous) -> Condition (changed) -> Discord: Send Message` + +Periodically check the owner list and alert via Discord if any owners are added or removed. + +### Threshold Security Alert + +`Schedule (daily) -> Safe: Get Threshold -> Condition (< 2) -> SendGrid: Send Email` + +Monitor the confirmation threshold and send an email alert if it drops below a safe minimum. + +### Transaction Activity Tracker + +`Schedule (every 10 min) -> Safe: Get Nonce -> Condition (> previous nonce) -> Discord: Send Message` + +Track the Safe nonce to detect newly executed transactions and notify your team in real time. + +### Pending Transaction Verification -**Example workflow:** ``` Schedule (every 5 min) -> Safe: Get Pending Transactions (signer = your address) @@ -44,3 +183,22 @@ Schedule (every 5 min) -> Condition: operation == 1 (DELEGATECALL) OR riskScore > 70 -> Discord: "Suspicious Safe tx: {{DecodeCalldata.functionName}} on {{GetPendingTransactions.transactions.to}}" ``` + +--- + +## Supported Chains + +| Chain | On-chain Reads | Pending Transactions | +|-------|---------------|---------------------| +| Ethereum (1) | Yes | Yes | +| Base (8453) | Yes | Yes | +| Arbitrum (42161) | Yes | Yes | +| Optimism (10) | Yes | Yes | +| Polygon | No | Yes | +| BSC | No | Yes | +| Avalanche | No | Yes | +| Gnosis | No | Yes | +| Sepolia | No | Yes | +| Base Sepolia | No | Yes | + +Safe wallets are deployed at unique, user-specified addresses on all chains. Provide your Safe address when configuring each action. diff --git a/drizzle/0024_rename-safe-wallet-to-safe.sql b/drizzle/0024_rename-safe-wallet-to-safe.sql new file mode 100644 index 000000000..aadec0579 --- /dev/null +++ b/drizzle/0024_rename-safe-wallet-to-safe.sql @@ -0,0 +1,69 @@ +-- Rename safe-wallet protocol to safe in all workflow node configurations. +-- Updates actionType fields (e.g. "safe-wallet/get-owners" -> "safe/get-owners"), +-- _protocolMeta.protocolSlug, and event trigger _eventProtocolSlug / _eventProtocolIconPath. + +-- 1. Update actionType in action nodes from "safe-wallet/*" to "safe/*" +UPDATE "workflow" +SET "nodes" = ( + SELECT jsonb_agg( + CASE + WHEN node->>'actionType' IS NOT NULL + AND node->>'actionType' LIKE 'safe-wallet/%' + THEN jsonb_set( + node, + '{actionType}', + to_jsonb(replace(node->>'actionType', 'safe-wallet/', 'safe/')) + ) + ELSE node + END + ) + FROM jsonb_array_elements("nodes") AS node +) +WHERE "nodes"::text LIKE '%safe-wallet/%'; + +-- 2. Update _protocolMeta.protocolSlug from "safe-wallet" to "safe" in action node data +UPDATE "workflow" +SET "nodes" = ( + SELECT jsonb_agg( + CASE + WHEN node->'data'->'_protocolMeta'->>'protocolSlug' = 'safe-wallet' + THEN jsonb_set( + node, + '{data,_protocolMeta,protocolSlug}', + '"safe"' + ) + ELSE node + END + ) + FROM jsonb_array_elements("nodes") AS node +) +WHERE "nodes"::text LIKE '%"protocolSlug":"safe-wallet"%' + OR "nodes"::text LIKE '%"protocolSlug": "safe-wallet"%'; + +-- 3. Update _eventProtocolSlug in trigger node data from "safe-wallet" to "safe" +UPDATE "workflow" +SET "nodes" = ( + SELECT jsonb_agg( + CASE + WHEN node->'data'->>'_eventProtocolSlug' = 'safe-wallet' + THEN jsonb_set( + jsonb_set( + node, + '{data,_eventProtocolSlug}', + '"safe"' + ), + '{data,_eventProtocolIconPath}', + '"/protocols/safe.png"' + ) + ELSE node + END + ) + FROM jsonb_array_elements("nodes") AS node +) +WHERE "nodes"::text LIKE '%"_eventProtocolSlug":"safe-wallet"%' + OR "nodes"::text LIKE '%"_eventProtocolSlug": "safe-wallet"%'; + +-- 4. Update featured_protocol column from "safe-wallet" to "safe" +UPDATE "workflow" +SET "featured_protocol" = 'safe' +WHERE "featured_protocol" = 'safe-wallet'; diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index c0d7eef64..1cbdeaff6 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -169,6 +169,13 @@ "when": 1771831464086, "tag": "0023_public_stick", "breakpoints": true + }, + { + "idx": 24, + "version": "7", + "when": 1772006400000, + "tag": "0024_rename-safe-wallet-to-safe", + "breakpoints": true } ] } \ No newline at end of file diff --git a/keeperhub/plugins/safe/index.ts b/keeperhub/plugins/safe/index.ts index 2926f95b5..98173549a 100644 --- a/keeperhub/plugins/safe/index.ts +++ b/keeperhub/plugins/safe/index.ts @@ -1,7 +1,5 @@ import type { IntegrationType } from "@/lib/types/integration"; -import type { IntegrationPlugin } from "@/plugins/registry"; -import { getIntegration, registerIntegration } from "@/plugins/registry"; -import { SafeIcon } from "./icon"; +import { getIntegration } from "@/plugins/registry"; const getPendingTransactionsAction = { slug: "get-pending-transactions", @@ -58,26 +56,14 @@ const getPendingTransactionsAction = { ], }; -// Inject get-pending-transactions into the safe-wallet protocol plugin +// Inject get-pending-transactions into the protocol-registered "safe" integration // so both on-chain reads and off-chain API actions appear under one "Safe" entry. -const safeWallet = getIntegration("safe-wallet" as IntegrationType); -if (safeWallet) { - safeWallet.actions.push(getPendingTransactionsAction); -} - -// Safe keeps its action for step registry generation (safe/get-pending-transactions). -// A legacy mapping alias creates the safe-wallet/get-pending-transactions entry. -// formFields/testConfig remain so the connection management UI can create credentials. -const safePlugin: IntegrationPlugin = { - type: "safe", - label: "Safe", - description: "Monitor and verify pending Safe multisig transactions", - - icon: SafeIcon, - - requiresCredentials: true, - - formFields: [ +// Also attach credential fields so the connection management UI can create Safe API keys. +const safeProtocol = getIntegration("safe" as IntegrationType); +if (safeProtocol) { + safeProtocol.actions.push(getPendingTransactionsAction); + safeProtocol.requiresCredentials = true; + safeProtocol.formFields = [ { id: "apiKey", label: "API Key", @@ -92,18 +78,17 @@ const safePlugin: IntegrationPlugin = { url: "https://developer.safe.global/", }, }, - ], - - testConfig: { + ]; + safeProtocol.testConfig = { getTestFunction: async () => { const { testSafe } = await import("./test"); return testSafe; }, - }, - - actions: [getPendingTransactionsAction], -}; - -registerIntegration(safePlugin); + }; +} -export default safePlugin; +// Export the enriched protocol integration for use by other modules. +// No separate registerIntegration call -- the protocol-registered "safe" +// integration already has everything (protocol actions + injected action + +// credential config). +export default safeProtocol; diff --git a/keeperhub/protocols/index.ts b/keeperhub/protocols/index.ts index ca48ec302..0145bb4ce 100644 --- a/keeperhub/protocols/index.ts +++ b/keeperhub/protocols/index.ts @@ -8,7 +8,7 @@ * This ensures the protocol registry is populated when the Next.js * server starts (via the plugin import chain). * - * Registered protocols: ajna, safe-wallet, sky, weth + * Registered protocols: ajna, safe, sky, weth */ import { @@ -18,14 +18,14 @@ import { import { registerIntegration } from "@/plugins/registry"; import ajnaDef from "./ajna"; -import safeWalletDef from "./safe-wallet"; +import safeDef from "./safe"; import skyDef from "./sky"; import wethDef from "./weth"; registerProtocol(ajnaDef); registerIntegration(protocolToPlugin(ajnaDef)); -registerProtocol(safeWalletDef); -registerIntegration(protocolToPlugin(safeWalletDef)); +registerProtocol(safeDef); +registerIntegration(protocolToPlugin(safeDef)); registerProtocol(skyDef); registerIntegration(protocolToPlugin(skyDef)); registerProtocol(wethDef); diff --git a/keeperhub/protocols/safe-wallet.ts b/keeperhub/protocols/safe.ts similarity index 99% rename from keeperhub/protocols/safe-wallet.ts rename to keeperhub/protocols/safe.ts index e5c0f6eb6..a809e5e7e 100644 --- a/keeperhub/protocols/safe-wallet.ts +++ b/keeperhub/protocols/safe.ts @@ -2,11 +2,11 @@ import { defineProtocol } from "@/keeperhub/lib/protocol-registry"; export default defineProtocol({ name: "Safe", - slug: "safe-wallet", + slug: "safe", description: "Safe multisig wallet -- read owners, threshold, nonce, and module status for any Safe address", website: "https://safe.global", - icon: "/protocols/safe-wallet.png", + icon: "/protocols/safe.png", contracts: { safe: { diff --git a/lib/types/integration.ts b/lib/types/integration.ts index 351ebcde9..8f66cb97f 100755 --- a/lib/types/integration.ts +++ b/lib/types/integration.ts @@ -9,7 +9,7 @@ * 2. Add a system integration to SYSTEM_INTEGRATION_TYPES in discover-plugins.ts * 3. Run: pnpm discover-plugins * - * Generated types: ai-gateway, ajna, clerk, code, database, discord, linear, math, protocol, resend, safe, safe-wallet, sendgrid, sky, slack, telegram, v0, web3, webflow, webhook, weth + * Generated types: ai-gateway, ajna, clerk, code, database, discord, linear, math, protocol, resend, safe, sendgrid, sky, slack, telegram, v0, web3, webflow, webhook, weth */ // Integration type union - plugins + system integrations @@ -25,7 +25,6 @@ export type IntegrationType = | "protocol" | "resend" | "safe" - | "safe-wallet" | "sendgrid" | "sky" | "slack" diff --git a/lib/workflow-executor.workflow.ts b/lib/workflow-executor.workflow.ts index 2f3078ed7..6c46e58ac 100644 --- a/lib/workflow-executor.workflow.ts +++ b/lib/workflow-executor.workflow.ts @@ -366,7 +366,7 @@ async function executeActionStep(input: { // Look up plugin action from the generated step registry let stepImporter = getStepImporter(actionType); - // Fallback: check legacy action mappings (e.g. "safe/get-pending-transactions" -> "safe-wallet/get-pending-transactions") + // Fallback: check legacy action mappings (e.g. "safe-wallet/get-owners" -> "safe/get-owners") if (!stepImporter) { const mapped = LEGACY_ACTION_MAPPINGS[actionType]; if (mapped) { diff --git a/plugins/legacy-mappings.ts b/plugins/legacy-mappings.ts index 9d2f8c675..3689a5c5b 100644 --- a/plugins/legacy-mappings.ts +++ b/plugins/legacy-mappings.ts @@ -31,9 +31,14 @@ export const LEGACY_ACTION_MAPPINGS: Record = { "Create Chat": "v0/create-chat", "Send Message": "v0/send-message", - // Safe: the get-pending-transactions action lives in the safe plugin but is - // injected into safe-wallet at runtime. This alias ensures the step registry - // has an entry for safe-wallet/get-pending-transactions. + // Safe: backward compatibility for workflows created before the safe-wallet + // protocol was renamed to safe. + "safe-wallet/get-owners": "safe/get-owners", + "safe-wallet/get-threshold": "safe/get-threshold", + "safe-wallet/is-owner": "safe/is-owner", + "safe-wallet/get-nonce": "safe/get-nonce", + "safe-wallet/is-module-enabled": "safe/is-module-enabled", + "safe-wallet/get-modules-paginated": "safe/get-modules-paginated", "safe-wallet/get-pending-transactions": "safe/get-pending-transactions", // Web3 diff --git a/public/protocols/safe-wallet.png b/public/protocols/safe.png similarity index 100% rename from public/protocols/safe-wallet.png rename to public/protocols/safe.png diff --git a/scripts/discover-plugins.ts b/scripts/discover-plugins.ts index 143f34bef..18eb73f11 100644 --- a/scripts/discover-plugins.ts +++ b/scripts/discover-plugins.ts @@ -1024,8 +1024,13 @@ async function generateStepRegistry(): Promise { .flatMap(({ actionId, integration, stepImportPath, stepFunction }) => { // Protocol plugins are virtual -- step files live in keeperhub/plugins/protocol/steps/ // regardless of which protocol they serve (e.g. weth, aave, etc.) + // Actions injected from non-protocol plugins (e.g. safe/get-pending-transactions) + // use their own step files, detected by stepImportPath not starting with "protocol-". let importPath: string; - if (protocolSlugSet.has(integration)) { + const isProtocolStep = + protocolSlugSet.has(integration) && + stepImportPath.startsWith("protocol-"); + if (isProtocolStep) { importPath = `@/keeperhub/plugins/protocol/steps/${stepImportPath}`; } else { const importBase = keeperhubPluginSet.has(integration) @@ -1255,9 +1260,9 @@ async function main(): Promise { console.log("Generating keeperhub/plugins/index.ts..."); generateKeeperHubIndexFile(keeperhub.enabled); // Only import enabled KeeperHub plugins - console.log("Updating README.md..."); - await updateReadme(); - + // Register protocols BEFORE importing plugins so that plugins that inject + // actions into protocol integrations (e.g. safe plugin -> safe protocol) + // can find the integration in the registry at import time. console.log("Registering protocol plugins..."); const protocolSlugs = await registerProtocolPlugins(); console.log(`Registered ${protocolSlugs.length} protocol(s)`); @@ -1265,6 +1270,9 @@ async function main(): Promise { console.log("Generating keeperhub/protocols/index.ts..."); generateProtocolsIndexFile(); + console.log("Updating README.md..."); + await updateReadme(); + console.log("\nGenerating lib/types/integration.ts..."); // Use all plugins for types (both base, keeperhub, and protocol slugs) generateTypesFile(base.all, keeperhub.all, protocolSlugs); diff --git a/tests/unit/protocol-safe-wallet.test.ts b/tests/unit/protocol-safe.test.ts similarity index 65% rename from tests/unit/protocol-safe-wallet.test.ts rename to tests/unit/protocol-safe.test.ts index 3a420ae77..2235dc03c 100644 --- a/tests/unit/protocol-safe-wallet.test.ts +++ b/tests/unit/protocol-safe.test.ts @@ -4,30 +4,30 @@ import { getProtocol, registerProtocol, } from "@/keeperhub/lib/protocol-registry"; -import safeWalletDef from "@/keeperhub/protocols/safe-wallet"; +import safeDef from "@/keeperhub/protocols/safe"; const KEBAB_CASE_REGEX = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/; -describe("Safe Wallet Protocol Definition", () => { +describe("Safe Protocol Definition", () => { it("imports without throwing", () => { - expect(safeWalletDef).toBeDefined(); - expect(safeWalletDef.name).toBe("Safe"); - expect(safeWalletDef.slug).toBe("safe-wallet"); + expect(safeDef).toBeDefined(); + expect(safeDef.name).toBe("Safe"); + expect(safeDef.slug).toBe("safe"); }); it("protocol slug is valid kebab-case", () => { - expect(safeWalletDef.slug).toMatch(KEBAB_CASE_REGEX); + expect(safeDef.slug).toMatch(KEBAB_CASE_REGEX); }); it("all action slugs are valid kebab-case", () => { - for (const action of safeWalletDef.actions) { + for (const action of safeDef.actions) { expect(action.slug).toMatch(KEBAB_CASE_REGEX); } }); it("every action references an existing contract", () => { - const contractKeys = new Set(Object.keys(safeWalletDef.contracts)); - for (const action of safeWalletDef.actions) { + const contractKeys = new Set(Object.keys(safeDef.contracts)); + for (const action of safeDef.actions) { expect( contractKeys.has(action.contract), `action "${action.slug}" references unknown contract "${action.contract}"` @@ -36,13 +36,13 @@ describe("Safe Wallet Protocol Definition", () => { }); it("has no duplicate action slugs", () => { - const slugs = safeWalletDef.actions.map((a) => a.slug); + const slugs = safeDef.actions.map((a) => a.slug); const uniqueSlugs = new Set(slugs); expect(slugs.length).toBe(uniqueSlugs.size); }); it("all read actions define outputs", () => { - const readActions = safeWalletDef.actions.filter((a) => a.type === "read"); + const readActions = safeDef.actions.filter((a) => a.type === "read"); for (const action of readActions) { expect( action.outputs, @@ -56,8 +56,8 @@ describe("Safe Wallet Protocol Definition", () => { }); it("each action's contract has at least one chain address", () => { - for (const action of safeWalletDef.actions) { - const contract = safeWalletDef.contracts[action.contract]; + for (const action of safeDef.actions) { + const contract = safeDef.contracts[action.contract]; expect(contract).toBeDefined(); expect( Object.keys(contract.addresses).length, @@ -67,28 +67,26 @@ describe("Safe Wallet Protocol Definition", () => { }); it("has exactly 6 actions", () => { - expect(safeWalletDef.actions).toHaveLength(6); + expect(safeDef.actions).toHaveLength(6); }); it("has 6 read actions and 0 write actions", () => { - const readActions = safeWalletDef.actions.filter((a) => a.type === "read"); - const writeActions = safeWalletDef.actions.filter( - (a) => a.type === "write" - ); + const readActions = safeDef.actions.filter((a) => a.type === "read"); + const writeActions = safeDef.actions.filter((a) => a.type === "write"); expect(readActions).toHaveLength(6); expect(writeActions).toHaveLength(0); }); it("has 1 contract", () => { - expect(Object.keys(safeWalletDef.contracts)).toHaveLength(1); + expect(Object.keys(safeDef.contracts)).toHaveLength(1); }); it("safe contract has userSpecifiedAddress enabled", () => { - expect(safeWalletDef.contracts.safe.userSpecifiedAddress).toBe(true); + expect(safeDef.contracts.safe.userSpecifiedAddress).toBe(true); }); it("safe contract is available on 4 chains", () => { - const chains = Object.keys(safeWalletDef.contracts.safe.addresses); + const chains = Object.keys(safeDef.contracts.safe.addresses); expect(chains).toHaveLength(4); expect(chains).toContain("1"); expect(chains).toContain("8453"); @@ -97,28 +95,28 @@ describe("Safe Wallet Protocol Definition", () => { }); it("registers in the protocol registry and is retrievable", () => { - registerProtocol(safeWalletDef); - const retrieved = getProtocol("safe-wallet"); + registerProtocol(safeDef); + const retrieved = getProtocol("safe"); expect(retrieved).toBeDefined(); - expect(retrieved?.slug).toBe("safe-wallet"); + expect(retrieved?.slug).toBe("safe"); expect(retrieved?.name).toBe("Safe"); }); it("has 12 events", () => { - expect(safeWalletDef.events).toBeDefined(); - expect(safeWalletDef.events).toHaveLength(12); + expect(safeDef.events).toBeDefined(); + expect(safeDef.events).toHaveLength(12); }); it("all event slugs are valid kebab-case", () => { - const events = safeWalletDef.events ?? []; + const events = safeDef.events ?? []; for (const event of events) { expect(event.slug).toMatch(KEBAB_CASE_REGEX); } }); it("every event references an existing contract", () => { - const contractKeys = new Set(Object.keys(safeWalletDef.contracts)); - const events = safeWalletDef.events ?? []; + const contractKeys = new Set(Object.keys(safeDef.contracts)); + const events = safeDef.events ?? []; for (const event of events) { expect( contractKeys.has(event.contract), @@ -128,13 +126,13 @@ describe("Safe Wallet Protocol Definition", () => { }); it("has no duplicate event slugs", () => { - const slugs = (safeWalletDef.events ?? []).map((e) => e.slug); + const slugs = (safeDef.events ?? []).map((e) => e.slug); const uniqueSlugs = new Set(slugs); expect(slugs.length).toBe(uniqueSlugs.size); }); it("buildEventAbiFragment produces valid JSON with correct structure", () => { - const events = safeWalletDef.events ?? []; + const events = safeDef.events ?? []; const event = events[0]; expect(event).toBeDefined(); const fragment = buildEventAbiFragment(event); From 6fde01d5a7406e32bb63726b2b7705ae41b33ce7 Mon Sep 17 00:00:00 2001 From: Jacob Sussmilch Date: Tue, 24 Feb 2026 16:36:33 +1100 Subject: [PATCH 3/3] fix: KEEP-1494 correct table name in safe-wallet rename migration Migration 0024 referenced "workflow" (singular) but the actual table is "workflows" (plural), causing all UPDATE statements to silently match zero rows. --- drizzle/0024_rename-safe-wallet-to-safe.sql | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/drizzle/0024_rename-safe-wallet-to-safe.sql b/drizzle/0024_rename-safe-wallet-to-safe.sql index aadec0579..4aa17b508 100644 --- a/drizzle/0024_rename-safe-wallet-to-safe.sql +++ b/drizzle/0024_rename-safe-wallet-to-safe.sql @@ -3,7 +3,7 @@ -- _protocolMeta.protocolSlug, and event trigger _eventProtocolSlug / _eventProtocolIconPath. -- 1. Update actionType in action nodes from "safe-wallet/*" to "safe/*" -UPDATE "workflow" +UPDATE "workflows" SET "nodes" = ( SELECT jsonb_agg( CASE @@ -22,7 +22,7 @@ SET "nodes" = ( WHERE "nodes"::text LIKE '%safe-wallet/%'; -- 2. Update _protocolMeta.protocolSlug from "safe-wallet" to "safe" in action node data -UPDATE "workflow" +UPDATE "workflows" SET "nodes" = ( SELECT jsonb_agg( CASE @@ -41,7 +41,7 @@ WHERE "nodes"::text LIKE '%"protocolSlug":"safe-wallet"%' OR "nodes"::text LIKE '%"protocolSlug": "safe-wallet"%'; -- 3. Update _eventProtocolSlug in trigger node data from "safe-wallet" to "safe" -UPDATE "workflow" +UPDATE "workflows" SET "nodes" = ( SELECT jsonb_agg( CASE @@ -64,6 +64,6 @@ WHERE "nodes"::text LIKE '%"_eventProtocolSlug":"safe-wallet"%' OR "nodes"::text LIKE '%"_eventProtocolSlug": "safe-wallet"%'; -- 4. Update featured_protocol column from "safe-wallet" to "safe" -UPDATE "workflow" +UPDATE "workflows" SET "featured_protocol" = 'safe' WHERE "featured_protocol" = 'safe-wallet';