mirror of
https://github.com/zkitter/zkitterd.git
synced 2026-01-09 13:47:56 -05:00
implement zkchat and ipfs integration; update app to use rln proofs (#5)
* add ipfs upload * add user auth for upload * insert upload data to postgres * limit upload to 100mb per user * wip * add zkchat endpoint * update schema * wip * add group info * remove log * fix semaphore signal hash * allow file upload for zk post * udpating interep service * return zk group type * verify rln proof * add server sent event
This commit is contained in:
BIN
circuitFiles/rln/rln.wasm
Normal file
BIN
circuitFiles/rln/rln.wasm
Normal file
Binary file not shown.
BIN
circuitFiles/rln/rln_final.zkey
Normal file
BIN
circuitFiles/rln/rln_final.zkey
Normal file
Binary file not shown.
119
circuitFiles/rln/verification_key.json
Normal file
119
circuitFiles/rln/verification_key.json
Normal file
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"protocol": "groth16",
|
||||
"curve": "bn128",
|
||||
"nPublic": 6,
|
||||
"vk_alpha_1": [
|
||||
"20491192805390485299153009773594534940189261866228447918068658471970481763042",
|
||||
"9383485363053290200918347156157836566562967994039712273449902621266178545958",
|
||||
"1"
|
||||
],
|
||||
"vk_beta_2": [
|
||||
[
|
||||
"6375614351688725206403948262868962793625744043794305715222011528459656738731",
|
||||
"4252822878758300859123897981450591353533073413197771768651442665752259397132"
|
||||
],
|
||||
[
|
||||
"10505242626370262277552901082094356697409835680220590971873171140371331206856",
|
||||
"21847035105528745403288232691147584728191162732299865338377159692350059136679"
|
||||
],
|
||||
[
|
||||
"1",
|
||||
"0"
|
||||
]
|
||||
],
|
||||
"vk_gamma_2": [
|
||||
[
|
||||
"10857046999023057135944570762232829481370756359578518086990519993285655852781",
|
||||
"11559732032986387107991004021392285783925812861821192530917403151452391805634"
|
||||
],
|
||||
[
|
||||
"8495653923123431417604973247489272438418190587263600148770280649306958101930",
|
||||
"4082367875863433681332203403145435568316851327593401208105741076214120093531"
|
||||
],
|
||||
[
|
||||
"1",
|
||||
"0"
|
||||
]
|
||||
],
|
||||
"vk_delta_2": [
|
||||
[
|
||||
"14962742607348761745067039667743739542918587958944430293802039673540545491766",
|
||||
"19420690459571091434654770613940591470063366130537730965999231781890983822228"
|
||||
],
|
||||
[
|
||||
"8025490964181160791180478620151567079603098158752936953068367103529675811355",
|
||||
"16899630578734283128344486213072467840624492210479261395286682179061749476405"
|
||||
],
|
||||
[
|
||||
"1",
|
||||
"0"
|
||||
]
|
||||
],
|
||||
"vk_alphabeta_12": [
|
||||
[
|
||||
[
|
||||
"2029413683389138792403550203267699914886160938906632433982220835551125967885",
|
||||
"21072700047562757817161031222997517981543347628379360635925549008442030252106"
|
||||
],
|
||||
[
|
||||
"5940354580057074848093997050200682056184807770593307860589430076672439820312",
|
||||
"12156638873931618554171829126792193045421052652279363021382169897324752428276"
|
||||
],
|
||||
[
|
||||
"7898200236362823042373859371574133993780991612861777490112507062703164551277",
|
||||
"7074218545237549455313236346927434013100842096812539264420499035217050630853"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"7077479683546002997211712695946002074877511277312570035766170199895071832130",
|
||||
"10093483419865920389913245021038182291233451549023025229112148274109565435465"
|
||||
],
|
||||
[
|
||||
"4595479056700221319381530156280926371456704509942304414423590385166031118820",
|
||||
"19831328484489333784475432780421641293929726139240675179672856274388269393268"
|
||||
],
|
||||
[
|
||||
"11934129596455521040620786944827826205713621633706285934057045369193958244500",
|
||||
"8037395052364110730298837004334506829870972346962140206007064471173334027475"
|
||||
]
|
||||
]
|
||||
],
|
||||
"IC": [
|
||||
[
|
||||
"1903896611309112537898744888962900116850557104688140496996114549467905845389",
|
||||
"18878807081439367511400607605971457252753640659171839147843428814578113606944",
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"3379380938593207080729160389717513882233130462616390491862768147575356927916",
|
||||
"7658366061956613070318477742617874370870162905631795036979866045784951077741",
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"6573257371221466457264319919959056878987621722065494829982803761054687125969",
|
||||
"10882724634177179759524298899662307047578489073607305659656545291770678864358",
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"7812315588577187126949469943175416873443639588836013906528191759531184751241",
|
||||
"11928578526804134334949431821595151892366575912030023302327660047097849106833",
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"8603322753346553805166809285487650902416020947317326006612301010819108432803",
|
||||
"3100089596497060388197556426101480177422659517238854788949908444987414217355",
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"548584601681146753562700521279324517960520272202999011217745775929880443671",
|
||||
"10775304696187723066725699321032508342393012304409445749847420882128296879500",
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"4832346778469239752999589970014962368162544386191392542939015835265757824362",
|
||||
"17497085844864771075682226779903142040218959461493641885155366729946818944068",
|
||||
"1"
|
||||
]
|
||||
]
|
||||
}
|
||||
BIN
circuitFiles/semaphore/semaphore.wasm
Normal file
BIN
circuitFiles/semaphore/semaphore.wasm
Normal file
Binary file not shown.
BIN
circuitFiles/semaphore/semaphore_final.zkey
Normal file
BIN
circuitFiles/semaphore/semaphore_final.zkey
Normal file
Binary file not shown.
1
circuitFiles/semaphore/verification_key.json
Normal file
1
circuitFiles/semaphore/verification_key.json
Normal file
@@ -0,0 +1 @@
|
||||
{"protocol":"groth16","curve":"bn128","nPublic":4,"vk_alpha_1":["20491192805390485299153009773594534940189261866228447918068658471970481763042","9383485363053290200918347156157836566562967994039712273449902621266178545958","1"],"vk_beta_2":[["6375614351688725206403948262868962793625744043794305715222011528459656738731","4252822878758300859123897981450591353533073413197771768651442665752259397132"],["10505242626370262277552901082094356697409835680220590971873171140371331206856","21847035105528745403288232691147584728191162732299865338377159692350059136679"],["1","0"]],"vk_gamma_2":[["10857046999023057135944570762232829481370756359578518086990519993285655852781","11559732032986387107991004021392285783925812861821192530917403151452391805634"],["8495653923123431417604973247489272438418190587263600148770280649306958101930","4082367875863433681332203403145435568316851327593401208105741076214120093531"],["1","0"]],"vk_delta_2":[["7912208710313447447762395792098481825752520616755888860068004689933335666613","12599857379517512478445603412764121041984228075771497593287716170335433683702"],["21679208693936337484429571887537508926366191105267550375038502782696042114705","11502426145685875357967720478366491326865907869902181704031346886834786027007"],["1","0"]],"vk_alphabeta_12":[[["2029413683389138792403550203267699914886160938906632433982220835551125967885","21072700047562757817161031222997517981543347628379360635925549008442030252106"],["5940354580057074848093997050200682056184807770593307860589430076672439820312","12156638873931618554171829126792193045421052652279363021382169897324752428276"],["7898200236362823042373859371574133993780991612861777490112507062703164551277","7074218545237549455313236346927434013100842096812539264420499035217050630853"]],[["7077479683546002997211712695946002074877511277312570035766170199895071832130","10093483419865920389913245021038182291233451549023025229112148274109565435465"],["4595479056700221319381530156280926371456704509942304414423590385166031118820","19831328484489333784475432780421641293929726139240675179672856274388269393268"],["11934129596455521040620786944827826205713621633706285934057045369193958244500","8037395052364110730298837004334506829870972346962140206007064471173334027475"]]],"IC":[["19918517214839406678907482305035208173510172567546071380302965459737278553528","7151186077716310064777520690144511885696297127165278362082219441732663131220","1"],["690581125971423619528508316402701520070153774868732534279095503611995849608","21271996888576045810415843612869789314680408477068973024786458305950370465558","1"],["16461282535702132833442937829027913110152135149151199860671943445720775371319","2814052162479976678403678512565563275428791320557060777323643795017729081887","1"],["4319780315499060392574138782191013129592543766464046592208884866569377437627","13920930439395002698339449999482247728129484070642079851312682993555105218086","1"],["3554830803181375418665292545416227334138838284686406179598687755626325482686","5951609174746846070367113593675211691311013364421437923470787371738135276998","1"]]}
|
||||
111
lib/zk-chat-server/src/index.ts
Normal file
111
lib/zk-chat-server/src/index.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import {logger} from "./utils/svc";
|
||||
import Chat, {ChatMessage} from "./services/chat.service";
|
||||
import DB from "./services/db.service";
|
||||
import config from "./utils/config";
|
||||
import {RLN, RLNFullProof} from "@zk-kit/protocols";
|
||||
import vkey from "../statics/circuitFiles/rln/verification_key.json";
|
||||
import {ShareModel} from "./models/shares.model";
|
||||
|
||||
export class ZKChat {
|
||||
DB: DB;
|
||||
Chat: Chat;
|
||||
|
||||
constructor() {
|
||||
this.DB = new DB();
|
||||
this.Chat = new Chat();
|
||||
}
|
||||
|
||||
async init() {
|
||||
try {
|
||||
await this.DB.start();
|
||||
await this.Chat.start();
|
||||
} catch (e) {
|
||||
logger.error(e.message, {stack: e.stack});
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
registerUser = async (address: string, ecdhPubkey: string) => {
|
||||
return this.DB.users?.insertUser(address, ecdhPubkey);
|
||||
}
|
||||
|
||||
getAllUsers = async (offset = 0, limit = 20) => {
|
||||
return this.DB.users?.getUsers(offset, limit);
|
||||
}
|
||||
|
||||
getDirectMessages = async (sender: string, receiver: string, offset = 0, limit = 20) => {
|
||||
return this.DB.chats?.getDirectMessages(sender, receiver, offset, limit);
|
||||
}
|
||||
|
||||
getDirectChatsForUser = async (address: string) => {
|
||||
return this.DB.chats?.getDirectChatsForUser(address);
|
||||
}
|
||||
|
||||
checkShare = async (share: ShareModel) => {
|
||||
return this.DB.shares?.checkShare(share);
|
||||
}
|
||||
|
||||
insertShare = async (share: ShareModel) => {
|
||||
return this.DB.shares?.insertShare(share);
|
||||
}
|
||||
|
||||
isEpochCurrent = async (epoch: string) => {
|
||||
const serverTimestamp = new Date();
|
||||
// serverTimestamp.setSeconds(Math.floor(serverTimestamp.getSeconds() / 10) * 10);
|
||||
serverTimestamp.setMilliseconds(0);
|
||||
const messageTimestamp = new Date(parseInt(epoch));
|
||||
|
||||
// Tolerate a difference of EPOCH_ALLOWED_DELAY_THRESHOLD seconds between client and server timestamp
|
||||
const difference_in_seconds = Math.abs(serverTimestamp.getTime() - messageTimestamp.getTime()) / 1000;
|
||||
if (difference_in_seconds >= config.EPOCH_ALLOWED_DELAY_THRESHOLD) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
verifyRLNProof = async (proof: RLNFullProof) => {
|
||||
return RLN.verifyProof(
|
||||
vkey as any,
|
||||
proof,
|
||||
);
|
||||
}
|
||||
|
||||
addChatMessage = async (chatMessage: ChatMessage) => {
|
||||
let data, r_user, s_user;
|
||||
|
||||
if (chatMessage.sender.address) {
|
||||
s_user = await this.DB.users?.getUserByAddress(chatMessage.sender.address);
|
||||
if (!s_user) throw new Error(`${chatMessage.sender} is not registered`);
|
||||
}
|
||||
|
||||
if (chatMessage.receiver.address) {
|
||||
r_user = await this.DB.users?.getUserByAddress(chatMessage.receiver.address);
|
||||
if (!r_user) throw new Error(`${chatMessage.receiver} is not registered`);
|
||||
}
|
||||
|
||||
if (chatMessage.type === 'DIRECT') {
|
||||
data = {
|
||||
message_id: chatMessage.messageId,
|
||||
type: chatMessage.type,
|
||||
sender_address: chatMessage.sender.hash
|
||||
? undefined
|
||||
: chatMessage.sender.address,
|
||||
sender_hash: chatMessage.sender.hash,
|
||||
sender_pubkey: chatMessage.sender.ecdh || s_user?.pubkey,
|
||||
timestamp: chatMessage.timestamp.getTime(),
|
||||
receiver_address: chatMessage.receiver.address,
|
||||
receiver_pubkey: chatMessage.receiver.ecdh || r_user?.pubkey,
|
||||
ciphertext: chatMessage.ciphertext,
|
||||
rln_serialized_proof: chatMessage.rln ? JSON.stringify(chatMessage.rln) : undefined,
|
||||
rln_root: chatMessage.rln
|
||||
? '0x' + BigInt(chatMessage.rln?.publicSignals.merkleRoot).toString(16)
|
||||
: undefined,
|
||||
};
|
||||
|
||||
await this.DB.chats?.insertChatMessage(data);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
194
lib/zk-chat-server/src/models/chat.model.ts
Normal file
194
lib/zk-chat-server/src/models/chat.model.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import {Mutex} from "async-mutex";
|
||||
import {BIGINT, ENUM, Op, QueryTypes, Sequelize, STRING} from "sequelize";
|
||||
|
||||
const mutex = new Mutex();
|
||||
|
||||
export type ChatMessageModel = {
|
||||
message_id: string;
|
||||
type: 'DIRECT' | 'PUBLIC_ROOM' | 'PRIVATE_ROOM';
|
||||
sender_address?: string;
|
||||
sender_pubkey?: string;
|
||||
sender_hash?: string;
|
||||
timestamp: number;
|
||||
rln_serialized_proof?: string;
|
||||
rln_root?: string;
|
||||
receiver_address?: string;
|
||||
receiver_pubkey?: string;
|
||||
ciphertext?: string;
|
||||
content?: string;
|
||||
reference?: string;
|
||||
attachment?: string;
|
||||
}
|
||||
|
||||
export type Chat = {
|
||||
type: 'DIRECT';
|
||||
receiver: string;
|
||||
receiverECDH: string;
|
||||
senderECDH: string;
|
||||
senderHash?: string;
|
||||
} | {
|
||||
type: 'PUBLIC_ROOM';
|
||||
receiver: string;
|
||||
}
|
||||
|
||||
const REALLY_BIG_NUMBER = 999999999999999999;
|
||||
|
||||
const chats = (sequelize: Sequelize) => {
|
||||
const model = sequelize.define('zkchat_chats', {
|
||||
message_id: {
|
||||
type: STRING,
|
||||
primaryKey: true,
|
||||
},
|
||||
type: {
|
||||
type: STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
sender_address: {
|
||||
type: STRING,
|
||||
},
|
||||
sender_pubkey: {
|
||||
type: STRING,
|
||||
},
|
||||
sender_hash: {
|
||||
type: STRING,
|
||||
},
|
||||
timestamp: {
|
||||
type: BIGINT,
|
||||
allowNull: false,
|
||||
},
|
||||
rln_serialized_proof: {
|
||||
type: STRING(65535),
|
||||
},
|
||||
rln_root: {
|
||||
type: STRING,
|
||||
},
|
||||
receiver_address: {
|
||||
type: STRING,
|
||||
},
|
||||
receiver_pubkey: {
|
||||
type: STRING,
|
||||
},
|
||||
ciphertext: {
|
||||
type: STRING,
|
||||
},
|
||||
content: {
|
||||
type: STRING,
|
||||
},
|
||||
reference: {
|
||||
type: STRING,
|
||||
},
|
||||
attachment: {
|
||||
type: STRING,
|
||||
},
|
||||
}, {
|
||||
indexes: [
|
||||
{ fields: ['message_id'] },
|
||||
{ fields: ['receiver_address'] },
|
||||
{ fields: ['receiver_pubkey'] },
|
||||
{ fields: ['sender_address'] },
|
||||
{ fields: ['sender_pubkey'] },
|
||||
{ fields: ['rln_root'] },
|
||||
],
|
||||
});
|
||||
|
||||
const insertChatMessage = async (data: ChatMessageModel) => {
|
||||
return mutex.runExclusive(async () => {
|
||||
const res = await model.create(data);
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
const removeChatMessage = async (message_id: string) => {
|
||||
return mutex.runExclusive(async () => {
|
||||
try {
|
||||
const res = await model.destroy({
|
||||
where: {
|
||||
message_id,
|
||||
},
|
||||
});
|
||||
return res;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const getDirectMessages = async (sender_pubkey: string, receiver_pubkey: string, offset = REALLY_BIG_NUMBER, limit = 20): Promise<ChatMessageModel[]> => {
|
||||
const values = await sequelize.query(`
|
||||
SELECT * FROM zkchat_chats zk
|
||||
WHERE (
|
||||
(zk.sender_pubkey = :sender_pubkey AND zk.receiver_pubkey = :receiver_pubkey)
|
||||
OR
|
||||
(zk.sender_pubkey = :receiver_pubkey AND zk.receiver_pubkey = :sender_pubkey)
|
||||
) AND (
|
||||
zk.timestamp < :offset
|
||||
)
|
||||
ORDER BY zk.timestamp DESC
|
||||
LIMIT :limit
|
||||
`, {
|
||||
type: QueryTypes.SELECT,
|
||||
replacements: {
|
||||
sender_pubkey,
|
||||
receiver_pubkey,
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
return values;
|
||||
}
|
||||
|
||||
const getDirectChatsForUser = async (pubkey: string): Promise<Chat[]> => {
|
||||
const values = await sequelize.query(`
|
||||
SELECT distinct zkc.receiver_pubkey as pubkey, zkc.receiver_address as address FROM zkchat_chats zkc
|
||||
WHERE zkc.receiver_pubkey IN (
|
||||
SELECT distinct receiver_pubkey FROM zkchat_chats WHERE sender_pubkey = :pubkey
|
||||
)
|
||||
UNION
|
||||
SELECT distinct zkc.sender_pubkey as pubkey, zkc.sender_address as address FROM zkchat_chats zkc
|
||||
WHERE zkc.sender_pubkey IN (
|
||||
SELECT distinct sender_pubkey FROM zkchat_chats WHERE receiver_pubkey = :pubkey
|
||||
);
|
||||
`, {
|
||||
type: QueryTypes.SELECT,
|
||||
replacements: {
|
||||
pubkey,
|
||||
},
|
||||
});
|
||||
|
||||
return values.map((data: any) => ({
|
||||
type: 'DIRECT',
|
||||
receiver: data.address,
|
||||
receiverECDH: data.pubkey,
|
||||
senderECDH: pubkey,
|
||||
}));
|
||||
}
|
||||
|
||||
const getMessagesByRoomId = async (roomId: string, offset = 0, limit = 20): Promise<ChatMessageModel[]> => {
|
||||
const res = await model.findAll({
|
||||
where: {
|
||||
receiver: roomId,
|
||||
},
|
||||
limit,
|
||||
offset,
|
||||
order: [
|
||||
['timestamp', 'DESC'],
|
||||
],
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
return res.map(data => data.toJSON());
|
||||
}
|
||||
|
||||
return {
|
||||
model,
|
||||
insertChatMessage,
|
||||
removeChatMessage,
|
||||
getDirectMessages,
|
||||
getMessagesByRoomId,
|
||||
getDirectChatsForUser,
|
||||
};
|
||||
}
|
||||
|
||||
export default chats;
|
||||
72
lib/zk-chat-server/src/models/shares.model.ts
Normal file
72
lib/zk-chat-server/src/models/shares.model.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import {Mutex} from "async-mutex";
|
||||
import {BIGINT, ENUM, Op, QueryTypes, Sequelize, STRING} from "sequelize";
|
||||
|
||||
const mutex = new Mutex();
|
||||
|
||||
export type ShareModel = {
|
||||
x_share: string;
|
||||
y_share: string;
|
||||
nullifier: string;
|
||||
epoch: string;
|
||||
}
|
||||
|
||||
const shares = (sequelize: Sequelize) => {
|
||||
const model = sequelize.define('zkchat_shares', {
|
||||
x_share: {
|
||||
type: STRING,
|
||||
},
|
||||
y_share: {
|
||||
type: STRING,
|
||||
},
|
||||
nullifier: {
|
||||
type: STRING,
|
||||
},
|
||||
epoch: {
|
||||
type: STRING,
|
||||
},
|
||||
}, {
|
||||
indexes: [
|
||||
{ fields: ['x_share', 'y_share', 'nullifier', 'epoch'], unique: true },
|
||||
{ fields: ['nullifier'] },
|
||||
{ fields: ['epoch'] },
|
||||
],
|
||||
});
|
||||
|
||||
const checkShare = async (data: ShareModel) => {
|
||||
const shares = await getShares(data.nullifier, data.epoch);
|
||||
const duplicate = await model.findOne({ where: data });
|
||||
return {
|
||||
shares,
|
||||
isDuplicate: !!duplicate,
|
||||
isSpam: !!shares.length,
|
||||
};
|
||||
};
|
||||
|
||||
const insertShare = async (data: ShareModel) => {
|
||||
return mutex.runExclusive(async () => {
|
||||
const res = await model.create(data);
|
||||
return res;
|
||||
});
|
||||
};
|
||||
|
||||
const getShares = async (nullifier: string, epoch: string): Promise<ShareModel[]> => {
|
||||
const res = await model.findAll({
|
||||
where: {
|
||||
nullifier,
|
||||
epoch,
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
return res.map(data => data.toJSON());
|
||||
};
|
||||
|
||||
return {
|
||||
model,
|
||||
checkShare,
|
||||
insertShare,
|
||||
getShares,
|
||||
}
|
||||
};
|
||||
|
||||
export default shares;
|
||||
92
lib/zk-chat-server/src/models/user.model.ts
Normal file
92
lib/zk-chat-server/src/models/user.model.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import {Mutex} from "async-mutex";
|
||||
import {BIGINT, ENUM, Op, QueryTypes, Sequelize, STRING} from "sequelize";
|
||||
|
||||
const mutex = new Mutex();
|
||||
|
||||
type ZKUserModel = {
|
||||
wallet_address: string;
|
||||
pubkey: string;
|
||||
}
|
||||
|
||||
const users = (sequelize: Sequelize) => {
|
||||
const model = sequelize.define('zkchat_users', {
|
||||
wallet_address: {
|
||||
type: STRING,
|
||||
primaryKey: true,
|
||||
},
|
||||
pubkey: {
|
||||
type: STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
}, {
|
||||
indexes: [
|
||||
{ fields: ['wallet_address'] },
|
||||
{ fields: ['pubkey'] },
|
||||
],
|
||||
});
|
||||
|
||||
const insertUser = async (wallet_address: string, pubkey: string) => {
|
||||
return mutex.runExclusive(async () => {
|
||||
const res = await model.findOne({
|
||||
where: {
|
||||
wallet_address,
|
||||
},
|
||||
});
|
||||
|
||||
if (res) {
|
||||
return res.update({
|
||||
pubkey,
|
||||
});
|
||||
} else {
|
||||
return model.create({
|
||||
wallet_address,
|
||||
pubkey,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const getUsers = async (offset = 0, limit = 20) => {
|
||||
const res = await model.findAll({
|
||||
limit,
|
||||
offset,
|
||||
order: [
|
||||
['createdAt', 'DESC'],
|
||||
],
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
return res.map(data => data.toJSON());
|
||||
}
|
||||
|
||||
const getUserByAddress = async (wallet_address: string): Promise<ZKUserModel | undefined> => {
|
||||
const res = await model.findOne({
|
||||
where: {
|
||||
wallet_address,
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
return res?.toJSON();
|
||||
}
|
||||
|
||||
const getUserByPubkey = async (pubkey: string) => {
|
||||
const res = await model.findOne({
|
||||
where: {
|
||||
pubkey,
|
||||
},
|
||||
});
|
||||
|
||||
return res?.toJSON();
|
||||
}
|
||||
|
||||
return {
|
||||
model,
|
||||
insertUser,
|
||||
getUsers,
|
||||
getUserByAddress,
|
||||
getUserByPubkey,
|
||||
};
|
||||
}
|
||||
|
||||
export default users;
|
||||
63
lib/zk-chat-server/src/services/chat.service.ts
Normal file
63
lib/zk-chat-server/src/services/chat.service.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import {GenericService} from "../utils/svc";
|
||||
import {RLNFullProof} from "@zk-kit/protocols";
|
||||
|
||||
export enum ChatMessageType {
|
||||
DIRECT = 'DIRECT',
|
||||
PUBLIC_ROOM = 'PUBLIC_ROOM',
|
||||
PRIVATE_ROOM = 'PRIVATE_ROOM',
|
||||
}
|
||||
|
||||
export type DirectChatMessage = {
|
||||
messageId: string;
|
||||
timestamp: Date;
|
||||
type: ChatMessageType.DIRECT;
|
||||
sender: {
|
||||
address?: string;
|
||||
ecdh?: string;
|
||||
hash?: string;
|
||||
};
|
||||
rln?: RLNFullProof & {
|
||||
epoch: number;
|
||||
x_share: string;
|
||||
group_id: string;
|
||||
};
|
||||
receiver: {
|
||||
address?: string;
|
||||
ecdh?: string;
|
||||
roomId?: string;
|
||||
};
|
||||
ciphertext: string;
|
||||
content?: string;
|
||||
reference?: string;
|
||||
attachment?: string;
|
||||
}
|
||||
|
||||
export type PublicRoomChatMessage = {
|
||||
messageId: string;
|
||||
timestamp: Date;
|
||||
type: ChatMessageType.PUBLIC_ROOM;
|
||||
sender: {
|
||||
address?: string;
|
||||
ecdh?: string;
|
||||
hash?: string;
|
||||
};
|
||||
rln?: RLNFullProof & {
|
||||
epoch: number;
|
||||
x_share: string;
|
||||
group_id: string;
|
||||
};
|
||||
receiver: {
|
||||
address?: string;
|
||||
ecdh?: string;
|
||||
roomId?: string;
|
||||
};
|
||||
content: string;
|
||||
reference: string;
|
||||
attachment: string;
|
||||
}
|
||||
|
||||
export type ChatMessage = DirectChatMessage | PublicRoomChatMessage;
|
||||
|
||||
export default class ChatService extends GenericService {
|
||||
|
||||
}
|
||||
53
lib/zk-chat-server/src/services/db.service.ts
Normal file
53
lib/zk-chat-server/src/services/db.service.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import {GenericService} from "../utils/svc";
|
||||
import {Dialect, Sequelize} from "sequelize";
|
||||
import config from "../utils/config";
|
||||
import chats from "../models/chat.model";
|
||||
import users from "../models/user.model";
|
||||
import shares from "../models/shares.model";
|
||||
|
||||
/**
|
||||
* Encapsulates the core functionality for managing user credentials as well as viewing the banned users.
|
||||
*/
|
||||
|
||||
class DBService extends GenericService {
|
||||
sequelize: Sequelize;
|
||||
chats?: ReturnType<typeof chats>;
|
||||
users?: ReturnType<typeof users>;
|
||||
shares?: ReturnType<typeof shares>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
if (!config.DB_DIALECT || config.DB_DIALECT === 'sqlite') {
|
||||
this.sequelize = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
storage: './zkchat.sqlite.db',
|
||||
logging: false,
|
||||
});
|
||||
} else {
|
||||
this.sequelize = new Sequelize(
|
||||
config.DB_NAME as string,
|
||||
config.DB_USERNAME as string,
|
||||
config.DB_PASSWORD,
|
||||
{
|
||||
host: config.DB_HOST,
|
||||
port: Number(config.DB_PORT),
|
||||
dialect: config.DB_DIALECT as Dialect,
|
||||
logging: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async start() {
|
||||
this.chats = await chats(this.sequelize);
|
||||
this.users = await users(this.sequelize);
|
||||
this.shares = await shares(this.sequelize);
|
||||
|
||||
await this.users.model.sync({ force: !!process.env.FORCE });
|
||||
await this.chats.model.sync({ force: !!process.env.FORCE });
|
||||
await this.shares.model.sync({ force: !!process.env.FORCE });
|
||||
}
|
||||
}
|
||||
|
||||
export default DBService
|
||||
7
lib/zk-chat-server/src/services/user.service.ts
Normal file
7
lib/zk-chat-server/src/services/user.service.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import {GenericService} from "../utils/svc";
|
||||
|
||||
export default class UserService extends GenericService {
|
||||
registerUser = async (walletAddress: string, ecdhPubKey: string): Promise<void> => {
|
||||
|
||||
}
|
||||
}
|
||||
93
lib/zk-chat-server/src/utils/chat.ts
Normal file
93
lib/zk-chat-server/src/utils/chat.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import {ChatMessage, ChatMessageType} from "../services/chat.service";
|
||||
import crypto from "crypto";
|
||||
import {RLNFullProof} from "@zk-kit/protocols";
|
||||
|
||||
export const validateChatMessage = async (chatMessage: any): Promise<boolean> => {
|
||||
assert(chatMessage.timestamp.getTime() > 0);
|
||||
assert(chatMessage.receiver);
|
||||
assert(chatMessage.messageId === await deriveMessageId(chatMessage));
|
||||
|
||||
if (!chatMessage.sender) {
|
||||
assert(chatMessage.rln);
|
||||
}
|
||||
|
||||
if (chatMessage.type === 'DIRECT') {
|
||||
if (!chatMessage.sender) {
|
||||
assert(chatMessage.pubkey);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (chatMessage.type === 'PUBLIC_ROOM') {
|
||||
assert(!chatMessage.ciphertext);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export const deriveMessageId = async (chatMessage: ChatMessage): Promise<string> => {
|
||||
let data = '';
|
||||
data += chatMessage.type;
|
||||
data += chatMessage.timestamp;
|
||||
data += chatMessage.sender;
|
||||
|
||||
if (chatMessage.rln) {
|
||||
data += JSON.stringify(chatMessage.rln)
|
||||
}
|
||||
|
||||
data += chatMessage.receiver;
|
||||
|
||||
if (chatMessage.type === 'DIRECT') {
|
||||
data += chatMessage.pubkey || '';
|
||||
data += chatMessage.ciphertext;
|
||||
}
|
||||
|
||||
if (chatMessage.type === 'PUBLIC_ROOM') {
|
||||
data += chatMessage.content;
|
||||
data += chatMessage.reference;
|
||||
data += chatMessage.attachment;
|
||||
}
|
||||
|
||||
return crypto.createHash('sha256').update(data).digest('hex');
|
||||
}
|
||||
|
||||
export const createChatMessage = async (opts: {
|
||||
type: 'DIRECT' | 'PUBLIC_ROOM',
|
||||
sender: string;
|
||||
timestamp?: Date;
|
||||
pubkey?: string;
|
||||
receiver: string;
|
||||
content?: string;
|
||||
reference?: string;
|
||||
attachment?: string;
|
||||
rln?: RLNFullProof & {
|
||||
epoch: number;
|
||||
x_share: string;
|
||||
};
|
||||
}): Promise<ChatMessage | null> => {
|
||||
let val: ChatMessage;
|
||||
|
||||
switch (opts.type) {
|
||||
case 'DIRECT':
|
||||
val = {
|
||||
messageId: '',
|
||||
type: ChatMessageType.DIRECT,
|
||||
timestamp: opts.timestamp || new Date(),
|
||||
sender: opts.sender,
|
||||
pubkey: opts.pubkey,
|
||||
receiver: opts.receiver,
|
||||
rln: opts.rln,
|
||||
ciphertext: '',
|
||||
};
|
||||
val.messageId = await deriveMessageId(val);
|
||||
return val;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function assert(data: any) {
|
||||
if (!data) throw new Error(`cannot be null`);
|
||||
}
|
||||
20
lib/zk-chat-server/src/utils/config.ts
Normal file
20
lib/zk-chat-server/src/utils/config.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import dotenv from "dotenv";
|
||||
|
||||
const {parsed} = dotenv.config({path: './lib/zk-chat-server/.env'});
|
||||
|
||||
export default {
|
||||
INTERREP_BASE_URL: parsed?.INTERREP_BASE_URL || "https://api.thegraph.com/subgraphs/name/interrep/kovan",
|
||||
INTERREP_V2: parsed?.INTERREP_V2 || "https://kovan.interep.link/api",
|
||||
SERVER_PORT: parseInt(parsed?.SERVER_PORT || "8080") || 8080,
|
||||
SPAM_TRESHOLD: parseInt(parsed?.SPAM_TRESHOLD || "2", 2) || 2,
|
||||
EPOCH_ALLOWED_DELAY_THRESHOLD: parseInt(parsed?.EPOCH_ALLOWED_DELAY_THRESHOLD || "20", 20) || 20,
|
||||
ZERO_VALUE: BigInt(0),
|
||||
RLN_IDENTIFIER: parseInt(parsed?.RLN_IDENTIFIER || "518137101") || 518137101,
|
||||
DELETE_MESSAGES_OLDER_THAN_DAYS: parseInt(parsed?.DELETE_MESSAGES_OLDER_THAN_DAYS || "5") || 5,
|
||||
DB_DIALECT: parsed?.DB_DIALECT || 'sqlite',
|
||||
DB_NAME: parsed?.DB_NAME || '',
|
||||
DB_USERNAME: parsed?.DB_USERNAME || '',
|
||||
DB_PASSWORD: parsed?.DB_PASSWORD || '',
|
||||
DB_HOST: parsed?.DB_HOST || 'localhost',
|
||||
DB_PORT: parsed?.DB_PORT || 5432,
|
||||
}
|
||||
2
lib/zk-chat-server/src/utils/db.ts
Normal file
2
lib/zk-chat-server/src/utils/db.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import config from "./config";
|
||||
|
||||
38
lib/zk-chat-server/src/utils/svc.ts
Normal file
38
lib/zk-chat-server/src/utils/svc.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import winston from "winston";
|
||||
const format = winston.format;
|
||||
const { combine, timestamp, prettyPrint } = format;
|
||||
|
||||
export const logger = winston.createLogger({
|
||||
level: 'info',
|
||||
format: combine(
|
||||
timestamp(),
|
||||
format.colorize(),
|
||||
format.json(),
|
||||
),
|
||||
transports: [
|
||||
new winston.transports.File({
|
||||
filename: 'error.log',
|
||||
level: 'error',
|
||||
}),
|
||||
new winston.transports.File({
|
||||
filename: 'combined.log',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
logger.add(new winston.transports.Console({
|
||||
level: 'info',
|
||||
format: winston.format.simple(),
|
||||
}));
|
||||
}
|
||||
|
||||
export class GenericService {
|
||||
async start() {
|
||||
|
||||
}
|
||||
|
||||
async stop() {
|
||||
|
||||
}
|
||||
}
|
||||
BIN
lib/zk-chat-server/statics/circuitFiles/rln/rln.wasm
Normal file
BIN
lib/zk-chat-server/statics/circuitFiles/rln/rln.wasm
Normal file
Binary file not shown.
BIN
lib/zk-chat-server/statics/circuitFiles/rln/rln_final.zkey
Normal file
BIN
lib/zk-chat-server/statics/circuitFiles/rln/rln_final.zkey
Normal file
Binary file not shown.
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"protocol": "groth16",
|
||||
"curve": "bn128",
|
||||
"nPublic": 6,
|
||||
"vk_alpha_1": [
|
||||
"20491192805390485299153009773594534940189261866228447918068658471970481763042",
|
||||
"9383485363053290200918347156157836566562967994039712273449902621266178545958",
|
||||
"1"
|
||||
],
|
||||
"vk_beta_2": [
|
||||
[
|
||||
"6375614351688725206403948262868962793625744043794305715222011528459656738731",
|
||||
"4252822878758300859123897981450591353533073413197771768651442665752259397132"
|
||||
],
|
||||
[
|
||||
"10505242626370262277552901082094356697409835680220590971873171140371331206856",
|
||||
"21847035105528745403288232691147584728191162732299865338377159692350059136679"
|
||||
],
|
||||
[
|
||||
"1",
|
||||
"0"
|
||||
]
|
||||
],
|
||||
"vk_gamma_2": [
|
||||
[
|
||||
"10857046999023057135944570762232829481370756359578518086990519993285655852781",
|
||||
"11559732032986387107991004021392285783925812861821192530917403151452391805634"
|
||||
],
|
||||
[
|
||||
"8495653923123431417604973247489272438418190587263600148770280649306958101930",
|
||||
"4082367875863433681332203403145435568316851327593401208105741076214120093531"
|
||||
],
|
||||
[
|
||||
"1",
|
||||
"0"
|
||||
]
|
||||
],
|
||||
"vk_delta_2": [
|
||||
[
|
||||
"14962742607348761745067039667743739542918587958944430293802039673540545491766",
|
||||
"19420690459571091434654770613940591470063366130537730965999231781890983822228"
|
||||
],
|
||||
[
|
||||
"8025490964181160791180478620151567079603098158752936953068367103529675811355",
|
||||
"16899630578734283128344486213072467840624492210479261395286682179061749476405"
|
||||
],
|
||||
[
|
||||
"1",
|
||||
"0"
|
||||
]
|
||||
],
|
||||
"vk_alphabeta_12": [
|
||||
[
|
||||
[
|
||||
"2029413683389138792403550203267699914886160938906632433982220835551125967885",
|
||||
"21072700047562757817161031222997517981543347628379360635925549008442030252106"
|
||||
],
|
||||
[
|
||||
"5940354580057074848093997050200682056184807770593307860589430076672439820312",
|
||||
"12156638873931618554171829126792193045421052652279363021382169897324752428276"
|
||||
],
|
||||
[
|
||||
"7898200236362823042373859371574133993780991612861777490112507062703164551277",
|
||||
"7074218545237549455313236346927434013100842096812539264420499035217050630853"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"7077479683546002997211712695946002074877511277312570035766170199895071832130",
|
||||
"10093483419865920389913245021038182291233451549023025229112148274109565435465"
|
||||
],
|
||||
[
|
||||
"4595479056700221319381530156280926371456704509942304414423590385166031118820",
|
||||
"19831328484489333784475432780421641293929726139240675179672856274388269393268"
|
||||
],
|
||||
[
|
||||
"11934129596455521040620786944827826205713621633706285934057045369193958244500",
|
||||
"8037395052364110730298837004334506829870972346962140206007064471173334027475"
|
||||
]
|
||||
]
|
||||
],
|
||||
"IC": [
|
||||
[
|
||||
"1903896611309112537898744888962900116850557104688140496996114549467905845389",
|
||||
"18878807081439367511400607605971457252753640659171839147843428814578113606944",
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"3379380938593207080729160389717513882233130462616390491862768147575356927916",
|
||||
"7658366061956613070318477742617874370870162905631795036979866045784951077741",
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"6573257371221466457264319919959056878987621722065494829982803761054687125969",
|
||||
"10882724634177179759524298899662307047578489073607305659656545291770678864358",
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"7812315588577187126949469943175416873443639588836013906528191759531184751241",
|
||||
"11928578526804134334949431821595151892366575912030023302327660047097849106833",
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"8603322753346553805166809285487650902416020947317326006612301010819108432803",
|
||||
"3100089596497060388197556426101480177422659517238854788949908444987414217355",
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"548584601681146753562700521279324517960520272202999011217745775929880443671",
|
||||
"10775304696187723066725699321032508342393012304409445749847420882128296879500",
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"4832346778469239752999589970014962368162544386191392542939015835265757824362",
|
||||
"17497085844864771075682226779903142040218959461493641885155366729946818944068",
|
||||
"1"
|
||||
]
|
||||
]
|
||||
}
|
||||
2164
package-lock.json
generated
2164
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -17,41 +17,49 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@ensdomains/ensjs": "^2.0.1",
|
||||
"@interep/reputation": "^0.1.0",
|
||||
"@interep/reputation": "^0.4.0",
|
||||
"@zk-kit/identity": "^1.4.1",
|
||||
"@zk-kit/protocols": "^1.8.2",
|
||||
"@zk-kit/protocols": "^1.11.1",
|
||||
"async-mutex": "^0.3.1",
|
||||
"bn.js": "^5.2.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"botometer": "^1.0.12",
|
||||
"connect-session-sequelize": "^7.1.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.0.1",
|
||||
"elliptic": "^6.5.4",
|
||||
"ethereumjs-util": "^7.1.2",
|
||||
"express": "^4.17.1",
|
||||
"express-fileupload": "^1.4.0",
|
||||
"express-session": "^1.17.2",
|
||||
"gun": "^0.2020.1232",
|
||||
"http-terminator": "^3.2.0",
|
||||
"ipfs-http-client": "56.x",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"link-preview-js": "^2.1.8",
|
||||
"link-preview-node": "^1.0.7",
|
||||
"lru-cache": "^6.0.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"oauth-1.0a": "^2.2.6",
|
||||
"pg": "^8.7.1",
|
||||
"semaphore-lib": "git+https://github.com/akinovak/semaphore-lib.git#dev",
|
||||
"sequelize": "^6.6.2",
|
||||
"sqlite3": "^5.0.2",
|
||||
"web3": "^1.3.5",
|
||||
"winston": "^3.3.3"
|
||||
"web3.storage": "^4.3.0",
|
||||
"winston": "^3.3.3",
|
||||
"zk-chat-server": "^1.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
||||
"@types/cors": "^2.8.10",
|
||||
"@types/elliptic": "^6.4.14",
|
||||
"@types/express": "^4.17.11",
|
||||
"@types/express-fileupload": "^1.2.2",
|
||||
"@types/express-session": "^1.17.4",
|
||||
"@types/jsonwebtoken": "^8.5.6",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^14.14.37",
|
||||
"@types/sinon": "^10.0.11",
|
||||
"@types/tape": "^4.13.2",
|
||||
|
||||
@@ -7,15 +7,21 @@ import HttpService from "./services/http";
|
||||
import GunService from "./services/gun";
|
||||
import InterrepService from "./services/interrep";
|
||||
import ArbitrumService from "./services/arbitrum";
|
||||
import IPFSService from "./services/ipfs";
|
||||
import ZKChatService from "./services/zkchat";
|
||||
import MerkleService from "./services/merkle";
|
||||
|
||||
(async function initApp() {
|
||||
try {
|
||||
const main = new MainService();
|
||||
main.add('db', new DBService());
|
||||
main.add('merkle', new MerkleService());
|
||||
main.add('interrep', new InterrepService());
|
||||
main.add('zkchat', new ZKChatService());
|
||||
main.add('ens', new ENSService());
|
||||
main.add('arbitrum', new ArbitrumService());
|
||||
main.add('gun', new GunService());
|
||||
main.add('ipfs', new IPFSService());
|
||||
main.add('http', new HttpService());
|
||||
await main.start();
|
||||
} catch (e) {
|
||||
|
||||
@@ -25,9 +25,10 @@ const interepGroups = (sequelize: Sequelize) => {
|
||||
},
|
||||
}, {
|
||||
indexes: [
|
||||
{ fields: ['root_hash'], unique: true },
|
||||
{ fields: ['root_hash'] },
|
||||
{ fields: ['provider'] },
|
||||
{ fields: ['name'] },
|
||||
{ fields: ['provider', 'name'], unique: true },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -45,7 +46,6 @@ const interepGroups = (sequelize: Sequelize) => {
|
||||
return mutex.runExclusive(async () => {
|
||||
const result = await model.findOne({
|
||||
where: {
|
||||
root_hash,
|
||||
provider,
|
||||
name,
|
||||
},
|
||||
@@ -57,14 +57,32 @@ const interepGroups = (sequelize: Sequelize) => {
|
||||
provider,
|
||||
name,
|
||||
});
|
||||
} else {
|
||||
return result.update({
|
||||
root_hash,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const getGroup = async (provider: string, name: string): Promise<InterepGroupModel> => {
|
||||
return mutex.runExclusive(async () => {
|
||||
const result = await model.findOne({
|
||||
where: {
|
||||
provider,
|
||||
name,
|
||||
},
|
||||
});
|
||||
|
||||
return result?.toJSON() as InterepGroupModel;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
model,
|
||||
findOneByHash,
|
||||
addHash,
|
||||
getGroup,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
56
src/models/merkle_root.ts
Normal file
56
src/models/merkle_root.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import {Sequelize, BIGINT, STRING} from "sequelize";
|
||||
|
||||
type MerkleRootModel = {
|
||||
root_hash: string;
|
||||
group_id: string;
|
||||
};
|
||||
|
||||
const merkleRoot = (sequelize: Sequelize) => {
|
||||
const model = sequelize.define('merkle_root', {
|
||||
root_hash: {
|
||||
type: STRING,
|
||||
},
|
||||
group_id: {
|
||||
type: STRING,
|
||||
},
|
||||
}, {
|
||||
indexes: [
|
||||
{ fields: ['root_hash'], unique: true },
|
||||
{ fields: ['group_id'] },
|
||||
],
|
||||
});
|
||||
|
||||
const getGroupByRoot = async (root_hash: string): Promise<MerkleRootModel> => {
|
||||
let result = await model.findOne({
|
||||
where: {
|
||||
root_hash,
|
||||
},
|
||||
});
|
||||
|
||||
return result?.toJSON() as MerkleRootModel;
|
||||
}
|
||||
|
||||
const addRoot = async (root_hash: string, group_id: string) => {
|
||||
const exist = await model.findOne({
|
||||
where: {
|
||||
root_hash,
|
||||
},
|
||||
});
|
||||
|
||||
if (!exist) {
|
||||
return model.create({
|
||||
root_hash,
|
||||
group_id,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
model,
|
||||
addRoot,
|
||||
getGroupByRoot,
|
||||
};
|
||||
};
|
||||
|
||||
export default merkleRoot
|
||||
@@ -3,8 +3,8 @@ import {Mutex} from "async-mutex";
|
||||
|
||||
type SemaphoreModel = {
|
||||
id_commitment: string;
|
||||
provider: string;
|
||||
name: string;
|
||||
group_id: string;
|
||||
root_hash: string;
|
||||
};
|
||||
|
||||
const mutex = new Mutex();
|
||||
@@ -15,19 +15,19 @@ const semaphore = (sequelize: Sequelize) => {
|
||||
type: STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
provider: {
|
||||
group_id: {
|
||||
type: STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
name: {
|
||||
root_hash: {
|
||||
type: STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
}, {
|
||||
indexes: [
|
||||
{ fields: ['id_commitment'] },
|
||||
{ fields: ['provider'] },
|
||||
{ fields: ['name'] },
|
||||
{ fields: ['group_id'] },
|
||||
{ fields: ['root_hash'] },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -54,33 +54,34 @@ const semaphore = (sequelize: Sequelize) => {
|
||||
return result.map(r => r.toJSON()) as SemaphoreModel[];
|
||||
}
|
||||
|
||||
const addID = async (id_commitment: string, provider: string, name: string) => {
|
||||
const addID = async (id_commitment: string, group_id: string, root_hash: string) => {
|
||||
return mutex.runExclusive(async () => {
|
||||
const result = await model.findOne({
|
||||
where: {
|
||||
id_commitment,
|
||||
provider,
|
||||
name,
|
||||
group_id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return model.create({
|
||||
id_commitment,
|
||||
provider,
|
||||
name,
|
||||
group_id,
|
||||
root_hash,
|
||||
});
|
||||
} else {
|
||||
return result.update({ root_hash });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const removeID = async (id_commitment: string, provider: string, name: string) => {
|
||||
const removeID = async (id_commitment: string, group_id: string, root_hash: string) => {
|
||||
return mutex.runExclusive(async () => {
|
||||
return model.destroy({
|
||||
where: {
|
||||
id_commitment,
|
||||
provider,
|
||||
name,
|
||||
group_id,
|
||||
root_hash,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
83
src/models/uploads.ts
Normal file
83
src/models/uploads.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import {Mutex} from "async-mutex";
|
||||
import {BIGINT, ENUM, QueryTypes, Sequelize, STRING} from "sequelize";
|
||||
|
||||
export type UploadModel = {
|
||||
cid: string;
|
||||
filename: string;
|
||||
username: string;
|
||||
size: number;
|
||||
mimetype: string;
|
||||
};
|
||||
|
||||
const mutex = new Mutex();
|
||||
|
||||
const uploads = (sequelize: Sequelize) => {
|
||||
const model = sequelize.define('uploads', {
|
||||
cid: {
|
||||
type: STRING,
|
||||
},
|
||||
filename: {
|
||||
type: STRING,
|
||||
},
|
||||
username: {
|
||||
type: STRING,
|
||||
},
|
||||
size: {
|
||||
type: BIGINT,
|
||||
},
|
||||
mimetype: {
|
||||
type: STRING,
|
||||
},
|
||||
}, {
|
||||
indexes: [
|
||||
{ fields: ['cid'] },
|
||||
{ fields: ['filename'] },
|
||||
{ fields: ['username'] },
|
||||
{ fields: ['mimetype'] },
|
||||
],
|
||||
});
|
||||
|
||||
const addUploadData = async (data: UploadModel) => {
|
||||
return mutex.runExclusive(async () => {
|
||||
const res = await model.create(data);
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
const removeUploadData = async (cid: string, filename: string) => {
|
||||
return mutex.runExclusive(async () => {
|
||||
try {
|
||||
const res = await model.destroy({
|
||||
where: {
|
||||
cid,
|
||||
filename,
|
||||
},
|
||||
});
|
||||
return res;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const getTotalUploadByUser = async (username: string) => {
|
||||
const res = await model.sum('size', {
|
||||
where: {
|
||||
username,
|
||||
},
|
||||
});
|
||||
|
||||
if (isNaN(res)) return 0;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
return {
|
||||
model,
|
||||
addUploadData,
|
||||
removeUploadData,
|
||||
getTotalUploadByUser,
|
||||
};
|
||||
}
|
||||
|
||||
export default uploads;
|
||||
@@ -214,7 +214,9 @@ const userSelectQuery = `
|
||||
"coverImage".value as "coverImage",
|
||||
"twitterVerification".value as "tweetId",
|
||||
"twitterVerification".key as "twitterHandle",
|
||||
website.value as website
|
||||
website.value as website,
|
||||
ecdh.value as ecdh,
|
||||
idcommitment.value as idcommitment
|
||||
FROM users u
|
||||
LEFT JOIN usermeta umt ON umt.name = u.name
|
||||
LEFT JOIN connections f ON f.subtype = 'FOLLOW' AND f.creator = :context AND f.name = u.name
|
||||
@@ -225,6 +227,8 @@ const userSelectQuery = `
|
||||
LEFT JOIN profiles "coverImage" ON "coverImage"."messageId" = (SELECT "messageId" FROM profiles WHERE creator = u.name AND subtype = 'COVER_IMAGE' ORDER BY "createdAt" DESC LIMIT 1)
|
||||
LEFT JOIN profiles "twitterVerification" ON "twitterVerification"."messageId" = (SELECT "messageId" FROM profiles WHERE creator = u.name AND subtype = 'TWT_VERIFICATION' ORDER BY "createdAt" DESC LIMIT 1)
|
||||
LEFT JOIN profiles website ON website."messageId" = (SELECT "messageId" FROM profiles WHERE creator = u.name AND subtype = 'WEBSITE' ORDER BY "createdAt" DESC LIMIT 1)
|
||||
LEFT JOIN profiles ecdh ON ecdh."messageId" = (SELECT "messageId" FROM profiles WHERE creator = u.name AND subtype = 'CUSTOM' AND key='ecdh_pubkey' ORDER BY "createdAt" DESC LIMIT 1)
|
||||
LEFT JOIN profiles idcommitment ON idcommitment."messageId" = (SELECT "messageId" FROM profiles WHERE creator = u.name AND subtype = 'CUSTOM' AND key='id_commitment' ORDER BY "createdAt" DESC LIMIT 1)
|
||||
`;
|
||||
|
||||
function inflateValuesToUserJSON(values: any[]): UserModel[] {
|
||||
@@ -249,6 +253,8 @@ function inflateValuesToUserJSON(values: any[]): UserModel[] {
|
||||
coverImage: value.coverImage || '',
|
||||
twitterVerification: twitterVerification,
|
||||
website: value.website || '',
|
||||
ecdh: value.ecdh || '',
|
||||
idcommitment: value.idcommitment || '',
|
||||
meta: {
|
||||
blockedCount: value.blockedCount ? Number(value.blockedCount) : 0,
|
||||
blockingCount: value.blockingCount ? Number(value.blockingCount) : 0,
|
||||
|
||||
@@ -18,6 +18,8 @@ import twitterAuth from "../../models/twitterAuth";
|
||||
import interepGroups from "../../models/interepGroups";
|
||||
import semaphoreCreators from "../../models/semaphore_creators";
|
||||
import threads from "../../models/thread";
|
||||
import uploads from "../../models/uploads";
|
||||
import merkleRoot from "../../models/merkle_root";
|
||||
|
||||
export default class DBService extends GenericService {
|
||||
sequelize: Sequelize;
|
||||
@@ -40,6 +42,8 @@ export default class DBService extends GenericService {
|
||||
interepGroups?: ReturnType<typeof interepGroups>;
|
||||
semaphoreCreators?: ReturnType<typeof semaphoreCreators>;
|
||||
threads?: ReturnType<typeof threads>;
|
||||
uploads?: ReturnType<typeof uploads>;
|
||||
merkleRoot?: ReturnType<typeof merkleRoot>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -195,6 +199,13 @@ export default class DBService extends GenericService {
|
||||
return this.threads;
|
||||
}
|
||||
|
||||
async getUploads(): Promise<ReturnType<typeof uploads>> {
|
||||
if (!this.uploads) {
|
||||
return Promise.reject(new Error('uploads is not initialized'));
|
||||
}
|
||||
return this.uploads;
|
||||
}
|
||||
|
||||
async start() {
|
||||
this.app = await app(this.sqlite);
|
||||
this.records = await records(this.sqlite);
|
||||
@@ -213,6 +224,8 @@ export default class DBService extends GenericService {
|
||||
this.interepGroups = await interepGroups(this.sequelize);
|
||||
this.semaphoreCreators = await semaphoreCreators(this.sequelize);
|
||||
this.threads = await threads(this.sequelize);
|
||||
this.uploads = await uploads(this.sequelize);
|
||||
this.merkleRoot = await merkleRoot(this.sequelize);
|
||||
|
||||
await this.app?.model.sync({ force: !!process.env.FORCE });
|
||||
await this.linkPreview?.model.sync({ force: !!process.env.FORCE });
|
||||
@@ -234,6 +247,8 @@ export default class DBService extends GenericService {
|
||||
await this.interepGroups?.model.sync({ force: !!process.env.FORCE });
|
||||
await this.semaphoreCreators?.model.sync({ force: !!process.env.FORCE });
|
||||
await this.threads?.model.sync({ force: !!process.env.FORCE });
|
||||
await this.uploads?.model.sync({ force: !!process.env.FORCE });
|
||||
await this.merkleRoot?.model.sync({ force: !!process.env.FORCE });
|
||||
|
||||
const appData = await this.app?.read();
|
||||
|
||||
|
||||
@@ -28,8 +28,6 @@ const getMutex = new Mutex();
|
||||
const putMutex = new Mutex();
|
||||
const insertMutex = new Mutex();
|
||||
|
||||
import { Semaphore, genExternalNullifier, genSignalHash } from "@zk-kit/protocols";
|
||||
|
||||
export default class GunService extends GenericService {
|
||||
gun?: IGunChainReference;
|
||||
|
||||
@@ -114,7 +112,15 @@ export default class GunService extends GenericService {
|
||||
attachment: payload.attachment,
|
||||
},
|
||||
});
|
||||
await this.insertPost(post, data.proof, data.publicSignals);
|
||||
await this.insertPost(
|
||||
post,
|
||||
!data.proof ? undefined : {
|
||||
proof: data.proof,
|
||||
publicSignals: data.publicSignals,
|
||||
x_share: data.x_share,
|
||||
epoch: data.epoch,
|
||||
},
|
||||
);
|
||||
return;
|
||||
case MessageType.Moderation:
|
||||
const moderation = new Moderation({
|
||||
@@ -236,7 +242,12 @@ export default class GunService extends GenericService {
|
||||
|
||||
}
|
||||
|
||||
async insertPost(post: Post, proof?: string, signals?: string) {
|
||||
async insertPost(post: Post, data?: {
|
||||
proof: string;
|
||||
publicSignals: string;
|
||||
x_share: string;
|
||||
epoch: string;
|
||||
}) {
|
||||
const json = await post.toJSON();
|
||||
const {
|
||||
type,
|
||||
@@ -269,46 +280,51 @@ export default class GunService extends GenericService {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!creator && (!proof || !signals)) {
|
||||
if (!creator && !data) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (proof && signals) {
|
||||
const parsedProof = JSON.parse(proof);
|
||||
const parsedSignals = JSON.parse(signals);
|
||||
const externalNullifier = await genExternalNullifier('POST');
|
||||
const signalHash = await genSignalHash(hash);
|
||||
if (data) {
|
||||
const proof = JSON.parse(data.proof);
|
||||
const publicSignals = JSON.parse(data.publicSignals);
|
||||
const verified = await this.call('zkchat', 'verifyRLNProof', {
|
||||
proof,
|
||||
publicSignals,
|
||||
x_share: data.x_share,
|
||||
epoch: data.epoch,
|
||||
});
|
||||
|
||||
if (BigInt(externalNullifier).toString() !== parsedSignals.externalNullifier) return;
|
||||
if (signalHash.toString() !== parsedSignals.signalHash) return;
|
||||
const share = {
|
||||
nullifier: publicSignals.internalNullifier,
|
||||
epoch: publicSignals.epoch,
|
||||
y_share: publicSignals.yShare,
|
||||
x_share: data.x_share,
|
||||
};
|
||||
|
||||
const hashData = await this.call('interrep', 'getBatchFromRootHash', parsedSignals.merkleRoot);
|
||||
const {
|
||||
shares,
|
||||
isSpam,
|
||||
isDuplicate,
|
||||
} = await this.call('zkchat', 'checkShare', share);
|
||||
|
||||
if (!hashData) {
|
||||
return;
|
||||
}
|
||||
const group = await this.call(
|
||||
'merkle',
|
||||
'getGroupByRoot',
|
||||
'0x' + BigInt(publicSignals.merkleRoot).toString(16),
|
||||
);
|
||||
|
||||
const res = await Semaphore.verifyProof(
|
||||
vKey as any,
|
||||
{
|
||||
proof: parsedProof,
|
||||
publicSignals: parsedSignals,
|
||||
});
|
||||
if (isSpam || isDuplicate || !verified || !group) return;
|
||||
|
||||
if (!res) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { name, provider } = hashData;
|
||||
await semaphoreCreatorsDB.addSemaphoreCreator(messageId, provider, name);
|
||||
const [protocol, groupName, groupType] = group.split('_')
|
||||
await semaphoreCreatorsDB.addSemaphoreCreator(messageId, groupName, groupType);
|
||||
}
|
||||
|
||||
await postDB.createPost({
|
||||
messageId: messageId,
|
||||
hash: hash,
|
||||
proof,
|
||||
signals,
|
||||
proof: data?.proof,
|
||||
signals: data?.publicSignals,
|
||||
type: type,
|
||||
subtype: subtype,
|
||||
creator: creator || '',
|
||||
@@ -537,6 +553,10 @@ export default class GunService extends GenericService {
|
||||
return;
|
||||
}
|
||||
|
||||
if (subtype === ProfileMessageSubType.Custom && payload.key === 'ecdh_pubkey') {
|
||||
await this.call('zkchat', 'registerUser', creator, payload.value);
|
||||
}
|
||||
|
||||
if (subtype === ProfileMessageSubType.TwitterVerification) {
|
||||
const { key, value } = payload;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {GenericService} from "../util/svc";
|
||||
import express, {Express, Request, Response} from "express";
|
||||
import express, {Express, NextFunction, Request, Response} from "express";
|
||||
import bodyParser from "body-parser";
|
||||
import cors, {CorsOptions} from "cors";
|
||||
import http from 'http';
|
||||
@@ -12,7 +12,7 @@ import { getLinkPreview } from "link-preview-js";
|
||||
import queryString from "querystring";
|
||||
import session from 'express-session';
|
||||
import jwt from "jsonwebtoken";
|
||||
import {Dialect, Sequelize} from "sequelize";
|
||||
import {Dialect, QueryTypes, Sequelize} from "sequelize";
|
||||
const SequelizeStore = require("connect-session-sequelize")(session.Store);
|
||||
import { calculateReputation, OAuthProvider } from "@interep/reputation";
|
||||
import {
|
||||
@@ -27,6 +27,19 @@ import {
|
||||
} from "../util/twitter";
|
||||
import {verifySignatureP256} from "../util/crypto";
|
||||
import {parseMessageId, PostMessageSubType} from "../util/message";
|
||||
import multer from 'multer';
|
||||
const upload = multer({
|
||||
dest: './uploaded_files',
|
||||
});
|
||||
import fs from 'fs';
|
||||
import { getFilesFromPath } from 'web3.storage';
|
||||
import {UploadModel} from "../models/uploads";
|
||||
import {genExternalNullifier, RLNFullProof, Semaphore, SemaphoreFullProof} from "@zk-kit/protocols";
|
||||
import vKey from "../../static/verification_key.json";
|
||||
import merkleRoot from "../models/merkle_root";
|
||||
import {sequelize} from "../util/sequelize";
|
||||
import crypto from 'crypto';
|
||||
import {addConnection, addTopic, keepAlive, publishTopic, removeConnection, SSEType} from "../util/sse";
|
||||
|
||||
const corsOptions: CorsOptions = {
|
||||
credentials: true,
|
||||
@@ -36,6 +49,9 @@ const corsOptions: CorsOptions = {
|
||||
};
|
||||
|
||||
const JWT_SECRET = config.jwtSecret;
|
||||
const ONE_MB = 1048576;
|
||||
const maxFileSize = ONE_MB * 5;
|
||||
const maxPerUserSize = ONE_MB * 100;
|
||||
|
||||
function makeResponse(payload: any, error?: boolean) {
|
||||
return {
|
||||
@@ -47,13 +63,96 @@ function makeResponse(payload: any, error?: boolean) {
|
||||
export default class HttpService extends GenericService {
|
||||
app: Express;
|
||||
httpServer: any;
|
||||
merkleRoot?: ReturnType<typeof merkleRoot>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.app = express();
|
||||
}
|
||||
|
||||
wrapHandler(handler: (req: Request, res: Response) => Promise<void>) {
|
||||
verifyAuth = (
|
||||
getExternalNullifer: (req: Request) => string | Promise<string>,
|
||||
getSignal: (req: Request) => string | Promise<string>,
|
||||
onError?: (req: Request) => void | Promise<void>,
|
||||
) => async (req: Request, res: Response, next: NextFunction) => {
|
||||
const signature = req.header('X-SIGNED-ADDRESS');
|
||||
const semaphoreProof = req.header('X-SEMAPHORE-PROOF');
|
||||
const rlnProof = req.header('X-RLN-PROOF');
|
||||
const userDB = await this.call('db', 'getUsers');
|
||||
|
||||
if (signature) {
|
||||
const params = signature.split('.');
|
||||
const user = await userDB.findOneByName(params[1]);
|
||||
if (!user || !verifySignatureP256(user.pubkey, params[1], params[0])) {
|
||||
res.status(403).send(makeResponse('user must be authenticated', true));
|
||||
if (onError) onError(req);
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
req.username = params[1];
|
||||
} else if (semaphoreProof) {
|
||||
const { proof, publicSignals } = JSON.parse(semaphoreProof) as SemaphoreFullProof;
|
||||
const externalNullifier = await genExternalNullifier(await getExternalNullifer(req));
|
||||
const signalHash = await Semaphore.genSignalHash(await getSignal(req));
|
||||
const matchNullifier = BigInt(externalNullifier).toString() === publicSignals.externalNullifier;
|
||||
const matchSignal = signalHash.toString() === publicSignals.signalHash;
|
||||
const hashData = await this.call(
|
||||
'interrep',
|
||||
'getBatchFromRootHash',
|
||||
publicSignals.merkleRoot
|
||||
);
|
||||
const verified = await Semaphore.verifyProof(
|
||||
vKey as any,
|
||||
{
|
||||
proof,
|
||||
publicSignals,
|
||||
},
|
||||
);
|
||||
|
||||
if (!matchNullifier || !matchSignal || !verified || !hashData) {
|
||||
res.status(403).send(makeResponse('invalid semaphore proof', true));
|
||||
if (onError) onError(req);
|
||||
return;
|
||||
}
|
||||
|
||||
} else if (rlnProof) {
|
||||
const { proof, publicSignals, x_share, epoch } = JSON.parse(rlnProof);
|
||||
const verified = await this.call('zkchat', 'verifyRLNProof', {
|
||||
proof,
|
||||
publicSignals,
|
||||
x_share: x_share,
|
||||
epoch: epoch,
|
||||
});
|
||||
const share = {
|
||||
nullifier: publicSignals.internalNullifier,
|
||||
epoch: publicSignals.epoch,
|
||||
y_share: publicSignals.yShare,
|
||||
x_share: x_share,
|
||||
};
|
||||
|
||||
const {
|
||||
shares,
|
||||
isSpam,
|
||||
isDuplicate,
|
||||
} = await this.call('zkchat', 'checkShare', share);
|
||||
|
||||
const group = await this.call(
|
||||
'merkle',
|
||||
'getGroupByRoot',
|
||||
'0x' + BigInt(publicSignals.merkleRoot).toString(16),
|
||||
);
|
||||
|
||||
if (isSpam || isDuplicate || !verified || !group) {
|
||||
res.status(403).send(makeResponse('invalid semaphore proof', true));
|
||||
if (onError) onError(req);
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
wrapHandler(handler: (req: Request, res: Response) => Promise<any>) {
|
||||
return async (req: Request, res: Response) => {
|
||||
logger.info('received request', {
|
||||
url: req.url,
|
||||
@@ -272,6 +371,214 @@ export default class HttpService extends GenericService {
|
||||
res.send(makeResponse(post));
|
||||
};
|
||||
|
||||
handleGetChatUsers = async (req: Request, res: Response) => {
|
||||
const limit = req.query.limit && Number(req.query.limit);
|
||||
const offset = req.query.offset && Number(req.query.offset);
|
||||
const users = await this.call('zkchat', 'getAllUsers', offset, limit);
|
||||
res.send(makeResponse(users));
|
||||
};
|
||||
|
||||
handlePostChatMessage = async (req: Request, res: Response) => {
|
||||
const {
|
||||
messageId,
|
||||
type,
|
||||
timestamp,
|
||||
sender,
|
||||
receiver,
|
||||
ciphertext,
|
||||
rln,
|
||||
} = req.body;
|
||||
const signature = req.header('X-SIGNED-ADDRESS');
|
||||
const userDB = await this.call('db', 'getUsers');
|
||||
|
||||
if (!sender.address && (!sender.hash && !sender.ecdh)) throw new Error('invalid sender');
|
||||
if (!receiver.address && !receiver.ecdh) throw new Error('invalid receiver');
|
||||
|
||||
if (rln) {
|
||||
if (!sender.hash) throw new Error('invalid request object');
|
||||
|
||||
const isEpochCurrent = await this.call('zkchat', 'isEpochCurrent', rln.epoch);
|
||||
const verified = await this.call('zkchat', 'verifyRLNProof', rln);
|
||||
const root = '0x' + BigInt(rln.publicSignals.merkleRoot).toString(16);
|
||||
const group = await this.call(
|
||||
'merkle',
|
||||
'getGroupByRoot',
|
||||
root,
|
||||
);
|
||||
|
||||
if (!isEpochCurrent) throw new Error('outdated message');
|
||||
if (!verified) throw new Error('invalid rln proof');
|
||||
if (!group) throw new Error('invalid merkle root');
|
||||
|
||||
rln.group_id = group;
|
||||
|
||||
const share = {
|
||||
nullifier: rln.publicSignals.internalNullifier,
|
||||
epoch: rln.publicSignals.epoch,
|
||||
y_share: rln.publicSignals.yShare,
|
||||
x_share: rln.x_share,
|
||||
};
|
||||
|
||||
const {
|
||||
shares,
|
||||
isSpam,
|
||||
isDuplicate,
|
||||
} = await this.call('zkchat', 'checkShare', share);
|
||||
|
||||
if (isDuplicate) {
|
||||
throw new Error('duplicate message');
|
||||
}
|
||||
|
||||
if (isSpam) {
|
||||
res.status(429).send('too many requests');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.call('zkchat', 'insertShare', share);
|
||||
await this.merkleRoot?.addRoot(root, group);
|
||||
} else if (signature) {
|
||||
const [sig, address] = signature.split('.');
|
||||
const user = await userDB.findOneByName(address);
|
||||
|
||||
if (user?.pubkey) {
|
||||
if (!verifySignatureP256(user.pubkey, address, sig)) {
|
||||
res.status(403).send(makeResponse('unauthorized', true));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
res.status(403).send(makeResponse('unauthorized', true));
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await this.call('zkchat', 'addChatMessage', {
|
||||
messageId,
|
||||
type,
|
||||
timestamp: new Date(timestamp),
|
||||
sender,
|
||||
receiver,
|
||||
ciphertext,
|
||||
rln,
|
||||
});
|
||||
|
||||
publishTopic(`ecdh:${data.sender_pubkey}`, {
|
||||
type: SSEType.NEW_CHAT_MESSAGE,
|
||||
message: data,
|
||||
});
|
||||
publishTopic(`ecdh:${data.receiver_pubkey}`, {
|
||||
type: SSEType.NEW_CHAT_MESSAGE,
|
||||
message: data,
|
||||
});
|
||||
res.send(makeResponse(data));
|
||||
}
|
||||
|
||||
handleGetDirectMessage = async (req: Request, res: Response) => {
|
||||
const {sender, receiver} = req.params;
|
||||
const limit = req.query.limit && Number(req.query.limit);
|
||||
const offset = req.query.offset && Number(req.query.offset);
|
||||
const data = await this.call(
|
||||
'zkchat',
|
||||
'getDirectMessages',
|
||||
sender,
|
||||
receiver,
|
||||
offset,
|
||||
limit,
|
||||
);
|
||||
res.send(makeResponse(data));
|
||||
}
|
||||
|
||||
handleGetDirectChats = async (req: Request, res: Response) => {
|
||||
const {pubkey} = req.params;
|
||||
|
||||
const values = await sequelize.query(`
|
||||
SELECT distinct zkc.receiver_pubkey as pubkey, zkc.receiver_address as address, null as group_id
|
||||
FROM zkchat_chats zkc
|
||||
WHERE zkc.receiver_pubkey IN (
|
||||
SELECT distinct receiver_pubkey FROM zkchat_chats WHERE sender_pubkey = :pubkey
|
||||
)
|
||||
UNION
|
||||
SELECT distinct zkc.sender_pubkey as pubkey, zkc.sender_address as address, mr.group_id
|
||||
FROM zkchat_chats zkc
|
||||
LEFT JOIN merkle_roots mr on mr.root_hash = zkc.rln_root
|
||||
WHERE zkc.sender_pubkey IN (
|
||||
SELECT distinct sender_pubkey FROM zkchat_chats WHERE receiver_pubkey = :pubkey
|
||||
);
|
||||
`, {
|
||||
type: QueryTypes.SELECT,
|
||||
replacements: {
|
||||
pubkey,
|
||||
},
|
||||
});
|
||||
|
||||
const data = values.map((val: any) => ({
|
||||
type: 'DIRECT',
|
||||
receiver: val.address,
|
||||
receiverECDH: val.pubkey,
|
||||
senderECDH: pubkey,
|
||||
group: val.group_id,
|
||||
}));
|
||||
|
||||
res.send(makeResponse(data));
|
||||
}
|
||||
|
||||
handleSearchChats = async (req: Request, res: Response) => {
|
||||
const {query} = req.params;
|
||||
const {sender} = req.query;
|
||||
const data = await this.call('zkchat', 'searchChats', query || '', sender);
|
||||
res.send(makeResponse(data));
|
||||
}
|
||||
|
||||
handleGetProofs = async (req: Request, res: Response) => {
|
||||
const {idCommitment} = req.params;
|
||||
const {group = ''} = req.query;
|
||||
|
||||
const proof = await this.call('merkle', 'findProof', group, idCommitment);
|
||||
|
||||
res.send(makeResponse({
|
||||
data: proof,
|
||||
group: group,
|
||||
}));
|
||||
}
|
||||
|
||||
handleGetEvents = async (req: Request, res: Response) => {
|
||||
const headers = {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Connection': 'keep-alive',
|
||||
'Cache-Control': 'no-cache',
|
||||
};
|
||||
|
||||
res.writeHead(200, headers);
|
||||
|
||||
const clientId = crypto.randomBytes(16).toString('hex');
|
||||
|
||||
addConnection(clientId, res);
|
||||
}
|
||||
|
||||
handleUpdateSSEClient = async (req: Request, res: Response) => {
|
||||
const {clientId} = req.params;
|
||||
const { topics } = req.body;
|
||||
|
||||
for (const topic of topics) {
|
||||
addTopic(clientId, topic);
|
||||
}
|
||||
|
||||
res.send(makeResponse('ok'));
|
||||
|
||||
}
|
||||
|
||||
handleSSEKeepAlive = async (req: Request, res: Response) => {
|
||||
const {clientId} = req.params;
|
||||
keepAlive(clientId);
|
||||
res.send(makeResponse('ok'));
|
||||
}
|
||||
|
||||
handleSSETerminate = async (req: Request, res: Response) => {
|
||||
const {clientId} = req.params;
|
||||
console.log(clientId);
|
||||
removeConnection(clientId);
|
||||
res.send(makeResponse('ok'));
|
||||
}
|
||||
|
||||
addRoutes() {
|
||||
this.app.get('/healthcheck', this.wrapHandler(async (req, res) => {
|
||||
res.send(makeResponse('ok'));
|
||||
@@ -289,6 +596,18 @@ export default class HttpService extends GenericService {
|
||||
this.app.get('/v1/homefeed', this.wrapHandler(this.handleGetHomefeed));
|
||||
this.app.get('/v1/post/:hash', this.wrapHandler(this.handleGetPostByHash));
|
||||
|
||||
this.app.get('/v1/zkchat/users', this.wrapHandler(this.handleGetChatUsers));
|
||||
this.app.post('/v1/zkchat/chat-messages', jsonParser, this.wrapHandler(this.handlePostChatMessage));
|
||||
this.app.get('/v1/zkchat/chat-messages/dm/:sender/:receiver', this.wrapHandler(this.handleGetDirectMessage));
|
||||
this.app.get('/v1/zkchat/chats/dm/:pubkey', this.wrapHandler(this.handleGetDirectChats));
|
||||
this.app.get('/v1/zkchat/chats/search/:query?', this.wrapHandler(this.handleSearchChats));
|
||||
|
||||
this.app.get('/v1/proofs/:idCommitment', this.wrapHandler(this.handleGetProofs));
|
||||
this.app.get('/v1/events', this.wrapHandler(this.handleGetEvents));
|
||||
this.app.post('/v1/events/:clientId', jsonParser, this.wrapHandler(this.handleUpdateSSEClient));
|
||||
this.app.get('/v1/events/:clientId/alive', jsonParser, this.wrapHandler(this.handleSSEKeepAlive));
|
||||
this.app.get('/v1/events/:clientId/terminate', jsonParser, this.wrapHandler(this.handleSSETerminate));
|
||||
|
||||
this.app.post('/interrep/groups/:provider/:name/:identityCommitment', jsonParser, this.wrapHandler(async (req, res) => {
|
||||
const identityCommitment = req.params.identityCommitment;
|
||||
const provider = req.params.provider;
|
||||
@@ -321,14 +640,40 @@ export default class HttpService extends GenericService {
|
||||
res.send(makeResponse(json));
|
||||
}));
|
||||
|
||||
this.app.get('/dev/interep/:identityCommitment', jsonParser, this.wrapHandler(async (req, res) => {
|
||||
const identityCommitment = req.params.identityCommitment;
|
||||
// @ts-ignore
|
||||
const resp = await fetch(`${config.interrepAPI}/api/v1/groups`);
|
||||
const { data: groups } = await resp.json();
|
||||
for (const group of groups) {
|
||||
// @ts-ignore
|
||||
const existResp = await fetch(`${config.interrepAPI}/api/v1/groups/${group.provider}/${group.name}/${identityCommitment}`);
|
||||
const { data: exist } = await existResp.json();
|
||||
|
||||
if (exist) {
|
||||
// @ts-ignore
|
||||
const proofResp = await fetch(`${config.interrepAPI}/api/v1/groups/${group.provider}/${group.name}/${identityCommitment}/proof`);
|
||||
const json = await proofResp.json();
|
||||
res.send(makeResponse({
|
||||
...json,
|
||||
provider: group.provider,
|
||||
name: group.name,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
res.send(makeResponse(null));
|
||||
}));
|
||||
|
||||
this.app.get('/interrep/:identityCommitment', jsonParser, this.wrapHandler(async (req, res) => {
|
||||
const identityCommitment = req.params.identityCommitment;
|
||||
const semaphoreDB = await this.call('db', 'getSemaphore');
|
||||
const exist = await semaphoreDB.findOneByCommitment(identityCommitment);
|
||||
// const exist = await semaphoreDB.findOneByCommitment(identityCommitment);
|
||||
|
||||
if (!exist || exist?.updatedAt.getTime() + 15 * 60 * 1000 > Date.now()) {
|
||||
await this.call('interrep', 'scanIDCommitment', identityCommitment);
|
||||
}
|
||||
// if (!exist || exist?.updatedAt.getTime() + 15 * 60 * 1000 > Date.now()) {
|
||||
// await this.call('interrep', 'scanIDCommitment', identityCommitment);
|
||||
// }
|
||||
|
||||
const sem = await semaphoreDB.findAllByCommitment(identityCommitment);
|
||||
const [group] = sem;
|
||||
@@ -539,26 +884,77 @@ export default class HttpService extends GenericService {
|
||||
if (req.session.twitterToken) delete req.session.twitterToken;
|
||||
res.send(makeResponse('ok'));
|
||||
}));
|
||||
|
||||
this.app.post(
|
||||
'/ipfs/upload',
|
||||
upload.any(),
|
||||
this.verifyAuth(
|
||||
async () => 'FILE_UPLOAD',
|
||||
// @ts-ignore
|
||||
async req => req.files[0].originalname.slice(0, 16),
|
||||
req => {
|
||||
// @ts-ignore
|
||||
const filepath = path.join(process.cwd(), req.files[0].path);
|
||||
fs.unlinkSync(filepath);
|
||||
}
|
||||
),
|
||||
this.wrapHandler(async (req, res) => {
|
||||
if (!req.files) throw new Error('file missing from formdata');
|
||||
|
||||
// @ts-ignore
|
||||
const username = req.username;
|
||||
|
||||
// @ts-ignore
|
||||
const {path: relPath, filename, size, mimetype} = req.files[0];
|
||||
const uploadDB = await this.call('db', 'getUploads');
|
||||
const filepath = path.join(process.cwd(), relPath);
|
||||
|
||||
if (size > maxFileSize) {
|
||||
fs.unlinkSync(filepath);
|
||||
throw new Error('file must be less than 5MB');
|
||||
}
|
||||
|
||||
if (username) {
|
||||
const existingSize = await uploadDB.getTotalUploadByUser(username);
|
||||
if (existingSize + size > maxPerUserSize) {
|
||||
fs.unlinkSync(filepath);
|
||||
throw new Error('account is out of space');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const files = await getFilesFromPath(filepath);
|
||||
|
||||
const cid = await this.call('ipfs', 'store', files);
|
||||
fs.unlinkSync(filepath);
|
||||
|
||||
if (username) {
|
||||
const uploadData: UploadModel = {
|
||||
cid,
|
||||
mimetype,
|
||||
size,
|
||||
filename,
|
||||
username: username,
|
||||
};
|
||||
await uploadDB.addUploadData(uploadData);
|
||||
}
|
||||
|
||||
res.send(makeResponse({
|
||||
cid,
|
||||
filename,
|
||||
url: `https://${cid}.ipfs.dweb.link/${filename}`,
|
||||
}));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async start() {
|
||||
this.merkleRoot = await merkleRoot(sequelize);
|
||||
const httpServer = http.createServer(this.app);
|
||||
|
||||
this.app.set('trust proxy', 1);
|
||||
this.app.use(cors(corsOptions));
|
||||
|
||||
const sequelize = new Sequelize(
|
||||
config.dbName as string,
|
||||
config.dbUsername as string,
|
||||
config.dbPassword,
|
||||
{
|
||||
host: config.dbHost,
|
||||
port: Number(config.dbPort),
|
||||
dialect: config.dbDialect as Dialect,
|
||||
logging: false,
|
||||
},
|
||||
);
|
||||
|
||||
const sessionStore = new SequelizeStore({
|
||||
db: sequelize,
|
||||
})
|
||||
@@ -580,6 +976,13 @@ export default class HttpService extends GenericService {
|
||||
this.app.use('/dev/semaphore_wasm', express.static(path.join(process.cwd(), 'static', 'semaphore.wasm')));
|
||||
this.app.use('/dev/semaphore_final_zkey', express.static(path.join(process.cwd(), 'static', 'semaphore_final.zkey')));
|
||||
this.app.use('/dev/semaphore_vkey', express.static(path.join(process.cwd(), 'static', 'verification_key.json')));
|
||||
|
||||
this.app.use('/circuits/semaphore/wasm', express.static(path.join(process.cwd(), 'static', 'semaphore', 'semaphore.wasm')));
|
||||
this.app.use('/circuits/semaphore/zkey', express.static(path.join(process.cwd(), 'static', 'semaphore', 'semaphore_final.zkey')));
|
||||
this.app.use('/circuits/semaphore/vkey', express.static(path.join(process.cwd(), 'static', 'semaphore', 'verification_key.json')));
|
||||
this.app.use('/circuits/rln/wasm', express.static(path.join(process.cwd(), 'static', 'rln', 'rln.wasm')));
|
||||
this.app.use('/circuits/rln/zkey', express.static(path.join(process.cwd(), 'static', 'rln', 'rln_final.zkey')));
|
||||
this.app.use('/circuits/rln/vkey', express.static(path.join(process.cwd(), 'static', 'rln', 'verification_key.json')));
|
||||
this.addRoutes();
|
||||
|
||||
this.httpServer = httpServer.listen(config.port);
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import {GenericService} from "../util/svc";
|
||||
import config from "../util/config";
|
||||
import interepGroups from "../models/interepGroups";
|
||||
import {sequelize} from "../util/sequelize";
|
||||
import semaphore from "../models/semaphore";
|
||||
import {clear} from "winston";
|
||||
|
||||
export type InterepGroup = {
|
||||
provider: 'twitter' | 'github' | 'reddit';
|
||||
@@ -8,60 +12,66 @@ export type InterepGroup = {
|
||||
size: number;
|
||||
}
|
||||
|
||||
const INTEREP_SYNC_INTERVAL = 15 * 60 * 1000;
|
||||
|
||||
export default class InterrepService extends GenericService {
|
||||
interepGroups?: ReturnType<typeof interepGroups>;
|
||||
semaphore?: ReturnType<typeof semaphore>;
|
||||
|
||||
groups: {
|
||||
[providerName: string]: InterepGroup[],
|
||||
};
|
||||
|
||||
providers = ['twitter', 'github', 'reddit'];
|
||||
|
||||
timeout: any;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.groups = {};
|
||||
}
|
||||
|
||||
async fetchGroups() {
|
||||
// @ts-ignore
|
||||
const resp = await fetch(`${config.interrepAPI}/api/v1/groups`);
|
||||
const json = await resp.json();
|
||||
|
||||
if (json?.data?.length) {
|
||||
for (let group of json.data) {
|
||||
if (this.providers.includes(group.provider)) {
|
||||
const bucket = this.groups[group.provider] || [];
|
||||
bucket.push(group);
|
||||
this.groups[group.provider] = bucket;
|
||||
}
|
||||
sync = async () => {
|
||||
try {
|
||||
if (this.timeout) {
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async addID(id_commitment: string, provider: string, name: string) {
|
||||
const semaphore = await this.call('db', 'getSemaphore');
|
||||
await semaphore.addID(id_commitment, provider, name);
|
||||
}
|
||||
|
||||
async removeID(id_commitment: string, provider: string, name: string) {
|
||||
const semaphore = await this.call('db', 'getSemaphore');
|
||||
await semaphore.removeID(id_commitment, provider, name);
|
||||
}
|
||||
|
||||
async scanIDCommitment(id: string) {
|
||||
for (let provider of this.providers) {
|
||||
if (await this.inProvider(provider, id)) {
|
||||
const groups = this.groups[provider];
|
||||
|
||||
if (groups) {
|
||||
for (let group of groups) {
|
||||
const proof = await this.getProofFromGroup(provider, group.name, id);
|
||||
if (proof) {
|
||||
await this.addID(id, provider, group.name);
|
||||
} else {
|
||||
await this.removeID(id, provider, group.name);
|
||||
}
|
||||
// @ts-ignore
|
||||
const resp = await fetch(`${config.interrepAPI}/api/v1/groups`);
|
||||
const json = await resp.json();
|
||||
const groups = json.data;
|
||||
if (groups?.length) {
|
||||
for (let group of groups) {
|
||||
const { root, provider, name } = group;
|
||||
const existing = await this.interepGroups!.getGroup(provider, name);
|
||||
if (existing?.root_hash !== root) {
|
||||
await this.fetchMembersFromGroup(root, provider, name);
|
||||
await this.interepGroups?.addHash(root, provider, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
this.timeout = setTimeout(this.sync, INTEREP_SYNC_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
async fetchMembersFromGroup(root: string, provider: string, name: string, limit = 1000, offset = 0): Promise<void> {
|
||||
const groupId = `interrep_${provider}_${name}`;
|
||||
// @ts-ignore
|
||||
const resp = await fetch(`${config.interrepAPI}/api/v1/groups/${provider}/${name}/members?limit=${limit}&offset=${offset}`);
|
||||
const json = await resp.json();
|
||||
const members: string[] = json.data;
|
||||
if (members.length) {
|
||||
for (const member of members) {
|
||||
await this.semaphore?.addID(BigInt(member).toString(16), groupId, root);
|
||||
}
|
||||
}
|
||||
|
||||
if (members.length >= limit) {
|
||||
await this.fetchMembersFromGroup(root, provider, name, limit + 1000, limit);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +126,12 @@ export default class InterrepService extends GenericService {
|
||||
}
|
||||
|
||||
async start() {
|
||||
await this.fetchGroups();
|
||||
this.interepGroups = await interepGroups(sequelize);
|
||||
this.semaphore = await semaphore(sequelize);
|
||||
await this.sync();
|
||||
}
|
||||
|
||||
async stop() {
|
||||
if (this.timeout) clearTimeout(this.timeout);
|
||||
}
|
||||
}
|
||||
23
src/services/ipfs.ts
Normal file
23
src/services/ipfs.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {GenericService} from "../util/svc";
|
||||
import config from "../util/config";
|
||||
import {CIDString, Filelike, Web3Storage} from "web3.storage";
|
||||
|
||||
export default class IPFSService extends GenericService {
|
||||
client: Web3Storage;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
// @ts-ignore
|
||||
this.client = new Web3Storage({
|
||||
token: config.web3StorageAPIKey as string,
|
||||
});
|
||||
}
|
||||
|
||||
store = (files: Iterable<Filelike>): Promise<CIDString> => {
|
||||
return this.client.put(files);
|
||||
}
|
||||
|
||||
status = (cid: CIDString) => {
|
||||
return this.client.status(cid);
|
||||
}
|
||||
}
|
||||
148
src/services/merkle.ts
Normal file
148
src/services/merkle.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import {GenericService} from "../util/svc";
|
||||
import {BindOrReplacements, Dialect, QueryTypes, Sequelize} from "sequelize";
|
||||
import {generateMerkleTree} from "@zk-kit/protocols";
|
||||
import {MerkleProof, IncrementalMerkleTree} from "@zk-kit/incremental-merkle-tree";
|
||||
import merkleRoot from "../models/merkle_root";
|
||||
import {sequelize} from "../util/sequelize";
|
||||
|
||||
export default class MerkleService extends GenericService {
|
||||
merkleRoot: ReturnType<typeof merkleRoot>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.merkleRoot = merkleRoot(sequelize);
|
||||
}
|
||||
|
||||
makeTree = async (group: string, zkType: 'rln' | 'semaphore' = 'rln'): Promise<IncrementalMerkleTree> => {
|
||||
const [protocol, groupName, groupType = ''] = group.split('_');
|
||||
const protocolBucket = SQL[protocol] || {};
|
||||
const groupBucket = protocolBucket[groupName] || {};
|
||||
const {sql, replacement} = groupBucket[groupType] || {};
|
||||
|
||||
if (!sql) throw new Error(`${group} does not exist`);
|
||||
|
||||
const options = {
|
||||
type: QueryTypes.SELECT,
|
||||
replacements: replacement || {
|
||||
group_id: group,
|
||||
},
|
||||
};
|
||||
|
||||
const leaves = await sequelize.query(sql, options);
|
||||
const tree = generateMerkleTree(
|
||||
zkType === 'rln' ? 15 : 20,
|
||||
BigInt(0),
|
||||
leaves.map(({ id_commitment }: any) => '0x' + id_commitment),
|
||||
);
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
getGroupByRoot = async (root: string): Promise<string | null> => {
|
||||
const exist = await this.merkleRoot.getGroupByRoot(root);
|
||||
return exist?.group_id || null;
|
||||
}
|
||||
|
||||
verifyProof = async (proof: MerkleProof): Promise<string | null> => {
|
||||
const groups = [
|
||||
'zksocial_all',
|
||||
'interrep_twitter_unrated',
|
||||
'interrep_twitter_bronze',
|
||||
'interrep_twitter_silver',
|
||||
'interrep_twitter_gold',
|
||||
];
|
||||
|
||||
const existingGroup = await this.getGroupByRoot(proof.root);
|
||||
|
||||
if (existingGroup) {
|
||||
const tree = await this.makeTree(existingGroup);
|
||||
if (tree.verifyProof(proof)) return existingGroup;
|
||||
}
|
||||
|
||||
for (const group of groups) {
|
||||
const tree = await this.makeTree(group);
|
||||
if (tree.verifyProof(proof)) return group;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
findProof = async (group: string, idCommitment: string) => {
|
||||
const tree = await this.makeTree(group);
|
||||
const proof = await tree.createProof(tree.indexOf(BigInt('0x' + idCommitment)));
|
||||
|
||||
if (!proof) throw new Error(`${idCommitment} is not in ${group}`);
|
||||
|
||||
const root = '0x' + proof.root.toString(16);
|
||||
|
||||
await this.addRoot(root, group);
|
||||
|
||||
return {
|
||||
root,
|
||||
siblings: proof.siblings.map((siblings) =>
|
||||
Array.isArray(siblings)
|
||||
? siblings.map((element) => '0x' + element.toString(16))
|
||||
: '0x' + siblings.toString(16)
|
||||
),
|
||||
pathIndices: proof.pathIndices,
|
||||
leaf: '0x' + proof.leaf.toString(16),
|
||||
};
|
||||
}
|
||||
|
||||
addRoot = async (rootHash: string, group: string) => {
|
||||
return this.merkleRoot.addRoot(rootHash, group);
|
||||
}
|
||||
|
||||
findRoot = async (rootHash: string) => {
|
||||
const cached = await this.merkleRoot.getGroupByRoot(rootHash);
|
||||
return cached?.group_id;
|
||||
}
|
||||
}
|
||||
|
||||
const SQL: {
|
||||
[protocol: string]: {
|
||||
[groupName: string]: {
|
||||
[groupType: string]: {
|
||||
sql: string;
|
||||
replacement?: BindOrReplacements;
|
||||
};
|
||||
};
|
||||
};
|
||||
} = {
|
||||
'zksocial': {
|
||||
'all': {
|
||||
'': {
|
||||
sql: `
|
||||
SELECT u.name as address, pf.value as id_commitment FROM users u
|
||||
LEFT JOIN profiles pf ON pf."messageId" = (
|
||||
SELECT "messageId" FROM profiles
|
||||
WHERE creator = u.name AND subtype = 'CUSTOM'
|
||||
AND key = 'id_commitment'
|
||||
ORDER BY "createdAt" DESC LIMIT 1
|
||||
)
|
||||
WHERE pf.value IS NOT NULL
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
'interrep': {
|
||||
'twitter': {
|
||||
'unrated': { sql: `SELECT id_commitment FROM semaphores WHERE group_id = :group_id` },
|
||||
'bronze': { sql: `SELECT id_commitment FROM semaphores WHERE group_id = :group_id` },
|
||||
'silver': { sql: `SELECT id_commitment FROM semaphores WHERE group_id = :group_id` },
|
||||
'gold': { sql: `SELECT id_commitment FROM semaphores WHERE group_id = :group_id` },
|
||||
},
|
||||
'github': {
|
||||
'unrated': { sql: `SELECT id_commitment FROM semaphores WHERE group_id = :group_id` },
|
||||
'bronze': { sql: `SELECT id_commitment FROM semaphores WHERE group_id = :group_id` },
|
||||
'silver': { sql: `SELECT id_commitment FROM semaphores WHERE group_id = :group_id` },
|
||||
'gold': { sql: `SELECT id_commitment FROM semaphores WHERE group_id = :group_id` },
|
||||
},
|
||||
'reddit': {
|
||||
'unrated': { sql: `SELECT id_commitment FROM semaphores WHERE group_id = :group_id` },
|
||||
'bronze': { sql: `SELECT id_commitment FROM semaphores WHERE group_id = :group_id` },
|
||||
'silver': { sql: `SELECT id_commitment FROM semaphores WHERE group_id = :group_id` },
|
||||
'gold': { sql: `SELECT id_commitment FROM semaphores WHERE group_id = :group_id` },
|
||||
},
|
||||
}
|
||||
};
|
||||
114
src/services/zkchat.ts
Normal file
114
src/services/zkchat.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import {GenericService} from "../util/svc";
|
||||
import {ZKChat} from "../../lib/zk-chat-server/src";
|
||||
import {ChatMessage} from "../../lib/zk-chat-server/src/services/chat.service";
|
||||
import {Dialect, QueryTypes, Sequelize} from "sequelize";
|
||||
import config from "../../lib/zk-chat-server/src/utils/config";
|
||||
import {RLN, RLNFullProof} from "@zk-kit/protocols";
|
||||
|
||||
export default class ZKChatService extends GenericService {
|
||||
zkchat: ZKChat;
|
||||
sequelize: Sequelize;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.zkchat = new ZKChat();
|
||||
this.sequelize = new Sequelize(
|
||||
config.DB_NAME as string,
|
||||
config.DB_USERNAME as string,
|
||||
config.DB_PASSWORD,
|
||||
{
|
||||
host: config.DB_HOST,
|
||||
port: Number(config.DB_PORT),
|
||||
dialect: config.DB_DIALECT as Dialect,
|
||||
logging: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
start = async () => {
|
||||
return this.zkchat.init();
|
||||
}
|
||||
|
||||
registerUser = async (address: string, ecdhPubkey: string) => {
|
||||
return this.zkchat.registerUser(address, ecdhPubkey);
|
||||
}
|
||||
|
||||
getAllUsers = async (offset = 0, limit = 20) => {
|
||||
return this.zkchat.getAllUsers(offset, limit);
|
||||
}
|
||||
|
||||
addChatMessage = async (chatMessage: ChatMessage) => {
|
||||
return this.zkchat.addChatMessage(chatMessage);
|
||||
}
|
||||
|
||||
getDirectMessages = async (senderPubkey: string, receiverPubkey: string, offset = 0, limit = 20) => {
|
||||
return this.zkchat.getDirectMessages(senderPubkey, receiverPubkey, offset, limit);
|
||||
}
|
||||
|
||||
getDirectChatsForUser = async (pubkey: string) => {
|
||||
return this.zkchat.getDirectChatsForUser(pubkey);
|
||||
}
|
||||
|
||||
isEpochCurrent = async (epoch: string) => {
|
||||
return this.zkchat.isEpochCurrent(epoch);
|
||||
}
|
||||
|
||||
verifyRLNProof = async (proof: RLNFullProof) => {
|
||||
return this.zkchat.verifyRLNProof(proof);
|
||||
}
|
||||
|
||||
checkShare = async (share: {
|
||||
nullifier: string;
|
||||
epoch: string;
|
||||
x_share: string;
|
||||
y_share: string;
|
||||
}) => {
|
||||
return this.zkchat.checkShare(share);
|
||||
}
|
||||
|
||||
insertShare = async (share: {
|
||||
nullifier: string;
|
||||
epoch: string;
|
||||
x_share: string;
|
||||
y_share: string;
|
||||
}) => {
|
||||
return this.zkchat.insertShare(share);
|
||||
}
|
||||
|
||||
searchChats = async (query: string, sender?: string, offset = 0, limit = 20) => {
|
||||
const values = await this.sequelize.query(`
|
||||
SELECT
|
||||
ecdh.value as receiver_ecdh,
|
||||
idcommitment.value as receiver_idcommitment,
|
||||
zku.wallet_address as receiver_address
|
||||
FROM zkchat_users zku
|
||||
LEFT JOIN profiles ecdh ON ecdh."messageId" = (SELECT "messageId" FROM profiles WHERE creator = zku.wallet_address AND subtype = 'CUSTOM' AND key='ecdh_pubkey' ORDER BY "createdAt" DESC LIMIT 1)
|
||||
LEFT JOIN profiles idcommitment ON idcommitment."messageId" = (SELECT "messageId" FROM profiles WHERE creator = zku.wallet_address AND subtype = 'CUSTOM' AND key='id_commitment' ORDER BY "createdAt" DESC LIMIT 1)
|
||||
LEFT JOIN profiles name ON name."messageId" = (SELECT "messageId" FROM profiles WHERE creator = zku.wallet_address AND subtype = 'NAME' ORDER BY "createdAt" DESC LIMIT 1)
|
||||
WHERE (
|
||||
LOWER(zku.wallet_address) LIKE :query
|
||||
OR LOWER(name.value) LIKE :query
|
||||
OR LOWER(name.creator) LIKE :query
|
||||
OR LOWER(name.creator) IN (SELECT LOWER(address) from ens WHERE LOWER(ens) LIKE :query)
|
||||
OR LOWER(name.creator) IN (SELECT LOWER(creator) from profiles WHERE subtype = 'NAME' AND LOWER(value) LIKE :query ORDER BY "createdAt" DESC LIMIT 1)
|
||||
)
|
||||
${!sender ? '' : `
|
||||
AND (
|
||||
zku.wallet_address IN (SELECT distinct zk.receiver_address FROM zkchat_chats zk WHERE zk.sender_address = :sender)
|
||||
OR zku.wallet_address IN (SELECT distinct zk.sender_address FROM zkchat_chats zk WHERE zk.receiver_address = :sender)
|
||||
)`}
|
||||
|
||||
LIMIT :limit OFFSET :offset
|
||||
`, {
|
||||
type: QueryTypes.SELECT,
|
||||
replacements: {
|
||||
query: `%${query.toLowerCase()}%`,
|
||||
sender,
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
});
|
||||
|
||||
return values;
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ let json: {
|
||||
twAccessKey?: string;
|
||||
twAccessSecret?: string;
|
||||
rapidAPIKey?: string;
|
||||
web3StorageAPIKey?: string;
|
||||
} = {};
|
||||
|
||||
try {
|
||||
@@ -71,6 +72,7 @@ const moderators = json.moderators || process.env?.MODERATORS?.split(' ') || [
|
||||
const interrepAPI = json.interrepAPI || process.env.INTERREP_API || 'https://kovan.interep.link';
|
||||
const interrepContract = json.interrepContract || process.env.INTERREP_CONTRACT || '';
|
||||
const jwtSecret = json.jwtSecret || process.env.JWT_SECRET || 'topjwtsecret';
|
||||
const web3StorageAPIKey = json.web3StorageAPIKey || process.env.WEB3_STORAGE_API_KEY;
|
||||
|
||||
if (!web3HttpProvider) throw new Error('WEB3_HTTP_PROVIDER is not valid');
|
||||
if (!ensResolver) throw new Error('ENS_RESOLVER is not valid');
|
||||
@@ -86,6 +88,7 @@ if (!twBearerToken) throw new Error(`twBearerToken is not valid`);
|
||||
if (!twAccessKey) throw new Error(`twAccessKey is not valid`);
|
||||
if (!twAccessSecret) throw new Error(`twAccessSecret is not valid`);
|
||||
if (!rapidAPIKey) throw new Error(`rapidAPIKey is not valid`);
|
||||
if (!web3StorageAPIKey) throw new Error(`web3StorageAPIKey is not valid`);
|
||||
|
||||
const config = {
|
||||
interrepAPI,
|
||||
@@ -115,6 +118,7 @@ const config = {
|
||||
twAccessSecret,
|
||||
rapidAPIKey,
|
||||
moderators,
|
||||
web3StorageAPIKey,
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -24,4 +24,4 @@ export function verifySignatureP256(pubkey: string, data: string, signature: str
|
||||
Buffer.from(data, 'utf-8').toString('hex'),
|
||||
Buffer.from(signature, 'hex').toJSON().data,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,10 +21,10 @@ const logger = winston.createLogger({
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
logger.add(new winston.transports.Console({
|
||||
level: 'info',
|
||||
format: winston.format.simple(),
|
||||
}));
|
||||
// logger.add(new winston.transports.Console({
|
||||
// level: 'info',
|
||||
// format: winston.format.simple(),
|
||||
// }));
|
||||
}
|
||||
|
||||
export default logger;
|
||||
32
src/util/sequelize.ts
Normal file
32
src/util/sequelize.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import config from "./config";
|
||||
import {Dialect, Sequelize} from "sequelize";
|
||||
|
||||
let cached: Sequelize;
|
||||
|
||||
function getSequelize(): Sequelize {
|
||||
if (cached) return sequelize;
|
||||
|
||||
if (!config.dbDialect || config.dbDialect === 'sqlite') {
|
||||
cached = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
storage: config.dbStorage,
|
||||
logging: false,
|
||||
});
|
||||
} else {
|
||||
cached = new Sequelize(
|
||||
config.dbName as string,
|
||||
config.dbUsername as string,
|
||||
config.dbPassword,
|
||||
{
|
||||
host: config.dbHost,
|
||||
port: Number(config.dbPort),
|
||||
dialect: config.dbDialect as Dialect,
|
||||
logging: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return cached;
|
||||
}
|
||||
|
||||
export const sequelize = getSequelize();
|
||||
129
src/util/sse.ts
Normal file
129
src/util/sse.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import {Response} from "express";
|
||||
|
||||
type SSEClient = {
|
||||
res: Response;
|
||||
lastUsed: number;
|
||||
}
|
||||
|
||||
const CLIENT_CACHE: {
|
||||
[clientId: string]: SSEClient;
|
||||
} = {};
|
||||
|
||||
const TOPIC_MAP: {
|
||||
[topic: string]: {
|
||||
[clientId: string]: boolean;
|
||||
};
|
||||
} = {};
|
||||
|
||||
const MAX_TTL = 2 * 60 * 1000;
|
||||
|
||||
let pruneTimeout: any;
|
||||
|
||||
export enum SSEType {
|
||||
INIT = 'INIT',
|
||||
NEW_CHAT_MESSAGE = 'NEW_CHAT_MESSAGE',
|
||||
HEALTHCHECK = 'HEALTHCHECK',
|
||||
}
|
||||
|
||||
export const addConnection = (clientId: string, res: Response) => {
|
||||
CLIENT_CACHE[clientId] = {
|
||||
res,
|
||||
lastUsed: Date.now(),
|
||||
};
|
||||
|
||||
const raw = `data: ${JSON.stringify({
|
||||
type: SSEType.INIT,
|
||||
clientId,
|
||||
})}\n\n`;
|
||||
|
||||
res.write(raw);
|
||||
}
|
||||
|
||||
export const keepAlive = (clientId: string) => {
|
||||
const client = CLIENT_CACHE[clientId];
|
||||
|
||||
if (!client) throw new Error(`${clientId} not found`);
|
||||
|
||||
client.lastUsed = Date.now();
|
||||
}
|
||||
|
||||
export const addTopic = (clientId: string, topic: string) => {
|
||||
const client = CLIENT_CACHE[clientId];
|
||||
|
||||
if (!client) {
|
||||
throw new Error(`${clientId} not found`);
|
||||
}
|
||||
|
||||
const bucket: { [id: string]: boolean } = TOPIC_MAP[topic] || {};
|
||||
bucket[clientId] = true;
|
||||
TOPIC_MAP[topic] = bucket;
|
||||
}
|
||||
|
||||
export const removeTopic = (clientId: string, topic: string) => {
|
||||
const client = CLIENT_CACHE[clientId];
|
||||
|
||||
if (!client) {
|
||||
throw new Error(`${clientId} not found`);
|
||||
}
|
||||
|
||||
const bucket: { [id: string]: boolean } = TOPIC_MAP[topic] || {};
|
||||
|
||||
if (bucket[clientId]) delete bucket[clientId];
|
||||
|
||||
TOPIC_MAP[topic] = bucket;
|
||||
}
|
||||
|
||||
export const publishTopic = async (topic: string, data: any) => {
|
||||
const bucket: { [id: string]: boolean } = TOPIC_MAP[topic] || {};
|
||||
|
||||
for (const clientId in bucket) {
|
||||
const client = CLIENT_CACHE[clientId];
|
||||
if (!client) {
|
||||
delete bucket[clientId];
|
||||
} else {
|
||||
const raw = `data: ${JSON.stringify(data)}\n\n`;
|
||||
client.res.write(raw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const removeConnection = (clientId: string) => {
|
||||
const client = CLIENT_CACHE[clientId];
|
||||
|
||||
if (!client) return;
|
||||
|
||||
delete CLIENT_CACHE[clientId];
|
||||
|
||||
client.res.end();
|
||||
|
||||
}
|
||||
|
||||
export const getConnection = (clientId: string) => {
|
||||
return CLIENT_CACHE[clientId];
|
||||
}
|
||||
|
||||
export const pruneConnections = async () => {
|
||||
const now = Date.now();
|
||||
for (const clientId in CLIENT_CACHE) {
|
||||
const client = CLIENT_CACHE[clientId];
|
||||
if (now - client.lastUsed > MAX_TTL) {
|
||||
removeConnection(clientId);
|
||||
for (const topic in TOPIC_MAP) {
|
||||
const bucket = TOPIC_MAP[topic];
|
||||
if (bucket[clientId]) delete bucket[clientId];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pruneLoop = async () => {
|
||||
if (pruneTimeout) {
|
||||
clearTimeout(pruneTimeout);
|
||||
}
|
||||
|
||||
await pruneConnections();
|
||||
|
||||
setTimeout(pruneLoop, MAX_TTL);
|
||||
}
|
||||
|
||||
pruneLoop();
|
||||
BIN
static/rln/rln.wasm
Normal file
BIN
static/rln/rln.wasm
Normal file
Binary file not shown.
BIN
static/rln/rln_final.zkey
Normal file
BIN
static/rln/rln_final.zkey
Normal file
Binary file not shown.
119
static/rln/verification_key.json
Normal file
119
static/rln/verification_key.json
Normal file
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"protocol": "groth16",
|
||||
"curve": "bn128",
|
||||
"nPublic": 6,
|
||||
"vk_alpha_1": [
|
||||
"20491192805390485299153009773594534940189261866228447918068658471970481763042",
|
||||
"9383485363053290200918347156157836566562967994039712273449902621266178545958",
|
||||
"1"
|
||||
],
|
||||
"vk_beta_2": [
|
||||
[
|
||||
"6375614351688725206403948262868962793625744043794305715222011528459656738731",
|
||||
"4252822878758300859123897981450591353533073413197771768651442665752259397132"
|
||||
],
|
||||
[
|
||||
"10505242626370262277552901082094356697409835680220590971873171140371331206856",
|
||||
"21847035105528745403288232691147584728191162732299865338377159692350059136679"
|
||||
],
|
||||
[
|
||||
"1",
|
||||
"0"
|
||||
]
|
||||
],
|
||||
"vk_gamma_2": [
|
||||
[
|
||||
"10857046999023057135944570762232829481370756359578518086990519993285655852781",
|
||||
"11559732032986387107991004021392285783925812861821192530917403151452391805634"
|
||||
],
|
||||
[
|
||||
"8495653923123431417604973247489272438418190587263600148770280649306958101930",
|
||||
"4082367875863433681332203403145435568316851327593401208105741076214120093531"
|
||||
],
|
||||
[
|
||||
"1",
|
||||
"0"
|
||||
]
|
||||
],
|
||||
"vk_delta_2": [
|
||||
[
|
||||
"14962742607348761745067039667743739542918587958944430293802039673540545491766",
|
||||
"19420690459571091434654770613940591470063366130537730965999231781890983822228"
|
||||
],
|
||||
[
|
||||
"8025490964181160791180478620151567079603098158752936953068367103529675811355",
|
||||
"16899630578734283128344486213072467840624492210479261395286682179061749476405"
|
||||
],
|
||||
[
|
||||
"1",
|
||||
"0"
|
||||
]
|
||||
],
|
||||
"vk_alphabeta_12": [
|
||||
[
|
||||
[
|
||||
"2029413683389138792403550203267699914886160938906632433982220835551125967885",
|
||||
"21072700047562757817161031222997517981543347628379360635925549008442030252106"
|
||||
],
|
||||
[
|
||||
"5940354580057074848093997050200682056184807770593307860589430076672439820312",
|
||||
"12156638873931618554171829126792193045421052652279363021382169897324752428276"
|
||||
],
|
||||
[
|
||||
"7898200236362823042373859371574133993780991612861777490112507062703164551277",
|
||||
"7074218545237549455313236346927434013100842096812539264420499035217050630853"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"7077479683546002997211712695946002074877511277312570035766170199895071832130",
|
||||
"10093483419865920389913245021038182291233451549023025229112148274109565435465"
|
||||
],
|
||||
[
|
||||
"4595479056700221319381530156280926371456704509942304414423590385166031118820",
|
||||
"19831328484489333784475432780421641293929726139240675179672856274388269393268"
|
||||
],
|
||||
[
|
||||
"11934129596455521040620786944827826205713621633706285934057045369193958244500",
|
||||
"8037395052364110730298837004334506829870972346962140206007064471173334027475"
|
||||
]
|
||||
]
|
||||
],
|
||||
"IC": [
|
||||
[
|
||||
"1903896611309112537898744888962900116850557104688140496996114549467905845389",
|
||||
"18878807081439367511400607605971457252753640659171839147843428814578113606944",
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"3379380938593207080729160389717513882233130462616390491862768147575356927916",
|
||||
"7658366061956613070318477742617874370870162905631795036979866045784951077741",
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"6573257371221466457264319919959056878987621722065494829982803761054687125969",
|
||||
"10882724634177179759524298899662307047578489073607305659656545291770678864358",
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"7812315588577187126949469943175416873443639588836013906528191759531184751241",
|
||||
"11928578526804134334949431821595151892366575912030023302327660047097849106833",
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"8603322753346553805166809285487650902416020947317326006612301010819108432803",
|
||||
"3100089596497060388197556426101480177422659517238854788949908444987414217355",
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"548584601681146753562700521279324517960520272202999011217745775929880443671",
|
||||
"10775304696187723066725699321032508342393012304409445749847420882128296879500",
|
||||
"1"
|
||||
],
|
||||
[
|
||||
"4832346778469239752999589970014962368162544386191392542939015835265757824362",
|
||||
"17497085844864771075682226779903142040218959461493641885155366729946818944068",
|
||||
"1"
|
||||
]
|
||||
]
|
||||
}
|
||||
BIN
static/semaphore/semaphore.wasm
Normal file
BIN
static/semaphore/semaphore.wasm
Normal file
Binary file not shown.
BIN
static/semaphore/semaphore_final.zkey
Normal file
BIN
static/semaphore/semaphore_final.zkey
Normal file
Binary file not shown.
1
static/semaphore/verification_key.json
Normal file
1
static/semaphore/verification_key.json
Normal file
@@ -0,0 +1 @@
|
||||
{"protocol":"groth16","curve":"bn128","nPublic":4,"vk_alpha_1":["20491192805390485299153009773594534940189261866228447918068658471970481763042","9383485363053290200918347156157836566562967994039712273449902621266178545958","1"],"vk_beta_2":[["6375614351688725206403948262868962793625744043794305715222011528459656738731","4252822878758300859123897981450591353533073413197771768651442665752259397132"],["10505242626370262277552901082094356697409835680220590971873171140371331206856","21847035105528745403288232691147584728191162732299865338377159692350059136679"],["1","0"]],"vk_gamma_2":[["10857046999023057135944570762232829481370756359578518086990519993285655852781","11559732032986387107991004021392285783925812861821192530917403151452391805634"],["8495653923123431417604973247489272438418190587263600148770280649306958101930","4082367875863433681332203403145435568316851327593401208105741076214120093531"],["1","0"]],"vk_delta_2":[["7912208710313447447762395792098481825752520616755888860068004689933335666613","12599857379517512478445603412764121041984228075771497593287716170335433683702"],["21679208693936337484429571887537508926366191105267550375038502782696042114705","11502426145685875357967720478366491326865907869902181704031346886834786027007"],["1","0"]],"vk_alphabeta_12":[[["2029413683389138792403550203267699914886160938906632433982220835551125967885","21072700047562757817161031222997517981543347628379360635925549008442030252106"],["5940354580057074848093997050200682056184807770593307860589430076672439820312","12156638873931618554171829126792193045421052652279363021382169897324752428276"],["7898200236362823042373859371574133993780991612861777490112507062703164551277","7074218545237549455313236346927434013100842096812539264420499035217050630853"]],[["7077479683546002997211712695946002074877511277312570035766170199895071832130","10093483419865920389913245021038182291233451549023025229112148274109565435465"],["4595479056700221319381530156280926371456704509942304414423590385166031118820","19831328484489333784475432780421641293929726139240675179672856274388269393268"],["11934129596455521040620786944827826205713621633706285934057045369193958244500","8037395052364110730298837004334506829870972346962140206007064471173334027475"]]],"IC":[["19918517214839406678907482305035208173510172567546071380302965459737278553528","7151186077716310064777520690144511885696297127165278362082219441732663131220","1"],["690581125971423619528508316402701520070153774868732534279095503611995849608","21271996888576045810415843612869789314680408477068973024786458305950370465558","1"],["16461282535702132833442937829027913110152135149151199860671943445720775371319","2814052162479976678403678512565563275428791320557060777323643795017729081887","1"],["4319780315499060392574138782191013129592543766464046592208884866569377437627","13920930439395002698339449999482247728129484070642079851312682993555105218086","1"],["3554830803181375418665292545416227334138838284686406179598687755626325482686","5951609174746846070367113593675211691311013364421437923470787371738135276998","1"]]}
|
||||
Reference in New Issue
Block a user