diff --git a/.github/workflows/propose-safe-multisig-tx.yml b/.github/workflows/propose-safe-multisig-tx.yml new file mode 100644 index 0000000..85b9f16 --- /dev/null +++ b/.github/workflows/propose-safe-multisig-tx.yml @@ -0,0 +1,78 @@ +name: 'Propose Safe Multisig Transaction' +on: + workflow_call: + inputs: + rpc-url: + description: 'RPC URL for the blockchain network' + required: true + type: string + safe-address: + description: 'Address of the Safe contract' + required: true + type: string + transaction-to: + description: 'Target address of the transaction' + required: true + type: string + transaction-value: + description: 'Value to send in the transaction (in wei, default: 0)' + required: false + default: '0' + type: string + transaction-data: + description: 'Transaction data/calldata' + required: true + type: string + secrets: + safe-proposer-private-key: + description: 'Private key of the proposer wallet' + required: true + safe-api-key: + description: 'Safe API key for transaction service' + required: true + outputs: + tx-hash: + description: 'Hash of the Safe transaction' + value: ${{ jobs.propose-transaction.outputs.tx-hash }} + tx-details: + description: 'Created transaction details' + value: ${{ jobs.propose-transaction.outputs.tx-details }} + +jobs: + propose-transaction: + runs-on: ubuntu-latest + outputs: + tx-hash: ${{ steps.safe-transaction.outputs.tx-hash }} + tx-details: ${{ steps.safe-transaction.outputs.tx-details }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + repository: iExecBlockchainComputing/github-actions-workflows + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install dependencies + run: npm ci + working-directory: ./propose-safe-multisig-tx + + - name: Build action + run: npm run build + working-directory: ./propose-safe-multisig-tx + + - name: Propose Safe Transaction + run: npm run propose + working-directory: ./propose-safe-multisig-tx + env: + RPC_URL: ${{ inputs.rpc-url }} + SAFE_ADDRESS: ${{ inputs.safe-address }} + TRANSACTION_TO: ${{ inputs.transaction-to }} + TRANSACTION_VALUE: ${{ inputs.transaction-value }} + TRANSACTION_DATA: ${{ inputs.transaction-data }} + SAFE_PROPOSER_PRIVATE_KEY: ${{ secrets.safe-proposer-private-key }} + SAFE_API_KEY: ${{ secrets.safe-api-key }} + id: safe-transaction diff --git a/README.md b/README.md index f7201a5..f6c84ee 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,9 @@ Provides a standardized workflow for building, testing, and publishing Rust pack ### ๐Ÿงน [Stale Issues and PRs](./stale) Automatically identifies and closes stale issues and pull requests to maintain a clean and focused repository. Helps your team concentrate on active work items and reduces maintenance overhead. +### ๐Ÿ›ก๏ธ [Safe Multisig Transaction Proposer](./propose-safe-multisig-tx) +Automates the process of proposing transactions to a Safe multi-signature wallet (Gnosis Safe). Features smart chain detection, comprehensive validation, and secure transaction handling for blockchain operations. + ## ๐Ÿ”ง Usage Each workflow has its own detailed documentation in its respective directory. The comprehensive documentation includes: diff --git a/propose-safe-multisig-tx/.gitignore b/propose-safe-multisig-tx/.gitignore new file mode 100644 index 0000000..cde314c --- /dev/null +++ b/propose-safe-multisig-tx/.gitignore @@ -0,0 +1,22 @@ +# Dependencies +node_modules/ + +# Build outputs +dist/ + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +Thumbs.db + +# Environment variables +.env +.env.local +.env.*.local + +# Logs +logs/ +*.log diff --git a/propose-safe-multisig-tx/CHANGELOG.md b/propose-safe-multisig-tx/CHANGELOG.md new file mode 100644 index 0000000..4792878 --- /dev/null +++ b/propose-safe-multisig-tx/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +## 1.0.0 (2025-09-29) + +### Features + +* **safe-transaction:** add workflow for safe transaction submission ([#85](https://github.com/iExecBlockchainComputing/github-actions-workflows/pull/85)) diff --git a/propose-safe-multisig-tx/README.md b/propose-safe-multisig-tx/README.md new file mode 100644 index 0000000..eb52334 --- /dev/null +++ b/propose-safe-multisig-tx/README.md @@ -0,0 +1,58 @@ +# Safe Multisig Transaction Proposer - Reusable Workflow + +## Overview ๐ŸŒŸ + +This reusable GitHub Actions workflow automates the process of proposing transactions to a Safe multi-signature wallet (Gnosis Safe). It handles the proposal submission, making it easy to integrate Safe transactions into your CI/CD pipeline. + +## Workflow Inputs ๐Ÿ› ๏ธ + +| **Input** | **Description** | **Required** | **Default** | +| ------------------------ | ------------------------------------------------------------- | ------------ | ----------------------------------- | +| **rpc-url** | RPC URL for the blockchain network | Yes | - | +| **safe-address** | Address of the Safe contract | Yes | - | +| **transaction-to** | Target address for the transaction | Yes | - | +| **transaction-value** | Value to send in the transaction (in wei) | No | `0` | +| **transaction-data** | Transaction data/calldata | Yes | - | +| **safe-proposer-private-key** | Private key of the proposer wallet | Yes (Secret) | - | +| **safe-api-key** | Safe API key for transaction service | Yes (Secret) | - | + +## Workflow Outputs ๐Ÿ“ค + +| **Output** | **Description** | +| ----------------- | ----------------------------------------- | +| **tx-hash** | Hash of the Safe transaction created | +| **tx-details** | Complete transaction details (JSON) | + +## How to Use This Reusable Workflow ๐Ÿ”„ + +1. **Call the Reusable Workflow** + In another workflow file, invoke this reusable workflow like so: + + ```yaml + name: Upgrade contract + + on: + workflow_dispatch: + + jobs: + upgrade: + uses: ./.github/workflows/propose-safe-multisig-tx.yml + secrets: + safe-proposer-private-key: ${{ secrets.SAFE_PROPOSER_PRIVATE_KEY }} + safe-api-key: ${{ secrets.SAFE_API_KEY }} + with: + rpc-url: 'https://...' + safe-address: '0xab...' + transaction-to: '0xcd...' + transaction-value: '0' + transaction-data: '0xef' # Upgrade transaction calldata + ``` + +2. **Configure Secrets** + Ensure that the required secrets are added to your repository's settings: + - `SAFE_PROPOSER_PRIVATE_KEY`: The private key of the wallet that will propose the transaction + - `SAFE_API_KEY`: Your Safe API key for the transaction service + +## Security Considerations ๐Ÿ›ก๏ธ + +โš ๏ธ **Important**: Never expose private keys in logs or code files. Always use GitHub Secrets to store sensitive information securely. diff --git a/propose-safe-multisig-tx/package-lock.json b/propose-safe-multisig-tx/package-lock.json new file mode 100644 index 0000000..c619ed8 --- /dev/null +++ b/propose-safe-multisig-tx/package-lock.json @@ -0,0 +1,559 @@ +{ + "name": "propose-safe-multisig-tx-github-action", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "propose-safe-multisig-tx-github-action", + "version": "1.0.0", + "dependencies": { + "@actions/core": "^1.10.1", + "@safe-global/api-kit": "^4.0.0", + "@safe-global/protocol-kit": "^6.1.1", + "@safe-global/types-kit": "^3.0.0", + "dotenv": "^16.3.1", + "viem": "^2.21.0", + "zod": "^3.22.4" + }, + "devDependencies": { + "@types/node": "^24.7.0", + "typescript": "^5.0.0" + } + }, + "node_modules/@actions/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", + "license": "MIT", + "dependencies": { + "@actions/exec": "^1.1.1", + "@actions/http-client": "^2.0.1" + } + }, + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "license": "MIT", + "dependencies": { + "@actions/io": "^1.0.1" + } + }, + "node_modules/@actions/http-client": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", + "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", + "license": "MIT", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^5.25.4" + } + }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", + "license": "MIT" + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT" + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.5.0.tgz", + "integrity": "sha512-YM/nFfskFJSlHqv59ed6dZlLZqtZQwjRVJ4bBAiWV08Oc+1rSd5lDZcBEx0lGDHfSoH3UziI2pXt2UM33KerPQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@safe-global/api-kit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@safe-global/api-kit/-/api-kit-4.0.0.tgz", + "integrity": "sha512-xtLLi6OXguLw8cLoYnzCxqmirzRK4sSORxaiBDXdxJfBXIZLLKvYwQyDjsPL+2W4jKlJVcSLCw5EfolJahNMYg==", + "license": "MIT", + "dependencies": { + "@safe-global/protocol-kit": "^6.1.0", + "@safe-global/types-kit": "^3.0.0", + "node-fetch": "^2.7.0", + "viem": "^2.21.8" + } + }, + "node_modules/@safe-global/protocol-kit": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@safe-global/protocol-kit/-/protocol-kit-6.1.1.tgz", + "integrity": "sha512-SlRosKB52h1CV2gMlKG4UOvh2j4tXuzz1GZ/yQ1HD0Zvm5azUlaytFwKzHun9xNVvfe+vvSNHUEGX2Umy+rQ9A==", + "license": "MIT", + "dependencies": { + "@safe-global/safe-deployments": "^1.37.42", + "@safe-global/safe-modules-deployments": "^2.2.14", + "@safe-global/types-kit": "^3.0.0", + "abitype": "^1.0.2", + "semver": "^7.7.2", + "viem": "^2.21.8" + }, + "optionalDependencies": { + "@noble/curves": "^1.6.0", + "@peculiar/asn1-schema": "^2.3.13" + } + }, + "node_modules/@safe-global/safe-deployments": { + "version": "1.37.45", + "resolved": "https://registry.npmjs.org/@safe-global/safe-deployments/-/safe-deployments-1.37.45.tgz", + "integrity": "sha512-HLH8nJSVbDlx/p3Yzhspyz9q9pITSGvw2UqlmXfAyrz6VSM8zc9xUWlBeqaUEzvmgon9YUgfstUMz2MElRUCfQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.6.2" + } + }, + "node_modules/@safe-global/safe-modules-deployments": { + "version": "2.2.17", + "resolved": "https://registry.npmjs.org/@safe-global/safe-modules-deployments/-/safe-modules-deployments-2.2.17.tgz", + "integrity": "sha512-G5VivmG0+UlTnaJgWJvkkFSQlhMzSXT40IoOTv2A134EtHoq9cs8BsCjXUErKb96KVmDguj6ku5oJmiLp6raNQ==", + "license": "MIT" + }, + "node_modules/@safe-global/types-kit": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@safe-global/types-kit/-/types-kit-3.0.0.tgz", + "integrity": "sha512-AZWIlR5MguDPdGiOj7BB4JQPY2afqmWQww1mu8m8Oi16HHBW99G01kFOu4NEHBwEU1cgwWOMY19hsI5KyL4W2w==", + "license": "MIT", + "dependencies": { + "abitype": "^1.0.2" + } + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@types/node": { + "version": "24.7.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.0.tgz", + "integrity": "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.14.0" + } + }, + "node_modules/abitype": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.1.1.tgz", + "integrity": "sha512-Loe5/6tAgsBukY95eGaPSDmQHIjRZYQq8PB1MpsNccDIK8WiV+Uw6WzaIXipvaxTEL2yEB0OpEaQv3gs8pkS9Q==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/asn1js": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.6.tgz", + "integrity": "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/ox": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.9.6.tgz", + "integrity": "sha512-8SuCbHPvv2eZLYXrNmC0EC12rdzXQLdhnOMlHDW2wiCPLxBrOOJwX5L5E61by+UjTPOryqQiRSnjIKCI+GykKg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.0.9", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/ox/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", + "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/viem": { + "version": "2.37.13", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.37.13.tgz", + "integrity": "sha512-05dh56iMmCyjRLcTIiu8bB4zZLnb9uLOVToDwmBLYDarmoOE8d8SLFkQLc2zLU57FlnYCQIO1VbUviGZYwFGgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.9.1", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0", + "abitype": "1.1.0", + "isows": "1.0.7", + "ox": "0.9.6", + "ws": "8.18.3" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/abitype": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.1.0.tgz", + "integrity": "sha512-6Vh4HcRxNMLA0puzPjM5GBgT4aAcFGKZzSgAXvuZ27shJP6NEpielTuqbBmZILR5/xd0PizkBGy5hReKz9jl5A==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/propose-safe-multisig-tx/package.json b/propose-safe-multisig-tx/package.json new file mode 100644 index 0000000..fd21684 --- /dev/null +++ b/propose-safe-multisig-tx/package.json @@ -0,0 +1,23 @@ +{ + "name": "propose-safe-multisig-tx-github-action", + "version": "1.0.0", + "description": "GitHub Action for proposing Safe Multisig transactions", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "propose": "node dist/index.js" + }, + "dependencies": { + "@actions/core": "^1.10.1", + "@safe-global/api-kit": "^4.0.0", + "@safe-global/protocol-kit": "^6.1.1", + "@safe-global/types-kit": "^3.0.0", + "dotenv": "^16.3.1", + "viem": "^2.21.0", + "zod": "^3.22.4" + }, + "devDependencies": { + "@types/node": "^24.7.0", + "typescript": "^5.0.0" + } +} diff --git a/propose-safe-multisig-tx/src/env.ts b/propose-safe-multisig-tx/src/env.ts new file mode 100644 index 0000000..2563421 --- /dev/null +++ b/propose-safe-multisig-tx/src/env.ts @@ -0,0 +1,55 @@ +import 'dotenv/config'; +import { z } from 'zod'; + +const addressRegex = /(^|\b)(0x)?[0-9a-fA-F]{40}(\b|$)/; +const privateKeyRegex = /(^|\b)(0x)?[0-9a-fA-F]{64}(\b|$)/; +const hexDataRegex = /^0x[0-9a-fA-F]*$/; + +const envSchema = z.object({ + // RPC URL for blockchain network connection + RPC_URL: z + .string() + .url("RPC_URL must be a valid URL") + .min(1, "RPC URL is required"), + + // Safe multisig contract address + SAFE_ADDRESS: z + .string() + .regex(addressRegex, "Invalid Safe address format") + .min(1, "Safe address is required"), + + // Target address for the transaction + TRANSACTION_TO: z + .string() + .regex(addressRegex, "Invalid transaction target address format") + .min(1, "Transaction target address is required"), + + // Value to send in the transaction (in wei) + TRANSACTION_VALUE: z + .string() + .default("0") + .pipe( + z.coerce + .bigint() + .nonnegative("Transaction value must be a positive amount") + ) + .transform(String), + + // Transaction data/calldata + TRANSACTION_DATA: z + .string() + .regex(hexDataRegex, "Transaction data must be valid hex data") + .default("0x"), + + // Private key of the proposer wallet + SAFE_PROPOSER_PRIVATE_KEY: z + .string() + .regex(privateKeyRegex, "Invalid private key format") + .min(1, "Safe proposer private key is required") + .transform((val) => (val.startsWith("0x") ? val : `0x${val}`)), + + // Safe API key for transaction service + SAFE_API_KEY: z.string().min(1, "Safe API key is required"), +}); + +export const env = envSchema.parse(process.env); diff --git a/propose-safe-multisig-tx/src/index.ts b/propose-safe-multisig-tx/src/index.ts new file mode 100644 index 0000000..26a8f74 --- /dev/null +++ b/propose-safe-multisig-tx/src/index.ts @@ -0,0 +1,99 @@ +import * as core from '@actions/core'; +import SafeApiKit from "@safe-global/api-kit"; +import Safe from "@safe-global/protocol-kit"; +import { OperationType, MetaTransactionData } from "@safe-global/types-kit"; +import { createPublicClient, http } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { env } from "./env.js"; + +async function run() { + // Environment variables are already validated and parsed + const { + RPC_URL: rpcUrl, + SAFE_ADDRESS: safeAddress, + TRANSACTION_TO: transactionTo, + TRANSACTION_VALUE: transactionValue, + TRANSACTION_DATA: transactionData, + SAFE_PROPOSER_PRIVATE_KEY: safeProposerPrivateKey, + SAFE_API_KEY: safeApiKey, + } = env; + + core.info(`๐Ÿš€ Starting Safe transaction proposal...`); + core.info(`๐Ÿ“ Safe Address: ${safeAddress}`); + core.info(`๐ŸŽฏ Target Address: ${transactionTo}`); + + // Initialize wallet + const account = privateKeyToAccount(safeProposerPrivateKey as `0x${string}`); + core.info(`๐Ÿ”‘ Proposer Address: ${account.address}`); + + // Detect chainId from RPC + const publicClient = createPublicClient({ + transport: http(rpcUrl), + }); + const chainId = await publicClient.getChainId(); + + // Initialize API Kit + const apiKit = new SafeApiKit({ + chainId: BigInt(chainId), + apiKey: safeApiKey, + }); + + // Initialize Protocol Kit + const protocolKit = await Safe.init({ + provider: rpcUrl, + signer: safeProposerPrivateKey, + safeAddress: safeAddress, + }); + + core.info(`๐Ÿ‘ค Safe initialized for: ${safeAddress}`); + + // Create transaction + const safeTransactionData: MetaTransactionData = { + to: transactionTo, + value: transactionValue, + data: transactionData, + operation: OperationType.Call, + }; + + core.info("๐Ÿ“ Creating Safe transaction..."); + + const safeTransaction = await protocolKit.createTransaction({ + transactions: [safeTransactionData], + }); + + const safeTxHash = await protocolKit.getTransactionHash(safeTransaction); + const signature = await protocolKit.signHash(safeTxHash); + + core.info(`๐Ÿ” Transaction signed - hash: ${safeTxHash}`); + + // Propose transaction to the service + await apiKit.proposeTransaction({ + safeAddress: safeAddress, + safeTransactionData: safeTransaction.data, + safeTxHash: safeTxHash, + senderAddress: account.address, + senderSignature: signature.data, + origin: "GitHub Action - Propose Safe Multisig Transaction", + }); + + // Get transaction details + const transaction = await apiKit.getTransaction(safeTxHash); + + // Set outputs + core.setOutput("tx-hash", safeTxHash); + core.setOutput("tx-details", JSON.stringify(transaction)); + + core.info(`โœ… Transaction proposed successfully!`); + core.info(`๐Ÿ”— Transaction Hash: ${safeTxHash}`); + core.info(`โณ Waiting for other owners to sign and execute...`); + core.info(`๐Ÿ“‹ Transaction Details: ${JSON.stringify(transaction, null, 2)}`); +} + +run().catch((error) => { + const errorMessage = error instanceof Error ? error.message : "Unknown error"; + const errorStack = error instanceof Error ? error.stack : undefined; + core.setFailed(`โŒ Error proposing Safe transaction: ${errorMessage}`); + if (errorStack) { + core.error(errorStack); + } +}); diff --git a/propose-safe-multisig-tx/tsconfig.json b/propose-safe-multisig-tx/tsconfig.json new file mode 100644 index 0000000..29a5052 --- /dev/null +++ b/propose-safe-multisig-tx/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2022", + "declaration": true, + "sourceMap": true, + "outDir": "dist", + "moduleResolution": "node", + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": false + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/propose-safe-multisig-tx/version.txt b/propose-safe-multisig-tx/version.txt new file mode 100644 index 0000000..3eefcb9 --- /dev/null +++ b/propose-safe-multisig-tx/version.txt @@ -0,0 +1 @@ +1.0.0