Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 103 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,15 @@ safe config init
```
This sets up your chain configurations and optional API keys.

**2. Import a wallet**
**2. Set up a wallet**
```bash
# Option A: Import from private key
# Option A: Create a new wallet (generates private key)
safe wallet create

# Option B: Import existing private key
safe wallet import

# Option B: Import Ledger hardware wallet (recommended)
# Option C: Import Ledger hardware wallet (recommended for production)
safe wallet import-ledger
```
Private keys are encrypted with a password. Ledger wallets use your hardware device for signing.
Expand Down Expand Up @@ -95,7 +98,8 @@ Follow the interactive prompts to configure owners and threshold.

| Command | Description |
|---------|-------------|
| `safe wallet import` | Import a wallet with your private key |
| `safe wallet create` | Create a new wallet with a generated private key |
| `safe wallet import` | Import a wallet with your existing private key |
| `safe wallet import-ledger` | Import a Ledger hardware wallet |
| `safe wallet list` | View all your wallets (shows wallet types) |
| `safe wallet use` | Switch to a different wallet |
Expand Down Expand Up @@ -138,6 +142,99 @@ Follow the interactive prompts to configure owners and threshold.

---

## 🔑 Wallet Management

### Creating a New Wallet

If you don't have an existing private key, you can generate a new wallet:

```bash
safe wallet create
```

**Security Flow:**

The CLI will guide you through a secure wallet creation process:

1. **Security warnings** - Read and acknowledge the risks
2. **Password setup** - Create a strong password for encryption
3. **Wallet naming** - Give your wallet a memorable name
4. **Key generation** - A cryptographically secure private key is generated
5. **Private key display** - Your key is shown **once** - back it up safely!
6. **Backup verification** - Confirm you've saved it by re-entering the last 8 characters
7. **Encryption** - The key is encrypted and stored locally

**Example:**
```bash
$ safe wallet create

