Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 22.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 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
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()

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 22.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 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
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 22.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 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
.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 22.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 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
`${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 22.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 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
)
)

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 22.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 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
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