membership.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. /*
  2. eslint-disable @typescript-eslint/naming-convention
  3. */
  4. import { EventContext, StoreContext, DatabaseManager, SubstrateEvent } from '@dzlzv/hydra-common'
  5. import { Members } from './generated/types'
  6. import { MemberId, BuyMembershipParameters, InviteMembershipParameters } from '@joystream/types/augment/all'
  7. import { MembershipMetadata } from '@joystream/metadata-protobuf'
  8. import { bytesToString, deserializeMetadata, genericEventFields } from './common'
  9. import {
  10. Membership,
  11. MembershipEntryMethod,
  12. MembershipSystemSnapshot,
  13. MemberMetadata,
  14. MembershipBoughtEvent,
  15. MemberProfileUpdatedEvent,
  16. MemberAccountsUpdatedEvent,
  17. MemberInvitedEvent,
  18. MemberVerificationStatusUpdatedEvent,
  19. InvitesTransferredEvent,
  20. StakingAccountConfirmedEvent,
  21. StakingAccountRemovedEvent,
  22. InitialInvitationCountUpdatedEvent,
  23. MembershipPriceUpdatedEvent,
  24. ReferralCutUpdatedEvent,
  25. InitialInvitationBalanceUpdatedEvent,
  26. StakingAccountAddedEvent,
  27. LeaderInvitationQuotaUpdatedEvent,
  28. MembershipEntryPaid,
  29. MembershipEntryInvited,
  30. } from 'query-node/dist/model'
  31. async function getMemberById(store: DatabaseManager, id: MemberId): Promise<Membership> {
  32. const member = await store.get(Membership, { where: { id: id.toString() }, relations: ['metadata'] })
  33. if (!member) {
  34. throw new Error(`Member(${id}) not found`)
  35. }
  36. return member
  37. }
  38. async function getLatestMembershipSystemSnapshot(store: DatabaseManager): Promise<MembershipSystemSnapshot> {
  39. const membershipSystem = await store.get(MembershipSystemSnapshot, {
  40. order: { snapshotBlock: 'DESC' },
  41. })
  42. if (!membershipSystem) {
  43. throw new Error(`Membership system snapshot not found! Forgot to run "yarn workspace query-node-root store:init"?`)
  44. }
  45. return membershipSystem
  46. }
  47. async function getOrCreateMembershipSnapshot({ store, event }: EventContext & StoreContext) {
  48. const latestSnapshot = await getLatestMembershipSystemSnapshot(store)
  49. const eventTime = new Date(event.blockTimestamp)
  50. return latestSnapshot.snapshotBlock === event.blockNumber
  51. ? latestSnapshot
  52. : new MembershipSystemSnapshot({
  53. ...latestSnapshot,
  54. createdAt: eventTime,
  55. updatedAt: eventTime,
  56. id: undefined,
  57. snapshotBlock: event.blockNumber,
  58. })
  59. }
  60. async function createNewMemberFromParams(
  61. store: DatabaseManager,
  62. event: SubstrateEvent,
  63. memberId: MemberId,
  64. entryMethod: typeof MembershipEntryMethod,
  65. params: BuyMembershipParameters | InviteMembershipParameters
  66. ): Promise<Membership> {
  67. const { defaultInviteCount } = await getLatestMembershipSystemSnapshot(store)
  68. const { root_account: rootAccount, controller_account: controllerAccount, handle, metadata: metatadaBytes } = params
  69. const metadata = deserializeMetadata(MembershipMetadata, metatadaBytes)
  70. const eventTime = new Date(event.blockTimestamp)
  71. const metadataEntity = new MemberMetadata({
  72. createdAt: eventTime,
  73. updatedAt: eventTime,
  74. name: metadata?.name || undefined,
  75. about: metadata?.about || undefined,
  76. // TODO: avatar
  77. })
  78. const member = new Membership({
  79. createdAt: eventTime,
  80. updatedAt: eventTime,
  81. id: memberId.toString(),
  82. rootAccount: rootAccount.toString(),
  83. controllerAccount: controllerAccount.toString(),
  84. handle: handle.unwrap().toString(),
  85. metadata: metadataEntity,
  86. entry: entryMethod,
  87. referredBy:
  88. entryMethod.isTypeOf === 'MembershipEntryPaid' && (params as BuyMembershipParameters).referrer_id.isSome
  89. ? new Membership({ id: (params as BuyMembershipParameters).referrer_id.unwrap().toString() })
  90. : undefined,
  91. isVerified: false,
  92. inviteCount: defaultInviteCount,
  93. boundAccounts: [],
  94. invitees: [],
  95. referredMembers: [],
  96. invitedBy:
  97. entryMethod.isTypeOf === 'MembershipEntryInvited'
  98. ? new Membership({ id: (params as InviteMembershipParameters).inviting_member_id.toString() })
  99. : undefined,
  100. isFoundingMember: false,
  101. isCouncilMember: false,
  102. })
  103. await store.save<MemberMetadata>(member.metadata)
  104. await store.save<Membership>(member)
  105. return member
  106. }
  107. export async function members_MembershipBought({ store, event }: EventContext & StoreContext): Promise<void> {
  108. const [memberId, buyMembershipParameters] = new Members.MembershipBoughtEvent(event).params
  109. const memberEntry = new MembershipEntryPaid()
  110. const member = await createNewMemberFromParams(store, event, memberId, memberEntry, buyMembershipParameters)
  111. const membershipBoughtEvent = new MembershipBoughtEvent({
  112. ...genericEventFields(event),
  113. newMember: member,
  114. controllerAccount: member.controllerAccount,
  115. rootAccount: member.rootAccount,
  116. handle: member.handle,
  117. metadata: new MemberMetadata({
  118. ...member.metadata,
  119. id: undefined,
  120. }),
  121. referrer: member.referredBy,
  122. })
  123. await store.save<MemberMetadata>(membershipBoughtEvent.metadata)
  124. await store.save<MembershipBoughtEvent>(membershipBoughtEvent)
  125. // Update the other side of event<->membership relation
  126. memberEntry.membershipBoughtEventId = membershipBoughtEvent.id
  127. await store.save<Membership>(member)
  128. }
  129. export async function members_MemberProfileUpdated({ store, event }: EventContext & StoreContext): Promise<void> {
  130. const [memberId] = new Members.MemberProfileUpdatedEvent(event).params
  131. const { metadata: metadataBytesOpt, handle } = new Members.UpdateProfileCall(event).args
  132. const metadata = metadataBytesOpt.isSome
  133. ? deserializeMetadata(MembershipMetadata, metadataBytesOpt.unwrap())
  134. : undefined
  135. const member = await getMemberById(store, memberId)
  136. const eventTime = new Date(event.blockTimestamp)
  137. // FIXME: https://github.com/Joystream/hydra/issues/435
  138. if (typeof metadata?.name === 'string') {
  139. member.metadata.name = (metadata.name || null) as string | undefined
  140. member.metadata.updatedAt = eventTime
  141. }
  142. if (typeof metadata?.about === 'string') {
  143. member.metadata.about = (metadata.about || null) as string | undefined
  144. member.metadata.updatedAt = eventTime
  145. }
  146. // TODO: avatar
  147. if (handle.isSome) {
  148. member.handle = bytesToString(handle.unwrap())
  149. member.updatedAt = eventTime
  150. }
  151. await store.save<MemberMetadata>(member.metadata)
  152. await store.save<Membership>(member)
  153. const memberProfileUpdatedEvent = new MemberProfileUpdatedEvent({
  154. ...genericEventFields(event),
  155. member: member,
  156. newHandle: member.handle,
  157. newMetadata: new MemberMetadata({
  158. ...member.metadata,
  159. id: undefined,
  160. }),
  161. })
  162. await store.save<MemberMetadata>(memberProfileUpdatedEvent.newMetadata)
  163. await store.save<MemberProfileUpdatedEvent>(memberProfileUpdatedEvent)
  164. }
  165. export async function members_MemberAccountsUpdated({ store, event }: EventContext & StoreContext): Promise<void> {
  166. const [memberId] = new Members.MemberAccountsUpdatedEvent(event).params
  167. const { newRootAccount, newControllerAccount } = new Members.UpdateAccountsCall(event).args
  168. const member = await getMemberById(store, memberId)
  169. const eventTime = new Date(event.blockTimestamp)
  170. if (newControllerAccount.isSome) {
  171. member.controllerAccount = newControllerAccount.unwrap().toString()
  172. }
  173. if (newRootAccount.isSome) {
  174. member.rootAccount = newRootAccount.unwrap().toString()
  175. }
  176. member.updatedAt = eventTime
  177. await store.save<Membership>(member)
  178. const memberAccountsUpdatedEvent = new MemberAccountsUpdatedEvent({
  179. ...genericEventFields(event),
  180. member: member,
  181. newRootAccount: member.rootAccount,
  182. newControllerAccount: member.controllerAccount,
  183. })
  184. await store.save<MemberAccountsUpdatedEvent>(memberAccountsUpdatedEvent)
  185. }
  186. export async function members_MemberVerificationStatusUpdated(
  187. store: DatabaseManager,
  188. event: SubstrateEvent
  189. ): Promise<void> {
  190. const [memberId, verificationStatus] = new Members.MemberVerificationStatusUpdatedEvent(event).params
  191. const member = await getMemberById(store, memberId)
  192. const eventTime = new Date(event.blockTimestamp)
  193. member.isVerified = verificationStatus.valueOf()
  194. member.updatedAt = eventTime
  195. await store.save<Membership>(member)
  196. const memberVerificationStatusUpdatedEvent = new MemberVerificationStatusUpdatedEvent({
  197. ...genericEventFields(event),
  198. member: member,
  199. isVerified: member.isVerified,
  200. })
  201. await store.save<MemberVerificationStatusUpdatedEvent>(memberVerificationStatusUpdatedEvent)
  202. }
  203. export async function members_InvitesTransferred({ store, event }: EventContext & StoreContext): Promise<void> {
  204. const [sourceMemberId, targetMemberId, numberOfInvites] = new Members.InvitesTransferredEvent(event).params
  205. const sourceMember = await getMemberById(store, sourceMemberId)
  206. const targetMember = await getMemberById(store, targetMemberId)
  207. const eventTime = new Date(event.blockTimestamp)
  208. sourceMember.inviteCount -= numberOfInvites.toNumber()
  209. sourceMember.updatedAt = eventTime
  210. targetMember.inviteCount += numberOfInvites.toNumber()
  211. targetMember.updatedAt = eventTime
  212. await store.save<Membership>(sourceMember)
  213. await store.save<Membership>(targetMember)
  214. const invitesTransferredEvent = new InvitesTransferredEvent({
  215. ...genericEventFields(event),
  216. sourceMember,
  217. targetMember,
  218. numberOfInvites: numberOfInvites.toNumber(),
  219. })
  220. await store.save<InvitesTransferredEvent>(invitesTransferredEvent)
  221. }
  222. export async function members_MemberInvited({ store, event }: EventContext & StoreContext): Promise<void> {
  223. const [memberId, inviteMembershipParameters] = new Members.MemberInvitedEvent(event).params
  224. const eventTime = new Date(event.blockTimestamp)
  225. const entryMethod = new MembershipEntryInvited()
  226. const invitedMember = await createNewMemberFromParams(store, event, memberId, entryMethod, inviteMembershipParameters)
  227. // Decrease invite count of inviting member
  228. const invitingMember = await getMemberById(store, inviteMembershipParameters.inviting_member_id)
  229. invitingMember.inviteCount -= 1
  230. invitingMember.updatedAt = eventTime
  231. await store.save<Membership>(invitingMember)
  232. const memberInvitedEvent = new MemberInvitedEvent({
  233. ...genericEventFields(event),
  234. invitingMember,
  235. newMember: invitedMember,
  236. handle: invitedMember.handle,
  237. rootAccount: invitedMember.rootAccount,
  238. controllerAccount: invitedMember.controllerAccount,
  239. metadata: new MemberMetadata({
  240. ...invitedMember.metadata,
  241. id: undefined,
  242. }),
  243. })
  244. await store.save<MemberMetadata>(memberInvitedEvent.metadata)
  245. await store.save<MemberInvitedEvent>(memberInvitedEvent)
  246. // Update the other side of event<->member relationship
  247. entryMethod.memberInvitedEventId = memberInvitedEvent.id
  248. await store.save<Membership>(invitedMember)
  249. }
  250. export async function members_StakingAccountAdded({ store, event }: EventContext & StoreContext): Promise<void> {
  251. const [accountId, memberId] = new Members.StakingAccountAddedEvent(event).params
  252. const stakingAccountAddedEvent = new StakingAccountAddedEvent({
  253. ...genericEventFields(event),
  254. member: new Membership({ id: memberId.toString() }),
  255. account: accountId.toString(),
  256. })
  257. await store.save<StakingAccountAddedEvent>(stakingAccountAddedEvent)
  258. }
  259. export async function members_StakingAccountConfirmed({ store, event }: EventContext & StoreContext): Promise<void> {
  260. const [accountId, memberId] = new Members.StakingAccountConfirmedEvent(event).params
  261. const member = await getMemberById(store, memberId)
  262. const eventTime = new Date(event.blockTimestamp)
  263. member.boundAccounts.push(accountId.toString())
  264. member.updatedAt = eventTime
  265. await store.save<Membership>(member)
  266. const stakingAccountConfirmedEvent = new StakingAccountConfirmedEvent({
  267. ...genericEventFields(event),
  268. member,
  269. account: accountId.toString(),
  270. })
  271. await store.save<StakingAccountConfirmedEvent>(stakingAccountConfirmedEvent)
  272. }
  273. export async function members_StakingAccountRemoved({ store, event }: EventContext & StoreContext): Promise<void> {
  274. const [accountId, memberId] = new Members.StakingAccountRemovedEvent(event).params
  275. const eventTime = new Date(event.blockTimestamp)
  276. const member = await getMemberById(store, memberId)
  277. member.boundAccounts.splice(
  278. member.boundAccounts.findIndex((a) => a === accountId.toString()),
  279. 1
  280. )
  281. member.updatedAt = eventTime
  282. await store.save<Membership>(member)
  283. const stakingAccountRemovedEvent = new StakingAccountRemovedEvent({
  284. ...genericEventFields(event),
  285. member,
  286. account: accountId.toString(),
  287. })
  288. await store.save<StakingAccountRemovedEvent>(stakingAccountRemovedEvent)
  289. }
  290. export async function members_InitialInvitationCountUpdated(ctx: EventContext & StoreContext): Promise<void> {
  291. const { event, store } = ctx
  292. const [newDefaultInviteCount] = new Members.InitialInvitationCountUpdatedEvent(event).params
  293. const membershipSystemSnapshot = await getOrCreateMembershipSnapshot(ctx)
  294. membershipSystemSnapshot.defaultInviteCount = newDefaultInviteCount.toNumber()
  295. await store.save<MembershipSystemSnapshot>(membershipSystemSnapshot)
  296. const initialInvitationCountUpdatedEvent = new InitialInvitationCountUpdatedEvent({
  297. ...genericEventFields(event),
  298. newInitialInvitationCount: newDefaultInviteCount.toNumber(),
  299. })
  300. await store.save<InitialInvitationCountUpdatedEvent>(initialInvitationCountUpdatedEvent)
  301. }
  302. export async function members_MembershipPriceUpdated(ctx: EventContext & StoreContext): Promise<void> {
  303. const { event, store } = ctx
  304. const [newMembershipPrice] = new Members.MembershipPriceUpdatedEvent(event).params
  305. const membershipSystemSnapshot = await getOrCreateMembershipSnapshot(ctx)
  306. membershipSystemSnapshot.membershipPrice = newMembershipPrice
  307. await store.save<MembershipSystemSnapshot>(membershipSystemSnapshot)
  308. const membershipPriceUpdatedEvent = new MembershipPriceUpdatedEvent({
  309. ...genericEventFields(event),
  310. newPrice: newMembershipPrice,
  311. })
  312. await store.save<MembershipPriceUpdatedEvent>(membershipPriceUpdatedEvent)
  313. }
  314. export async function members_ReferralCutUpdated(ctx: EventContext & StoreContext): Promise<void> {
  315. const { event, store } = ctx
  316. const [newReferralCut] = new Members.ReferralCutUpdatedEvent(event).params
  317. const membershipSystemSnapshot = await getOrCreateMembershipSnapshot(ctx)
  318. membershipSystemSnapshot.referralCut = newReferralCut.toNumber()
  319. await store.save<MembershipSystemSnapshot>(membershipSystemSnapshot)
  320. const referralCutUpdatedEvent = new ReferralCutUpdatedEvent({
  321. ...genericEventFields(event),
  322. newValue: newReferralCut.toNumber(),
  323. })
  324. await store.save<ReferralCutUpdatedEvent>(referralCutUpdatedEvent)
  325. }
  326. export async function members_InitialInvitationBalanceUpdated(ctx: EventContext & StoreContext): Promise<void> {
  327. const { event, store } = ctx
  328. const [newInvitedInitialBalance] = new Members.InitialInvitationBalanceUpdatedEvent(event).params
  329. const membershipSystemSnapshot = await getOrCreateMembershipSnapshot(ctx)
  330. membershipSystemSnapshot.invitedInitialBalance = newInvitedInitialBalance
  331. await store.save<MembershipSystemSnapshot>(membershipSystemSnapshot)
  332. const initialInvitationBalanceUpdatedEvent = new InitialInvitationBalanceUpdatedEvent({
  333. ...genericEventFields(event),
  334. newInitialBalance: newInvitedInitialBalance,
  335. })
  336. await store.save<InitialInvitationBalanceUpdatedEvent>(initialInvitationBalanceUpdatedEvent)
  337. }
  338. export async function members_LeaderInvitationQuotaUpdated({
  339. store,
  340. event,
  341. }: EventContext & StoreContext): Promise<void> {
  342. const [newQuota] = new Members.LeaderInvitationQuotaUpdatedEvent(event).params
  343. const leaderInvitationQuotaUpdatedEvent = new LeaderInvitationQuotaUpdatedEvent({
  344. ...genericEventFields(event),
  345. newInvitationQuota: newQuota.toNumber(),
  346. })
  347. await store.save<LeaderInvitationQuotaUpdatedEvent>(leaderInvitationQuotaUpdatedEvent)
  348. }