Files
pwap/shared/lib/multisig.ts
T
pezkuwichain 1295c36241 Rebrand: polkadot → pezkuwi build fixes
- Fixed TypeScript type assertion issues
- Updated imports from api-augment/substrate to api-augment/bizinikiwi
- Fixed imgConvert.mjs header and imports
- Added @ts-expect-error for runtime-converted types
- Fixed all @polkadot copyright headers to @pezkuwi
2026-01-07 02:32:54 +03:00

326 lines
9.2 KiB
TypeScript

// ========================================
// Multisig Utilities for USDT Treasury
// ========================================
// Full on-chain multisig using Substrate pallet-multisig
import type { ApiPromise } from '@pezkuwi/api';
import type { SubmittableExtrinsic } from '@pezkuwi/api/types';
import { Tiki } from './tiki';
import { encodeAddress, sortAddresses } from '@pezkuwi/util-crypto';
// ========================================
// MULTISIG CONFIGURATION
// ========================================
export interface MultisigMember {
role: string;
tiki: Tiki;
isUnique: boolean;
address?: string; // For non-unique roles, hardcoded address
}
export const USDT_MULTISIG_CONFIG = {
threshold: 3,
members: [
{ role: 'Founder/President', tiki: Tiki.Serok, isUnique: true },
{ role: 'Parliament Speaker', tiki: Tiki.SerokiMeclise, isUnique: true },
{ role: 'Treasurer', tiki: Tiki.Xezinedar, isUnique: true },
{ role: 'Notary', tiki: Tiki.Noter, isUnique: false, address: '' }, // Will be set at runtime
{ role: 'Spokesperson', tiki: Tiki.Berdevk, isUnique: false, address: '' },
] as MultisigMember[],
};
// ========================================
// MULTISIG MEMBER QUERIES
// ========================================
/**
* Get all multisig members from on-chain tiki holders
* @param api - Polkadot API instance
* @param specificAddresses - Addresses for non-unique roles {tiki: address}
* @returns Sorted array of member addresses
*/
export async function getMultisigMembers(
api: ApiPromise,
specificAddresses: Record<string, string> = {}
): Promise<string[]> {
const members: string[] = [];
for (const memberConfig of USDT_MULTISIG_CONFIG.members) {
if (memberConfig.isUnique) {
// Query from chain for unique roles
try {
const holder = await api.query.tiki.tikiHolder(memberConfig.tiki);
if (holder.isSome) {
const address = holder.unwrap().toString();
members.push(address);
} else {
console.warn(`No holder found for unique role: ${memberConfig.tiki}`);
}
} catch (error) {
console.error(`Error querying ${memberConfig.tiki}:`, error);
}
} else {
// Use hardcoded address for non-unique roles
const address = specificAddresses[memberConfig.tiki] || memberConfig.address;
if (address) {
members.push(address);
} else {
console.warn(`No address specified for non-unique role: ${memberConfig.tiki}`);
}
}
}
// Multisig requires sorted addresses
return sortAddresses(members);
}
/**
* Calculate deterministic multisig account address
* @param members - Sorted array of member addresses
* @param threshold - Signature threshold (default: 3)
* @param ss58Format - SS58 format for address encoding (default: 42)
* @returns Multisig account address
*/
export function calculateMultisigAddress(
members: string[],
threshold: number = USDT_MULTISIG_CONFIG.threshold,
ss58Format: number = 42
): string {
// Sort members (multisig requires sorted order)
const sortedMembers = sortAddresses(members);
// Create multisig address
// Formula: blake2(b"modlpy/utilisuba" + concat(sorted_members) + threshold)
const multisigId = encodeAddress(
new Uint8Array([
...Buffer.from('modlpy/utilisuba'),
...sortedMembers.flatMap((addr) => Array.from(Buffer.from(addr, 'hex'))),
threshold,
]),
ss58Format
);
return multisigId;
}
/**
* Check if an address is a multisig member
* @param api - Polkadot API instance
* @param address - Address to check
* @param specificAddresses - Addresses for non-unique roles
* @returns boolean
*/
export async function isMultisigMember(
api: ApiPromise,
address: string,
specificAddresses: Record<string, string> = {}
): Promise<boolean> {
const members = await getMultisigMembers(api, specificAddresses);
return members.includes(address);
}
/**
* Get multisig member info for display
* @param api - Polkadot API instance
* @param specificAddresses - Addresses for non-unique roles
* @returns Array of member info objects
*/
export async function getMultisigMemberInfo(
api: ApiPromise,
specificAddresses: Record<string, string> = {}
): Promise<Array<{ role: string; tiki: Tiki; address: string; isUnique: boolean }>> {
const memberInfo = [];
for (const memberConfig of USDT_MULTISIG_CONFIG.members) {
let address = '';
if (memberConfig.isUnique) {
try {
const holder = await api.query.tiki.tikiHolder(memberConfig.tiki);
if (holder.isSome) {
address = holder.unwrap().toString();
}
} catch (error) {
console.error(`Error querying ${memberConfig.tiki}:`, error);
}
} else {
address = specificAddresses[memberConfig.tiki] || memberConfig.address || '';
}
if (address) {
memberInfo.push({
role: memberConfig.role,
tiki: memberConfig.tiki,
address,
isUnique: memberConfig.isUnique,
});
}
}
return memberInfo;
}
// ========================================
// MULTISIG TRANSACTION HELPERS
// ========================================
export interface MultisigTimepoint {
height: number;
index: number;
}
/**
* Create a new multisig transaction (first signature)
* @param api - Polkadot API instance
* @param call - The extrinsic to execute via multisig
* @param otherSignatories - Other multisig members (excluding current signer)
* @param threshold - Signature threshold
* @returns Multisig transaction
*/
export function createMultisigTx(
api: ApiPromise,
call: SubmittableExtrinsic<'promise'>,
otherSignatories: string[],
threshold: number = USDT_MULTISIG_CONFIG.threshold
) {
const maxWeight = {
refTime: 1000000000,
proofSize: 64 * 1024,
};
return api.tx.multisig.asMulti(
threshold,
sortAddresses(otherSignatories),
null, // No timepoint for first call
call,
maxWeight
);
}
/**
* Approve an existing multisig transaction
* @param api - Polkadot API instance
* @param call - The original extrinsic
* @param otherSignatories - Other multisig members
* @param timepoint - Block height and index of the first approval
* @param threshold - Signature threshold
* @returns Approval transaction
*/
export function approveMultisigTx(
api: ApiPromise,
call: SubmittableExtrinsic<'promise'>,
otherSignatories: string[],
timepoint: MultisigTimepoint,
threshold: number = USDT_MULTISIG_CONFIG.threshold
) {
const maxWeight = {
refTime: 1000000000,
proofSize: 64 * 1024,
};
return api.tx.multisig.asMulti(
threshold,
sortAddresses(otherSignatories),
timepoint,
call,
maxWeight
);
}
/**
* Cancel a multisig transaction
* @param api - Polkadot API instance
* @param callHash - Hash of the call to cancel
* @param otherSignatories - Other multisig members
* @param timepoint - Block height and index of the call
* @param threshold - Signature threshold
* @returns Cancel transaction
*/
export function cancelMultisigTx(
api: ApiPromise,
callHash: string,
otherSignatories: string[],
timepoint: MultisigTimepoint,
threshold: number = USDT_MULTISIG_CONFIG.threshold
) {
return api.tx.multisig.cancelAsMulti(
threshold,
sortAddresses(otherSignatories),
timepoint,
callHash
);
}
// ========================================
// MULTISIG STORAGE QUERIES
// ========================================
/**
* Get pending multisig calls
* @param api - Polkadot API instance
* @param multisigAddress - The multisig account address
* @returns Array of pending calls
*/
export async function getPendingMultisigCalls(
api: ApiPromise,
multisigAddress: string
): Promise<any[]> {
try {
const multisigs = await api.query.multisig.multisigs.entries(multisigAddress);
return multisigs.map(([key, value]) => {
const callHash = key.args[1].toHex();
const multisigData = value.toJSON() as any;
return {
callHash,
when: multisigData.when,
deposit: multisigData.deposit,
depositor: multisigData.depositor,
approvals: multisigData.approvals,
};
});
} catch (error) {
console.error('Error fetching pending multisig calls:', error);
return [];
}
}
// ========================================
// DISPLAY HELPERS
// ========================================
/**
* Format multisig address for display
* @param address - Full multisig address
* @returns Shortened address
*/
export function formatMultisigAddress(address: string): string {
if (!address) return '';
return `${address.slice(0, 8)}...${address.slice(-8)}`;
}
/**
* Get approval status text
* @param approvals - Number of approvals
* @param threshold - Required threshold
* @returns Status text
*/
export function getApprovalStatus(approvals: number, threshold: number): string {
if (approvals >= threshold) return 'Ready to Execute';
return `${approvals}/${threshold} Approvals`;
}
/**
* Get approval status color
* @param approvals - Number of approvals
* @param threshold - Required threshold
* @returns Tailwind color class
*/
export function getApprovalStatusColor(approvals: number, threshold: number): string {
if (approvals >= threshold) return 'text-green-500';
if (approvals >= threshold - 1) return 'text-yellow-500';
return 'text-gray-500';
}