mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 21:47:56 +00:00
7b95b8a409
This commit reorganizes the codebase to eliminate duplication between web and mobile frontends by moving all commonly used files to the shared folder. Changes: - Moved lib files to shared/lib/: * wallet.ts, staking.ts, tiki.ts, identity.ts * multisig.ts, usdt.ts, scores.ts, citizenship-workflow.ts - Moved utils to shared/utils/: * auth.ts, dex.ts * Created format.ts (extracted formatNumber from web utils) - Created shared/theme/: * colors.ts (Kurdistan and App color definitions) - Updated web configuration: * Added @pezkuwi/* path aliases in tsconfig.json and vite.config.ts * Updated all imports to use @pezkuwi/lib/*, @pezkuwi/utils/*, @pezkuwi/theme/* * Removed duplicate files from web/src/lib and web/src/utils - Updated mobile configuration: * Added @pezkuwi/* path aliases in tsconfig.json * Updated theme/colors.ts to re-export from shared * Mobile already uses relative imports to shared (no changes needed) Architecture Benefits: - Single source of truth for common code - No duplication between frontends - Easier maintenance and consistency - Clear separation of shared vs platform-specific code Web-specific files kept: - web/src/lib/supabase.ts - web/src/lib/utils.ts (cn function for Tailwind, re-exports formatNumber from shared) All imports updated and tested. Both web and mobile now use the centralized shared folder.
326 lines
9.2 KiB
TypeScript
326 lines
9.2 KiB
TypeScript
// ========================================
|
|
// Multisig Utilities for USDT Treasury
|
|
// ========================================
|
|
// Full on-chain multisig using Substrate pallet-multisig
|
|
|
|
import type { ApiPromise } from '@polkadot/api';
|
|
import type { SubmittableExtrinsic } from '@polkadot/api/types';
|
|
import { Tiki } from './tiki';
|
|
import { encodeAddress, sortAddresses } from '@polkadot/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';
|
|
}
|