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
63 changes: 39 additions & 24 deletions src/commands/account/change-threshold.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,32 +88,55 @@ export async function changeThreshold(account?: string) {
return
}

// Check if wallet is an owner
if (!safe.owners || !safe.threshold) {
p.log.error('Safe owner information not available. Please sync Safe data.')
// Get chain
const chain = configStore.getChain(safe.chainId)
if (!chain) {
p.log.error(`Chain ${safe.chainId} not found in configuration`)
p.outro('Failed')
return
}

// Fetch live owners and threshold from blockchain
const spinner = p.spinner()
spinner.start('Fetching Safe information from blockchain...')

let owners: Address[]
let currentThreshold: number
try {
const txService = new TransactionService(chain)
;[owners, currentThreshold] = await Promise.all([
txService.getOwners(safe.address as Address),
txService.getThreshold(safe.address as Address),
])
spinner.stop('Safe information fetched')
} catch (error) {
spinner.stop('Failed to fetch Safe information')
p.log.error(
error instanceof Error ? error.message : 'Failed to fetch Safe data from blockchain'
)
p.outro('Failed')
return
}

if (!safe.owners.some((owner) => owner.toLowerCase() === activeWallet.address.toLowerCase())) {
// Check if wallet is an owner
if (!owners.some((owner) => owner.toLowerCase() === activeWallet.address.toLowerCase())) {
p.log.error('Active wallet is not an owner of this Safe')
p.outro('Failed')
return
}

// Ask for new threshold
const newThreshold = await p.text({
message: `New threshold (current: ${safe.threshold}, max: ${safe.owners.length}):`,
placeholder: `${safe.threshold}`,
message: `New threshold (current: ${currentThreshold}, max: ${owners.length}):`,
placeholder: `${currentThreshold}`,
validate: (value) => {
if (!value) return 'Threshold is required'
const num = parseInt(value, 10)
if (isNaN(num) || num < 1) return 'Threshold must be at least 1'
if (!safe.owners || !safe.threshold) return 'Safe data not available'
if (num > safe.owners.length) {
return `Threshold cannot exceed ${safe.owners.length} (current owners)`
if (num > owners.length) {
return `Threshold cannot exceed ${owners.length} (current owners)`
}
if (num === safe.threshold) {
if (num === currentThreshold) {
return 'New threshold must be different from current threshold'
}
return undefined
Expand All @@ -131,8 +154,8 @@ export async function changeThreshold(account?: string) {
console.log('')
console.log(pc.bold('Change Threshold Summary:'))
console.log(` ${pc.dim('Safe:')} ${safe.name}`)
console.log(` ${pc.dim('Owners:')} ${safe.owners.length}`)
console.log(` ${pc.dim('Old Threshold:')} ${safe.threshold}`)
console.log(` ${pc.dim('Owners:')} ${owners.length}`)
console.log(` ${pc.dim('Old Threshold:')} ${currentThreshold}`)
console.log(` ${pc.dim('New Threshold:')} ${thresholdNum}`)
console.log('')

Expand All @@ -146,16 +169,8 @@ export async function changeThreshold(account?: string) {
return
}

// Get chain
const chain = configStore.getChain(safe.chainId)
if (!chain) {
p.log.error(`Chain ${safe.chainId} not found in configuration`)
p.outro('Failed')
return
}

const spinner = p.spinner()
spinner.start('Creating change threshold transaction...')
const spinner2 = p.spinner()
spinner2.start('Creating change threshold transaction...')

// Create the change threshold transaction using Safe SDK
const txService = new TransactionService(chain)
Expand All @@ -174,13 +189,13 @@ export async function changeThreshold(account?: string) {
activeWallet.address as Address
)

spinner.stop('Transaction created')
spinner2.stop('Transaction created')

await renderScreen(ThresholdChangeSuccessScreen, {
safeTxHash: safeTransaction.safeTxHash,
safeAddress: safe.address as Address,
chainId: safe.chainId,
oldThreshold: safe.threshold,
oldThreshold: currentThreshold,
newThreshold: thresholdNum,
})
} catch (error) {
Expand Down
61 changes: 38 additions & 23 deletions src/commands/account/remove-owner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,21 +88,45 @@ export async function removeOwner(account?: string) {
return
}

// Check if wallet is an owner
if (!safe.owners || !safe.threshold) {
p.log.error('Safe owner information not available. Please sync Safe data.')
// Get chain
const chain = configStore.getChain(safe.chainId)
if (!chain) {
p.log.error(`Chain ${safe.chainId} not found in configuration`)
p.outro('Failed')
return
}

// Fetch live owners and threshold from blockchain
const spinner = p.spinner()
spinner.start('Fetching Safe information from blockchain...')

let owners: Address[]
let currentThreshold: number
try {
const txService = new TransactionService(chain)
;[owners, currentThreshold] = await Promise.all([
txService.getOwners(safe.address as Address),
txService.getThreshold(safe.address as Address),
])
spinner.stop('Safe information fetched')
} catch (error) {
spinner.stop('Failed to fetch Safe information')
p.log.error(
error instanceof Error ? error.message : 'Failed to fetch Safe data from blockchain'
)
p.outro('Failed')
return
}

if (!safe.owners.some((owner) => owner.toLowerCase() === activeWallet.address.toLowerCase())) {
// Check if wallet is an owner
if (!owners.some((owner) => owner.toLowerCase() === activeWallet.address.toLowerCase())) {
p.log.error('Active wallet is not an owner of this Safe')
p.outro('Failed')
return
}

// Check that Safe has at least 2 owners
if (safe.owners.length <= 1) {
if (owners.length <= 1) {
p.log.error('Cannot remove the last owner from a Safe')
p.outro('Failed')
return
Expand All @@ -111,7 +135,7 @@ export async function removeOwner(account?: string) {
// Select owner to remove
const ownerToRemove = await p.select({
message: 'Select owner to remove:',
options: safe.owners.map((owner) => ({
options: owners.map((owner) => ({
value: owner,
label: owner,
})),
Expand All @@ -125,10 +149,9 @@ export async function removeOwner(account?: string) {
const removeAddress = ownerToRemove as Address

// Calculate max threshold after removal
const maxThreshold = safe.owners.length - 1
const maxThreshold = owners.length - 1

// Ask about threshold
const currentThreshold = safe.threshold
const suggestedThreshold = Math.min(currentThreshold, maxThreshold)

const newThreshold = await p.text({
Expand Down Expand Up @@ -158,9 +181,9 @@ export async function removeOwner(account?: string) {
console.log(pc.bold('Remove Owner Summary:'))
console.log(` ${pc.dim('Safe:')} ${safe.name}`)
console.log(` ${pc.dim('Remove Owner:')} ${removeAddress}`)
console.log(` ${pc.dim('Current Owners:')} ${safe.owners.length}`)
console.log(` ${pc.dim('New Owners:')} ${safe.owners.length - 1}`)
console.log(` ${pc.dim('Old Threshold:')} ${safe.threshold}`)
console.log(` ${pc.dim('Current Owners:')} ${owners.length}`)
console.log(` ${pc.dim('New Owners:')} ${owners.length - 1}`)
console.log(` ${pc.dim('Old Threshold:')} ${currentThreshold}`)
console.log(` ${pc.dim('New Threshold:')} ${thresholdNum}`)
console.log('')

Expand All @@ -174,16 +197,8 @@ export async function removeOwner(account?: string) {
return
}

// Get chain
const chain = configStore.getChain(safe.chainId)
if (!chain) {
p.log.error(`Chain ${safe.chainId} not found in configuration`)
p.outro('Failed')
return
}

const spinner = p.spinner()
spinner.start('Creating remove owner transaction...')
const spinner2 = p.spinner()
spinner2.start('Creating remove owner transaction...')

// Create the remove owner transaction using Safe SDK
const txService = new TransactionService(chain)
Expand All @@ -203,13 +218,13 @@ export async function removeOwner(account?: string) {
activeWallet.address as Address
)

spinner.stop('Transaction created')
spinner2.stop('Transaction created')

await renderScreen(OwnerRemoveSuccessScreen, {
safeTxHash: safeTransaction.safeTxHash,
safeAddress: safe.address as Address,
chainId: safe.chainId,
threshold: safe.threshold,
threshold: currentThreshold,
})
} catch (error) {
if (error instanceof SafeCLIError) {
Expand Down
15 changes: 15 additions & 0 deletions src/commands/config/chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,28 @@ export async function addChain() {
return
}

const transactionServiceUrl = await p.text({
message: 'Safe Transaction Service URL (optional):',
placeholder: 'https://safe-transaction-mainnet.safe.global',
validate: (value) => {
if (value && !isValidUrl(value)) return 'Invalid URL'
return undefined
},
})

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

const chainConfig: ChainConfig = {
name: name as string,
chainId: chainId as string,
shortName: shortName as string,
rpcUrl: rpcUrl as string,
explorer: explorer ? (explorer as string) : undefined,
currency: currency as string,
transactionServiceUrl: transactionServiceUrl ? (transactionServiceUrl as string) : undefined,
}

const spinner = p.spinner()
Expand Down
4 changes: 4 additions & 0 deletions src/commands/config/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
rpcUrl: 'RPC endpoint URL',
currency: 'Native currency symbol (e.g., "ETH")',
explorer: '(Optional) Block explorer base URL',
transactionServiceUrl: '(Optional) Safe Transaction Service API URL',
contractNetworks: '(Optional) Safe contract addresses for this chain',
Copy link

Copilot AI Oct 25, 2025

Choose a reason for hiding this comment

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

The help text for contractNetworks is unclear about the expected format. Based on the ChainConfig type, this should be an object with contract addresses. Consider clarifying the format, e.g., '(Optional) Safe contract addresses object for this chain (e.g., {"1.3.0": "0x..."})'.

Suggested change
contractNetworks: '(Optional) Safe contract addresses for this chain',
contractNetworks: '(Optional) Safe contract addresses object for this chain (e.g., {"1.3.0": "0x..."})',

Copilot uses AI. Check for mistakes.
},
chains,
}
Expand Down Expand Up @@ -64,10 +66,10 @@
unlinkSync(tempFile) // Clean up temp file

// Parse and validate
let parsedConfig: any

Check warning on line 69 in src/commands/config/edit.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 69 in src/commands/config/edit.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 69 in src/commands/config/edit.ts

View workflow job for this annotation

GitHub Actions / Test on Node.js 22.x

Unexpected any. Specify a different type
try {
parsedConfig = JSON.parse(editedContent)
} catch (error) {

Check warning on line 72 in src/commands/config/edit.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 72 in src/commands/config/edit.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 72 in src/commands/config/edit.ts

View workflow job for this annotation

GitHub Actions / Test on Node.js 22.x

'error' is defined but never used
throw new SafeCLIError('Invalid JSON format. Changes not saved.')
}

Expand All @@ -79,7 +81,7 @@

// Validate each chain
for (const [chainId, chain] of Object.entries(newChains)) {
const c = chain as any

Check warning on line 84 in src/commands/config/edit.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 84 in src/commands/config/edit.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 84 in src/commands/config/edit.ts

View workflow job for this annotation

GitHub Actions / Test on Node.js 22.x

Unexpected any. Specify a different type

if (!c.chainId || !c.name || !c.shortName || !c.rpcUrl || !c.currency) {
throw new SafeCLIError(
Expand Down Expand Up @@ -158,7 +160,7 @@

// Add/update chains
for (const [chainId, chain] of Object.entries(newChains)) {
const c = chain as any

Check warning on line 163 in src/commands/config/edit.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 163 in src/commands/config/edit.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 163 in src/commands/config/edit.ts

View workflow job for this annotation

GitHub Actions / Test on Node.js 22.x

Unexpected any. Specify a different type
configStore.setChain(chainId, {
chainId: c.chainId,
name: c.name,
Expand All @@ -166,6 +168,8 @@
rpcUrl: c.rpcUrl,
currency: c.currency,
explorer: c.explorer,
transactionServiceUrl: c.transactionServiceUrl,
contractNetworks: c.contractNetworks,
})
}

Expand Down
44 changes: 28 additions & 16 deletions src/commands/tx/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,34 @@
return
}

// Check if wallet is an owner
if (!safe.owners) {
p.log.error('Safe owner information not available. Please sync Safe data.')
// Get chain
const chain = configStore.getChain(chainId)
if (!chain) {
p.log.error(`Chain ${chainId} not found in configuration`)
p.outro('Failed')
return
}

if (!safe.owners.some((owner) => owner.toLowerCase() === activeWallet.address.toLowerCase())) {
// Fetch live owners from blockchain
const spinner = p.spinner()
spinner.start('Fetching Safe information from blockchain...')

let owners: Address[]
try {
const txService = new TransactionService(chain)
owners = await txService.getOwners(address as Address)
spinner.stop('Safe information fetched')
} catch (error) {
spinner.stop('Failed to fetch Safe information')
p.log.error(
error instanceof Error ? error.message : 'Failed to fetch Safe data from blockchain'
)
p.outro('Failed')
return
}

// Check if wallet is an owner
if (!owners.some((owner) => owner.toLowerCase() === activeWallet.address.toLowerCase())) {
p.log.error('Active wallet is not an owner of this Safe')
p.outro('Failed')
return
Expand Down Expand Up @@ -108,28 +128,20 @@
return
}

// Get chain for contract detection
const chain = configStore.getChain(safe.chainId)
if (!chain) {
p.log.error(`Chain ${safe.chainId} not found in configuration`)
p.outro('Failed')
return
}

// Check if address is a contract
const contractService = new ContractService(chain)
let isContract = false
let value = '0'
let data: `0x${string}` = '0x'

const spinner = p.spinner()
spinner.start('Checking if address is a contract...')
const spinner2 = p.spinner()
spinner2.start('Checking if address is a contract...')

try {
isContract = await contractService.isContract(to)
spinner.stop(isContract ? 'Contract detected' : 'EOA (regular address)')
spinner2.stop(isContract ? 'Contract detected' : 'EOA (regular address)')
} catch (error) {

Check warning on line 143 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 143 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 143 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
spinner.stop('Failed to check contract')
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,7 +160,7 @@
}

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

Check warning on line 163 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 163 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 163 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 Down Expand Up @@ -193,16 +205,16 @@
const combinedAbi = [...implAbi]
const existingSignatures = new Set(
implAbi
.filter((item: any) => item.type === 'function')

Check warning on line 208 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 208 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 208 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 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 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 22.x

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

Check warning on line 211 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 211 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 211 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 217 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 217 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 217 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 Down
Loading