membership.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. /*
  2. eslint-disable @typescript-eslint/naming-convention
  3. */
  4. import { EventContext, StoreContext, DatabaseManager, SubstrateEvent } from '@joystream/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. })
  102. await store.save<MemberMetadata>(member.metadata)
  103. await store.save<Membership>(member)
  104. return member
  105. }
  106. export async function members_MembershipBought({ store, event }: EventContext & StoreContext): Promise<void> {
  107. const [memberId, buyMembershipParameters] = new Members.MembershipBoughtEvent(event).params
  108. const memberEntry = new MembershipEntryPaid()
  109. const member = await createNewMemberFromParams(store, event, memberId, memberEntry, buyMembershipParameters)
  110. const membershipBoughtEvent = new MembershipBoughtEvent({
  111. ...genericEventFields(event),
  112. newMember: member,
  113. controllerAccount: member.controllerAccount,
  114. rootAccount: member.rootAccount,
  115. handle: member.handle,
  116. metadata: new MemberMetadata({
  117. ...member.metadata,
  118. id: undefined,
  119. }),
  120. referrer: member.referredBy,
  121. })
  122. await store.save<MemberMetadata>(membershipBoughtEvent.metadata)
  123. await store.save<MembershipBoughtEvent>(membershipBoughtEvent)
  124. // Update the other side of event<->membership relation
  125. memberEntry.membershipBoughtEventId = membershipBoughtEvent.id
  126. await store.save<Membership>(member)
  127. }
  128. export async function members_MemberProfileUpdated({ store, event }: EventContext & StoreContext): Promise<void> {
  129. const [memberId, newHandle, newMetadata] = new Members.MemberProfileUpdatedEvent(event).params
  130. const metadata = newMetadata.isSome ? deserializeMetadata(MembershipMetadata, newMetadata.unwrap()) : undefined
  131. const member = await getMemberById(store, memberId)
  132. const eventTime = new Date(event.blockTimestamp)
  133. // FIXME: https://github.com/Joystream/hydra/issues/435
  134. if (typeof metadata?.name === 'string') {
  135. member.metadata.name = (metadata.name || null) as string | undefined
  136. member.metadata.updatedAt = eventTime
  137. }
  138. if (typeof metadata?.about === 'string') {
  139. member.metadata.about = (metadata.about || null) as string | undefined
  140. member.metadata.updatedAt = eventTime
  141. }
  142. // TODO: avatar
  143. if (newHandle.isSome) {
  144. member.handle = bytesToString(newHandle.unwrap())
  145. member.updatedAt = eventTime
  146. }
  147. await store.save<MemberMetadata>(member.metadata)
  148. await store.save<Membership>(member)
  149. const memberProfileUpdatedEvent = new MemberProfileUpdatedEvent({
  150. ...genericEventFields(event),
  151. member: member,
  152. newHandle: member.handle,
  153. newMetadata: new MemberMetadata({
  154. ...member.metadata,
  155. id: undefined,
  156. }),
  157. })
  158. await store.save<MemberMetadata>(memberProfileUpdatedEvent.newMetadata)
  159. await store.save<MemberProfileUpdatedEvent>(memberProfileUpdatedEvent)
  160. }
  161. export async function members_MemberAccountsUpdated({ store, event }: EventContext & StoreContext): Promise<void> {
  162. const [memberId, newRootAccount, newControllerAccount] = new Members.MemberAccountsUpdatedEvent(event).params
  163. const member = await getMemberById(store, memberId)
  164. const eventTime = new Date(event.blockTimestamp)
  165. if (newControllerAccount.isSome) {
  166. member.controllerAccount = newControllerAccount.unwrap().toString()
  167. }
  168. if (newRootAccount.isSome) {
  169. member.rootAccount = newRootAccount.unwrap().toString()
  170. }
  171. member.updatedAt = eventTime
  172. await store.save<Membership>(member)
  173. const memberAccountsUpdatedEvent = new MemberAccountsUpdatedEvent({
  174. ...genericEventFields(event),
  175. member: member,
  176. newRootAccount: member.rootAccount,
  177. newControllerAccount: member.controllerAccount,
  178. })
  179. await store.save<MemberAccountsUpdatedEvent>(memberAccountsUpdatedEvent)
  180. }
  181. export async function members_MemberVerificationStatusUpdated(
  182. store: DatabaseManager,
  183. event: SubstrateEvent
  184. ): Promise<void> {
  185. const [memberId, verificationStatus] = new Members.MemberVerificationStatusUpdatedEvent(event).params
  186. const member = await getMemberById(store, memberId)
  187. const eventTime = new Date(event.blockTimestamp)
  188. member.isVerified = verificationStatus.valueOf()
  189. member.updatedAt = eventTime
  190. await store.save<Membership>(member)
  191. const memberVerificationStatusUpdatedEvent = new MemberVerificationStatusUpdatedEvent({
  192. ...genericEventFields(event),
  193. member: member,
  194. isVerified: member.isVerified,
  195. })
  196. await store.save<MemberVerificationStatusUpdatedEvent>(memberVerificationStatusUpdatedEvent)
  197. }
  198. export async function members_InvitesTransferred({ store, event }: EventContext & StoreContext): Promise<void> {
  199. const [sourceMemberId, targetMemberId, numberOfInvites] = new Members.InvitesTransferredEvent(event).params
  200. const sourceMember = await getMemberById(store, sourceMemberId)
  201. const targetMember = await getMemberById(store, targetMemberId)
  202. const eventTime = new Date(event.blockTimestamp)
  203. sourceMember.inviteCount -= numberOfInvites.toNumber()
  204. sourceMember.updatedAt = eventTime
  205. targetMember.inviteCount += numberOfInvites.toNumber()
  206. targetMember.updatedAt = eventTime
  207. await store.save<Membership>(sourceMember)
  208. await store.save<Membership>(targetMember)
  209. const invitesTransferredEvent = new InvitesTransferredEvent({
  210. ...genericEventFields(event),
  211. sourceMember,
  212. targetMember,
  213. numberOfInvites: numberOfInvites.toNumber(),
  214. })
  215. await store.save<InvitesTransferredEvent>(invitesTransferredEvent)
  216. }
  217. export async function members_MemberInvited({ store, event }: EventContext & StoreContext): Promise<void> {
  218. const [memberId, inviteMembershipParameters] = new Members.MemberInvitedEvent(event).params
  219. const eventTime = new Date(event.blockTimestamp)
  220. const entryMethod = new MembershipEntryInvited()
  221. const invitedMember = await createNewMemberFromParams(store, event, memberId, entryMethod, inviteMembershipParameters)
  222. // Decrease invite count of inviting member
  223. const invitingMember = await getMemberById(store, inviteMembershipParameters.inviting_member_id)
  224. invitingMember.inviteCount -= 1
  225. invitingMember.updatedAt = eventTime
  226. await store.save<Membership>(invitingMember)
  227. const memberInvitedEvent = new MemberInvitedEvent({
  228. ...genericEventFields(event),
  229. invitingMember,
  230. newMember: invitedMember,
  231. handle: invitedMember.handle,
  232. rootAccount: invitedMember.rootAccount,
  233. controllerAccount: invitedMember.controllerAccount,
  234. metadata: new MemberMetadata({
  235. ...invitedMember.metadata,
  236. id: undefined,
  237. }),
  238. })
  239. await store.save<MemberMetadata>(memberInvitedEvent.metadata)
  240. await store.save<MemberInvitedEvent>(memberInvitedEvent)
  241. // Update the other side of event<->member relationship
  242. entryMethod.memberInvitedEventId = memberInvitedEvent.id
  243. await store.save<Membership>(invitedMember)
  244. }
  245. export async function members_StakingAccountAdded({ store, event }: EventContext & StoreContext): Promise<void> {
  246. const [accountId, memberId] = new Members.StakingAccountAddedEvent(event).params
  247. const stakingAccountAddedEvent = new StakingAccountAddedEvent({
  248. ...genericEventFields(event),
  249. member: new Membership({ id: memberId.toString() }),
  250. account: accountId.toString(),
  251. })
  252. await store.save<StakingAccountAddedEvent>(stakingAccountAddedEvent)
  253. }
  254. export async function members_StakingAccountConfirmed({ store, event }: EventContext & StoreContext): Promise<void> {
  255. const [accountId, memberId] = new Members.StakingAccountConfirmedEvent(event).params
  256. const member = await getMemberById(store, memberId)
  257. const eventTime = new Date(event.blockTimestamp)
  258. member.boundAccounts.push(accountId.toString())
  259. member.updatedAt = eventTime
  260. await store.save<Membership>(member)
  261. const stakingAccountConfirmedEvent = new StakingAccountConfirmedEvent({
  262. ...genericEventFields(event),
  263. member,
  264. account: accountId.toString(),
  265. })
  266. await store.save<StakingAccountConfirmedEvent>(stakingAccountConfirmedEvent)
  267. }
  268. export async function members_StakingAccountRemoved({ store, event }: EventContext & StoreContext): Promise<void> {
  269. const [accountId, memberId] = new Members.StakingAccountRemovedEvent(event).params
  270. const eventTime = new Date(event.blockTimestamp)
  271. const member = await getMemberById(store, memberId)
  272. member.boundAccounts.splice(
  273. member.boundAccounts.findIndex((a) => a === accountId.toString()),
  274. 1
  275. )
  276. member.updatedAt = eventTime
  277. await store.save<Membership>(member)
  278. const stakingAccountRemovedEvent = new StakingAccountRemovedEvent({
  279. ...genericEventFields(event),
  280. member,
  281. account: accountId.toString(),
  282. })
  283. await store.save<StakingAccountRemovedEvent>(stakingAccountRemovedEvent)
  284. }
  285. export async function members_InitialInvitationCountUpdated(ctx: EventContext & StoreContext): Promise<void> {
  286. const { event, store } = ctx
  287. const [newDefaultInviteCount] = new Members.InitialInvitationCountUpdatedEvent(event).params
  288. const membershipSystemSnapshot = await getOrCreateMembershipSnapshot(ctx)
  289. membershipSystemSnapshot.defaultInviteCount = newDefaultInviteCount.toNumber()
  290. await store.save<MembershipSystemSnapshot>(membershipSystemSnapshot)
  291. const initialInvitationCountUpdatedEvent = new InitialInvitationCountUpdatedEvent({
  292. ...genericEventFields(event),
  293. newInitialInvitationCount: newDefaultInviteCount.toNumber(),
  294. })
  295. await store.save<InitialInvitationCountUpdatedEvent>(initialInvitationCountUpdatedEvent)
  296. }
  297. export async function members_MembershipPriceUpdated(ctx: EventContext & StoreContext): Promise<void> {
  298. const { event, store } = ctx
  299. const [newMembershipPrice] = new Members.MembershipPriceUpdatedEvent(event).params
  300. const membershipSystemSnapshot = await getOrCreateMembershipSnapshot(ctx)
  301. membershipSystemSnapshot.membershipPrice = newMembershipPrice
  302. await store.save<MembershipSystemSnapshot>(membershipSystemSnapshot)
  303. const membershipPriceUpdatedEvent = new MembershipPriceUpdatedEvent({
  304. ...genericEventFields(event),
  305. newPrice: newMembershipPrice,
  306. })
  307. await store.save<MembershipPriceUpdatedEvent>(membershipPriceUpdatedEvent)
  308. }
  309. export async function members_ReferralCutUpdated(ctx: EventContext & StoreContext): Promise<void> {
  310. const { event, store } = ctx
  311. const [newReferralCut] = new Members.ReferralCutUpdatedEvent(event).params
  312. const membershipSystemSnapshot = await getOrCreateMembershipSnapshot(ctx)
  313. membershipSystemSnapshot.referralCut = newReferralCut.toNumber()
  314. await store.save<MembershipSystemSnapshot>(membershipSystemSnapshot)
  315. const referralCutUpdatedEvent = new ReferralCutUpdatedEvent({
  316. ...genericEventFields(event),
  317. newValue: newReferralCut.toNumber(),
  318. })
  319. await store.save<ReferralCutUpdatedEvent>(referralCutUpdatedEvent)
  320. }
  321. export async function members_InitialInvitationBalanceUpdated(ctx: EventContext & StoreContext): Promise<void> {
  322. const { event, store } = ctx
  323. const [newInvitedInitialBalance] = new Members.InitialInvitationBalanceUpdatedEvent(event).params
  324. const membershipSystemSnapshot = await getOrCreateMembershipSnapshot(ctx)
  325. membershipSystemSnapshot.invitedInitialBalance = newInvitedInitialBalance
  326. await store.save<MembershipSystemSnapshot>(membershipSystemSnapshot)
  327. const initialInvitationBalanceUpdatedEvent = new InitialInvitationBalanceUpdatedEvent({
  328. ...genericEventFields(event),
  329. newInitialBalance: newInvitedInitialBalance,
  330. })
  331. await store.save<InitialInvitationBalanceUpdatedEvent>(initialInvitationBalanceUpdatedEvent)
  332. }
  333. export async function members_LeaderInvitationQuotaUpdated({
  334. store,
  335. event,
  336. }: EventContext & StoreContext): Promise<void> {
  337. const [newQuota] = new Members.LeaderInvitationQuotaUpdatedEvent(event).params
  338. const leaderInvitationQuotaUpdatedEvent = new LeaderInvitationQuotaUpdatedEvent({
  339. ...genericEventFields(event),
  340. newInvitationQuota: newQuota.toNumber(),
  341. })
  342. await store.save<LeaderInvitationQuotaUpdatedEvent>(leaderInvitationQuotaUpdatedEvent)
  343. }