File-Timestamp-App
mkdir File-Timestamp-App
cd File-Timestamp-App
npmの初期化
$ npm init -y
{
"name": "peachheart",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
プロジェクトを初期化するコマンド
- カレントディレクトリにpackage.jsonを自動生成
-
npm init
で聞かれる質問をスキップしてデフォルトを設定
Hardhatのインストール
npm install --save-dev hardhat
-
npm install
npmを使ってhardhatをインストール -
—save-dev
インストールするパッケージを開発依存関係として指定する- hardhatはpackage.jsonのdevDependenciesセクションに追加される
- 開発依存関係 : 開発時のみ必要で本番環境では必要ではないということ
- hardhatはスマートコントラクトの開発やテスト,デプロイなどの開発プロセスのサポートのために使う
- npm5以降では—saveオプションがデフォルトになっていて
npm install
では自動的に dependenciesセクションに追加されるが開発依存関係を明示的に指定するためには—save-dev
オブションをつけないといけない
Hardhatプロジェクトの初期化
以下のコマンドを実行するとCLIインターフェースが起動してプロジェクトのオプションを選択できる。
npx hardhat init
888 888 888 888 888
888 888 888 888 888
888 888 888 888 888
8888888888 8888b. 888d888 .d88888 88888b. 8888b. 888888
888 888 "88b 888P" d88" 888 888 "88b "88b 888
888 888 .d888888 888 888 888 888 888 .d888888 888
888 888 888 888 888 Y88b 888 888 888 888 888 Y88b.
888 888 "Y888888 888 "Y88888 888 888 "Y888888 "Y888
👷 Welcome to Hardhat v2.22.17 👷
✔ What do you want to do? · Create a TypeScript project
✔ Hardhat project root: デフォでEnter
✔ Do you want to add a .gitignore? (Y/n) · y
✔ Do you want to install this sample project's dependencies with npm (@nomicfoundation/hardhat-toolbox)? (Y/n) · y
...
✨ Project created ✨
See the README.md file for some example tasks you can run
Give Hardhat a star on Github if you're enjoying it! ⭐️✨
https://github.com/NomicFoundation/hardhat
選択したオプションに基づいてhardhatが必要なファイルとフォルダ構造を生成する
テストの実行
デフォルトであるLock.solのテストを実行してみましょう
$ npx hardhat test
Compiled 1 Solidity file successfully (evm target: paris).
Lock
Deployment
✔ Should set the right unlockTime (860ms)
✔ Should set the right owner
✔ Should receive and store the funds to lock
✔ Should fail if the unlockTime is not in the future
Withdrawals
Validations
✔ Should revert with the right error if called too soon
✔ Should revert with the right error if called from another account
✔ Shouldn't fail if the unlockTime has arrived and the owner calls it
Events
✔ Should emit an event on withdrawals
Transfers
✔ Should transfer the funds to the owner
9 passing (888ms)
コントラクトの作成
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "hardhat/console.sol";
contract FileTimestamp {
mapping(uint256 => uint256) public hashToTimestamp;
constructor() {
console.log("Deploying a contract");
}
function setTimestamp(uint256 hash, uint256 timestamp) public {
require(hashToTimestamp[hash] == 0, "Timestamp already set");
hashToTimestamp[hash] = timestamp;
}
function getTimestamp(uint256 hash) public view returns (uint256) {
require(hashToTimestamp[hash] != 0, "Timestamp not set");
return hashToTimestamp[hash];
}
}
実行してみる
Scripts/run.tsを作成し実行するスクリプトを作成する
import { ethers } from "hardhat";
const main = async () => {
const fileTimestampContract = await ethers.deployContract("FileTimestamp");
await fileTimestampContract.waitForDeployment();
console.log("FileTimestamp deployed to:", fileTimestampContract.target);
}
const runMain = async () => {
try {
await main();
process.exit(0);
} catch (error) {
console.error(error);
process.exit(1);
}
};
runMain();
package.json
を書き換える
...
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"run:script" : "npx hardhat run script/run.ts"
},
...
npm run run:script
> file-timestamp-app@1.0.0 run:script
> npx hardhat run scripts/run.ts
Deploying a contract
FileTimestamp deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
成功しました
run.ts
のmainを以下のように書き換えてみます
const main = async () => {
const fileTimestampContract = await ethers.deployContract("FileTimestamp");
await fileTimestampContract.waitForDeployment();
console.log("FileTimestamp deployed to:", fileTimestampContract.target);
const now_timestamp = Date.now();
await fileTimestampContract.setTimestamp(0, now_timestamp);
const timestamp = await fileTimestampContract.getTimestamp(0);
if (timestamp != BigInt(now_timestamp)) {
throw new Error("Timestamps do not match");
} else {
console.log("Timestamps match");
}
}
$ npm run run:script
> file-timestamp-app@1.0.0 run:script
> npx hardhat run scripts/run.ts
Deploying a contract
FileTimestamp deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Timestamps match
これでハッシュ値をキーにしてタイムスタンプを格納できることが確認できた
ローカル環境にデプロイ
package.jsonに以下を追加する
...
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start":"npx hardhat node",
"run:script" : "npx hardhat run scripts/run.ts"
},
...
そして以下のコマンドを実行する
npm start
このように出力されました
> file-timestamp-app@1.0.0 start
> npx hardhat node
Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/
Accounts
========
WARNING: These accounts, and their private keys, are publicly known.
Any funds sent to them on Mainnet or any other live network WILL BE LOST.
Account #0: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (10000 ETH)
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
Account #1: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 (10000 ETH)
Private Key: 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
Account #2: 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC (10000 ETH)
Private Key: 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
Account #3: 0x90F79bf6EB2c4f870365E785982E1f101E93b906 (10000 ETH)
Private Key: 0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6
Account #4: 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 (10000 ETH)
Private Key: 0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a
Account #5: 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc (10000 ETH)
Private Key: 0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba
Account #6: 0x976EA74026E726554dB657fA54763abd0C3a0aa9 (10000 ETH)
Private Key: 0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e
Account #7: 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955 (10000 ETH)
Private Key: 0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356
Account #8: 0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f (10000 ETH)
Private Key: 0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97
Account #9: 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720 (10000 ETH)
Private Key: 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6
Account #10: 0xBcd4042DE499D14e55001CcbB24a551F3b954096 (10000 ETH)
Private Key: 0xf214f2b2cd398c806f84e317254e0f0b801d0643303237d97a22a48e01628897
Account #11: 0x71bE63f3384f5fb98995898A86B02Fb2426c5788 (10000 ETH)
Private Key: 0x701b615bbdfb9de65240bc28bd21bbc0d996645a3dd57e7b12bc2bdf6f192c82
Account #12: 0xFABB0ac9d68B0B445fB7357272Ff202C5651694a (10000 ETH)
Private Key: 0xa267530f49f8280200edf313ee7af6b827f2a8bce2897751d06a843f644967b1
Account #13: 0x1CBd3b2770909D4e10f157cABC84C7264073C9Ec (10000 ETH)
Private Key: 0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd
Account #14: 0xdF3e18d64BC6A983f673Ab319CCaE4f1a57C7097 (10000 ETH)
Private Key: 0xc526ee95bf44d8fc405a158bb884d9d1238d99f0612e9f33d006bb0789009aaa
Account #15: 0xcd3B766CCDd6AE721141F452C550Ca635964ce71 (10000 ETH)
Private Key: 0x8166f546bab6da521a8369cab06c5d2b9e46670292d85c875ee9ec20e84ffb61
Account #16: 0x2546BcD3c84621e976D8185a91A922aE77ECEc30 (10000 ETH)
Private Key: 0xea6c44ac03bff858b476bba40716402b03e41b8e97e276d1baec7c37d42484a0
Account #17: 0xbDA5747bFD65F08deb54cb465eB87D40e51B197E (10000 ETH)
Private Key: 0x689af8efa8c651a91ad287602527f3af2fe9f6501a7ac4b061667b5a93e037fd
Account #18: 0xdD2FD4581271e230360230F9337D5c0430Bf44C0 (10000 ETH)
Private Key: 0xde9be858da4a475276426320d5e9262ecfc3ba460bfac56360bfa6c4c28b4ee0
Account #19: 0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199 (10000 ETH)
Private Key: 0xdf57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e
WARNING: These accounts, and their private keys, are publicly known.
Any funds sent to them on Mainnet or any other live network WILL BE LOST.
eth_blockNumber (2)
eth_call
WARNING: Calling an account which is not a contract
From: 0x491ecb84868b5233168a826d6755c2193f0332f3
To: 0xf02d6e666e309e75108f4676d3b99af52678d766
このコマンドを実行することによって,模擬的なEthereumネットワークをローカルに起動することができます.
このネットワークにスマートコントラクトをデプロイしていきます.擬似的な環境なのでガス代がかかる心配はありせん
ネットワークを起動したターミナルは消さないでいてください.
deploy.ts
の作成
scriptsにdeploy.tsを作成してデプロイを行うためのプログラムを書いていきます.
import { ethers } from "hardhat";
const main = async () => {
const [deployer] = await ethers.getSigners();
const accountBalance = await deployer.provider.getBalance(deployer.address);
console.log("Deploying contracts with the account:", deployer.address);
console.log("Deployer account balance:", accountBalance.toString());
const fileTimestampContract = await ethers.deployContract("FileTimestamp");
await fileTimestampContract.waitForDeployment();
console.log("FileTimestamp deployed to:", fileTimestampContract.target);
console.log("Deployer account balance:", accountBalance.toString());
const now_timestamp = Date.now();
const setTx = await fileTimestampContract.setTimestamp(0, now_timestamp);
await setTx.wait();
const timestamp = await fileTimestampContract.getTimestamp(0);
if (timestamp != BigInt(now_timestamp)) {
throw new Error("Timestamps do not match");
} else {
console.log("Timestamps match");
}
}
const runMain = async () => {
try {
await main();
process.exit(0);
} catch (error) {
console.error(error);
process.exit(1);
}
};
runMain();
内容は run.ts
とほぼ一緒です
デプロイする
package.json
にデプロイ用のコマンドを書き加えます
...package.json
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start":"npx hardhat node",
"run:script" : "npx hardhat run scripts/run.ts",
"deploy:localhost": "npx hardhat run scripts/deploy.ts --network localhost",
},
...
続いてこのコマンドを打ちます
npm run deploy:localhost
> file-timestamp-app@1.0.0 deploy:localhost
> npx hardhat run scripts/deploy.ts --network localhost
Deploying contracts with the account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Deployer account balance: 10000000000000000000000
FileTimestamp deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Deployer account balance: 10000000000000000000000
Timestamps match
eth_blockNumber (2)
eth_call
WARNING: Calling an account which is not a contract
From: 0x491ecb84868b5233168a826d6755c2193f0332f3
To: 0xf02d6e666e309e75108f4676d3b99af52678d766
eth_accounts
hardhat_metadata (20)
eth_getBalance
eth_accounts
hardhat_metadata (20)
eth_blockNumber
eth_getBlockByNumber
eth_feeHistory
eth_maxPriorityFeePerGas
eth_sendTransaction
Contract deployment: FileTimestamp
Contract address: 0x5fbdb2315678afecb367f032d93f642f64180aa3
Transaction: 0xf2746fe6736d6b90b627c5bfbe46589acd3c368d33a64443b4042966fc061a41
From: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
Value: 0 ETH
Gas used: 285411 of 30000000
Block #1: 0xe908341ae5451cdaee8c8fca04aecf63831505216f7047072d410d4df4ffb38b
console.log:
Deploying a contract
eth_getTransactionByHash
eth_getTransactionReceipt
eth_blockNumber (2)
eth_feeHistory
eth_sendTransaction
Contract call: FileTimestamp#setTimestamp
Transaction: 0x903fb61eefa30ce9cb2c76a5077505b574c5b14d059caa180f5c42681960d07a
From: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To: 0x5fbdb2315678afecb367f032d93f642f64180aa3
Value: 0 ETH
Gas used: 44286 of 30000000
Block #2: 0x5530d3c1211cb6928e33b17c40961de14728b4512a7179624c6ad897110fdf2d
eth_getTransactionByHash
eth_call
Contract call: FileTimestamp#getTimestamp
From: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
To: 0x5fbdb2315678afecb367f032d93f642f64180aa3
テストネットにデプロイ
実際のプロックチェーンにコントラクトをデプロイする準備をします.
MetaMaskに登録する
MetaMaskをブラウザの拡張機能に追加します.
https://metamask.io/download/
MetaMaskはイーサリアムなどのブロックチェーンに対応した仮想通貨ウォレットです.拡張機能版の他にモバイル版もあります.
イーサリアムやトークンを管理する機能があり,今回はコントラクトをデプロイしたりコントラクトを利用するために使用します.
Alchemyに登録,ネットワークを作成
Alchemyはブロックチェーン開発者向けのプラットフォームで,DAppの開発を支援するインフラを提供しています.
アカウントを作成してください.
https://www.alchemy.com/
Alchemyでプロジェクトを作成します.
- Alchemyのアカウントを作成,サインイン
- 「Apps」ページから「Create new app」
- 「1. Create new app」では名前と目的を適当に入力
- 「2. Choose chains」は Ethereumを選択してNext
- 「3. Activate default services」はデフォルトでNext
- NetworkをSepoliaにします.
- Network URLをあとで使います
今回はSepoliaというEthereumのテストネットにデプロイします.
テストネットは定期的にリセットされるので仮のデプロイ先だと思ってください.
Sepoliaのfaucetを入手する
Sepoliaテストネットで使う擬似的なETHを入手します.「Sepolia faucet」などで検索すれば少額のETHを配布しているサービスがたくさんあります.
今回はhttps://cloud.google.com/application/web3/faucet/ethereum/sepoliaを使います
MetaMaskに表示されているアドレスを入力してfaucetを入手します.
API・秘密鍵の設定
.env
の作成
.gitignore
ファイルに.env
が設定されていることを確認して.env
を作成します.
touch .env
.env
ファイルを扱うdotenvパッケージを追加します.
npm install dotenv
.env
ファイルに設定を記述していきます
ALCHEMY_API_URL = ここにAlchemyのAPI URLを貼り付ける
PRIVATE_SEPOLIA_ACCOUNT_KEY = ここにMetaMaskから取得した秘密鍵を貼り付ける
hardhat.config.ts
ファイルを編集
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
require("dotenv").config();
const config: HardhatUserConfig = {
solidity: "0.8.28",
networks: {
sepolia: {
url: process.env.ALCHEMY_API_URL ?? "",
accounts: [process.env.PRIVATE_SEPOLIA_ACCOUNT_KEY ?? ""],
}
}
};
export default config;
package.jsonを編集
"scripts": {
"run:script": "npx hardhat run scripts/run.ts",
"test": "npx hardhat test",
"deploy": "npx hardhat run scripts/deploy.ts --network sepolia",
"deploy:localhost": "npx hardhat run scripts/deploy.ts --network localhost",
"start": "npx hardhat node"
},
デプロイします
npm run deploy
出力されたコントラクトアドレスはあとで使うのでメモしておいてください
https://sepolia.etherscan.io/
でアドレスを検索しトランザクションを確認できたら成功です
フロントエンドの作成
Next.jsの環境構築
Next.jsのプロジェクトを作成します.ターミナルで以下のコマンドを実行します
npx create-next-app@latest
Need to install the following packages:
create-next-app@15.1.4
Ok to proceed? (y) y
✔ What is your project named? … front
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like your code inside a `src/` directory? … Yes
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to use Turbopack for `next dev`? … Yes
✔ Would you like to customize the import alias (`@/*` by default)? … No
Creating a new Next.js app in /Users/kubotadaichi/Dev/Solidity/File-Timestamp-App/front.
Using npm.
Initializing project with template: app-tw
...
実行が完了するとfrotディレクトリが作成されているので移動してnext.jsプロジェクトを実行していきます.
cd front
npm run dev
> front@0.1.0 dev
> next dev --turbopack
▲ Next.js 15.1.4 (Turbopack)
- Local: http://localhost:3000
- Network: http://131.206.227.151:3000
✓ Starting...
control + c で一旦終了して次の作業に移ります.
ethers.jsのインストール
ethers.jsはethereumブロックチェーンと対話するためのJavaScriptのライブラリです.
ethers.jsはweb3.jsの代替として開発されより効率的で使いやすく設計されています.
npm install ethers
コンポーネントの作成
-
Headerコンポーネント:
アプリケーションのタイトルやナビゲーションリンクを表示します。
-
ConnectWalletコンポーネント:
ユーザーがウォレットを接続するためのボタンを提供します。
-
SetTimestampコンポーネント:
ユーザーがハッシュとタイムスタンプを入力し、setTimestamp関数を呼び出すためのフォームを提供します。
-
GetTimestampコンポーネント:
ユーザーがハッシュを入力し、getTimestamp関数を呼び出してタイムスタンプを取得するためのフォームを提供します。
-
DisplayTimestampコンポーネント:
取得したタイムスタンプを表示します。
-
Footerコンポーネント:
フッター情報を表示します。
mkdir src/components
touch src/components/Header.tx
touch src/components/ConnectWallet.tx
touch src/components/SetTimestamp.tx
touch src/components/GetTimestamp.tx
touch src/components/DisplayTimestamp.tx
touch src/components/Footer.tx
一旦ヘッダーとフッターとpage.tsxを次のように編集します.
import React from 'react';
const Header = () => {
return (
<header>
<h1>File Timestamp DApp</h1>
</header>
);
};
export default Header;
import React from 'react';
const Footer = () => {
return (
<footer>
<p>© 2025 File Timestamp DApp</p>
</footer>
);
};
export default Footer;
"use client";
import React, { useState } from 'react';
import { ethers } from 'ethers';
import Header from '../components/Header';
import ConnectWallet from '../components/ConnectWallet';
import SetTimestamp from '../components/SetTimestamp';
import GetTimestamp from '../components/GetTimestamp';
import DisplayTimestamp from '../components/DisplayTimestamp';
import Footer from '../components/Footer';
const App = () => {
return (
<div>
<Header />
<Footer />
</div>
);
};
export default App;
/front/src/app/globals.css
の中身を消し
npm run dev
Webアプリからスマートコントラクトを呼び出す
まずはウォレットの接続部分を作っていきます
page.tsx
...
const App = () => {
const [currentAccount, setCurrentAccount] = useState<ethers.Signer | null>(null);
return (
<div>
<Header />
<ConnectWallet setCurrentAccount={setCurrentAccount} />
{currentAccount && (
<div>
<p> Current Account: {currentAccount?.toString()}</p>
</div>
)}
<Footer />
</div>
);
};
export default App;
ConnectWallet.tsx
import React from 'react';
import { ethers } from 'ethers';
const ConnectWallet = ({ setCurrentAccount }) => {
const connectWallet = async () => {
try {
const { ethereum } = window;
if (!ethereum) {
alert('Please install MetaMask!');
return;
}
const accounts = await ethereum.request({
method: 'eth_requestAccounts'
}) as string[];
console.log("Connected: "+ accounts[0]);
setCurrentAccount(accounts[0]);
} catch (error) {
console.log(error);
}
};
return (
<button onClick={connectWallet}>Connect Wallet</button>
);
};
export default ConnectWallet;
ボタンを押して接続するとアドレスが表示されます
SetTimestanp
続いてコントラクトにアクセスしてタイムスタンプを登録する部分を作成していきます
コントラクトを取得します
page.tsx
import React, { useState, useEffect } from 'react';
const App = () => {
const [currentAccount, setCurrentAccount] = useState<ethers.Signer | null>(null);
const [contract, setContract] = useState<ethers.Contract | null>(null);
const contractAddress = "";
const contractABI = [];
const getContract = async () => {
if (!currentAccount) return;
const provider = new ethers.providers.Web3Provider(currentAccount.provider);
const signer = provider.getSigner();
const contract = new ethers.Contract(
contractAddress,
contractABI,
signer);
setContract(contract);
};
useEffect(() => {
getContract();
}, [currentAccount]);
...
contractAddress = "0xb79016ed80cdf217cba5ffbdd0a5ad7f4f83505bfb0b88f355867115d00c93ae"
ABIファイルを取得する
artifacts/contracts/FileTimestamp.sol/FileTimestamp.json
を
File-Timestamp-App/front/src/app/utils
にコピーします
cp artifacts/contracts/FileTimestamp.sol/FileTimestamp.json front/src/app/utils
import abi from "./utils/FileTimestamp.json";
const App = () => {
const [currentAccount, setCurrentAccount] = useState<ethers.Signer | null>(null);
const [contract, setContract] = useState<ethers.Contract | null>(null);
const contractAddress = "0xb79016ed80cdf217cba5ffbdd0a5ad7f4f83505bfb0b88f355867115d00c93ae";
const contractABI = abi.abi;
SetTimestamp.tsx
import React, { useState } from 'react';
import { ethers } from 'ethers';
const GetTimestamp = ({ contract }) => {
const [file, setFile] = useState<File | null>(null);
const [timestamp, setTimestamp] = useState<string | null>(null);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
setFile(e.target.files[0]);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!file) return;
const fileBuffer = await file.arrayBuffer();
const hash = ethers.keccak256(new Uint8Array(fileBuffer));
const ts = Date.now();
try {
const timestamp = await contract.setTimestamp(hash, ts);
console.log("Mining transaction...", timestamp);
await timestamp.wait();
console.log("Transaction mined!");
setTimestamp(timestamp.toString());
} catch (error) {
console.log("Error fetching timestamp:", error);
setTimestamp(null);
}
};
return (
<div>
<form onSubmit={handleSubmit}>
<input type="file" onChange={handleFileChange} required />
<button type="submit">Get Timestamp</button>
</form>
{timestamp && <p>Timestamp: {timestamp}</p>}
</div>
);
};
export default GetTimestamp;
Vercelにアプリをホストする
誠意執筆中
参考