Skip to content

Commit

Permalink
fix: a lot of small fixes in fellowship including correct voting sub (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
johnthecat authored Jan 16, 2025
1 parent 14aa623 commit a19d1df
Show file tree
Hide file tree
Showing 37 changed files with 421 additions and 262 deletions.
4 changes: 2 additions & 2 deletions src/renderer/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import { walletModel } from '@/entities/wallet';
import { navigationModel } from '@/features/navigation';
import { ROUTES_CONFIG } from '@/pages/index';

import { initModel } from './modelInit';
import { bootstrap } from './bootstrap';
import { GraphqlProvider, MultisigChainProvider, StatusModalProvider } from './providers';

logger.init();
initModel();
bootstrap();

export const App = () => {
const navigate = useNavigate();
Expand Down
35 changes: 31 additions & 4 deletions src/renderer/app/modelInit.ts → src/renderer/app/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/* eslint-disable import-x/max-dependencies */

import { kernelModel } from '@/shared/core';
import { registerFeatures } from '@/shared/feature';
import { createFeature, registerFeatures } from '@/shared/feature';
import { config as collectivesConfig, tracksService } from '@/domains/collectives';
import { accounts } from '@/domains/network';
import { basketModel } from '@/entities/basket';
import { governanceModel } from '@/entities/governance';
Expand All @@ -22,6 +23,7 @@ import { importDBFeature } from '@/features/import-db';
import { multisigOperationDetailsFeature } from '@/features/multisig-operation-details';
import { notificationsNavigationFeature } from '@/features/notifications-navigation';
import { operationsNavigationFeature } from '@/features/operations-navigation';
import { polkadotExtensionWalletFeature } from '@/features/polkadot-extension-wallet';
import { proxiesModel } from '@/features/proxies';
import { proxyOperationDetailFeature } from '@/features/proxy-operation-details';
import { settingsNavigationFeature } from '@/features/settings-navigation';
Expand All @@ -42,23 +44,46 @@ import { walletSelectFeature } from '@/features/wallet-select';
import { walletWalletConnectFeature } from '@/features/wallet-wallet-connect';
import { walletWatchOnlyFeature } from '@/features/wallet-watch-only';

import { polkadotExtensionWalletFeature } from 'src/renderer/features/polkadot-extension-wallet';
const configureDomains = () => {
const config = createFeature({ name: 'spektr/config' });

export const initModel = () => {
config.inject(collectivesConfig.calculateVoteWeightPipeline, (defaultValue, { pallet, excessRank }) => {
if (pallet === 'fellowship') {
return tracksService.getGeometricVoteWeight(excessRank);
}

if (pallet === 'ambassador') {
return tracksService.getLinearVoteWeight(excessRank);
}

return defaultValue;
});

return config;
};

const populate = () => {
accounts.populate();
walletModel.populate();

// TODO rework as populate effects
kernelModel.events.appStarted();
governanceModel.events.governanceStarted();
proxiesModel.events.workerStarted();
walletModel.events.walletStarted();
networkModel.events.networkStarted();
proxyModel.events.proxyStarted();
assetsSettingsModel.events.assetsStarted();
notificationModel.events.notificationsStarted();
basketModel.events.basketStarted();
multisigsModel.events.subscribe();
};

export const bootstrap = () => {
const config = configureDomains();

registerFeatures([
config,

assetsNavigationFeature,
stakingNavigationFeature,
governanceNavigationFeature,
Expand Down Expand Up @@ -95,4 +120,6 @@ export const initModel = () => {

importDBFeature,
]);

populate();
};
7 changes: 7 additions & 0 deletions src/renderer/domains/collectives/configuration/inject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createPipeline } from '@/shared/di';
import { type CollectivePalletsType } from '../_lib/types';

export const calculateVoteWeightPipeline = createPipeline<
number,
{ pallet: CollectivePalletsType; excessRank: number }
>();
2 changes: 2 additions & 0 deletions src/renderer/domains/collectives/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export {
voting,
} from './init';

export * as config from './configuration/inject';

export type { CollectivePalletsType } from './_lib/types';

export type {
Expand Down
22 changes: 19 additions & 3 deletions src/renderer/domains/collectives/members/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,26 @@ const {

fn();

// TODO check if section name is correct
return polkadotjsHelpers.subscribeSystemEvents({ api, section: `${palletType}Core` }, fn).then(fn => () => {
const unsubscribe = Promise.all([
polkadotjsHelpers.subscribeSystemEvents(
{
api,
section: `${palletType}Collective`,
methods: ['MemberAdded', 'MemberExchanged', 'MemberRemoved', 'RankChanged'],
},
fn,
),
polkadotjsHelpers.subscribeSystemEvents(
{ api, section: `${palletType}Core`, methods: ['Imported', 'Swapped', 'Promoted', 'Demoted', 'ActiveChanged'] },
fn,
),
]);

return unsubscribe.then(fns => () => {
abortController.abort();
fn();
for (const fn of fns) {
fn();
}
});
},
map: (store, { params, result }) => {
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/domains/collectives/referendum/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createStore } from 'effector';

import { type ChainId } from '@/shared/core';
import { createDataSource, createDataSubscription, createPagesHandler } from '@/shared/effector';
import { merge, pickNestedValue, setNestedValue } from '@/shared/lib/utils';
import { isEqual, merge, pickNestedValue, setNestedValue } from '@/shared/lib/utils';
import { type ReferendumId, referendaPallet } from '@/shared/pallet/referenda';
import { polkadotjsHelpers } from '@/shared/polkadotjs-helpers';
import { type CollectivePalletsType, type CollectivesStruct } from '../_lib/types';
Expand Down Expand Up @@ -63,6 +63,7 @@ const { pending, subscribe, unsubscribe, received, fulfilled } = createDataSubsc
b: result,
mergeBy: x => x.id,
sort: (a, b) => b.id - a.id,
filter: (a, b) => !isEqual(a, b),
}),
);
},
Expand Down
55 changes: 40 additions & 15 deletions src/renderer/domains/collectives/tracks/service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { BN, BN_BILLION, BN_ONE, bnMax, bnMin } from '@polkadot/util';

import { type TrackId } from '@/shared/pallet/referenda';
import { type CollectivePalletsType } from '../_lib/types';
import { calculateVoteWeightPipeline } from '../configuration/inject';
import { type Member } from '../members/types';
import { type Tally } from '../referendum/types';

Expand All @@ -9,40 +11,62 @@ import { type Track, type VotingCurve, type VotingThreshold } from './types';
/**
* @see /~https://github.com/paritytech/polkadot-sdk/blob/master/cumulus/parachains/runtimes/collectives/collectives-westend/src/fellowship/tracks.rs#L63
*/
const getMinimumRank = (trackId: TrackId, maxRank: number) => {
if (trackId >= 0 && trackId <= 9) {
return trackId;
const getMinimumRank = (track: TrackId, maxRank: number) => {
if (track >= 0 && track <= 9) {
return track;
}

if (trackId >= 11 && trackId <= 16) {
return trackId - 8;
if (track >= 11 && track <= 16) {
return track - 8;
}

if (trackId >= 21 && trackId <= 26) {
return trackId - 18;
if (track >= 21 && track <= 26) {
return track - 18;
}

if (trackId >= 31 && trackId <= 36) {
return trackId - 28;
if (track >= 31 && track <= 36) {
return track - 28;
}

return maxRank;
};

const getExcessRank = (rank: number, maxRank: number, track: TrackId) => {
return rank - getMinimumRank(track, maxRank);
};

/**
* @see /~https://github.com/paritytech/polkadot-sdk/blob/34352e82cf557f20375c1757a2d934e3a9d2a6b0/substrate/frame/ranked-collective/src/lib.rs#L238
* Not meant to be used directly, use getVoteWeight method instead.
*/
const getGeometricVoteWeight = (rank: number) => {
const v = rank + 1;
const getGeometricVoteWeight = (excessRank: number) => {
const v = excessRank + 1;

return (v * (v + 1)) / 2;
};

/**
* @see /~https://github.com/paritytech/polkadot-sdk/blob/34352e82cf557f20375c1757a2d934e3a9d2a6b0/substrate/frame/ranked-collective/src/lib.rs#L223
* Not meant to be used directly, use getVoteWeight method instead.
*/
const getLinearVoteWeight = (rank: number) => {
return rank + 1;
const getLinearVoteWeight = (excessRank: number) => {
return excessRank + 1;
};

const getVoteWeight = ({
pallet,
rank,
maxRank,
track,
}: {
pallet: CollectivePalletsType;
rank: number;
maxRank: number;
track: TrackId;
}) => {
const excessRank = getExcessRank(rank, maxRank, track);

return calculateVoteWeightPipeline(0, { pallet, excessRank });
};

const getThreshold = (curve: VotingCurve, minRank: number, maxRank: number): BN => {
Expand Down Expand Up @@ -128,14 +152,15 @@ const approvalThreshold = ({ track, maxRank, tally }: ApprovalParams): VotingThr
};
};

const rankSatisfiesVotingThreshold = (rank: number, maxRank: number, trackId: TrackId) => {
return getMinimumRank(trackId, maxRank) <= rank;
const rankSatisfiesVotingThreshold = (rank: number, maxRank: number, track: TrackId) => {
return getExcessRank(rank, maxRank, track) >= 0;
};

export const tracksService = {
getMinimumRank,
getLinearVoteWeight,
getGeometricVoteWeight,
getVoteWeight,
getThreshold,
supportThreshold,
approvalThreshold,
Expand Down
63 changes: 39 additions & 24 deletions src/renderer/domains/collectives/votingHistory/model.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { type ApiPromise } from '@polkadot/api';
import { type UnitValue, combine, createStore } from 'effector';
import { readonly } from 'patronum';
import { z } from 'zod';

import { type ChainId, ExternalType } from '@/shared/core';
import { createDataSource, createDataSubscription } from '@/shared/effector';
import { merge, nullable, pickNestedValue, setNestedValue } from '@/shared/lib/utils';
import { collectivePallet } from '@/shared/pallet/collective';
import { type ReferendumId } from '@/shared/pallet/referenda';
import { merge, pickNestedValue, setNestedValue, toAccountId } from '@/shared/lib/utils';
import { type ReferendumId, referendaPallet } from '@/shared/pallet/referenda';
import { polkadotjsHelpers } from '@/shared/polkadotjs-helpers';
import { type AccountId } from '@/shared/polkadotjs-schemas';
import { networkModel } from '@/entities/network';
import { type CollectivePalletsType, type CollectivesStruct } from '../_lib/types';
Expand Down Expand Up @@ -72,42 +73,56 @@ type VotingSubscribeParams = {
palletType: CollectivePalletsType;
api: ApiPromise;
chainId: ChainId;
referendums: ReferendumId[];
accounts: AccountId[];
};

const { subscribe: subscribeAccountsVoting, unsubscribe: unsubscribeAccountsVoting } = createDataSubscription<
UnitValue<typeof $votes>,
VotingSubscribeParams,
Awaited<ReturnType<typeof collectivePallet.storage.voting>>
Vote
>({
initial: $votes,

fn({ palletType, api, accounts, referendums }, callback) {
const keys = referendums.flatMap(referendum => accounts.map(account => [referendum, account] as const));

return collectivePallet.storage.subscribeVoting(palletType, api, keys, value => {
callback({ done: true, value });
fn({ palletType, api, accounts }, callback) {
const number = z.string().transform(v => parseInt(v));
const eventSchema = z.object({
who: z.string(),
poll: number.transform(referendaPallet.helpers.toReferendumId),
vote: z.object({ Aye: number }).or(z.object({ Nay: number })),
});
},

map(store, { params: { chainId, palletType }, result: response }) {
const newVotes: Vote[] = [];
for (const vote of response.values()) {
if (nullable(vote.vote)) continue;

newVotes.push({
accountId: vote.key.accountId,
referendumId: vote.key.referendumId,
decision: vote.vote.type,
votes: vote.vote.data,
});
}
const unsubscribe = polkadotjsHelpers.subscribeSystemEvents(
{ api, section: `${palletType}Collective`, methods: ['Voted'] },
event => {
console.log(event);
const data = eventSchema.parse(event.data.toHuman());
const accountId = toAccountId(data.who);
if (!accounts.some(a => a === accountId)) {
return;
}

const vote: Vote = {
accountId,
referendumId: data.poll,
decision: 'Aye' in data.vote ? 'Aye' : 'Nay',
votes: 'Aye' in data.vote ? data.vote.Aye : data.vote.Nay,
};

callback({
value: vote,
done: true,
});
},
);

return unsubscribe;
},

map(store, { params: { chainId, palletType }, result: vote }) {
const existingVotes = pickNestedValue(store, palletType, chainId) ?? [];
const merged = merge({
a: existingVotes,
b: newVotes,
b: [vote],
mergeBy: ({ referendumId, accountId }) => [referendumId, accountId],
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ describe('entities/wallet/model/wallet-model', () => {
test('should set $allWallets, $activeWallets with data on appStarted', async () => {
const wallets = walletMock.getWallets(1);

jest.spyOn(storageService.contacts, 'readAll').mockResolvedValue([]);
jest.spyOn(storageService.wallets, 'readAll').mockResolvedValue(wallets);
jest.spyOn(storageService.wallets, 'update').mockResolvedValue(1);

const scope = fork({
handlers: [[accounts.populate, () => walletMock.accounts]],
handlers: [
[accounts.populate, () => walletMock.accounts],
[walletModel.populate, () => wallets],
],
});

await allSettled(walletModel.events.walletStarted, { scope });
await allSettled(accounts.populate, { scope });
await allSettled(walletModel.populate, { scope });
expect(scope.getState(walletModel.$allWallets)).toEqual(wallets);
expect(scope.getState(walletModel.$activeWallet)).toEqual(wallets[0]);
});
Expand Down
Loading

0 comments on commit a19d1df

Please sign in to comment.