Skip to content

Commit

Permalink
feat: foreign assets balances subscription (#2823)
Browse files Browse the repository at this point in the history
  • Loading branch information
johnthecat authored Dec 10, 2024
1 parent d0d38ff commit c463642
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 20 deletions.
54 changes: 35 additions & 19 deletions src/renderer/shared/api/balances/service/balanceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { type AccountData, type Balance as ChainBalance } from '@polkadot/types/
import { type PalletBalancesBalanceLock } from '@polkadot/types/lookup';
import { type Codec } from '@polkadot/types/types';
import { type BN, BN_ZERO, hexToU8a } from '@polkadot/util';
import { camelCase } from 'lodash';
import noop from 'lodash/noop';
import uniq from 'lodash/uniq';

Expand All @@ -18,7 +19,7 @@ import {
type LockTypes,
type OrmlExtras,
} from '@/shared/core';
import { getAssetId, getRepeatedIndex, toAddress } from '@/shared/lib/utils';
import { getAssetId, getRepeatedIndex, groupBy, isHex, nullable, toAddress } from '@/shared/lib/utils';

type NoIdBalance = Omit<Balance, 'id'>;

Expand All @@ -45,25 +46,25 @@ function subscribeBalances(
): UnsubscribePromise[] {
const uniqueAccountIds = uniq(accountIds);

const { nativeAsset, statemineAssets, ormlAssets } = chain.assets.reduce<{
nativeAsset?: Asset;
statemineAssets: Asset[];
ormlAssets: Asset[];
}>(
(acc, asset) => {
if (asset.type === AssetType.NATIVE) acc.nativeAsset = asset;
if (asset.type === AssetType.STATEMINE) acc.statemineAssets.push(asset);
if (asset.type === AssetType.ORML) acc.ormlAssets.push(asset);
const nativeAsset = chain.assets.find((asset) => asset.type === AssetType.NATIVE);
const statemineAssets = chain.assets.filter((asset) => asset.type === AssetType.STATEMINE);
const ormlAssets = chain.assets.filter((asset) => asset.type === AssetType.ORML);

return acc;
},
{ nativeAsset: undefined, statemineAssets: [], ormlAssets: [] },
);
const stateminePalletGroups = groupBy(statemineAssets, (asset) => {
if (asset.typeExtras && 'palletName' in asset.typeExtras) {
return camelCase(asset.typeExtras.palletName);
}

return 'assets';
});

return [
subscribeNativeAssetsChange(api, chain, nativeAsset?.assetId, uniqueAccountIds, callback),
subscribeStatemineAssetsChange(api, chain, statemineAssets, uniqueAccountIds, callback),
subscribeOrmlAssetsChange(api, chain, ormlAssets, uniqueAccountIds, callback),

...Object.entries(stateminePalletGroups).map(([pallet, assets]) => {
return subscribeStatemineAssetsChange(api, pallet, chain, assets, uniqueAccountIds, callback);
}),
];
}

Expand Down Expand Up @@ -143,25 +144,40 @@ function subscribeNativeAssetsChange(

function subscribeStatemineAssetsChange(
api: ApiPromise,
pallet: string,
chain: Chain,
assets: Asset[],
accountIds: AccountId[],
callback: (newBalances: NoIdBalance[]) => void,
): UnsubscribePromise {
if (!api || !assets.length || !accountIds.length || !api.query.assets) return Promise.resolve(noop);
if (!api || !assets.length || !accountIds.length) return Promise.resolve(noop);

if (!api.query[pallet]) {
throw new Error(`Pallet ${pallet} not found.`);
}

const type = api.tx.foreignAssets.transfer.meta.args[0].type;
if (nullable(type)) {
return Promise.resolve(noop);
}

const assetsTuples = assets.reduce<[string | Codec, Address][]>((acc, asset) => {
const assetId = getAssetId(asset);
// @ts-expect-error type argument in createType has incorrect types
const location = isHex(assetId) ? api.createType(type, assetId) : assetId;

const assetsTuples = assets.reduce<[string, Address][]>((acc, asset) => {
for (const accountId of accountIds) {
acc.push([getAssetId(asset), toAddress(accountId, { prefix: chain.addressPrefix })]);
acc.push([location, toAddress(accountId, { prefix: chain.addressPrefix })]);
}

return acc;
}, []);

return api.query.assets.account.multi(assetsTuples, (data) => {
return api.query[pallet].account.multi(assetsTuples, (data) => {
const newBalances: NoIdBalance[] = [];

for (const [index, accountInfo] of data.entries()) {
// @ts-expect-error it's hard to type such cases
const free = accountInfo.isNone ? BN_ZERO : accountInfo.unwrap().balance.toBn();
const accountIndex = index % accountIds.length;
const assetIndex = getRepeatedIndex(index, accountIds.length);
Expand Down
1 change: 1 addition & 0 deletions src/renderer/shared/core/types/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const enum AssetType {

export type StatemineExtras = {
assetId: string;
palletName?: string;
};

export type OrmlExtras = {
Expand Down
22 changes: 21 additions & 1 deletion src/renderer/shared/lib/utils/__tests__/arrays.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { addUnique, merge, splice } from '../arrays';
import { addUnique, groupBy, merge, splice } from '../arrays';

describe('shared/lib/onChainUtils/arrays', () => {
test('should insert element in the beginning', () => {
Expand Down Expand Up @@ -145,4 +145,24 @@ describe('shared/lib/onChainUtils/arrays', () => {
]);
});
});

describe('groupBy', () => {
it('should group', () => {
const list = [
{ type: 'a', v: 1 },
{ type: 'b', v: 1 },
{ type: 'a', v: 2 },
] as const;

const groups = groupBy(list, (v) => v.type);

expect(groups).toEqual({
a: [
{ type: 'a', v: 1 },
{ type: 'a', v: 2 },
],
b: [{ type: 'b', v: 1 }],
});
});
});
});
21 changes: 21 additions & 0 deletions src/renderer/shared/lib/utils/arrays.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,24 @@ export const merge = <T>({ a, b, mergeBy, sort }: MergeParams<T>) => {

return isFunction(sort) ? res.sort(sort) : res;
};

export const groupBy = <const T, const K extends PropertyKey>(
iterable: Iterable<T>,
map: (value: T) => K,
): Record<K, T[]> => {
const groups: Partial<Record<K, T[]>> = {};

for (const item of iterable) {
const itemKey = map(item);

let list = groups[itemKey];
if (list === undefined) {
list = [];
groups[itemKey] = list;
}

list.push(item);
}

return groups as Record<K, T[]>;
};
5 changes: 5 additions & 0 deletions src/renderer/shared/lib/utils/strings.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { type HexString } from '@/shared/core';
import { type Identity } from '@/shared/core/types/identity';

/**
Expand Down Expand Up @@ -159,3 +160,7 @@ export const splitCamelCaseString = (value: string): string => {
export const addLeadingZero = (value: number): string => {
return value < 10 ? `0${value}` : `${value}`;
};

export const isHex = (v: string): v is HexString => {
return v.startsWith('0x');
};

0 comments on commit c463642

Please sign in to comment.