Api.ts 21 KB

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