Crowdloan contributions derive (#3704)

* Crwodloan contributions derive

* Handle deltas

* Opt

* Expand childKey

* Keep all previous changes

* Rework

* Adjust

* Cleanups

* Cleanups

* Pass through childKey

* deltas

* CHANGELOG

* ownContributions

* cleanups

* watch events
This commit is contained in:
Jaco
2021-07-08 10:30:27 +02:00
committed by GitHub
parent 76d23c54a6
commit e2fa996df4
9 changed files with 313 additions and 20 deletions
@@ -0,0 +1,37 @@
// Copyright 2017-2021 @polkadot/api-derive authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Observable } from 'rxjs';
import type { ApiInterfaceRx } from '@polkadot/api/types';
import type { Option } from '@polkadot/types';
import type { FundInfo, ParaId } from '@polkadot/types/interfaces';
import { map } from 'rxjs';
import { u8aConcat, u8aToHex } from '@polkadot/util';
import { blake2AsU8a } from '@polkadot/util-crypto';
import { memo } from '../util';
function createChildKey ({ trieIndex }: FundInfo): string {
return u8aToHex(
u8aConcat(
':child_storage:default:',
blake2AsU8a(
u8aConcat('crowdloan', trieIndex.toU8a())
)
)
);
}
export function childKey (instanceId: string, api: ApiInterfaceRx): (paraId: string | number | ParaId) => Observable<string | null> {
return memo(instanceId, (paraId: string | number | ParaId): Observable<string | null> =>
api.query.crowdloan.funds<Option<FundInfo>>(paraId).pipe(
map((optInfo) =>
optInfo.isSome
? createChildKey(optInfo.unwrap())
: null
)
)
);
}
@@ -0,0 +1,135 @@
// Copyright 2017-2021 @polkadot/api-derive authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Observable } from 'rxjs';
import type { ApiInterfaceRx } from '@polkadot/api/types';
import type { StorageKey } from '@polkadot/types';
import type { ParaId } from '@polkadot/types/interfaces';
import type { DeriveContributions } from '../types';
import { BehaviorSubject, combineLatest, EMPTY, map, of, startWith, switchMap, tap, toArray } from 'rxjs';
import { arrayFlatten, isFunction } from '@polkadot/util';
import { memo } from '../util';
import { extractContributed } from './util';
interface Changes {
added: string[];
blockHash: string;
removed: string[];
}
const PAGE_SIZE_K = 1000; // limit aligned with the 1k on the node (trie lookups are heavy)
function _getUpdates (api: ApiInterfaceRx, paraId: string | number | ParaId): Observable<Changes> {
let added: string[] = [];
let removed: string[] = [];
return api.query.system.events().pipe(
switchMap((events): Observable<Changes> => {
const changes = extractContributed(paraId, events);
if (changes.added.length || changes.removed.length) {
added = added.concat(...changes.added);
removed = removed.concat(...changes.removed);
return of({ added, addedDelta: changes.added, blockHash: events.createdAtHash?.toHex() || '-', removed, removedDelta: changes.removed });
}
return EMPTY;
}),
startWith({ added, addedDelta: [], blockHash: '-', removed, removedDelta: [] })
);
}
function _eventTriggerAll (api: ApiInterfaceRx, paraId: string | number | ParaId): Observable<string> {
return api.query.system.events().pipe(
switchMap((events): Observable<string> => {
const items = events.filter(({ event: { data: [eventParaId], method, section } }) =>
section === 'crowdloan' &&
['AllRefunded', 'Dissolved', 'PartiallyRefunded'].includes(method) &&
eventParaId.eq(paraId)
);
return items.length
? of(events.createdAtHash?.toHex() || '-')
: EMPTY;
}),
startWith('-')
);
}
function _getKeysPaged (api: ApiInterfaceRx, childKey: string): Observable<StorageKey[]> {
const startSubject = new BehaviorSubject<string | undefined>(undefined);
return startSubject.pipe(
switchMap((startKey) =>
api.rpc.childstate.getKeysPaged(childKey, '0x', PAGE_SIZE_K, startKey)
),
tap((keys): void => {
setTimeout((): void => {
keys.length === PAGE_SIZE_K
? startSubject.next(keys[PAGE_SIZE_K - 1].toHex())
: startSubject.complete();
}, 0);
}),
toArray(), // toArray since we want to startSubject to be completed
map((keyArr: StorageKey[][]) => arrayFlatten(keyArr))
);
}
function _getAll (api: ApiInterfaceRx, paraId: string | number | ParaId, childKey: string): Observable<string[]> {
return _eventTriggerAll(api, paraId).pipe(
switchMap(() =>
// FIXME Needs testing and being enabled
// eslint-disable-next-line no-constant-condition
isFunction(api.rpc.childstate.getKeysPaged) && false
? _getKeysPaged(api, childKey)
: api.rpc.childstate.getKeys(childKey, '0x')
),
map((keys) =>
keys.map((k) => k.toHex())
)
);
}
function _contributions (api: ApiInterfaceRx, paraId: string | number | ParaId, childKey: string): Observable<DeriveContributions> {
return combineLatest([
_getAll(api, paraId, childKey),
_getUpdates(api, paraId)
]).pipe(
map(([keys, { added, blockHash, removed }]): DeriveContributions => {
const contributorsMap: Record<string, boolean> = {};
keys.forEach((k): void => {
contributorsMap[k] = true;
});
added.forEach((k): void => {
contributorsMap[k] = true;
});
removed.forEach((k): void => {
delete contributorsMap[k];
});
return {
blockHash,
contributorsHex: Object.keys(contributorsMap)
};
})
);
}
export function contributions (instanceId: string, api: ApiInterfaceRx): (paraId: string | number | ParaId) => Observable<DeriveContributions> {
return memo(instanceId, (paraId: string | number | ParaId): Observable<DeriveContributions> =>
api.derive.crowdloan.childKey(paraId).pipe(
switchMap((childKey) =>
childKey
? _contributions(api, paraId, childKey)
: of({ blockHash: '-', contributorsHex: [] })
)
)
);
}
@@ -0,0 +1,6 @@
// Copyright 2017-2021 @polkadot/api-derive authors & contributors
// SPDX-License-Identifier: Apache-2.0
export * from './childKey';
export * from './contributions';
export * from './onwContributions';
@@ -0,0 +1,72 @@
// Copyright 2017-2021 @polkadot/api-derive authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Observable } from 'rxjs';
import type { ApiInterfaceRx } from '@polkadot/api/types';
import type { ParaId } from '@polkadot/types/interfaces';
import type { DeriveOwnContributions } from '../types';
import { combineLatest, EMPTY, map, of, startWith, switchMap } from 'rxjs';
import { memo } from '../util';
import { extractContributed } from './util';
function _getValues (api: ApiInterfaceRx, childKey: string, keys: string[]): Observable<DeriveOwnContributions> {
// We actually would love to use multi-keys https://github.com/paritytech/substrate/issues/9203
return combineLatest(keys.map((k) => api.rpc.childstate.getStorage(childKey, k))).pipe(
map((values) =>
values
.map((v) => api.registry.createType('Option<StorageData>', v))
.map((o) =>
o.isSome
? api.registry.createType('Balance', o.unwrap())
: api.registry.createType('Balance')
)
.reduce((all: DeriveOwnContributions, b, index): DeriveOwnContributions => ({
...all,
[keys[index]]: b
}), {})
)
);
}
function _watchOwnChanges (api: ApiInterfaceRx, paraId: string | number | ParaId, childkey: string, keys: string[]): Observable<DeriveOwnContributions> {
return api.query.system.events().pipe(
switchMap((events): Observable<DeriveOwnContributions> => {
const changes = extractContributed(paraId, events);
const filtered = keys.filter((k) =>
changes.added.includes(k) ||
changes.removed.includes(k)
);
return filtered.length
? _getValues(api, childkey, filtered)
: EMPTY;
}),
startWith({})
);
}
function _contributions (api: ApiInterfaceRx, paraId: string | number | ParaId, childKey: string, keys: string[]): Observable<DeriveOwnContributions> {
return combineLatest([
_getValues(api, childKey, keys),
_watchOwnChanges(api, paraId, childKey, keys)
]).pipe(
map(([all, latest]) => ({
...all,
...latest
}))
);
}
export function ownContributions (instanceId: string, api: ApiInterfaceRx): (paraId: string | number | ParaId, keys: string[]) => Observable<DeriveOwnContributions> {
return memo(instanceId, (paraId: string | number | ParaId, keys: string[]): Observable<DeriveOwnContributions> =>
api.derive.crowdloan.childKey(paraId).pipe(
switchMap((childKey) =>
childKey && keys.length
? _contributions(api, paraId, childKey, keys)
: of({})
)
)
);
}
@@ -0,0 +1,11 @@
// Copyright 2017-2021 @polkadot/api-derive authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Balance } from '@polkadot/types/interfaces';
export interface DeriveContributions {
blockHash: string;
contributorsHex: string[];
}
export type DeriveOwnContributions = Record<string, Balance>;
+32
View File
@@ -0,0 +1,32 @@
// Copyright 2017-2021 @polkadot/api-derive authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Vec } from '@polkadot/types';
import type { EventRecord, ParaId } from '@polkadot/types/interfaces';
interface Changes {
added: string[];
blockHash: string;
removed: string[];
}
export function extractContributed (paraId: string | number | ParaId, events: Vec<EventRecord>): Changes {
const added: string[] = [];
const removed: string[] = [];
return events
.filter(({ event: { data: [, eventParaId], method, section } }) =>
section === 'crowdloan' &&
['Contributed', 'Withdrew'].includes(method) &&
eventParaId.eq(paraId)
)
.reduce((result: Changes, { event: { data: [accountId], method } }): Changes => {
if (method === 'Contributed') {
result.added.push(accountId.toHex());
} else {
result.removed.push(accountId.toHex());
}
return result;
}, { added, blockHash: events.createdAtHash?.toHex() || '-', removed });
}