mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-07-22 19:35:48 +00:00
dd58fe9164
- calculateMultisigAddress was completely broken (hex-decoded an SS58 string and never hashed the preimage) - fixed via @pezkuwi/util-crypto's real encodeMultiAddress/createKeyMulti, verified against the actual known multisig address on-chain. - ReservesDashboardPage had stale Noter/Berdevk addresses that don't match the real signers - centralized as BRIDGE_MULTISIG_SPECIFIC_ADDRESSES. - USDTBridge withdrawal called assets.burn directly as a single-signer extrinsic (always fails - only the multisig is Admin) while only checking status.isFinalized (a failed dispatch is still finalized, so it silently did nothing) - replaced with the correct transfer-to-custody flow the relayer actually watches for. - New MultisigOperationsPage (/multisig/pending) lists pending calls from real Multisig.Multisigs storage and lets any of the 5 signers approve/reject with their own wallet extension. - New standalone sign/ app (deployed separately at pezbridge-sign.pex.mom) - a dedicated, gated signing portal for the same operations, so signing isn't dependent on this app alone.
106 lines
4.3 KiB
TypeScript
106 lines
4.3 KiB
TypeScript
// ========================================
|
|
// Multisig Pending Operations (higher-level orchestration for the review/approve UI)
|
|
// ========================================
|
|
// Sits on top of multisig.ts's low-level pallet-multisig calls and usdt.ts's bridge constants.
|
|
// There is no code path connecting this web app to the usdt-bridge relayer's private local
|
|
// database (confirmed while researching this feature - see the usdt-bridge-multisig-migration
|
|
// memory), so pending calls are described purely from on-chain state: the one deterministic,
|
|
// always-reconstructible candidate (the automation key's periodic allowance renewal) is
|
|
// auto-matched by hash; anything else needs a signer to paste the known call data, exactly
|
|
// like the Android wallet's "Enter Call Data" fallback exists for the same reason - a call
|
|
// hash alone is cryptographically insufficient to recover the original call.
|
|
|
|
import type { ApiPromise } from '@pezkuwi/api';
|
|
import type { SubmittableExtrinsic } from '@pezkuwi/api/types';
|
|
import {
|
|
getPendingMultisigCalls,
|
|
USDT_MULTISIG_CONFIG,
|
|
type MultisigTimepoint,
|
|
} from './multisig';
|
|
import { WUSDT_ASSET_ID, WUSDT_AUTOMATION_KEY_ADDRESS, STANDARD_RENEWAL_TOPUP } from './usdt';
|
|
|
|
export interface PendingOperation {
|
|
callHash: string;
|
|
when: MultisigTimepoint;
|
|
deposit: string;
|
|
depositor: string;
|
|
approvals: string[];
|
|
approvalsCount: number;
|
|
threshold: number;
|
|
description: string;
|
|
resolvedCall: SubmittableExtrinsic<'promise'> | null;
|
|
isAutoMatched: boolean;
|
|
}
|
|
|
|
/**
|
|
* The one deterministic, always-reconstructible pending-call shape this UI can auto-describe:
|
|
* the automation key's periodic Assets.approve_transfer renewal (see PezbridgeBot /
|
|
* pezkuwi-DKS/tools/usdt-bridge/src/bin/pezbridge_bot.rs, which builds the exact same call).
|
|
*/
|
|
export function buildRenewalCandidateCall(api: ApiPromise): SubmittableExtrinsic<'promise'> {
|
|
return api.tx.assets.approveTransfer(
|
|
WUSDT_ASSET_ID,
|
|
WUSDT_AUTOMATION_KEY_ADDRESS,
|
|
STANDARD_RENEWAL_TOPUP.toString()
|
|
) as unknown as SubmittableExtrinsic<'promise'>;
|
|
}
|
|
|
|
/**
|
|
* Lists every pending Multisig.Multisigs entry for the bridge's custody account and attaches a
|
|
* human-readable description + the real call object wherever it can be determined.
|
|
*/
|
|
export async function listPendingOperations(
|
|
api: ApiPromise,
|
|
multisigAddress: string
|
|
): Promise<PendingOperation[]> {
|
|
const raw = await getPendingMultisigCalls(api, multisigAddress);
|
|
const renewalCandidate = buildRenewalCandidateCall(api);
|
|
const renewalHash = renewalCandidate.method.hash.toHex().toLowerCase();
|
|
|
|
return raw.map((entry) => {
|
|
const approvals = (entry.approvals ?? []) as string[];
|
|
const isRenewal = String(entry.callHash).toLowerCase() === renewalHash;
|
|
|
|
return {
|
|
callHash: entry.callHash,
|
|
when: entry.when,
|
|
deposit: String(entry.deposit),
|
|
depositor: entry.depositor,
|
|
approvals,
|
|
approvalsCount: approvals.length,
|
|
threshold: USDT_MULTISIG_CONFIG.threshold,
|
|
description: isRenewal
|
|
? `Automation key allowance renewal (${(Number(STANDARD_RENEWAL_TOPUP) / 1e6).toFixed(2)} wUSDT)`
|
|
: 'Unknown call - paste the call data below to decode it',
|
|
resolvedCall: isRenewal ? renewalCandidate : null,
|
|
isAutoMatched: isRenewal,
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Decodes raw hex call data a signer pasted in (the manual fallback for any pending call this
|
|
* UI couldn't auto-reconstruct) and verifies its hash actually matches the pending entry before
|
|
* returning it - pasted data is never trusted blindly.
|
|
*/
|
|
export function decodeAndVerifyCallData(
|
|
api: ApiPromise,
|
|
callDataHex: string,
|
|
expectedCallHash: string
|
|
): { call: SubmittableExtrinsic<'promise'>; description: string } {
|
|
const decoded = api.createType('Call', callDataHex);
|
|
const call = api.tx(decoded) as unknown as SubmittableExtrinsic<'promise'>;
|
|
const actualHash = call.method.hash.toHex().toLowerCase();
|
|
|
|
if (actualHash !== expectedCallHash.toLowerCase()) {
|
|
throw new Error(
|
|
`Call data does not match this operation: decoded hash ${actualHash}, expected ${expectedCallHash.toLowerCase()}`
|
|
);
|
|
}
|
|
|
|
const method = call.method as unknown as { section: string; method: string; args: unknown[] };
|
|
const argsText = method.args.map((a) => String(a)).join(', ');
|
|
|
|
return { call, description: `${method.section}.${method.method}(${argsText})` };
|
|
}
|