┌ Create New Wallet
◆ 🔐 Security Warning
│ ⚠️ This command will generate a NEW private key
│ ⚠️ You are SOLELY responsible for backing it up securely
│ ⚠️ Loss of your private key = permanent loss of funds
│ ⚠️ NEVER share your private key with anyone
│ ⚠️ This CLI stores keys encrypted locally with your password
◆ Do you understand these risks and wish to continue?
│ Yes
◆ Create a password to encrypt your wallets
│ ********
◆ Confirm password
│ ********
◆ Give this wallet a name
│ My New Wallet
◇ Private key generated successfully
◆ 🔑 Your Private Key
│ 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
│ ⚠️ Write this down and store it in a secure location.
│ ⚠️ This is the ONLY time it will be displayed in plain text.
│ ⚠️ Anyone with this key has FULL control of your wallet.
◆ To confirm you have saved it, enter the last 8 characters:
│ 90abcdef
◇ Wallet created successfully
│ Name: My New Wallet
│ Address: 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb
└ Done!
```

**🔒 Security Best Practices:**

- **Write it down** - Store your private key on paper in a secure location
- **Multiple copies** - Keep backups in different safe locations
- **Never digital** - Don't store in cloud storage, email, or screenshots
- **Use hardware wallets** - For significant funds, use `safe wallet import-ledger` instead
- **Test first** - Try creating a test wallet on testnet (Sepolia) before mainnet

**⚠️ Important Notes:**

- Your private key is encrypted with your password and stored locally
- Without your password, the encrypted key cannot be decrypted
- If you lose your private key, you lose access to your funds permanently
- The CLI cannot recover your private key if lost

### Importing an Existing Wallet

If you already have a private key:

```bash
safe wallet import
```

Follow the prompts to securely import your existing key. It will be encrypted with your password.

---

## 🔐 Hardware Wallet Support

Safe CLI supports **Ledger hardware wallets** for enhanced security. Your private keys never leave the device.
Expand Down Expand Up @@ -419,10 +516,12 @@ These are optional but recommended for enhanced functionality:
## 🔐 Security

### Private Key Wallets
- **Key generation**: Uses Node.js crypto.randomBytes() for cryptographically secure random generation
- **Encryption**: Private keys encrypted with AES-256-GCM
- **Key derivation**: PBKDF2 with 100,000 iterations
- **Local storage**: All data stored locally on your machine
- **No exposure**: Keys never logged or transmitted in plain text
- **Backup verification**: Confirmation required when creating new wallets

### Hardware Wallets (Ledger)
- **Maximum security**: Private keys never leave the device
Expand Down
12 changes: 12 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { addChain, listChains, removeChain } from './commands/config/chains.js'
import { editChains } from './commands/config/edit.js'
import { importWallet } from './commands/wallet/import.js'
import { importLedgerWallet } from './commands/wallet/import-ledger.js'
import { createWallet } from './commands/wallet/create.js'
import { listWallets } from './commands/wallet/list.js'
import { useWallet } from './commands/wallet/use.js'
import { removeWallet } from './commands/wallet/remove.js'
Expand Down Expand Up @@ -129,6 +130,17 @@ wallet
}
})

wallet
.command('create')
.description('Create a new wallet with a generated private key')
.action(async () => {
try {
await createWallet()
} catch (error) {
handleError(error)
}
})

wallet
.command('list')
.description('List all imported wallets')
Expand Down
164 changes: 164 additions & 0 deletions src/commands/wallet/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import * as p from '@clack/prompts'
import { type Address } from 'viem'
import { getWalletStorage } from '../../storage/wallet-store.js'
import { getConfigStore } from '../../storage/config-store.js'
import { getValidationService } from '../../services/validation-service.js'
import { logError } from '../../ui/messages.js'
import { renderScreen } from '../../ui/render.js'
import { WalletImportSuccessScreen } from '../../ui/screens/index.js'
import { promptPassword } from '../../utils/command-helpers.js'
import { generatePrivateKey } from '../../utils/key-generation.js'

/**
* Security warnings displayed before generating a private key.
*/
const SECURITY_WARNINGS = [
'This command will generate a NEW private key',
'You are SOLELY responsible for backing it up securely',
'Loss of your private key = permanent loss of funds',
'NEVER share your private key with anyone',
'This CLI stores keys encrypted locally with your password',
]

/**
* Displays security warnings and gets user confirmation.
* @returns true if user confirmed, false if cancelled
*/
async function showSecurityWarnings(): Promise<boolean> {
p.note(SECURITY_WARNINGS.join('\n'), '🔐 Security Warning')

const confirmed = await p.confirm({
message: 'Do you understand these risks and wish to continue?',
initialValue: false,
})

if (p.isCancel(confirmed) || !confirmed) {
return false
}

return true
}

/**
* Displays the generated private key with instructions.
* @param privateKey - The generated private key
*/
function displayPrivateKey(privateKey: string): void {
p.note(
`${privateKey}\n\n` +
'Write this down and store it in a secure location.\n' +
'This is the ONLY time it will be displayed in plain text.\n' +
'Anyone with this key has FULL control of your wallet.',
'🔑 Your Private Key'
)
}

/**
* Verifies that the user has backed up their private key.
* Requires user to type the last 8 characters of the key.
*
* @param privateKey - The private key to verify
* @returns true if verification succeeded, false if cancelled
*/
async function verifyBackup(privateKey: string): Promise<boolean> {
const lastChars = privateKey.slice(-8)

const verify = await p.text({
message: 'To confirm you have saved it, enter the last 8 characters:',
placeholder: lastChars,
validate: (value) => {
if (!value) return 'Please enter the last 8 characters'
if (value !== lastChars) return 'Incorrect. Please try again.'
return undefined
},
})

if (p.isCancel(verify)) {
return false
}

return true
}

/**
* Creates a new wallet with a randomly generated private key.
* Displays security warnings, generates key, requires backup verification,
* and stores the wallet encrypted.
*/
export async function createWallet() {
p.intro('Create New Wallet')

const validator = getValidationService()

// Step 1: Show security warnings
const acceptedWarnings = await showSecurityWarnings()
if (!acceptedWarnings) {
p.cancel('Operation cancelled')
return
}

// Step 2: Get password for encryption
const password = await promptPassword(true)
if (!password) return

const walletStorage = getWalletStorage()
walletStorage.setPassword(password)

// Step 3: Get wallet name
const name = await p.text({
message: 'Give this wallet a name:',
placeholder: 'my-wallet',
validate: (value) => validator.validateRequired(value, 'Wallet name'),
})

if (p.isCancel(name)) {
p.cancel('Operation cancelled')
return
}

// Step 4: Generate private key
const spinner = p.spinner()
spinner.start('Generating secure private key...')

let privateKey: `0x${string}`
try {
privateKey = generatePrivateKey()
spinner.stop('Private key generated successfully')
} catch (error) {
spinner.stop('Failed to generate private key')
logError(error instanceof Error ? error.message : 'Unknown error')
process.exit(1)
}

// Step 5: Display private key
displayPrivateKey(privateKey)

// Step 6: Verify backup
const backupConfirmed = await verifyBackup(privateKey)
if (!backupConfirmed) {
p.cancel('Operation cancelled - wallet was not saved')
return
}

// Step 7: Store wallet
spinner.start('Storing wallet...')

try {
const wallet = await walletStorage.importWallet(name as string, privateKey, password as string)
Copy link

Copilot AI Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type assertions name as string and password as string are unnecessary since the control flow guarantees these variables are strings (early returns prevent null/undefined). Remove the type assertions for cleaner code.

Suggested change
const wallet = await walletStorage.importWallet(name as string, privateKey, password as string)
const wallet = await walletStorage.importWallet(name, privateKey, password)

Copilot uses AI. Check for mistakes.
spinner.stop('Wallet created successfully')

// Get default chain for balance check
const configStore = getConfigStore()
const defaultChain = configStore.getDefaultChain()

await renderScreen(WalletImportSuccessScreen, {
name: wallet.name,
address: wallet.address as Address,
chain: defaultChain,
})
} catch (error) {
spinner.stop('Failed to create wallet')
logError(error instanceof Error ? error.message : 'Unknown error')
process.exit(1)
}
}
5 changes: 3 additions & 2 deletions src/storage/wallet-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { Wallet, WalletStore, PrivateKeyWallet, LedgerWallet } from '../typ
import { WalletError } from '../utils/errors.js'
import { isValidPrivateKey, normalizePrivateKey } from '../utils/validation.js'
import { checksumAddress } from '../utils/ethereum.js'
import { generateWalletId } from '../utils/key-generation.js'

// Simple encryption for private keys
// Note: For production, consider using OS keychain (keytar/keychain)
Expand Down Expand Up @@ -118,7 +119,7 @@ export class WalletStorageService {
}

// Create wallet metadata
const walletId = randomBytes(16).toString('hex')
const walletId = generateWalletId()
const wallet: PrivateKeyWallet = {
type: 'private-key',
id: walletId,
Expand Down Expand Up @@ -161,7 +162,7 @@ export class WalletStorageService {
}

// Create wallet metadata
const walletId = randomBytes(16).toString('hex')
const walletId = generateWalletId()
const wallet: LedgerWallet = {
type: 'ledger',
id: walletId,
Expand Down
Loading