-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathOnChainJournal.sol
More file actions
159 lines (142 loc) · 6.87 KB
/
Copy pathOnChainJournal.sol
File metadata and controls
159 lines (142 loc) · 6.87 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
157
158
159
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/Base64.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
/**
* @title OnChainJournal
* @author Gemini
* @notice A smart contract for minting on-chain journal entries as NFTs.
* This is the final version, ready for deployment. The SVG image has a
* gradient border with a solid color background.
*/
contract OnChainJournal is ERC721 {
using Counters for Counters.Counter;
using Strings for uint256;
Counters.Counter private _tokenIdCounter;
struct JournalEntry {
string text;
string mood;
uint256 timestamp;
address owner;
}
mapping(uint256 => JournalEntry) public journalEntries;
constructor() ERC721("On-Chain Journal", "JOURNAL") {}
function _escapeString(string memory _str) internal pure returns (string memory) {
bytes memory strBytes = bytes(_str);
// FIX: Increased allocation from 5 to 6 to prevent buffer overflow from '"' or '''
bytes memory result = new bytes(strBytes.length * 6);
uint resultIndex = 0;
for (uint i = 0; i < strBytes.length; i++) {
if (strBytes[i] == '&') {
result[resultIndex++] = '&';
result[resultIndex++] = 'a';
result[resultIndex++] = 'm';
result[resultIndex++] = 'p';
result[resultIndex++] = ';';
} else if (strBytes[i] == '<') {
result[resultIndex++] = '&';
result[resultIndex++] = 'l';
result[resultIndex++] = 't';
result[resultIndex++] = ';';
} else if (strBytes[i] == '>') {
result[resultIndex++] = '&';
result[resultIndex++] = 'g';
result[resultIndex++] = 't';
result[resultIndex++] = ';';
} else if (strBytes[i] == '"') {
result[resultIndex++] = '&';
result[resultIndex++] = 'q';
result[resultIndex++] = 'u';
result[resultIndex++] = 'o';
result[resultIndex++] = 't';
result[resultIndex++] = ';';
// FIX: Added escaping for single quotes for robustness
} else if (strBytes[i] == '\'') {
result[resultIndex++] = '&';
result[resultIndex++] = 'a';
result[resultIndex++] = 'p';
result[resultIndex++] = 'o';
result[resultIndex++] = 's';
result[resultIndex++] = ';';
} else {
result[resultIndex++] = strBytes[i];
}
}
bytes memory finalResult = new bytes(resultIndex);
for(uint i = 0; i < resultIndex; i++) {
finalResult[i] = result[i];
}
return string(finalResult);
}
function mintEntry(string memory _text, string memory _mood) public {
// FIX: Added input validation to prevent Gas Griefing / DoS attacks
require(bytes(_text).length <= 400, "OnChainJournal: Text exceeds 400 bytes.");
require(bytes(_mood).length <= 64, "OnChainJournal: Mood exceeds 64 bytes.");
uint256 newItemId = _tokenIdCounter.current();
_safeMint(msg.sender, newItemId);
journalEntries[newItemId] = JournalEntry({
text: _text,
mood: _mood,
timestamp: block.timestamp,
owner: msg.sender
});
_tokenIdCounter.increment();
}
function tokenURI(uint256 tokenId) public view override returns (string memory) {
require(_ownerOf(tokenId) != address(0), "URI query for nonexistent token");
JournalEntry memory entry = journalEntries[tokenId];
string memory svg = generateSVG(entry);
string memory encodedSvg = Base64.encode(bytes(svg));
string memory json = string(
abi.encodePacked(
'{"name": "Journal Entry #', tokenId.toString(), '",',
'"description": "', string(abi.encodePacked("A personal journal entry minted at timestamp ", entry.timestamp.toString(), ".")), '",',
'"attributes": [',
'{"trait_type": "Mood", "value": "', _escapeString(entry.mood), '"},',
'{"trait_type": "Timestamp", "value": "', entry.timestamp.toString(), '"},',
'{"trait_type": "Text", "value": "', _escapeString(entry.text), '"}',
'],',
'"image": "data:image/svg+xml;base64,', encodedSvg, '"',
'}'
)
);
return string(
abi.encodePacked("data:application/json;base64,", Base64.encode(bytes(json)))
);
}
/**
* @notice A public function to generate and preview SVG code without minting.
* @dev This function now includes the final gradient border design.
* @param entry The JournalEntry struct containing the data to preview.
* @return A string containing the complete SVG code.
*/
function generateSVG(JournalEntry memory entry) public pure returns (string memory) {
string memory escapedText = _escapeString(entry.text);
string memory escapedMood = _escapeString(entry.mood);
return string(abi.encodePacked(
'<svg width="500" height="500" xmlns="http://www.w3.org/2000/svg">',
'<defs>',
'<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">',
'<stop offset="0%" style="stop-color:#f25d00;stop-opacity:1" />',
'<stop offset="100%" style="stop-color:#c026d3;stop-opacity:1" />',
'</linearGradient>',
'</defs>',
'',
'<rect width="100%" height="100%" rx="20" ry="20" fill="url(#grad)"/>',
'',
'<rect x="8" y="8" width="484" height="484" rx="15" ry="15" fill="#f25d00"/>',
'',
'<text x="450" y="90" font-family="sans-serif" font-size="70" text-anchor="end" fill="white">', escapedMood, '</text>',
'<text x="50" y="75" font-family="monospace" font-size="20" fill="white" fill-opacity="0.8">Timestamp: ', entry.timestamp.toString(), '</text>',
'<foreignObject x="50" y="120" width="400" height="280">',
'<div xmlns="http://www.w3.org/1999/xhtml" style="color: white; font-family: sans-serif; font-size: 22px; word-wrap: break-word; line-height: 1.5;">',
escapedText,
'</div>',
'</foreignObject>',
'<text x="50" y="450" font-family="monospace" font-size="16" fill="white" fill-opacity="0.5">On-Chain Journal</text>',
'</svg>'
));
}
}