2

I am trying to setup a simple operation for a user to pay X amount to mint a specific NFT. Have it working on the client only with different code but dont like that the user could just change values on the client.

Here is the button that will kick off the flow and then deserialize and sign the transaction.

import { mplCore } from "@metaplex-foundation/mpl-core";
import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";
import { walletAdapterIdentity } from "@metaplex-foundation/umi-signer-wallet-adapters";
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
import { Connection, clusterApiUrl } from "@solana/web3.js";
import axios from "axios";
import bs58 from "bs58";

const MintButton = () => {
    const { connection } = useConnection();
    const wallet = useWallet();

    const signAndSendTransaction = async (serializedTransaction) => {
        const connection = new Connection(clusterApiUrl('devnet'), 'confirmed');
        const umi = createUmi(connection).use(mplCore());
        umi.use(walletAdapterIdentity(wallet));

        const bs58Deserialized = bs58.decode(serializedTransaction);
        const deserialized = umi.transactions.deserialize(bs58Deserialized);
        await umi.identity.signTransaction(deserialized);
        await umi.rpc.sendTransaction(deserialized);
    };

    const handleMint = async () => {
        if (!wallet.publicKey) {
            alert("Please connect your wallet first.");
            return;
        }

        console.log('public Key', wallet.publicKey);
        try {
            const response = await axios.post('/api/mint', {
                walletAddress: wallet.publicKey.toString()
            });
            await signAndSendTransaction(response.data.tx);

        } catch (error) {
            console.error('Minting failed:', error);
            alert("Minting failed, please try again.");
        }
    };

    return <button onClick={handleMint}>Mint NFT With Umi</button>;
};

export default MintButton;

Here is the route that accepts the call from the button and calls createMintingTransaction to queue up the necessary transactions.

export async function POST(request) {
    const { walletAddress } = await request.json();
    console.log('request', walletAddress);
    try {
        const transaction = await createMintingTransaction(walletAddress, 0.5);
        return Response.json({ transaction: transaction });
    } catch (error) {
        console.error('Error minting NFT:', error);
        return new Response({ success: false, message: 'Failed to mint NFT.' }, { status: 500 })
    }
}

Lastly here is the createMintingTransaction function

export async function createMintingTransaction(userPublicKey: string, mintPrice) {
    const connection = new Connection(clusterApiUrl('devnet'), 'confirmed');
    const umi = createUmi(connection).use(mplCore()).use(mplTokenMetadata());

    // Securely generate or load a keypair for the backend

    const backendSigner = generateSigner(umi);
    umi.use(signerIdentity(backendSigner))

    // Generate transaction instructions for creating an NFT

    console.log('create nft...')
    const instructions = createNft(umi, {
        mint,
        name: 'does it work?',
        uri,
        sellerFeeBasisPoints: percentAmount(5),  // 5.5% royalty
        tokenOwner: publicKey(userPublicKey),
    }).getInstructions();

    const blockhash = await umi.rpc.getLatestBlockhash();
    console.log('blockhash', blockhash)
    const tx = umi.transactions.create({
        version: 0,
        // payer: backendKeypair.publicKey,
        payer: publicKey(userPublicKey),
        instructions,
        blockhash: blockhash.blockhash,
    });

    // Sign the transaction with backend's keypair
    backendSigner.signTransaction(tx);

    const serialized = umi.transactions.serialize(tx);
    const serializedBase58 = bs58.encode(serialized);

    return { status: 200, tx: serializedBase58 };
}

Updated

By following the answers below and adding bs58 encoding I am able to get the wallet to pop but appears to be failing at the wallet signing now.

WalletSignTransactionError: Unexpected error at StandardWalletAdapter._StandardWalletAdapter_signTransaction [as signTransaction] (adapter.js:333:19)

WalletSignTransactionError: Unexpected error at StandardWalletAdapter._StandardWalletAdapter_signTransaction [as signTransaction] (adapter.js:333:19)

Another update After changing payer to the userPublicKey we now are seeing:

Error: failed to send transaction: Transaction signature verification failure
    at Connection.sendEncodedTransaction (index.browser.esm.js:7424:13)
    at async Connection.sendRawTransaction (index.browser.esm.js:7389:20)
    at async Object.sendTransaction (createWeb3JsRpc.mjs:165:25)
    at async signAndSendTransaction (MintButton.tsx:37:13)
    at async handleMint (MintButton.tsx:54:13)

Another Update

Readded backend signing so the frontend client cannot just manipulate the transaction

The error I get is back to: Error: failed to send transaction: Transaction signature verification failure at Connection.sendEncodedTransaction (index.browser.esm.js:7424:13) at async Connection.sendRawTransaction (index.browser.esm.js:7389:20) at async Object.sendTransaction (createWeb3JsRpc.mjs:165:25) at async signAndSendTransaction (MintButton.tsx:44:13) at async handleMint (MintButton.tsx:61:13)

1
  • Whats the error you are getting
    – Jonas H.
    Commented Apr 23 at 8:47

1 Answer 1

3
+100

Backend Code Modifications and Clarifications

Original Implementation Issues:

