diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 0dc28a2..2d53830 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -34,21 +34,30 @@ jobs:
fi
go build -o $PECTRA_BINARY_NAME cmd/main.go
- # Build signer
+ # Build private key signer
SIGNER_BINARY_NAME="signer"
if [ "${{ matrix.goos }}" = "windows" ]; then
SIGNER_BINARY_NAME="${SIGNER_BINARY_NAME}.exe"
fi
go build -o $SIGNER_BINARY_NAME scripts/sign.go
+ # Build mnemonic signer
+ MNEMONIC_SIGNER_BINARY_NAME="sign-mnemonic"
+ if [ "${{ matrix.goos }}" = "windows" ]; then
+ MNEMONIC_SIGNER_BINARY_NAME="${MNEMONIC_SIGNER_BINARY_NAME}.exe"
+ fi
+ go build -o $MNEMONIC_SIGNER_BINARY_NAME scripts/sign_mnemonic.go
+
# Create dist directory and move binaries
mkdir -p dist
if [ "${{ matrix.goos }}" = "windows" ]; then
mv $PECTRA_BINARY_NAME "dist/pectra-cli-${{ matrix.goos }}-${{ matrix.goarch }}.exe"
mv $SIGNER_BINARY_NAME "dist/signer-${{ matrix.goos }}-${{ matrix.goarch }}.exe"
+ mv $MNEMONIC_SIGNER_BINARY_NAME "dist/sign-mnemonic-${{ matrix.goos }}-${{ matrix.goarch }}.exe"
else
mv $PECTRA_BINARY_NAME "dist/pectra-cli-${{ matrix.goos }}-${{ matrix.goarch }}"
mv $SIGNER_BINARY_NAME "dist/signer-${{ matrix.goos }}-${{ matrix.goarch }}"
+ mv $MNEMONIC_SIGNER_BINARY_NAME "dist/sign-mnemonic-${{ matrix.goos }}-${{ matrix.goarch }}"
fi
- name: Create Release
@@ -58,4 +67,4 @@ jobs:
files: dist/*
generate_release_notes: true
env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/README.md b/README.md
index 18869d7..5acdb7a 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
A powerful airgapped CLI tool for executing Ethereum validator operations including consolidation, switching, and both partial and full withdrawals with seamless batching enabled by EIP-7702.
-# Background
+## Background
The Pectra upgrade (Prague + Electra) introduces key validator enhancements: consolidation allows merging multiple validators into one to simplify management and reduce overhead; switch enables validators to update their BLS keys, supporting strategies like autocompounding without needing to exit and re-enter; and execution layer exits allow validators to exit directly via the execution layer, streamlining the exit process and enabling better integration with smart contracts and tooling.
@@ -28,15 +28,19 @@ Moreover, the CLI is designed to be airgapped, ensuring security by allowing use
## Installation
-1. Ensure you have Go (version 1.21+ recommended, as per `.github/workflows/release.yml` line 18) installed on your system.
-2. Clone the repository (if you haven't already).
-3. Build the CLI tool:
+1. Ensure you have Go (version 1.21+ recommended, as per .github/workflows/release.yml line 18) installed on your system.
+2. Clone the repository (if you haven't already).
+3. Build the CLI tool and mnemonic signer:
```bash
+ # Build main CLI
go build -o pectra-cli ./cmd/main.go
+
+ # Build mnemonic signer
+ go build -o sign-mnemonic scripts/sign_mnemonic.go
```
-
- This will create an executable file named `pectra-cli` in the current directory.
+
+ This will create two executables: pectra-cli and sign-mnemonic in the current directory.
## Deployed Contracts
- Mainnet - [0x17c11FDdADac2b341F2455aFe988fec4c3ba26e3](https://etherscan.io/address/0x17c11FDdADac2b341F2455aFe988fec4c3ba26e3)
@@ -44,9 +48,9 @@ Moreover, the CLI is designed to be airgapped, ensuring security by allowing use
## Configuration
-Create a JSON configuration file named `config.json` in the same directory as the `pectra-cli` executable. You can use `sample_config.json` as a template.
+Create a JSON configuration file named config.json in the same directory as the pectra-cli executable. You can use sample_config.json as a template.
-**Example `config.json`:**
+**Example config.json:**
```json
{
@@ -98,17 +102,52 @@ Create a JSON configuration file named `config.json` in the same directory as th
## Private Key Handling
-Use the `--airgapped` or `-a` to run the CLI in airgapped mode; alternatively, omit the flag to sign directly in the CLI by providing the private key. The CLI will securely prompt you to enter it at runtime when an operation is initiated.
-(See `internal/config/config.go` lines 88-114)
+The CLI supports multiple secure signing methods:
+
+### 1. Airgapped Mode (Recommended)
+Use the `--airgapped` or `-a` flag to generate unsigned transactions that can be signed later:
+
+```bash
+./pectra-cli switch -c config.json -a
+```
+
+This creates an `unsigned_txn.json` file that can be signed using either:
+
+#### A) Private Key Signing
+```bash
+go run scripts/sign.go unsigned_txn.json
+```
+
+#### B) Mnemonic Phrase Signing
+```bash
+./sign-mnemonic unsigned_txn.json
+```
+
+The mnemonic signer will prompt for:
+- Your BIP-39 seed phrase (12, 15, 18, 21, or 24 words)
+- Derivation path (default: m/44'/60'/0'/0/0)
+
+Supported derivation paths:
+- Standard Ethereum: m/44'/60'/0'/0/0 (default)
+- Ledger Live: m/44'/60'/0'/0/0
+- MEW/MyCrypto: m/44'/60'/0'/0
+- Custom accounts: m/44'/60'/0'/0/[index]
+
+### 2. Direct Signing
+For convenience, you can sign directly by providing the private key when prompted (not recommended for production use):
+
+```bash
+./pectra-cli switch -c config.json
+```
-⚠️ Ensure that correct private keys are provided for the validators — otherwise, transactions will succeed but no validator operation will occur, wasting gas.
+⚠ Ensure that correct private keys are provided for the validators — otherwise, transactions will succeed but no validator operation will occur, wasting gas.
## 📜 ABI Dependency
-The CLI requires the Pectra batch contract's ABI. Place the `abi.json` file in the same directory as the `pectra-cli` executable.
+The CLI requires the Pectra batch contract's ABI. Place the abi.json file in the same directory as the pectra-cli executable.
(The ABI is loaded from `./abi.json` as seen in `cmd/main.go` line 49)
-## ⚠️ Unset Delegation
+## ⚠ Unset Delegation
> It is HIGHLY recommended to unset delegation after performing any operation. This helps with restoration of EOA functionality and prevents the address from being used as a smart contract.
@@ -132,49 +171,55 @@ To get a gist of the CLI, run:
./pectra-cli --help
```
-Replace `` with one of the operations listed below and `config.json` with the path to your configuration file.
+Replace with one of the operations listed below and config.json with the path to your configuration file.
Add the `-a` or `--airgapped` flag to run the CLI in airgapped mode.
### Switch Validators
-Updates deposit credentials for the validators specified in `config.json` under the `switch` section. You can switch up to 200 validators in a single batch.
+Updates deposit credentials for the validators specified in config.json under the switch section. You can switch up to 200 validators in a single batch.
```bash
./pectra-cli switch -c config.json
```
-⚠️ Do not switch a validator that has already been switched — the transaction will succeed but the switch won't take effect, wasting gas.
+⚠ Do not switch a validator that has already been switched — the transaction will succeed but the switch won't take effect, wasting gas.
### Consolidate Validators
-Consolidates funds from `sourceValidators` to `targetValidator` as specified in `config.json` under the `consolidate` section. You can consolidate from up to 63 source validators into one target validator.
+Consolidates funds from sourceValidators to targetValidator as specified in config.json under the consolidate section. You can consolidate from up to 63 source validators into one target validator.
```bash
./pectra-cli consolidate -c config.json
```
-⚠️ Do not use exited validators as source or target — transactions will succeed but consolidation won't occur, wasting gas.
+⚠ Do not use exited validators as source or target — transactions will succeed but consolidation won't occur, wasting gas.
### Execution Layer (EL) Exit
-Performs partial or full exits for validators specified in `config.json` under the `elExit` section. You can exit up to 200 validators in a single batch.
+Performs partial or full exits for validators specified in config.json under the elExit section. You can exit up to 200 validators in a single batch.
```bash
./pectra-cli el-exit -c config.json
```
-⚠️ Do not attempt to exit a validator that has already exited — the transaction will succeed but no exit will occur, wasting gas.
+⚠ Do not attempt to exit a validator that has already exited — the transaction will succeed but no exit will occur, wasting gas.
### Signing and Broadcast for airgapped mode
-To sign an unsigned transaction, use `scripts/sign.go` on the `unsigned_txn.json` file — this will generate a `signed_txn.json`.
+After generating an unsigned transaction in airgapped mode, you have two signing options:
+#### Option 1: Sign with Private Key
```bash
go run scripts/sign.go unsigned_txn.json
```
-Once signed, use the CLI's broadcast command to submit the `signed_txn.json` to the network.
+#### Option 2: Sign with Mnemonic Phrase
+```bash
+./sign-mnemonic unsigned_txn.json
+```
+
+Both methods will generate a signed_txn.json file. Broadcast it using:
```bash
./pectra-cli broadcast -c config.json -f signed_txn.json
@@ -183,31 +228,36 @@ Once signed, use the CLI's broadcast command to submit the `signed_txn.json` to
## 📝 Important Notes
- **Validator Public Keys**: All validator public keys in the `config.json` file must be in hexadecimal format, without the "0x" prefix.
-- **Transaction Fees**: The fee required per validator for each operation (switch, consolidate, EL exit) is automatically fetched from the smart contract functions (`getConsolidationFee`, `getExitFee`). This fee is in Wei. The total transaction `value` sent will be `(number of validators) * (fee per validator)`.
+- **Transaction Fees**: The fee required per validator for each operation (switch, consolidate, EL exit) is automatically fetched from the smart contract functions (`getConsolidationFee`, `getExitFee`). This fee is in Wei. The total transaction value sent will be `(number of validators) * (fee per validator)`.
(Fee fetching logic: `cmd/main.go` lines 67-74, 78-79, 91-92, 105-106) and `internal/utils/utils.go` lines 98-125
- **Execution Layer (EL) Exits**:
- - The `amount` specified in the `elExit.validators` section of `config.json` is in **Gwei** (1 ETH = 1,000,000,000 Gwei).
+ - The amount specified in the `elExit.validators` section of `config.json` is in **Gwei** (1 ETH = 1,000,000,000 Gwei).
(See `internal/utils/utils.go` for usage notes, and `internal/operations/partialexit.go` lines 41-49 for handling)
- - For a **full exit**, set `amount` to `0` (or `0.0`) and `confirmFullExit` to `true`.
- - For a **partial exit**, specify the desired `amount` in Gwei (e.g., `10.0` for 10 Gwei) and ensure `confirmFullExit` is `false`.
+ - For a **full exit**, set amount to `0` (or `0.0`) and `confirmFullExit` to `true`.
+ - For a **partial exit**, specify the desired amount in Gwei (e.g., `10.0` for 10 Gwei) and ensure `confirmFullExit` is `false`.
- **Transaction Authorization**: This tool utilizes EIP-7702 SetCode transaction authorization for its operations.
(See `internal/transaction/transaction.go` lines 18-61)
+- **Mnemonic Security**: When using the mnemonic signer:
+ - Never share your seed phrase
+ - Run the signer on an airgapped machine when possible
+ - Verify the derived address matches your expected wallet address
+ - Consider using a dedicated account for validator operations
+
## Sample Output
(The CLI provides informative output during its execution, including connection status, fees, transaction hashes, and success/failure messages.)
### Sample Output for airgapped mode
-
-
-
-
+
+
+

### Sample Output for non-airgapped mode

-_(Note: The sample output images might show older field names or values; refer to the current configuration guidelines.)_
+(Note: The sample output images might show older field names or values; refer to the current configuration guidelines.)
diff --git a/go.mod b/go.mod
index e301901..740f35b 100644
--- a/go.mod
+++ b/go.mod
@@ -8,6 +8,8 @@ require (
github.com/ethereum/go-ethereum v1.15.11
github.com/fatih/color v1.18.0
github.com/holiman/uint256 v1.3.2
+ github.com/tyler-smith/go-bip32 v1.0.0
+ github.com/tyler-smith/go-bip39 v1.1.0
github.com/urfave/cli/v2 v2.27.6
golang.org/x/term v0.30.0
)
@@ -18,6 +20,8 @@ require (
)
require (
+ github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e // indirect
+ github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/bits-and-blooms/bitset v1.20.0 // indirect
diff --git a/go.sum b/go.sum
index 6a16180..00d3c05 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,9 @@
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
+github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e h1:ahyvB3q25YnZWly5Gq1ekg6jcmWaGj/vG/MhF4aisoc=
+github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e/go.mod h1:kGUqhHd//musdITWjFvNTHn90WG9bMLBEPQZ17Cmlpw=
+github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec h1:1Qb69mGp/UtRPn422BH4/Y4Q3SLUrD9KHuDkm8iodFc=
+github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec/go.mod h1:CD8UlnlLDiqb36L110uqiP2iSflVjx9g/3U9hCI4q2U=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
@@ -14,6 +18,8 @@ github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk=
github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e h1:0XBUw73chJ1VYSsfvcPvVT7auykAJce9FpRr10L6Qhw=
+github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e/go.mod h1:P13beTBKr5Q18lJe1rIoLUqjM+CB1zYrRg44ZqGuQSA=
github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I=
github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8=
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4=
@@ -38,6 +44,7 @@ github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOV
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM=
github.com/crate-crypto/go-kzg-4844 v1.1.0 h1:EN/u9k2TF6OWSHrCCDBBU6GLNMq88OspHHlMnHfoyU4=
github.com/crate-crypto/go-kzg-4844 v1.1.0/go.mod h1:JolLjpSff1tCCJKaJx4psrlEdlXuJEC996PL3tTAFks=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
@@ -172,6 +179,7 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU=
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
+github.com/stretchr/testify v1.1.5-0.20170601210322-f6abca593680/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/supranational/blst v0.3.14 h1:xNMoHRJOTwMn63ip6qoWJ2Ymgvj7E2b9jY2FAwY+qRo=
@@ -182,20 +190,28 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
-github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
-github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
+github.com/tyler-smith/go-bip32 v1.0.0 h1:sDR9juArbUgX+bO/iblgZnMPeWY1KZMUC2AFUJdv5KE=
+github.com/tyler-smith/go-bip32 v1.0.0/go.mod h1:onot+eHknzV4BVPwrzqY5OoVpyCvnwD7lMawL5aQupE=
+github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8=
+github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U=
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
+golang.org/x/crypto v0.0.0-20170613210332-850760c427c5/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -207,6 +223,7 @@ golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
@@ -219,5 +236,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+launchpad.net/gocheck v0.0.0-20140225173054-000000000087 h1:Izowp2XBH6Ya6rv+hqbceQyw/gSGoXfH/UPoTGduL54=
+launchpad.net/gocheck v0.0.0-20140225173054-000000000087/go.mod h1:hj7XX3B/0A+80Vse0e+BUHsHMTEhd0O4cpUHr/e/BUM=
rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU=
rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA=
diff --git a/scripts/sign_mnemonic.go b/scripts/sign_mnemonic.go
new file mode 100644
index 0000000..7e62992
--- /dev/null
+++ b/scripts/sign_mnemonic.go
@@ -0,0 +1,299 @@
+package main
+
+import (
+ "crypto/ecdsa"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "log"
+ "os"
+ "strconv"
+ "strings"
+
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/rlp"
+ "github.com/fatih/color"
+ "github.com/tyler-smith/go-bip32"
+ "github.com/tyler-smith/go-bip39"
+ "golang.org/x/term"
+)
+
+const (
+ // Default derivation path for Ethereum accounts
+ defaultDerivationPath = "m/44'/60'/0'/0/0"
+)
+
+func main() {
+ color.Green("Pectra CLI - Mnemonic-based Transaction Signer")
+ color.Cyan("This tool signs transactions using a mnemonic phrase (seed phrase)")
+ fmt.Println()
+
+ // Get mnemonic from user
+ mnemonic, err := getMnemonic()
+ if err != nil {
+ log.Fatalf("Failed to get mnemonic: %v", err)
+ }
+
+ // Get derivation path from user
+ derivationPath, err := getDerivationPath()
+ if err != nil {
+ log.Fatalf("Failed to get derivation path: %v", err)
+ }
+
+ // Derive private key from mnemonic
+ privateKey, address, err := derivePrivateKeyFromMnemonic(mnemonic, derivationPath)
+ if err != nil {
+ log.Fatalf("Failed to derive private key from mnemonic: %v", err)
+ }
+
+ color.Green("Derived address: %s", address)
+ fmt.Println()
+
+ // Determine input filename
+ inputFile := "unsigned_txn.json"
+ if len(os.Args) > 1 {
+ inputFile = os.Args[1]
+ }
+
+ color.Cyan("Reading unsigned transaction from: %s", inputFile)
+
+ // Read the unsigned transaction from file
+ data, err := os.ReadFile(inputFile)
+ if err != nil {
+ log.Fatalf("Failed to read %s: %v", inputFile, err)
+ }
+
+ var txData struct {
+ UnsignedTransaction string `json:"unsignedTransaction"`
+ ChainId string `json:"chainId"`
+ }
+
+ if err := json.Unmarshal(data, &txData); err != nil {
+ log.Fatalf("Failed to parse JSON: %v", err)
+ }
+
+ hexTx := txData.UnsignedTransaction
+
+ // Remove 0x prefix if present
+ if len(hexTx) > 2 && hexTx[0:2] == "0x" {
+ hexTx = hexTx[2:]
+ }
+
+ // Decode hex to bytes
+ txBytes, err := hex.DecodeString(hexTx)
+ if err != nil {
+ log.Fatalf("Failed to decode hex string: %v", err)
+ }
+
+ // Decode transaction
+ tx := new(types.Transaction)
+ err = rlp.DecodeBytes(txBytes, tx)
+ if err != nil {
+ log.Fatalf("Failed to decode transaction: %v", err)
+ }
+
+ color.Cyan("Transaction details:")
+ color.White(" Hash: %s", tx.Hash().Hex())
+ color.White(" Chain ID: %s", tx.ChainId().String())
+ color.White(" To: %s", tx.To().Hex())
+ color.White(" Value: %s Wei", tx.Value().String())
+ color.White(" Gas: %d", tx.Gas())
+ fmt.Println()
+
+ // Confirm signing
+ if !confirmSigning() {
+ color.Yellow("Transaction signing cancelled by user")
+ return
+ }
+
+ // Sign the authorization
+ color.Cyan("Signing authorization...")
+ signedAuthorization, err := types.SignSetCode(privateKey, tx.SetCodeAuthorizations()[0])
+ if err != nil {
+ log.Fatalf("Failed to sign the authorization: %v", err)
+ }
+
+ // Update the transaction with signed authorization
+ tx.SetCodeAuthorizations()[0] = signedAuthorization
+
+ // Sign the transaction
+ color.Cyan("Signing transaction...")
+ signedTx, err := types.SignTx(tx, types.LatestSignerForChainID(tx.ChainId()), privateKey)
+ if err != nil {
+ log.Fatalf("Failed to sign the transaction: %v", err)
+ }
+
+ // Encode the signed transaction
+ signedTxBytes, err := rlp.EncodeToBytes(signedTx)
+ if err != nil {
+ log.Fatalf("Failed to encode signed transaction: %v", err)
+ }
+
+ // Write the signed transaction to a file
+ signedData := map[string]string{
+ "signedTransaction": hex.EncodeToString(signedTxBytes),
+ }
+
+ jsonData, err := json.MarshalIndent(signedData, "", " ")
+ if err != nil {
+ log.Fatalf("Failed to marshal to JSON: %v", err)
+ }
+
+ outputFile := "signed_txn.json"
+ if err := os.WriteFile(outputFile, jsonData, 0644); err != nil {
+ log.Fatalf("Failed to write to %s: %v", outputFile, err)
+ }
+
+ color.Green("Transaction successfully signed!")
+ color.Green("Signed transaction written to: %s", outputFile)
+ color.Cyan("You can now broadcast this transaction using: ./pectra-cli broadcast -f %s -c config.json", outputFile)
+}
+
+// getMnemonic securely prompts the user for their mnemonic phrase
+func getMnemonic() (string, error) {
+ color.Cyan("Please enter your mnemonic phrase (12, 15, 18, 21, or 24 words):")
+ color.Yellow("Note: The mnemonic will not be displayed for security. Just type/paste and press Enter.")
+ fmt.Print("> ")
+
+ // Read mnemonic without echoing to terminal
+ bytePassword, err := term.ReadPassword(int(os.Stdin.Fd()))
+ if err != nil {
+ return "", fmt.Errorf("failed to read mnemonic: %w", err)
+ }
+ fmt.Println()
+
+ mnemonic := strings.TrimSpace(string(bytePassword))
+
+ // Basic validation
+ words := strings.Fields(mnemonic)
+ if len(words) < 12 || len(words) > 24 {
+ return "", fmt.Errorf("invalid mnemonic length: expected 12-24 words, got %d", len(words))
+ }
+
+ // Validate mnemonic using BIP-39
+ if !bip39.IsMnemonicValid(mnemonic) {
+ return "", fmt.Errorf("invalid mnemonic phrase")
+ }
+
+ return mnemonic, nil
+}
+
+// getDerivationPath prompts the user for a derivation path or uses the default
+func getDerivationPath() (string, error) {
+ color.Cyan("Enter derivation path (or press Enter for default: %s):", defaultDerivationPath)
+ color.Yellow("Common paths:")
+ color.Yellow(" Ethereum (default): m/44'/60'/0'/0/0")
+ color.Yellow(" Ledger Live: m/44'/60'/0'/0/0")
+ color.Yellow(" MEW/MyCrypto: m/44'/60'/0'/0")
+ color.Yellow(" Custom account index: m/44'/60'/0'/0/[index]")
+ fmt.Print("> ")
+
+ var input string
+ _, err := fmt.Scanln(&input)
+ if err != nil && err.Error() != "unexpected newline" {
+ return "", fmt.Errorf("failed to read derivation path: %w", err)
+ }
+
+ input = strings.TrimSpace(input)
+ if input == "" {
+ return defaultDerivationPath, nil
+ }
+
+ // Basic validation for derivation path format
+ if !strings.HasPrefix(input, "m/") {
+ return "", fmt.Errorf("derivation path must start with 'm/'")
+ }
+
+ return input, nil
+}
+
+// derivePrivateKeyFromMnemonic derives a private key from a mnemonic using the specified derivation path
+func derivePrivateKeyFromMnemonic(mnemonic, derivationPath string) (*ecdsa.PrivateKey, string, error) {
+ // Generate seed from mnemonic
+ seed := bip39.NewSeed(mnemonic, "")
+
+ // Create master key
+ masterKey, err := bip32.NewMasterKey(seed)
+ if err != nil {
+ return nil, "", fmt.Errorf("failed to create master key: %w", err)
+ }
+
+ // Parse and apply derivation path
+ path, err := parseDerivationPath(derivationPath)
+ if err != nil {
+ return nil, "", fmt.Errorf("failed to parse derivation path: %w", err)
+ }
+
+ key := masterKey
+ for _, index := range path {
+ key, err = key.NewChildKey(index)
+ if err != nil {
+ return nil, "", fmt.Errorf("failed to derive child key: %w", err)
+ }
+ }
+
+ // Convert to ECDSA private key
+ privateKey, err := crypto.ToECDSA(key.Key)
+ if err != nil {
+ return nil, "", fmt.Errorf("failed to convert to ECDSA private key: %w", err)
+ }
+
+ // Get the Ethereum address
+ address := crypto.PubkeyToAddress(privateKey.PublicKey).Hex()
+
+ return privateKey, address, nil
+}
+
+// parseDerivationPath parses a derivation path string into uint32 indexes
+func parseDerivationPath(path string) ([]uint32, error) {
+ if !strings.HasPrefix(path, "m/") {
+ return nil, fmt.Errorf("derivation path must start with 'm/'")
+ }
+
+ path = path[2:]
+ if path == "" {
+ return []uint32{}, nil
+ }
+
+ parts := strings.Split(path, "/")
+ indexes := make([]uint32, len(parts))
+
+ for i, part := range parts {
+ var index uint64
+ var err error
+
+ if strings.HasSuffix(part, "'") {
+ part = part[:len(part)-1]
+ index, err = strconv.ParseUint(part, 10, 32)
+ if err != nil {
+ return nil, fmt.Errorf("invalid derivation path component: %s", part)
+ }
+ index += 0x80000000
+ } else {
+ index, err = strconv.ParseUint(part, 10, 32)
+ if err != nil {
+ return nil, fmt.Errorf("invalid derivation path component: %s", part)
+ }
+ }
+
+ indexes[i] = uint32(index)
+ }
+
+ return indexes, nil
+}
+
+// confirmSigning asks the user to confirm they want to sign the transaction
+func confirmSigning() bool {
+ color.Yellow("Are you sure you want to sign this transaction? (y/N): ")
+ fmt.Print("> ")
+
+ var response string
+ _, err := fmt.Scanln(&response)
+ if err != nil {
+ return false
+ }
+
+ response = strings.ToLower(strings.TrimSpace(response))
+ return response == "y" || response == "yes"
+}
\ No newline at end of file