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
102 changes: 102 additions & 0 deletions src/commands/cm/guard/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { input } from '@inquirer/prompts'
import {
fetchCandyGuard,
deleteCandyGuard,
} from '@metaplex-foundation/mpl-core-candy-machine'
import { isPublicKey, publicKey } from '@metaplex-foundation/umi'
import { Flags } from '@oclif/core'
import ora from 'ora'
import { TransactionCommand } from '../../../TransactionCommand.js'
import { terminalColors } from '../../../lib/StandardColors.js'
import umiSendAndConfirmTransaction from '../../../lib/umi/sendAndConfirm.js'

export default class CmGuardDelete extends TransactionCommand<typeof CmGuardDelete> {
static override description = `Delete a candy guard account and reclaim rent

The candy guard must first be removed (unwrapped) from the candy machine
before it can be deleted. Use 'cm guard remove' first if needed.

⚠️ WARNING: This permanently deletes the candy guard account. This cannot be undone.
`

static override examples = [
'$ mplx cm guard delete --address <candy-guard-address>',
'$ mplx cm guard delete --address <candy-guard-address> --force',
]

static override usage = 'cm guard delete [FLAGS]'

static override flags = {
address: Flags.string({
char: 'a',
description: 'The address of the candy guard to delete',
required: true,
}),
force: Flags.boolean({
description: 'Skip confirmation prompt',
default: false,
}),
}

public async run(): Promise<unknown> {
const { flags } = await this.parse(CmGuardDelete)
const { umi } = this.context

const candyGuardAddress = flags.address
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (!isPublicKey(candyGuardAddress)) {
this.error(`Invalid address format: ${candyGuardAddress}`)
}

// Verify the candy guard exists
const verifySpinner = ora('Verifying candy guard...').start()

try {
await fetchCandyGuard(umi, publicKey(candyGuardAddress))
verifySpinner.succeed(`Found candy guard: ${candyGuardAddress}`)
} catch (error) {
verifySpinner.fail('Candy guard not found')
if (error instanceof Error && error.name === 'AccountNotFoundError') {
this.error(`The account at ${candyGuardAddress} does not exist or is not a valid candy guard.`)
}
this.error(`Failed to fetch candy guard: ${error instanceof Error ? error.message : String(error)}`)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Confirmation
if (!flags.force) {
this.log(`\n${terminalColors.BgRed}${terminalColors.FgWhite}You are about to permanently delete this candy guard account${terminalColors.FgDefault}${terminalColors.BgDefault}`)
this.log(`Candy guard: ${candyGuardAddress}`)
this.log(`\nThis action cannot be undone. The rent will be returned to your wallet.\n`)

await input({
message: `Type 'yes-delete' to confirm`,
validate: (val) => {
if (val === 'yes-delete') return true
return 'Please type "yes-delete" to confirm'
}
})
}

// Delete the candy guard
const deleteSpinner = ora('Deleting candy guard...').start()

try {
const tx = deleteCandyGuard(umi, {
candyGuard: publicKey(candyGuardAddress),
})

await umiSendAndConfirmTransaction(umi, tx)
deleteSpinner.succeed('Candy guard deleted successfully')
} catch (error) {
deleteSpinner.fail('Failed to delete candy guard')
this.error(`Delete failed: ${error instanceof Error ? error.message : String(error)}`)
}

this.log(`Rent has been returned to your wallet.`)
this.logSuccess('Candy guard deleted!')

return {
candyGuardAddress,
}
}
}
21 changes: 21 additions & 0 deletions src/commands/cm/guard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Command } from '@oclif/core'

export default class CmGuard extends Command {
static override description = 'Manage candy machine guards'

static override examples = [
'<%= config.bin %> <%= command.id %> update',
'<%= config.bin %> <%= command.id %> remove',
'<%= config.bin %> <%= command.id %> delete',
]

public async run(): Promise<void> {
this.log('Available candy machine guard commands:')
this.log(' update - Update guards on a candy machine')
this.log(' remove - Remove (unwrap) the candy guard from a candy machine')
this.log(' delete - Delete a candy guard account and reclaim rent')
this.log('')
this.log('Use --help with any command for more details')
this.log('Example: mplx cm guard update --help')
}
}
133 changes: 133 additions & 0 deletions src/commands/cm/guard/remove.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { input } from '@inquirer/prompts'
import {
fetchCandyMachine,
fetchCandyGuard,
unwrap,
} from '@metaplex-foundation/mpl-core-candy-machine'
import { publicKey } from '@metaplex-foundation/umi'
import { Flags } from '@oclif/core'
import ora from 'ora'
import { TransactionCommand } from '../../../TransactionCommand.js'
import { terminalColors } from '../../../lib/StandardColors.js'
import { readCmConfig } from '../../../lib/cm/cm-utils.js'
import umiSendAndConfirmTransaction from '../../../lib/umi/sendAndConfirm.js'

