Skip to main content

Redeem a mint voucher

The payload of a mint voucher contains the following fields:

{
"data": {
"contract": "0xC13Fc7A39000961847187Dbc042572319931Ca13",
"minter": "0xa99D7eD4bB4E3EEFed18a0269bE46Aa8c49Ab165",
"network_id": 59140,
"signature": "0x6a7b1b4e9f8fa7d359a227a4c3b5885c2877b73e02d513a7ee8d4b28c569b063222afb06ba075b4bb69c6665b9aa1d1f1edc0c1487261eee5cb9fe4640bd21d61c",
"voucher": {
"net_recipient": "0xabf518e9B51D8DfD936E552d22B35Cfd8bDB48a5",
"initial_recipient": "0xdf50409aBe6B503B3F6bA64E7e26e1312048A84E",
"initial_recipient_amount": "0",
"quantity": 1,
"nonce": 1,
"expiry": 1674740015,
"price": "100000000000000000",
"token_id": "11",
"currency": "0x0000000000000000000000000000000000000000"
}
},
"expires_at": "2023-01-26T13:33:35",
"id": "3604184b-cb69-4783-8f58-ec291225c7d5"
}
  • data:
    • contract: The token contract for the collection. You must call this contract with the mint voucher params to redeem the voucher.
    • minter: The wallet address of the voucher owner. This is the buyer's ethereum address sent in the purchase intent.
    • network_id: The blockchain network for the items to redeem.
    • signature: A signature signed by the platform backend. When a minter tries to call the contract, the contract first validates that the voucher is valid and is being called by the right owner, then performs the mint.
    • voucher: The params that you must call the token contract with.
      • net_recipient: The payout address of your organization.
      • initial_recipient: The wallet address of the platform.
      • initial_recipient_amount: The platform fee.
      • quantity: How many items to mint in the voucher.
      • nonce: A way of accounting for how many vouchers a minter has redeemed.
      • expiry: Timestamp of the expiration of the voucher. After which, the voucher can no longer be redeemed.
      • price: Price of the items in wei or the currency's corresponding decimals. For ERC-20 tokens, this is usually 10e18.
      • token_id: The token id of the item to mint. If 0, the mint voucher is from a collection with EXTERNAL token_id_assignment_strategy. The platform will assign a token ID when it detects a mint.
      • currency: the currency address the voucher accepts. A 0x0 address means the voucher accepts ETH.
  • expires_at: The expiration of the payment session duration. Payments must be completed within this time frame.
  • id: An internal ID by the platform for this purchase intent.

To call the token contract with the mint voucher params, you will need a library that connects with a Web3Provider, such as ethers.js or wagmi.sh. You will also need the contract address, the mint voucher abi, and the function name you want to call, in this case, is mintWithVoucher.

Redeem a "free" voucher

Here is an example using wagmi.sh

// Must match the order of arguments in the abi
const voucherArgs = [
voucherData.voucher.net_recipient,
voucherData.voucher.initial_recipient,
voucherData.voucher.initial_recipient_amount,
voucherData.voucher.quantity,
voucherData.voucher.nonce,
voucherData.voucher.expiry,
voucherData.voucher.price,
voucherData.voucher.token_id,
voucherData.voucher.currency,
];
const { config, isError } = usePrepareContractWrite({
address: voucherData.contract,
abi: MintVoucherABI,
functionName: "mintWithVoucher",
chainId: voucherData.network_id,
args: [voucherArgs, voucherData.signature],
});
const { write, writeAsync, isLoading, isSuccess } = useContractWrite({
...config,
onSuccess(data) {
...
},
onError: (err) => {
handlePrepareError(err);
},
});
const handleMint = async () => {
if (writeAsync) {
try {
const tx = await writeAsync();
const txReceipt = await tx.wait();
if (txReceipt.status === 1) {
...
} else {
setTxError(true);
}
} catch (err) {
handleTxError(err);
setTxError(true);
}
}
};

