buy-nft
Demonstrates the NFT purchase flow: approve-then-transfer on EVM (ERC-721), and token account transfer on Solana.
Dependencies
"use client";
import { useAccount, useWriteContract, useWaitForTransactionReceipt } from "wagmi";
import { useState } from "react";
// ★ ERC-721 approve + transferFrom pattern
// This is the marketplace-agnostic flow: seller approves, buyer calls transferFrom
// Real marketplaces (OpenSea, Blur) wrap this in an escrow/order contract
const erc721Abi = [
{
name: "approve",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "to", type: "address" },
{ name: "tokenId", type: "uint256" },
],
outputs: [],
},
{
name: "transferFrom",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "tokenId", type: "uint256" },
],
outputs: [],
},
{
name: "ownerOf",
type: "function",
stateMutability: "view",
inputs: [{ name: "tokenId", type: "uint256" }],
outputs: [{ name: "", type: "address" }],
},
] as const;
// ★ Step 1: Seller approves the buyer (or a marketplace contract) to transfer
export function ApproveNFT() {
const [contractAddress, setContractAddress] = useState("");
const [approved, setApproved] = useState("");
const [tokenId, setTokenId] = useState("");
const { writeContract, data: hash, isPending } = useWriteContract();
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash });
return (
<div>
<h2>Step 1: Approve NFT Transfer (Seller)</h2>
<input value={contractAddress} onChange={(e) => setContractAddress(e.target.value)} placeholder="NFT contract (0x...)" />
<input value={approved} onChange={(e) => setApproved(e.target.value)} placeholder="Buyer or marketplace address (0x...)" />
<input value={tokenId} onChange={(e) => setTokenId(e.target.value)} placeholder="Token ID" />
<button
onClick={() => writeContract({
address: contractAddress as `0x${string}`,
abi: erc721Abi,
functionName: "approve",
args: [approved as `0x${string}`, BigInt(tokenId)],
})}
disabled={isPending}
>
{isPending ? "Approving..." : "Approve"}
</button>
{isConfirming && <p>Confirming...</p>}
{isSuccess && <p>Approved! Tx: {hash}</p>}
</div>
);
}
// ★ Step 2: Buyer calls transferFrom to claim the NFT
export function BuyNFT() {
const { address } = useAccount();
const [contractAddress, setContractAddress] = useState("");
const [seller, setSeller] = useState("");
const [tokenId, setTokenId] = useState("");
const { writeContract, data: hash, isPending } = useWriteContract();
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash });
return (
<div>
<h2>Step 2: Buy NFT (Buyer)</h2>
<p><em>Requires seller to have approved you first.</em></p>
<input value={contractAddress} onChange={(e) => setContractAddress(e.target.value)} placeholder="NFT contract (0x...)" />
<input value={seller} onChange={(e) => setSeller(e.target.value)} placeholder="Seller address (0x...)" />
<input value={tokenId} onChange={(e) => setTokenId(e.target.value)} placeholder="Token ID" />
<button
onClick={() => writeContract({
address: contractAddress as `0x${string}`,
abi: erc721Abi,
functionName: "transferFrom",
args: [seller as `0x${string}`, address as `0x${string}`, BigInt(tokenId)],
})}
disabled={isPending}
>
{isPending ? "Buying..." : "Buy (Transfer to Me)"}
</button>
{isConfirming && <p>Confirming...</p>}
{isSuccess && <p>NFT transferred! Tx: {hash}</p>}
</div>
);
}Buy NFT — Learn
How NFT marketplaces work
At the protocol level, buying an NFT is a token transfer. The complexity lies in making that transfer trustless — the buyer shouldn't pay without receiving the NFT, and the seller shouldn't give up the NFT without receiving payment.
Marketplaces solve this with escrow:
Simple (risky): Buyer sends ETH → Seller sends NFT (two separate txs — seller can ghost)
Marketplace (safe):
Seller approves marketplace contract
Buyer calls marketplace.buy() with ETH value
Contract atomically: transfers ETH to seller + transfers NFT to buyer
One transaction — either both happen or neither does
EVM: The approve + transferFrom pattern
How approval works
// Seller calls this
function approve(address to, uint256 tokenId) external {
require(ownerOf(tokenId) == msg.sender);
_tokenApprovals[tokenId] = to; // Anyone can read this
}
// Marketplace contract calls this
function transferFrom(address from, address to, uint256 tokenId) external {
require(
msg.sender == from ||
msg.sender == _tokenApprovals[tokenId] || // ★ approved party can transfer
isApprovedForAll(from, msg.sender)
);
_transfer(from, to, tokenId);
}
setApprovalForAll
Instead of approving token by token, sellers can approve all their tokens to a marketplace:
nft.setApprovalForAll(marketplaceAddress, true);
// Now the marketplace can transfer any of your NFTs
// This is what "listing on OpenSea" does
This is convenient but grants the marketplace broad authority over your NFTs.
How a marketplace buy works
// Simplified marketplace contract
function buy(address nftContract, uint256 tokenId) external payable {
Listing memory listing = listings[nftContract][tokenId];
require(msg.value >= listing.price, "Insufficient payment");
// Atomic: payment + transfer in one tx
IERC721(nftContract).transferFrom(listing.seller, msg.sender, tokenId);
payable(listing.seller).transfer(listing.price);
emit NFTSold(nftContract, tokenId, listing.seller, msg.sender, listing.price);
}
Royalties
EIP-2981 (EVM)
// On the NFT contract
function royaltyInfo(uint256 tokenId, uint256 salePrice)
external view returns (address receiver, uint256 royaltyAmount)
{
return (creatorAddress, salePrice * royaltyBps / 10000);
}
EIP-2981 is informational — it says "here's how much royalty is owed." Enforcement is up to the marketplace. Some marketplaces (like Blur) made royalties optional, causing controversy in the NFT space.
Metaplex royalties (Solana)
{
"seller_fee_basis_points": 500, // 5% royalty
"properties": {
"creators": [{ "address": "CreatorWallet...", "share": 100 }]
}
}
Similarly, Metaplex royalties are enforced by marketplaces, not the protocol. The Metaplex Programmable NFTs (pNFTs) standard was introduced to allow on-chain royalty enforcement via transfer hooks.
Solana: Escrow-based buying
On Solana, the common pattern is:
1. Seller: deposit NFT into marketplace escrow account
2. Seller: create a listing account with price + terms
3. Buyer: call marketplace.executeSale() with SOL
4. Program: atomically releases NFT to buyer + SOL to seller
5. Program: closes escrow account, returns rent to seller
The NFT physically sits in an escrow token account between listing and sale. This is more capital-efficient than EVM approvals because the NFT is locked — you can't accidentally list the same NFT on two marketplaces.
Auction patterns
Beyond fixed-price sales, common auction types:
| Type | How it works | Example |
|---|---|---|
| English (ascending) | Bids increase, highest wins at deadline | eBay style |
| Dutch (descending) | Price drops over time, first buyer wins | Art Blocks |
| Reserve price | Sale only happens if minimum bid met | Most NFT auctions |
| FCFS (fixed price) | First come, first served | Mint events |
Each requires different smart contract logic, but all build on the same approve/transfer primitives.