Skip to content
6 changes: 3 additions & 3 deletions src/commands/bg/collection/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ The Bubblegum V2 plugin is required for collections that will contain compressed
basisPoints: Math.round(flags.royalties * 100),
creators: [
{
address: this.context.signer.publicKey,
address: umi.identity.publicKey,
percentage: 100,
},
],
Expand Down Expand Up @@ -104,7 +104,7 @@ Collection: ${collectionAddress}
Name: ${flags.name}
URI: ${flags.uri}
Royalties: ${flags.royalties}%
Update Authority: ${this.context.signer.publicKey}
Update Authority: ${umi.identity.publicKey}
Plugins: ${pluginsList}

Transaction: ${signature}
Expand All @@ -123,7 +123,7 @@ Use it with: mplx bg nft create <tree> --collection ${collectionAddress}
name: flags.name,
uri: flags.uri,
royalties: flags.royalties,
updateAuthority: this.context.signer.publicKey.toString(),
updateAuthority: umi.identity.publicKey.toString(),
}
} catch (error) {
spinner.fail('Failed to create collection')
Expand Down
2 changes: 1 addition & 1 deletion src/commands/bg/nft/burn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Note: The current owner (or their delegate) must be the signer. This action is i
const leafOwner = assetWithProof.leafOwner
const leafDelegate = assetWithProof.leafDelegate

const signerKey = this.context.signer.publicKey.toString()
const signerKey = umi.identity.publicKey.toString()
const ownerKey = leafOwner.toString()
const delegateKey = leafDelegate ? leafDelegate.toString() : null

Expand Down
4 changes: 2 additions & 2 deletions src/commands/bg/nft/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ Note: Bubblegum V2 uses Metaplex Core collections. To create a Core collection:

private resolveOwner(ownerFlag?: string): PublicKey {
if (!ownerFlag) {
return this.context.signer.publicKey
return this.context.umi.identity.publicKey
}

return this.parsePublicKey('owner', ownerFlag)
Expand Down Expand Up @@ -321,7 +321,7 @@ Note: Bubblegum V2 uses Metaplex Core collections. To create a Core collection:
collection: collectionPubkey,
creators: [
{
address: this.context.signer.publicKey,
address: this.context.umi.identity.publicKey,
verified: true,
share: 100,
},
Expand Down
2 changes: 1 addition & 1 deletion src/commands/bg/nft/transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Note: The current owner (or their delegate) must be the signer.`
const leafOwner = assetWithProof.leafOwner
const leafDelegate = assetWithProof.leafDelegate

const signerKey = this.context.signer.publicKey.toString()
const signerKey = umi.identity.publicKey.toString()
const ownerKey = leafOwner.toString()
const delegateKey = leafDelegate ? leafDelegate.toString() : null

Expand Down
9 changes: 5 additions & 4 deletions src/commands/cm/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,11 +404,12 @@ export default class CmCreate extends TransactionCommand<typeof CmCreate> {

// Collection creation onchain
const collectionCreationSpinner = ora('🏭 Creating collection onchain...').start()
await createCollection(umi, {
const collectionTx = createCollection(umi, {
collection,
name: collectionJson.name,
uri: collectionJson.uri,
}).sendAndConfirm(umi, { send: { commitment: 'finalized' }, confirm: { commitment: 'finalized' } })
})
await umiSendAndConfirmTransaction(umi, collectionTx, { commitment: 'finalized' })

const collectionRes = await fetchCollection(umi, collection.publicKey)

Expand Down Expand Up @@ -441,7 +442,7 @@ export default class CmCreate extends TransactionCommand<typeof CmCreate> {
guards: parsedGuards.guards,
groups: parsedGuards.groups,
});
await tx.sendAndConfirm(umi);
await umiSendAndConfirmTransaction(umi, tx);
candyMachineCreatorSpinner.succeed(`Candy machine created with guards - ${candyMachine.publicKey}`)
} else {
// Create candy machine without candy guard (authority-only minting)
Expand All @@ -453,7 +454,7 @@ export default class CmCreate extends TransactionCommand<typeof CmCreate> {
isMutable: candyMachineConfig.config.isMutable,
...getConfigLineSettings(candyMachineConfig),
});
await tx.sendAndConfirm(umi);
await umiSendAndConfirmTransaction(umi, tx);
candyMachineCreatorSpinner.succeed(`Candy machine created (authority-only minting) - ${candyMachine.publicKey}`)
}
} catch (error) {
Expand Down
9 changes: 5 additions & 4 deletions src/commands/cm/withdraw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { generateExplorerUrl } from '../../explorers.js'
import { terminalColors } from '../../lib/StandardColors.js'
import { txSignatureToString } from '../../lib/util.js'
import { readCmConfig } from '../../lib/cm/cm-utils.js'
import umiSendAndConfirmTransaction from '../../lib/umi/sendAndConfirm.js'

export default class CmWithdraw extends TransactionCommand<typeof CmWithdraw> {
static override description = `Withdraw a candy machine and recover funds
Expand Down Expand Up @@ -91,7 +92,7 @@ export default class CmWithdraw extends TransactionCommand<typeof CmWithdraw> {
}

const res = await this.withdraw(candyMachinePk);
const signature = txSignatureToString(res.signature)
const signature = txSignatureToString(res.transaction.signature as Uint8Array)
this.log(`${terminalColors.BgGreen}${terminalColors.FgWhite}Candy machine withdrawn successfully${terminalColors.FgDefault}${terminalColors.BgDefault}`);
this.log(`${terminalColors.BgGreen}${terminalColors.FgWhite}Candy machine ID: ${candyMachineId}${terminalColors.FgDefault}${terminalColors.BgDefault}`);
this.log(`${terminalColors.BgGreen}${terminalColors.FgWhite}Transaction hash: ${signature}${terminalColors.FgDefault}${terminalColors.BgDefault}`);
Expand All @@ -108,10 +109,10 @@ export default class CmWithdraw extends TransactionCommand<typeof CmWithdraw> {

private async withdraw(candyMachinePk: ReturnType<typeof publicKey>) {
const { umi } = this.context;
const res = await deleteCandyMachine(umi, {
const tx = deleteCandyMachine(umi, {
candyMachine: candyMachinePk,
}).sendAndConfirm(umi);
});

return res;
return await umiSendAndConfirmTransaction(umi, tx);
}
}
146 changes: 111 additions & 35 deletions src/commands/config/wallets/add.ts
Original file line number Diff line number Diff line change
@@ -1,84 +1,160 @@
import { Args, Command } from '@oclif/core'
import { Args, Command, Flags } from '@oclif/core'
import fs from 'fs'
import { dirname } from 'path'
import { createSignerFromPath, getDefaultConfigPath, readConfig } from '../../../lib/Context.js'
import { fetchAsset, findAssetSignerPda } from '@metaplex-foundation/mpl-core'
import { publicKey } from '@metaplex-foundation/umi'
import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
import { mplCore } from '@metaplex-foundation/mpl-core'
import { createSignerFromPath, consolidateConfigs, DEFAULT_CONFIG, getDefaultConfigPath, readConfig, WalletEntry } from '../../../lib/Context.js'
import { ensureDirectoryExists, writeJsonSync } from '../../../lib/file.js'
import { shortenAddress } from '../../../lib/util.js'
import { shortenAddress, DUMMY_UMI } from '../../../lib/util.js'

export default class ConfigWalletAddCommand extends Command {
static enableJsonFlag = true

static override description = 'Add a new wallet to your configuration'
static override description = 'Add a new wallet to your configuration. Use --asset to add an asset-signer wallet.'

static override args = {
name: Args.string({
description: 'Name of wallet (alphanumeric, hyphens and underscores only)',
required: true,
}),
path: Args.string({ description: 'Path to keypair json file', required: true }),
path: Args.string({ description: 'Path to keypair json file (not required for --asset)', required: false }),
}

static override flags = {
asset: Flags.string({
description: 'Asset ID to create an asset-signer wallet from',
}),
}

static override examples = [
'<%= config.bin %> <%= command.id %> my-wallet ~/.config/solana/id.json',
'<%= config.bin %> <%= command.id %> mainnet-wallet ./wallets/mainnet.json',
'<%= config.bin %> <%= command.id %> dev-wallet /Users/dev/.solana/devnet.json',
'<%= config.bin %> <%= command.id %> vault --asset <assetId>',
]

public async run(): Promise<unknown> {
const { flags, args } = await this.parse(ConfigWalletAddCommand)

// Validate name (removed character limit for MCP compatibility)

// Validate name contains only safe characters for all platforms
// TODO: Move validation to validations file that is in other PR
if (!/^[a-zA-Z0-9-_]+$/.test(args.name)) {
this.error(`Invalid wallet name '${args.name}'. Name must contain only letters, numbers, hyphens (-), and underscores (_). Example: 'my-wallet' or 'dev_wallet_1'`)
}

// Validate path
if (!args.path.endsWith('.json')) {
this.error(`Invalid file type. Wallet file must be a .json keypair file. Received: ${args.path}`)
const configPath = flags.config ?? getDefaultConfigPath()
const config = readConfig(configPath)

if (!config.wallets) {
config.wallets = []
}

// Check if the file exists
if (!fs.existsSync(args.path)) {
this.error(`Wallet file not found at: ${args.path}\nPlease check the path and ensure the keypair file exists.`)
// Check for duplicate name
const existingName = config.wallets.find((wallet) => wallet.name === args.name)
if (existingName) {
this.error(`A wallet named '${args.name}' already exists.\nUse a different name or run 'mplx config wallets remove ${args.name}' to remove the existing wallet first.`)
}

const path = flags.config ?? getDefaultConfigPath()
let wallet: WalletEntry

const config = readConfig(path)
if (flags.asset) {
// Asset-signer wallet
const assetPubkey = publicKey(flags.asset)
const [pdaPubkey] = findAssetSignerPda(DUMMY_UMI, { asset: assetPubkey })

const signer = await createSignerFromPath(args.path)
// Fetch the asset on-chain to determine the owner
const mergedConfig = consolidateConfigs(DEFAULT_CONFIG, config, { rpcUrl: flags.rpc })
const umi = createUmi(mergedConfig.rpcUrl!).use(mplCore())
const asset = await fetchAsset(umi, assetPubkey).catch(() => {
this.error(`Could not fetch asset ${flags.asset}. Make sure it exists and your RPC is reachable.`)
})

if (!config.wallets) {
config.wallets = []
} else {
const existingName = config.wallets.find((wallet) => wallet.name === args.name)
if (existingName) {
this.error(`A wallet named '${args.name}' already exists.\nUse a different name or run 'mplx config wallets remove ${args.name}' to remove the existing wallet first.`)
}
const ownerAddress = asset.owner.toString()

const existingPath = config.wallets.find((wallet) => wallet.path === args.path)
if (existingPath) {
this.error(`This wallet file is already configured as '${existingPath.name}'.\nUse 'mplx config wallets set ${existingPath.name}' to switch to it.`)
// Find the saved wallet that matches the asset owner
const ownerWallet = config.wallets.find(
w => w.address === ownerAddress && w.type !== 'asset-signer'
)

if (!ownerWallet) {
this.error(
`Asset owner ${shortenAddress(ownerAddress)} is not in your saved wallets.\n` +
`Add the owner wallet first: mplx config wallets add <name> <keypair-path>`
)
}

const existingAddress = config.wallets.find((wallet) => wallet.address === signer.publicKey.toString())
const existingAddress = config.wallets.find((w) => w.address === pdaPubkey.toString())
if (existingAddress) {
this.error(`This wallet address (${shortenAddress(signer.publicKey)}) is already configured as '${existingAddress.name}'.\nUse 'mplx config wallets set ${existingAddress.name}' to switch to it.`)
this.error(`This asset's signer PDA (${shortenAddress(pdaPubkey)}) is already configured as '${existingAddress.name}'.`)
}

wallet = {
name: args.name,
type: 'asset-signer',
asset: flags.asset,
address: pdaPubkey.toString(),
payer: ownerWallet.name,
}

config.wallets.push(wallet)

const dir = dirname(configPath)
ensureDirectoryExists(dir)
writeJsonSync(configPath, config)

this.log(
`✅ Asset-signer wallet '${args.name}' added!\n` +
` Asset: ${flags.asset}\n` +
` Signer PDA: ${pdaPubkey.toString()}\n` +
` Owner: ${ownerWallet.name} (${shortenAddress(ownerAddress)})\n` +
`\nUse 'mplx config wallets set ${args.name}' to make this your active wallet.`
)

return {
name: args.name,
type: 'asset-signer',
asset: flags.asset,
address: pdaPubkey.toString(),
owner: ownerWallet.name,
}
}

config.wallets?.push({
// Standard file-based wallet
if (!args.path) {
this.error('Path to keypair file is required for file-based wallets. Use --asset for asset-signer wallets.')
}

if (!args.path.endsWith('.json')) {
this.error(`Invalid file type. Wallet file must be a .json keypair file. Received: ${args.path}`)
}

if (!fs.existsSync(args.path)) {
this.error(`Wallet file not found at: ${args.path}\nPlease check the path and ensure the keypair file exists.`)
}

const signer = await createSignerFromPath(args.path)

const existingPath = config.wallets.find((w) => 'path' in w && w.path === args.path)
if (existingPath) {
this.error(`This wallet file is already configured as '${existingPath.name}'.\nUse 'mplx config wallets set ${existingPath.name}' to switch to it.`)
}

const existingAddress = config.wallets.find((w) => w.address === signer.publicKey.toString())
if (existingAddress) {
this.error(`This wallet address (${shortenAddress(signer.publicKey)}) is already configured as '${existingAddress.name}'.\nUse 'mplx config wallets set ${existingAddress.name}' to switch to it.`)
}

wallet = {
name: args.name,
address: signer.publicKey,
address: signer.publicKey.toString(),
path: args.path,
})
}

config.wallets.push(wallet)

const dir = dirname(path)
const dir = dirname(configPath)
ensureDirectoryExists(dir)
writeJsonSync(path, config)
writeJsonSync(configPath, config)

this.log(`✅ Wallet '${args.name}' successfully added to configuration!\n Address: ${signer.publicKey}\n Path: ${args.path}\n\nUse 'mplx config wallets set ${args.name}' to make this your active wallet.`)

Expand Down
26 changes: 16 additions & 10 deletions src/commands/config/wallets/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,24 @@ export default class ConfigWalletListCommand extends Command {
return { wallets: [] }
}

const wallets = config.wallets.map(wallet => ({
name: wallet.name,
address: wallet.address,
path: wallet.path,
active: wallet.path === config.keypair,
}))
const wallets = config.wallets.map(wallet => {
const type = wallet.type || 'file'
const isActive = type === 'asset-signer'
? wallet.name === config.activeWallet
: ('path' in wallet && wallet.path === config.keypair && !config.activeWallet)

return {
name: wallet.name,
address: wallet.address,
type,
active: isActive,
...(type === 'asset-signer' && 'asset' in wallet ? { asset: wallet.asset } : {}),
...('path' in wallet ? { path: wallet.path } : {}),
}
})

this.log('Installed Wallets:')
for (const wallet of wallets) {
const marker = wallet.active ? ' (active)' : ''
this.log(` ${wallet.name}: ${wallet.address}${marker}`)
}
console.table(wallets)

return { wallets }
}
Expand Down
2 changes: 1 addition & 1 deletion src/commands/config/wallets/new.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export default class ConfigWalletsNew extends BaseCommand<typeof ConfigWalletsNe
}

// Check for existing wallet with same path
const existingPath = config.wallets.find((w) => w.path === filePath)
const existingPath = config.wallets.find((w) => 'path' in w && w.path === filePath)
if (existingPath) {
this.error(`Wallet with path ${filePath} already exists`)
}
Expand Down
Loading
Loading