It is generally a good idea to add some padding to the estimated gas, just in case the estimate is not accurate or does not account for changes in the recent state.

// Add 20% to the estimated gas just to be safe
const configWithPaddedGasLimit = useMemo(() => {
if (!config || !config.request) {
return config;
} else {
return {
...config,
request: {
...config.request,
gasLimit: addGasMargin(config.request.gasLimit),
},
};
}
}, [config]);
export function addGasMargin(value: BigNumber): BigNumber {
return value.mul(BigNumber.from(10000 + 2000)).div(BigNumber.from(10000));
}

Then you can modify the gas limit for your actual contract write in the following way:

const { write, writeAsync } = useContractWrite(configWithPaddedGasLimit);

It might also be a good idea to protect your users from minting if the estimated gas is unreasonably high. Depending on the complexity of your contract, a mint on the Ethereum network should cost somewhere between 21,000 and 80,000. If the estimated gas is above 200,000, for example, it should be a cause for concern.

// Sanity check gas isn't too high. If it is, protect user by setting an error to disable the mint button.
useEffect(() => {
if (
!prepareError &&
config &&
config.request &&
config.request.gasLimit.gt(MAX_GAS)
) {
handlePrepareError(new Error("Estimated gas limit is too high"));
}
}, [config, handlePrepareError, prepareError]);

Redeem a voucher with payment in ETH

For a voucher priced in ETH, you need to send the token contract the total amount of ETH equal to price multiplied by quantity. To do this, you need to override the value field in contract call:

const mintCost = BigNumber.from(voucherData.voucher.price).mul(
voucherData.voucher.quantity,
);
const { config, isError } = usePrepareContractWrite({
address: voucherData.contract,
abi: MintVoucherABI,
functionName: "mintWithVoucher",
chainId: voucherData.network_id,
args: [voucherArgs, voucherData.signature],
// If paying with native currency, send with a value
overrides: {
...(voucherData.voucher.currency === constants.AddressZero
? { value: mintCost }
: {}),
},
});

Redeem a voucher with payment in ERC20

For a voucher priced in ERC20, there are two steps that must be implemented:

  1. When a minter initiates a purchase, they must first approve the amount of ERC20 tokens to use for this transaction. This action grants permission to the token contract to transfer up to the approved amount (called an allowance) from the minter's account. This requires you to call the ERC20 contract with method approve, and pass in token contract as the spender.
  2. When the token amount is approved, you can implement the mint function just like with a "free" voucher. You do not need to override the value field in this case.
// Approval is not needed for native currency
const [approvalNeeded, setApprovalNeeded] = useState(
voucherData.voucher.currency !== constants.AddressZero,
);
const mintCost = BigNumber.from(voucherData.voucher.price).mul(
voucherData.voucher.quantity,
);
const { data: currentAllowance } = useContractRead({
address: voucherData.voucher.currency,
abi: erc20ABI,
functionName: "allowance",
chainId: voucherData.network_id,
args: [voucherData.minter, voucherData.contract],
onSuccess: (currentAllowance) => {
// Approval is not needed if current allowance greater than the mint cost
if (BigNumber.from(currentAllowance).gt(mintCost)) {
setApprovalNeeded(false);
} else {
setApprovalNeeded(true);
}
},
});

To create an approve action, you would call the ERC20 contract method approve, and pass in the token contract as the spender:

const { config, isLoading } = usePrepareContractWrite({
address: voucherData.voucher.currency,
abi: erc20ABI,
functionName: "approve",
chainId: voucherData.network_id,
// approve 10x amount to save gas for future mints
args: [voucherData.contract, mintCost.mul(10)],
onError: (err) => {
handlePrepareError(err);
},
});

Once ERC20 tokens are approved for use, you can then proceed with the mint action like before:

<>
{approvalNeeded ? (
<ApproveToken
voucherData={voucherData}
setApprovalNeeded={setApprovalNeeded}
/>
) : (
<VoucherMintButton voucherData={voucherData} />
)}
<VoucherCancelButton voucherData={voucherData} />
</>