From a2bd1a0184b4be9a705af336f62619ab3865828e Mon Sep 17 00:00:00 2001 From: Victor Oliva Date: Thu, 16 Jan 2025 16:31:51 +0100 Subject: [PATCH 1/3] feat(referenda-sdk): watch referenda --- .../src/bounties/bounties-sdk.ts | 45 ++----------- .../src/bounties/child-bounties-sdk.ts | 45 ++----------- .../src/referenda/referenda-sdk.ts | 64 +++++++++++++++++++ .../sdk-governance/src/referenda/sdk-types.ts | 6 ++ .../sdk-governance/src/util/watchEntries.ts | 43 +++++++++++++ 5 files changed, 127 insertions(+), 76 deletions(-) create mode 100644 packages/sdk-governance/src/util/watchEntries.ts diff --git a/packages/sdk-governance/src/bounties/bounties-sdk.ts b/packages/sdk-governance/src/bounties/bounties-sdk.ts index b66bb70..14c445c 100644 --- a/packages/sdk-governance/src/bounties/bounties-sdk.ts +++ b/packages/sdk-governance/src/bounties/bounties-sdk.ts @@ -1,12 +1,7 @@ -import { combineKeys, partitionByKey, toKeySet } from "@react-rxjs/utils" +import { partitionEntries } from "@/util/watchEntries" +import { combineKeys, toKeySet } from "@react-rxjs/utils" import { Binary, TxEvent } from "polkadot-api" -import { - combineLatest, - distinctUntilChanged, - map, - mergeMap, - takeWhile, -} from "rxjs" +import { combineLatest, distinctUntilChanged, map } from "rxjs" import { getBountyDescriptions$ } from "./bounty-descriptions" import { BountiesSdkTypedApi, BountyWithoutDescription } from "./descriptors" import { @@ -138,32 +133,8 @@ export function createBountiesSdk(typedApi: BountiesSdkTypedApi): BountiesSdk { } function watchBounties() { - const [getBountyById$, bountyKeyChanges$] = partitionByKey( - typedApi.query.Bounties.Bounties.watchEntries().pipe( - mergeMap((v) => - v.deltas - ? [ - ...v.deltas.deleted.map((d) => ({ - id: d.args[0], - value: undefined, - })), - ...v.deltas.upserted.map((d) => ({ - id: d.args[0], - value: d.value, - })), - ].sort((a, b) => a.id - b.id) - : [], - ), - ), - (res) => res.id, - (group$, id) => - group$.pipe( - takeWhile(({ value }) => Boolean(value), false), - map((bounty) => ({ - id, - bounty: bounty.value!, - })), - ), + const [getBountyById$, bountyKeyChanges$] = partitionEntries( + typedApi.query.Bounties.Bounties.watchEntries(), ) const descriptions$ = getBountyDescriptions$( typedApi.query.Bounties.BountyDescriptions.getEntries, @@ -184,13 +155,11 @@ export function createBountiesSdk(typedApi: BountiesSdkTypedApi): BountiesSdk { distinctUntilChanged(), ), ]).pipe( - map(([{ id, bounty }, description]) => - enhanceBounty(bounty, description, id), - ), + map(([bounty, description]) => enhanceBounty(bounty, description, id)), ) return { - bounties$: combineKeys(bountyIds$, getEnhancedBountyById$), + bounties$: combineKeys(bountyKeyChanges$, getEnhancedBountyById$), getBountyById$: getEnhancedBountyById$, bountyIds$, } diff --git a/packages/sdk-governance/src/bounties/child-bounties-sdk.ts b/packages/sdk-governance/src/bounties/child-bounties-sdk.ts index d2a6b9a..5855956 100644 --- a/packages/sdk-governance/src/bounties/child-bounties-sdk.ts +++ b/packages/sdk-governance/src/bounties/child-bounties-sdk.ts @@ -1,11 +1,7 @@ -import { combineKeys, partitionByKey, toKeySet } from "@react-rxjs/utils" -import { - combineLatest, - distinctUntilChanged, - map, - mergeMap, - takeWhile, -} from "rxjs" +import { keyedMemo } from "@/util/memo" +import { partitionEntries } from "@/util/watchEntries" +import { combineKeys, toKeySet } from "@react-rxjs/utils" +import { combineLatest, distinctUntilChanged, map } from "rxjs" import { getBountyDescriptions$ } from "./bounty-descriptions" import { ChildBountiesSdkTypedApi, @@ -16,7 +12,6 @@ import { ChildBounty, GenericChildBounty, } from "./child-sdk-types" -import { keyedMemo } from "@/util/memo" export function createChildBountiesSdk( typedApi: ChildBountiesSdkTypedApi, @@ -111,32 +106,8 @@ export function createChildBountiesSdk( } function watchChildBounties(parentId: number) { - const [getBountyById$, bountyKeyChanges$] = partitionByKey( - typedApi.query.ChildBounties.ChildBounties.watchEntries(parentId).pipe( - mergeMap((v) => - v.deltas - ? [ - ...v.deltas.deleted.map((d) => ({ - id: d.args[1], - value: undefined, - })), - ...v.deltas.upserted.map((d) => ({ - id: d.args[1], - value: d.value, - })), - ].sort((a, b) => a.id - b.id) - : [], - ), - ), - (res) => res.id, - (group$, id) => - group$.pipe( - takeWhile(({ value }) => Boolean(value), false), - map((bounty) => ({ - id, - bounty: bounty.value!, - })), - ), + const [getBountyById$, bountyKeyChanges$] = partitionEntries( + typedApi.query.ChildBounties.ChildBounties.watchEntries(parentId), ) const descriptions$ = getBountyDescriptions$( typedApi.query.ChildBounties.ChildBountyDescriptions.getEntries, @@ -157,9 +128,7 @@ export function createChildBountiesSdk( distinctUntilChanged(), ), ]).pipe( - map(([{ id, bounty }, description]) => - enhanceBounty(bounty, description, id), - ), + map(([bounty, description]) => enhanceBounty(bounty, description, id)), ) return { diff --git a/packages/sdk-governance/src/referenda/referenda-sdk.ts b/packages/sdk-governance/src/referenda/referenda-sdk.ts index 3ffc096..1b0696e 100644 --- a/packages/sdk-governance/src/referenda/referenda-sdk.ts +++ b/packages/sdk-governance/src/referenda/referenda-sdk.ts @@ -1,5 +1,8 @@ +import { Deltas, partitionEntries } from "@/util/watchEntries" import { blake2b } from "@noble/hashes/blake2b" +import { combineKeys, toKeySet } from "@react-rxjs/utils" import { Binary, TxEvent } from "polkadot-api" +import { map, scan } from "rxjs" import { getPreimageResolver } from "../preimages" import { originToTrack, polkadotSpenderOrigin } from "./chainConfig" import { @@ -134,6 +137,62 @@ export function createReferendaSdk( .filter((v) => !!v) } + const [rawReferendumById$, referendaKeyChange$] = partitionEntries( + typedApi.query.Referenda.ReferendumInfoFor.watchEntries().pipe( + scan( + (acc, v) => { + if (!v.deltas) return { ...acc, deltas: null } + const deleted = v.deltas.deleted.map((v) => ({ + ...v, + value: v.value.value as RawOngoingReferendum, + })) + const upserted = v.deltas.upserted + .map((v) => { + if (v.value.type === "Ongoing") { + acc.referendums[v.args[0]] = v.value.value + return { + args: v.args, + value: v.value.value, + } + } + if (v.args[0] in acc.referendums) { + // An Ongoing has become closed, remove from list + deleted.push({ + args: v.args, + value: acc.referendums[v.args[0]], + }) + delete acc.referendums[v.args[0]] + } + return null! + }) + .filter(Boolean) + + return { + referendums: acc.referendums, + deltas: { deleted, upserted }, + } + }, + { + referendums: {} as Record, + deltas: null as Deltas | null, + }, + ), + ), + ) + + const getOngoingReferendumById$ = (id: number) => + rawReferendumById$(id).pipe( + map((entry) => enhanceOngoingReferendum(id, entry)), + ) + const ongoingReferenda$ = combineKeys( + referendaKeyChange$, + getOngoingReferendumById$, + ) + const ongoingReferendaIds$ = referendaKeyChange$.pipe( + toKeySet(), + map((set) => [...set]), + ) + const getSpenderTrack: ReferendaSdk["getSpenderTrack"] = (value) => { const spenderOriginType = spenderOrigin(value) const origin: PolkadotRuntimeOriginCaller = spenderOriginType @@ -223,6 +282,11 @@ export function createReferendaSdk( : null return { + watch: { + ongoingReferenda$, + getOngoingReferendumById$, + ongoingReferendaIds$, + }, getOngoingReferenda, getSpenderTrack, getTrack, diff --git a/packages/sdk-governance/src/referenda/sdk-types.ts b/packages/sdk-governance/src/referenda/sdk-types.ts index 0da4453..7e75731 100644 --- a/packages/sdk-governance/src/referenda/sdk-types.ts +++ b/packages/sdk-governance/src/referenda/sdk-types.ts @@ -8,6 +8,7 @@ import { ReferendumInfo, TraitsScheduleDispatchTime, } from "./descriptors" +import { Observable } from "rxjs" type RawOngoingReferendum = (ReferendumInfo & { type: "Ongoing" })["value"] @@ -60,6 +61,11 @@ export type ReferendaTrack = Omit< export interface ReferendaSdk { getOngoingReferenda(): Promise + watch: { + ongoingReferenda$: Observable> + ongoingReferendaIds$: Observable + getOngoingReferendumById$: (key: number) => Observable + } getSpenderTrack(value: bigint): { origin: PolkadotRuntimeOriginCaller track: Promise diff --git a/packages/sdk-governance/src/util/watchEntries.ts b/packages/sdk-governance/src/util/watchEntries.ts new file mode 100644 index 0000000..c6e5892 --- /dev/null +++ b/packages/sdk-governance/src/util/watchEntries.ts @@ -0,0 +1,43 @@ +import { partitionByKey } from "@react-rxjs/utils" +import { map, mergeMap, Observable, takeWhile } from "rxjs" + +export type Deltas = { + deleted: { + args: Array + value: T + }[] + upserted: { + args: Array + value: T + }[] +} +export function partitionEntries( + entries$: Observable<{ + deltas: Deltas | null + }>, +) { + return partitionByKey( + entries$.pipe( + mergeMap((v) => + v.deltas + ? [ + ...v.deltas.deleted.map((d) => ({ + id: d.args.at(-1)!, + value: undefined, + })), + ...v.deltas.upserted.map((d) => ({ + id: d.args.at(-1)!, + value: d.value, + })), + ].sort((a, b) => a.id - b.id) + : [], + ), + ), + (res) => res.id, + (group$) => + group$.pipe( + takeWhile(({ value }) => Boolean(value), false), + map(({ value }) => value!), + ), + ) +} From c6c9230328861faf8bbddd456bdb930281f7286c Mon Sep 17 00:00:00 2001 From: Victor Oliva Date: Fri, 17 Jan 2025 10:13:39 +0100 Subject: [PATCH 2/3] feat: add get referendum by id --- packages/sdk-governance/src/referenda/referenda-sdk.ts | 9 +++++++++ packages/sdk-governance/src/referenda/sdk-types.ts | 1 + 2 files changed, 10 insertions(+) diff --git a/packages/sdk-governance/src/referenda/referenda-sdk.ts b/packages/sdk-governance/src/referenda/referenda-sdk.ts index 1b0696e..8b0b2cd 100644 --- a/packages/sdk-governance/src/referenda/referenda-sdk.ts +++ b/packages/sdk-governance/src/referenda/referenda-sdk.ts @@ -136,6 +136,14 @@ export function createReferendaSdk( }) .filter((v) => !!v) } + async function getOngoingReferendum(id: number) { + const referendum = + await typedApi.query.Referenda.ReferendumInfoFor.getValue(id) + if (referendum?.type === "Ongoing") { + return enhanceOngoingReferendum(id, referendum.value) + } + return null + } const [rawReferendumById$, referendaKeyChange$] = partitionEntries( typedApi.query.Referenda.ReferendumInfoFor.watchEntries().pipe( @@ -288,6 +296,7 @@ export function createReferendaSdk( ongoingReferendaIds$, }, getOngoingReferenda, + getOngoingReferendum, getSpenderTrack, getTrack, createReferenda, diff --git a/packages/sdk-governance/src/referenda/sdk-types.ts b/packages/sdk-governance/src/referenda/sdk-types.ts index 7e75731..d5c1520 100644 --- a/packages/sdk-governance/src/referenda/sdk-types.ts +++ b/packages/sdk-governance/src/referenda/sdk-types.ts @@ -61,6 +61,7 @@ export type ReferendaTrack = Omit< export interface ReferendaSdk { getOngoingReferenda(): Promise + getOngoingReferendum(id: number): Promise watch: { ongoingReferenda$: Observable> ongoingReferendaIds$: Observable From 54a6856b8b73ae350adb228c4869e044b0f434c4 Mon Sep 17 00:00:00 2001 From: Victor Oliva Date: Fri, 17 Jan 2025 10:56:13 +0100 Subject: [PATCH 3/3] fix rounding on track block estimation --- packages/sdk-governance/src/referenda/track.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/sdk-governance/src/referenda/track.ts b/packages/sdk-governance/src/referenda/track.ts index 83eda2e..e5c5fd9 100644 --- a/packages/sdk-governance/src/referenda/track.ts +++ b/packages/sdk-governance/src/referenda/track.ts @@ -41,6 +41,7 @@ export function trackFetcher(typedApi: ReferendaSdkTypedApi) { } const BILLION = 1_000_000_000_000 +const BIG_BILLION = 1_000_000_000_000n const blockToPerBill = (block: number, period: number) => (block * BILLION) / period const perBillToBlock = (perBillion: number, period: number) => @@ -181,10 +182,14 @@ function reciprocal({ // Below horizontal asymptote => +Infinity if (bigValue <= -y_offset) return Number.POSITIVE_INFINITY // Above y-axis cut => -Infinity - if (x_offset != 0n && bigValue > factor / x_offset - y_offset) + // It needs to be multiplied by BIG_BILLION when dividing because we're working with perbillion + if ( + x_offset != 0n && + bigValue > (BIG_BILLION * factor) / x_offset - y_offset + ) return Number.NEGATIVE_INFINITY - return Number(factor / (bigValue + y_offset) - x_offset) + return Number((BIG_BILLION * factor) / (bigValue + y_offset) - x_offset) } const getData = (step: number) => { const result: Array<{