Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/common-utils/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

## Added

- Property `waited` in `AsyncTransaction` to get a `Promise<Transaction>`

## 0.0.1-1 2025-02-17

Initial release
8 changes: 8 additions & 0 deletions packages/common-utils/src/asyncTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type AsyncTransaction<
> & {
decodedCall: Promise<Transaction<Arg, Pallet, Name, Asset>["decodedCall"]>
getEncodedData: () => Promise<Binary>
waited: Promise<Transaction<Arg, Pallet, Name, Asset>>
}

export const wrapAsyncTx = <
Expand All @@ -23,6 +24,12 @@ export const wrapAsyncTx = <
fn: () => Promise<Transaction<Arg, Pallet, Name, Asset>>,
): AsyncTransaction<Arg, Pallet, Name, Asset> => {
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) =>
Expand All @@ -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,
}
}
3 changes: 3 additions & 0 deletions packages/sdk-multisig/.papi/descriptors/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*
!.gitignore
!package.json
24 changes: 24 additions & 0 deletions packages/sdk-multisig/.papi/descriptors/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
Binary file added packages/sdk-multisig/.papi/metadata/dot.scale
Binary file not shown.
Binary file not shown.
21 changes: 21 additions & 0 deletions packages/sdk-multisig/.papi/polkadot-api.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
9 changes: 9 additions & 0 deletions packages/sdk-multisig/.papi/whitelist.ts
Original file line number Diff line number Diff line change
@@ -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",
]
5 changes: 5 additions & 0 deletions packages/sdk-multisig/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Changelog

## Unreleased

Initial release
1 change: 1 addition & 0 deletions packages/sdk-multisig/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @polkadot-api/sdk-multisig
55 changes: 55 additions & 0 deletions packages/sdk-multisig/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
2 changes: 2 additions & 0 deletions packages/sdk-multisig/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { createMultisigSdk } from "./multisig-sdk"
export type { MultisigSdk } from "./sdk-types"
193 changes: 193 additions & 0 deletions packages/sdk-multisig/src/multisig-sdk.ts
Original file line number Diff line number Diff line change
@@ -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<unknown> = {
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<Addr>["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<any, any, any, any> =
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
}
Loading