export default class CmGuardRemove extends TransactionCommand<typeof CmGuardRemove> {
static override description = `Remove (unwrap) the candy guard from a candy machine

This changes the mint authority back to the candy machine authority,
meaning minting will require the authority to sign instead of using guards.
The candy guard account is NOT deleted and can be re-attached later.
Use 'cm guard delete' to permanently delete the candy guard account.
`

static override examples = [
'$ mplx cm guard remove',
'$ mplx cm guard remove --address <candy-machine-address>',
'$ mplx cm guard remove --force',
]

static override usage = 'cm guard remove [FLAGS]'

static override flags = {
address: Flags.string({
char: 'a',
description: 'The address of the candy machine',
required: false,
}),
force: Flags.boolean({
description: 'Skip confirmation prompt',
default: false,
}),
}

public async run(): Promise<unknown> {
const { flags } = await this.parse(CmGuardRemove)
const { umi } = this.context

// Resolve candy machine address
let candyMachineAddress = flags.address

if (!candyMachineAddress) {
try {
const config = readCmConfig()
candyMachineAddress = config.candyMachineId
} catch {
// no config file found
}
}

if (!candyMachineAddress) {
this.error('No candy machine address provided. Use --address or run from a directory with cm-config.json')
}

// Fetch candy machine to find its candy guard
const fetchSpinner = ora('Fetching candy machine...').start()
let candyGuardAddress: string
let candyMachine

try {
candyMachine = await fetchCandyMachine(umi, publicKey(candyMachineAddress))
} catch (error) {
fetchSpinner.fail('Failed to fetch candy machine')
this.error(`Failed: ${error instanceof Error ? error.message : String(error)}`)
}

if (candyMachine.mintAuthority === candyMachine.authority) {
fetchSpinner.fail('Candy machine uses authority-only minting')
this.error('This candy machine uses authority-only minting and has no candy guard attached. Nothing to remove.')
}

candyGuardAddress = candyMachine.mintAuthority

try {
// Verify it's actually a candy guard
await fetchCandyGuard(umi, publicKey(candyGuardAddress))
fetchSpinner.succeed(`Found candy guard: ${candyGuardAddress}`)
} catch (error) {
fetchSpinner.fail('Failed to fetch candy guard')
this.error(`Failed: ${error instanceof Error ? error.message : String(error)}`)
}

// Confirmation
if (!flags.force) {
this.log(`\n${terminalColors.BgRed}${terminalColors.FgWhite}You are about to remove the candy guard from this candy machine${terminalColors.FgDefault}${terminalColors.BgDefault}`)
this.log(`Candy machine: ${candyMachineAddress}`)
this.log(`Candy guard: ${candyGuardAddress}`)
this.log(`\nAfter removal, minting will require the authority to sign directly.`)
this.log(`The candy guard account will still exist and can be re-attached.\n`)

await input({
message: `Type 'yes-remove' to confirm`,
validate: (val) => {
if (val === 'yes-remove') return true
return 'Please type "yes-remove" to confirm'
}
})
}

// Unwrap the candy guard
const unwrapSpinner = ora('Removing candy guard...').start()

try {
const tx = unwrap(umi, {
candyGuard: publicKey(candyGuardAddress),
candyMachine: publicKey(candyMachineAddress),
})

await umiSendAndConfirmTransaction(umi, tx)
unwrapSpinner.succeed('Candy guard removed successfully')
} catch (error) {
unwrapSpinner.fail('Failed to remove candy guard')
this.error(`Remove failed: ${error instanceof Error ? error.message : String(error)}`)
}

this.log(`Mint authority has been returned to the candy machine authority.`)
this.logSuccess('Candy guard removed!')

return {
candyMachineAddress,
candyGuardAddress,
}
}
}
Loading
Loading