diff --git a/packages/hardhat/contracts/ExpirableERC721.sol b/packages/hardhat/contracts/ExpirableERC721.sol
index 19223e6..bb41a7e 100644
--- a/packages/hardhat/contracts/ExpirableERC721.sol
+++ b/packages/hardhat/contracts/ExpirableERC721.sol
@@ -1,18 +1,43 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
-import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
+import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title ExpirableERC721
* @dev ERC721 Token that can be minted in batches and supports token expiration.
*/
-contract ExpirableERC721 is ERC721, Ownable {
+contract ExpirableERC721 is ERC721Enumerable, Ownable {
uint256 private _currentTokenId = 0;
mapping(uint256 => uint256) private _tokenExpirations;
- event TokenMinted(address indexed to, uint256 indexed tokenId, uint256 expirationTime);
+ /**
+ * @dev Struct to store the schema information for each token.
+ */
+ struct Voucher {
+ string title;
+ uint256 issueDate;
+ uint256 amount;
+ uint256 expiryDate;
+ string usageScope;
+ }
+
+ // Mapping from token ID to Voucher details
+ mapping(uint256 => Voucher) private _tokenVouchers;
+
+ /**
+ * @dev Emitted when a token is minted.
+ */
+ event TokenMinted(
+ address indexed to,
+ uint256 indexed tokenId,
+ string title,
+ uint256 issueDate,
+ uint256 amount,
+ uint256 expiryDate,
+ string usageScope
+ );
/**
* @dev Constructor that initializes the ERC721 token with a name, symbol, and sets the initial owner.
@@ -26,24 +51,65 @@ contract ExpirableERC721 is ERC721, Ownable {
address initialOwner
) ERC721(name, symbol) Ownable(initialOwner) {}
- /**
- * @dev Mints multiple tokens to a single address with specified expiration times.
- * @param to The address to mint the tokens to.
- * @param numberOfTokens The number of tokens to mint.
- * @param expirationTimes An array of expiration timestamps for each token.
- */
- function mintBatch(address to, uint256 numberOfTokens, uint256[] memory expirationTimes) external onlyOwner {
- require(numberOfTokens == expirationTimes.length, "Mismatched inputs");
+ function mintBatch(
+ address to,
+ uint256 numberOfTokens,
+ string memory title,
+ uint256 issueDate,
+ uint256 amount,
+ uint256 expiryDate,
+ string memory usageScope
+ ) external {
+ require(to != address(0), "Cannot mint to zero address");
+ require(numberOfTokens > 0, "Must mint at least one token");
for (uint256 i = 0; i < numberOfTokens; i++) {
uint256 newTokenId = _currentTokenId;
_mint(to, newTokenId);
- _tokenExpirations[newTokenId] = expirationTimes[i];
- emit TokenMinted(to, newTokenId, expirationTimes[i]);
+
+ // Assign the schema data to the newly minted token
+ _tokenVouchers[newTokenId] = Voucher({
+ title: title,
+ issueDate: issueDate,
+ amount: amount,
+ expiryDate: expiryDate,
+ usageScope: usageScope
+ });
+
+ emit TokenMinted(
+ to,
+ newTokenId,
+ title,
+ issueDate,
+ amount,
+ expiryDate,
+ usageScope
+ );
+
_currentTokenId++;
}
}
+ /**
+ * @dev Returns a list of Voucher structs owned by a specific address.
+ * @param owner The address to query vouchers for.
+ * @return An array containing the Voucher structs owned by the address.
+ *
+ * @notice Be cautious when calling this function with a large number of tokens,
+ * as it may consume a lot of memory and exceed block gas limits.
+ */
+ function listVouchers(address owner) external view returns (Voucher[] memory) {
+ uint256 balance = balanceOf(owner);
+ Voucher[] memory vouchers = new Voucher[](balance);
+
+ for (uint256 i = 0; i < balance; i++) {
+ uint256 tokenId = tokenOfOwnerByIndex(owner, i);
+ vouchers[i] = _tokenVouchers[tokenId];
+ }
+ return vouchers;
+ }
+
+
/**
* @dev Returns the expiration time of a specific token.
* @param tokenId The ID of the token.
diff --git a/packages/nextjs/app/user/list/page.tsx b/packages/nextjs/app/user/list/page.tsx
new file mode 100644
index 0000000..8336700
--- /dev/null
+++ b/packages/nextjs/app/user/list/page.tsx
@@ -0,0 +1,39 @@
+import type { NextPage } from "next";
+import { CardList } from "~~/components/CardList";
+
+const UserListPage: NextPage = () => {
+ const cards = [
+ {
+ title: "引換券",
+ issueDate: "2024.12.05",
+ amount: 7000,
+ expiryDate: "2025.06.30",
+ usageScope: "店舗全体",
+ },
+ {
+ title: "引換券",
+ issueDate: "2024.12.05",
+ amount: 7000,
+ expiryDate: "2025.06.30",
+ usageScope: "店舗全体",
+ },
+ {
+ title: "引換券",
+ issueDate: "2024.12.05",
+ amount: 7000,
+ expiryDate: "2025.06.30",
+ usageScope: "店舗全体",
+ },
+ {
+ title: "引換券",
+ issueDate: "2024.12.05",
+ amount: 7000,
+ expiryDate: "2025.06.30",
+ usageScope: "店舗全体",
+ },
+ ];
+
+ return ;
+};
+
+export default UserListPage;
diff --git a/packages/nextjs/components/Card.tsx b/packages/nextjs/components/Card.tsx
new file mode 100644
index 0000000..c3229c0
--- /dev/null
+++ b/packages/nextjs/components/Card.tsx
@@ -0,0 +1,65 @@
+"use client";
+
+import React from "react";
+import { Box, CardContent, Card as MuiCard, Typography } from "@mui/material";
+
+type CardProps = {
+ title: string; // タイトル
+ issueDate: string; // 発行日
+ amount: number; // 金額
+ expiryDate: string; // 有効期限
+ usageScope: string; // 使用範囲
+};
+
+export const Card = ({ title, issueDate, amount, expiryDate, usageScope }: CardProps) => {
+ return (
+
+
+
+ {title}
+
+
+
+ {issueDate} 発行
+
+
+
+ {amount}円
+
+
+
+
+ 有効期限: {expiryDate}
+
+
+
+
+ 使用範囲: {usageScope}
+
+
+
+ );
+};
diff --git a/packages/nextjs/components/CardList.tsx b/packages/nextjs/components/CardList.tsx
new file mode 100644
index 0000000..9a4975a
--- /dev/null
+++ b/packages/nextjs/components/CardList.tsx
@@ -0,0 +1,41 @@
+import React from "react";
+import { Card } from "~~/components/Card";
+
+type CardData = {
+ title: string;
+ issueDate: string;
+ amount: number;
+ expiryDate: string;
+ usageScope: string;
+};
+
+type CardListProps = {
+ cards: CardData[];
+};
+
+export const CardList = ({ cards }: CardListProps) => {
+ return (
+
+ {cards.map((card, index) => (
+
+ ))}
+
+ );
+};
diff --git a/packages/nextjs/contracts/deployedContracts.ts b/packages/nextjs/contracts/deployedContracts.ts
index ff3b7d3..bdeec41 100644
--- a/packages/nextjs/contracts/deployedContracts.ts
+++ b/packages/nextjs/contracts/deployedContracts.ts
@@ -7,7 +7,7 @@ import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract";
const deployedContracts = {
31337: {
ExpirableERC721: {
- address: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
+ address: "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707",
abi: [
{
inputs: [
@@ -30,6 +30,11 @@ const deployedContracts = {
stateMutability: "nonpayable",
type: "constructor",
},
+ {
+ inputs: [],
+ name: "ERC721EnumerableForbiddenBatchMint",
+ type: "error",
+ },
{
inputs: [
{
@@ -133,6 +138,22 @@ const deployedContracts = {
name: "ERC721NonexistentToken",
type: "error",
},
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "owner",
+ type: "address",
+ },
+ {
+ internalType: "uint256",
+ name: "index",
+ type: "uint256",
+ },
+ ],
+ name: "ERC721OutOfBoundsIndex",
+ type: "error",
+ },
{
inputs: [
{
@@ -239,12 +260,36 @@ const deployedContracts = {
name: "tokenId",
type: "uint256",
},
+ {
+ indexed: false,
+ internalType: "string",
+ name: "title",
+ type: "string",
+ },
{
indexed: false,
internalType: "uint256",
- name: "expirationTime",
+ name: "issueDate",
type: "uint256",
},
+ {
+ indexed: false,
+ internalType: "uint256",
+ name: "amount",
+ type: "uint256",
+ },
+ {
+ indexed: false,
+ internalType: "uint256",
+ name: "expiryDate",
+ type: "uint256",
+ },
+ {
+ indexed: false,
+ internalType: "string",
+ name: "usageScope",
+ type: "string",
+ },
],
name: "TokenMinted",
type: "event",
@@ -373,6 +418,52 @@ const deployedContracts = {
stateMutability: "view",
type: "function",
},
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "owner",
+ type: "address",
+ },
+ ],
+ name: "listVouchers",
+ outputs: [
+ {
+ components: [
+ {
+ internalType: "string",
+ name: "title",
+ type: "string",
+ },
+ {
+ internalType: "uint256",
+ name: "issueDate",
+ type: "uint256",
+ },
+ {
+ internalType: "uint256",
+ name: "amount",
+ type: "uint256",
+ },
+ {
+ internalType: "uint256",
+ name: "expiryDate",
+ type: "uint256",
+ },
+ {
+ internalType: "string",
+ name: "usageScope",
+ type: "string",
+ },
+ ],
+ internalType: "struct ExpirableERC721.Voucher[]",
+ name: "",
+ type: "tuple[]",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
{
inputs: [
{
@@ -386,9 +477,29 @@ const deployedContracts = {
type: "uint256",
},
{
- internalType: "uint256[]",
- name: "expirationTimes",
- type: "uint256[]",
+ internalType: "string",
+ name: "title",
+ type: "string",
+ },
+ {
+ internalType: "uint256",
+ name: "issueDate",
+ type: "uint256",
+ },
+ {
+ internalType: "uint256",
+ name: "amount",
+ type: "uint256",
+ },
+ {
+ internalType: "uint256",
+ name: "expiryDate",
+ type: "uint256",
+ },
+ {
+ internalType: "string",
+ name: "usageScope",
+ type: "string",
},
],
name: "mintBatch",
@@ -549,6 +660,25 @@ const deployedContracts = {
stateMutability: "view",
type: "function",
},
+ {
+ inputs: [
+ {
+ internalType: "uint256",
+ name: "index",
+ type: "uint256",
+ },
+ ],
+ name: "tokenByIndex",
+ outputs: [
+ {
+ internalType: "uint256",
+ name: "",
+ type: "uint256",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
{
inputs: [
{
@@ -568,6 +698,30 @@ const deployedContracts = {
stateMutability: "view",
type: "function",
},
+ {
+ inputs: [
+ {
+ internalType: "address",
+ name: "owner",
+ type: "address",
+ },
+ {
+ internalType: "uint256",
+ name: "index",
+ type: "uint256",
+ },
+ ],
+ name: "tokenOfOwnerByIndex",
+ outputs: [
+ {
+ internalType: "uint256",
+ name: "",
+ type: "uint256",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
{
inputs: [
{
@@ -587,6 +741,19 @@ const deployedContracts = {
stateMutability: "view",
type: "function",
},
+ {
+ inputs: [],
+ name: "totalSupply",
+ outputs: [
+ {
+ internalType: "uint256",
+ name: "",
+ type: "uint256",
+ },
+ ],
+ stateMutability: "view",
+ type: "function",
+ },
{
inputs: [
{
@@ -625,18 +792,35 @@ const deployedContracts = {
},
],
inheritedFunctions: {
- approve: "@openzeppelin/contracts/token/ERC721/ERC721.sol",
- balanceOf: "@openzeppelin/contracts/token/ERC721/ERC721.sol",
- getApproved: "@openzeppelin/contracts/token/ERC721/ERC721.sol",
- isApprovedForAll: "@openzeppelin/contracts/token/ERC721/ERC721.sol",
- name: "@openzeppelin/contracts/token/ERC721/ERC721.sol",
- ownerOf: "@openzeppelin/contracts/token/ERC721/ERC721.sol",
- safeTransferFrom: "@openzeppelin/contracts/token/ERC721/ERC721.sol",
- setApprovalForAll: "@openzeppelin/contracts/token/ERC721/ERC721.sol",
- supportsInterface: "@openzeppelin/contracts/token/ERC721/ERC721.sol",
- symbol: "@openzeppelin/contracts/token/ERC721/ERC721.sol",
- tokenURI: "@openzeppelin/contracts/token/ERC721/ERC721.sol",
- transferFrom: "@openzeppelin/contracts/token/ERC721/ERC721.sol",
+ approve:
+ "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol",
+ balanceOf:
+ "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol",
+ getApproved:
+ "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol",
+ isApprovedForAll:
+ "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol",
+ name: "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol",
+ ownerOf:
+ "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol",
+ safeTransferFrom:
+ "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol",
+ setApprovalForAll:
+ "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol",
+ supportsInterface:
+ "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol",
+ symbol:
+ "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol",
+ tokenByIndex:
+ "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol",
+ tokenOfOwnerByIndex:
+ "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol",
+ tokenURI:
+ "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol",
+ totalSupply:
+ "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol",
+ transferFrom:
+ "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol",
owner: "@openzeppelin/contracts/access/Ownable.sol",
renounceOwnership: "@openzeppelin/contracts/access/Ownable.sol",
transferOwnership: "@openzeppelin/contracts/access/Ownable.sol",