3
1

index.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730
  1. import { Op } from "sequelize";
  2. import { ApiPromise } from "@polkadot/api";
  3. // models
  4. import {
  5. Account,
  6. Balance,
  7. Block,
  8. Category,
  9. Channel,
  10. Council,
  11. Consul,
  12. Commitment,
  13. Era,
  14. Event,
  15. Member,
  16. Post,
  17. Proposal,
  18. ProposalPost,
  19. ProposalVote,
  20. Thread,
  21. Moderation,
  22. } from "../db/models";
  23. // library
  24. import {
  25. getBlockHash,
  26. getHead,
  27. getTimestamp,
  28. getEra,
  29. getEraStake,
  30. getEvents,
  31. getCouncil,
  32. getCouncils,
  33. getCouncilRound,
  34. getCouncilElectionStatus,
  35. getCouncilElectionDurations,
  36. getCommitment,
  37. getCommitments,
  38. getProposalCount,
  39. getProposal,
  40. getProposalVotes,
  41. getProposalPost,
  42. getProposalPosts,
  43. getProposalPostCount,
  44. getProposalThreadCount,
  45. getNextMember,
  46. getNextChannel,
  47. getNextCategory,
  48. getNextThread,
  49. getNextPost,
  50. getCategory,
  51. getThread,
  52. getPost,
  53. getAccount,
  54. getAccounts,
  55. getMember,
  56. getMemberIdByAccount,
  57. getValidators,
  58. getValidatorCount,
  59. } from "./lib/api";
  60. //import { fetchReports } from './lib/github'
  61. import axios from "axios";
  62. import moment from "moment";
  63. import chalk from "chalk";
  64. // types
  65. import {
  66. AccountBalance,
  67. CacheEvent,
  68. BlockEvent,
  69. Round,
  70. Vote,
  71. ProposalDetail,
  72. } from "./lib/types";
  73. import {
  74. AccountId,
  75. BlockNumber,
  76. Hash,
  77. Moment,
  78. ActiveEraInfo,
  79. EventRecord,
  80. } from "@polkadot/types/interfaces";
  81. import { HeaderExtended } from "@polkadot/api-derive/types";
  82. //import { AccountInfo } from '@polkadot/types/interfaces/system'
  83. import { SealedVote, Seat } from "@joystream/types/council";
  84. import { MemberId, Membership } from "@joystream/types/members";
  85. import {
  86. ProposalId,
  87. DiscussionPost,
  88. SpendingParams,
  89. VoteKind,
  90. } from "@joystream/types/proposals";
  91. import { Status } from "../types";
  92. import {
  93. MemberType,
  94. CategoryType,
  95. ChannelType,
  96. CommitmentType,
  97. PostType,
  98. ThreadType,
  99. CouncilType,
  100. ModerationType,
  101. ProposalPostType,
  102. } from "../types/model";
  103. const WORKERS = 3;
  104. const DELAY = 100; // ms
  105. let lastUpdate = 0;
  106. let queuedAll = false;
  107. let queue: any[] = [];
  108. let processing = "";
  109. let busy = 0;
  110. const skipEvents: { [key: string]: string[] } = {
  111. system: ["ExtrinsicSuccess"],
  112. imOnline: ["HeartbeatReceived", "AllGood"],
  113. utility: ["BatchCompleted"],
  114. grandpa: ["NewAuthorities"],
  115. session: ["NewSession"],
  116. };
  117. const processNext = async () => {
  118. if (busy === WORKERS) return; //console.log(`ne free worker`)
  119. const task = queue.shift();
  120. if (!task) return; //console.log(`no task`)
  121. busy++;
  122. if (busy < WORKERS) setTimeout(processNext, DELAY);
  123. const result = await task();
  124. busy--;
  125. setTimeout(processNext, DELAY);
  126. };
  127. export const addBlock = async (
  128. api: ApiPromise,
  129. io: any,
  130. header: HeaderExtended,
  131. status: Status = {
  132. block: 0,
  133. election: {
  134. durations: [],
  135. stage: null,
  136. round: 0,
  137. stageEndsAt: 0,
  138. termEndsAt: 0,
  139. },
  140. era: 0,
  141. round: 0,
  142. members: 0,
  143. channels: 0,
  144. categories: 0,
  145. threads: 0,
  146. posts: 0,
  147. proposals: 0,
  148. proposalPosts: 0,
  149. }
  150. ): Promise<Status> => {
  151. const id = header.number.toNumber();
  152. const exists = await Block.findByPk(id);
  153. if (exists || !header.author) return status;
  154. const key = header.author.toHuman();
  155. const block = await processBlock(api, id);
  156. const [account] = await Account.findOrCreate({ where: { key } });
  157. await block.setValidator(account.key);
  158. io.emit("block", await Block.findByIdWithIncludes(id));
  159. // log
  160. const member = await fetchMemberByAccount(api, header.author);
  161. const author = member ? member.handle : key;
  162. console.log(`[Joystream] block ${id} ${author} [${logStatus()}]`);
  163. const shouldUpdate = id / 10 === Math.floor(id / 10);
  164. return shouldUpdate ? updateStatus(api, id) : status;
  165. };
  166. const logStatus = () =>
  167. queue.length ? `${busy}/${queue.length}: ${processing}` : processing;
  168. const processBlock = async (api: ApiPromise, id: number) => {
  169. const exists = await Block.findByPk(id);
  170. if (exists) return exists;
  171. let [block, created] = await Block.findOrCreate({ where: { id } });
  172. return block;
  173. processing = `block ${id}`;
  174. console.log(processing);
  175. const hash = await getBlockHash(api, id);
  176. const last = await Block.findByPk(id - 1);
  177. const lastTimestamp: number = last?.timestamp
  178. ? last.timestamp
  179. : await getTimestamp(api, await getBlockHash(api, id - 1));
  180. const timestamp = await getTimestamp(api, hash);
  181. console.log(`timestamp`, timestamp, lastTimestamp);
  182. const blocktime = timestamp - lastTimestamp;
  183. return Block.create({ id, hash: String(hash), timestamp, blocktime });
  184. processEvents(api, id, hash);
  185. importEraAtBlock(api, id, hash);
  186. processNext();
  187. return block;
  188. };
  189. const loadEventsCache = (path: string): [{ id: number }[], BlockEvent[]] => {
  190. const blocks: { id: number }[] = [];
  191. const events: BlockEvent[] = [];
  192. for (const [blockId, cache] of require(path)) {
  193. blocks.push({ id: blockId });
  194. cache.forEach((event: CacheEvent) => {
  195. const { section, method } = event;
  196. if (skipEvents[section]?.indexOf(method) >= 0) return;
  197. const data = JSON.stringify(event.data);
  198. events.push({ blockId, section, method, data });
  199. });
  200. }
  201. return [blocks, events];
  202. };
  203. export const importEvents = async (files: string[]) => {
  204. for (const filename of files) {
  205. console.log(`-> Loading ${filename}`);
  206. const [blocks, events] = loadEventsCache(`../../${filename}`);
  207. console.log(`-> Adding ${blocks.length} blocks`);
  208. await Block.bulkCreate(blocks, { ignoreDuplicates: true }).then(() => {
  209. console.log(`-> Adding ${events.length} events`);
  210. Event.bulkCreate(events, { ignoreDuplicates: true });
  211. });
  212. }
  213. };
  214. // TODO only fetchAll() once, then respond to chain events
  215. const updateStatus = async (
  216. api: ApiPromise,
  217. block: number
  218. ): Promise<Status> => {
  219. const hash = await getBlockHash(api, block);
  220. const status = {
  221. block,
  222. era: await getEra(api, hash),
  223. round: await getCouncilRound(api, hash),
  224. election: await getCouncilElectionStatus(api, hash),
  225. members: (await getNextMember(api, hash)) - 1,
  226. channels: (await getNextChannel(api, hash)) - 1,
  227. categories: (await getNextCategory(api, hash)) - 1,
  228. threads: (await getNextThread(api, hash)) - 1,
  229. posts: (await getNextPost(api, hash)) - 1,
  230. proposals: await getProposalCount(api, hash),
  231. proposalPosts: await getProposalPostCount(api),
  232. };
  233. if (!queuedAll) fetchAll(api, status);
  234. else {
  235. fetchMember(api, status.members);
  236. fetchCategory(api, status.categories);
  237. fetchThread(api, status.threads);
  238. fetchPost(api, status.posts);
  239. fetchProposal(api, status.proposals);
  240. }
  241. processNext();
  242. return status;
  243. };
  244. const fetchAll = async (api: ApiPromise, status: Status) => {
  245. await getCouncils(api, status.block).then((rounds: Round[]) =>
  246. rounds.forEach((round) => fetchCouncil(api, round))
  247. );
  248. queue.push(() => fetchAccounts(api));
  249. queue.push(() => fetchMember(api, status.members));
  250. queue.push(() => fetchCategory(api, status.categories));
  251. queue.push(() => fetchThread(api, status.threads));
  252. queue.push(() => fetchPost(api, status.posts));
  253. queue.push(() => fetchProposal(api, status.proposals));
  254. queue.push(() => fetchProposalPosts(api));
  255. queuedAll = true;
  256. };
  257. const processEvents = async (api: ApiPromise, blockId: number, hash: Hash) => {
  258. processing = `events block ${blockId}`;
  259. console.log(processing);
  260. getEvents(api, hash).then((events) =>
  261. events.forEach((event: EventRecord) => saveEvent(blockId, event))
  262. );
  263. };
  264. const saveEvent = (blockId: number, event: EventRecord) => {
  265. const { section, method, data } = event.event;
  266. if (skipEvents[section]?.indexOf(method) >= 0) return;
  267. console.log(section, method, data);
  268. // TODO catch votes, posts, proposals
  269. Event.findOrCreate({ blockId, section, method, data: JSON.stringify(data) });
  270. };
  271. const importEraAtBlock = async (
  272. api: ApiPromise,
  273. blockId: number,
  274. hash: Hash
  275. ) => {
  276. const id = await getEra(api, hash);
  277. const [era] = await Era.findOrCreate({ where: { id } });
  278. era.addBlock(blockId);
  279. if (era.active) return;
  280. processing = `era ${id}`;
  281. getValidators(api, hash).then(async (validators: any[]) => {
  282. const validatorCount = validators.length;
  283. if (!validatorCount) return;
  284. console.log(`[Joystream] Found validator info for era ${id}`);
  285. era.slots = await getValidatorCount(api, hash);
  286. era.active = Math.min(era.slots, validatorCount);
  287. era.waiting = validatorCount > era.slots ? validatorCount - era.slots : 0;
  288. era.stake = await getEraStake(api, hash, id);
  289. const timestamp = await getTimestamp(api, hash);
  290. console.log(id, timestamp, hash);
  291. era.timestamp = timestamp;
  292. era.blockId = blockId;
  293. era.save();
  294. updateBalances(api, hash);
  295. });
  296. };
  297. const validatorStatus = async (
  298. api: ApiPromise,
  299. blockId: BlockNumber | number
  300. ) => {
  301. const hash = await getBlockHash(api, blockId);
  302. const totalValidators = await getValidators(api, hash);
  303. if (!totalValidators.length) return;
  304. const totalNrValidators = totalValidators.length;
  305. const maxSlots = await getValidatorCount(api, hash);
  306. const actives = Math.min(maxSlots, totalNrValidators);
  307. const waiting =
  308. totalNrValidators > maxSlots ? totalNrValidators - maxSlots : 0;
  309. const date = await getTimestamp(api, hash);
  310. console.log(`validator`, date);
  311. return { blockId, actives, waiting, maxSlots, date };
  312. };
  313. const updateBalances = async (api: ApiPromise, hash: Hash) => {
  314. const currentEra: number = await getEra(api, hash);
  315. const era = await Era.findOrCreate({ where: { id: currentEra } });
  316. try {
  317. processing = `balances ${era.id}`;
  318. Account.findAll().then(async (account: any) => {
  319. const { key } = account;
  320. if (!key) return;
  321. console.log(`updating balance of`, key, key);
  322. const { data } = await getAccount(api, hash, key);
  323. const { free, reserved, miscFrozen, feeFrozen } = data;
  324. const balance = { available: free, reserved, frozen: miscFrozen };
  325. console.log(`balance era ${era}`, balance);
  326. const where = { accountKey: key, eraId: era.id };
  327. const exists = Balance.findOne({ where });
  328. if (exists) Balance.update(balance, { where });
  329. else
  330. Balance.create(balance).then((balance: any) => {
  331. balance.setAccount(key);
  332. balance.setEra(era.id);
  333. });
  334. });
  335. } catch (e) {
  336. console.error(`balances era ${era}`);
  337. }
  338. };
  339. const fetchTokenomics = async () => {
  340. console.debug(`Updating tokenomics`);
  341. const { data } = await axios.get("https://status.joystream.org/status");
  342. if (!data) return;
  343. // TODO save 'tokenomics', data
  344. };
  345. const fetchCategory = async (api: ApiPromise, id: number) => {
  346. if (id <= 0) return;
  347. queue.push(() => fetchCategory(api, id - 1));
  348. const exists = await Category.findByPk(+id);
  349. if (exists) return exists;
  350. processing = `category ${id}`;
  351. const {
  352. created_at,
  353. title,
  354. description,
  355. deleted,
  356. archived,
  357. moderator_id,
  358. num_direct_subcategories,
  359. num_direct_moderated_threads,
  360. num_direct_unmoderated_threads,
  361. position_in_parent_category,
  362. } = await getCategory(api, id);
  363. const created = created_at.block.toNumber();
  364. const category = { id, title, description, created, deleted, archived };
  365. return Category.create(category).then((category: CategoryType) => {
  366. if (moderator_id)
  367. createModeration(api, { categoryId: id }, moderator_id, category);
  368. return category;
  369. });
  370. };
  371. const fetchPost = async (api: ApiPromise, id: number): Promise<PostType> => {
  372. if (id > 1) queue.push(() => fetchPost(api, id - 1));
  373. const exists = await Post.findByPk(id);
  374. if (exists) return exists;
  375. processing = `post ${id}`;
  376. const { created_at, author_id, thread_id, current_text, moderation } =
  377. await getPost(api, id);
  378. const author = author_id;
  379. const member = await fetchMemberByAccount(api, author);
  380. const authorId = member ? member.id : null;
  381. const threadId = Number(thread_id);
  382. const thread = await fetchThread(api, threadId);
  383. const text = current_text;
  384. const created = created_at.block.toNumber();
  385. const post = await savePost(id, { authorId, text, created, threadId });
  386. if (moderation)
  387. createModeration(api, { postId: id }, moderation.moderator_id, post);
  388. return post;
  389. };
  390. const savePost = async (id: number, data: any): Promise<PostType> => {
  391. const [post] = await Post.findOrCreate({ where: { id } });
  392. post.update(data);
  393. return post;
  394. };
  395. const createModeration = async (
  396. api: ApiPromise,
  397. association: {},
  398. accountId: AccountId,
  399. object: { setModeration: (id: number) => {} }
  400. ) => {
  401. if (!accountId) return;
  402. const key = accountId.toHuman();
  403. await Account.findOrCreate({ where: { key } });
  404. const where = { ...association, moderatorKey: key };
  405. return; // TODO
  406. const [moderation] = await Moderation.findOrCreate({ where });
  407. if (moderation) object.setModeration(moderation.id);
  408. };
  409. const fetchThread = async (api: ApiPromise, id: number) => {
  410. if (id <= 0) return;
  411. const exists = await Thread.findByPk(id);
  412. if (exists) return exists;
  413. processing = `thread ${id}`;
  414. const {
  415. author_id,
  416. created_at,
  417. category_id,
  418. title,
  419. moderation,
  420. nr_in_category,
  421. } = await getThread(api, id);
  422. const [thread] = await Thread.findOrCreate({ where: { id } });
  423. thread.update({
  424. id,
  425. title,
  426. nrInCategory: +nr_in_category,
  427. created: +created_at.block,
  428. });
  429. const category = await fetchCategory(api, +category_id);
  430. if (category) thread.setCategory(category.id);
  431. const author = await fetchMemberByAccount(api, author_id);
  432. if (author) thread.setCreator(author.id);
  433. if (moderation) {
  434. const { moderated_at, moderator_id, rationale } = moderation;
  435. const created = moderated_at.block;
  436. const createdAt = moderated_at.time;
  437. createModeration(
  438. api,
  439. { created, createdAt, rationale },
  440. moderator_id,
  441. thread
  442. );
  443. }
  444. return thread;
  445. };
  446. // council
  447. interface Council {
  448. round: number;
  449. start: number;
  450. startDate?: number;
  451. end: number;
  452. endDate?: number;
  453. }
  454. const fetchCouncil = async (api: ApiPromise, term: Round) => {
  455. const { round, start, end } = term;
  456. const exists = await Council.findByPk(round);
  457. //if (exists) return exists
  458. processing = `council ${round}`;
  459. let council: Council = { start, end, round };
  460. const startHash = await getBlockHash(api, start);
  461. council.startDate = await getTimestamp(api, startHash);
  462. const seats: Seat[] = await getCouncil(api, startHash);
  463. const head = Number(await getHead(api));
  464. if (end < head) {
  465. const endHash = await getBlockHash(api, end);
  466. if (endHash) council.endDate = await getTimestamp(api, endHash);
  467. } else console.log(`fetchCouncil: round ${round} is ongoing.`);
  468. // TODO import report generator and save tokenomics
  469. saveCouncil(api, council, seats);
  470. saveCommitments(api, round, start - 2);
  471. };
  472. const saveCommitments = async (
  473. api: ApiPromise,
  474. round: number,
  475. block: number
  476. ) => {
  477. const hash = await getBlockHash(api, block);
  478. const commitments: Hash[] = await getCommitments(api, hash);
  479. const council = await Council.findByPk(round);
  480. if (!council)
  481. return console.warn(`saveCommitments: council ${round} not found.`);
  482. Promise.all(
  483. commitments.map((voteHash: Hash) => getCommitment(api, hash, voteHash))
  484. ).then((votes: SealedVote[]) =>
  485. votes.map(async (v) => {
  486. const voter: AccountId = v.voter;
  487. const stake = v.stake.new.toNumber();
  488. const vote = String(v.vote);
  489. const member = await fetchMemberByAccount(api, voter);
  490. const memberId = member?.id;
  491. Commitment.findOrCreate({
  492. where: { councilRound: round, stake, memberId },
  493. }).then(([c]: [CommitmentType]) => {
  494. if (vote) c.update({ vote });
  495. c.setCouncil(council.id);
  496. });
  497. })
  498. );
  499. };
  500. const saveCouncil = async (
  501. api: ApiPromise,
  502. council: Council,
  503. seats: Seat[]
  504. ) => {
  505. const { round } = council;
  506. Council.findOrCreate({ where: { round } }).then(
  507. ([council]: [CouncilType]) => {
  508. council.update(council);
  509. seats.map((seat) =>
  510. fetchMemberByAccount(api, seat.member).then(
  511. (member: MemberType | undefined) =>
  512. member && saveConsul(api, round, member.id, seat)
  513. )
  514. );
  515. }
  516. );
  517. };
  518. const saveConsul = async (
  519. api: ApiPromise,
  520. councilRound: number,
  521. memberId: number,
  522. seat?: Seat
  523. ) => {
  524. const [consul] = await Consul.findOrCreate({
  525. where: { councilRound, memberId },
  526. });
  527. if (!seat) return;
  528. const stake = Number(seat.stake);
  529. consul.update({ stake });
  530. seat.backers.map(async ({ member, stake }) =>
  531. fetchMemberByAccount(api, member).then(({ id }: any) =>
  532. saveCommitment(Number(stake), consul.id, id)
  533. )
  534. );
  535. };
  536. const saveCommitment = async (
  537. stake: number,
  538. consulId: number,
  539. memberId: number,
  540. vote?: string
  541. ) =>
  542. Commitment.findOrCreate({ where: { stake, consulId, memberId } }).then(
  543. ([c]: [CommitmentType]) => vote && c.update({ vote })
  544. );
  545. const fetchProposal = async (api: ApiPromise, id: number) => {
  546. if (id <= 0) return;
  547. queue.push(() => fetchProposal(api, id - 1));
  548. const exists = await Proposal.findByIdWithIncludes(id);
  549. if (exists && exists.result !== `Pending`) {
  550. if (!exists.votes.length) queue.push(() => fetchProposalVotes(api, id));
  551. return exists;
  552. }
  553. processing = `proposal ${id}`;
  554. const proposal = await getProposal(api, id as unknown as ProposalId);
  555. console.log(`proposal ${id}: ${proposal.result}`);
  556. await fetchMember(api, proposal.authorId);
  557. queue.push(() => fetchProposalVotes(api, id));
  558. // save
  559. const found = await Proposal.findByPk(id);
  560. if (found) Proposal.update(proposal, { where: { id } });
  561. else Proposal.create(proposal);
  562. return proposal;
  563. };
  564. const saveProposalPost = (id: number, proposalId: number, data: any) =>
  565. ProposalPost.findOrCreate({ where: { id } }).then(
  566. ([post]: [ProposalPostType]) => {
  567. post.update(data);
  568. post.setProposal(proposalId);
  569. console.log(post);
  570. }
  571. );
  572. const fetchProposalPosts = async (api: ApiPromise) => {
  573. processing = `proposal posts`;
  574. getProposalPosts(api).then((posts: [any, DiscussionPost][]) => {
  575. posts.map(async (p) => {
  576. const [proposalId, id] = p[0].toHuman();
  577. await fetchProposal(api, proposalId);
  578. const { text, created_at, author_id, edition_number } = p[1];
  579. saveProposalPost(id, proposalId, {
  580. text: text.toHuman(),
  581. created: created_at.toNumber(),
  582. version: edition_number.toNumber(),
  583. authorId: author_id.toNumber(),
  584. });
  585. });
  586. });
  587. };
  588. const councilAt = (block: number): Promise<CouncilType> | void => {
  589. if (block)
  590. return Council.findOne({
  591. where: { start: { [Op.lte]: block }, end: { [Op.gte]: block } },
  592. });
  593. };
  594. const fetchProposalVotes = async (api: ApiPromise, id: number) => {
  595. const proposal = await Proposal.findByPk(id);
  596. if (!proposal)
  597. return console.warn(`fetchProposalVotes: proposal ${id} not found.`);
  598. processing = `votes proposal ${proposal.id}`;
  599. // find council for creation and finalization time
  600. let councils: number[] = [];
  601. const { created, finalizedAt } = proposal;
  602. const councilStart = await councilAt(created);
  603. if (councilStart) {
  604. councilStart.addProposal(proposal.id);
  605. councils.push(councilStart.round);
  606. }
  607. const councilEnd = await councilAt(finalizedAt);
  608. if (councilEnd) councils.push(councilEnd.round);
  609. const votes = await getProposalVotes(api, id);
  610. votes?.forEach(({ memberId, vote }) =>
  611. saveProposalVote(id, councils, memberId, vote)
  612. );
  613. };
  614. const saveProposalVote = (
  615. proposalId: number,
  616. councils: number[],
  617. memberId: number,
  618. vote: string
  619. ): void =>
  620. Consul.findOne({
  621. where: { memberId, councilRound: { [Op.or]: councils } },
  622. }).then((consul: any) => {
  623. if (!consul)
  624. return console.log(`consul not found: member ${memberId}`, councils);
  625. const where = { memberId, proposalId, consulId: consul.id };
  626. if (!consul)
  627. return console.log(`saveProposalVote: No Consul found.`, where);
  628. ProposalVote.findOne({ where }).then((exists: any) => {
  629. const pv = { ...where, vote };
  630. if (!exists) ProposalVote.create(pv);
  631. });
  632. });
  633. // accounts
  634. const fetchAccounts = async (api: ApiPromise) => {
  635. processing = `accounts`;
  636. getAccounts(api).then((accounts: AccountBalance[]) =>
  637. accounts.map(({ accountId }) =>
  638. Account.findOrCreate({ where: { key: accountId } })
  639. )
  640. );
  641. };
  642. const fetchMemberByAccount = async (
  643. api: ApiPromise,
  644. accountId: AccountId
  645. ): Promise<MemberType | undefined> => {
  646. const rootKey = accountId.toHuman();
  647. const member = await Member.findOne({ where: { rootKey } });
  648. if (member) return member;
  649. const id = await getMemberIdByAccount(api, accountId);
  650. if (id) return fetchMember(api, id.toNumber());
  651. };
  652. const fetchMember = async (
  653. api: ApiPromise,
  654. id: number
  655. ): Promise<MemberType | undefined> => {
  656. if (id > 0) queue.push(() => fetchMember(api, id - 1));
  657. const exists = await Member.findByPk(id);
  658. if (exists && exists.handle) return exists;
  659. processing = `member ${id}`;
  660. const membership = await getMember(api, id);
  661. if (!membership) {
  662. console.warn(`fetchMember: empty membership`);
  663. return;
  664. }
  665. const about = String(membership.about);
  666. const handle = String(membership.handle);
  667. const created = +membership.registered_at_block;
  668. const rootKey = String(membership.root_account);
  669. const where = { id };
  670. return Member.findOrCreate({ where }).then(([member]: [MemberType]) => {
  671. member.update({ id, about, created, handle, rootKey });
  672. Account.findOrCreate({ where: { key: rootKey } }).then(([account]: any) =>
  673. account?.setMember(id)
  674. );
  675. return member;
  676. });
  677. };