Repository navigation
Expand file tree
/
Copy pathblockchain.js
More file actions
156 lines (130 loc) · 5.89 KB
/
Copy pathblockchain.js
File metadata and controls
156 lines (130 loc) · 5.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import { ethers } from "ethers";
// ─── ABI (paste generated ABI here after compilation) ───────────────────────
export const CONTRACT_ABI = [
"function sendMessage(address receiver, bytes32 messageHash) external returns (uint256)",
"function getInbox() external view returns (tuple(address sender, address receiver, bytes32 messageHash, uint256 timestamp, uint256 messageId, bool exists)[])",
"function getSentMessages() external view returns (tuple(address sender, address receiver, bytes32 messageHash, uint256 timestamp, uint256 messageId, bool exists)[])",
"function getMessage(uint256 messageId) external view returns (tuple(address sender, address receiver, bytes32 messageHash, uint256 timestamp, uint256 messageId, bool exists))",
"function verifyMessage(uint256 messageId, string calldata plaintextMessage) external returns (bool)",
"function hashMessage(string calldata message) external pure returns (bytes32)",
"function totalMessages() external view returns (uint256)",
"event MessageSent(uint256 indexed messageId, address indexed sender, address indexed receiver, bytes32 messageHash, uint256 timestamp)",
"event MessageVerified(uint256 indexed messageId, address verifier, bool isValid, uint256 timestamp)",
];
// ─── CONTRACT ADDRESS (update after deployment) ──────────────────────────────
// Run: npx hardhat run scripts/deploy.js --network hardhat
// Then paste the deployed address here:
export const CONTRACT_ADDRESS = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
// ─── NETWORK CONFIG ──────────────────────────────────────────────────────────
export const EXPECTED_CHAIN_ID = 31337; // Hardhat / Ganache local
// ─── WALLET UTILS ────────────────────────────────────────────────────────────
/**
* Connect MetaMask and return { provider, signer, address }
*/
export async function connectWallet() {
if (!window.ethereum) {
throw new Error("MetaMask not detected. Please install MetaMask.");
}
// Request accounts
const accounts = await window.ethereum.request({ method: "eth_requestAccounts" });
const provider = new ethers.BrowserProvider(window.ethereum);
const network = await provider.getNetwork();
if (Number(network.chainId) !== EXPECTED_CHAIN_ID) {
throw new Error(
`Wrong network! Please switch MetaMask to Localhost (Chain ID: ${EXPECTED_CHAIN_ID}).\n` +
`You are currently on chain ID: ${network.chainId}`
);
}
const signer = await provider.getSigner();
const address = await signer.getAddress();
return { provider, signer, address };
}
/**
* Get a contract instance (read-only or with signer)
*/
export function getContract(signerOrProvider) {
return new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, signerOrProvider);
}
// ─── MESSAGE UTILS ───────────────────────────────────────────────────────────
/**
* Hash a plaintext message using keccak256 (same as Solidity).
* This is done CLIENT-SIDE before sending to the blockchain.
*/
export function hashMessage(plaintext) {
return ethers.keccak256(ethers.toUtf8Bytes(plaintext));
}
/**
* Send a message — hashes it and calls the smart contract.
*/
export async function sendMessage(signer, receiverAddress, plaintextMessage) {
if (!ethers.isAddress(receiverAddress)) {
throw new Error("Invalid Ethereum address.");
}
if (!plaintextMessage.trim()) {
throw new Error("Message cannot be empty.");
}
const contract = getContract(signer);
const messageHash = hashMessage(plaintextMessage);
const tx = await contract.sendMessage(receiverAddress, messageHash);
const receipt = await tx.wait();
// Extract message ID from event logs
const event = receipt.logs
.map((log) => {
try { return contract.interface.parseLog(log); } catch { return null; }
})
.find((e) => e && e.name === "MessageSent");
const messageId = event ? Number(event.args.messageId) : null;
return {
transactionHash: receipt.hash,
blockNumber: receipt.blockNumber,
gasUsed: receipt.gasUsed.toString(),
messageId,
messageHash,
};
}
/**
* Fetch inbox messages for the connected wallet.
*/
export async function getInbox(signer) {
const contract = getContract(signer);
const raw = await contract.getInbox();
return raw.map(formatMessage);
}
/**
* Fetch sent messages for the connected wallet.
*/
export async function getSentMessages(signer) {
const contract = getContract(signer);
const raw = await contract.getSentMessages();
return raw.map(formatMessage);
}
/**
* Verify a message's authenticity on-chain.
*/
export async function verifyMessage(signer, messageId, plaintextMessage) {
const contract = getContract(signer);
// Use staticCall to read result without spending gas
const isValid = await contract.verifyMessage.staticCall(
messageId,
plaintextMessage
);
return isValid;
}
// ─── HELPERS ─────────────────────────────────────────────────────────────────
function formatMessage(raw) {
return {
sender: raw.sender,
receiver: raw.receiver,
messageHash: raw.messageHash,
timestamp: Number(raw.timestamp),
messageId: Number(raw.messageId),
date: new Date(Number(raw.timestamp) * 1000).toLocaleString(),
};
}
export function shortenAddress(addr) {
if (!addr) return "";
return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
}
export function formatTimestamp(ts) {
return new Date(ts * 1000).toLocaleString();
}