diff --git a/packages/common-utils/CHANGELOG.md b/packages/common-utils/CHANGELOG.md index 533fd37..004c54c 100644 --- a/packages/common-utils/CHANGELOG.md +++ b/packages/common-utils/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Added + +- Property `waited` in `AsyncTransaction` to get a `Promise` + ## 0.0.1-1 2025-02-17 Initial release diff --git a/packages/common-utils/src/asyncTransaction.ts b/packages/common-utils/src/asyncTransaction.ts index a8e476a..e6b2605 100644 --- a/packages/common-utils/src/asyncTransaction.ts +++ b/packages/common-utils/src/asyncTransaction.ts @@ -12,6 +12,7 @@ export type AsyncTransaction< > & { decodedCall: Promise["decodedCall"]> getEncodedData: () => Promise + waited: Promise> } export const wrapAsyncTx = < @@ -23,6 +24,12 @@ export const wrapAsyncTx = < fn: () => Promise>, ): AsyncTransaction => { const promise = fn() + + // Prevent some runtimes from terminating for an uncaught exception + promise.catch((ex) => { + console.error(ex) + }) + return { sign: (...args) => promise.then((tx) => tx.sign(...args)), signSubmitAndWatch: (...args) => @@ -34,5 +41,6 @@ export const wrapAsyncTx = < promise.then((tx) => tx.getPaymentInfo(...args)), decodedCall: promise.then((tx) => tx.decodedCall), getEncodedData: () => promise.then((tx) => tx.getEncodedData()), + waited: promise, } } diff --git a/packages/sdk-multisig/.papi/descriptors/.gitignore b/packages/sdk-multisig/.papi/descriptors/.gitignore new file mode 100644 index 0000000..557cc81 --- /dev/null +++ b/packages/sdk-multisig/.papi/descriptors/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!package.json diff --git a/packages/sdk-multisig/.papi/descriptors/package.json b/packages/sdk-multisig/.papi/descriptors/package.json new file mode 100644 index 0000000..69a7d46 --- /dev/null +++ b/packages/sdk-multisig/.papi/descriptors/package.json @@ -0,0 +1,24 @@ +{ + "version": "0.1.0-autogenerated.237886621120070658", + "name": "@polkadot-api/descriptors", + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "module": "./dist/index.mjs", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "browser": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "sideEffects": false, + "peerDependencies": { + "polkadot-api": ">=1.11.2" + } +} diff --git a/packages/sdk-multisig/.papi/metadata/dot.scale b/packages/sdk-multisig/.papi/metadata/dot.scale new file mode 100644 index 0000000..89ec73b Binary files /dev/null and b/packages/sdk-multisig/.papi/metadata/dot.scale differ diff --git a/packages/sdk-multisig/.papi/metadata/moonbeam.scale b/packages/sdk-multisig/.papi/metadata/moonbeam.scale new file mode 100644 index 0000000..c7e9215 Binary files /dev/null and b/packages/sdk-multisig/.papi/metadata/moonbeam.scale differ diff --git a/packages/sdk-multisig/.papi/polkadot-api.json b/packages/sdk-multisig/.papi/polkadot-api.json new file mode 100644 index 0000000..4e864be --- /dev/null +++ b/packages/sdk-multisig/.papi/polkadot-api.json @@ -0,0 +1,21 @@ +{ + "version": 0, + "descriptorPath": ".papi/descriptors", + "options": { + "noDescriptorsPackage": true + }, + "entries": { + "dot": { + "chain": "polkadot", + "metadata": ".papi/metadata/dot.scale", + "genesis": "0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3", + "codeHash": "0x0627ae9ae2a8d0de99a7dad056136f855f32f82391c48bf8a531506fe231a42b" + }, + "moonbeam": { + "wsUrl": "wss://moonbeam-rpc.dwellir.com", + "metadata": ".papi/metadata/moonbeam.scale", + "genesis": "0xfe58ea77779b7abda7da4ec526d14db9b1e9cd40a217c34892af80a9b332b76d", + "codeHash": "0x802c6b285502245d97dc73bdb164c128b9c7794637c67a3f23a105327d5566d2" + } + } +} diff --git a/packages/sdk-multisig/.papi/whitelist.ts b/packages/sdk-multisig/.papi/whitelist.ts new file mode 100644 index 0000000..cd90bb2 --- /dev/null +++ b/packages/sdk-multisig/.papi/whitelist.ts @@ -0,0 +1,9 @@ +import type { DotWhitelistEntry } from "./descriptors" + +export const whitelist: DotWhitelistEntry[] = [ + "query.Multisig.Multisigs", + "tx.Multisig.as_multi_threshold_1", + "tx.Multisig.as_multi", + "tx.Multisig.approve_as_multi", + "api.TransactionPaymentApi.query_info", +] diff --git a/packages/sdk-multisig/CHANGELOG.md b/packages/sdk-multisig/CHANGELOG.md new file mode 100644 index 0000000..f6efa27 --- /dev/null +++ b/packages/sdk-multisig/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +Initial release diff --git a/packages/sdk-multisig/README.md b/packages/sdk-multisig/README.md new file mode 100644 index 0000000..054dae4 --- /dev/null +++ b/packages/sdk-multisig/README.md @@ -0,0 +1 @@ +# @polkadot-api/sdk-multisig diff --git a/packages/sdk-multisig/package.json b/packages/sdk-multisig/package.json new file mode 100644 index 0000000..0e5d101 --- /dev/null +++ b/packages/sdk-multisig/package.json @@ -0,0 +1,55 @@ +{ + "name": "@polkadot-api/sdk-multisig", + "version": "0.0.1", + "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/src/index.mjs", + "require": "./dist/index.js", + "default": "./dist/index.js" + }, + "import": "./dist/esm/src/index.mjs", + "require": "./dist/index.js", + "default": "./dist/index.js" + }, + "module": "./dist/esm/src/index.mjs", + "import": "./dist/esm/src/index.mjs", + "require": "./dist/index.js", + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "main": "./dist/index.js", + "module": "./dist/esm/src/index.mjs", + "browser": "./dist/esm/src/index.mjs", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "papi generate --whitelist .papi/whitelist.ts && mv .papi/descriptors/dist/index.mjs .papi/descriptors/dist/index.js && 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:*", + "@polkadot-api/substrate-bindings": "^0.16.3" + }, + "peerDependencies": { + "polkadot-api": ">=1.14.1", + "rxjs": ">=7.8.0" + }, + "devDependencies": { + "polkadot-api": "^1.14.1", + "rxjs": "^7.8.2" + } +} diff --git a/packages/sdk-multisig/src/index.ts b/packages/sdk-multisig/src/index.ts new file mode 100644 index 0000000..a77e0b5 --- /dev/null +++ b/packages/sdk-multisig/src/index.ts @@ -0,0 +1,2 @@ +export { createMultisigSdk } from "./multisig-sdk" +export type { MultisigSdk } from "./sdk-types" diff --git a/packages/sdk-multisig/src/multisig-sdk.ts b/packages/sdk-multisig/src/multisig-sdk.ts new file mode 100644 index 0000000..950da29 --- /dev/null +++ b/packages/sdk-multisig/src/multisig-sdk.ts @@ -0,0 +1,193 @@ +import { wrapAsyncTx } from "@polkadot-api/common-sdk-utils" +import { + Binary, + Blake2256, + getMultisigAccountId, + getSs58AddressInfo, + HexString, + sortMultisigSignatories, + SS58String, +} from "@polkadot-api/substrate-bindings" +import { AccountId, Transaction } from "polkadot-api" +import { fromHex, toHex } from "polkadot-api/utils" +import { dot, moonbeam } from "../.papi/descriptors/dist" +import { CreateMultisigSdk, MultisigSdk, MultisigTxOptions } from "./sdk-types" + +const defaultMultisigTxOptions: MultisigTxOptions = { + method: (approvals, threshold) => + approvals.length === threshold - 1 ? "as_multi" : "approve_as_multi", +} + +export const createMultisigSdk: CreateMultisigSdk = (client, addrType) => { + type Addr = typeof addrType extends "acc20" ? HexString : SS58String + const isAddr20 = addrType === "acc20" + + const ss58Api = client.getTypedApi(dot) + const addr20Api = client.getTypedApi(moonbeam) + const activeApi = isAddr20 ? addr20Api : ss58Api + + const toSS58 = AccountId().dec + + const getMultisigTx: MultisigSdk["getMultisigTx"] = ( + multisig, + signatory, + txOrCallData, + options, + ) => { + options = { + ...defaultMultisigTxOptions, + ...options, + } + + const toAddress = (value: Uint8Array): Addr => { + if (isAddr20) { + return toHex(value) as Addr + } + return toSS58(value) as Addr + } + + const pubKeys = sortMultisigSignatories( + multisig.signatories.map(getPublicKey), + ) + const multisigId = getMultisigAccountId({ + threshold: multisig.threshold, + signatories: pubKeys, + }) + + const signatoryId = getPublicKey(signatory) + const otherSignatories = pubKeys.filter( + (addr) => !u8ArrEq(addr, signatoryId), + ) + if (otherSignatories.length === multisig.signatories.length) { + throw new Error("Signer is not one of the signatories of the multisig") + } + + return wrapAsyncTx(async () => { + const [tx, callData] = + "getEncodedData" in txOrCallData + ? [txOrCallData, await txOrCallData.getEncodedData()] + : [await activeApi.txFromCallData(txOrCallData), txOrCallData] + + if (multisig.threshold === 1) { + return activeApi.tx.Multisig.as_multi_threshold_1({ + other_signatories: otherSignatories.map(toAddress), + call: tx.decodedCall, + }) + } + + const callHash = Blake2256(callData.asBytes()) + const [multisigInfo, weightInfo] = await Promise.all([ + activeApi.query.Multisig.Multisigs.getValue( + toAddress(multisigId), + Binary.fromBytes(callHash), + ), + tx.getPaymentInfo(signatoryId), + ]) + + if ( + multisigInfo?.approvals.some((approval) => + u8ArrEq(getPublicKey(approval), signatoryId), + ) + ) { + throw new Error("Multisig call already approved by signer") + } + + const method = options.method( + multisigInfo?.approvals ?? [], + multisig.threshold, + ) + + const commonPayload = { + threshold: multisig.threshold, + other_signatories: otherSignatories.map(toAddress), + max_weight: weightInfo.weight, + maybe_timepoint: multisigInfo?.when, + } + + const wrappedTx: Transaction = + method === "approve_as_multi" + ? activeApi.tx.Multisig.approve_as_multi({ + ...commonPayload, + call_hash: Binary.fromBytes(callHash), + }) + : activeApi.tx.Multisig.as_multi({ + ...commonPayload, + call: tx.decodedCall, + }) + + return wrappedTx + }) + } + + return { + getMultisigTx, + getMultisigSigner(multisig, signer, options) { + const pubKeys = sortMultisigSignatories( + multisig.signatories.map(getPublicKey), + ) + const multisigId = getMultisigAccountId({ + threshold: multisig.threshold, + signatories: pubKeys, + }) + + const toAddress = (value: Uint8Array): Addr => { + if (multisig.signatories[0].startsWith("0x")) { + return toHex(value) as Addr + } + return toSS58(value) as Addr + } + + const signerId = + "accountId" in signer + ? (signer.accountId as Uint8Array) + : signer.publicKey + + return { + publicKey: signer.publicKey, + accountId: multisigId, + signBytes() { + throw new Error("Raw bytes can't be signed with a multisig") + }, + async signTx( + callData, + signedExtensions, + metadata, + atBlockNumber, + hasher, + ) { + const tx = await activeApi.txFromCallData(Binary.fromBytes(callData)) + const wrappedTx = getMultisigTx( + multisig, + toAddress(signerId), + tx, + options, + ) + + return signer.signTx( + (await wrappedTx.getEncodedData()).asBytes(), + signedExtensions, + metadata, + atBlockNumber, + hasher, + ) + }, + } + }, + } +} + +const u8ArrEq = (a: Uint8Array, b: Uint8Array) => { + if (a.length !== b.length) return false + return a.every((v, i) => v === b[i]) +} + +const getPublicKey = (addr: SS58String | HexString) => { + if (addr.startsWith("0x")) { + return fromHex(addr) + } + const info = getSs58AddressInfo(addr) + if (!info.isValid) { + throw new Error(`Invalid SS58 address ${addr}`) + } + return info.publicKey +} diff --git a/packages/sdk-multisig/src/sdk-types.ts b/packages/sdk-multisig/src/sdk-types.ts new file mode 100644 index 0000000..34c22db --- /dev/null +++ b/packages/sdk-multisig/src/sdk-types.ts @@ -0,0 +1,42 @@ +import { AsyncTransaction } from "@polkadot-api/common-sdk-utils" +import { + Binary, + HexString, + PolkadotClient, + PolkadotSigner, + SS58String, + Transaction, +} from "polkadot-api" + +export interface MultisigAccount { + signatories: Addr[] + threshold: number +} + +export interface MultisigTxOptions { + method: ( + approvals: Array, + threshold: number, + ) => "as_multi" | "approve_as_multi" +} + +export interface CreateMultisigSdk { + ( + client: PolkadotClient, + addrType?: AType, + ): MultisigSdk +} + +export interface MultisigSdk { + getMultisigTx( + multisig: MultisigAccount, + signatory: Addr, + txOrCallData: Transaction | Binary, + options?: MultisigTxOptions, + ): AsyncTransaction + getMultisigSigner( + multisig: MultisigAccount, + signer: PolkadotSigner, + options?: MultisigTxOptions, + ): PolkadotSigner +} diff --git a/packages/sdk-multisig/tsconfig.json b/packages/sdk-multisig/tsconfig.json new file mode 100644 index 0000000..73f9f03 --- /dev/null +++ b/packages/sdk-multisig/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base", + "include": ["src", "tests"], + "compilerOptions": { + "baseUrl": "src", + "resolveJsonModule": true, + "skipLibCheck": true, + "paths": { + "@/*": ["*"] + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2412e32..4f8b7ad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -183,6 +183,22 @@ importers: specifier: ^7.8.2 version: 7.8.2 + packages/sdk-multisig: + dependencies: + '@polkadot-api/common-sdk-utils': + specifier: workspace:* + version: link:../common-utils + '@polkadot-api/substrate-bindings': + specifier: ^0.16.3 + version: 0.16.3 + devDependencies: + polkadot-api: + specifier: ^1.14.1 + version: 1.18.0(postcss@8.5.6)(rxjs@7.8.2) + rxjs: + specifier: ^7.8.2 + version: 7.8.2 + packages/sdk-remote-proxy: dependencies: '@polkadot-api/meta-signers':