ps-filter 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
package/Procfile ADDED
@@ -0,0 +1 @@
1
+ web: node ./src/api.js
package/README.md ADDED
@@ -0,0 +1,22 @@
1
+ <p align="center">
2
+ <a href="https://decent.land">
3
+ <img src="./img/logo25.png" height="124">
4
+ </a>
5
+ <h3 align="center"><code>@decentdotland/ps-filter</code></h3>
6
+ <p align="center">cache node & FE filter for the PublicSquare protocol</p>
7
+ </p>
8
+
9
+ ## Synopsis
10
+ A cache node for the [filter layer](./contracts) (SmartWeave contract) of the PublicSquare protocol.
11
+
12
+ ## API Endpoints
13
+
14
+ ### 1- decoded cached feed
15
+ - Request: GET `/feeds`
16
+ - Response: Array of posts objects
17
+
18
+ ## Further Documentation
19
+ TODO
20
+
21
+ ## License
22
+ This project is licensed under the [MIT license](./LICENSE).
@@ -0,0 +1,525 @@
1
+ export async function handle(state, action) {
2
+ const input = action.input;
3
+ const caller = action.caller;
4
+
5
+ const rate_limit = state.rate_limit;
6
+ const post_char_limit = state.post_char_limit;
7
+ const representatives = state.representatives;
8
+ const super_representatives = state.super_representatives;
9
+
10
+ const ERROR_INVALID_NFT_SRC = "post's NFT SRC is not supported";
11
+ const ERROR_PID_ALREADY_EXIST = "PID already exits";
12
+ const ERROR_PID_NOT_EXIST = "the given PID not found";
13
+ const ERROR_INVALID_REPLY_TYPE = "reply's type must be of type 'post'";
14
+ const ERROR_USER_ADDRESS_NOT_EXIST =
15
+ "the given address has not interacted with the contract";
16
+ const ERROR_USER_ALREADY_SUSPENDED =
17
+ "user already suspended - duplicated action";
18
+ const ERROR_USER_NEVER_REPORTED =
19
+ "cannot suspend a user without at least a single report by a representative";
20
+ const ERROR_INVALID_CHAR_LIMIT =
21
+ "characters limit must be an integer greater than the min-safe limit";
22
+ const ERROR_INVALID_RATE_LIMIT =
23
+ "rate limit must be an integer less than the max-safe limit";
24
+ const ERROR_INVALID_INPUT = "the function has been given an invalid argument";
25
+ const ERROR_INVALID_PRIMITIVE_TYPE = "invalid primitive data type";
26
+ const ERROR_INVALID_STRING_LENGTH =
27
+ "the string surpass the allowed min-max limits";
28
+ const ERROR_CONTENT_BODY_IS_NOT_JSON =
29
+ "post content body is not a valid JSON";
30
+ const ERROR_INVALID_ARWEAVE_ADDRESS_TRANSACTION =
31
+ "the syntax of the string is not a valid Arweave address/TX";
32
+ const ERROR_POST_OWNER_NOT_CALLER = "the PID owner is not the caller";
33
+ const ERROR_INVALID_TX_TAG = "the TX has invalid TX tag(s)";
34
+ const ERROR_INVALID_POST_STRUCTURE = "PID data structure is not valid";
35
+ const ERROR_RATE_LIMIT = "user cannot post, has reached the rate limit";
36
+ const ERROR_USER_SUSPENDED = "user cannot post, has been suspended";
37
+ const ERROR_CALLER_NOT_REPRESENTATIVE =
38
+ "only a representative address can invoke this function";
39
+ const ERROR_REPORT_NOT_EXIST_OR_EXECUTED =
40
+ "report ID does not exist or the report has been executed";
41
+ const ERROR_CONTRACT_TEMPORARY_SEALED =
42
+ "the filter contract is sealed temporary - users interactions are revoked automatically";
43
+
44
+ if (input.function === "post") {
45
+ const txid = input.txid;
46
+
47
+ _checkSealing(state.is_sealed, caller);
48
+ _validateArweaveAddress(txid);
49
+ _checkPostDuplication(txid);
50
+ // get TX tags & validate post ownership
51
+ const tx_tags = await _getTransactionTags(txid, caller);
52
+
53
+ const post_src = state.nfts_src[0];
54
+ // const poll_src = state.nfts_src[1];
55
+ let post_type; // post or poll
56
+
57
+ _checkTagExistence(tx_tags, "App-Name", "SmartWeaveContract");
58
+ _checkTagExistence(tx_tags, "App-Version", "0.3.0");
59
+ _checkTagExistence(tx_tags, "Content-Type", "application/json");
60
+ _checkTagExistence(tx_tags, "Protocol-Name", "DecentLand");
61
+ _checkTagExistence(tx_tags, "Protocol-Action", "post");
62
+ _checkTagExistence(tx_tags, "Tribus-ID", SmartWeave.contract.id);
63
+
64
+ if (
65
+ !tx_tags["Contract-Src"] ||
66
+ !state.nfts_src.includes(tx_tags["Contract-Src"])
67
+ ) {
68
+ throw new ContractError(ERROR_INVALID_NFT_SRC);
69
+ }
70
+
71
+ await _checkPostStructure(txid);
72
+
73
+ // if (tx_tags["Contract-Src"] === poll_src) {
74
+ // await _checkPollStructure(txid);
75
+ // post_type = "poll"
76
+ // }
77
+
78
+ if (!(caller in state.users)) {
79
+ state.users[caller] = {
80
+ last_interaction: 0,
81
+ reports_count: 0,
82
+ user_status: "OK",
83
+ };
84
+ }
85
+
86
+ _checkUserStatus(caller);
87
+ _checkUserRateLimit(caller);
88
+
89
+ state.users[caller].last_interaction = SmartWeave.block.height;
90
+
91
+ state.feed.push({
92
+ pid: txid,
93
+ type: "post", // hardcoded
94
+ owner: caller,
95
+ timestamp: SmartWeave.block.timestamp,
96
+ replies: [],
97
+ });
98
+
99
+ return { state };
100
+ }
101
+
102
+ if (input.function === "reply") {
103
+ const txid = input.txid;
104
+ const post_id = input.post_id;
105
+
106
+ _checkSealing(state.is_sealed, caller);
107
+ _validateArweaveAddress(txid);
108
+ _checkPostDuplication(txid);
109
+
110
+ const post_id_index = state.feed.findIndex((post) => post.pid === post_id);
111
+ const post_type = _getPostType(post_id);
112
+
113
+ if (post_id_index === -1) {
114
+ throw new ContractError(ERROR_PID_NOT_EXIST);
115
+ }
116
+
117
+ // get TX tags & validate post ownership
118
+ const tx_tags = await _getTransactionTags(txid, caller);
119
+
120
+ const post_src = state.nfts_src[0];
121
+
122
+ _checkTagExistence(tx_tags, "App-Name", "SmartWeaveContract");
123
+ _checkTagExistence(tx_tags, "App-Version", "0.3.0");
124
+ _checkTagExistence(tx_tags, "Content-Type", "application/json");
125
+ _checkTagExistence(tx_tags, "Protocol-Name", "DecentLand");
126
+ _checkTagExistence(tx_tags, "Protocol-Action", "reply");
127
+ _checkTagExistence(tx_tags, "reply_to", post_id);
128
+ _checkTagExistence(tx_tags, "Tribus-ID", SmartWeave.contract.id);
129
+
130
+ if (
131
+ !tx_tags["Contract-Src"] ||
132
+ !state.nfts_src.includes(tx_tags["Contract-Src"])
133
+ ) {
134
+ throw new ContractError(ERROR_INVALID_NFT_SRC);
135
+ }
136
+
137
+ // reply can be type of 'post' only
138
+ if (tx_tags["Contract-Src"] !== post_src) {
139
+ throw new ContractError(ERROR_INVALID_REPLY_TYPE);
140
+ }
141
+
142
+ await _checkPostStructure(txid);
143
+
144
+ if (!(caller in state.users)) {
145
+ state.users[caller] = {
146
+ last_interaction: 0,
147
+ reports_count: 0,
148
+ user_status: "OK",
149
+ };
150
+ }
151
+
152
+ _checkUserStatus(caller);
153
+ _checkUserRateLimit(caller);
154
+
155
+ state.users[caller].last_interaction = SmartWeave.block.height;
156
+
157
+ state.feed[post_id_index]["replies"].push({
158
+ pid: txid,
159
+ childOf: post_id,
160
+ owner: caller,
161
+ timestamp: SmartWeave.block.timestamp,
162
+ });
163
+
164
+ return { state };
165
+ }
166
+
167
+ // REPRESENTATIVES ACTIONS
168
+ if (input.function === "report_post") {
169
+ const pid = input.pid;
170
+ const message = input.message;
171
+
172
+ _isRepresentative(caller);
173
+
174
+ const is_post = state.feed.find((post) => post["pid"] === pid);
175
+
176
+ if (!is_post) {
177
+ throw new ContractError(ERROR_PID_NOT_EXIST);
178
+ }
179
+
180
+ const report_index = state.reports.findIndex(
181
+ (report) =>
182
+ report.type === "post_report" &&
183
+ report["pid"] === pid &&
184
+ !report?.reporters?.includes(caller)
185
+ );
186
+
187
+ if (report_index !== -1) {
188
+ const report = state.reports[report_index];
189
+ report.reporters.push(caller);
190
+ reports_count += 1;
191
+
192
+ return { state };
193
+ }
194
+
195
+ state.reports.push({
196
+ report_id: SmartWeave.transaction.id,
197
+ pid: pid,
198
+ type: "post_report",
199
+ message: message,
200
+ reporters: [caller],
201
+ reports_count: 0,
202
+ });
203
+
204
+ return { state };
205
+ }
206
+ // SUPER REPRESENTATIVE ACTIONS
207
+ if (input.function === "execute_report") {
208
+ const report_id = input.report_id;
209
+
210
+ _isSuperRepresentative(caller);
211
+
212
+ const report_index = _getReportIndex(report_id);
213
+ const report = state.reports[report_index];
214
+
215
+ if (report["type"] === "post_report") {
216
+ const pid = report["pid"];
217
+ const post_index = state.feed.findIndex((post) => post["pid"] === pid);
218
+ const post_owner = state.feed[post_index]["owner"];
219
+
220
+ state.feed.splice(post_index, 1);
221
+ state.reports[report_index].status = "executed";
222
+ state.users[post_owner]["reports_count"] += 1;
223
+
224
+ return { state };
225
+ }
226
+
227
+ if (report["type"] === "reply_report") {
228
+ const pid = report["pid"];
229
+ const post_index = state.feed.findIndex((post) =>
230
+ post["replies"].find((reply) => reply["pid"] === pid)
231
+ );
232
+ const reply_index = state.feed[post_index]["replies"].find(
233
+ (reply) => reply["pid"] === pid
234
+ );
235
+ const post_owner =
236
+ state.feed[post_index]["replies"][reply_index]["owner"];
237
+
238
+ state.feed[post_index]["replies"].splice(reply_index, 1);
239
+ state.reports[report_index].status = "executed";
240
+ state.users[post_owner]["reports_count"] += 1;
241
+
242
+ return { state };
243
+ }
244
+
245
+ return { state };
246
+ }
247
+
248
+ if (input.function === "suspend_user") {
249
+ const user_address = input.user_address;
250
+
251
+ _validateArweaveAddress(user_address);
252
+ _isSuperRepresentative(caller);
253
+
254
+ if (!state.users.user_address) {
255
+ throw new ContractError(ERROR_USER_ADDRESS_NOT_EXIST);
256
+ }
257
+
258
+ if (state.users.user_address.user_status !== "OK") {
259
+ throw new ContractError(ERROR_USER_ALREADY_SUSPENDED);
260
+ }
261
+
262
+ const user_reports_count = state.users.user_address.reports_count;
263
+ const user_super_reports =
264
+ state.users.user_address?.super_rep_reports_count;
265
+ const super_rep_half_plus_one =
266
+ Math.trunc(state.super_representatives.length / 2) + 1;
267
+
268
+ // user must be reported by atleast a single representative
269
+ if (user_reports_count === 0) {
270
+ throw new ContractError(ERROR_USER_NEVER_REPORTED);
271
+ }
272
+
273
+ if (!user_super_reports) {
274
+ state.users.user_address.super_rep_reports_count = 1;
275
+ // executed if sup_rep count = 1
276
+ if (user_super_reports >= super_rep_half_plus_one) {
277
+ state.users.user_address.user_status = "SUSPENDED";
278
+ return { state };
279
+ }
280
+ return { state };
281
+ }
282
+
283
+ if (user_super_reports < super_rep_half_plus_one) {
284
+ state.users.user_address.super_rep_reports_count += 1;
285
+
286
+ if (user_super_reports >= super_rep_half_plus_one) {
287
+ state.users.user_address.user_status = "SUSPENDED";
288
+ return { state };
289
+ }
290
+
291
+ return { state };
292
+ }
293
+ state.users.user_address.super_rep_reports_count += 1;
294
+ state.users.user_address.user_status = "SUSPENDED";
295
+
296
+ return { state };
297
+ }
298
+
299
+ if (input.function === "edit_characters_limit") {
300
+ const new_char_limit = input.new_char_limit;
301
+ const safe_minimum_limit = 280;
302
+
303
+ _isSuperRepresentative(caller);
304
+
305
+ if (
306
+ !Number.isInteger(new_char_limit) ||
307
+ new_char_limit < safe_minimum_limit
308
+ ) {
309
+ throw new ContractError(ERROR_INVALID_CHAR_LIMIT);
310
+ }
311
+
312
+ state.post_char_limit = new_limit;
313
+
314
+ return { state };
315
+ }
316
+
317
+ if (input.function === "edit_rate_limit") {
318
+ const new_rate_limit = input.new_rate_limit;
319
+ // 30 blocks delay between every interaction;
320
+ const safe_maximum_limit = 30;
321
+
322
+ _isSuperRepresentative(caller);
323
+
324
+ if (!Number.isInteger(new_rate_limit) || new_limit > safe_maximum_limit) {
325
+ throw new ContractError(ERROR_INVALID_RATE_LIMIT);
326
+ }
327
+
328
+ state.rate_limit = new_rate_limit;
329
+
330
+ return { state };
331
+ }
332
+
333
+ if (input.function === "edit_sealing") {
334
+ const sealing = input.sealing;
335
+
336
+ _isSuperRepresentative(caller);
337
+
338
+ if (![true, false].includes(sealing)) {
339
+ throw new ContractError(ERROR_INVALID_INPUT);
340
+ }
341
+
342
+ state.is_sealed = sealing;
343
+
344
+ return { state };
345
+ }
346
+
347
+ if (input.function === "remove_representative") {
348
+ const address = input.address;
349
+
350
+ _isSuperRepresentative(caller);
351
+ _isRepresentative(address);
352
+
353
+ const representativeIndex = state.representatives.findIndex(address);
354
+ state.representatives.splice(representativeIndex, 1);
355
+
356
+ return { state };
357
+ }
358
+
359
+ // HELPER FUNCTIONS
360
+ function _validateStringTypeLen(str, minLen, maxLen) {
361
+ if (typeof str !== "string") {
362
+ throw new ContractError(ERROR_INVALID_PRIMITIVE_TYPE);
363
+ }
364
+
365
+ if (str.length < minLen || str.length > maxLen) {
366
+ throw new ContractError(ERROR_INVALID_STRING_LENGTH);
367
+ }
368
+ }
369
+
370
+ function _validateArweaveAddress(str) {
371
+ _validateStringTypeLen(str, 43, 43);
372
+
373
+ const validity = /[a-z0-9_-]{43}/i.test(str);
374
+ if (!validity) {
375
+ throw new ContractError(ERROR_INVALID_ARWEAVE_ADDRESS_TRANSACTION);
376
+ }
377
+ }
378
+
379
+ async function _getTransactionTags(txid, address) {
380
+ const tags = {};
381
+ const tx_object = await SmartWeave.unsafeClient.transactions.get(txid);
382
+ const tx_owner_decoded =
383
+ await SmartWeave.unsafeClient.wallets.ownerToAddress(tx_object.owner);
384
+ const tx_tags = tx_object.get("tags");
385
+
386
+ for (let tag of tx_tags) {
387
+ const key = tag.get("name", { decode: true, string: true });
388
+ const value = tag.get("value", { decode: true, string: true });
389
+
390
+ tags[key] = value;
391
+ }
392
+
393
+ if (tx_owner_decoded !== address) {
394
+ throw new ContractError(ERROR_POST_OWNER_NOT_CALLER);
395
+ }
396
+
397
+ return tags;
398
+ }
399
+
400
+ function _checkTagExistence(tags_object, key, value) {
401
+ if (!(key in tags_object) || tags_object[key] !== value) {
402
+ throw new ContractError(ERROR_INVALID_TX_TAG);
403
+ }
404
+ }
405
+
406
+ function _checkPostDuplication(txid) {
407
+ const post_existence = state.feed.find((post) => post.pid === txid);
408
+ const reply_existence = state.feed.find((post) =>
409
+ post["replies"].find((reply) => reply.pid === txid)
410
+ );
411
+
412
+ if (post_existence || reply_existence) {
413
+ throw new ContractError(ERROR_PID_ALREADY_EXIST);
414
+ }
415
+ }
416
+
417
+ function _get_data_type(data) {
418
+ return Object.prototype.toString.call(data);
419
+ }
420
+
421
+ async function _checkPostStructure(txid) {
422
+ const content_string = await SmartWeave.unsafeClient.transactions.getData(
423
+ txid,
424
+ { decode: true, string: true }
425
+ );
426
+
427
+ try {
428
+ JSON.parse(content_string);
429
+ } catch (error) {
430
+ throw new ContractError(ERROR_CONTENT_BODY_IS_NOT_JSON);
431
+ }
432
+
433
+ const content_object = JSON.parse(content_string);
434
+
435
+ const isObject = _get_data_type(content_object) === "[object Object]";
436
+ const hasContent =
437
+ content_object.content &&
438
+ _get_data_type(content_object.content) === "[object String]";
439
+ const hasMediaArray =
440
+ content_object.media &&
441
+ _get_data_type(content_object.media) === "[object Array]";
442
+ const hasOnlyContentAndMedia = Object.keys(content_object).length === 2;
443
+ const isEmptyPost =
444
+ content_object?.content?.length + content_object?.media?.length === 0;
445
+ const isContentBelowLimit =
446
+ content_object.content?.length < post_char_limit;
447
+
448
+ if (
449
+ isObject &&
450
+ hasContent &&
451
+ hasMediaArray &&
452
+ hasOnlyContentAndMedia &&
453
+ isContentBelowLimit &&
454
+ !isEmptyPost
455
+ ) {
456
+ return true;
457
+ }
458
+
459
+ throw new ContractError(ERROR_INVALID_POST_STRUCTURE);
460
+ }
461
+
462
+ function _checkUserRateLimit(address) {
463
+ const current_blockheight = SmartWeave.block.height;
464
+ const current_user_blockheight = state.users[address].last_interaction;
465
+ const get_rate_limit = state.users[address]["rate_limit"]
466
+ ? state.users[address]["rate_limit"]
467
+ : state.rate_limit;
468
+
469
+ if (!(current_user_blockheight < current_blockheight + get_rate_limit)) {
470
+ throw new ContractError(ERROR_RATE_LIMIT);
471
+ }
472
+ }
473
+
474
+ function _checkUserStatus(address) {
475
+ const status = state.users[address].user_status;
476
+
477
+ if (status !== "OK") {
478
+ throw new ContractError(ERROR_USER_SUSPENDED);
479
+ }
480
+ }
481
+
482
+ function _isRepresentative(address) {
483
+ const is_rep = representatives.includes(address);
484
+
485
+ if (!is_rep) {
486
+ throw new ContractError(ERROR_CALLER_NOT_REPRESENTATIVE);
487
+ }
488
+ }
489
+
490
+ function _isSuperRepresentative(address) {
491
+ const is_rep = super_representatives.includes(address);
492
+
493
+ if (!is_rep) {
494
+ throw new ContractError(ERROR_CALLER_NOT_REPRESENTATIVE);
495
+ }
496
+ }
497
+
498
+ function _getReportIndex(id) {
499
+ // report.status is defined when the report get executed
500
+ // not executed report have no status
501
+ const index = state.reports.findIndex(
502
+ (report) => report["report_id"] === id && report.status
503
+ );
504
+
505
+ if (index === -1) {
506
+ throw new ContractError(ERROR_REPORT_NOT_EXIST_OR_EXECUTED);
507
+ }
508
+ }
509
+
510
+ function _getPostType(post_id) {
511
+ const post_index = state.feed.findIndex((post) => post.pid === post_id);
512
+ if (post_index === -1) {
513
+ throw new ContractError(ERROR_PID_NOT_EXIST);
514
+ }
515
+
516
+ return state.feed[post_index].type;
517
+ }
518
+
519
+ function _checkSealing(filter_sealing_state, address) {
520
+ if (filter_sealing_state && !(address in state.users)) {
521
+ throw new ContractError(ERROR_CONTRACT_TEMPORARY_SEALED);
522
+ }
523
+ }
524
+ }
525
+
@@ -0,0 +1,10 @@
1
+ { "nfts_src": ["I8xgq3361qpR8_DvqcGpkCYAUTMktyAgvkm6kGhJzEQ"],
2
+ "rate_limit": 0,
3
+ "post_char_limit": 750,
4
+ "is_sealed": false,
5
+ "feed": [],
6
+ "users": {},
7
+ "representatives": ["vZY2XY1RD9HIfWi8ift-1_DnHLDadZMWrufSh-_rKF0"],
8
+ "super_representatives": ["vZY2XY1RD9HIfWi8ift-1_DnHLDadZMWrufSh-_rKF0"],
9
+ "reports": []
10
+ }
package/img/logo25.png ADDED
Binary file
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "ps-filter",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "description": "a cache node for the filter-layer of the PublicSquare protocol",
6
+ "main": "./src/api.js",
7
+ "scripts": {
8
+ "start": "node ./src/api.js"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/decentldotland/ps-filter.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/decentldotland/ps-filter/issues"
16
+ },
17
+ "keywords": [
18
+ "public-square",
19
+ "web3",
20
+ "arweave",
21
+ "social"
22
+ ],
23
+ "author": "charmful0x",
24
+ "license": "MIT",
25
+ "dependencies": {
26
+ "arweave": "^1.10.23",
27
+ "axios": "^0.26.0",
28
+ "base64url": "^3.0.1",
29
+ "cors": "^2.8.5",
30
+ "express": "^4.17.3",
31
+ "node-cache": "^5.1.2",
32
+ "redstone-smartweave": "^0.4.71"
33
+ }
34
+ }
package/src/api.js ADDED
@@ -0,0 +1,37 @@
1
+ import express from "express";
2
+ import base64url from "base64url";
3
+ import cors from "cors";
4
+ import { getFeed, cache } from "./utils/cache.js";
5
+ import { timeout } from "./utils/arweave.js";
6
+
7
+ const app = express();
8
+ const port = process.env.PORT || 7777;
9
+
10
+ app.use(
11
+ cors({
12
+ origin: "*",
13
+ })
14
+ );
15
+
16
+ app.get("/feeds", async (req, res) => {
17
+ res.setHeader("Content-Type", "application/json");
18
+ const encodedFeed = await getFeed();
19
+ const jsonRes = JSON.parse(base64url.decode(encodedFeed));
20
+ res.send(jsonRes);
21
+ });
22
+
23
+ app.listen(port, async () => {
24
+ await polling();
25
+ console.log(`listening at PORT:${port}`);
26
+ });
27
+
28
+ async function polling() {
29
+ while (true) {
30
+ try {
31
+ await cache();
32
+ await timeout();
33
+ } catch (error) {
34
+ console.log(error);
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,34 @@
1
+ import Arweave from "arweave";
2
+ import { SmartWeaveNodeFactory } from "redstone-smartweave";
3
+ import { FILTER_SWC_ADDRESS, BLOCK_SLEEPING_TIMEOUT } from "./constants.js";
4
+
5
+ export const arweave = Arweave.init({
6
+ host: "arweave.net",
7
+ port: 443,
8
+ protocol: "https",
9
+ timeout: 20000,
10
+ logging: false,
11
+ });
12
+
13
+ const smartweave = SmartWeaveNodeFactory.memCached(arweave);
14
+
15
+ export async function readFilterContract() {
16
+ try {
17
+ const contract = smartweave.contract(FILTER_SWC_ADDRESS);
18
+ const { state, validity } = await contract.readState();
19
+
20
+ return state;
21
+ } catch (error) {
22
+ console.log(error);
23
+ return false;
24
+ }
25
+ }
26
+
27
+ export function timeout() {
28
+ // a block is ~ 2 min
29
+ const ms = BLOCK_SLEEPING_TIMEOUT * 2 * 60 * 1e3;
30
+ console.log(
31
+ `\nsleeping for ${BLOCK_SLEEPING_TIMEOUT} network blocks or ${ms} ms\n`
32
+ );
33
+ return new Promise((resolve) => setTimeout(resolve, ms));
34
+ }
@@ -0,0 +1,61 @@
1
+ import { readFilterContract, arweave } from "./arweave.js";
2
+ import { readAndEncode } from "./filter.js";
3
+ import { getLastInteraction } from "./getLastInteraction.js";
4
+ import NodeCache from "node-cache";
5
+ import base64url from "base64url";
6
+
7
+ const base64Cache = new NodeCache();
8
+
9
+ export async function cache() {
10
+ const lastInteraction = (await getLastInteraction())[0]?.block;
11
+ const currentBlock = (await arweave.blocks.getCurrent())?.height;
12
+
13
+ // initialization
14
+ if (!base64Cache.has("feed")) {
15
+ const feed = await readAndEncode();
16
+ base64Cache.set("raw", feed.raw);
17
+ base64Cache.set("feed", feed.loaded);
18
+ base64Cache.set("height", currentBlock);
19
+
20
+ console.log(`STATE ALREADY CACHED - HEIGHT: ${base64Cache.get("height")}`);
21
+ }
22
+
23
+ // cache new interactions
24
+ if (
25
+ !base64Cache.get("height") ||
26
+ base64Cache.get("height") < lastInteraction
27
+ ) {
28
+ const feed = await readAndEncode();
29
+ base64Cache.set("raw", feed.raw);
30
+ base64Cache.set("feed", feed.loaded);
31
+ base64Cache.set("height", lastInteraction);
32
+
33
+ console.log(`NEW STATE CACHED - HEIGHT: ${lastInteraction}`);
34
+ }
35
+ }
36
+
37
+ export async function getPidData(pid) {
38
+ try {
39
+ // feed is passed by readAndEncode();
40
+ if (base64Cache.has(pid)) {
41
+ return base64Cache.get(pid);
42
+ }
43
+ // if not cached, cache it
44
+ const contentData = await arweave.transactions.getData(pid, {
45
+ decode: true,
46
+ string: true,
47
+ });
48
+ base64Cache.set(pid, contentData);
49
+ return base64Cache.get(pid);
50
+ } catch (error) {
51
+ console.log(error);
52
+ }
53
+ }
54
+
55
+ export async function getFeed() {
56
+ if (!base64Cache.has("feed")) {
57
+ return "e30";
58
+ }
59
+
60
+ return base64Cache.get("feed");
61
+ }
@@ -0,0 +1,4 @@
1
+ export const FILTER_SWC_ADDRESS = "k-ld4VIqoZF5XnlQLCt71wbow5vit1nATnCTMAnug5g";
2
+ export const BLOCK_SLEEPING_TIMEOUT = 3;
3
+
4
+ // not used: IfPzGFarZAaIlxpmAz5DvEYR4127K3mPI6sFoTfxwNg
@@ -0,0 +1,46 @@
1
+ import { readFilterContract } from "./arweave.js";
2
+ import { getPidData } from "./cache.js";
3
+ import base64url from "base64url";
4
+
5
+ export async function readAndEncode() {
6
+ try {
7
+ const feed0 = (await readFilterContract()).feed;
8
+ const feed1 = await getFeed1(feed0);
9
+ const feed2 = await getFeed2(feed1);
10
+
11
+ return {
12
+ loaded: base64url(JSON.stringify(feed2)), // post's TXID (data) is decoded
13
+ raw: base64url(JSON.stringify(feed0)) // post content is encoded as Arweave TXID
14
+ }
15
+ } catch (error) {
16
+ console.log(error);
17
+ }
18
+ }
19
+
20
+ async function getFeed1(feed) {
21
+ try {
22
+ for (let thread of feed) {
23
+ thread.pid = await getPidData(thread.pid);
24
+ }
25
+
26
+ return feed;
27
+ } catch (error) {
28
+ console.log(error);
29
+ }
30
+ }
31
+
32
+ async function getFeed2(feed) {
33
+ try {
34
+ for (let thread of feed) {
35
+ if (thread.replies.length > 0) {
36
+ for (let reply of thread.replies) {
37
+ reply.pid = await getPidData(reply.pid);
38
+ }
39
+ }
40
+ }
41
+
42
+ return feed;
43
+ } catch (error) {
44
+ console.log(error);
45
+ }
46
+ }
@@ -0,0 +1,58 @@
1
+ import axios from "axios";
2
+ import { FILTER_SWC_ADDRESS } from "./constants.js";
3
+ import { arweave } from "./arweave.js";
4
+
5
+ const gqlQuery = {
6
+ query: `query {
7
+ transactions(
8
+ tags: [
9
+ { name: "App-Name", values: "SmartWeaveAction"},
10
+ { name: "Contract", values: "${FILTER_SWC_ADDRESS}"}
11
+ ]
12
+ first: 1
13
+ ) {
14
+ edges {
15
+ node {
16
+ id
17
+ owner { address }
18
+ block { height }
19
+
20
+ }
21
+ }
22
+ }
23
+ }`,
24
+ };
25
+
26
+ async function gqlTemplate(query) {
27
+ const response = await axios.post("https://arweave.net/graphql", query, {
28
+ headers: { "Content-Type": "application/json" },
29
+ });
30
+
31
+ const transactionIds = [];
32
+
33
+ const res_arr = response.data.data.transactions.edges;
34
+
35
+ for (let element of res_arr) {
36
+ const tx = element["node"];
37
+ const txExistence = transactionIds.find((txObj) => txObj.id === tx.id);
38
+
39
+ if (!txExistence) {
40
+ transactionIds.push({
41
+ id: tx.id,
42
+ owner: tx.owner.address,
43
+ block: tx.block ? tx.block.height : void 0,
44
+ });
45
+ }
46
+ }
47
+
48
+ return transactionIds;
49
+ }
50
+
51
+ export async function getLastInteraction() {
52
+ try {
53
+ const re = await gqlTemplate(gqlQuery);
54
+ return re;
55
+ } catch (error) {
56
+ console.log(error);
57
+ }
58
+ }