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.
| Call | Direction | Effect |
|---|---|---|
Locker.deposit | NFT → CT | Sends NFTs to the Locker, mints tokenIds.length * 1e18 CollectionToken to _recipient |
Locker.redeem | CT → NFT | Burns tokenIds.length * 1e18 CollectionToken from caller, releases the listed NFTs to _recipient |
Locker.swap / swapBatch | NFT ↔ NFT | Swaps caller’s NFTs for protocol-held NFTs of the same collection 1-for-1, no CT minted/burned |
Prerequisites
- Collection created and
initialized.
- Note:
depositworks pre-initialization too, butredeemandswaprequire the pool exists for any useful frontend integration.
- Note:
- For
deposit/swap: caller has approved the Locker on the ERC-721 (setApprovalForAll(locker, true)). - For
redeem/swap: caller holds enoughCollectionTokenand 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;| Param | Meaning |
|---|---|
_collection | ERC-721 address |
_tokenIds / _tokenIdsIn / _tokenIdsOut | tokenIds in or out of the floor pool |
_recipient | Where the resulting tokens (CT or NFTs) land. Defaults to msg.sender. |
_tokenIdIn / _tokenIdOut | The 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:
Locker.collectionToken(_collection)→ ERC-20 address, for balance display.Locker.isListing(_collection, _tokenIdOut)→ reject if the requested_tokenIdOutis 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
| Error | Call | Cause |
|---|---|---|
NoTokenIds | all | Empty array |
TokenIdsLengthMismatch | swapBatch | tokenIdsIn.length != tokenIdsOut.length |
CannotSwapSameToken | swap | _tokenIdIn == _tokenIdOut |
TokenIsListing(tokenId) | redeem / swap | The requested tokenIdOut is an active liquid/dutch listing — fill or cancel it instead |
InsufficientTokenIds | redeem / swap | The Locker doesn’t actually hold the requested tokenIds |
Paused() | all | Protocol 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: aListingrow withformat: NONEand nullowner/duration/floorMultiple, plus aListingActivity { activityType: DEPOSITED }.redeem→ the matchingListingrow is removed,ListingActivity { activityType: REDEEMED }.swap→ the in/out pair: the incoming tokenId becomes a new floor Listing (CREATEDactivity), the outgoing tokenId’s Listing is removed (SWAPPEDactivity).
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
}
}