// ======================================== // 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 = {} ): Promise { 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) as any; 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 = {} ): Promise { 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 = {} ): Promise> { 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) as any; 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 { 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'; }