3
1

index.ts 20 KB

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