User journeysDeposit / redeem / swap

Deposit / redeem / swap

These three calls implement the floor pool — the part of NFTX that gives every floor NFT a fungible price of 1.0 CollectionToken. They are the simplest write paths and are aggressively used by the FE for instant buy/sell UX.

CallDirectionEffect
Locker.depositNFT → CTSends NFTs to the Locker, mints tokenIds.length * 1e18 CollectionToken to _recipient
Locker.redeemCT → NFTBurns tokenIds.length * 1e18 CollectionToken from caller, releases the listed NFTs to _recipient
Locker.swap / swapBatchNFT ↔ NFTSwaps caller’s NFTs for protocol-held NFTs of the same collection 1-for-1, no CT minted/burned

Prerequisites

  • Collection created and initialized.
    • Note: deposit works pre-initialization too, but redeem and swap require the pool exists for any useful frontend integration.
  • For deposit / swap: caller has approved the Locker on the ERC-721 (setApprovalForAll(locker, true)).
  • For redeem / swap: caller holds enough CollectionToken and has approved the Locker on the ERC-20 (or uses Permit2).
  • For redeem / swap: each requested tokenId is currently held by the Locker and is not an active liquid/dutch listing (Locker.isListing(_collection, _tokenId) == false).
  • Locker is not paused.

Inputs

// deposit
function deposit(address _collection, uint[] calldata _tokenIds) external;
function deposit(address _collection, uint[] calldata _tokenIds, address _recipient) external;
 
// redeem
function redeem(address _collection, uint[] calldata _tokenIds) external;
function redeem(address _collection, uint[] calldata _tokenIds, address _recipient) external;
 
// swap
function swap(address _collection, uint _tokenIdIn, uint _tokenIdOut) external;
function swapBatch(address _collection, uint[] calldata _tokenIdsIn, uint[] calldata _tokenIdsOut) external;
ParamMeaning
_collectionERC-721 address
_tokenIds / _tokenIdsIn / _tokenIdsOuttokenIds in or out of the floor pool
_recipientWhere the resulting tokens (CT or NFTs) land. Defaults to msg.sender.
_tokenIdIn / _tokenIdOutThe 1-for-1 swap pair
⚠️

swap requires _tokenIdIn != _tokenIdOut (CannotSwapSameToken). swapBatch requires equal-length arrays (TokenIdsLengthMismatch).

Contract calls

deposit and redeem are single-call paths once approvals are settled.

swap is also single-call on the wire, but the FE typically does two reads first:

  1. Locker.collectionToken(_collection) → ERC-20 address, for balance display.
  2. Locker.isListing(_collection, _tokenIdOut) → reject if the requested _tokenIdOut is currently a liquid/dutch listing.

Code

deposit-redeem-swap.ts
import { lockerAbi, collectionTokenAbi } from "@nftx/contracts";
import { NFTX_CONTRACTS } from "@nftx/config";
import { parseUnits } from "viem";
 
const collection = "0xCollection";
const locker = NFTX_CONTRACTS[base.id].locker;
 
// 1) Deposit two NFTs, receive 2.0 CT
await walletClient.writeContract({
  address: locker,
  abi: lockerAbi,
  functionName: "deposit",
  args: [collection, [101n, 102n]],
});
 
// 2) Redeem two NFTs, burning 2.0 CT
const collectionToken = await publicClient.readContract({
  address: locker,
  abi: lockerAbi,
  functionName: "collectionToken",
  args: [collection],
});
 
await walletClient.writeContract({
  address: collectionToken,
  abi: collectionTokenAbi,
  functionName: "approve",
  args: [locker, parseUnits("2", 18)],
});
 
await walletClient.writeContract({
  address: locker,
  abi: lockerAbi,
  functionName: "redeem",
  args: [collection, [101n, 102n]],
});
 
// 3) Swap tokenId 7 (mine) for tokenId 42 (in the locker)
await walletClient.writeContract({
  address: locker,
  abi: lockerAbi,
  functionName: "swap",
  args: [collection, 7n, 42n],
});

Failure modes

ErrorCallCause
NoTokenIdsallEmpty array
TokenIdsLengthMismatchswapBatchtokenIdsIn.length != tokenIdsOut.length
CannotSwapSameTokenswap_tokenIdIn == _tokenIdOut
TokenIsListing(tokenId)redeem / swapThe requested tokenIdOut is an active liquid/dutch listing — fill or cancel it instead
InsufficientTokenIdsredeem / swapThe Locker doesn’t actually hold the requested tokenIds
Paused()allProtocol paused

For redeem / swap: a missing CT approval surfaces as a generic ERC-20 allowance revert, not a custom error. Always preflight the allowance.

Subgraph follow-through

Each deposit/redeem/swap writes to both Collection (counters) and Listing / ListingActivity:

  • deposit → for each tokenId: a Listing row with format: NONE and null owner/duration/floorMultiple, plus a ListingActivity { activityType: DEPOSITED }.
  • redeem → the matching Listing row is removed, ListingActivity { activityType: REDEEMED }.
  • swap → the in/out pair: the incoming tokenId becomes a new floor Listing (CREATED activity), the outgoing tokenId’s Listing is removed (SWAPPED activity).
query FloorListings($chainId: Int!, $collection: String!) {
  Listing(
    where: {
      chainId: { _eq: $chainId }
      collection: { collection: { _eq: $collection } }
      format: { _eq: NONE }
    }
    order_by: { tokenId: asc }
    limit: 100
  ) {
    tokenId
    createdAt
  }
}

For activity:

query LockerActivity($chainId: Int!, $collection: String!) {
  ListingActivity(
    where: {
      chainId: { _eq: $chainId }
      collection: { collection: { _eq: $collection } }
      activityType: { _in: [DEPOSITED, REDEEMED, SWAPPED] }
    }
    order_by: { created: desc }
    limit: 50
  ) {
    activityType
    tokenId
    txHash
    from
    to
    created
  }
}