Api.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. import BN from 'bn.js'
  2. import { createType, types } from '@joystream/types/'
  3. import { ApiPromise, WsProvider } from '@polkadot/api'
  4. import { AugmentedQuery, SubmittableExtrinsic } from '@polkadot/api/types'
  5. import { formatBalance } from '@polkadot/util'
  6. import { Balance, BlockNumber } from '@polkadot/types/interfaces'
  7. import { KeyringPair } from '@polkadot/keyring/types'
  8. import { Codec } from '@polkadot/types/types'
  9. import { Option, UInt } from '@polkadot/types'
  10. import {
  11. AccountSummary,
  12. WorkingGroups,
  13. Reward,
  14. GroupMember,
  15. OpeningStatus,
  16. GroupOpeningStage,
  17. GroupOpening,
  18. GroupApplication,
  19. openingPolicyUnstakingPeriodsKeys,
  20. UnstakingPeriods,
  21. StakingPolicyUnstakingPeriodKey,
  22. UnaugmentedApiPromise,
  23. CouncilInfo,
  24. } from './Types'
  25. import { DeriveBalancesAll } from '@polkadot/api-derive/types'
  26. import { CLIError } from '@oclif/errors'
  27. import { Worker, WorkerId, RoleStakeProfile, Application as WGApplication } from '@joystream/types/working-group'
  28. import { Opening, Application, OpeningStage, ApplicationId, OpeningId, StakingPolicy } from '@joystream/types/hiring'
  29. import { MemberId, Membership } from '@joystream/types/members'
  30. import { RewardRelationshipId } from '@joystream/types/recurring-rewards'
  31. import { StakeId } from '@joystream/types/stake'
  32. import { InputValidationLengthConstraint, ChannelId } from '@joystream/types/common'
  33. import {
  34. CuratorGroup,
  35. CuratorGroupId,
  36. Channel,
  37. Video,
  38. VideoId,
  39. ChannelCategoryId,
  40. VideoCategoryId,
  41. } from '@joystream/types/content'
  42. import { Observable } from 'rxjs'
  43. import { BagId, DataObject, DataObjectId } from '@joystream/types/storage'
  44. export const DEFAULT_API_URI = 'ws://localhost:9944/'
  45. // Mapping of working group to api module
  46. export const apiModuleByGroup = {
  47. [WorkingGroups.StorageProviders]: 'storageWorkingGroup',
  48. [WorkingGroups.Curators]: 'contentWorkingGroup',
  49. [WorkingGroups.OperationsAlpha]: 'operationsWorkingGroupAlpha',
  50. [WorkingGroups.OperationsBeta]: 'operationsWorkingGroupBeta',
  51. [WorkingGroups.OperationsGamma]: 'operationsWorkingGroupGamma',
  52. [WorkingGroups.Gateway]: 'gatewayWorkingGroup',
  53. [WorkingGroups.Distribution]: 'distributionWorkingGroup',
  54. } as const
  55. // Api wrapper for handling most common api calls and allowing easy API implementation switch in the future
  56. export default class Api {
  57. private _api: ApiPromise
  58. public isDevelopment = false
  59. private constructor(originalApi: ApiPromise, isDevelopment: boolean) {
  60. this.isDevelopment = isDevelopment
  61. this._api = originalApi
  62. }
  63. public getOriginalApi(): ApiPromise {
  64. return this._api
  65. }
  66. // Get api for use-cases where no type augmentations are desirable
  67. public getUnaugmentedApi(): UnaugmentedApiPromise {
  68. return (this._api as unknown) as UnaugmentedApiPromise
  69. }
  70. private static async initApi(apiUri: string = DEFAULT_API_URI, metadataCache: Record<string, any>) {
  71. const wsProvider: WsProvider = new WsProvider(apiUri)
  72. const api = new ApiPromise({ provider: wsProvider, types, metadata: metadataCache })
  73. await api.isReadyOrError
  74. // Initializing some api params based on pioneer/packages/react-api/Api.tsx
  75. const [properties, chainType] = await Promise.all([api.rpc.system.properties(), api.rpc.system.chainType()])
  76. const tokenSymbol = properties.tokenSymbol.unwrap()[0].toString()
  77. const tokenDecimals = properties.tokenDecimals.unwrap()[0].toNumber()
  78. // formatBlanace config
  79. formatBalance.setDefaults({
  80. decimals: tokenDecimals,
  81. unit: tokenSymbol,
  82. })
  83. return { api, properties, chainType }
  84. }
  85. static async create(apiUri = DEFAULT_API_URI, metadataCache: Record<string, any>): Promise<Api> {
  86. const { api, chainType } = await Api.initApi(apiUri, metadataCache)
  87. return new Api(api, chainType.isDevelopment || chainType.isLocal)
  88. }
  89. async bestNumber(): Promise<number> {
  90. return (await this._api.derive.chain.bestNumber()).toNumber()
  91. }
  92. async getAccountsBalancesInfo(accountAddresses: string[]): Promise<DeriveBalancesAll[]> {
  93. const accountsBalances: DeriveBalancesAll[] = await Promise.all(
  94. accountAddresses.map((addr) => this._api.derive.balances.all(addr))
  95. )
  96. return accountsBalances
  97. }
  98. // Get on-chain data related to given account.
  99. // For now it's just account balances
  100. async getAccountSummary(accountAddresses: string): Promise<AccountSummary> {
  101. const balances: DeriveBalancesAll = (await this.getAccountsBalancesInfo([accountAddresses]))[0]
  102. // TODO: Some more information can be fetched here in the future
  103. return { balances }
  104. }
  105. async getCouncilInfo(): Promise<CouncilInfo> {
  106. const [
  107. activeCouncil,
  108. termEndsAt,
  109. autoStart,
  110. newTermDuration,
  111. candidacyLimit,
  112. councilSize,
  113. minCouncilStake,
  114. minVotingStake,
  115. announcingPeriod,
  116. votingPeriod,
  117. ] = await Promise.all([
  118. this._api.query.council.activeCouncil(),
  119. this._api.query.council.termEndsAt(),
  120. this._api.query.councilElection.autoStart(),
  121. this._api.query.councilElection.newTermDuration(),
  122. this._api.query.councilElection.candidacyLimit(),
  123. this._api.query.councilElection.councilSize(),
  124. this._api.query.councilElection.minCouncilStake(),
  125. this._api.query.councilElection.minVotingStake(),
  126. this._api.query.councilElection.announcingPeriod(),
  127. this._api.query.councilElection.votingPeriod(),
  128. ])
  129. // Promise.all only allows 10 types, so we need to split the queries
  130. const [revealingPeriod, round, stage] = await Promise.all([
  131. this._api.query.councilElection.revealingPeriod(),
  132. this._api.query.councilElection.round(),
  133. this._api.query.councilElection.stage(),
  134. ])
  135. return {
  136. activeCouncil,
  137. termEndsAt,
  138. autoStart,
  139. newTermDuration,
  140. candidacyLimit,
  141. councilSize,
  142. minCouncilStake,
  143. minVotingStake,
  144. announcingPeriod,
  145. votingPeriod,
  146. revealingPeriod,
  147. round,
  148. stage,
  149. }
  150. }
  151. async estimateFee(account: KeyringPair, tx: SubmittableExtrinsic<'promise'>): Promise<Balance> {
  152. const paymentInfo = await tx.paymentInfo(account)
  153. return paymentInfo.partialFee
  154. }
  155. createTransferTx(recipient: string, amount: BN) {
  156. return this._api.tx.balances.transfer(recipient, amount)
  157. }
  158. // Working groups
  159. // TODO: This is a lot of repeated logic from "/pioneer/joy-utils/transport"
  160. // It will be refactored to "joystream-js" soon
  161. async entriesByIds<IDType extends UInt, ValueType extends Codec>(
  162. apiMethod: AugmentedQuery<'promise', (key: IDType) => Observable<ValueType>, [IDType]>
  163. ): Promise<[IDType, ValueType][]> {
  164. const entries: [IDType, ValueType][] = (await apiMethod.entries()).map(([storageKey, value]) => [
  165. storageKey.args[0] as IDType,
  166. value,
  167. ])
  168. return entries.sort((a, b) => a[0].toNumber() - b[0].toNumber())
  169. }
  170. protected async blockHash(height: number): Promise<string> {
  171. const blockHash = await this._api.rpc.chain.getBlockHash(height)
  172. return blockHash.toString()
  173. }
  174. protected async blockTimestamp(height: number): Promise<Date> {
  175. const blockTime = await this._api.query.timestamp.now.at(await this.blockHash(height))
  176. return new Date(blockTime.toNumber())
  177. }
  178. protected workingGroupApiQuery(group: WorkingGroups) {
  179. const module = apiModuleByGroup[group]
  180. return this._api.query[module]
  181. }
  182. protected async membershipById(memberId: MemberId): Promise<Membership | null> {
  183. const profile = await this._api.query.members.membershipById(memberId)
  184. // Can't just use profile.isEmpty because profile.suspended is Bool (which isEmpty method always returns false)
  185. return profile.handle.isEmpty ? null : profile
  186. }
  187. async groupLead(group: WorkingGroups): Promise<GroupMember | null> {
  188. const optLeadId = await this.workingGroupApiQuery(group).currentLead()
  189. if (!optLeadId.isSome) {
  190. return null
  191. }
  192. const leadWorkerId = optLeadId.unwrap()
  193. const leadWorker = await this.workerByWorkerId(group, leadWorkerId.toNumber())
  194. return await this.parseGroupMember(leadWorkerId, leadWorker)
  195. }
  196. protected async stakeValue(stakeId: StakeId): Promise<Balance> {
  197. const stake = await this._api.query.stake.stakes(stakeId)
  198. return stake.value
  199. }
  200. protected async workerStake(stakeProfile: RoleStakeProfile): Promise<Balance> {
  201. return this.stakeValue(stakeProfile.stake_id)
  202. }
  203. protected async workerReward(relationshipId: RewardRelationshipId): Promise<Reward> {
  204. const rewardRelationship = await this._api.query.recurringRewards.rewardRelationships(relationshipId)
  205. return {
  206. totalRecieved: rewardRelationship.total_reward_received,
  207. value: rewardRelationship.amount_per_payout,
  208. interval: rewardRelationship.payout_interval.unwrapOr(undefined)?.toNumber(),
  209. nextPaymentBlock: rewardRelationship.next_payment_at_block.unwrapOr(new BN(0)).toNumber(),
  210. }
  211. }
  212. protected async parseGroupMember(id: WorkerId, worker: Worker): Promise<GroupMember> {
  213. const roleAccount = worker.role_account_id
  214. const memberId = worker.member_id
  215. const profile = await this.membershipById(memberId)
  216. if (!profile) {
  217. throw new Error(`Group member profile not found! (member id: ${memberId.toNumber()})`)
  218. }
  219. let stake: Balance | undefined
  220. if (worker.role_stake_profile && worker.role_stake_profile.isSome) {
  221. stake = await this.workerStake(worker.role_stake_profile.unwrap())
  222. }
  223. let reward: Reward | undefined
  224. if (worker.reward_relationship && worker.reward_relationship.isSome) {
  225. reward = await this.workerReward(worker.reward_relationship.unwrap())
  226. }
  227. return {
  228. workerId: id,
  229. roleAccount,
  230. memberId,
  231. profile,
  232. stake,
  233. reward,
  234. }
  235. }
  236. async workerByWorkerId(group: WorkingGroups, workerId: number): Promise<Worker> {
  237. const nextId = await this.workingGroupApiQuery(group).nextWorkerId()
  238. // This is chain specfic, but if next id is still 0, it means no workers have been added yet
  239. if (workerId < 0 || workerId >= nextId.toNumber()) {
  240. throw new CLIError('Invalid worker id!')
  241. }
  242. const worker = await this.workingGroupApiQuery(group).workerById(workerId)
  243. if (worker.isEmpty) {
  244. throw new CLIError('This worker is not active anymore')
  245. }
  246. return worker
  247. }
  248. async groupMember(group: WorkingGroups, workerId: number): Promise<GroupMember> {
  249. const worker = await this.workerByWorkerId(group, workerId)
  250. return await this.parseGroupMember(this._api.createType('WorkerId', workerId), worker)
  251. }
  252. async groupMembers(group: WorkingGroups): Promise<GroupMember[]> {
  253. const workerEntries = await this.groupWorkers(group)
  254. const groupMembers: GroupMember[] = await Promise.all(
  255. workerEntries.map(([id, worker]) => this.parseGroupMember(id, worker))
  256. )
  257. return groupMembers.reverse() // Sort by newest
  258. }
  259. groupWorkers(group: WorkingGroups): Promise<[WorkerId, Worker][]> {
  260. return this.entriesByIds(this.workingGroupApiQuery(group).workerById)
  261. }
  262. async openingsByGroup(group: WorkingGroups): Promise<GroupOpening[]> {
  263. let openings: GroupOpening[] = []
  264. const nextId = await this.workingGroupApiQuery(group).nextOpeningId()
  265. // This is chain specfic, but if next id is still 0, it means no openings have been added yet
  266. if (!nextId.eq(0)) {
  267. const ids = Array.from(Array(nextId.toNumber()).keys()).reverse() // Sort by newest
  268. openings = await Promise.all(ids.map((id) => this.groupOpening(group, id)))
  269. }
  270. return openings
  271. }
  272. protected async hiringOpeningById(id: number | OpeningId): Promise<Opening> {
  273. const result = await this._api.query.hiring.openingById(id)
  274. return result
  275. }
  276. protected async hiringApplicationById(id: number | ApplicationId): Promise<Application> {
  277. const result = await this._api.query.hiring.applicationById(id)
  278. return result
  279. }
  280. async wgApplicationById(group: WorkingGroups, wgApplicationId: number): Promise<WGApplication> {
  281. const nextAppId = await this.workingGroupApiQuery(group).nextApplicationId()
  282. if (wgApplicationId < 0 || wgApplicationId >= nextAppId.toNumber()) {
  283. throw new CLIError('Invalid working group application ID!')
  284. }
  285. const result = await this.workingGroupApiQuery(group).applicationById(wgApplicationId)
  286. return result
  287. }
  288. protected async parseApplication(wgApplicationId: number, wgApplication: WGApplication): Promise<GroupApplication> {
  289. const appId = wgApplication.application_id
  290. const application = await this.hiringApplicationById(appId)
  291. const { active_role_staking_id: roleStakingId, active_application_staking_id: appStakingId } = application
  292. return {
  293. wgApplicationId,
  294. applicationId: appId.toNumber(),
  295. wgOpeningId: wgApplication.opening_id.toNumber(),
  296. member: await this.membershipById(wgApplication.member_id),
  297. roleAccout: wgApplication.role_account_id,
  298. stakes: {
  299. application: appStakingId.isSome ? (await this.stakeValue(appStakingId.unwrap())).toNumber() : 0,
  300. role: roleStakingId.isSome ? (await this.stakeValue(roleStakingId.unwrap())).toNumber() : 0,
  301. },
  302. humanReadableText: application.human_readable_text.toString(),
  303. stage: application.stage.type,
  304. }
  305. }
  306. async groupApplication(group: WorkingGroups, wgApplicationId: number): Promise<GroupApplication> {
  307. const wgApplication = await this.wgApplicationById(group, wgApplicationId)
  308. return await this.parseApplication(wgApplicationId, wgApplication)
  309. }
  310. protected async groupOpeningApplications(group: WorkingGroups, wgOpeningId: number): Promise<GroupApplication[]> {
  311. const wgApplicationEntries = await this.entriesByIds(this.workingGroupApiQuery(group).applicationById)
  312. return Promise.all(
  313. wgApplicationEntries
  314. .filter(([, /* id */ wgApplication]) => wgApplication.opening_id.eqn(wgOpeningId))
  315. .map(([id, wgApplication]) => this.parseApplication(id.toNumber(), wgApplication))
  316. )
  317. }
  318. async groupOpening(group: WorkingGroups, wgOpeningId: number): Promise<GroupOpening> {
  319. const nextId = (await this.workingGroupApiQuery(group).nextOpeningId()).toNumber()
  320. if (wgOpeningId < 0 || wgOpeningId >= nextId) {
  321. throw new CLIError('Invalid working group opening ID!')
  322. }
  323. const groupOpening = await this.workingGroupApiQuery(group).openingById(wgOpeningId)
  324. const openingId = groupOpening.hiring_opening_id.toNumber()
  325. const opening = await this.hiringOpeningById(openingId)
  326. const applications = await this.groupOpeningApplications(group, wgOpeningId)
  327. const stage = await this.parseOpeningStage(opening.stage)
  328. const type = groupOpening.opening_type
  329. const { application_staking_policy: applSP, role_staking_policy: roleSP } = opening
  330. const stakes = {
  331. application: applSP.unwrapOr(undefined),
  332. role: roleSP.unwrapOr(undefined),
  333. }
  334. const unstakingPeriod = (period: Option<BlockNumber>) => period.unwrapOr(new BN(0)).toNumber()
  335. const spUnstakingPeriod = (sp: Option<StakingPolicy>, key: StakingPolicyUnstakingPeriodKey) =>
  336. sp.isSome ? unstakingPeriod(sp.unwrap()[key]) : 0
  337. const unstakingPeriods: Partial<UnstakingPeriods> = {
  338. 'review_period_expired_application_stake_unstaking_period_length': spUnstakingPeriod(
  339. applSP,
  340. 'review_period_expired_unstaking_period_length'
  341. ),
  342. 'crowded_out_application_stake_unstaking_period_length': spUnstakingPeriod(
  343. applSP,
  344. 'crowded_out_unstaking_period_length'
  345. ),
  346. 'review_period_expired_role_stake_unstaking_period_length': spUnstakingPeriod(
  347. roleSP,
  348. 'review_period_expired_unstaking_period_length'
  349. ),
  350. 'crowded_out_role_stake_unstaking_period_length': spUnstakingPeriod(
  351. roleSP,
  352. 'crowded_out_unstaking_period_length'
  353. ),
  354. }
  355. openingPolicyUnstakingPeriodsKeys.forEach((key) => {
  356. unstakingPeriods[key] = unstakingPeriod(groupOpening.policy_commitment[key])
  357. })
  358. return {
  359. wgOpeningId,
  360. openingId,
  361. opening,
  362. stage,
  363. stakes,
  364. applications,
  365. type,
  366. unstakingPeriods: unstakingPeriods as UnstakingPeriods,
  367. }
  368. }
  369. async parseOpeningStage(stage: OpeningStage): Promise<GroupOpeningStage> {
  370. let status: OpeningStatus | undefined, stageBlock: number | undefined, stageDate: Date | undefined
  371. if (stage.isOfType('WaitingToBegin')) {
  372. const stageData = stage.asType('WaitingToBegin')
  373. const currentBlockNumber = (await this._api.derive.chain.bestNumber()).toNumber()
  374. const expectedBlockTime = this._api.consts.babe.expectedBlockTime.toNumber()
  375. status = OpeningStatus.WaitingToBegin
  376. stageBlock = stageData.begins_at_block.toNumber()
  377. stageDate = new Date(Date.now() + (stageBlock - currentBlockNumber) * expectedBlockTime)
  378. }
  379. if (stage.isOfType('Active')) {
  380. const stageData = stage.asType('Active')
  381. const substage = stageData.stage
  382. if (substage.isOfType('AcceptingApplications')) {
  383. status = OpeningStatus.AcceptingApplications
  384. stageBlock = substage.asType('AcceptingApplications').started_accepting_applicants_at_block.toNumber()
  385. }
  386. if (substage.isOfType('ReviewPeriod')) {
  387. status = OpeningStatus.InReview
  388. stageBlock = substage.asType('ReviewPeriod').started_review_period_at_block.toNumber()
  389. }
  390. if (substage.isOfType('Deactivated')) {
  391. status = substage.asType('Deactivated').cause.isOfType('Filled')
  392. ? OpeningStatus.Complete
  393. : OpeningStatus.Cancelled
  394. stageBlock = substage.asType('Deactivated').deactivated_at_block.toNumber()
  395. }
  396. if (stageBlock) {
  397. stageDate = new Date(await this.blockTimestamp(stageBlock))
  398. }
  399. }
  400. return {
  401. status: status || OpeningStatus.Unknown,
  402. block: stageBlock,
  403. date: stageDate,
  404. }
  405. }
  406. async getMemberIdsByControllerAccount(address: string): Promise<MemberId[]> {
  407. const ids = await this._api.query.members.memberIdsByControllerAccountId(address)
  408. return ids.toArray()
  409. }
  410. async workerExitRationaleConstraint(group: WorkingGroups): Promise<InputValidationLengthConstraint> {
  411. return await this.workingGroupApiQuery(group).workerExitRationaleText()
  412. }
  413. // Content directory
  414. async availableChannels(): Promise<[ChannelId, Channel][]> {
  415. return await this.entriesByIds(this._api.query.content.channelById)
  416. }
  417. async availableVideos(): Promise<[VideoId, Video][]> {
  418. return await this.entriesByIds(this._api.query.content.videoById)
  419. }
  420. availableCuratorGroups(): Promise<[CuratorGroupId, CuratorGroup][]> {
  421. return this.entriesByIds(this._api.query.content.curatorGroupById)
  422. }
  423. async curatorGroupById(id: number): Promise<CuratorGroup | null> {
  424. const exists = !!(await this._api.query.content.curatorGroupById.size(id)).toNumber()
  425. return exists ? await this._api.query.content.curatorGroupById(id) : null
  426. }
  427. async nextCuratorGroupId(): Promise<number> {
  428. return (await this._api.query.content.nextCuratorGroupId()).toNumber()
  429. }
  430. async channelById(channelId: ChannelId | number | string): Promise<Channel> {
  431. // isEmpty will not work for { MemmberId: 0 } ownership
  432. const exists = !!(await this._api.query.content.channelById.size(channelId)).toNumber()
  433. if (!exists) {
  434. throw new CLIError(`Channel by id ${channelId.toString()} not found!`)
  435. }
  436. const channel = await this._api.query.content.channelById(channelId)
  437. return channel
  438. }
  439. async videoById(videoId: VideoId | number | string): Promise<Video> {
  440. const video = await this._api.query.content.videoById(videoId)
  441. if (video.isEmpty) {
  442. throw new CLIError(`Video by id ${videoId.toString()} not found!`)
  443. }
  444. return video
  445. }
  446. async dataObjectsByIds(bagId: BagId, ids: DataObjectId[]): Promise<DataObject[]> {
  447. return this._api.query.storage.dataObjectsById.multi(ids.map((id) => [bagId, id]))
  448. }
  449. async channelCategoryIds(): Promise<ChannelCategoryId[]> {
  450. // There is currently no way to differentiate between unexisting and existing category
  451. // other than fetching all existing category ids (event the .size() trick does not work, as the object is empty)
  452. return (await this.entriesByIds(this._api.query.content.channelCategoryById)).map(([id]) => id)
  453. }
  454. async videoCategoryIds(): Promise<VideoCategoryId[]> {
  455. // There is currently no way to differentiate between unexisting and existing category
  456. // other than fetching all existing category ids (event the .size() trick does not work, as the object is empty)
  457. return (await this.entriesByIds(this._api.query.content.videoCategoryById)).map(([id]) => id)
  458. }
  459. async dataObjectsInBag(bagId: BagId): Promise<[DataObjectId, DataObject][]> {
  460. return (await this._api.query.storage.dataObjectsById.entries(bagId)).map(([{ args: [, dataObjectId] }, value]) => [
  461. dataObjectId,
  462. value,
  463. ])
  464. }
  465. async getMembers(ids: MemberId[] | number[]): Promise<Membership[]> {
  466. return this._api.query.members.membershipById.multi(ids)
  467. }
  468. async memberEntriesByIds(ids: MemberId[] | number[]): Promise<[MemberId, Membership][]> {
  469. const memberships = await this._api.query.members.membershipById.multi<Membership>(ids)
  470. return ids.map((id, i) => [createType('MemberId', id), memberships[i]])
  471. }
  472. allMemberEntries(): Promise<[MemberId, Membership][]> {
  473. return this.entriesByIds(this._api.query.members.membershipById)
  474. }
  475. }