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
27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ npm install -g @0xsequence/builder-cli
sequence-builder --help
```

## Quick Start for Agents
## Private Key Encryption

Set `SEQUENCE_PASSPHRASE` in your environment to auto-encrypt and store the private key. No need to pass `-k` after wallet creation.

```bash
export SEQUENCE_PASSPHRASE="your-strong-secret"
```

```bash
# 1. Generate a wallet
Expand All @@ -34,7 +40,7 @@ npx @0xsequence/builder-cli projects create "My Project"
# 5. Get your Sequence wallet address (where to send tokens)
npx @0xsequence/builder-cli wallet-info -k <private-key> -a <access-key>

# 6. Fund the Sequence wallet with tokens
# 6. Fund the Sequence wallet via the Trails link from step 5

# 7. Send an ERC20 transfer (gas paid with same token!)
npx @0xsequence/builder-cli transfer \
Expand Down Expand Up @@ -117,6 +123,11 @@ Sequence Wallet: 0xA715064b5601Aebf197aC84A469b72Bb7Dc6A646
Important:
Send tokens to the Sequence Wallet address for use with the transfer command.
The Sequence Wallet can pay gas fees with ERC20 tokens (no ETH needed).

Fund your wallet:
Click the link below to fund your Sequence Wallet via Trails:

