How to make a sell programatically

A tutorial on how to make a script for selling NFTs on caviar.

This tutorial assumes you already went through the buying tutorial here. If you haven't read through that tutorial yet I would highly recommend reading it first.

In the previous tutorial we already installed the required dependencies and purchased some BAYCs. Now, let's figure out how to sell those NFTs back into the same pool.

First, create a file called sell.js and then import the dependencies and setup the required interfaces:

const ethers = require("ethers");
const caviarAbi = require("./caviar.abi.json");
const pairAbi = require("./pair.abi.json");
const { Alchemy, Network } = require("alchemy-sdk");
const fetch = require("node-fetch");
require("dotenv").config();

const main = async () => {
  // create the provider to connect to the network
  const provider = new ethers.providers.AlchemyProvider(
    "goerli",
    process.env.ALCHEMY_API_KEY
  );

  // create the signer to sign transactions from a wallet
  const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider);

  // create the contract instance for caviar
  const GOERLI_CAVIAR_ADDRESS = "0x15B9D8ba57E67D6683f3E7Bec24A32b98a7cdb6b";
  const GOERLI_BAYC_ADDRESS = "0xc1a308d95344716054d4c078831376fc78c4fd72";
  const Caviar = new ethers.Contract(
    GOERLI_CAVIAR_ADDRESS,
    caviarAbi,
    provider
  );

  // fetch the address of the floor pair for BAYC:ETH
  const baycEthPairAddress = await Caviar.pairs(
    GOERLI_BAYC_ADDRESS, // address of the NFT token
    ethers.constants.AddressZero, // address of the base token (address(0) for ETH)
    ethers.constants.HashZero // hash of the merkle root for valid token ids (hash(0) for no merkle root)
  );

  // create the contract instance for the floor BAYC:ETH pair
  const BaycEthPair = new ethers.Contract(baycEthPairAddress, pairAbi, signer);
  console.log("BAYC:ETH pair address:", baycEthPairAddress);

  // fetch the reserves for the BAYC:ETH pair
  const baseTokenReserves = await BaycEthPair.baseTokenReserves();
  const fractionalTokenReserves = await BaycEthPair.fractionalTokenReserves();
};

main();

Now using the xy=k formula and re-implementing the sellQuote logic in the pair contract, we can figure out how much ETH we expect to receive from selling the two BAYCs into the pool. We will use this value as the minimum expected output when submitting the transaction to the pair contract.

const main = async () => {
  /// ... SNIP ... ///

  // calculate the amount of ETH received when selling 2 BAYCs using xy=k formula
  // 2 BAYC == 2 * 10^18
  const AMOUNT_TO_SELL = ethers.BigNumber.from("2").mul(ethers.utils.parseUnits("1", 18));
  const ethReceived = AMOUNT_TO_SELL
    .mul("990")
    .mul(baseTokenReserves)
    .div(fractionalTokenReserves.mul("1000").add(AMOUNT_TO_SELL.mul("990")));

  console.log("ETH received:", ethers.utils.formatEther(ethReceived), "ETH");
};

Great, now we can fetch the token IDs of the BAYCs that we hold and use them as input to the sell transaction.

const main = async () => {
  /// ... SNIP ... ///
  
  // create the alchemy instance
  const alchemy = new Alchemy({
    apiKey: process.env.ALCHEMY_API_KEY,
    network: Network.ETH_GOERLI,
  });

  // fetch some BAYC NFTs that you currently hold
  const { ownedNfts } = await alchemy.nft.getNftsForOwner(signer.address, {
    contractAddresses: [GOERLI_BAYC_ADDRESS],
  });

  const tokenIdsToSell = [ownedNfts[0].tokenId, ownedNfts[1].tokenId];
  console.log("Token IDs to sell:", tokenIdsToSell);
};

To execute the sell, the pair contract must be first approved for spending the BAYCs from your wallet. So on your first sell two transactions must be submitted - one to approve, and one to execute the sell. After you have approved the contract once, it does not need to be done again and you can execute sells directly. We will need the ERC721 ABI to make the approval. Create a file erc721.abi.json and copy and paste the abi from here. And then import it at the top of the file:

const erc721Abi = require("./erc721.abi.json");

Now lets make the approval and swap:

const main = async () => {
  /// ... SNIP ... ///
  
  const bayc = new ethers.Contract(GOERLI_BAYC_ADDRESS, erc721Abi, signer);
  const approveTx = await bayc.setApprovalForAll(baycEthPairAddress, true);
  console.log("Approve transaction:", approveTx);
  await approveTx.wait();

  // deadline for the trade is 60 minutes from now
  const deadline = parseInt((Date.now() + 1000 * 60 * 60) / 1000);
  console.log("Trade deadline unix timestamp:", deadline);

  // fetch the stolen NFT proofs from reservoir
  const reservoirUrl = `https://api.reservoir.tools/oracle/tokens/status/v2?${tokenIdsToSell
    .map((tokenId) => `tokens=${GOERLI_BAYC_ADDRESS}:${tokenId}`)
    .join("&")}`;

  const { messages } = await fetch(reservoirUrl, {
    headers: { "x-api-key": "demo-api-key" }, // you can use your own API key here or the default "demo-api-key"
  }).then((res) => res.json());
  
  // order the stolen NFT proofs so that the match up with the tokenIdsToSell array
  const orderedMessages = tokenIdsToSell.map(
    (tokenId) =>
      messages.find(({ token }) => token.split(":")[1] === tokenId.toString())
        .message
  );

  const tx = await BaycEthPair.nftSell(
    tokenIdsToSell,
    ethReceived,
    deadline,
    [],
    orderedMessages
  );
  console.log("Sell transaction:", tx);
};

Nice, you just sold two BAYCs. You should see a transaction very similar to this one here. The code for this tutorial is all available on github here. Thanks for reading and feel free to reach out on discord if you have any questions!

Last updated