Skip to content

Commit bf11676

Browse files
added trails funding flow and encryption (#1)
* added trails funding flow and encryption * Fix test and format --------- Co-authored-by: Ahmet Buğra Yiğiter <yigiterahmetbugra@gmail.com>
1 parent 9038509 commit bf11676

File tree

9 files changed

+246
-22
lines changed

9 files changed

+246
-22
lines changed

README.md

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ npm install -g @0xsequence/builder-cli
1717
sequence-builder --help
1818
```
1919

20-
## Quick Start for Agents
20+
## Private Key Encryption
21+
22+
Set `SEQUENCE_PASSPHRASE` in your environment to auto-encrypt and store the private key. No need to pass `-k` after wallet creation.
23+
24+
```bash
25+
export SEQUENCE_PASSPHRASE="your-strong-secret"
26+
```
2127

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

37-
# 6. Fund the Sequence wallet with tokens
43+
# 6. Fund the Sequence wallet via the Trails link from step 5
3844

3945
# 7. Send an ERC20 transfer (gas paid with same token!)
4046
npx @0xsequence/builder-cli transfer \
@@ -117,6 +123,11 @@ Sequence Wallet: 0xA715064b5601Aebf197aC84A469b72Bb7Dc6A646
117123
Important:
118124
Send tokens to the Sequence Wallet address for use with the transfer command.
119125
The Sequence Wallet can pay gas fees with ERC20 tokens (no ETH needed).
126+
127+
Fund your wallet:
128+
Click the link below to fund your Sequence Wallet via Trails:
129+
130+
https://demo.trails.build/?mode=swap&toAddress=0xA715064b5601Aebf197aC84A469b72Bb7Dc6A646&toChainId=137&toToken=0x3c499c542cef5e3811e1192ce70d8cc03d5c3359&apiKey=AQAAAAAAAKhGHJc3N5V2AWqfJ1v9xZ2u0nA&theme=light
120131
```
121132

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

289300
- JWT token for authentication
290301
- Environment settings
302+
- Encrypted private key (if `SEQUENCE_PASSPHRASE` is set)
303+
304+
### Encrypted Key Storage
305+
306+
When `SEQUENCE_PASSPHRASE` is set as an environment variable, the CLI will:
307+
308+
1. **On `create-wallet` / `login`**: Encrypt the private key with AES-256-GCM and store it in config
309+
2. **On all other commands**: Automatically decrypt and use the stored key (no `-k` flag needed)
310+
311+
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.
291312

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

294315
## Environment Support
295316

src/__tests__/cli.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ describe('CLI commands', () => {
4444

4545
it('should output JSON format with --json flag', () => {
4646
const result = runCli('create-wallet --json')
47-
const output = result.stdout.trim().split('\n').slice(-4).join('\n') // Get JSON part
47+
const raw = result.stdout.trim()
48+
const output = raw.substring(raw.indexOf('{'), raw.lastIndexOf('}') + 1) // Get JSON part
4849
expect(() => JSON.parse(output)).not.toThrow()
4950
const json = JSON.parse(output)
5051
expect(json).toHaveProperty('privateKey')

src/commands/create-wallet.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Command } from 'commander'
22
import chalk from 'chalk'
33
import { generateWallet } from '../lib/wallet.js'
4+
import { storeEncryptedKey } from '../lib/config.js'
45

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

13+
// Auto-encrypt and store if SEQUENCE_PASSPHRASE is set
14+
const stored = storeEncryptedKey(wallet.privateKey)
15+
1216
if (options.json) {
1317
console.log(
1418
JSON.stringify(
1519
{
1620
privateKey: wallet.privateKey,
1721
address: wallet.address,
22+
keyStored: stored,
1823
},
1924
null,
2025
2
@@ -29,14 +34,26 @@ export const createWalletCommand = new Command('create-wallet')
2934
console.log(chalk.white('Private Key:'), chalk.yellow(wallet.privateKey))
3035
console.log(chalk.white('Address: '), chalk.cyan(wallet.address))
3136
console.log('')
32-
console.log(
33-
chalk.red.bold('IMPORTANT:'),
34-
chalk.white('Store these credentials securely. They will not be shown again.')
35-
)
37+
if (stored) {
38+
console.log(
39+
chalk.green('✓ Private key encrypted and stored.'),
40+
chalk.gray("You won't need to pass -k for future commands.")
41+
)
42+
} else {
43+
console.log(
44+
chalk.red.bold('IMPORTANT:'),
45+
chalk.white('Store these credentials securely. They will not be shown again.')
46+
)
47+
console.log(
48+
chalk.gray('Tip: Set SEQUENCE_PASSPHRASE env var to auto-encrypt and store the key.')
49+
)
50+
}
3651
console.log('')
3752
console.log(chalk.gray('To use this wallet:'))
3853
console.log(chalk.gray(' 1. Fund it with native token for gas fees'))
39-
console.log(chalk.gray(' 2. Run: sequence-builder login -k <your-private-key>'))
54+
console.log(
55+
chalk.gray(` 2. Run: sequence-builder login${stored ? '' : ' -k <your-private-key>'}`)
56+
)
4057
console.log('')
4158
} catch (error) {
4259
console.error(

src/commands/login.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,27 @@ import { Command } from 'commander'
22
import chalk from 'chalk'
33
import { generateEthAuthProof } from '../lib/ethauth.js'
44
import { getAuthToken } from '../lib/api.js'
5-
import { updateConfig, EXIT_CODES, isLoggedIn, getValidJwtToken } from '../lib/config.js'
5+
import {
6+
updateConfig,
7+
EXIT_CODES,
8+
isLoggedIn,
9+
getValidJwtToken,
10+
getPrivateKey,
11+
storeEncryptedKey,
12+
} from '../lib/config.js'
613
import { isValidPrivateKey, getAddressFromPrivateKey } from '../lib/wallet.js'
714

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

1927
// Validate private key format
2028
if (!isValidPrivateKey(privateKey)) {
@@ -73,6 +81,9 @@ export const loginCommand = new Command('login')
7381
apiUrl: apiUrl,
7482
})
7583

84+
// Auto-encrypt and store private key if SEQUENCE_PASSPHRASE is set
85+
const stored = storeEncryptedKey(privateKey)
86+
7687
if (json) {
7788
console.log(
7889
JSON.stringify(
@@ -93,6 +104,12 @@ export const loginCommand = new Command('login')
93104
console.log('')
94105
console.log(chalk.white('Address: '), chalk.cyan(address))
95106
console.log(chalk.white('Expires: '), chalk.gray(response.auth.expiresAt))
107+
if (stored) {
108+
console.log(
109+
chalk.green('Key: '),
110+
chalk.green('Encrypted and stored (no need to pass -k again)')
111+
)
112+
}
96113
console.log('')
97114
console.log(chalk.gray('You can now use commands like:'))
98115
console.log(chalk.gray(' sequence-builder projects'))

src/commands/projects.ts

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Command } from 'commander'
22
import chalk from 'chalk'
33
import { Session } from '@0xsequence/auth'
44
import { listProjects, createProject, getProject, getDefaultAccessKey } from '../lib/api.js'
5-
import { isLoggedIn, EXIT_CODES } from '../lib/config.js'
5+
import { isLoggedIn, EXIT_CODES, loadConfig } from '../lib/config.js'
66
import { isValidPrivateKey } from '../lib/wallet.js'
77

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

163-
// Derive Sequence wallet address if private key is provided
163+
// Derive Sequence wallet address if private key is available (explicit or stored)
164+
let resolvedPrivateKey = privateKey
165+
if (!resolvedPrivateKey) {
166+
// Try to get stored key without exiting -- it's optional for project creation
167+
try {
168+
const config = loadConfig()
169+
const passphrase = process.env.SEQUENCE_PASSPHRASE
170+
if (
171+
config.encryptedPrivateKey &&
172+
config.encryptionSalt &&
173+
config.encryptionIv &&
174+
passphrase
175+
) {
176+
const { decryptPrivateKey } = await import('../lib/crypto.js')
177+
resolvedPrivateKey = decryptPrivateKey(
178+
{
179+
encrypted: config.encryptedPrivateKey,
180+
salt: config.encryptionSalt,
181+
iv: config.encryptionIv,
182+
},
183+
passphrase
184+
)
185+
}
186+
} catch {
187+
// No stored key available, that's fine for project creation
188+
}
189+
}
164190
let sequenceWalletAddress: string | undefined
165-
if (privateKey && accessKey && isValidPrivateKey(privateKey)) {
191+
if (resolvedPrivateKey && accessKey && isValidPrivateKey(resolvedPrivateKey)) {
166192
try {
167-
const normalizedKey = privateKey.startsWith('0x') ? privateKey : '0x' + privateKey
193+
const normalizedKey = resolvedPrivateKey.startsWith('0x')
194+
? resolvedPrivateKey
195+
: '0x' + resolvedPrivateKey
168196
const session = await Session.singleSigner({
169197
signer: normalizedKey,
170198
projectAccessKey: accessKey,

src/commands/transfer.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Command } from 'commander'
22
import chalk from 'chalk'
33
import { ethers } from 'ethers'
44
import { Session } from '@0xsequence/auth'
5-
import { EXIT_CODES } from '../lib/config.js'
5+
import { EXIT_CODES, getPrivateKey } from '../lib/config.js'
66
import { isValidPrivateKey } from '../lib/wallet.js'
77

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

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

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

3131
try {
32+
const privateKey = getPrivateKey(options)
33+
3234
// Validate private key format
3335
if (!isValidPrivateKey(privateKey)) {
3436
if (json) {

src/commands/wallet-info.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import { Command } from 'commander'
22
import chalk from 'chalk'
33
import { Session } from '@0xsequence/auth'
4-
import { EXIT_CODES } from '../lib/config.js'
4+
import { EXIT_CODES, getPrivateKey } from '../lib/config.js'
55
import { isValidPrivateKey, getAddressFromPrivateKey } from '../lib/wallet.js'
66

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

1515
try {
16+
const privateKey = getPrivateKey(options)
17+
1618
// Validate private key format
1719
if (!isValidPrivateKey(privateKey)) {
1820
if (json) {
@@ -46,12 +48,15 @@ export const walletInfoCommand = new Command('wallet-info')
4648

4749
const sequenceWalletAddress = session.account.address
4850

51+
const fundingUrl = `https://demo.trails.build/?mode=swap&toAddress=${sequenceWalletAddress}&toChainId=137&toToken=0x3c499c542cef5e3811e1192ce70d8cc03d5c3359&apiKey=AQAAAAAAAKhGHJc3N5V2AWqfJ1v9xZ2u0nA&theme=light`
52+
4953
if (json) {
5054
console.log(
5155
JSON.stringify(
5256
{
5357
eoaAddress,
5458
sequenceWalletAddress,
59+
fundingUrl,
5560
},
5661
null,
5762
2
@@ -76,6 +81,11 @@ export const walletInfoCommand = new Command('wallet-info')
7681
chalk.yellow(' The Sequence Wallet can pay gas fees with ERC20 tokens (no ETH needed).')
7782
)
7883
console.log('')
84+
console.log(chalk.green.bold('Fund your wallet:'))
85+
console.log(chalk.green(' Click the link below to fund your Sequence Wallet via Trails:'))
86+
console.log('')
87+
console.log(chalk.cyan.underline(fundingUrl))
88+
console.log('')
7989
} catch (error) {
8090
const errorMessage = error instanceof Error ? error.message : String(error)
8191
if (json) {

0 commit comments

Comments
 (0)