diff --git a/src/commands/core/asset/transfer.ts b/src/commands/core/asset/transfer.ts index 5e78f97..376eed3 100644 --- a/src/commands/core/asset/transfer.ts +++ b/src/commands/core/asset/transfer.ts @@ -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' @@ -40,23 +39,19 @@ export default class AssetTransfer extends TransactionCommand { @@ -67,7 +66,7 @@ export default class TmTransfer extends TransactionCommand { mint: asset.publicKey, token: findAssociatedTokenPda(umi, { mint: asset.publicKey, - owner: getEffectiveOwner(umi), + owner: umi.identity.publicKey, })[0], }) @@ -91,7 +90,7 @@ export default class TmTransfer extends TransactionCommand { transferIx = transferV1(umi, { mint: asset.publicKey, authority: umi.identity, - tokenOwner: getEffectiveOwner(umi), + tokenOwner: umi.identity.publicKey, destinationOwner, destinationToken: destinationToken[0], tokenStandard: TokenStandard.ProgrammableNonFungible, @@ -106,7 +105,7 @@ export default class TmTransfer extends TransactionCommand { transferIx = transferV1(umi, { mint: asset.publicKey, authority: umi.identity, - tokenOwner: getEffectiveOwner(umi), + tokenOwner: umi.identity.publicKey, destinationOwner, tokenStandard: unwrapOptionRecursively(asset.metadata.tokenStandard) || TokenStandard.NonFungible }) diff --git a/src/commands/toolbox/sol/airdrop.ts b/src/commands/toolbox/sol/airdrop.ts index 0b8a291..dcb6e78 100644 --- a/src/commands/toolbox/sol/airdrop.ts +++ b/src/commands/toolbox/sol/airdrop.ts @@ -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' @@ -50,7 +49,7 @@ export default class ToolboxSolAirdrop extends TransactionCommand `-------------------------------- @@ -42,7 +41,7 @@ export default class ToolboxSolBalance extends TransactionCommand null) @@ -48,7 +47,7 @@ export default class ToolboxSolUnwrap extends TransactionCommand null) @@ -57,7 +56,7 @@ export default class ToolboxSolWrap extends TransactionCommand w.name === walletPayerName) @@ -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()) diff --git a/src/lib/umi/assetSignerPlugin.ts b/src/lib/umi/assetSignerPlugin.ts index 2e84647..3fbdb0d 100644 --- a/src/lib/umi/assetSignerPlugin.ts +++ b/src/lib/umi/assetSignerPlugin.ts @@ -1,4 +1,4 @@ -import { publicKey, PublicKey, Signer, Umi } from '@metaplex-foundation/umi' +import { Signer, Umi } from '@metaplex-foundation/umi' import { AssetSignerInfo } from '../Context.js' export type AssetSignerState = { @@ -6,38 +6,26 @@ export type AssetSignerState = { authority: Signer } -const ASSET_SIGNER_KEY = '__assetSigner' +const assetSignerStore = new WeakMap() /** * 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) } diff --git a/src/lib/umi/sendTransaction.ts b/src/lib/umi/sendTransaction.ts index 3384f53..f57736d 100644 --- a/src/lib/umi/sendTransaction.ts +++ b/src/lib/umi/sendTransaction.ts @@ -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 { @@ -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}`) }) diff --git a/src/lib/umi/wrapForAssetSigner.ts b/src/lib/umi/wrapForAssetSigner.ts index 2557ac4..00e9421 100644 --- a/src/lib/umi/wrapForAssetSigner.ts +++ b/src/lib/umi/wrapForAssetSigner.ts @@ -1,31 +1,13 @@ import { execute, fetchAsset, fetchCollection } from '@metaplex-foundation/mpl-core' -import { Instruction, publicKey, Signer, TransactionBuilder, Umi } from '@metaplex-foundation/umi' +import { publicKey, Signer, TransactionBuilder, Umi } from '@metaplex-foundation/umi' import { AssetSignerInfo } from '../Context.js' -/** - * Rewrites signer accounts in an instruction: swaps the wallet pubkey for the - * PDA. Only isSigner accounts are rewritten so derived addresses (ATAs, PDAs) - * and payer accounts stay correct. - */ -function rewriteSignerAuthority(ix: Instruction, walletPubkey: string, pdaPubkey: string): Instruction { - const pda = publicKey(pdaPubkey) - return { - ...ix, - keys: ix.keys.map(k => - k.pubkey.toString() === walletPubkey && k.isSigner - ? { ...k, pubkey: pda } - : k - ), - } -} - /** * Wraps a TransactionBuilder's instructions inside an MPL Core `execute` call * so the asset's signer PDA signs for them on-chain. * - * umi.identity is the real wallet, so instructions have the wallet as - * authority. This function rewrites signer accounts (wallet → PDA) then - * wraps in execute() with the wallet as the caller. + * Since umi.identity is a noopSigner keyed to the PDA, instructions are + * already built with the PDA as authority — no rewriting needed. */ export const wrapForAssetSigner = async ( umi: Umi, @@ -41,10 +23,7 @@ export const wrapForAssetSigner = async ( collection = await fetchCollection(umi, asset.updateAuthority.address) } - const walletPubkey = authority.publicKey.toString() - const instructions = transaction.getInstructions().map(ix => - rewriteSignerAuthority(ix, walletPubkey, assetSigner.signerPda) - ) + const instructions = transaction.getInstructions() return execute(umi, { asset,