From e8dbcc606007f79de29e826419fff2872dabcc42 Mon Sep 17 00:00:00 2001 From: aki-0517 Date: Thu, 5 Dec 2024 15:07:14 +0900 Subject: [PATCH 1/4] add Card component --- packages/nextjs/app/user/list/page.tsx | 25 ++++++++++ packages/nextjs/components/Card.tsx | 65 ++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 packages/nextjs/app/user/list/page.tsx create mode 100644 packages/nextjs/components/Card.tsx diff --git a/packages/nextjs/app/user/list/page.tsx b/packages/nextjs/app/user/list/page.tsx new file mode 100644 index 0000000..0b318d9 --- /dev/null +++ b/packages/nextjs/app/user/list/page.tsx @@ -0,0 +1,25 @@ +import type { NextPage } from "next"; +import { Card } from "~~/components/Card"; + +const UserListPage: NextPage = () => { + 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} + + + + ); +}; From c479ef3f845b3d16ec0b97c9ab9973a63163c567 Mon Sep 17 00:00:00 2001 From: aki-0517 Date: Thu, 5 Dec 2024 15:22:41 +0900 Subject: [PATCH 2/4] add CardList --- packages/nextjs/app/user/list/page.tsx | 52 ++++++++++++++++--------- packages/nextjs/components/CardList.tsx | 41 +++++++++++++++++++ 2 files changed, 74 insertions(+), 19 deletions(-) create mode 100644 packages/nextjs/components/CardList.tsx diff --git a/packages/nextjs/app/user/list/page.tsx b/packages/nextjs/app/user/list/page.tsx index 0b318d9..8336700 100644 --- a/packages/nextjs/app/user/list/page.tsx +++ b/packages/nextjs/app/user/list/page.tsx @@ -1,25 +1,39 @@ import type { NextPage } from "next"; -import { Card } from "~~/components/Card"; +import { CardList } from "~~/components/CardList"; const UserListPage: NextPage = () => { - return ( -
- - - - -
- ); + 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/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) => ( + + ))} +
+ ); +}; From 4f94f399cdc8b1c5498165f5c95b0d225b5564a6 Mon Sep 17 00:00:00 2001 From: aki-0517 Date: Thu, 5 Dec 2024 17:51:13 +0900 Subject: [PATCH 3/4] add contract --- .../hardhat/contracts/ExpirableERC721.sol | 92 ++++++++++++++++--- 1 file changed, 79 insertions(+), 13 deletions(-) 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. From 487e7117f26c356b4192a3258e5df52969ed6f84 Mon Sep 17 00:00:00 2001 From: aki-0517 Date: Thu, 5 Dec 2024 19:32:48 +0900 Subject: [PATCH 4/4] deploy script --- .../nextjs/contracts/deployedContracts.ts | 218 ++++++++++++++++-- 1 file changed, 201 insertions(+), 17 deletions(-) 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",