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
31 changes: 13 additions & 18 deletions src/commands/core/asset/transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import ora from 'ora'

import { generateExplorerUrl } from '../../../explorers.js'
import { TransactionCommand } from '../../../TransactionCommand.js'
import { getAssetSigner } from '../../../lib/umi/assetSignerPlugin.js'
import umiSendAndConfirmTransaction from '../../../lib/umi/sendAndConfirm.js'
import { txSignatureToString } from '../../../lib/util.js'

Expand Down Expand Up @@ -40,23 +39,19 @@ export default class AssetTransfer extends TransactionCommand<typeof AssetTransf
this.error('Cannot transfer: asset is frozen')
}

// Skip client-side validation for asset-signer wallets — the execute
// instruction handles authorization on-chain via the PDA.
if (!getAssetSigner(umi)) {
const transferError = await validateTransfer(umi, {
authority: umi.identity.publicKey,
asset,
collection,
recipient: publicKey(args.newOwner),
})

if (transferError) {
spinner.fail('Asset transfer failed')
const message = transferError === LifecycleValidationError.NoAuthority
? 'Cannot transfer: you are not the owner or an authorized delegate of this asset'
: `Cannot transfer: ${transferError}`
this.error(message)
}
const transferError = await validateTransfer(umi, {
authority: umi.identity.publicKey,
asset,
collection,
recipient: publicKey(args.newOwner),
})

if (transferError) {
spinner.fail('Asset transfer failed')
const message = transferError === LifecycleValidationError.NoAuthority
? 'Cannot transfer: you are not the owner or an authorized delegate of this asset'
: `Cannot transfer: ${transferError}`
this.error(message)
}

spinner.text = 'Transferring asset...'
Expand Down
7 changes: 3 additions & 4 deletions src/commands/tm/transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { TransactionCommand } from '../../TransactionCommand.js'
import { txSignatureToString } from '../../lib/util.js'
import { generateExplorerUrl } from '../../explorers.js'
import { TOKEN_AUTH_RULES_ID } from '../../constants.js'
import { getEffectiveOwner } from '../../lib/umi/assetSignerPlugin.js'
import umiSendAndConfirmTransaction from '../../lib/umi/sendAndConfirm.js'

export default class TmTransfer extends TransactionCommand<typeof TmTransfer> {
Expand Down Expand Up @@ -67,7 +66,7 @@ export default class TmTransfer extends TransactionCommand<typeof TmTransfer> {
mint: asset.publicKey,
token: findAssociatedTokenPda(umi, {
mint: asset.publicKey,
owner: getEffectiveOwner(umi),
owner: umi.identity.publicKey,
})[0],
})

Expand All @@ -91,7 +90,7 @@ export default class TmTransfer extends TransactionCommand<typeof TmTransfer> {
transferIx = transferV1(umi, {
mint: asset.publicKey,
authority: umi.identity,
tokenOwner: getEffectiveOwner(umi),
tokenOwner: umi.identity.publicKey,
destinationOwner,
destinationToken: destinationToken[0],
tokenStandard: TokenStandard.ProgrammableNonFungible,
Expand All @@ -106,7 +105,7 @@ export default class TmTransfer extends TransactionCommand<typeof TmTransfer> {
transferIx = transferV1(umi, {
mint: asset.publicKey,
authority: umi.identity,
tokenOwner: getEffectiveOwner(umi),
tokenOwner: umi.identity.publicKey,
destinationOwner,
tokenStandard: unwrapOptionRecursively(asset.metadata.tokenStandard) || TokenStandard.NonFungible
})
Expand Down
3 changes: 1 addition & 2 deletions src/commands/toolbox/sol/airdrop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { Args } from '@oclif/core'
import ora from 'ora'
import { TransactionCommand } from '../../../TransactionCommand.js'
import umiAirdrop from '../../../lib/toolbox/airdrop.js'
import { getEffectiveOwner } from '../../../lib/umi/assetSignerPlugin.js'



Expand Down Expand Up @@ -50,7 +49,7 @@ export default class ToolboxSolAirdrop extends TransactionCommand<typeof Toolbox
throw err
})

const address = (args.address ?? getEffectiveOwner(umi)).toString()
const address = (args.address ?? umi.identity.publicKey).toString()

spinner.succeed('Airdropped SOL successfully')

Expand Down
3 changes: 1 addition & 2 deletions src/commands/toolbox/sol/balance.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { amountToNumber, isPublicKey, publicKey } from '@metaplex-foundation/umi';
import { Args } from '@oclif/core';
import { TransactionCommand } from '../../../TransactionCommand.js';
import { getEffectiveOwner } from '../../../lib/umi/assetSignerPlugin.js';
import { shortenAddress } from '../../../lib/util.js';

const SUCCESS_MESSAGE = (address: string, balance: number) => `--------------------------------
Expand Down Expand Up @@ -42,7 +41,7 @@ export default class ToolboxSolBalance extends TransactionCommand<typeof Toolbox
const { umi } = this.context

const validatedInput = await this.validateInput(args.address)
const targetAddress = validatedInput.address ? publicKey(validatedInput.address) : getEffectiveOwner(umi)
const targetAddress = validatedInput.address ? publicKey(validatedInput.address) : umi.identity.publicKey

try {
const balance = await umi.rpc.getBalance(targetAddress)
Expand Down
5 changes: 2 additions & 3 deletions src/commands/toolbox/sol/unwrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import ora from 'ora'

import { generateExplorerUrl } from '../../../explorers.js'
import { TransactionCommand } from '../../../TransactionCommand.js'
import { getEffectiveOwner } from '../../../lib/umi/assetSignerPlugin.js'
import umiSendAndConfirmTransaction from '../../../lib/umi/sendAndConfirm.js'
import { txSignatureToString } from '../../../lib/util.js'

Expand All @@ -32,7 +31,7 @@ export default class ToolboxSolUnwrap extends TransactionCommand<typeof ToolboxS
try {
const [associatedTokenPda] = findAssociatedTokenPda(umi, {
mint: NATIVE_MINT,
owner: getEffectiveOwner(umi),
owner: umi.identity.publicKey,
})

const accountInfo = await umi.rpc.getAccount(associatedTokenPda).catch(() => null)
Expand All @@ -48,7 +47,7 @@ export default class ToolboxSolUnwrap extends TransactionCommand<typeof ToolboxS

const tx = closeToken(umi, {
account: associatedTokenPda,
destination: getEffectiveOwner(umi),
destination: umi.identity.publicKey,
owner: umi.identity,
})

Expand Down
5 changes: 2 additions & 3 deletions src/commands/toolbox/sol/wrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import ora from 'ora'

import { generateExplorerUrl } from '../../../explorers.js'
import { TransactionCommand } from '../../../TransactionCommand.js'
import { getEffectiveOwner } from '../../../lib/umi/assetSignerPlugin.js'
import umiSendAndConfirmTransaction from '../../../lib/umi/sendAndConfirm.js'
import { txSignatureToString } from '../../../lib/util.js'

Expand Down Expand Up @@ -47,7 +46,7 @@ export default class ToolboxSolWrap extends TransactionCommand<typeof ToolboxSol
try {
const [associatedTokenPda] = findAssociatedTokenPda(umi, {
mint: NATIVE_MINT,
owner: getEffectiveOwner(umi),
owner: umi.identity.publicKey,
})

const accountInfo = await umi.rpc.getAccount(associatedTokenPda).catch(() => null)
Expand All @@ -57,7 +56,7 @@ export default class ToolboxSolWrap extends TransactionCommand<typeof ToolboxSol
if (!accountInfo || !accountInfo.exists) {
const createTokenInstruction = createAssociatedToken(umi, {
mint: NATIVE_MINT,
owner: getEffectiveOwner(umi),
owner: umi.identity.publicKey,
})
transaction = transaction.add(createTokenInstruction)
}
Expand Down
2 changes: 1 addition & 1 deletion src/commands/toolbox/token/add-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ IMPORTANT: You must be the mint authority of the token to add metadata.`
metadata: metadataPda,
mint: mintPubkey,
mintAuthority: umi.identity,
payer: umi.payer,
payer: this.context.payer ?? umi.payer,
updateAuthority: umi.identity.publicKey,
data: {
name,
Expand Down
5 changes: 2 additions & 3 deletions src/commands/toolbox/token/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import umiSendAndConfirmTransaction from '../../../lib/umi/sendAndConfirm.js'
import imageUploader from '../../../lib/uploader/imageUploader.js'
import uploadJson from '../../../lib/uploader/uploadJson.js'
import { RpcChain, txSignatureToString } from '../../../lib/util.js'
import { getEffectiveOwner } from '../../../lib/umi/assetSignerPlugin.js'
import { validateMintAmount, validateTokenName, validateTokenSymbol } from '../../../lib/validations.js'
import createTokenPrompt from '../../../prompts/createTokenPrompt.js'

Expand Down Expand Up @@ -305,11 +304,11 @@ export default class ToolboxTokenCreate extends TransactionCommand<typeof Toolbo
})
.add(createTokenIfMissing(umi, {
mint: mint.publicKey,
owner: getEffectiveOwner(umi),
owner: umi.identity.publicKey,
}))
.add(mintTokensTo(umi, {
mint: mint.publicKey,
token: findAssociatedTokenPda(umi, { mint: mint.publicKey, owner: getEffectiveOwner(umi) }),
token: findAssociatedTokenPda(umi, { mint: mint.publicKey, owner: umi.identity.publicKey }),
amount: input.mintAmount,
}))

Expand Down
3 changes: 1 addition & 2 deletions src/commands/toolbox/token/mint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import ora from 'ora'
import { TransactionCommand } from '../../../TransactionCommand.js'
import { ExplorerType, generateExplorerUrl } from '../../../explorers.js'
import umiSendAndConfirmTransaction from '../../../lib/umi/sendAndConfirm.js'
import { getEffectiveOwner } from '../../../lib/umi/assetSignerPlugin.js'
import { RpcChain, txSignatureToString } from '../../../lib/util.js'

const SUCCESS_MESSAGE = async (
Expand Down Expand Up @@ -84,7 +83,7 @@ Note: You must have mint authority for the specified token mint.`
}

// Determine recipient - default to current keypair
const recipientAddress = flags.recipient || getEffectiveOwner(umi).toString();
const recipientAddress = flags.recipient || umi.identity.publicKey.toString();
let recipientPublicKey;
try {
recipientPublicKey = publicKey(recipientAddress);
Expand Down
3 changes: 1 addition & 2 deletions src/commands/toolbox/token/transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import ora from 'ora'
import { generateExplorerUrl } from '../../../explorers.js'
import umiSendAndConfirmTransaction from '../../../lib/umi/sendAndConfirm.js'
import { TransactionCommand } from '../../../TransactionCommand.js'
import { getEffectiveOwner } from '../../../lib/umi/assetSignerPlugin.js'
import { txSignatureToString } from '../../../lib/util.js'

/*
Expand Down Expand Up @@ -51,7 +50,7 @@ export default class ToolboxTokenTransfer extends TransactionCommand<typeof Tool
})

const transferTokenIx = transferTokens(umi, {
source: findAssociatedTokenPda(umi, { mint: publicKey(args.mintAddress), owner: getEffectiveOwner(umi) }),
source: findAssociatedTokenPda(umi, { mint: publicKey(args.mintAddress), owner: umi.identity.publicKey }),
destination: findAssociatedTokenPda(umi, { mint: publicKey(args.mintAddress), owner: publicKey(args.destination) }),
amount: args.amount
})
Expand Down
42 changes: 30 additions & 12 deletions src/lib/Context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Umi,
createNoopSigner as createUmiNoopSigner,
generateSigner,
publicKey,
signerIdentity,
signerPayer,
} from '@metaplex-foundation/umi'
Expand Down Expand Up @@ -56,7 +57,6 @@ export type ConfigJson = {

export type AssetSignerInfo = {
asset: string
signerPda: string
}

export type Context = {
Expand Down Expand Up @@ -174,11 +174,13 @@ export const createContext = async (configPath: string, overrides: ConfigJson, i
let signer: Signer
let assetSigner: AssetSignerInfo | undefined
let payerPath: string | undefined = config.payer
let pdaIdentity: Signer | undefined
let realWalletSigner: Signer | undefined

if (isAssetSigner) {
// For asset-signer wallets, umi.identity is the real wallet (required for
// correct mpl-core CPI account layout). The asset-signer plugin stores
// PDA info on umi; use getEffectiveOwner(umi) for address derivation.
// Asset-signer mode: umi.identity AND umi.payer = noopSigner(PDA) so
// instructions are built with the PDA for all accounts naturally.
// The send layer overrides the transaction fee payer to the real wallet.
const walletPayerName = activeWallet.payer
if (!payerPath && walletPayerName && config.wallets) {
const payerWallet = config.wallets.find(w => w.name === walletPayerName)
Expand All @@ -199,25 +201,41 @@ export const createContext = async (configPath: string, overrides: ConfigJson, i
)
}

// Identity is the real wallet — commands build instructions normally.
// The asset-signer plugin on umi tells the send layer to wrap in execute().
signer = await createSignerFromPath(payerPath)
assetSigner = { asset: activeWallet.asset, signerPda: activeWallet.address }
// The real wallet pays gas; context.signer stays as the real wallet
// for backward compat (genesis/distro commands).
realWalletSigner = await createSignerFromPath(payerPath)
signer = realWalletSigner

// Identity is a noop signer keyed to the PDA — instructions naturally
// use the PDA address. The send layer wraps them in execute().
pdaIdentity = createUmiNoopSigner(publicKey(activeWallet.address))
assetSigner = { asset: activeWallet.asset }
} else if (isTransactionContext) {
signer = await createSignerFromPath(overrides.keypair || config.keypair)
} else {
signer = config.keypair ? await createSignerFromPath(config.keypair) : createNoopSigner()
}

const payer = payerPath ? await createSignerFromPath(payerPath) : signer
// For asset-signer mode, payer is already set as assetSignerPayer above.
// For normal mode, resolve payer from payerPath or fall back to signer.
const payer = isAssetSigner ? signer : (payerPath ? await createSignerFromPath(payerPath) : signer)

const umi = createUmi(config.rpcUrl!, {
commitment: config.commitment!,
})

umi.use(signerIdentity(signer))
.use(signerPayer(payer))
.use(mplCore())
if (isAssetSigner) {
// Both identity and payer = noopSigner(PDA). Instructions are built with
// the PDA for all accounts. The send layer overrides the transaction fee
// payer to the real wallet via setFeePayer() before buildAndSign.
umi.use(signerIdentity(pdaIdentity!))
.use(signerPayer(pdaIdentity!))
} else {
umi.use(signerIdentity(signer))
.use(signerPayer(payer))
}

umi.use(mplCore())
.use(mplTokenMetadata())
.use(mplToolbox())
.use(mplBubblegum())
Expand Down
30 changes: 9 additions & 21 deletions src/lib/umi/assetSignerPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,31 @@
import { publicKey, PublicKey, Signer, Umi } from '@metaplex-foundation/umi'
import { Signer, Umi } from '@metaplex-foundation/umi'
import { AssetSignerInfo } from '../Context.js'

export type AssetSignerState = {
info: AssetSignerInfo
authority: Signer
}

const ASSET_SIGNER_KEY = '__assetSigner'
const assetSignerStore = new WeakMap<Umi, AssetSignerState>()

/**
* Umi plugin that activates asset-signer mode. Stores the asset-signer state
* on the umi instance so the send layer can wrap transactions in execute().
* keyed by the umi instance so the send layer can wrap transactions in execute().
*
* umi.identity remains the real wallet (required for correct mpl-core account
* resolution in CPI). Use getEffectiveOwner(umi) for address derivation.
* Both umi.identity and umi.payer are noopSigner(PDA), so instructions are
* built with the PDA for all accounts. The send layer overrides the
* transaction fee payer to the real wallet (authority) via setFeePayer().
*/
export const assetSignerPlugin = (state: AssetSignerState) => ({
install(umi: Umi) {
;(umi as any)[ASSET_SIGNER_KEY] = state
assetSignerStore.set(umi, state)
},
})

/**
* Reads asset-signer state from a umi instance.
* Reads asset-signer state for a umi instance.
* Returns undefined when no asset-signer wallet is active.
*/
export const getAssetSigner = (umi: Umi): AssetSignerState | undefined => {
return (umi as any)[ASSET_SIGNER_KEY]
}

/**
* Returns the effective owner for the current umi context:
* - Asset-signer active → the PDA pubkey
* - Normal mode → umi.identity.publicKey
*
* Use this for address derivation (ATA lookups, balance checks, default
* recipients) where the result should reflect the PDA, not the gas payer.
*/
export const getEffectiveOwner = (umi: Umi): PublicKey => {
const state = (umi as any)[ASSET_SIGNER_KEY] as AssetSignerState | undefined
return state ? publicKey(state.info.signerPda) : umi.identity.publicKey
return assetSignerStore.get(umi)
}
9 changes: 9 additions & 0 deletions src/lib/umi/sendTransaction.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { setComputeUnitPrice } from '@metaplex-foundation/mpl-toolbox'
import { BlockhashWithExpiryBlockHeight, Signer, TransactionBuilder, TransactionSignature, Umi } from '@metaplex-foundation/umi'
import { getAssetSigner } from './assetSignerPlugin.js'
import { UmiSendOptions } from './sendOptions.js'

export interface UmiTransactionResponse {
Expand Down Expand Up @@ -37,6 +38,14 @@ const umiSendTransaction = async (
)
}

// In asset-signer mode umi.payer is a noopSigner(PDA) so instructions use
// the PDA for payer accounts. Override the transaction fee payer to the real
// wallet so it pays gas and provides a valid signature.
const assetSigner = getAssetSigner(umi)
if (assetSigner) {
transaction = transaction.setFeePayer(assetSigner.authority)
}

let signedTx = await transaction.buildAndSign(umi).catch((error) => {
throw new Error(`Error building and signing transaction: ${error.message}`)
})
Expand Down
Loading
Loading