mirror of
https://github.com/pezkuwichain/pezkuwi-subquery.git
synced 2026-07-22 22:45:41 +00:00
Call finalize_staking_details() - nothing ever promoted matured pending entries
The staking-score pallet's audit-fix (commit 9afd99ae) added a dispute-window model: receive_staking_details() only writes to PendingStakingDetails, and a separate, permissionless finalize_staking_details() call promotes it to CachedStakingDetails once DisputeWindow blocks have passed. No caller of that promotion step existed anywhere in this bot (or anywhere else in this repo) - confirmed live: Serok's and QaziM's real stake was correctly submitted to Pending after the earlier NotRegisteredNoter/nonce fixes, but sat there indefinitely with no path to ever reaching CachedStakingDetails/TrustScores. Adds finalizeMaturedPending(), called once per periodic scan cycle (and once at startup): scans PendingStakingDetails.entries(), finalizes whichever have matured, batched into one utility.batchAll like the existing submission path, routed through the same submitQueue so it can never race with a concurrent submitStakingDetails call.
This commit is contained in:
+70
-28
@@ -245,33 +245,19 @@ function hasDataChanged(fresh, cached) {
|
|||||||
let submitQueue = Promise.resolve();
|
let submitQueue = Promise.resolve();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submit receive_staking_details for one or more (address, source) pairs.
|
* Routes a signAndSend through the shared serialization queue, with the standard
|
||||||
* Batches multiple calls into a single utility.batchAll transaction.
|
* dispatch-error decoding/logging both callers below need.
|
||||||
*/
|
*/
|
||||||
function submitStakingDetails(peopleApi, noterKeypair, updates) {
|
function queueSubmit(peopleApi, noterKeypair, tx, description) {
|
||||||
if (updates.length === 0) return Promise.resolve();
|
const task = submitQueue.then(() => doSignAndSend(peopleApi, noterKeypair, tx, description));
|
||||||
|
|
||||||
const task = submitQueue.then(() => doSubmitStakingDetails(peopleApi, noterKeypair, updates));
|
|
||||||
// Never let one caller's failure poison the queue for the next caller's submission.
|
// Never let one caller's failure poison the queue for the next caller's submission.
|
||||||
submitQueue = task.catch(() => {});
|
submitQueue = task.catch(() => {});
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
|
||||||
function doSubmitStakingDetails(peopleApi, noterKeypair, updates) {
|
function doSignAndSend(peopleApi, noterKeypair, tx, description) {
|
||||||
const calls = updates.map(({ address, source, data }) =>
|
|
||||||
peopleApi.tx.stakingScore.receiveStakingDetails(
|
|
||||||
address,
|
|
||||||
source,
|
|
||||||
data.stakedAmount.toString(),
|
|
||||||
data.nominationsCount,
|
|
||||||
data.unlockingChunksCount,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const tx = calls.length === 1 ? calls[0] : peopleApi.tx.utility.batchAll(calls);
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
tx.signAndSend(noterKeypair, ({ status, dispatchError, events }) => {
|
tx.signAndSend(noterKeypair, ({ status, dispatchError }) => {
|
||||||
if (status.isInBlock) {
|
if (status.isInBlock) {
|
||||||
if (dispatchError) {
|
if (dispatchError) {
|
||||||
let errMsg = dispatchError.toString();
|
let errMsg = dispatchError.toString();
|
||||||
@@ -281,13 +267,10 @@ function doSubmitStakingDetails(peopleApi, noterKeypair, updates) {
|
|||||||
errMsg = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`;
|
errMsg = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`;
|
||||||
} catch { /* use default */ }
|
} catch { /* use default */ }
|
||||||
}
|
}
|
||||||
log('ERROR', 'TX failed', { error: errMsg, block: status.asInBlock.toHex() });
|
log('ERROR', 'TX failed', { description, error: errMsg, block: status.asInBlock.toHex() });
|
||||||
reject(new Error(errMsg));
|
reject(new Error(errMsg));
|
||||||
} else {
|
} else {
|
||||||
const addresses = updates.map(u => u.address.slice(0, 8) + '...').join(', ');
|
log('INFO', `TX success: ${description}`, { block: status.asInBlock.toHex() });
|
||||||
log('INFO', `TX success: ${updates.length} update(s) for [${addresses}]`, {
|
|
||||||
block: status.asInBlock.toHex()
|
|
||||||
});
|
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -295,6 +278,62 @@ function doSubmitStakingDetails(peopleApi, noterKeypair, updates) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit receive_staking_details for one or more (address, source) pairs.
|
||||||
|
* Batches multiple calls into a single utility.batchAll transaction.
|
||||||
|
*/
|
||||||
|
function submitStakingDetails(peopleApi, noterKeypair, updates) {
|
||||||
|
if (updates.length === 0) return Promise.resolve();
|
||||||
|
|
||||||
|
const calls = updates.map(({ address, source, data }) =>
|
||||||
|
peopleApi.tx.stakingScore.receiveStakingDetails(
|
||||||
|
address,
|
||||||
|
source,
|
||||||
|
data.stakedAmount.toString(),
|
||||||
|
data.nominationsCount,
|
||||||
|
data.unlockingChunksCount,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const tx = calls.length === 1 ? calls[0] : peopleApi.tx.utility.batchAll(calls);
|
||||||
|
const addresses = updates.map(u => u.address.slice(0, 8) + '...').join(', ');
|
||||||
|
|
||||||
|
return queueSubmit(peopleApi, noterKeypair, tx, `${updates.length} update(s) for [${addresses}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* finalize_staking_details() is the permissionless step that promotes a matured
|
||||||
|
* PendingStakingDetails entry into CachedStakingDetails (see the pallet's
|
||||||
|
* DisputeWindow doc comment) - nothing in this codebase called it before this fix,
|
||||||
|
* so entries sat in Pending forever even after their dispute window elapsed.
|
||||||
|
* Scans all pending entries once per periodic cycle and finalizes whichever have matured.
|
||||||
|
*/
|
||||||
|
async function finalizeMaturedPending(peopleApi, noterKeypair) {
|
||||||
|
const disputeWindow = peopleApi.consts.stakingScore.disputeWindow.toNumber();
|
||||||
|
const currentBlock = (await peopleApi.rpc.chain.getHeader()).number.toNumber();
|
||||||
|
|
||||||
|
const entries = await peopleApi.query.stakingScore.pendingStakingDetails.entries();
|
||||||
|
const matured = entries.filter(([, value]) => {
|
||||||
|
const submittedAt = value.unwrap().submittedAt.toNumber();
|
||||||
|
return currentBlock >= submittedAt + disputeWindow;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matured.length === 0) return;
|
||||||
|
|
||||||
|
log('INFO', `Finalizing ${matured.length} matured pending submission(s)`);
|
||||||
|
|
||||||
|
const calls = matured.map(([key]) => {
|
||||||
|
const [who, source] = key.args;
|
||||||
|
return peopleApi.tx.stakingScore.finalizeStakingDetails(who, source);
|
||||||
|
});
|
||||||
|
const tx = calls.length === 1 ? calls[0] : peopleApi.tx.utility.batchAll(calls);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await queueSubmit(peopleApi, noterKeypair, tx, `finalize ${matured.length} matured entrie(s)`);
|
||||||
|
} catch (err) {
|
||||||
|
log('ERROR', 'Finalize batch failed', { error: err.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// PROCESS SINGLE ACCOUNT
|
// PROCESS SINGLE ACCOUNT
|
||||||
// ========================================
|
// ========================================
|
||||||
@@ -487,6 +526,7 @@ async function main() {
|
|||||||
|
|
||||||
// Run initial full scan
|
// Run initial full scan
|
||||||
await fullScan(relayApi, assetHubApi, peopleApi, noterKeypair);
|
await fullScan(relayApi, assetHubApi, peopleApi, noterKeypair);
|
||||||
|
await finalizeMaturedPending(peopleApi, noterKeypair);
|
||||||
|
|
||||||
// Start event listener for real-time processing
|
// Start event listener for real-time processing
|
||||||
await startEventListener(relayApi, assetHubApi, peopleApi, noterKeypair);
|
await startEventListener(relayApi, assetHubApi, peopleApi, noterKeypair);
|
||||||
@@ -494,9 +534,11 @@ async function main() {
|
|||||||
// Schedule periodic full scans
|
// Schedule periodic full scans
|
||||||
log('INFO', `Periodic scan scheduled every ${SCAN_INTERVAL / 1000}s`);
|
log('INFO', `Periodic scan scheduled every ${SCAN_INTERVAL / 1000}s`);
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
fullScan(relayApi, assetHubApi, peopleApi, noterKeypair).catch(err => {
|
fullScan(relayApi, assetHubApi, peopleApi, noterKeypair)
|
||||||
log('ERROR', 'Periodic scan failed', { error: err.message });
|
.then(() => finalizeMaturedPending(peopleApi, noterKeypair))
|
||||||
});
|
.catch(err => {
|
||||||
|
log('ERROR', 'Periodic scan failed', { error: err.message });
|
||||||
|
});
|
||||||
}, SCAN_INTERVAL);
|
}, SCAN_INTERVAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user