diff --git a/packages/sdk-accounts/CHANGELOG.md b/packages/sdk-accounts/CHANGELOG.md new file mode 100644 index 0000000..f6efa27 --- /dev/null +++ b/packages/sdk-accounts/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +Initial release diff --git a/packages/sdk-accounts/README.md b/packages/sdk-accounts/README.md new file mode 100644 index 0000000..3d74aa0 --- /dev/null +++ b/packages/sdk-accounts/README.md @@ -0,0 +1 @@ +# @polkadot-api/sdk-accounts diff --git a/packages/sdk-accounts/package.json b/packages/sdk-accounts/package.json new file mode 100644 index 0000000..17bc9dc --- /dev/null +++ b/packages/sdk-accounts/package.json @@ -0,0 +1,57 @@ +{ + "name": "@polkadot-api/sdk-accounts", + "version": "0.0.1-0", + "sideEffects": false, + "author": "Victor Oliva (https://github.com/voliva)", + "repository": { + "type": "git", + "url": "git+https://github.com/polkadot-api/papi-sdks.git" + }, + "exports": { + ".": { + "node": { + "production": { + "import": "./dist/esm/index.mjs", + "require": "./dist/min/index.js", + "default": "./dist/index.js" + }, + "import": "./dist/esm/index.mjs", + "require": "./dist/index.js", + "default": "./dist/index.js" + }, + "module": "./dist/esm/index.mjs", + "import": "./dist/esm/index.mjs", + "require": "./dist/index.js", + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "main": "./dist/index.js", + "module": "./dist/esm/index.mjs", + "browser": "./dist/esm/index.mjs", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc --noEmit && rollup -c ../../rollup.config.js", + "lint": "prettier --check README.md \"src/**/*.{js,jsx,ts,tsx,json,md}\"", + "format": "prettier --write README.md \"src/**/*.{js,jsx,ts,tsx,json,md}\"", + "prepack": "pnpm run build" + }, + "license": "MIT", + "dependencies": { + "@polkadot-api/common-sdk-utils": "workspace:*", + "@react-rxjs/utils": "^0.9.7" + }, + "peerDependencies": { + "@noble/hashes": ">=1.6.1", + "polkadot-api": ">=1.8.1", + "rxjs": ">=7.8.0" + }, + "devDependencies": { + "@noble/hashes": "^1.6.1", + "polkadot-api": "^1.8.1", + "rxjs": "^7.8.1" + } +} diff --git a/packages/sdk-accounts/src/index.ts b/packages/sdk-accounts/src/index.ts new file mode 100644 index 0000000..174c47c --- /dev/null +++ b/packages/sdk-accounts/src/index.ts @@ -0,0 +1,4 @@ +export { createLinkedAccountsSdk } from "./linkedAccounts/linked-accounts-sdk" +export { novasamaProvider } from "./linkedAccounts/novasama-provider" +export type * from "./linkedAccounts/descriptors" +export type * from "./linkedAccounts/sdk-types" diff --git a/packages/sdk-accounts/src/linkedAccounts/descriptors.ts b/packages/sdk-accounts/src/linkedAccounts/descriptors.ts new file mode 100644 index 0000000..4ec79db --- /dev/null +++ b/packages/sdk-accounts/src/linkedAccounts/descriptors.ts @@ -0,0 +1,60 @@ +import { + ApisTypedef, + Enum, + PalletsTypedef, + SS58String, + StorageDescriptor, + TypedApi, +} from "polkadot-api" + +type ProxyType = Enum<{ + Any: undefined + NonTransfer: undefined + Governance: undefined + Staking: undefined + CancelProxy: undefined + Auction: undefined + NominationPools: undefined +}> + +type LinkedAccountsSdkPallets = PalletsTypedef< + { + Proxy: { + /** + * The set of account proxies. Maps the account which has delegated to the accounts + * which are being delegated to, together with the amount held on deposit. + */ + Proxies: StorageDescriptor< + [Key: SS58String], + [ + Array<{ + delegate: SS58String + proxy_type: ProxyType + delay: number + }>, + unknown, + ], + false, + never + > + } + }, + {}, + {}, + {}, + {} +> +type LinkedAccountsSdkDefinition = SdkDefinition< + LinkedAccountsSdkPallets, + ApisTypedef<{}> +> +export type LinkedAccountsSdkTypedApi = TypedApi + +type SdkDefinition = { + descriptors: Promise & { + pallets: P + apis: R + } + asset: any + metadataTypes: any +} diff --git a/packages/sdk-accounts/src/linkedAccounts/linked-accounts-sdk.ts b/packages/sdk-accounts/src/linkedAccounts/linked-accounts-sdk.ts new file mode 100644 index 0000000..97abebf --- /dev/null +++ b/packages/sdk-accounts/src/linkedAccounts/linked-accounts-sdk.ts @@ -0,0 +1,114 @@ +import { SS58String } from "polkadot-api" +import { + combineLatest, + defaultIfEmpty, + filter, + from, + map, + merge, + Observable, + of, + shareReplay, + startWith, + switchMap, + take, +} from "rxjs" +import { LinkedAccountsSdkTypedApi } from "./descriptors" +import { + LinkedAccountsResult, + LinkedAccountsSdk, + MultisigProvider, + NestedLinkedAccountsResult, +} from "./sdk-types" + +export function createLinkedAccountsSdk( + typedApi: LinkedAccountsSdkTypedApi, + multisigProvider: MultisigProvider, +): LinkedAccountsSdk { + const proxy$ = (address: string) => + from(typedApi.query.Proxy.Proxies.getValue(address)).pipe( + map((r) => r[0].map((v) => v.delegate)), + ) + + const cache: Record> = {} + const getLinkedAccounts$ = (address: SS58String) => { + if (address in cache) return cache[address] + cache[address] = merge( + proxy$(address).pipe( + filter((v) => v.length > 0), + map( + (value): LinkedAccountsResult => ({ + type: "proxy", + value: { addresses: value }, + }), + ), + ), + from(multisigProvider(address)).pipe( + filter((v) => !!v), + map((value): LinkedAccountsResult => ({ type: "multisig", value })), + ), + ).pipe( + take(1), + defaultIfEmpty({ + type: "root", + } satisfies LinkedAccountsResult), + shareReplay(1), + ) + return cache[address] + } + + const getNestedLinkedAccounts$ = ( + address: SS58String, + ): Observable => + getLinkedAccounts$(address).pipe( + switchMap((result) => { + if (result.type === "root") return of(result) + + const accounts$ = combineLatest( + result.value.addresses.map((inner) => + getNestedLinkedAccounts$(inner), + ), + ).pipe( + map((nested) => + result.value.addresses.map((inner, i) => ({ + address: inner, + linkedAccounts: nested[i], + })), + ), + startWith( + result.value.addresses.map((inner) => ({ + address: inner, + linkedAccounts: null, + })), + ), + ) + + if (result.type === "proxy") { + return accounts$.pipe( + map( + (accounts): NestedLinkedAccountsResult => ({ + type: "proxy", + value: { accounts }, + }), + ), + ) + } + return accounts$.pipe( + map( + (accounts): NestedLinkedAccountsResult => ({ + type: "multisig", + value: { + threshold: result.value.threshold, + accounts, + }, + }), + ), + ) + }), + ) + + return { + getLinkedAccounts$, + getNestedLinkedAccounts$, + } +} diff --git a/packages/sdk-accounts/src/linkedAccounts/novasama-provider.ts b/packages/sdk-accounts/src/linkedAccounts/novasama-provider.ts new file mode 100644 index 0000000..0721201 --- /dev/null +++ b/packages/sdk-accounts/src/linkedAccounts/novasama-provider.ts @@ -0,0 +1,64 @@ +import { AccountId, Binary, getSs58AddressInfo } from "polkadot-api" +import { MultisigProvider } from "./sdk-types" + +export const novasamaProvider: MultisigProvider = async (account) => { + try { + const info = getSs58AddressInfo(account) + if (!info.isValid) { + throw new Error("Invalid SS58") + } + const accountIdCodec = AccountId(info.ss58Format) + + const response = await fetch( + "https://subquery-proxy-polkadot-prod.novasama-tech.org", + { + method: "POST", + headers: { + accept: "application/json", + "content-type": "application/json", + }, + body: JSON.stringify({ + query: ` + query Multisig($address: String) { + accounts(filter: {id: {equalTo: $address}, isMultisig: {equalTo: true}}, first: 1) { + nodes { + signatories { + nodes { + signatoryId + } + } + threshold + } + } + } + `, + variables: { + address: Binary.fromBytes(info.publicKey).asHex(), + }, + operationName: "Multisig", + }), + }, + ) + const result = await (response.json() as Promise<{ + data: { + accounts: { + nodes: Array<{ + signatories: { nodes: Array<{ signatoryId: string }> } + threshold: number + }> + } + } + }>) + const multisig = result.data.accounts.nodes[0] + if (!multisig) return null + return { + threshold: multisig.threshold, + addresses: multisig.signatories.nodes.map((v) => + accountIdCodec.dec(v.signatoryId), + ), + } + } catch (ex) { + console.error(ex) + return null + } +} diff --git a/packages/sdk-accounts/src/linkedAccounts/sdk-types.ts b/packages/sdk-accounts/src/linkedAccounts/sdk-types.ts new file mode 100644 index 0000000..2d35fa4 --- /dev/null +++ b/packages/sdk-accounts/src/linkedAccounts/sdk-types.ts @@ -0,0 +1,56 @@ +import { SS58String } from "polkadot-api" +import { Observable } from "rxjs" + +export type LinkedAccountsResult = + | { + type: "root" + } + | { + type: "proxy" + value: { + addresses: SS58String[] + } + } + | { + type: "multisig" + value: { + threshold: number + addresses: SS58String[] + } + } + +export type NestedLinkedAccountsResult = + | { + type: "root" + } + | { + type: "proxy" + value: { + accounts: Array<{ + address: SS58String + linkedAccounts: NestedLinkedAccountsResult | null + }> + } + } + | { + type: "multisig" + value: { + threshold: number + accounts: Array<{ + address: SS58String + linkedAccounts: NestedLinkedAccountsResult | null + }> + } + } + +export interface LinkedAccountsSdk { + getLinkedAccounts$: (address: SS58String) => Observable + getNestedLinkedAccounts$: ( + address: SS58String, + ) => Observable +} + +export type MultisigProvider = (address: SS58String) => Promise<{ + addresses: SS58String[] + threshold: number +} | null> diff --git a/packages/sdk-accounts/tsconfig.json b/packages/sdk-accounts/tsconfig.json new file mode 100644 index 0000000..73f9f03 --- /dev/null +++ b/packages/sdk-accounts/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base", + "include": ["src", "tests"], + "compilerOptions": { + "baseUrl": "src", + "resolveJsonModule": true, + "skipLibCheck": true, + "paths": { + "@/*": ["*"] + } + } +} diff --git a/packages/sdk-governance/README.md b/packages/sdk-governance/README.md index 87d2c2e..440f216 100644 --- a/packages/sdk-governance/README.md +++ b/packages/sdk-governance/README.md @@ -1 +1 @@ -# @polkadot-api/ink-sdk +# @polkadot-api/sdk-governance diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39de144..63e4701 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,6 +70,25 @@ importers: specifier: ^7.8.1 version: 7.8.1 + packages/sdk-accounts: + dependencies: + '@polkadot-api/common-sdk-utils': + specifier: workspace:* + version: link:../common-utils + '@react-rxjs/utils': + specifier: ^0.9.7 + version: 0.9.7(@react-rxjs/core@0.10.7)(react@19.0.0)(rxjs@7.8.1) + devDependencies: + '@noble/hashes': + specifier: ^1.6.1 + version: 1.6.1 + polkadot-api: + specifier: ^1.8.1 + version: 1.8.1(rxjs@7.8.1) + rxjs: + specifier: ^7.8.1 + version: 7.8.1 + packages/sdk-governance: dependencies: '@polkadot-api/common-sdk-utils':