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
1 change: 0 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
"conf": "^13.0.1",
"ink": "^6.3.1",
"ink-spinner": "^5.0.0",
"picocolors": "^1.1.1",
"react": "^19.2.0",
"viem": "^2.21.8",
"zod": "^3.24.1"
Expand Down
62 changes: 42 additions & 20 deletions src/commands/account/create.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import * as p from '@clack/prompts'
import pc from 'picocolors'
import { type Address } from 'viem'
import { getConfigStore } from '../../storage/config-store.js'
import { getSafeStorage } from '../../storage/safe-store.js'
import { getWalletStorage } from '../../storage/wallet-store.js'
import { SafeService } from '../../services/safe-service.js'
import { isValidAddress } from '../../utils/validation.js'
import { checksumAddress, shortenAddress } from '../../utils/ethereum.js'
import { formatSafeAddress } from '../../utils/eip3770.js'
import { logError } from '../../ui/messages.js'
import { renderScreen } from '../../ui/render.js'
import { AccountCreateSuccessScreen } from '../../ui/screens/index.js'

export async function createSafe() {
p.intro(pc.bgCyan(pc.black(' Create Safe Account ')))
p.intro('Create Safe Account')

const configStore = getConfigStore()
const safeStorage = getSafeStorage()
Expand All @@ -27,7 +27,7 @@ export async function createSafe() {
}

console.log('')
console.log(pc.dim(`Active wallet: ${activeWallet.name} (${activeWallet.address})`))
console.log(`Active wallet: ${activeWallet.name} (${activeWallet.address})`)
console.log('')

// Select chain
Expand Down Expand Up @@ -62,7 +62,7 @@ export async function createSafe() {

if (includeActiveWallet) {
owners.push(checksumAddress(activeWallet.address))
console.log(pc.green(`✓ Added ${shortenAddress(activeWallet.address)}`))
console.log(`✓ Added ${shortenAddress(activeWallet.address)}`)
}

// Add more owners
Expand Down Expand Up @@ -102,7 +102,7 @@ export async function createSafe() {

const checksummed = checksumAddress(ownerAddress as string)
owners.push(checksummed)
console.log(pc.green(`✓ Added ${shortenAddress(checksummed)}`))
console.log(`✓ Added ${shortenAddress(checksummed)}`)
}

if (owners.length === 0) {
Expand Down Expand Up @@ -145,18 +145,16 @@ export async function createSafe() {

// Summary
console.log('')
console.log(pc.bold('📋 Safe Configuration Summary'))
console.log('Safe Configuration Summary')
console.log('')
console.log(` ${pc.dim('Chain:')} ${chain.name} (${chain.chainId})`)
console.log(` ${pc.dim('Version:')} 1.4.1`)
console.log(` ${pc.dim('Owners:')} ${owners.length}`)
console.log(` Chain: ${chain.name} (${chain.chainId})`)
console.log(` Version: 1.4.1`)
console.log(` Owners: ${owners.length}`)
owners.forEach((owner, i) => {
const isActive = owner.toLowerCase() === activeWallet.address.toLowerCase()
console.log(
` ${pc.dim(`${i + 1}.`)} ${shortenAddress(owner)}${isActive ? pc.green(' (you)') : ''}`
)
console.log(` ${i + 1}. ${shortenAddress(owner)}${isActive ? ' (you)' : ''}`)
})
console.log(` ${pc.dim('Threshold:')} ${thresholdNum} / ${owners.length}`)
console.log(` Threshold: ${thresholdNum} / ${owners.length}`)
console.log('')

const spinner = p.spinner()
Expand Down Expand Up @@ -203,7 +201,7 @@ export async function createSafe() {
throw new Error(`Could not find available Safe address after ${maxAttempts} attempts`)
}

spinner.stop('Safe created!')
spinner.stop()

// Save to storage
const safe = safeStorage.createSafe({
Expand All @@ -218,13 +216,37 @@ export async function createSafe() {
},
})

// Display success screen with Safe details and next steps
await renderScreen(AccountCreateSuccessScreen, {
name: safe.name,
address: safe.address as Address,
chainId: safe.chainId,
chainName: chain.name,
// Show brief success message
const allChains = configStore.getAllChains()
const eip3770 = formatSafeAddress(safe.address as Address, safe.chainId, allChains)
console.log('')
console.log('✓ Safe created successfully!')
console.log('')
console.log(` Name: ${safe.name}`)
console.log(` Address: ${eip3770}`)
console.log(` Chain: ${chain.name}`)
console.log(` Status: Not deployed`)
console.log('')

// Offer to deploy the Safe
const shouldDeploy = await p.confirm({
message: 'Would you like to deploy this Safe now?',
initialValue: true,
})

if (!p.isCancel(shouldDeploy) && shouldDeploy) {
console.log('')
const { deploySafe } = await import('./deploy.js')
await deploySafe(eip3770)
} else {
// Show full success screen with next steps
await renderScreen(AccountCreateSuccessScreen, {
name: safe.name,
address: safe.address as Address,
chainId: safe.chainId,
chainName: chain.name,
})
}
} catch (error) {
spinner.stop('Failed to create Safe')
logError(error instanceof Error ? error.message : 'Unknown error')
Expand Down
17 changes: 8 additions & 9 deletions src/commands/account/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as p from '@clack/prompts'
import pc from 'picocolors'
import { type Address } from 'viem'
import { getConfigStore } from '../../storage/config-store.js'
import { getSafeStorage } from '../../storage/safe-store.js'
Expand All @@ -11,7 +10,7 @@ import { renderScreen } from '../../ui/render.js'
import { AccountDeploySuccessScreen } from '../../ui/screens/index.js'

export async function deploySafe(account?: string) {
p.intro(pc.bgCyan(pc.black(' Deploy Safe ')))
p.intro('Deploy Safe')

const configStore = getConfigStore()
const safeStorage = getSafeStorage()
Expand Down Expand Up @@ -100,7 +99,7 @@ export async function deploySafe(account?: string) {
}
} catch {
// If we can't verify, log warning but continue
console.log(pc.yellow('⚠ Warning: Could not verify on-chain deployment status'))
console.log('⚠ Warning: Could not verify on-chain deployment status')
}

// Get active wallet
Expand All @@ -114,15 +113,15 @@ export async function deploySafe(account?: string) {
const eip3770 = formatSafeAddress(safe.address as Address, safe.chainId, chains)

console.log('')
console.log(pc.bold('Safe to Deploy:'))
console.log(` ${pc.dim('Name:')} ${safe.name}`)
console.log(` ${pc.dim('Address:')} ${pc.cyan(eip3770)}`)
console.log(` ${pc.dim('Chain:')} ${chain.name}`)
console.log('Safe to Deploy:')
console.log(` Name: ${safe.name}`)
console.log(` Address: ${eip3770}`)
console.log(` Chain: ${chain.name}`)
console.log(
` ${pc.dim('Owners:')} ${safe.predictedConfig.threshold} / ${safe.predictedConfig.owners.length}`
` Owners: ${safe.predictedConfig.threshold} / ${safe.predictedConfig.owners.length}`
)
console.log('')
console.log(pc.dim(`Deploying with wallet: ${activeWallet.name} (${activeWallet.address})`))
console.log(`Deploying with wallet: ${activeWallet.name} (${activeWallet.address})`)
console.log('')

const confirm = await p.confirm({
Expand Down
7 changes: 3 additions & 4 deletions src/commands/config/chains.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as p from '@clack/prompts'
import pc from 'picocolors'
import { getConfigStore } from '../../storage/config-store.js'
import type { ChainConfig } from '../../types/config.js'
import { isValidChainId, isValidUrl } from '../../utils/validation.js'
Expand All @@ -12,7 +11,7 @@ import {
} from '../../ui/screens/index.js'

export async function addChain() {
p.intro(pc.bgCyan(pc.black(' Add Chain ')))
p.intro('Add Chain')

const configStore = getConfigStore()

Expand Down Expand Up @@ -138,7 +137,7 @@ export async function addChain() {
}

export async function listChains() {
p.intro(pc.bgCyan(pc.black(' Configured Chains ')))
p.intro('Configured Chains')

const configStore = getConfigStore()
const chains = Object.values(configStore.getAllChains())
Expand All @@ -149,7 +148,7 @@ export async function listChains() {
}

export async function removeChain() {
p.intro(pc.bgCyan(pc.black(' Remove Chain ')))
p.intro('Remove Chain')

const configStore = getConfigStore()
const chains = configStore.getAllChains()
Expand Down
67 changes: 43 additions & 24 deletions src/commands/tx/create.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as p from '@clack/prompts'
import pc from 'picocolors'
import { isAddress, type Address } from 'viem'
import { getConfigStore } from '../../storage/config-store.js'
import { getSafeStorage } from '../../storage/safe-store.js'
Expand Down Expand Up @@ -140,7 +139,7 @@
try {
isContract = await contractService.isContract(to)
spinner2.stop(isContract ? 'Contract detected' : 'EOA (regular address)')
} catch (error) {

Check warning on line 142 in src/commands/tx/create.ts

View workflow job for this annotation

GitHub Actions / Test on Node.js 18.x

'error' is defined but never used

Check warning on line 142 in src/commands/tx/create.ts

View workflow job for this annotation

GitHub Actions / Test on Node.js 20.x

'error' is defined but never used

Check warning on line 142 in src/commands/tx/create.ts

View workflow job for this annotation

GitHub Actions / Test on Node.js 22.x

'error' is defined but never used
spinner2.stop('Failed to check contract')
p.log.warning('Could not determine if address is a contract, falling back to manual input')
}
Expand All @@ -148,19 +147,19 @@
// If contract, try to fetch ABI and use transaction builder
if (isContract) {
console.log('')
console.log(pc.dim('Attempting to fetch contract ABI...'))
console.log('Attempting to fetch contract ABI...')

const config = configStore.getConfig()
const etherscanApiKey = config.preferences?.etherscanApiKey

// Inform user about ABI source based on API key availability
if (!etherscanApiKey) {
console.log(pc.dim(' Using Sourcify for ABI (free, no API key required)'))
console.log(pc.dim(' Note: Proxy contract detection requires an Etherscan API key'))
console.log(' Using Sourcify for ABI (free, no API key required)')
console.log(' Note: Proxy contract detection requires an Etherscan API key')
}

const abiService = new ABIService(chain, etherscanApiKey)
let abi: any = null

Check warning on line 162 in src/commands/tx/create.ts

View workflow job for this annotation

GitHub Actions / Test on Node.js 18.x

Unexpected any. Specify a different type

Check warning on line 162 in src/commands/tx/create.ts

View workflow job for this annotation

GitHub Actions / Test on Node.js 20.x

Unexpected any. Specify a different type

Check warning on line 162 in src/commands/tx/create.ts

View workflow job for this annotation

GitHub Actions / Test on Node.js 22.x

Unexpected any. Specify a different type
let contractName: string | undefined

try {
Expand All @@ -171,18 +170,18 @@

// Check if Etherscan detected this as a proxy
if (implementationAddress) {
console.log(pc.cyan(`✓ Proxy detected! Implementation: ${implementationAddress}`))
console.log(`✓ Proxy detected! Implementation: ${implementationAddress}`)

if (contractName) {
console.log(pc.green(`✓ Proxy ABI found: ${pc.bold(contractName)}`))
console.log(`✓ Proxy ABI found: ${contractName}`)
} else {
console.log(pc.green('✓ Proxy ABI found!'))
console.log('✓ Proxy ABI found!')
}
} else {
if (contractName) {
console.log(pc.green(`✓ Contract ABI found: ${pc.bold(contractName)}`))
console.log(`✓ Contract ABI found: ${contractName}`)
} else {
console.log(pc.green('✓ Contract ABI found!'))
console.log('✓ Contract ABI found!')
}
}

Expand All @@ -195,9 +194,9 @@
// Use implementation name as the main contract name
if (implInfo.name) {
contractName = implInfo.name
console.log(pc.green(`✓ Implementation ABI found: ${pc.bold(implInfo.name)}`))
console.log(`✓ Implementation ABI found: ${implInfo.name}`)
} else {
console.log(pc.green('✓ Implementation ABI found!'))
console.log('✓ Implementation ABI found!')
}

// Merge ABIs (implementation functions + proxy functions)
Expand All @@ -205,16 +204,16 @@
const combinedAbi = [...implAbi]
const existingSignatures = new Set(
implAbi
.filter((item: any) => item.type === 'function')

Check warning on line 207 in src/commands/tx/create.ts

View workflow job for this annotation

GitHub Actions / Test on Node.js 18.x

Unexpected any. Specify a different type

Check warning on line 207 in src/commands/tx/create.ts

View workflow job for this annotation

GitHub Actions / Test on Node.js 20.x

Unexpected any. Specify a different type

Check warning on line 207 in src/commands/tx/create.ts

View workflow job for this annotation

GitHub Actions / Test on Node.js 22.x

Unexpected any. Specify a different type
.map(
(item: any) =>

Check warning on line 209 in src/commands/tx/create.ts

View workflow job for this annotation

GitHub Actions / Test on Node.js 18.x

Unexpected any. Specify a different type

Check warning on line 209 in src/commands/tx/create.ts

View workflow job for this annotation

GitHub Actions / Test on Node.js 20.x

Unexpected any. Specify a different type

Check warning on line 209 in src/commands/tx/create.ts

View workflow job for this annotation

GitHub Actions / Test on Node.js 22.x

Unexpected any. Specify a different type
`${item.name}(${item.inputs?.map((i: any) => i.type).join(',') || ''})`

Check warning on line 210 in src/commands/tx/create.ts

View workflow job for this annotation

GitHub Actions / Test on Node.js 18.x

Unexpected any. Specify a different type

Check warning on line 210 in src/commands/tx/create.ts

View workflow job for this annotation

GitHub Actions / Test on Node.js 20.x

Unexpected any. Specify a different type

Check warning on line 210 in src/commands/tx/create.ts

View workflow job for this annotation

GitHub Actions / Test on Node.js 22.x

Unexpected any. Specify a different type
)
)

for (const item of abi) {
if (item.type === 'function') {
const sig = `${item.name}(${item.inputs?.map((i: any) => i.type).join(',') || ''})`

Check warning on line 216 in src/commands/tx/create.ts

View workflow job for this annotation

GitHub Actions / Test on Node.js 18.x

Unexpected any. Specify a different type

Check warning on line 216 in src/commands/tx/create.ts

View workflow job for this annotation

GitHub Actions / Test on Node.js 20.x

Unexpected any. Specify a different type

Check warning on line 216 in src/commands/tx/create.ts

View workflow job for this annotation

GitHub Actions / Test on Node.js 22.x

Unexpected any. Specify a different type
if (!existingSignatures.has(sig)) {
combinedAbi.push(item)
}
Expand All @@ -225,17 +224,17 @@
}

abi = combinedAbi
console.log(pc.dim(` Combined: ${abi.length} items total`))
console.log(` Combined: ${abi.length} items total`)
} catch (error) {
console.log(pc.yellow('⚠ Could not fetch implementation ABI, using proxy ABI only'))
console.log(pc.dim(` Found ${abi.length} items in proxy ABI`))
console.log('⚠ Could not fetch implementation ABI, using proxy ABI only')
console.log(` Found ${abi.length} items in proxy ABI`)
}
} else {
console.log(pc.dim(` Found ${abi.length} items in ABI`))
console.log(` Found ${abi.length} items in ABI`)
}
} catch (error) {
console.log(pc.yellow('⚠ Could not fetch ABI'))
console.log(pc.dim(' Contract may not be verified. Falling back to manual input.'))
console.log('⚠ Could not fetch ABI')
console.log(' Contract may not be verified. Falling back to manual input.')
}

// If ABI found, offer transaction builder
Expand All @@ -244,7 +243,7 @@

console.log('')
if (functions.length > 0) {
console.log(pc.green(`✓ Found ${functions.length} writable function(s)`))
console.log(`✓ Found ${functions.length} writable function(s)`)

const useBuilder = await p.confirm({
message: 'Use transaction builder to interact with contract?',
Expand Down Expand Up @@ -295,9 +294,9 @@
data = result.data
}
} else {
console.log(pc.yellow('⚠ No writable functions found in ABI'))
console.log(pc.dim(' Contract may only have view/pure functions'))
console.log(pc.dim(' Falling back to manual input'))
console.log('⚠ No writable functions found in ABI')
console.log(' Contract may only have view/pure functions')
console.log(' Falling back to manual input')
}
}
}
Expand Down Expand Up @@ -413,11 +412,31 @@
activeWallet.address as Address
)

createSpinner.stop('Transaction created')
createSpinner.stop()

await renderScreen(TransactionCreateSuccessScreen, {
safeTxHash: createdTx.safeTxHash,
// Show transaction hash
console.log('')
console.log('✓ Transaction created successfully!')
console.log('')
console.log(` Safe TX Hash: ${createdTx.safeTxHash}`)
console.log('')

// Offer to sign the transaction
const shouldSign = await p.confirm({
message: 'Would you like to sign this transaction now?',
initialValue: true,
})

if (!p.isCancel(shouldSign) && shouldSign) {
console.log('')
const { signTransaction } = await import('./sign.js')
await signTransaction(createdTx.safeTxHash)
} else {
// Show full success screen with next steps
await renderScreen(TransactionCreateSuccessScreen, {
safeTxHash: createdTx.safeTxHash,
})
}
} catch (error) {
if (error instanceof SafeCLIError) {
p.log.error(error.message)
Expand Down
3 changes: 1 addition & 2 deletions src/commands/tx/pull.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as p from '@clack/prompts'
import pc from 'picocolors'
import type { Address } from 'viem'
import { getConfigStore } from '../../storage/config-store.js'
import { getSafeStorage } from '../../storage/safe-store.js'
Expand All @@ -12,7 +11,7 @@ import { renderScreen } from '../../ui/render.js'
import { TransactionPullSuccessScreen, type TransactionPullResult } from '../../ui/screens/index.js'

export async function pullTransactions(account?: string) {
p.intro(pc.bgCyan(pc.black(' Pull Transactions from Safe API ')))
p.intro('Pull Transactions from Safe API')

try {
const configStore = getConfigStore()
Expand Down
Loading
Loading