ps-filter 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ }