1
0

index.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676
  1. import { Op } from 'sequelize'
  2. import {
  3. Account,
  4. Balance,
  5. Block,
  6. Category,
  7. Channel,
  8. Council,
  9. Consul,
  10. ConsulStake,
  11. Era,
  12. Event,
  13. Member,
  14. Post,
  15. Proposal,
  16. ProposalPost,
  17. ProposalVote,
  18. Thread,
  19. Moderation,
  20. } from '../db/models'
  21. import * as get from './lib/getters'
  22. //import {fetchReports} from './lib/github'
  23. import axios from 'axios'
  24. import moment from 'moment'
  25. import chalk from 'chalk'
  26. import { VoteKind } from '@joystream/types/proposals'
  27. import { Seats } from '@joystream/types/council'
  28. import { AccountInfo } from '@polkadot/types/interfaces/system'
  29. import {
  30. Api,
  31. Handles,
  32. IState,
  33. MemberType,
  34. CategoryType,
  35. ChannelType,
  36. PostType,
  37. Seat,
  38. ThreadType,
  39. CouncilType,
  40. ProposalDetail,
  41. Status,
  42. } from '../types'
  43. import {
  44. AccountId,
  45. Moment,
  46. ActiveEraInfo,
  47. EventRecord,
  48. } from '@polkadot/types/interfaces'
  49. import Option from '@polkadot/types/codec/Option'
  50. import { Vec } from '@polkadot/types'
  51. // TODO fetch consts from db/chain
  52. const TERMDURATION = 144000
  53. const VOTINGDURATION = 57601
  54. const CYCLE = VOTINGDURATION + TERMDURATION
  55. const DELAY = 0 // ms
  56. let lastUpdate = 0
  57. let queuedAll = false
  58. let queue: any[] = []
  59. let processing = ''
  60. let busy = false
  61. const processNext = async () => {
  62. if (busy) return
  63. const task = queue.shift()
  64. if (!task) return
  65. const result = await task()
  66. busy = false
  67. setTimeout(() => processNext(), DELAY)
  68. }
  69. const getBlockHash = (api: Api, blockId: number) =>
  70. api.rpc.chain.getBlockHash(blockId).then((array: any) => array.toHuman())
  71. const getEraAtHash = (api: Api, hash: string) =>
  72. api.query.staking.activeEra
  73. .at(hash)
  74. .then((era: Option<ActiveEraInfo>) => era.unwrap().index.toNumber())
  75. const getEraAtBlock = async (api: Api, block: number) =>
  76. getEraAtHash(api, await getBlockHash(api, block))
  77. const getTimestamp = async (api: Api, hash?: string) => {
  78. const timestamp = hash
  79. ? await api.query.timestamp.now.at(hash)
  80. : await api.query.timestamp.now()
  81. return moment.utc(timestamp.toNumber()).valueOf()
  82. }
  83. const findCouncilAtBlock = (api: Api, block: number) =>
  84. Council.findOne({
  85. where: {
  86. start: { [Op.lte]: block },
  87. end: { [Op.gte]: block - VOTINGDURATION },
  88. },
  89. })
  90. const addBlock = async (
  91. api: Api,
  92. io: any,
  93. header: { number: number; author: string },
  94. status: Status = {
  95. block: 0,
  96. era: 0,
  97. round: 0,
  98. members: 0,
  99. channels: 0,
  100. categories: 0,
  101. threads: 0,
  102. posts: 0,
  103. proposals: 0,
  104. proposalPosts: 0,
  105. }
  106. ): Promise<Status> => {
  107. const id = +header.number
  108. const exists = await Block.findByPk(id)
  109. if (exists) {
  110. console.error(`TODO handle fork`, String(header.author))
  111. return status
  112. }
  113. const block = await processBlock(api, id)
  114. const key = header.author?.toString()
  115. const [account] = await Account.findOrCreate({ where: { key } })
  116. await block.setValidator(account.key)
  117. //account.addBlock(block.id) // TODO needed?
  118. io.emit('block', await Block.findByIdWithIncludes(id))
  119. // logging
  120. const handle = await getHandleOrKey(api, key)
  121. const q = queue.length ? chalk.green(` [${queue.length}:${processing}]`) : ''
  122. console.log(`[Joystream] block ${id} ${handle} ${q}`)
  123. return updateStatus(api, status, id)
  124. }
  125. const processBlock = async (api: Api, id: number) => {
  126. const exists = await Block.findByPk(id)
  127. if (exists) return exists
  128. processing = `block ${id}`
  129. const last = await Block.findByPk(id - 1)
  130. let lastBlockTimestamp;
  131. if (last) {
  132. lastBlockTimestamp = last.timestamp.getTime();
  133. } else {
  134. let lastBlockHash = await getBlockHash(api, id - 1);
  135. lastBlockTimestamp = await getTimestamp(api, lastBlockHash);
  136. }
  137. const [block] = await Block.findOrCreate({ where: { id } })
  138. block.hash = await getBlockHash(api, id)
  139. let currentBlockTimestamp = await getTimestamp(api, block.hash);
  140. block.timestamp = new Date(currentBlockTimestamp)
  141. block.blocktime = (currentBlockTimestamp - lastBlockTimestamp)
  142. block.save()
  143. processEvents(api, id, block.hash)
  144. await importEraAtBlock(api, id, block.hash)
  145. return block
  146. }
  147. const addBlockRange = async (
  148. api: Api,
  149. startBlock: number,
  150. endBlock: number
  151. ) => {
  152. for (let block = startBlock; block <= endBlock; block++)
  153. queue.push(() => processBlock(api, block))
  154. }
  155. const updateStatus = async (api: Api, old: Status, block: number) => {
  156. const status = {
  157. block,
  158. era: await getEraAtBlock(api, block),
  159. round: Number(await api.query.councilElection.round()),
  160. members: (await api.query.members.nextMemberId()) - 1,
  161. channels: await get.currentChannelId(api),
  162. categories: await get.currentCategoryId(api),
  163. threads: await get.currentThreadId(api),
  164. posts: await get.currentPostId(api),
  165. proposals: await get.proposalCount(api),
  166. proposalPosts: (await api.query.proposalsDiscussion.postCount()).toHuman(),
  167. }
  168. if (!queuedAll) fetchAll(api, status)
  169. else {
  170. // TODO catch if more than one are added
  171. status.members > old.members && fetchMember(api, status.members)
  172. status.posts > old.posts && fetchPost(api, status.posts)
  173. status.proposals > old.proposals && fetchProposal(api, status.proposals)
  174. status.channels > old.channels && fetchChannel(api, status.channels)
  175. status.categories > old.categories && fetchCategory(api, status.categories)
  176. status.proposalPosts > old.proposalPosts &&
  177. fetchProposalPosts(api, status.proposalPosts)
  178. }
  179. return status
  180. }
  181. const fetchAll = async (api: Api, status: Status) => {
  182. queue.push(() => fetchAccounts(api, status.block))
  183. for (let id = status.members; id > 0; id--) {
  184. queue.push(() => fetchMember(api, id))
  185. }
  186. for (let id = status.round; id > 0; id--) {
  187. queue.push(() => fetchCouncil(api, id))
  188. }
  189. for (let id = status.proposals; id > 0; id--) {
  190. queue.push(() => fetchProposal(api, id))
  191. }
  192. for (let id = status.channels; id > 0; id--) {
  193. queue.push(() => fetchChannel(api, id))
  194. }
  195. for (let id = status.categories; id > 0; id--) {
  196. queue.push(() => fetchCategory(api, id))
  197. }
  198. for (let id = status.threads; id > 0; id--) {
  199. queue.push(() => fetchThread(api, id))
  200. }
  201. for (let id = status.posts; id > 0; id--) {
  202. queue.push(() => fetchPost(api, id))
  203. }
  204. queue.push(() => fetchProposalPosts(api, status.proposalPosts))
  205. queue.push(() => addBlockRange(api, 1, status.block))
  206. queuedAll = true
  207. processNext()
  208. }
  209. const processEvents = async (api: Api, blockId: number, hash: string) => {
  210. processing = `events block ${blockId}`
  211. try {
  212. const blockEvents = await api.query.system.events.at(hash)
  213. blockEvents.forEach(({ event }: EventRecord) => {
  214. let { section, method, data } = event
  215. Event.create({ blockId, section, method, data: JSON.stringify(data) })
  216. })
  217. } catch (e) {
  218. console.log(`failed to fetch events for block ${blockId} ${hash}`)
  219. }
  220. // TODO catch votes, posts, proposals?
  221. }
  222. const fetchValidators = async (api: Api, hash: string) =>
  223. api.query.staking.snapshotValidators.at(hash) as Option<Vec<AccountId>>
  224. const importEraAtBlock = async (api: Api, blockId: number, hash: string) => {
  225. const id = await getEraAtHash(api, hash)
  226. const [era] = await Era.findOrCreate({ where: { id } })
  227. era.addBlock(blockId)
  228. if (era.active) return
  229. processing = `era ${id}`
  230. try {
  231. fetchValidators(api, hash).then(
  232. async (snapshot: Option<Vec<AccountId>>) => {
  233. if (snapshot.isEmpty) return
  234. console.log(`[Joystream] Found validator info for era ${id}`)
  235. const validatorCount = snapshot.unwrap().length
  236. era.slots = (await api.query.staking.validatorCount.at(hash)).toNumber()
  237. era.active = Math.min(era.slots, validatorCount)
  238. era.waiting =
  239. validatorCount > era.slots ? validatorCount - era.slots : 0
  240. era.stake = await api.query.staking.erasTotalStake.at(hash, id)
  241. const chainTimestamp = (await api.query.timestamp.now.at(
  242. hash
  243. )) as Moment
  244. era.timestamp = moment(chainTimestamp.toNumber())
  245. // era.update({ slots, active, waiting, stake, timestamp })
  246. era.blockId = id
  247. era.save()
  248. updateBalances(api, hash)
  249. }
  250. )
  251. } catch (e) {
  252. console.error(`import era ${blockId} ${hash}`, e)
  253. }
  254. }
  255. const validatorStatus = async (api: Api, blockId: number) => {
  256. const hash = await getBlockHash(api, blockId)
  257. let totalValidators = await api.query.staking.snapshotValidators.at(hash)
  258. if (totalValidators.isEmpty) return
  259. let totalNrValidators = totalValidators.unwrap().length
  260. const maxSlots = Number(await api.query.staking.validatorCount.at(hash))
  261. const actives = Math.min(maxSlots, totalNrValidators)
  262. const waiting =
  263. totalNrValidators > maxSlots ? totalNrValidators - maxSlots : 0
  264. let timestamp = await api.query.timestamp.now.at(hash)
  265. const date = moment(timestamp.toNumber()).valueOf()
  266. return { blockId, actives, waiting, maxSlots, date }
  267. }
  268. const updateBalances = async (api: Api, blockHash: string) => {
  269. const currentEra: number = await api.query.staking.currentEra.at(blockHash)
  270. const era = await Era.findOrCreate({ where: { id: currentEra } })
  271. try {
  272. processing = `balances ${era}`
  273. Account.findAll().then(async (account: any) => {
  274. const { key } = account
  275. if (!key) return
  276. console.log(`updating balance of`, key, key)
  277. const { data } = await getAccountAtBlock(api, blockHash, key)
  278. const { free, reserved, miscFrozen, feeFrozen } = data
  279. const balance = { available: free, reserved, frozen: miscFrozen }
  280. console.log(`balance ${era}`, balance)
  281. Balance.create(balance).then((balance: any) => {
  282. balance.setAccount(key)
  283. balance.setEra(era.id)
  284. console.log(`balance`, era.id, key, balance.available)
  285. })
  286. })
  287. } catch (e) {
  288. console.error(`balances era ${era}`)
  289. }
  290. }
  291. const fetchTokenomics = async () => {
  292. console.debug(`Updating tokenomics`)
  293. const { data } = await axios.get('https://status.joystream.org/status')
  294. if (!data) return
  295. // TODO save 'tokenomics', data
  296. }
  297. const fetchChannel = async (api: Api, id: number) => {
  298. if (id <= 0) return
  299. const exists = await Channel.findByPk(id)
  300. if (exists) return exists
  301. processing = `channel ${id}`
  302. const data = await api.query.contentWorkingGroup.channelById(id)
  303. const { handle, title, description, avatar, banner, content, created } = data
  304. // TODO const accountId = String(data.role_account)
  305. const channel = {
  306. id,
  307. handle: String(handle),
  308. title: String(title),
  309. description: String(description),
  310. avatar: String(avatar),
  311. banner: String(banner),
  312. content: String(content),
  313. publicationStatus: data.publication_status === 'Public' ? true : false,
  314. curation: String(data.curation_status),
  315. createdAt: +created,
  316. principal: Number(data.principal_id),
  317. }
  318. const chan = await Channel.create(channel)
  319. const owner = await fetchMember(api, data.owner)
  320. chan.setOwner(owner)
  321. return chan
  322. }
  323. const fetchCategory = async (api: Api, id: number) => {
  324. if (id <= 0) return
  325. const exists = await Category.findByPk(+id)
  326. if (exists) return exists
  327. processing = `category ${id}`
  328. const data = await api.query.forum.categoryById(id)
  329. const { title, description, deleted, archived } = data
  330. const category = await Category.create({
  331. id,
  332. title,
  333. threadId: +data.thread_id, // TODO needed?
  334. description,
  335. createdAt: +data.created_at.block,
  336. deleted,
  337. archived,
  338. subcategories: Number(data.num_direct_subcategories),
  339. moderatedThreads: Number(data.num_direct_moderated_threads),
  340. unmoderatedThreads: Number(data.num_direct_unmoderated_threads),
  341. //position:+data.position_in_parent_category // TODO sometimes NaN,
  342. })
  343. createModeration(api, { categoryId: id }, String(data.moderator_id), category)
  344. return category
  345. }
  346. const fetchPost = async (api: Api, id: number) => {
  347. if (id <= 0) return
  348. const exists = await Post.findByPk(id)
  349. if (exists) return exists
  350. processing = `post ${id}`
  351. const data = await api.query.forum.postById(id)
  352. const author: string = String(data.author_id)
  353. const member = await fetchMemberByAccount(api, author)
  354. const authorId = member ? member.id : null
  355. const threadId = Number(data.thread_id)
  356. const thread = await fetchThread(api, threadId)
  357. const text = data.current_text
  358. const history = data.text_change_history // TODO needed?
  359. const createdAt = data.created_at.block
  360. const post = await Post.create({ id, authorId, text, createdAt, threadId })
  361. if (data.moderation)
  362. createModeration(api, { postId: id }, data.moderation, post)
  363. return post
  364. }
  365. const createModeration = async (
  366. api: Api,
  367. where: {},
  368. key: string,
  369. object: { setModeration: (id: number) => {} }
  370. ) => {
  371. if (key === '') return
  372. await Account.findOrCreate({ where: { key } })
  373. const moderation = await Moderation.create({ moderatorKey: key })
  374. object.setModeration(moderation.id)
  375. return moderation
  376. }
  377. const fetchThread = async (api: Api, id: number) => {
  378. if (id <= 0) return
  379. const exists = await Thread.findByPk(id)
  380. if (exists) return exists
  381. processing = `thread ${id}`
  382. const data = await api.query.forum.threadById(id)
  383. const { title, moderation, nr_in_category } = data
  384. const account = String(data.author_id)
  385. const t = {
  386. id,
  387. title,
  388. nrInCategory: +nr_in_category,
  389. createdAt: +data.created_at.block,
  390. }
  391. const thread = await Thread.create(t)
  392. const category = await fetchCategory(api, +data.category_id)
  393. if (category) thread.setCategory(category.id)
  394. const author = await fetchMemberByAccount(api, account)
  395. if (author) thread.setCreator(author.id)
  396. if (moderation) {
  397. const { moderated_at, moderator_id, rationale } = moderation
  398. const created = moderated_at.block
  399. const createdAt = moment.utc(moderated_at.time)
  400. createModeration(
  401. api,
  402. { created, createdAt, rationale },
  403. moderator_id.toHuman(),
  404. thread
  405. )
  406. }
  407. return thread
  408. }
  409. const fetchCouncil = async (api: Api, round: number) => {
  410. if (round <= 0) return console.log(chalk.red(`[fetchCouncil] round:${round}`))
  411. const exists = await Council.findByPk(round)
  412. if (exists) return exists
  413. processing = `council ${round}`
  414. const start = 57601 + (round - 1) * CYCLE
  415. const end = start + TERMDURATION
  416. let council = { round, start, end, startDate: 0, endDate: 0 }
  417. let seats: Seats
  418. try {
  419. const startHash = await getBlockHash(api, start)
  420. council.startDate = await getTimestamp(api, startHash)
  421. seats = await api.query.council.activeCouncil.at(startHash)
  422. } catch (e) {
  423. return console.log(`council term ${round} lies in the future ${start}`)
  424. }
  425. try {
  426. const endHash = await getBlockHash(api, end)
  427. council.endDate = await getTimestamp(api, endHash)
  428. } catch (e) {
  429. console.warn(`end of council term ${round} lies in the future ${end}`)
  430. }
  431. try {
  432. Council.create(council).then(({ round }: any) =>
  433. seats.map(({ member, stake, backers }) =>
  434. fetchMemberByAccount(api, member.toHuman()).then(({ id }: any) =>
  435. Consul.create({
  436. stake: Number(stake),
  437. councilRound: round,
  438. memberId: id,
  439. }).then((consul: any) =>
  440. backers.map(async ({ member, stake }) =>
  441. fetchMemberByAccount(api, member.toHuman()).then(({ id }: any) =>
  442. ConsulStake.create({
  443. stake: Number(stake),
  444. consulId: consul.id,
  445. memberId: id,
  446. })
  447. )
  448. )
  449. )
  450. )
  451. )
  452. )
  453. } catch (e) {
  454. console.error(`Failed to save council ${round}`, e)
  455. }
  456. }
  457. const fetchProposal = async (api: Api, id: number) => {
  458. if (id <= 0) return
  459. const exists = await Proposal.findByPk(+id)
  460. if (exists) {
  461. fetchProposalVotes(api, exists)
  462. return exists
  463. }
  464. processing = `proposal ${id}`
  465. const proposal = await get.proposalDetail(api, id)
  466. await fetchMember(api, proposal.authorId)
  467. fetchProposalVotes(api, proposal)
  468. return Proposal.create(proposal)
  469. }
  470. const fetchProposalPost = (api: Api, threadId: number, postId: number) =>
  471. api.query.proposalsDiscussion.postThreadIdByPostId(threadId, postId)
  472. const fetchProposalPosts = async (api: Api, posts: number) => {
  473. const threads = (await api.query.proposalsDiscussion.threadCount()).toNumber()
  474. let proposalId = 1
  475. for (let id = 1; id <= posts && proposalId <= threads; ) {
  476. const exists = await ProposalPost.findByPk(id)
  477. if (exists) {
  478. id++
  479. proposalId = 1
  480. continue
  481. }
  482. processing = `proposal post ${id}/${posts} ${proposalId}/${threads}`
  483. const post = await fetchProposalPost(api, proposalId, id)
  484. if (!post.text.length) {
  485. proposalId++
  486. continue
  487. }
  488. const proposal = await Proposal.findByPk(proposalId)
  489. if (!proposal) {
  490. console.warn(`[fetchProposalPosts] proposal ${proposalId} not found.`)
  491. id++
  492. continue
  493. }
  494. ProposalPost.create({
  495. id,
  496. text: post.text.toHuman(),
  497. created: Number(post.created_at),
  498. updated: Number(post.updated_at),
  499. edition: Number(post.edition_number),
  500. authorId: Number(post.author_id),
  501. }).then((p: any) => proposal.addPost(p))
  502. id++
  503. proposalId = 1
  504. }
  505. }
  506. const fetchProposalVotes = async (api: Api, proposal: ProposalDetail) => {
  507. if (!proposal) return console.error(`[fetchProposalVotes] empty proposal`)
  508. processing = `votes proposal ${proposal.id}`
  509. const { createdAt, finalizedAt } = proposal
  510. try {
  511. const start = createdAt ? await findCouncilAtBlock(api, createdAt) : null
  512. if (start) start.addProposal(proposal.id)
  513. else
  514. return console.error(
  515. `[fetchProposalVotes] no council found for proposal ${proposal.id}`
  516. )
  517. // some proposals make it into a second term
  518. const end = finalizedAt ? await findCouncilAtBlock(api, finalizedAt) : null
  519. const councils = [start && start.round, end && end.round]
  520. const consuls = await Consul.findAll({
  521. where: { councilRound: { [Op.or]: councils } },
  522. })
  523. consuls.map(({ id, memberId }: any) =>
  524. fetchProposalVoteByConsul(api, proposal.id, id, memberId)
  525. )
  526. } catch (e) {
  527. console.log(`failed to fetch votes of proposal ${proposal.id}`, e)
  528. }
  529. }
  530. const fetchProposalVoteByConsul = async (
  531. api: Api,
  532. proposalId: number,
  533. consulId: number,
  534. memberId: number
  535. ): Promise<any> => {
  536. processing = `vote by ${consulId} for proposal ${proposalId}`
  537. const exists = await ProposalVote.findOne({
  538. where: { proposalId, memberId, consulId },
  539. })
  540. if (exists) return exists
  541. const query = api.query.proposalsEngine
  542. const args = [proposalId, memberId]
  543. const hasVoted = await query.voteExistsByProposalByVoter.size(...args)
  544. if (!hasVoted.toNumber()) return
  545. const vote = (await query.voteExistsByProposalByVoter(...args)).toHuman()
  546. await fetchMember(api, memberId) // TODO needed?
  547. return ProposalVote.create({ vote: vote, proposalId, consulId, memberId })
  548. }
  549. // accounts
  550. const getHandleOrKey = async (api: Api, key: string) => {
  551. const member = await fetchMemberByAccount(api, key)
  552. return member ? member.handle : key //abbrKey(key)
  553. }
  554. const abbrKey = (key: string) =>
  555. `${key.slice(0, 5)}..${key.slice(key.length - 5)}`
  556. const getAccountAtBlock = (
  557. api: Api,
  558. hash: string,
  559. account: string
  560. ): Promise<AccountInfo> => api.query.system.account.at(hash, account)
  561. const fetchAccounts = async (api: Api, blockId: number) => {
  562. processing = `accounts`
  563. api.query.system.account
  564. .entries()
  565. .then((account: any) =>
  566. Account.findOrCreate({ where: { key: account[0][0].toHuman()[0] } })
  567. )
  568. }
  569. const fetchMemberByAccount = async (api: Api, rootKey: string) => {
  570. const member = await Member.findOne({ where: { rootKey } })
  571. if (member) return member
  572. const id = Number(await get.memberIdByAccount(api, rootKey))
  573. if (id) return fetchMember(api, id)
  574. else Account.findOrCreate({ where: { key: rootKey } })
  575. }
  576. const fetchMember = async (
  577. api: Api,
  578. id: number
  579. ): Promise<MemberType | undefined> => {
  580. if (id <= 0) return
  581. const exists = await Member.findByPk(+id)
  582. if (exists) return exists
  583. processing = `member ${id}`
  584. const membership = await get.membership(api, id)
  585. const about = String(membership.about)
  586. const handle = String(membership.handle)
  587. const createdAt = +membership.registered_at_block
  588. const rootKey = String(membership.root_account)
  589. return Member.create({ id, about, createdAt, handle, rootKey }).then(
  590. (member: any) => {
  591. Account.findOrCreate({ where: { key: rootKey } }).then(([account]: any) =>
  592. account.setMember(id)
  593. )
  594. return member
  595. }
  596. )
  597. }
  598. module.exports = { addBlock, addBlockRange }