https://demo.trails.build/?mode=swap&toAddress=0xA715064b5601Aebf197aC84A469b72Bb7Dc6A646&toChainId=137&toToken=0x3c499c542cef5e3811e1192ce70d8cc03d5c3359&apiKey=AQAAAAAAAKhGHJc3N5V2AWqfJ1v9xZ2u0nA&theme=light
```

## Login
Expand Down Expand Up @@ -288,8 +299,18 @@ Configuration is stored in `~/.sequence-builder/config.json`:

- JWT token for authentication
- Environment settings
- Encrypted private key (if `SEQUENCE_PASSPHRASE` is set)

### Encrypted Key Storage

When `SEQUENCE_PASSPHRASE` is set as an environment variable, the CLI will:

1. **On `create-wallet` / `login`**: Encrypt the private key with AES-256-GCM and store it in config
2. **On all other commands**: Automatically decrypt and use the stored key (no `-k` flag needed)

The private key is encrypted using a key derived from `SEQUENCE_PASSPHRASE` via scrypt. Only the encrypted ciphertext, salt, and IV are stored -- never the raw key.

**Note**: Private keys are NOT stored. You must provide them with each command that requires signing.
To disable encrypted storage, simply unset the env var. You can always override with an explicit `-k` flag.

## Environment Support

Expand Down
3 changes: 2 additions & 1 deletion src/__tests__/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ describe('CLI commands', () => {

it('should output JSON format with --json flag', () => {
const result = runCli('create-wallet --json')
const output = result.stdout.trim().split('\n').slice(-4).join('\n') // Get JSON part
const raw = result.stdout.trim()
const output = raw.substring(raw.indexOf('{'), raw.lastIndexOf('}') + 1) // Get JSON part
expect(() => JSON.parse(output)).not.toThrow()
const json = JSON.parse(output)
expect(json).toHaveProperty('privateKey')
Expand Down
27 changes: 22 additions & 5 deletions src/commands/create-wallet.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Command } from 'commander'
import chalk from 'chalk'
import { generateWallet } from '../lib/wallet.js'
import { storeEncryptedKey } from '../lib/config.js'

export const createWalletCommand = new Command('create-wallet')
.description('Generate a new EOA keypair for use with Sequence Builder')
Expand All @@ -9,12 +10,16 @@ export const createWalletCommand = new Command('create-wallet')
try {
const wallet = generateWallet()

// Auto-encrypt and store if SEQUENCE_PASSPHRASE is set
const stored = storeEncryptedKey(wallet.privateKey)

if (options.json) {
console.log(
JSON.stringify(
{
privateKey: wallet.privateKey,
address: wallet.address,
keyStored: stored,
},
null,
2
Expand All @@ -29,14 +34,26 @@ export const createWalletCommand = new Command('create-wallet')
console.log(chalk.white('Private Key:'), chalk.yellow(wallet.privateKey))
console.log(chalk.white('Address: '), chalk.cyan(wallet.address))
console.log('')
console.log(
chalk.red.bold('IMPORTANT:'),
chalk.white('Store these credentials securely. They will not be shown again.')
)
if (stored) {
console.log(
chalk.green('✓ Private key encrypted and stored.'),
chalk.gray("You won't need to pass -k for future commands.")
)
} else {
console.log(
chalk.red.bold('IMPORTANT:'),
chalk.white('Store these credentials securely. They will not be shown again.')
)
console.log(
chalk.gray('Tip: Set SEQUENCE_PASSPHRASE env var to auto-encrypt and store the key.')
)
}
console.log('')
console.log(chalk.gray('To use this wallet:'))
console.log(chalk.gray(' 1. Fund it with native token for gas fees'))
console.log(chalk.gray(' 2. Run: sequence-builder login -k <your-private-key>'))
console.log(
chalk.gray(` 2. Run: sequence-builder login${stored ? '' : ' -k <your-private-key>'}`)
)
console.log('')
} catch (error) {
console.error(
Expand Down
23 changes: 20 additions & 3 deletions src/commands/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,27 @@ import { Command } from 'commander'
import chalk from 'chalk'
import { generateEthAuthProof } from '../lib/ethauth.js'
import { getAuthToken } from '../lib/api.js'
import { updateConfig, EXIT_CODES, isLoggedIn, getValidJwtToken } from '../lib/config.js'
import {
updateConfig,
EXIT_CODES,
isLoggedIn,
getValidJwtToken,
getPrivateKey,
storeEncryptedKey,
} from '../lib/config.js'
import { isValidPrivateKey, getAddressFromPrivateKey } from '../lib/wallet.js'

export const loginCommand = new Command('login')
.description('Authenticate with Sequence Builder using your private key')
.requiredOption('-k, --private-key <key>', 'Your wallet private key')
.option('-k, --private-key <key>', 'Your wallet private key (or use stored encrypted key)')
.option('-e, --email <email>', 'Email address to associate with your account')
.option('--json', 'Output in JSON format')
.option('--env <environment>', 'Environment to use (prod, dev)', 'prod')
.option('--api-url <url>', 'Custom API URL')
.action(async (options) => {
try {
const { privateKey, email, json, env, apiUrl } = options
const { email, json, env, apiUrl } = options
const privateKey = getPrivateKey(options)

// Validate private key format
if (!isValidPrivateKey(privateKey)) {
Expand Down Expand Up @@ -73,6 +81,9 @@ export const loginCommand = new Command('login')
apiUrl: apiUrl,
})

// Auto-encrypt and store private key if SEQUENCE_PASSPHRASE is set
const stored = storeEncryptedKey(privateKey)

if (json) {
console.log(
JSON.stringify(
Expand All @@ -93,6 +104,12 @@ export const loginCommand = new Command('login')
console.log('')
console.log(chalk.white('Address: '), chalk.cyan(address))
console.log(chalk.white('Expires: '), chalk.gray(response.auth.expiresAt))
if (stored) {
console.log(
chalk.green('Key: '),
chalk.green('Encrypted and stored (no need to pass -k again)')
)
}
console.log('')
console.log(chalk.gray('You can now use commands like:'))
console.log(chalk.gray(' sequence-builder projects'))
Expand Down
36 changes: 32 additions & 4 deletions src/commands/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Command } from 'commander'
import chalk from 'chalk'
import { Session } from '@0xsequence/auth'
import { listProjects, createProject, getProject, getDefaultAccessKey } from '../lib/api.js'
import { isLoggedIn, EXIT_CODES } from '../lib/config.js'
import { isLoggedIn, EXIT_CODES, loadConfig } from '../lib/config.js'
import { isValidPrivateKey } from '../lib/wallet.js'

export const projectsCommand = new Command('projects')
Expand Down Expand Up @@ -160,11 +160,39 @@ async function createProjectAction(
// Access key fetch failed, but project was created
}

// Derive Sequence wallet address if private key is provided
// Derive Sequence wallet address if private key is available (explicit or stored)
let resolvedPrivateKey = privateKey
if (!resolvedPrivateKey) {
// Try to get stored key without exiting -- it's optional for project creation
try {
const config = loadConfig()
const passphrase = process.env.SEQUENCE_PASSPHRASE
if (
config.encryptedPrivateKey &&
config.encryptionSalt &&
config.encryptionIv &&
passphrase
) {
const { decryptPrivateKey } = await import('../lib/crypto.js')
resolvedPrivateKey = decryptPrivateKey(
{
encrypted: config.encryptedPrivateKey,
salt: config.encryptionSalt,
iv: config.encryptionIv,
},
passphrase
)
}
} catch {
// No stored key available, that's fine for project creation
}
}
let sequenceWalletAddress: string | undefined
if (privateKey && accessKey && isValidPrivateKey(privateKey)) {
if (resolvedPrivateKey && accessKey && isValidPrivateKey(resolvedPrivateKey)) {
try {
const normalizedKey = privateKey.startsWith('0x') ? privateKey : '0x' + privateKey
const normalizedKey = resolvedPrivateKey.startsWith('0x')
? resolvedPrivateKey
: '0x' + resolvedPrivateKey
const session = await Session.singleSigner({
signer: normalizedKey,
projectAccessKey: accessKey,
Expand Down
8 changes: 5 additions & 3 deletions src/commands/transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Command } from 'commander'
import chalk from 'chalk'
import { ethers } from 'ethers'
import { Session } from '@0xsequence/auth'
import { EXIT_CODES } from '../lib/config.js'
import { EXIT_CODES, getPrivateKey } from '../lib/config.js'
import { isValidPrivateKey } from '../lib/wallet.js'

// ERC20 ABI for transfer function
Expand All @@ -15,20 +15,22 @@ const ERC20_ABI = [

export const transferCommand = new Command('transfer')
.description('Send an ERC20 token transfer using Sequence smart wallet')
.requiredOption('-k, --private-key <key>', 'Your wallet private key')
.option('-k, --private-key <key>', 'Your wallet private key (or use stored encrypted key)')
.requiredOption('-a, --access-key <key>', 'Project access key')
.requiredOption('-t, --token <address>', 'ERC20 token contract address')
.requiredOption('-r, --recipient <address>', 'Recipient address')
.requiredOption('-m, --amount <amount>', 'Amount to send (in token units, e.g., "1.5")')
.requiredOption('-c, --chain-id <chainId>', 'Chain ID (e.g., 137 for Polygon)')
.option('--json', 'Output in JSON format')
.action(async (options) => {
const { privateKey, accessKey, token, recipient, amount, chainId: chainIdStr, json } = options
const { accessKey, token, recipient, amount, chainId: chainIdStr, json } = options

// Track wallet address for error reporting
let walletAddress: string | undefined

try {
const privateKey = getPrivateKey(options)

// Validate private key format
if (!isValidPrivateKey(privateKey)) {
if (json) {
Expand Down
16 changes: 13 additions & 3 deletions src/commands/wallet-info.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { Command } from 'commander'
import chalk from 'chalk'
import { Session } from '@0xsequence/auth'
import { EXIT_CODES } from '../lib/config.js'
import { EXIT_CODES, getPrivateKey } from '../lib/config.js'
import { isValidPrivateKey, getAddressFromPrivateKey } from '../lib/wallet.js'

export const walletInfoCommand = new Command('wallet-info')
.description('Show wallet addresses (EOA and Sequence smart wallet)')
.requiredOption('-k, --private-key <key>', 'Your wallet private key')
.option('-k, --private-key <key>', 'Your wallet private key (or use stored encrypted key)')
.requiredOption('-a, --access-key <key>', 'Project access key')
.option('--json', 'Output in JSON format')
.action(async (options) => {
const { privateKey, accessKey, json } = options
const { accessKey, json } = options

try {
const privateKey = getPrivateKey(options)

// Validate private key format
if (!isValidPrivateKey(privateKey)) {
if (json) {
Expand Down Expand Up @@ -46,12 +48,15 @@ export const walletInfoCommand = new Command('wallet-info')

const sequenceWalletAddress = session.account.address

const fundingUrl = `https://demo.trails.build/?mode=swap&toAddress=${sequenceWalletAddress}&toChainId=137&toToken=0x3c499c542cef5e3811e1192ce70d8cc03d5c3359&apiKey=AQAAAAAAAKhGHJc3N5V2AWqfJ1v9xZ2u0nA&theme=light`

if (json) {
console.log(
JSON.stringify(
{
eoaAddress,
sequenceWalletAddress,
fundingUrl,
},
null,
2
Expand All @@ -76,6 +81,11 @@ export const walletInfoCommand = new Command('wallet-info')
chalk.yellow(' The Sequence Wallet can pay gas fees with ERC20 tokens (no ETH needed).')
)
console.log('')
console.log(chalk.green.bold('Fund your wallet:'))
console.log(chalk.green(' Click the link below to fund your Sequence Wallet via Trails:'))
console.log('')
console.log(chalk.cyan.underline(fundingUrl))
console.log('')
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
if (json) {
Expand Down
Loading