Incorrect Usage of asset: Initially, asset was mistakenly used where a mint address should have been specified. The asset parameter was incorrectly assumed to be interchangeable with a mint address, but it represents different concepts. asset seemed to be related to a signer object, not a mint.

Misunderstanding of Backend Signing: The backendSigner was supposed to sign the transaction to add a layer of verification and integrity to the transaction creation process but was not clearly utilized in earlier versions. Revised Backend Code:

export async function createMintingTransaction(userPublicKey, mintPrice) {
    const connection = new Connection(clusterApiUrl('devnet'), 'confirmed');
    const umi = createUmi(connection).use(mplCore());
    
    // Securely generate or load a keypair for the backend
    const backendKeypair = umi.eddsa.createKeypairFromSecretKey(new Uint8Array(secretKey));
    umi.use(keypairIdentity(backendKeypair));
    
    // Generate transaction instructions for creating an NFT
    const instructions = createNft(umi, {
      mint: generateMintAddress(),  // Assume generateMintAddress is a function that generates a new mint address
      name: 'My NFT',
      uri: 'https://example.com/my-nft.json',
      sellerFeeBasisPoints: 550,  // 5.5% royalty
      tokenOwner: userPublicKey,
      token: generateTokenAccountAddress(userPublicKey),  // Assume this generates or provides a token account address
    }).getInstructions();
    
    const blockhash = await umi.rpc.getLatestBlockhash();
    const tx = umi.transactions.create({
      version: 1,
      payer: backendKeypair.publicKey,
      instructions,
      blockhash: blockhash.blockhash,
    });
    
    // Sign the transaction with backend's keypair
    backendKeypair.signTransaction(tx);
    const serialized = umi.transactions.serialize(tx);
    return { status: 200, tx: serialized };
}

Changes Made:

Correct Usage of Mint and Token Parameters: The mint address and token accounts are correctly handled.

Backend Signing for Integrity:

The backend transaction is properly signed by the backendKeypair to ensure the transaction's integrity and origin are verifiable, adding a non-repudiation layer.

Frontend Code Modifications

Original Issues:

Signature Missing in Transaction Processing:

There was confusion about how transactions were being signed and sent to the network, with missing steps in deserialization and signing.

const MintButton = () => {
    const { connection } = useConnection();
    const wallet = useWallet();

    const signAndSendTransaction = async (serializedTransaction) => {
        const connection = new Connection(clusterApiUrl('devnet'), 'confirmed');
        const umi = createUmi(connection).use(mplCore());
        umi.use(walletAdapterIdentity(wallet));

        const deserialized = umi.transactions.deserialize(serializedTransaction)
        await umi.identity.signTransaction(deserialized)
        await umi.rpc.sendTransaction(deserialized)
        
    };
    
    const handleMint = async () => {
        if (!wallet.publicKey) {
            alert("Please connect your wallet first.");
            return;
        }

        console.log('public Key', wallet.publicKey);
        try {
            const response = await axios.post('/api/mint', {
                walletAddress: wallet.publicKey.toString()
            });
            signAndSendTransaction(response.data.transaction);

        } catch (error) {
            console.error('Minting failed:', error);
            alert("Minting failed, please try again.");
        }
    };

    return <button onClick={handleMint}>Mint NFT</button>;
};

export default MintButton;

Summary

Signing by both backend and frontend:

The transaction is initially signed by the backend to validate and authorize the creation and structure of the transaction (often to add integrity and non-repudiation to the transaction initiation). The frontend, ideally the end-user's wallet, signs the transaction to authorize it finally, especially for operations that involve user's assets or tokens. This dual-signing mechanism is critical for security and operation validation in decentralized applications.

Clarification on backendSigner and asset:

It appears there was confusion in your earlier implementation where backendSigner might have been misused or misassigned. In blockchain transactions, particularly those involving NFTs or tokens, the backendSigner should only be used to sign transactions and should not be confused with asset or mint parameters which typically represent token identifiers or related configurations.

https://developers.metaplex.com/umi/recipes

10
  • One of my worries is the time it takes for the actual User to sign the transaction on the frontend, as the blockhash having been established in the backend this means that it is a time sensitive matter so best approaches should be made to ensure this is signed as quickly as possible once the transaction is initially signed by the backend to being sent on the frontend. Commented Apr 23 at 21:11
  • Lol really spent like 2 hours working on this hope I get those points though. Commented Apr 23 at 21:12
  • I've updated my question with the new code that I have tried, and the new error I am seeing any clue with this?
    – mstelz
    Commented Apr 23 at 21:55
  • @mstelz Glad we are able to move forward, also sorry about missing that mint portion of Mint: The Mint account of the asset. If it doesn't exist, it must be provided as a Signer as it will be initialized. Typically, we generate a new keypair for this purpose. Found it at developers.metaplex.com/token-metadata/mint and sec while I look into it Commented Apr 23 at 22:15
  • First time actually opening the Metaplex docs to work on it today so bear with me haha, I also did have the option at hand to convert to base64 but went with the docs instead as I don't have a test environment set up at the moment for Metaplex but just one sec. @mstelz Commented Apr 23 at 22:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.