├── .gitignore ├── README.md ├── _config.yml ├── contracts ├── weiwendappss │ ├── weiwendappss.abi │ ├── weiwendappss.cpp │ └── weiwendappss.wasm └── weiwentokens │ ├── weiwentokens.abi │ ├── weiwentokens.cpp │ ├── weiwentokens.hpp │ └── weiwentokens.wasm └── frontend ├── config-overrides.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json └── src ├── App.css ├── App.js ├── api ├── common.js ├── config.js ├── fetch.js ├── login.js ├── send.js └── service.js ├── components ├── Header.js ├── Posts.js └── User.js ├── index.js ├── store ├── actionCreator.js ├── actionTypes.js ├── index.js └── reducer.js └── util └── Utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | /.vscode 4 | 5 | # dependencies 6 | /frontend/node_modules 7 | /frontend/.pnp 8 | /frontend/.pnp.js 9 | 10 | # testing 11 | /frontend/coverage 12 | 13 | # production 14 | /frontend/build 15 | 16 | # misc 17 | /frontend/.DS_Store 18 | /frontend/.env.local 19 | /frontend/.env.development.local 20 | /frontend/.env.test.local 21 | /frontend/.env.production.local 22 | 23 | /frontend/npm-debug.log* 24 | /frontend/yarn-debug.log* 25 | /frontend/yarn-error.log* 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 微文 - 基于EOS和IPFS的类微博DAPP 2 | 3 | ### 访问地址 https://ipfs.io/ipfs/QmdLbNon8GZD5PScLxChDCbuQfy7ohummLBYTaQzmYYUjG 4 | 5 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /contracts/weiwendappss/weiwendappss.abi: -------------------------------------------------------------------------------- 1 | { 2 | "____comment": "This file was generated with eosio-abigen. DO NOT EDIT ", 3 | "version": "eosio::abi/1.1", 4 | "types": [], 5 | "structs": [ 6 | { 7 | "name": "comment", 8 | "base": "", 9 | "fields": [ 10 | { 11 | "name": "author", 12 | "type": "name" 13 | }, 14 | { 15 | "name": "content", 16 | "type": "string" 17 | }, 18 | { 19 | "name": "post_id", 20 | "type": "uint64" 21 | }, 22 | { 23 | "name": "has_parent", 24 | "type": "bool" 25 | }, 26 | { 27 | "name": "pid", 28 | "type": "uint64" 29 | }, 30 | { 31 | "name": "reply_to", 32 | "type": "name" 33 | } 34 | ] 35 | }, 36 | { 37 | "name": "commenttable", 38 | "base": "", 39 | "fields": [ 40 | { 41 | "name": "id", 42 | "type": "uint64" 43 | }, 44 | { 45 | "name": "post_id", 46 | "type": "uint64" 47 | }, 48 | { 49 | "name": "author", 50 | "type": "name" 51 | }, 52 | { 53 | "name": "time", 54 | "type": "time_point_sec" 55 | }, 56 | { 57 | "name": "content", 58 | "type": "string" 59 | }, 60 | { 61 | "name": "balance", 62 | "type": "asset" 63 | }, 64 | { 65 | "name": "like_num", 66 | "type": "uint32" 67 | }, 68 | { 69 | "name": "has_parent", 70 | "type": "bool" 71 | }, 72 | { 73 | "name": "pid", 74 | "type": "uint64" 75 | }, 76 | { 77 | "name": "reply_to", 78 | "type": "name" 79 | } 80 | ] 81 | }, 82 | { 83 | "name": "follow", 84 | "base": "", 85 | "fields": [ 86 | { 87 | "name": "from", 88 | "type": "name" 89 | }, 90 | { 91 | "name": "to", 92 | "type": "name" 93 | } 94 | ] 95 | }, 96 | { 97 | "name": "followtable", 98 | "base": "", 99 | "fields": [ 100 | { 101 | "name": "id", 102 | "type": "uint64" 103 | }, 104 | { 105 | "name": "from", 106 | "type": "name" 107 | }, 108 | { 109 | "name": "to", 110 | "type": "name" 111 | } 112 | ] 113 | }, 114 | { 115 | "name": "like", 116 | "base": "", 117 | "fields": [ 118 | { 119 | "name": "author", 120 | "type": "name" 121 | }, 122 | { 123 | "name": "type", 124 | "type": "uint32" 125 | }, 126 | { 127 | "name": "type_id", 128 | "type": "uint64" 129 | } 130 | ] 131 | }, 132 | { 133 | "name": "liketable", 134 | "base": "", 135 | "fields": [ 136 | { 137 | "name": "id", 138 | "type": "uint64" 139 | }, 140 | { 141 | "name": "type", 142 | "type": "uint32" 143 | }, 144 | { 145 | "name": "type_id", 146 | "type": "uint64" 147 | }, 148 | { 149 | "name": "author", 150 | "type": "name" 151 | } 152 | ] 153 | }, 154 | { 155 | "name": "post", 156 | "base": "", 157 | "fields": [ 158 | { 159 | "name": "author", 160 | "type": "name" 161 | }, 162 | { 163 | "name": "content", 164 | "type": "string" 165 | }, 166 | { 167 | "name": "attachtype", 168 | "type": "uint32" 169 | }, 170 | { 171 | "name": "attachment", 172 | "type": "string" 173 | } 174 | ] 175 | }, 176 | { 177 | "name": "posttable", 178 | "base": "", 179 | "fields": [ 180 | { 181 | "name": "id", 182 | "type": "uint64" 183 | }, 184 | { 185 | "name": "author", 186 | "type": "name" 187 | }, 188 | { 189 | "name": "content", 190 | "type": "string" 191 | }, 192 | { 193 | "name": "attachtype", 194 | "type": "uint32" 195 | }, 196 | { 197 | "name": "attachment", 198 | "type": "string" 199 | }, 200 | { 201 | "name": "time", 202 | "type": "time_point_sec" 203 | }, 204 | { 205 | "name": "balance", 206 | "type": "asset" 207 | }, 208 | { 209 | "name": "like_num", 210 | "type": "uint32" 211 | }, 212 | { 213 | "name": "comment_num", 214 | "type": "uint32" 215 | } 216 | ] 217 | }, 218 | { 219 | "name": "reward", 220 | "base": "", 221 | "fields": [ 222 | { 223 | "name": "account", 224 | "type": "name" 225 | } 226 | ] 227 | }, 228 | { 229 | "name": "unfollow", 230 | "base": "", 231 | "fields": [ 232 | { 233 | "name": "from", 234 | "type": "name" 235 | }, 236 | { 237 | "name": "id", 238 | "type": "uint64" 239 | } 240 | ] 241 | }, 242 | { 243 | "name": "usertable", 244 | "base": "", 245 | "fields": [ 246 | { 247 | "name": "account", 248 | "type": "name" 249 | }, 250 | { 251 | "name": "balance", 252 | "type": "asset" 253 | }, 254 | { 255 | "name": "follow_num", 256 | "type": "uint32" 257 | }, 258 | { 259 | "name": "fans_num", 260 | "type": "uint32" 261 | }, 262 | { 263 | "name": "post_num", 264 | "type": "uint32" 265 | }, 266 | { 267 | "name": "like_num", 268 | "type": "uint32" 269 | }, 270 | { 271 | "name": "last_reward_time", 272 | "type": "time_point_sec" 273 | }, 274 | { 275 | "name": "last_like_time", 276 | "type": "time_point_sec" 277 | }, 278 | { 279 | "name": "like_times", 280 | "type": "uint32" 281 | } 282 | ] 283 | }, 284 | { 285 | "name": "withdraw", 286 | "base": "", 287 | "fields": [ 288 | { 289 | "name": "account", 290 | "type": "name" 291 | }, 292 | { 293 | "name": "quantity", 294 | "type": "asset" 295 | } 296 | ] 297 | } 298 | ], 299 | "actions": [ 300 | { 301 | "name": "comment", 302 | "type": "comment", 303 | "ricardian_contract": "" 304 | }, 305 | { 306 | "name": "follow", 307 | "type": "follow", 308 | "ricardian_contract": "" 309 | }, 310 | { 311 | "name": "like", 312 | "type": "like", 313 | "ricardian_contract": "" 314 | }, 315 | { 316 | "name": "post", 317 | "type": "post", 318 | "ricardian_contract": "" 319 | }, 320 | { 321 | "name": "reward", 322 | "type": "reward", 323 | "ricardian_contract": "" 324 | }, 325 | { 326 | "name": "unfollow", 327 | "type": "unfollow", 328 | "ricardian_contract": "" 329 | }, 330 | { 331 | "name": "withdraw", 332 | "type": "withdraw", 333 | "ricardian_contract": "" 334 | } 335 | ], 336 | "tables": [ 337 | { 338 | "name": "commenttable", 339 | "type": "commenttable", 340 | "index_type": "i64", 341 | "key_names": [], 342 | "key_types": [] 343 | }, 344 | { 345 | "name": "followtable", 346 | "type": "followtable", 347 | "index_type": "i64", 348 | "key_names": [], 349 | "key_types": [] 350 | }, 351 | { 352 | "name": "liketable", 353 | "type": "liketable", 354 | "index_type": "i64", 355 | "key_names": [], 356 | "key_types": [] 357 | }, 358 | { 359 | "name": "posttable", 360 | "type": "posttable", 361 | "index_type": "i64", 362 | "key_names": [], 363 | "key_types": [] 364 | }, 365 | { 366 | "name": "usertable", 367 | "type": "usertable", 368 | "index_type": "i64", 369 | "key_names": [], 370 | "key_types": [] 371 | } 372 | ], 373 | "ricardian_clauses": [], 374 | "variants": [] 375 | } -------------------------------------------------------------------------------- /contracts/weiwendappss/weiwendappss.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #define TOKEN_SYMBOL symbol("WEI", 4) 7 | #define GENESIS_TIME 1557849600 8 | #define TYPE_POST 1 9 | #define TYPE_COMMENT 2 10 | 11 | using namespace eosio; 12 | 13 | /** 14 | * 随机数生成器 15 | */ 16 | class random_gen { 17 | private: 18 | static random_gen instance; 19 | uint64_t seed = 0; 20 | 21 | public: 22 | static random_gen& get_instance(name account) { 23 | instance.seed = current_time_point().time_since_epoch().count() + account.value; 24 | return instance; 25 | } 26 | 27 | uint32_t range(uint32_t to) { 28 | checksum256 result = sha256((char *)&seed, sizeof(seed)); 29 | auto arr = result.extract_as_byte_array(); 30 | seed = arr[1]; 31 | seed <<= 32; 32 | seed |= arr[0]; 33 | return (uint32_t)(seed % to); 34 | } 35 | 36 | uint32_t range(uint32_t from, uint32_t to) { 37 | if(from == to){ 38 | return from; 39 | }else if(from < to){ 40 | return range(to - from + 1) + from; 41 | } 42 | return range(to); 43 | } 44 | }; 45 | 46 | /** 47 | * 合约类 48 | */ 49 | CONTRACT weiwendappss : public eosio::contract { 50 | 51 | public: 52 | weiwendappss(name self, name first_receiver, datastream ds) : 53 | contract(self, first_receiver, ds), users(self, self.value) {} 54 | 55 | /** 56 | * 每日签到领币 57 | */ 58 | ACTION reward(name account) { 59 | require_auth(account); 60 | 61 | auto itr = users.find(account.value); 62 | 63 | if(itr == users.end()){ 64 | itr = users.emplace(account, [&](auto& user){ 65 | user.account = account; 66 | user.balance = asset(0, TOKEN_SYMBOL); 67 | user.follow_num = 0; 68 | user.fans_num = 0; 69 | user.post_num = 0; 70 | user.like_num = 0; 71 | user.last_reward_time = time_point_sec(); 72 | user.last_like_time = time_point_sec(); 73 | user.like_times = 0; 74 | }); 75 | } 76 | 77 | if(!is_today(itr->last_reward_time.sec_since_epoch())){ 78 | 79 | auto tokens = asset(get_reward(account), TOKEN_SYMBOL); 80 | issue_token(tokens); 81 | 82 | users.modify(itr, account, [&](auto& user){ 83 | user.last_reward_time = time_point_sec(current_time_point()); 84 | user.balance += tokens; 85 | }); 86 | } 87 | } 88 | 89 | /** 90 | * 发微文 91 | */ 92 | ACTION post(name author, const std::string& content, uint32_t attachtype, const std::string& attachment) { 93 | require_auth(author); 94 | 95 | check_user(author); 96 | if(attachtype != 0){ 97 | check(attachment != "", "attachment can not be empty"); 98 | } 99 | 100 | post_t posts(_self, _self.value); 101 | posts.emplace(author, [&](auto& post){ 102 | post.id = posts.available_primary_key(); 103 | post.author = author; 104 | post.content = content; 105 | post.attachtype = attachtype; 106 | post.attachment = attachment; 107 | post.time = time_point_sec(current_time_point()); 108 | post.balance = asset(0, TOKEN_SYMBOL); 109 | post.like_num = 0; 110 | post.comment_num = 0; 111 | }); 112 | 113 | auto itr = users.find(author.value); 114 | users.modify(itr, same_payer, [&](auto& user){ 115 | user.post_num++; 116 | }); 117 | } 118 | 119 | /** 120 | * 评论 121 | */ 122 | ACTION comment(name author, const std::string& content, uint64_t post_id, 123 | bool has_parent, uint64_t pid, name reply_to) { 124 | 125 | require_auth(author); 126 | 127 | check_user(author); 128 | 129 | post_t posts(_self, _self.value); 130 | auto itr = posts.find(post_id); 131 | check(itr != posts.end(), "post does not exist"); 132 | 133 | comment_t comments(_self, _self.value); 134 | if(has_parent){ 135 | check(comments.find(pid) != comments.end(), "parent comment does not exist"); 136 | } 137 | 138 | comments.emplace(author, [&](auto& comment){ 139 | comment.id = comments.available_primary_key(); 140 | comment.post_id = post_id; 141 | comment.author = author; 142 | comment.time = time_point_sec(current_time_point()); 143 | comment.content = content; 144 | comment.balance = asset(0, TOKEN_SYMBOL); 145 | comment.like_num = 0; 146 | comment.has_parent = has_parent; 147 | comment.pid = pid; 148 | comment.reply_to = reply_to; 149 | }); 150 | 151 | posts.modify(itr, same_payer, [&](auto& post){ 152 | post.comment_num++; 153 | }); 154 | } 155 | 156 | /** 157 | * 点赞 158 | */ 159 | ACTION like(name author, uint32_t type, uint64_t type_id) { 160 | require_auth(author); 161 | 162 | check_user(author); 163 | check(type == TYPE_POST || type == TYPE_COMMENT, "invalid like type"); 164 | check_liked(author, type, type_id); 165 | 166 | if(type == TYPE_POST){ 167 | post_t posts(_self, _self.value); 168 | check(posts.find(type_id) != posts.end(), "post does not exist"); 169 | 170 | update_like_data(author, type_id); 171 | 172 | }else if(type == TYPE_COMMENT){ 173 | comment_t comments(_self, _self.value); 174 | check(comments.find(type_id) != comments.end(), "comment does not exist"); 175 | 176 | update_like_data(author, type_id); 177 | } 178 | 179 | like_t likes(_self, _self.value); 180 | likes.emplace(author, [&](auto& like){ 181 | like.id = likes.available_primary_key(); 182 | like.type = type; 183 | like.type_id = type_id; 184 | like.author = author; 185 | }); 186 | } 187 | 188 | /** 189 | * 关注 190 | */ 191 | ACTION follow(name from, name to) { 192 | require_auth(from); 193 | 194 | check_user(from); 195 | check_user(to); 196 | 197 | check(from != to, "can not follow yourself"); 198 | 199 | follow_t follows(_self, _self.value); 200 | auto secondary = follows.get_index<"byfrom"_n>(); 201 | 202 | for(auto itr = secondary.lower_bound(from.value); itr != secondary.upper_bound(from.value); itr++){ 203 | if(itr->to == to){ 204 | check(false, "already followed"); 205 | } 206 | } 207 | 208 | follows.emplace(from, [&](auto& follow){ 209 | follow.id = follows.available_primary_key(); 210 | follow.from = from; 211 | follow.to = to; 212 | }); 213 | } 214 | 215 | /** 216 | * 取消关注 217 | */ 218 | ACTION unfollow(name from, uint64_t id) { 219 | require_auth(from); 220 | 221 | follow_t follows(_self, _self.value); 222 | auto itr = follows.find(id); 223 | 224 | check(itr != follows.end(), "follow does not exist"); 225 | check(itr->from == from, "invalid follow"); 226 | 227 | follows.erase(itr); 228 | } 229 | 230 | /** 231 | * 提现 232 | */ 233 | ACTION withdraw(name account, asset quantity) { 234 | require_auth(account); 235 | 236 | auto itr = users.find(account.value); 237 | 238 | check(quantity.amount > 0, "withdraw amount must be positive"); 239 | check(itr->balance >= quantity, "overdrawn balance"); 240 | 241 | transfer_token(account, quantity); 242 | users.modify(itr, account, [&](auto& user){ 243 | user.balance -= quantity; 244 | }); 245 | } 246 | 247 | /** 248 | * 充值 249 | */ 250 | [[eosio::on_notify("weiwentokens::transfer")]] 251 | void deposit(name from, name to, asset quantity, std::string memo) { 252 | if(to != _self) return; 253 | 254 | if(quantity.symbol == TOKEN_SYMBOL){ 255 | auto itr = users.find(from.value); 256 | 257 | if(itr != users.end()){ 258 | users.modify(itr, _self, [&](auto& user){ 259 | user.balance += quantity; 260 | }); 261 | } 262 | } 263 | } 264 | 265 | private: 266 | 267 | /** 268 | * 判断指定时间是否是今天 269 | */ 270 | bool is_today(uint32_t last_time){ 271 | return current_time_point().sec_since_epoch() / 86400 == last_time / 86400; 272 | } 273 | 274 | /** 275 | * 检查用户是否存在 276 | */ 277 | void check_user(name account){ 278 | auto itr = users.find(account.value); 279 | check(itr != users.end(), "user does not exist"); 280 | } 281 | 282 | /** 283 | * 计算每日签到领币的数量 284 | */ 285 | uint32_t get_reward(name account){ 286 | uint32_t seconds = current_time_point().sec_since_epoch() - GENESIS_TIME; 287 | if(seconds <= 30*86400){ 288 | return random_gen().get_instance(account).range(10000, 50000) * 10000; 289 | }else if(seconds > 30*86400 && seconds <= 120*86400){ 290 | return random_gen().get_instance(account).range(5000, 20000) * 10000; 291 | }else if(seconds > 120*86400 && seconds <= 360*86400){ 292 | return random_gen().get_instance(account).range(2000, 10000) * 10000; 293 | } 294 | return random_gen().get_instance(account).range(1000, 5000) * 10000; 295 | } 296 | 297 | /** 298 | * 计算每次点赞领币的数量 299 | */ 300 | uint32_t get_like_reward(name account){ 301 | auto itr = users.find(account.value); 302 | auto rnum = random_gen().get_instance(account).range(100, 500); 303 | return itr->balance.amount * rnum * 0.00001; 304 | } 305 | 306 | /** 307 | * 检查是否已经点赞 308 | */ 309 | void check_liked(name author, uint32_t type, uint64_t type_id){ 310 | bool liked = false; 311 | 312 | like_t likes(_self, _self.value); 313 | auto secondary = likes.get_index<"byauthor"_n>(); 314 | 315 | for(auto itr = secondary.lower_bound(author.value); itr != secondary.upper_bound(author.value); itr++){ 316 | if(itr->type == type && itr->type_id == type_id){ 317 | liked = true; 318 | } 319 | } 320 | 321 | if(liked) check(false, "already liked"); 322 | } 323 | 324 | /** 325 | * 更新点赞相关的数据 326 | */ 327 | template 328 | void update_like_data(name liker, uint64_t type_id){ 329 | 330 | T datas(_self, _self.value); 331 | auto itr = datas.find(type_id); 332 | auto liker_itr = users.find(liker.value); 333 | 334 | datas.modify(itr, same_payer, [&](auto& row){ 335 | row.like_num++; 336 | }); 337 | users.modify(users.find(itr->author.value), same_payer, [&](auto& user){ 338 | user.like_num++; 339 | }); 340 | 341 | if(liker != itr->author){ 342 | 343 | bool istoday = is_today(liker_itr->last_like_time.sec_since_epoch()); 344 | 345 | if(!istoday || (istoday && liker_itr->like_times < 10)){ 346 | 347 | auto tokens = asset(get_like_reward(liker), TOKEN_SYMBOL); 348 | 349 | issue_token(tokens); 350 | users.modify(liker_itr, same_payer, [&](auto& user){ 351 | user.balance += tokens; 352 | user.last_like_time = time_point_sec(current_time_point()); 353 | user.like_times = istoday ? (user.like_times + 1) : 1; 354 | }); 355 | 356 | issue_token(tokens); 357 | users.modify(users.find(itr->author.value), same_payer, [&](auto& user){ 358 | user.balance += tokens; 359 | }); 360 | datas.modify(itr, same_payer, [&](auto& row){ 361 | row.balance += tokens; 362 | }); 363 | } 364 | } 365 | } 366 | 367 | /** 368 | * 发行代币 369 | */ 370 | void issue_token(asset quantity){ 371 | action( 372 | permission_level{_self,"active"_n}, 373 | "weiwentokens"_n, 374 | "issue"_n, 375 | std::make_tuple(_self, quantity, std::string("issue token")) 376 | ).send(); 377 | } 378 | 379 | /** 380 | * 转账代币 381 | */ 382 | void transfer_token(name to, asset quantity){ 383 | action( 384 | permission_level{_self,"active"_n}, 385 | "weiwentokens"_n, 386 | "transfer"_n, 387 | std::make_tuple(_self, to, quantity, std::string("transfer token")) 388 | ).send(); 389 | } 390 | 391 | 392 | 393 | //用户表 394 | TABLE usertable { 395 | name account; //EOS账户 396 | asset balance; //代币余额 397 | uint32_t follow_num; //关注数 398 | uint32_t fans_num; //粉丝数 399 | uint32_t post_num; //微文数 400 | uint32_t like_num; //获赞数 401 | time_point_sec last_reward_time;//上次领币时间 402 | time_point_sec last_like_time; //上次点赞时间 403 | uint32_t like_times; //当天已点赞次数 404 | 405 | uint64_t primary_key() const { return account.value; } 406 | }; 407 | 408 | typedef multi_index<"usertable"_n, usertable> user_t; 409 | user_t users; 410 | 411 | //微文表 412 | TABLE posttable { 413 | uint64_t id; //自增id 414 | name author; //作者 415 | std::string content; //内容 416 | uint32_t attachtype; //附件类型 0=无 1=url 2=ipfshash 3=pic 4=video 5=file 417 | std::string attachment; //附件 418 | time_point_sec time; //创建时间 419 | asset balance; //获得代币数 420 | uint32_t like_num; //获得赞数 421 | uint32_t comment_num; //评论数 422 | 423 | uint64_t primary_key() const { return id; } 424 | uint64_t get_secondary_1() const { return author.value; } 425 | }; 426 | 427 | typedef multi_index<"posttable"_n, posttable, 428 | indexed_by<"byauthor"_n, const_mem_fun> 429 | > post_t; 430 | 431 | //评论表 432 | TABLE commenttable { 433 | uint64_t id; //自增id 434 | uint64_t post_id; //微文id 435 | name author; //评论者 436 | time_point_sec time; //创建时间 437 | std::string content; //评论内容 438 | asset balance; //获得代币数 439 | uint32_t like_num; //获得赞数 440 | bool has_parent; //是否有父级评论 441 | uint64_t pid; //父级评论id 442 | name reply_to; //回复 @账户名:xxx 443 | 444 | uint64_t primary_key() const { return id; } 445 | uint64_t get_secondary_1() const { return post_id; } 446 | uint64_t get_secondary_2() const { return author.value; } 447 | }; 448 | 449 | typedef multi_index<"commenttable"_n, commenttable, 450 | indexed_by<"bypost"_n, const_mem_fun>, 451 | indexed_by<"byauthor"_n, const_mem_fun> 452 | > comment_t; 453 | 454 | //点赞表 455 | TABLE liketable { 456 | uint64_t id; //自增id 457 | uint32_t type; //点赞类型 1=微文点赞 2=评论点赞 458 | uint64_t type_id; //微文或评论的id 459 | name author; //点赞者 460 | 461 | uint64_t primary_key() const { return id; } 462 | uint64_t get_secondary_1() const { return type_id; } 463 | uint64_t get_secondary_2() const { return author.value; } 464 | }; 465 | 466 | typedef multi_index<"liketable"_n, liketable, 467 | indexed_by<"bytypeid"_n, const_mem_fun>, 468 | indexed_by<"byauthor"_n, const_mem_fun> 469 | > like_t; 470 | 471 | //关注表 472 | TABLE followtable { 473 | uint64_t id; //自增id 474 | name from; //关注者 475 | name to; //被关注者 476 | 477 | uint64_t primary_key() const { return id; } 478 | uint64_t get_secondary_1() const { return from.value; } 479 | uint64_t get_secondary_2() const { return to.value; } 480 | }; 481 | 482 | typedef multi_index<"followtable"_n, followtable, 483 | indexed_by<"byfrom"_n, const_mem_fun>, 484 | indexed_by<"byto"_n, const_mem_fun> 485 | > follow_t; 486 | 487 | }; -------------------------------------------------------------------------------- /contracts/weiwendappss/weiwendappss.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/songguo6/weiwen-dapp/9ae9cfdc27f77f506461f605901b3921bc95da42/contracts/weiwendappss/weiwendappss.wasm -------------------------------------------------------------------------------- /contracts/weiwentokens/weiwentokens.abi: -------------------------------------------------------------------------------- 1 | { 2 | "____comment": "This file was generated with eosio-abigen. DO NOT EDIT ", 3 | "version": "eosio::abi/1.1", 4 | "types": [], 5 | "structs": [ 6 | { 7 | "name": "account", 8 | "base": "", 9 | "fields": [ 10 | { 11 | "name": "balance", 12 | "type": "asset" 13 | } 14 | ] 15 | }, 16 | { 17 | "name": "close", 18 | "base": "", 19 | "fields": [ 20 | { 21 | "name": "owner", 22 | "type": "name" 23 | }, 24 | { 25 | "name": "symbol", 26 | "type": "symbol" 27 | } 28 | ] 29 | }, 30 | { 31 | "name": "create", 32 | "base": "", 33 | "fields": [ 34 | { 35 | "name": "issuer", 36 | "type": "name" 37 | }, 38 | { 39 | "name": "maximum_supply", 40 | "type": "asset" 41 | } 42 | ] 43 | }, 44 | { 45 | "name": "currency_stats", 46 | "base": "", 47 | "fields": [ 48 | { 49 | "name": "supply", 50 | "type": "asset" 51 | }, 52 | { 53 | "name": "max_supply", 54 | "type": "asset" 55 | }, 56 | { 57 | "name": "issuer", 58 | "type": "name" 59 | } 60 | ] 61 | }, 62 | { 63 | "name": "issue", 64 | "base": "", 65 | "fields": [ 66 | { 67 | "name": "to", 68 | "type": "name" 69 | }, 70 | { 71 | "name": "quantity", 72 | "type": "asset" 73 | }, 74 | { 75 | "name": "memo", 76 | "type": "string" 77 | } 78 | ] 79 | }, 80 | { 81 | "name": "open", 82 | "base": "", 83 | "fields": [ 84 | { 85 | "name": "owner", 86 | "type": "name" 87 | }, 88 | { 89 | "name": "symbol", 90 | "type": "symbol" 91 | }, 92 | { 93 | "name": "ram_payer", 94 | "type": "name" 95 | } 96 | ] 97 | }, 98 | { 99 | "name": "retire", 100 | "base": "", 101 | "fields": [ 102 | { 103 | "name": "quantity", 104 | "type": "asset" 105 | }, 106 | { 107 | "name": "memo", 108 | "type": "string" 109 | } 110 | ] 111 | }, 112 | { 113 | "name": "transfer", 114 | "base": "", 115 | "fields": [ 116 | { 117 | "name": "from", 118 | "type": "name" 119 | }, 120 | { 121 | "name": "to", 122 | "type": "name" 123 | }, 124 | { 125 | "name": "quantity", 126 | "type": "asset" 127 | }, 128 | { 129 | "name": "memo", 130 | "type": "string" 131 | } 132 | ] 133 | } 134 | ], 135 | "actions": [ 136 | { 137 | "name": "close", 138 | "type": "close", 139 | "ricardian_contract": "" 140 | }, 141 | { 142 | "name": "create", 143 | "type": "create", 144 | "ricardian_contract": "" 145 | }, 146 | { 147 | "name": "issue", 148 | "type": "issue", 149 | "ricardian_contract": "" 150 | }, 151 | { 152 | "name": "open", 153 | "type": "open", 154 | "ricardian_contract": "" 155 | }, 156 | { 157 | "name": "retire", 158 | "type": "retire", 159 | "ricardian_contract": "" 160 | }, 161 | { 162 | "name": "transfer", 163 | "type": "transfer", 164 | "ricardian_contract": "" 165 | } 166 | ], 167 | "tables": [ 168 | { 169 | "name": "accounts", 170 | "type": "account", 171 | "index_type": "i64", 172 | "key_names": [], 173 | "key_types": [] 174 | }, 175 | { 176 | "name": "stat", 177 | "type": "currency_stats", 178 | "index_type": "i64", 179 | "key_names": [], 180 | "key_types": [] 181 | } 182 | ], 183 | "ricardian_clauses": [], 184 | "variants": [] 185 | } -------------------------------------------------------------------------------- /contracts/weiwentokens/weiwentokens.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * @copyright defined in eos/LICENSE.txt 4 | */ 5 | 6 | #include "weiwentokens.hpp" 7 | 8 | namespace eosio { 9 | 10 | void weiwentokens::create( name issuer, 11 | asset maximum_supply ) 12 | { 13 | require_auth( _self ); 14 | 15 | auto sym = maximum_supply.symbol; 16 | check( sym.is_valid(), "invalid symbol name" ); 17 | check( maximum_supply.is_valid(), "invalid supply"); 18 | check( maximum_supply.amount > 0, "max-supply must be positive"); 19 | 20 | stats statstable( _self, sym.code().raw() ); 21 | auto existing = statstable.find( sym.code().raw() ); 22 | check( existing == statstable.end(), "token with symbol already exists" ); 23 | 24 | statstable.emplace( _self, [&]( auto& s ) { 25 | s.supply.symbol = maximum_supply.symbol; 26 | s.max_supply = maximum_supply; 27 | s.issuer = issuer; 28 | }); 29 | } 30 | 31 | 32 | void weiwentokens::issue( name to, asset quantity, string memo ) 33 | { 34 | auto sym = quantity.symbol; 35 | check( sym.is_valid(), "invalid symbol name" ); 36 | check( memo.size() <= 256, "memo has more than 256 bytes" ); 37 | 38 | stats statstable( _self, sym.code().raw() ); 39 | auto existing = statstable.find( sym.code().raw() ); 40 | check( existing != statstable.end(), "token with symbol does not exist, create token before issue" ); 41 | const auto& st = *existing; 42 | 43 | require_auth( st.issuer ); 44 | check( quantity.is_valid(), "invalid quantity" ); 45 | check( quantity.amount > 0, "must issue positive quantity" ); 46 | 47 | check( quantity.symbol == st.supply.symbol, "symbol precision mismatch" ); 48 | check( quantity.amount <= st.max_supply.amount - st.supply.amount, "quantity exceeds available supply"); 49 | 50 | statstable.modify( st, same_payer, [&]( auto& s ) { 51 | s.supply += quantity; 52 | }); 53 | 54 | add_balance( st.issuer, quantity, st.issuer ); 55 | 56 | if( to != st.issuer ) { 57 | SEND_INLINE_ACTION( *this, transfer, { {st.issuer, "active"_n} }, 58 | { st.issuer, to, quantity, memo } 59 | ); 60 | } 61 | } 62 | 63 | void weiwentokens::retire( asset quantity, string memo ) 64 | { 65 | auto sym = quantity.symbol; 66 | check( sym.is_valid(), "invalid symbol name" ); 67 | check( memo.size() <= 256, "memo has more than 256 bytes" ); 68 | 69 | stats statstable( _self, sym.code().raw() ); 70 | auto existing = statstable.find( sym.code().raw() ); 71 | check( existing != statstable.end(), "token with symbol does not exist" ); 72 | const auto& st = *existing; 73 | 74 | require_auth( st.issuer ); 75 | check( quantity.is_valid(), "invalid quantity" ); 76 | check( quantity.amount > 0, "must retire positive quantity" ); 77 | 78 | check( quantity.symbol == st.supply.symbol, "symbol precision mismatch" ); 79 | 80 | statstable.modify( st, same_payer, [&]( auto& s ) { 81 | s.supply -= quantity; 82 | }); 83 | 84 | sub_balance( st.issuer, quantity ); 85 | } 86 | 87 | void weiwentokens::transfer( name from, 88 | name to, 89 | asset quantity, 90 | string memo ) 91 | { 92 | check( from != to, "cannot transfer to self" ); 93 | require_auth( from ); 94 | check( is_account( to ), "to account does not exist"); 95 | auto sym = quantity.symbol.code(); 96 | stats statstable( _self, sym.raw() ); 97 | const auto& st = statstable.get( sym.raw() ); 98 | 99 | require_recipient( from ); 100 | require_recipient( to ); 101 | 102 | check( quantity.is_valid(), "invalid quantity" ); 103 | check( quantity.amount > 0, "must transfer positive quantity" ); 104 | check( quantity.symbol == st.supply.symbol, "symbol precision mismatch" ); 105 | check( memo.size() <= 256, "memo has more than 256 bytes" ); 106 | 107 | auto payer = has_auth( to ) ? to : from; 108 | 109 | sub_balance( from, quantity ); 110 | add_balance( to, quantity, payer ); 111 | } 112 | 113 | void weiwentokens::sub_balance( name owner, asset value ) { 114 | accounts from_acnts( _self, owner.value ); 115 | 116 | const auto& from = from_acnts.get( value.symbol.code().raw(), "no balance object found" ); 117 | check( from.balance.amount >= value.amount, "overdrawn balance" ); 118 | 119 | from_acnts.modify( from, owner, [&]( auto& a ) { 120 | a.balance -= value; 121 | }); 122 | } 123 | 124 | void weiwentokens::add_balance( name owner, asset value, name ram_payer ) 125 | { 126 | accounts to_acnts( _self, owner.value ); 127 | auto to = to_acnts.find( value.symbol.code().raw() ); 128 | if( to == to_acnts.end() ) { 129 | to_acnts.emplace( ram_payer, [&]( auto& a ){ 130 | a.balance = value; 131 | }); 132 | } else { 133 | to_acnts.modify( to, same_payer, [&]( auto& a ) { 134 | a.balance += value; 135 | }); 136 | } 137 | } 138 | 139 | void weiwentokens::open( name owner, const symbol& symbol, name ram_payer ) 140 | { 141 | require_auth( ram_payer ); 142 | 143 | auto sym_code_raw = symbol.code().raw(); 144 | 145 | stats statstable( _self, sym_code_raw ); 146 | const auto& st = statstable.get( sym_code_raw, "symbol does not exist" ); 147 | check( st.supply.symbol == symbol, "symbol precision mismatch" ); 148 | 149 | accounts acnts( _self, owner.value ); 150 | auto it = acnts.find( sym_code_raw ); 151 | if( it == acnts.end() ) { 152 | acnts.emplace( ram_payer, [&]( auto& a ){ 153 | a.balance = asset{0, symbol}; 154 | }); 155 | } 156 | } 157 | 158 | void weiwentokens::close( name owner, const symbol& symbol ) 159 | { 160 | require_auth( owner ); 161 | accounts acnts( _self, owner.value ); 162 | auto it = acnts.find( symbol.code().raw() ); 163 | check( it != acnts.end(), "Balance row already deleted or never existed. Action won't have any effect." ); 164 | check( it->balance.amount == 0, "Cannot close because the balance is not zero." ); 165 | acnts.erase( it ); 166 | } 167 | 168 | } /// namespace eosio 169 | -------------------------------------------------------------------------------- /contracts/weiwentokens/weiwentokens.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * @copyright defined in eos/LICENSE.txt 4 | */ 5 | #pragma once 6 | 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | namespace eosiosystem { 13 | class system_contract; 14 | } 15 | 16 | namespace eosio { 17 | 18 | using std::string; 19 | 20 | class [[eosio::contract]] weiwentokens : public contract { 21 | public: 22 | using contract::contract; 23 | 24 | [[eosio::action]] 25 | void create( name issuer, 26 | asset maximum_supply); 27 | 28 | [[eosio::action]] 29 | void issue( name to, asset quantity, string memo ); 30 | 31 | [[eosio::action]] 32 | void retire( asset quantity, string memo ); 33 | 34 | [[eosio::action]] 35 | void transfer( name from, 36 | name to, 37 | asset quantity, 38 | string memo ); 39 | 40 | [[eosio::action]] 41 | void open( name owner, const symbol& symbol, name ram_payer ); 42 | 43 | [[eosio::action]] 44 | void close( name owner, const symbol& symbol ); 45 | 46 | static asset get_supply( name token_contract_account, symbol_code sym_code ) 47 | { 48 | stats statstable( token_contract_account, sym_code.raw() ); 49 | const auto& st = statstable.get( sym_code.raw() ); 50 | return st.supply; 51 | } 52 | 53 | static asset get_balance( name token_contract_account, name owner, symbol_code sym_code ) 54 | { 55 | accounts accountstable( token_contract_account, owner.value ); 56 | const auto& ac = accountstable.get( sym_code.raw() ); 57 | return ac.balance; 58 | } 59 | 60 | private: 61 | struct [[eosio::table]] account { 62 | asset balance; 63 | 64 | uint64_t primary_key()const { return balance.symbol.code().raw(); } 65 | }; 66 | 67 | struct [[eosio::table]] currency_stats { 68 | asset supply; 69 | asset max_supply; 70 | name issuer; 71 | 72 | uint64_t primary_key()const { return supply.symbol.code().raw(); } 73 | }; 74 | 75 | typedef eosio::multi_index< "accounts"_n, account > accounts; 76 | typedef eosio::multi_index< "stat"_n, currency_stats > stats; 77 | 78 | void sub_balance( name owner, asset value ); 79 | void add_balance( name owner, asset value, name ram_payer ); 80 | }; 81 | 82 | } /// namespace eosio -------------------------------------------------------------------------------- /contracts/weiwentokens/weiwentokens.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/songguo6/weiwen-dapp/9ae9cfdc27f77f506461f605901b3921bc95da42/contracts/weiwentokens/weiwentokens.wasm -------------------------------------------------------------------------------- /frontend/config-overrides.js: -------------------------------------------------------------------------------- 1 | const { override, fixBabelImports, addLessLoader } = require('customize-cra'); 2 | 3 | module.exports = override( 4 | fixBabelImports('import', { 5 | libraryName: 'antd', 6 | libraryDirectory: 'es', 7 | style: true, 8 | }), 9 | addLessLoader({ 10 | javascriptEnabled: true, 11 | modifyVars: { 12 | '@primary-color': '#1da1f2', 13 | '@layout-body-background': '#fff', 14 | '@layout-header-background': '#fff', 15 | }, 16 | }), 17 | ); -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "homepage": ".", 4 | "version": "0.1.0", 5 | "private": true, 6 | "dependencies": { 7 | "antd": "^3.18.1", 8 | "babel-plugin-import": "^1.11.2", 9 | "customize-cra": "^0.2.12", 10 | "eosjs": "^20.0.0", 11 | "less": "^3.9.0", 12 | "less-loader": "^5.0.0", 13 | "moment": "^2.24.0", 14 | "qrcode.react": "^0.9.3", 15 | "react": "^16.8.6", 16 | "react-app-rewired": "^2.1.3", 17 | "react-copy-to-clipboard": "^5.0.1", 18 | "react-dom": "^16.8.6", 19 | "react-redux": "^7.0.3", 20 | "react-router-dom": "^5.0.0", 21 | "react-scripts": "3.0.0", 22 | "redux": "^4.0.1", 23 | "redux-thunk": "^2.3.0", 24 | "scatterjs-core": "^2.7.18", 25 | "scatterjs-plugin-eosjs2": "^1.5.0" 26 | }, 27 | "scripts": { 28 | "start": "react-app-rewired start", 29 | "build": "react-app-rewired build", 30 | "test": "react-app-rewired test", 31 | "eject": "react-scripts eject" 32 | }, 33 | "eslintConfig": { 34 | "extends": "react-app" 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/songguo6/weiwen-dapp/9ae9cfdc27f77f506461f605901b3921bc95da42/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React App 23 | 24 | 25 | 26 |
27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | .ant-layout-header { border-bottom: 0.5px solid #e1e1e1; } 2 | .ant-layout-header div { float: left; color: #1da1f2; width: 200px; font-size: 28px; } 3 | .ant-layout-content { text-align: center; } 4 | .ant-layout-footer { text-align: center; } 5 | .ant-list-item { text-align: left; } 6 | .avatar { margin-bottom: 24px; text-align: center; } 7 | .avatar img { width: 104px; height: 104px; margin-bottom: 20px;} 8 | .avatar .name { margin-bottom: 4px; font-weight: 500; font-size: 20px; line-height: 28px; } 9 | .user { font-size: 20px; } 10 | .user .item { margin-top: 20px; } 11 | .user .item span { color: rgba(0, 0, 0, 0.65); } 12 | .login-btn { float:right; margin-top: 15px; } 13 | .sign-btn { float: right; margin: 15px 15px 0 0; } 14 | .edit-btn { float: right; margin: 15px 0 0 15px; } 15 | .modal-label { margin: 23px 0 14px 0; } 16 | .item-content { max-width: 720px; line-height: 22px; } 17 | .item-attach { margin-top: 16px; line-height: 22px; } 18 | .item-extra { margin-top: 16px; color: rgba(0, 0, 0, 0.45); line-height:22px; } 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import { Layout, Row, Col } from 'antd'; 5 | 6 | import { login, logout, checkLogin } from './api/login'; 7 | import { reward } from './api/service'; 8 | 9 | import User from './components/User'; 10 | import Header from './components/Header' 11 | import Posts from './components/Posts'; 12 | 13 | import './App.css'; 14 | 15 | class App extends Component { 16 | 17 | componentDidMount(){ 18 | this.props.checkLogin(); 19 | } 20 | 21 | render() { 22 | const { logged, login, logout, user, reward } = this.props; 23 | const { Content, Footer } = Layout; 24 | 25 | return ( 26 | 27 | 28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | 微文 ©2019 Created by Songguo 41 |
42 | 43 | 44 | ); 45 | } 46 | } 47 | 48 | const mapStateToProps = (state) => { 49 | return { 50 | logged: state.logged, 51 | user : state.user, 52 | } 53 | } 54 | 55 | const mapDispatchToProps = (dispatch) => { 56 | return { 57 | login(){ 58 | dispatch(login); 59 | }, 60 | logout(){ 61 | dispatch(logout); 62 | }, 63 | checkLogin(){ 64 | dispatch(checkLogin); 65 | }, 66 | reward(){ 67 | dispatch(reward); 68 | }, 69 | } 70 | } 71 | 72 | export default connect(mapStateToProps, mapDispatchToProps)(App); 73 | -------------------------------------------------------------------------------- /frontend/src/api/common.js: -------------------------------------------------------------------------------- 1 | import ScatterJS from 'scatterjs-core' 2 | 3 | import { notify } from '../util/Utils'; 4 | import { appName } from './config' 5 | 6 | export const checkConnected = async () => { 7 | const connected = await ScatterJS.scatter.connect( 8 | appName, 9 | { initTimeout: 5000 }, 10 | ); 11 | if (!connected) notify('没有检测到Scatter', '请安装Scatter或激活'); 12 | } -------------------------------------------------------------------------------- /frontend/src/api/config.js: -------------------------------------------------------------------------------- 1 | import { Api, JsonRpc } from 'eosjs' 2 | 3 | import ScatterJS from 'scatterjs-core' 4 | import ScatterEOS from 'scatterjs-plugin-eosjs2' 5 | 6 | const appName = '微文'; 7 | const contract = 'weiwendappss'; 8 | 9 | // jungle testnet 10 | const network = { 11 | blockchain: 'eos', 12 | protocol: 'https', 13 | host: 'jungle2.cryptolions.io', 14 | port: 443, 15 | chainId: 'e70aaab8997e1dfce58fbfac80cbbb8fecec7b99cf982a9444273cbc64c41473', 16 | }; 17 | 18 | // local 19 | // const network = { 20 | // blockchain: 'eos', 21 | // protocol: 'http', 22 | // host: '127.0.0.1', 23 | // port: 8888, 24 | // chainId: 'cf057bbfb72640471fd910bcb67639c22df9f92470936cddc1ade0e2f2e7dc4f', 25 | // }; 26 | 27 | // mainnet 28 | // const network = { 29 | // blockchain: 'eos', 30 | // protocol: 'https', 31 | // host: 'api.eosnewyork.io', 32 | // port: 80, 33 | // chainId: 'aca376f206b8fc25a6ed44dbdc66547c36c6c33e3a119ffbeaef943642f0e906', 34 | // }; 35 | 36 | ScatterJS.plugins(new ScatterEOS()); 37 | 38 | const signatureProvider = ScatterJS.scatter.eosHook(network, null, true); 39 | const url = network.protocol + '://' + network.host + ':' + network.port; 40 | 41 | const rpc = new JsonRpc(url, { fetch }) 42 | const api = new Api({ 43 | rpc, 44 | signatureProvider, 45 | chainId: network.chainId, 46 | textDecoder: new TextDecoder(), 47 | textEncoder: new TextEncoder(), 48 | }); 49 | 50 | export { api, rpc, network, appName, contract } -------------------------------------------------------------------------------- /frontend/src/api/fetch.js: -------------------------------------------------------------------------------- 1 | import { rpc, contract } from './config' 2 | 3 | export const fetchAll = async (table, options) => { 4 | const res = await rpc.get_table_rows({ 5 | json: true, 6 | code: contract, 7 | scope: contract, 8 | table, 9 | limit: 9999, 10 | reverse: true, 11 | key_type: 'i64', 12 | index_position: 1, 13 | ...options, 14 | }); 15 | return res.rows; 16 | } 17 | 18 | export const fetchOne = async (table, keyValue) => { 19 | const res = await rpc.get_table_rows({ 20 | json: true, 21 | code: contract, 22 | scope: contract, 23 | table, 24 | lower_bound: keyValue, 25 | upper_bound: keyValue, 26 | limit: 1, 27 | }); 28 | return res.rows[0]; 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/api/login.js: -------------------------------------------------------------------------------- 1 | import ScatterJS from 'scatterjs-core' 2 | import { network } from './config' 3 | import { checkConnected } from './common'; 4 | 5 | import * as actionCreator from '../store/actionCreator'; 6 | 7 | export const login = async (dispatch) => { 8 | try { 9 | await checkConnected(); 10 | const identity = await ScatterJS.scatter.login({accounts:[network]}); 11 | const account = identity.accounts[0]; 12 | if(account){ 13 | dispatch(actionCreator.changeLoginStatus(account)); 14 | dispatch(actionCreator.getUserInfo(account.name)); 15 | } 16 | } catch (error) { 17 | console.error(error); 18 | } 19 | } 20 | 21 | export const logout = async (dispatch) => { 22 | try { 23 | await ScatterJS.scatter.logout(); 24 | dispatch(actionCreator.changeLoginStatus(false)); 25 | dispatch(actionCreator.changeUserInfo({})); 26 | } catch (error) { 27 | console.error(error); 28 | } 29 | } 30 | 31 | export const checkLogin = async (dispatch) => { 32 | try { 33 | await checkConnected(); 34 | const res = await ScatterJS.scatter.checkLogin(); 35 | if(res) dispatch(login); 36 | } catch (error) { 37 | console.error(error); 38 | } 39 | }; 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /frontend/src/api/send.js: -------------------------------------------------------------------------------- 1 | import { api, contract } from './config' 2 | 3 | export const pushAction = async (actor, permission, action, data) => { 4 | const result = await api.transact({ 5 | actions: [{ 6 | account: contract, 7 | name: action, 8 | authorization: [{ 9 | actor, 10 | permission, 11 | }], 12 | data, 13 | }] 14 | }, { 15 | blocksBehind: 3, 16 | expireSeconds: 30, 17 | }); 18 | return result; 19 | } -------------------------------------------------------------------------------- /frontend/src/api/service.js: -------------------------------------------------------------------------------- 1 | import { fetchAll } from './fetch'; 2 | import { pushAction } from './send'; 3 | import { msgTx, msgError } from '../util/Utils'; 4 | import store from '../store'; 5 | import { getUserInfo } from '../store/actionCreator'; 6 | 7 | export const reward = async (dispatch) => { 8 | const logged = store.getState().logged; 9 | if(logged.name){ 10 | try{ 11 | const res = await pushAction(logged.name, logged.authority, 'reward', {account: logged.name}); 12 | msgTx(res.transaction_id); 13 | dispatch(getUserInfo(logged.name)); 14 | }catch(error){ 15 | msgError(error.message); 16 | } 17 | } 18 | } 19 | 20 | export const withdraw = async (quantity) => { 21 | const logged = store.getState().logged; 22 | try { 23 | const res = await pushAction(logged.name, logged.authority, 'withdraw', { 24 | account: logged.name, 25 | quantity, 26 | }); 27 | msgTx(res.transaction_id); 28 | store.dispatch(getUserInfo(logged.name)); 29 | } catch (error) { 30 | msgError(error.message); 31 | } 32 | } 33 | 34 | export const post = async (content, attachtype, attachment, callback) => { 35 | const logged = store.getState().logged; 36 | try{ 37 | const res = await pushAction(logged.name, logged.authority, 'post', { 38 | author: logged.name, 39 | content, 40 | attachtype, 41 | attachment, 42 | }); 43 | callback(res); 44 | }catch(error){ 45 | callback(error); 46 | } 47 | } 48 | 49 | export const like = async (type, typeId, callback) => { 50 | const logged = store.getState().logged; 51 | try{ 52 | const res = await pushAction(logged.name, logged.authority, 'like', { 53 | author: logged.name, 54 | type, 55 | type_id: typeId, 56 | }); 57 | callback(res); 58 | }catch(error){ 59 | callback(error); 60 | } 61 | } 62 | 63 | export const isLiked = async (type, typeId) => { 64 | let isLiked = false; 65 | const logged = store.getState().logged; 66 | const res = await fetchAll('liketable', { 67 | index_position: 3, 68 | lower_bound: logged.name, 69 | upper_bound: logged.name, 70 | }) 71 | for(const k in res){ 72 | const item = res[k]; 73 | if(item.type === type && item.type_id === typeId){ 74 | isLiked = true; 75 | break; 76 | } 77 | } 78 | return isLiked; 79 | } 80 | 81 | export const comment = async (content, postId, callback, hasParent = false, pid = 0, replyTo = '') => { 82 | const logged = store.getState().logged; 83 | try { 84 | const res = await pushAction(logged.name, logged.authority, 'comment', { 85 | author: logged.name, 86 | content, 87 | post_id: postId, 88 | has_parent: hasParent, 89 | pid, 90 | reply_to: replyTo, 91 | }); 92 | callback(res); 93 | } catch (error) { 94 | callback(error); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /frontend/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import { Layout, Button, Modal, Input, Radio } from 'antd'; 4 | 5 | import * as Utils from '../util/Utils'; 6 | import { post } from '../api/service'; 7 | 8 | class Header extends Component { 9 | 10 | constructor(props){ 11 | super(props); 12 | this.showModal = this.showModal.bind(this); 13 | this.handleOk = this.handleOk.bind(this); 14 | this.handleCancel = this.handleCancel.bind(this); 15 | this.onRadioChange = this.onRadioChange.bind(this); 16 | this.onTextareaChange = this.onTextareaChange.bind(this); 17 | this.onInputChange = this.onInputChange.bind(this); 18 | } 19 | 20 | state = { 21 | modalVisible: false, 22 | confirmLoading: false, 23 | content: '', 24 | attachtype: 0, 25 | attachment: '', 26 | } 27 | 28 | postButton(){ 29 | const { logged, user } = this.props; 30 | 31 | if(logged.name && user.account === logged.name){ 32 | return ( 33 | 41 | ); 42 | } 43 | return ''; 44 | } 45 | 46 | showModal(){ 47 | this.setState({modalVisible: true}); 48 | } 49 | 50 | handleOk(){ 51 | const { content, attachtype, attachment } = this.state; 52 | if(!content){ 53 | Utils.msgError('内容不能为空'); 54 | return; 55 | } 56 | if(attachtype && !attachment){ 57 | Utils.msgError('附件不能为空'); 58 | return; 59 | } 60 | 61 | this.setState({confirmLoading: true}); 62 | 63 | post(content, attachtype, attachment, (res) => { 64 | if(res.transaction_id){ 65 | Utils.msgTx(res.transaction_id); 66 | this.setState({ 67 | modalVisible: false, 68 | confirmLoading: false, 69 | content: '', 70 | attachtype: 0, 71 | attachment: '', 72 | }); 73 | window.location.reload(); 74 | }else if(res.message){ 75 | Utils.msgError(res.message); 76 | this.setState({ 77 | modalVisible: false, 78 | confirmLoading: false, 79 | }); 80 | } 81 | }); 82 | } 83 | 84 | handleCancel(){ 85 | this.setState({modalVisible: false}); 86 | } 87 | 88 | signButton(){ 89 | const { logged, user } = this.props; 90 | 91 | if((logged.name && user.account === logged.name && !Utils.isToday(user.last_reward_time)) 92 | || (logged.name && user.account !== logged.name) ){ 93 | 94 | return ( 95 | 102 | ); 103 | } 104 | return ''; 105 | } 106 | 107 | onRadioChange(e){ 108 | this.setState({attachtype: e.target.value}); 109 | } 110 | 111 | onInputChange(e){ 112 | this.setState({attachment: e.target.value}); 113 | } 114 | 115 | onTextareaChange(e){ 116 | this.setState({content: e.target.value}); 117 | } 118 | 119 | render(){ 120 | const { modalVisible, confirmLoading, attachtype } = this.state; 121 | const { logged, login, logout } = this.props; 122 | 123 | return ( 124 | 125 |
微文
126 | 135 | 139 |
附件类型:
140 | 141 | 142 | 链接 143 | IPFS哈希值 144 | 图片 145 | 视频 146 | 其他文件 147 | 148 | {attachtype ?
附件:
: ''} 149 | {attachtype ? 150 | : ''} 151 |
152 | {this.postButton()} 153 | 160 | {this.signButton()} 161 |
162 | ); 163 | } 164 | } 165 | 166 | export default Header; 167 | -------------------------------------------------------------------------------- /frontend/src/components/Posts.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import moment from 'moment'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | import { Card, List, Avatar, Icon, Modal, Input, Divider } from 'antd'; 6 | import { fetchAll } from '../api/fetch'; 7 | import { like, isLiked, comment } from '../api/service'; 8 | import * as Utils from '../util/Utils'; 9 | 10 | class Posts extends Component { 11 | 12 | constructor(props){ 13 | super(props); 14 | this.showModal = this.showModal.bind(this); 15 | this.handleOk = this.handleOk.bind(this); 16 | this.handleCancel = this.handleCancel.bind(this); 17 | this.onTextareaChange = this.onTextareaChange.bind(this); 18 | } 19 | 20 | state = { 21 | list: [], 22 | currentPost: {}, 23 | commentContent: '', 24 | commentList: [], 25 | modalVisible: false, 26 | confirmLoading: false, 27 | } 28 | 29 | async componentDidMount(){ 30 | const res = await fetchAll('posttable'); 31 | this.setState({list: res}); 32 | } 33 | 34 | async handleLike(type, id){ 35 | if(await isLiked(type, id)){ 36 | Utils.msgError('已赞过该微文'); 37 | }else{ 38 | await like(type, id, (res) => { 39 | if(res.transaction_id){ 40 | Utils.msgTx(res.transaction_id); 41 | }else if(res.message){ 42 | Utils.msgError(res.message); 43 | } 44 | }); 45 | window.location.reload(); 46 | } 47 | } 48 | 49 | async showModal(item){ 50 | this.setState({ 51 | modalVisible: true, 52 | currentPost: item, 53 | }); 54 | const res = await fetchAll('commenttable', { 55 | index_position: 2, 56 | lower_bound: item.id, 57 | upper_bound: item.id, 58 | }); 59 | this.setState({commentList: res}); 60 | } 61 | 62 | handleCancel(){ 63 | this.setState({ 64 | modalVisible: false, 65 | currentPost: {}, 66 | }); 67 | } 68 | 69 | handleOk(){ 70 | const { logged } = this.props; 71 | const { commentList, commentContent, currentPost } = this.state; 72 | 73 | if(!commentContent){ 74 | Utils.msgError('回复内容不能为空'); 75 | return; 76 | } 77 | 78 | this.setState({confirmLoading: true}); 79 | 80 | comment(commentContent, currentPost.id, (res) => { 81 | if(res.transaction_id){ 82 | Utils.msgTx(res.transaction_id); 83 | let newCommentList = [...commentList]; 84 | newCommentList.splice(0, 0, { 85 | author: logged.name, 86 | content: commentContent, 87 | }); 88 | this.setState({ 89 | confirmLoading: false, 90 | commentContent: '', 91 | commentList: newCommentList, 92 | }); 93 | }else if(res.message){ 94 | Utils.msgError(res.message); 95 | this.setState({ 96 | modalVisible: false, 97 | confirmLoading: false, 98 | }); 99 | } 100 | }); 101 | } 102 | 103 | onTextareaChange(e){ 104 | this.setState({commentContent: e.target.value}); 105 | } 106 | 107 | render(){ 108 | const IconText = ({ type, text, onClick }) => ( 109 | 110 | 111 | {text} 112 | 113 | ); 114 | const { list, modalVisible, confirmLoading, currentPost, commentList } = this.state; 115 | return ( 116 | 117 | 126 | {currentPost.content} 127 | 128 | ( 136 | 137 | {item.author + ': ' + item.content} 138 | 139 | )} 140 | /> 141 | 145 | 146 | ( 152 | , 156 | {this.handleLike(1, item.id)}} />, 157 | {this.showModal(item)}} />, 158 | ]} 159 | > 160 |
161 |
{item.content}
162 | { 163 | item.attachtype ? 164 |
165 | 166 | 167 | 168 |
: '' 169 | } 170 |
171 | 172 | {item.author} 发布于  173 | {moment(moment(item.time).valueOf()+8*3600000).format('YYYY-MM-DD HH:mm:ss')} 174 |
175 |
176 |
177 | )} 178 | /> 179 |
180 | ) 181 | } 182 | } 183 | 184 | export default Posts; -------------------------------------------------------------------------------- /frontend/src/components/User.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import { 4 | Row, Col, Card, Divider, Avatar, Popover, 5 | Modal, Button, InputNumber, Icon, 6 | } from 'antd'; 7 | import { CopyToClipboard } from 'react-copy-to-clipboard'; 8 | import { Link } from 'react-router-dom'; 9 | import { withdraw } from '../api/service'; 10 | import { contract } from '../api/config'; 11 | import * as Utils from '../util/Utils'; 12 | 13 | var QRCode = require('qrcode.react'); 14 | 15 | class User extends Component { 16 | 17 | constructor(props){ 18 | super(props); 19 | this.handleDeposit = this.handleDeposit.bind(this); 20 | this.handleWithdraw = this.handleWithdraw.bind(this); 21 | this.handleVisibleChange = this.handleVisibleChange.bind(this); 22 | this.handleOk = this.handleOk.bind(this); 23 | this.handleCancel = this.handleCancel.bind(this); 24 | this.onInputChange = this.onInputChange.bind(this); 25 | } 26 | 27 | state = { 28 | popoverVisible: false, 29 | modalVisible: false, 30 | confirmLoading: false, 31 | quantity: '', 32 | } 33 | 34 | renderCol(label, value){ 35 | return ( 36 | 37 | 38 | {label} {this.props.user.account ? value : ''} 39 | 40 | 41 | ); 42 | } 43 | 44 | handleWithdraw(){ 45 | this.setState({ 46 | popoverVisible: false, 47 | modalVisible: true, 48 | }); 49 | } 50 | 51 | handleDeposit(){ 52 | this.setState({ popoverVisible: false }); 53 | Modal.info({ 54 | title: '充值账户二维码', 55 | okText: '知道了', 56 | bodyStyle: {marginLeft: 0}, 57 | content: ( 58 |
59 | 60 | {Utils.msgSuccess('已复制')}} 63 | > 64 |

65 | {contract}  66 |

67 |
68 |
69 | ), 70 | }); 71 | } 72 | 73 | handleVisibleChange(visible){ 74 | this.setState({ popoverVisible:visible }); 75 | } 76 | 77 | async handleOk(){ 78 | const { quantity } = this.state; 79 | let balance = this.props.user.balance; 80 | 81 | if(!quantity){ 82 | Utils.msgError('请输入提现数量,最小0.0001'); 83 | return; 84 | } 85 | balance = parseFloat(balance.slice(0, balance.length-4)); 86 | if(quantity > balance){ 87 | Utils.msgError('余额不足'); 88 | return; 89 | } 90 | 91 | this.setState({confirmLoading: true}); 92 | await withdraw(quantity.toFixed(4).toString()+' WEI'); 93 | this.setState({ 94 | modalVisible: false, 95 | confirmLoading: false, 96 | }); 97 | } 98 | 99 | handleCancel(){ 100 | this.setState({modalVisible: false}); 101 | } 102 | 103 | onInputChange(value){ 104 | this.setState({quantity: value}); 105 | } 106 | 107 | render(){ 108 | const { logged, user } = this.props; 109 | const { modalVisible, confirmLoading } = this.state; 110 | return ( 111 | 112 | 121 | 提现数量: 122 | 123 |  WEI 124 | 125 |
126 | 127 |
{logged.name}
128 |
129 | 130 | 131 | {this.renderCol('微文', user.post_num)} 132 | {this.renderCol('获赞', user.like_num)} 133 | {this.renderCol('关注', user.follow_num)} 134 | {this.renderCol('粉丝', user.fans_num)} 135 | 136 | 137 | 138 | 139 | 142 | 143 | 144 | 145 | } 146 | title={user.account ? user.balance : 'WEI'} 147 | trigger='click' 148 | visible={this.state.popoverVisible} 149 | onVisibleChange={this.handleVisibleChange} 150 | > 151 | 152 | 获得收益 {user.account ? user.balance : ''} 153 | 158 | 159 | 160 | 161 | 162 |
163 | ) 164 | } 165 | } 166 | 167 | export default User; -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import store from './store'; 5 | import App from './App'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | -------------------------------------------------------------------------------- /frontend/src/store/actionCreator.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from './actionTypes'; 2 | import { fetchOne } from '../api/fetch'; 3 | 4 | export const changeLoginStatus = (value) => ({ 5 | type: actionTypes.CHANGE_LOGIN_STATUS, 6 | value, 7 | }) 8 | 9 | export const changeUserInfo = (value) => ({ 10 | type: actionTypes.CHANGE_USER_INFO, 11 | value, 12 | }) 13 | 14 | export const getUserInfo = (value) => ( 15 | async (dispatch) => { 16 | const res = await fetchOne('usertable', value); 17 | if(res){ 18 | dispatch(changeUserInfo(res)); 19 | } 20 | } 21 | ) -------------------------------------------------------------------------------- /frontend/src/store/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const CHANGE_LOGIN_STATUS = 'CHANGE_LOGIN_STATUS'; 2 | export const CHANGE_USER_INFO = 'CHANGE_USER_INFO'; -------------------------------------------------------------------------------- /frontend/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, compose, applyMiddleware } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import reducer from './reducer'; 4 | 5 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 6 | const store = createStore(reducer, composeEnhancers( 7 | applyMiddleware(thunk) 8 | )); 9 | 10 | export default store; -------------------------------------------------------------------------------- /frontend/src/store/reducer.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from './actionTypes'; 2 | 3 | const defaultState = { 4 | logged: {}, 5 | user: {}, 6 | } 7 | 8 | export default (state = defaultState, action) => { 9 | let newState = JSON.parse(JSON.stringify(state)); 10 | switch(action.type){ 11 | case actionTypes.CHANGE_LOGIN_STATUS: 12 | newState.logged = action.value; 13 | break; 14 | case actionTypes.CHANGE_USER_INFO: 15 | newState.user = action.value; 16 | break; 17 | default: 18 | break; 19 | } 20 | return newState; 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/util/Utils.js: -------------------------------------------------------------------------------- 1 | import { notification, message } from 'antd'; 2 | 3 | export const msgSuccess = (msg) => { 4 | message.success(msg); 5 | } 6 | 7 | export const msgError = (msg) => { 8 | message.error(msg); 9 | } 10 | 11 | export const msgTx = (txid) => { 12 | msgSuccess('excute success, tx_id:' + txid); 13 | } 14 | 15 | export const notify = (message, description) => { 16 | notification.error({ 17 | message, 18 | description, 19 | }); 20 | }; 21 | 22 | export const isToday = (time) => { 23 | return new Date(time).toDateString() === new Date(new Date().valueOf()-8*3600000).toDateString(); 24 | } 25 | --------------------------------------------------------------------------------