Skip to content

Commit

Permalink
feat: Get chainSpec, metadata from observables, consts from metadata (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
rossbulat authored Nov 3, 2024
2 parents b316a5d + 78a6765 commit 3c95088
Show file tree
Hide file tree
Showing 33 changed files with 64,702 additions and 60,051 deletions.
1 change: 0 additions & 1 deletion packages/app/src/contexts/ChainSpaceEnv/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export const dummyChainSpec: APIChainSpec = {
transactionVersion: 0,
},
ss58Prefix: 0,
magicNumber: 0,
metadata: {},
consts: {},
};
13 changes: 10 additions & 3 deletions packages/app/src/controllers/Api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,16 @@ export class ApiController {
return instanceIndex;
}

// Get an instance `api` by ownerId and instanceIndex.
static getInstanceApi(ownerId: OwnerId, instanceIndex: number) {
return this.#instances[ownerId][instanceIndex].api;
// Get an instance `api` by ownerId and instanceIndex. Returns Polkadot JS API instance by
// default, or Polkadot API's Observable client if `observable` is true.
static getInstanceApi(
ownerId: OwnerId,
instanceIndex: number,
observable = false
) {
return !observable
? this.#instances[ownerId][instanceIndex].api
: this.#instances[ownerId][instanceIndex].papiClient;
}

// ------------------------------------------------------
Expand Down
23 changes: 21 additions & 2 deletions packages/app/src/controllers/Subscriptions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,34 @@
import type { AccountBalances } from 'model/AccountBalances';
import type { BlockNumber } from 'model/BlockNumber';
import type { NextFreeParaId } from 'model/NextFreeParaId';
import type { ChainSpec } from 'model/Observables/ChainSpec';
import type { TaggedMetadata } from 'model/Observables/TaggedMetadata';

// Define all possible subscription classes.
export type Subscription = AccountBalances | BlockNumber | NextFreeParaId;
export type Subscription = UnsubSubscriptions | ObservableGetters;

// Polkadot JS API subscriptions (unsubscribe functions).
export type UnsubSubscriptions = AccountBalances | BlockNumber | NextFreeParaId;

// Polkadot API Getters (observables wrapped in an async function that resolve upon completion).
export type ObservableGetters = ChainSpec | TaggedMetadata;

// the record of subscriptions, keyed by tabId.
export type ChainSubscriptions = Record<string, Subscription>;
export type ChainSubscriptions = Record<
string,
Subscription | ObservableGetters
>;

// Abstract class that ensures all subscription classes have an unsubscribe method.
export abstract class Unsubscribable {
// Unsubscribe from unsubs present in this class.
abstract unsubscribe: () => void;
}

// Abstract class that allows an await-able function to get a value from an observable.
export abstract class ObservableGetter {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
declare get: () => Promise<any>;
// Unsubscribe from unsubs present in this class.
abstract unsubscribe: () => void;
}
4 changes: 2 additions & 2 deletions packages/app/src/model/AccountBalances/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,15 @@ export class AccountBalances implements Unsubscribable {
accountsAdded.forEach(async (address) => {
this.#accounts.push(address);

const unsub = await api.queryMulti<AnyJson>(
const unsub = await api.queryMulti(
[
[api.query.system.account, address],
[api.query.balances.locks, address],
],
async ([
{ data: accountData, nonce },
locksResult,
]): Promise<void> => {
]: AnyJson): Promise<void> => {
// Update balance data for this address.
this.balances[address] = {
nonce: nonce.toNumber(),
Expand Down
140 changes: 89 additions & 51 deletions packages/app/src/model/Api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,33 @@ import type { ChainId } from 'config/networks/types';
import type {
APIChainSpec,
APIStatusEventDetail,
APIChainSpecVersion,
EventStatus,
ErrDetail,
ApiInstanceId,
APIChainSpecEventDetail,
PapiObservableClient,
PapiDynamicBuilder,
} from './types';
import { MetadataController } from 'controllers/Metadata';
import { SubscriptionsController } from 'controllers/Subscriptions';
import type { AnyJson } from '@w3ux/types';
import BigNumber from 'bignumber.js';
import type { ChainSpaceId, OwnerId } from 'types';
import type { JsonRpcProvider } from '@polkadot-api/ws-provider/web';
import { getWsProvider } from '@polkadot-api/ws-provider/web';
import { createClient as createRawClient } from '@polkadot-api/substrate-client';
import { getObservableClient } from '@polkadot-api/observable-client';
import { ChainSpec } from 'model/Observables/ChainSpec';

import { getDataFromObservable } from 'model/Observables/util';
import { TaggedMetadata } from 'model/Observables/TaggedMetadata';
import { MetadataV15 } from 'model/Metadata/MetadataV15';
import { PalletScraper } from 'model/Scraper/Pallet';
import {
getLookupFn,
getDynamicBuilder,
} from '@polkadot-api/metadata-builders';
import { formatChainSpecName } from './util';
import { MetadataController } from 'controllers/Metadata';
import BigNumber from 'bignumber.js';

export class Api {
// ------------------------------------------------------
Expand Down Expand Up @@ -53,11 +64,14 @@ export class Api {
// PAPI Instance.
#papiClient: PapiObservableClient;

// PAPI Dynamic Builder.
#papiBuilder: PapiDynamicBuilder;

// The current RPC endpoint.
#rpcEndpoint: string;

// The current chain spec.
chainSpec: APIChainSpec | undefined;
chainSpec: APIChainSpec;

// Chain constants.
consts: Record<string, AnyJson> = {};
Expand Down Expand Up @@ -104,6 +118,10 @@ export class Api {
return this.#papiClient;
}

get papiBuilder() {
return this.#papiBuilder;
}

get rpcEndpoint() {
return this.#rpcEndpoint;
}
Expand Down Expand Up @@ -168,48 +186,63 @@ export class Api {
}

async fetchChainSpec() {
// Fetch chain specs.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const newChainSpec = await Promise.all<any>([
this.api.rpc.system.chain(),
this.api.consts.system.version,
this.api.consts.system.ss58Prefix,
]);

// Check that chain values have been fetched before committing to state.
if (newChainSpec.every((c) => c?.toHuman() !== undefined)) {
const chain = newChainSpec[0].toString();
const specVersion =
newChainSpec[1].toJSON() as unknown as APIChainSpecVersion;
const ss58Prefix = Number(newChainSpec[2].toString());

// Also retreive the JSON formatted metadata here for the UI to construct from.
const metadataPJs = this.api.runtimeMetadata;
const metadataJson = metadataPJs.asV15.toJSON();

// Set chainspec and metadata, or dispatch an error and disconnect otherwise.
if (specVersion && metadataJson) {
const magicNumber = metadataJson.magicNumber;

this.chainSpec = {
chain,
version: specVersion,
ss58Prefix,
magicNumber: magicNumber as number,
metadata: MetadataController.instantiate(metadataJson),
consts: {},
};
} else {
this.dispatchEvent(this.ensureEventStatus('error'), {
err: 'ChainSpecError',
});
try {
const [resultChainSpec, resultTaggedMetadata] = await Promise.all([
// Get chain spec via observable.
getDataFromObservable(
this.#instanceId,
'chainSpec',
new ChainSpec(this.#ownerId, this.#instanceId)
),
// Get metadata via observable.
getDataFromObservable(
this.#instanceId,
'metadata',
new TaggedMetadata(this.#ownerId, this.#instanceId)
),
]);

if (!resultChainSpec || !resultTaggedMetadata) {
throw new Error();
}

// Now metadata has been retrieved, create a dynamic builder for the metadata and persist it
// to this class.
this.#papiBuilder = getDynamicBuilder(getLookupFn(resultTaggedMetadata));

// Format a human-readable chain name based on spec name.
const chainName = formatChainSpecName(resultChainSpec.specName);

// Prepare metadata class and scraper to retrieve constants.
const resultTaggedMetadataV15 = new MetadataV15(resultTaggedMetadata);
const scraper = new PalletScraper(resultTaggedMetadataV15);

// Get SS58 Prefix via metadata - defaults to 0.
const ss58Prefix = this.#papiBuilder
.buildConstant('System', 'SS58Prefix')
.dec(String(scraper.getConstantValue('System', 'SS58Prefix') || '0x'));

// We are still using the Polkadot JS API raw metadata format.
const metadataPJs = this.api.runtimeMetadata;
const metadataPJsJson = metadataPJs.asV15.toJSON();

// Format resulting class chain spec and persist to class.
this.chainSpec = {
chain: chainName,
version: resultChainSpec.specVersion,
ss58Prefix: Number(ss58Prefix),
metadata: MetadataController.instantiate(metadataPJsJson),
consts: {},
};
} catch (e) {
// Flag an error if there are any issues bootstrapping chain spec.
this.dispatchEvent(this.ensureEventStatus('error'), {
err: 'ChainSpecError',
});
}
}

async handleFetchChainData() {
// Fetch chain spec from Polkadot JS API.
//
// NOTE: This is a one-time fetch. It's currently not possible to update the chain spec without
// a refresh.
if (!this.chainSpec) {
Expand All @@ -236,21 +269,26 @@ export class Api {

// Fetch chain consts. Must be called after chain spec is fetched.
fetchConsts = () => {
const metadata = this.chainSpec?.metadata;
const metadata = this.chainSpec.metadata;
const newConsts: Record<string, AnyJson> = {};

try {
if (metadata) {
const hasBalancesPallet = metadata.palletExists('Balances');
const existentialDeposit = hasBalancesPallet
? new BigNumber(
this.api.consts.balances.existentialDeposit.toString()
const scraper = new PalletScraper(metadata);
const hasBalancesPallet = metadata.palletExists('Balances');

// Attempt to fetch existential deposit if Balances pallet exists.
if (hasBalancesPallet) {
const existentialDeposit = this.#papiBuilder
.buildConstant('Balances', 'ExistentialDeposit')
.dec(
String(
scraper.getConstantValue('Balances', 'ExistentialDeposit') || '0x'
)
: null;
);

this.consts = {
existentialDeposit,
};
newConsts['existentialDeposit'] = new BigNumber(existentialDeposit);
}
this.consts = newConsts;
} catch (e) {
this.consts = {};
}
Expand Down
10 changes: 7 additions & 3 deletions packages/app/src/model/Api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ import type { ChainId } from 'config/networks/types';
import type { MetadataVersion } from 'controllers/Metadata/types';
import type { ChainSpaceId, OwnerId } from 'types';

// TODO: Replace with actual PAPI client interface when available
export type PapiObservableClient = unknown;
// NOTE: Replace with actual PAPI client interface when available.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type PapiObservableClient = any;

// NOTE: Replace with actual PAPI builder interface when available.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type PapiDynamicBuilder = any;

// An id associated with an api instance. ChainState, ChainSpec, subscriptions, etc. all use this id
// to associate with an api instance.
Expand Down Expand Up @@ -44,7 +49,6 @@ export interface APIChainSpec {
chain: string | null;
version: APIChainSpecVersion;
ss58Prefix: number;
magicNumber: number;
metadata: MetadataVersion | AnyJson; // NOTE: This could be improved, but no significant impact on the app.
consts: AnyJson;
}
Expand Down
7 changes: 7 additions & 0 deletions packages/app/src/model/Api/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,10 @@ export const getIndexFromInstanceId = (str: ApiInstanceId) => {
const result = str.substring(index + 1);
return Number(result);
};

// Format chain spac names into a human-readable format.
export const formatChainSpecName = (specName: string) =>
specName
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
Loading

0 comments on commit 3c95088

Please sign in to comment.