common.ts 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. import {
  2. DatabaseManager,
  3. SubstrateEvent,
  4. SubstrateExtrinsic,
  5. ExtrinsicArg,
  6. EventContext,
  7. StoreContext,
  8. } from '@joystream/hydra-common'
  9. import { Bytes } from '@polkadot/types'
  10. import { WorkingGroup, WorkerId, ThreadId, ContentParameters } from '@joystream/types/augment/all'
  11. import { Worker, Event, Network, DataObject, LiaisonJudgement, DataObjectOwner } from 'query-node/dist/model'
  12. import { BaseModel } from '@joystream/warthog'
  13. import { ContentParameters as Custom_ContentParameters } from '@joystream/types/storage'
  14. import { registry } from '@joystream/types'
  15. import { metaToObject } from '@joystream/metadata-protobuf/utils'
  16. import { AnyMetadataClass, DecodedMetadataObject } from '@joystream/metadata-protobuf/types'
  17. import BN from 'bn.js'
  18. export const CURRENT_NETWORK = Network.OLYMPIA
  19. /*
  20. Simple logger enabling error and informational reporting.
  21. FIXME: `Logger` class will not be needed in the future when Hydra v3 will be released.
  22. Hydra will provide logger instance and relevant code using `Logger` should be refactored.
  23. */
  24. class Logger {
  25. /*
  26. Log significant event.
  27. */
  28. info(message: string, data?: unknown) {
  29. console.log(message, data)
  30. }
  31. /*
  32. Log significant error.
  33. */
  34. error(message: string, data?: unknown) {
  35. console.error(message, data)
  36. }
  37. }
  38. export const logger = new Logger()
  39. /*
  40. Reports that insurmountable inconsistent state has been encountered and throws an exception.
  41. */
  42. export function inconsistentState(extraInfo: string, data?: unknown): never {
  43. const errorMessage = 'Inconsistent state: ' + extraInfo
  44. // log error
  45. logger.error(errorMessage, data)
  46. throw errorMessage
  47. }
  48. /*
  49. Reports that insurmountable unexpected data has been encountered and throws an exception.
  50. */
  51. export function unexpectedData(extraInfo: string, data?: unknown): never {
  52. const errorMessage = 'Unexpected data: ' + extraInfo
  53. // log error
  54. logger.error(errorMessage, data)
  55. throw errorMessage
  56. }
  57. /*
  58. Reports that metadata inserted by the user are not entirely valid, but the problem can be overcome.
  59. */
  60. export function invalidMetadata(extraInfo: string, data?: unknown): void {
  61. const errorMessage = 'Invalid metadata: ' + extraInfo
  62. // log error
  63. logger.info(errorMessage, data)
  64. }
  65. export async function createDataObject(
  66. { event, store }: EventContext & StoreContext,
  67. contentParameters: ContentParameters,
  68. owner: typeof DataObjectOwner
  69. ): Promise<DataObject> {
  70. const {
  71. size_in_bytes: sizeInBytes,
  72. type_id: typeId,
  73. content_id: contentId,
  74. ipfs_content_id: ipfsContentId,
  75. } = new Custom_ContentParameters(registry, contentParameters.toJSON() as any)
  76. const dataObjectId = contentId.encode()
  77. const dataObject = new DataObject({
  78. id: dataObjectId,
  79. owner,
  80. createdAt: new Date(event.blockTimestamp),
  81. updatedAt: new Date(event.blockTimestamp),
  82. createdInBlock: event.blockNumber,
  83. typeId: typeId.toNumber(),
  84. size: new BN(sizeInBytes.toString()),
  85. liaisonJudgement: LiaisonJudgement.PENDING, // judgement is pending at start; liaison id is set when content is accepted/rejected
  86. ipfsContentId: ipfsContentId.toUtf8(),
  87. joystreamContentId: dataObjectId,
  88. createdById: '1',
  89. updatedById: '1',
  90. })
  91. await store.save<DataObject>(dataObject)
  92. return dataObject
  93. }
  94. /// //////////////// Sudo extrinsic calls ///////////////////////////////////////
  95. // soft-peg interface for typegen-generated `*Call` types
  96. export interface IGenericExtrinsicObject<T> {
  97. readonly extrinsic: SubstrateExtrinsic
  98. readonly expectedArgTypes: string[]
  99. args: T
  100. }
  101. // arguments for calling extrinsic as sudo
  102. export interface ISudoCallArgs<T> extends ExtrinsicArg {
  103. args: T
  104. callIndex: string
  105. }
  106. /*
  107. Extracts extrinsic arguments from the Substrate event. Supports both direct extrinsic calls and sudo calls.
  108. */
  109. export function extractExtrinsicArgs<DataParams, EventObject extends IGenericExtrinsicObject<DataParams>>(
  110. rawEvent: SubstrateEvent,
  111. callFactoryConstructor: new (event: SubstrateEvent) => EventObject,
  112. // in ideal world this parameter would not be needed, but there is no way to associate parameters
  113. // used in sudo to extrinsic parameters without it
  114. argsIndeces: Record<keyof DataParams, number>
  115. ): EventObject['args'] {
  116. const CallFactory = callFactoryConstructor
  117. // this is equal to DataParams but only this notation works properly
  118. // escape when extrinsic info is not available
  119. if (!rawEvent.extrinsic) {
  120. throw new Error('Invalid event - no extrinsic set') // this should never happen
  121. }
  122. // regural extrinsic call?
  123. if (rawEvent.extrinsic.section !== 'sudo') {
  124. return new CallFactory(rawEvent).args
  125. }
  126. // sudo extrinsic call
  127. const callArgs = extractSudoCallParameters<DataParams>(rawEvent)
  128. // convert naming convention (underscore_names to camelCase)
  129. const clearArgs = Object.keys(callArgs.args).reduce((acc, key) => {
  130. const formattedName = key.replace(/_([a-z])/g, (tmp) => tmp[1].toUpperCase())
  131. acc[formattedName] = callArgs.args[key]
  132. return acc
  133. }, {} as DataParams)
  134. // prepare partial event object
  135. const partialEvent = {
  136. extrinsic: ({
  137. args: Object.keys(argsIndeces).reduce((acc, key) => {
  138. acc[argsIndeces[key]] = {
  139. value: clearArgs[key],
  140. }
  141. return acc
  142. }, [] as unknown[]),
  143. } as unknown) as SubstrateExtrinsic,
  144. } as SubstrateEvent
  145. // create event object and extract processed args
  146. const finalArgs = new CallFactory(partialEvent).args
  147. return finalArgs
  148. }
  149. /*
  150. Extracts extrinsic call parameters used inside of sudo call.
  151. */
  152. export function extractSudoCallParameters<DataParams>(rawEvent: SubstrateEvent): ISudoCallArgs<DataParams> {
  153. if (!rawEvent.extrinsic) {
  154. throw new Error('Invalid event - no extrinsic set') // this should never happen
  155. }
  156. // see Substrate's sudo frame for more info about sudo extrinsics and `call` argument index
  157. const argIndex =
  158. false ||
  159. (rawEvent.extrinsic.method === 'sudoAs' && 1) || // who, *call*
  160. (rawEvent.extrinsic.method === 'sudo' && 0) || // *call*
  161. (rawEvent.extrinsic.method === 'sudoUncheckedWeight' && 0) // *call*, _weight
  162. // ensure `call` argument was found
  163. if (argIndex === false) {
  164. // this could possibly happen in sometime in future if new sudo options are introduced in Substrate
  165. throw new Error('Not implemented situation with sudo')
  166. }
  167. // typecast call arguments
  168. const callArgs = (rawEvent.extrinsic.args[argIndex].value as unknown) as ISudoCallArgs<DataParams>
  169. return callArgs
  170. }
  171. // FIXME:
  172. type MappingsMemoryCache = {
  173. lastCreatedProposalThreadId?: ThreadId
  174. }
  175. export const MemoryCache: MappingsMemoryCache = {}
  176. export function genericEventFields(substrateEvent: SubstrateEvent): Partial<BaseModel & Event> {
  177. const { blockNumber, indexInBlock, extrinsic, blockTimestamp } = substrateEvent
  178. const eventTime = new Date(blockTimestamp)
  179. return {
  180. createdAt: eventTime,
  181. updatedAt: eventTime,
  182. id: `${CURRENT_NETWORK}-${blockNumber}-${indexInBlock}`,
  183. inBlock: blockNumber,
  184. network: CURRENT_NETWORK,
  185. inExtrinsic: extrinsic?.hash,
  186. indexInBlock,
  187. }
  188. }
  189. export function deserializeMetadata<T>(
  190. metadataType: AnyMetadataClass<T>,
  191. metadataBytes: Bytes
  192. ): DecodedMetadataObject<T> | null {
  193. try {
  194. return metaToObject(metadataType, metadataType.decode(metadataBytes.toU8a(true)))
  195. } catch (e) {
  196. invalidMetadata(`Cannot deserialize ${metadataType.name}! Provided bytes: (${metadataBytes.toHex()})`)
  197. return null
  198. }
  199. }
  200. export function bytesToString(b: Bytes): string {
  201. return (
  202. Buffer.from(b.toU8a(true))
  203. .toString()
  204. // eslint-disable-next-line no-control-regex
  205. .replace(/\u0000/g, '')
  206. )
  207. }
  208. export function perpareString(s: string): string {
  209. // eslint-disable-next-line no-control-regex
  210. return s.replace(/\u0000/g, '')
  211. }
  212. export function hasValuesForProperties<
  213. T extends Record<string, unknown>,
  214. P extends keyof T & string,
  215. PA extends readonly P[]
  216. >(obj: T, props: PA): obj is T & { [K in PA[number]]: NonNullable<K> } {
  217. props.forEach((p) => {
  218. if (obj[p] === null || obj[p] === undefined) {
  219. return false
  220. }
  221. })
  222. return true
  223. }
  224. export type WorkingGroupModuleName =
  225. | 'storageWorkingGroup'
  226. | 'contentDirectoryWorkingGroup'
  227. | 'forumWorkingGroup'
  228. | 'membershipWorkingGroup'
  229. | 'operationsWorkingGroup'
  230. | 'gatewayWorkingGroup'
  231. export function getWorkingGroupModuleName(group: WorkingGroup): WorkingGroupModuleName {
  232. if (group.isContent) {
  233. return 'contentDirectoryWorkingGroup'
  234. } else if (group.isMembership) {
  235. return 'membershipWorkingGroup'
  236. } else if (group.isForum) {
  237. return 'forumWorkingGroup'
  238. } else if (group.isStorage) {
  239. return 'storageWorkingGroup'
  240. } else if (group.isOperations) {
  241. return 'operationsWorkingGroup'
  242. } else if (group.isGateway) {
  243. return 'gatewayWorkingGroup'
  244. }
  245. unexpectedData('Unsupported working group encountered:', group.type)
  246. }
  247. export async function getWorker(
  248. store: DatabaseManager,
  249. groupName: WorkingGroupModuleName,
  250. runtimeId: WorkerId | number
  251. ): Promise<Worker> {
  252. const workerDbId = `${groupName}-${runtimeId}`
  253. const worker = await store.get(Worker, { where: { id: workerDbId } })
  254. if (!worker) {
  255. inconsistentState(`Expected worker not found by id ${workerDbId}`)
  256. }
  257. return worker
  258. }