mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-21 22:38:00 +00:00
feat: staking score 3-state model and noter integration
This commit is contained in:
@@ -1,11 +1,17 @@
|
||||
/**
|
||||
* @file: staking-score.live.test.js
|
||||
* @description: Live integration tests for the StakingScore pallet.
|
||||
*
|
||||
* @description: Live integration tests for the StakingScore pallet (v1020007+).
|
||||
*
|
||||
* Tests the noter-based staking score system:
|
||||
* - start_score_tracking() — no stake requirement, user opt-in
|
||||
* - receive_staking_details() — noter/root submits cached staking data
|
||||
* - CachedStakingDetails storage — dual-source (RelayChain, AssetHub)
|
||||
* - Zero stake cleanup
|
||||
*
|
||||
* @preconditions:
|
||||
* 1. A local Pezkuwi dev node must be running and accessible at `ws://127.0.0.1:8082`.
|
||||
* 2. The node must have the `stakingScore` and `staking` pallets.
|
||||
* 3. Test accounts must be funded to be able to bond stake.
|
||||
* 1. A local Pezkuwi dev node must be running at WS_ENDPOINT.
|
||||
* 2. The node must have stakingScore pallet (spec >= 1020007).
|
||||
* 3. Sudo account (//Alice) must be available for root-origin calls.
|
||||
*/
|
||||
|
||||
import { ApiPromise, WsProvider, Keyring } from '@pezkuwi/api';
|
||||
@@ -16,41 +22,31 @@ import { jest } from '@jest/globals';
|
||||
// TEST CONFIGURATION
|
||||
// ========================================
|
||||
|
||||
const WS_ENDPOINT = 'ws://127.0.0.1:8082';
|
||||
jest.setTimeout(120000); // 2 minutes, as this involves waiting for blocks
|
||||
const WS_ENDPOINT = process.env.WS_ENDPOINT || 'ws://127.0.0.1:8082';
|
||||
jest.setTimeout(120000);
|
||||
|
||||
const UNITS = new BN('1000000000000'); // 10^12
|
||||
|
||||
// ========================================
|
||||
// TEST SETUP & TEARDOWN
|
||||
// HELPERS
|
||||
// ========================================
|
||||
|
||||
let api;
|
||||
let keyring;
|
||||
let user1;
|
||||
let sudo;
|
||||
let user1;
|
||||
|
||||
// Helper to wait for N finalized blocks
|
||||
const waitForBlocks = async (count) => {
|
||||
let blocksLeft = count;
|
||||
return new Promise(resolve => {
|
||||
const unsubscribe = api.rpc.chain.subscribeFinalizedHeads(() => {
|
||||
blocksLeft--;
|
||||
if (blocksLeft <= 0) {
|
||||
unsubscribe();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Helper to send a transaction and wait for it to be finalized
|
||||
const sendAndFinalize = (tx, signer) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
tx.signAndSend(signer, ({ status, dispatchError }) => {
|
||||
if (status.isFinalized) {
|
||||
if (dispatchError) {
|
||||
const decoded = api.registry.findMetaError(dispatchError.asModule);
|
||||
reject(new Error(`${decoded.section}.${decoded.name}`));
|
||||
if (dispatchError.isModule) {
|
||||
const decoded = api.registry.findMetaError(dispatchError.asModule);
|
||||
reject(new Error(`${decoded.section}.${decoded.name}`));
|
||||
} else {
|
||||
reject(new Error(dispatchError.toString()));
|
||||
}
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
@@ -59,98 +55,157 @@ const sendAndFinalize = (tx, signer) => {
|
||||
});
|
||||
};
|
||||
|
||||
const sendSudoAndFinalize = (call) => {
|
||||
const sudoTx = api.tx.sudo.sudo(call);
|
||||
return sendAndFinalize(sudoTx, sudo);
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const wsProvider = new WsProvider(WS_ENDPOINT);
|
||||
api = await ApiPromise.create({ provider: wsProvider });
|
||||
keyring = new Keyring({ type: 'sr5519' });
|
||||
|
||||
// Using a fresh account for each test run to avoid state conflicts
|
||||
user1 = keyring.addFromUri(`//StakingScoreUser${Date.now()}`)
|
||||
keyring = new Keyring({ type: 'sr25519' });
|
||||
|
||||
// You may need to fund this account using sudo if it has no balance
|
||||
// For example:
|
||||
// const sudo = keyring.addFromUri('//Alice');
|
||||
// const transferTx = api.tx.balances.transfer(user1.address, UNITS.mul(new BN(10000)));
|
||||
// await sendAndFinalize(transferTx, sudo);
|
||||
sudo = keyring.addFromUri('//Alice');
|
||||
user1 = keyring.addFromUri(`//StakingScoreUser${Date.now()}`);
|
||||
|
||||
console.log('Connected to node and initialized account for StakingScore tests.');
|
||||
}, 40000);
|
||||
// Fund test account
|
||||
const transferTx = api.tx.balances.transferKeepAlive(user1.address, UNITS.mul(new BN(100)));
|
||||
await sendAndFinalize(transferTx, sudo);
|
||||
|
||||
console.log('Setup complete. User1:', user1.address);
|
||||
}, 60000);
|
||||
|
||||
afterAll(async () => {
|
||||
if (api) await api.disconnect();
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// LIVE PALLET TESTS
|
||||
// TESTS
|
||||
// ========================================
|
||||
|
||||
describe('StakingScore Pallet Live Workflow', () => {
|
||||
describe('StakingScore Pallet (v1020007+)', () => {
|
||||
|
||||
it('should calculate the base score correctly based on staked amount only', async () => {
|
||||
console.log('Testing base score calculation...');
|
||||
it('start_score_tracking() succeeds without any stake', async () => {
|
||||
// User1 has no staking ledger — should still succeed (no NoStakeFound error)
|
||||
const tx = api.tx.stakingScore.startScoreTracking();
|
||||
await sendAndFinalize(tx, user1);
|
||||
|
||||
// Stake 500 PEZ (should result in a base score of 40)
|
||||
const stakeAmount = UNITS.mul(new BN(500));
|
||||
const bondTx = api.tx.staking.bond(stakeAmount, 'Staked'); // Bond to self
|
||||
await sendAndFinalize(bondTx, user1);
|
||||
|
||||
// Without starting tracking, score should be based on amount only
|
||||
const { score: scoreBeforeTracking } = await api.query.stakingScore.getStakingScore(user1.address);
|
||||
expect(scoreBeforeTracking.toNumber()).toBe(40);
|
||||
console.log(`Verified base score for ${stakeAmount} stake is ${scoreBeforeTracking.toNumber()}.`);
|
||||
|
||||
// Even after waiting, score should not change
|
||||
await waitForBlocks(5);
|
||||
const { score: scoreAfterWaiting } = await api.query.stakingScore.getStakingScore(user1.address);
|
||||
expect(scoreAfterWaiting.toNumber()).toBe(40);
|
||||
console.log('Verified score does not change without tracking enabled.');
|
||||
// Verify StakingStartBlock is set
|
||||
const startBlock = await api.query.stakingScore.stakingStartBlock(user1.address);
|
||||
expect(startBlock.isSome).toBe(true);
|
||||
console.log('start_score_tracking succeeded, startBlock:', startBlock.unwrap().toNumber());
|
||||
});
|
||||
|
||||
it('should apply duration multiplier after tracking is started', async () => {
|
||||
console.log('Testing duration multiplier...');
|
||||
const MONTH_IN_BLOCKS = api.consts.stakingScore.monthInBlocks.toNumber();
|
||||
|
||||
// User1 already has 500 PEZ staked from the previous test.
|
||||
// Now, let's start tracking.
|
||||
const startTrackingTx = api.tx.stakingScore.startScoreTracking();
|
||||
await sendAndFinalize(startTrackingTx, user1);
|
||||
console.log('Score tracking started for User1.');
|
||||
|
||||
// Wait for 4 months
|
||||
console.log(`Waiting for 4 months (${4 * MONTH_IN_BLOCKS} blocks)...`);
|
||||
await waitForBlocks(4 * MONTH_IN_BLOCKS);
|
||||
|
||||
// Score should now be 40 (base) * 1.5 (4 month multiplier) = 60
|
||||
const { score: scoreAfter4Months } = await api.query.stakingScore.getStakingScore(user1.address);
|
||||
expect(scoreAfter4Months.toNumber()).toBe(60);
|
||||
console.log(`Verified score after 4 months is ${scoreAfter4Months.toNumber()}.`);
|
||||
|
||||
// Wait for another 9 months (total 13 months) to reach max multiplier
|
||||
console.log(`Waiting for another 9 months (${9 * MONTH_IN_BLOCKS} blocks)...`);
|
||||
await waitForBlocks(9 * MONTH_IN_BLOCKS);
|
||||
|
||||
// Score should be 40 (base) * 2.0 (12+ month multiplier) = 80
|
||||
const { score: scoreAfter13Months } = await api.query.stakingScore.getStakingScore(user1.address);
|
||||
expect(scoreAfter13Months.toNumber()).toBe(80);
|
||||
console.log(`Verified score after 13 months is ${scoreAfter13Months.toNumber()}.`);
|
||||
it('start_score_tracking() fails with TrackingAlreadyStarted', async () => {
|
||||
const tx = api.tx.stakingScore.startScoreTracking();
|
||||
await expect(sendAndFinalize(tx, user1))
|
||||
.rejects.toThrow('stakingScore.TrackingAlreadyStarted');
|
||||
});
|
||||
|
||||
it('should fail to start tracking if no stake is found or already tracking', async () => {
|
||||
const freshUser = keyring.addFromUri(`//FreshUser${Date.now()}`);
|
||||
// You would need to fund this freshUser account for it to pay transaction fees.
|
||||
it('receive_staking_details() fails without noter authority', async () => {
|
||||
// User1 is not a noter — should fail with NotAuthorized
|
||||
const tx = api.tx.stakingScore.receiveStakingDetails(
|
||||
user1.address,
|
||||
'RelayChain',
|
||||
UNITS.mul(new BN(500)).toString(), // 500 HEZ
|
||||
3, // nominations_count
|
||||
0, // unlocking_chunks_count
|
||||
);
|
||||
await expect(sendAndFinalize(tx, user1))
|
||||
.rejects.toThrow('stakingScore.NotAuthorized');
|
||||
});
|
||||
|
||||
console.log('Testing failure cases for start_score_tracking...');
|
||||
it('receive_staking_details() succeeds via sudo (root origin)', async () => {
|
||||
const call = api.tx.stakingScore.receiveStakingDetails(
|
||||
user1.address,
|
||||
'RelayChain',
|
||||
UNITS.mul(new BN(500)).toString(), // 500 HEZ
|
||||
3, // nominations_count
|
||||
1, // unlocking_chunks_count
|
||||
);
|
||||
await sendSudoAndFinalize(call);
|
||||
|
||||
// Case 1: No stake found
|
||||
await expect(
|
||||
sendAndFinalize(api.tx.stakingScore.startScoreTracking(), freshUser)
|
||||
).rejects.toThrow('stakingScore.NoStakeFound');
|
||||
console.log('Verified: Cannot start tracking without a stake.');
|
||||
// Verify CachedStakingDetails is set
|
||||
const cached = await api.query.stakingScore.cachedStakingDetails(
|
||||
user1.address, 'RelayChain'
|
||||
);
|
||||
expect(cached.isSome).toBe(true);
|
||||
|
||||
// Case 2: Already tracking (using user1 from previous tests)
|
||||
await expect(
|
||||
sendAndFinalize(api.tx.stakingScore.startScoreTracking(), user1)
|
||||
).rejects.toThrow('stakingScore.TrackingAlreadyStarted');
|
||||
console.log('Verified: Cannot start tracking when already started.');
|
||||
const details = cached.unwrap().toJSON();
|
||||
const stakedAmount = BigInt(details.stakedAmount ?? details.staked_amount ?? '0');
|
||||
expect(stakedAmount).toBe(UNITS.mul(new BN(500)).toBigInt());
|
||||
expect(details.nominationsCount ?? details.nominations_count).toBe(3);
|
||||
expect(details.unlockingChunksCount ?? details.unlocking_chunks_count).toBe(1);
|
||||
|
||||
console.log('CachedStakingDetails verified:', details);
|
||||
});
|
||||
|
||||
it('receive_staking_details() supports dual-source (AssetHub)', async () => {
|
||||
const call = api.tx.stakingScore.receiveStakingDetails(
|
||||
user1.address,
|
||||
'AssetHub',
|
||||
UNITS.mul(new BN(200)).toString(), // 200 HEZ from Asset Hub pool
|
||||
0,
|
||||
0,
|
||||
);
|
||||
await sendSudoAndFinalize(call);
|
||||
|
||||
// Both sources should exist
|
||||
const relay = await api.query.stakingScore.cachedStakingDetails(user1.address, 'RelayChain');
|
||||
const assetHub = await api.query.stakingScore.cachedStakingDetails(user1.address, 'AssetHub');
|
||||
expect(relay.isSome).toBe(true);
|
||||
expect(assetHub.isSome).toBe(true);
|
||||
|
||||
console.log('Dual-source staking data verified');
|
||||
});
|
||||
|
||||
it('zero stake cleans up CachedStakingDetails for a source', async () => {
|
||||
// Send zero stake for AssetHub — should remove that entry
|
||||
const call = api.tx.stakingScore.receiveStakingDetails(
|
||||
user1.address,
|
||||
'AssetHub',
|
||||
'0', // zero stake
|
||||
0,
|
||||
0,
|
||||
);
|
||||
await sendSudoAndFinalize(call);
|
||||
|
||||
// AssetHub entry should be gone
|
||||
const assetHub = await api.query.stakingScore.cachedStakingDetails(user1.address, 'AssetHub');
|
||||
expect(assetHub.isNone || assetHub.isEmpty).toBe(true);
|
||||
|
||||
// RelayChain entry should still exist
|
||||
const relay = await api.query.stakingScore.cachedStakingDetails(user1.address, 'RelayChain');
|
||||
expect(relay.isSome).toBe(true);
|
||||
|
||||
// StakingStartBlock should still exist (still has relay stake)
|
||||
const startBlock = await api.query.stakingScore.stakingStartBlock(user1.address);
|
||||
expect(startBlock.isSome).toBe(true);
|
||||
|
||||
console.log('Zero stake cleanup verified (AssetHub removed, RelayChain kept)');
|
||||
});
|
||||
|
||||
it('zero stake on all sources removes StakingStartBlock', async () => {
|
||||
// Remove relay chain stake too
|
||||
const call = api.tx.stakingScore.receiveStakingDetails(
|
||||
user1.address,
|
||||
'RelayChain',
|
||||
'0',
|
||||
0,
|
||||
0,
|
||||
);
|
||||
await sendSudoAndFinalize(call);
|
||||
|
||||
// Both sources should be empty
|
||||
const relay = await api.query.stakingScore.cachedStakingDetails(user1.address, 'RelayChain');
|
||||
const assetHub = await api.query.stakingScore.cachedStakingDetails(user1.address, 'AssetHub');
|
||||
expect(relay.isNone || relay.isEmpty).toBe(true);
|
||||
expect(assetHub.isNone || assetHub.isEmpty).toBe(true);
|
||||
|
||||
// StakingStartBlock should also be cleaned up
|
||||
const startBlock = await api.query.stakingScore.stakingStartBlock(user1.address);
|
||||
expect(startBlock.isNone || startBlock.isEmpty).toBe(true);
|
||||
|
||||
console.log('Full cleanup verified (all sources + StakingStartBlock removed)');
|
||||
});
|
||||
});
|
||||
|
||||
+24
-5
@@ -25,6 +25,7 @@ export interface UserScores {
|
||||
|
||||
export interface StakingScoreStatus {
|
||||
isTracking: boolean;
|
||||
hasCachedData: boolean; // Whether noter has submitted staking data
|
||||
startBlock: number | null;
|
||||
currentBlock: number;
|
||||
durationBlocks: number;
|
||||
@@ -188,28 +189,46 @@ export async function getStakingScoreStatus(
|
||||
): Promise<StakingScoreStatus> {
|
||||
try {
|
||||
if (!peopleApi?.query?.stakingScore?.stakingStartBlock) {
|
||||
return { isTracking: false, startBlock: null, currentBlock: 0, durationBlocks: 0 };
|
||||
return { isTracking: false, hasCachedData: false, startBlock: null, currentBlock: 0, durationBlocks: 0 };
|
||||
}
|
||||
|
||||
const startBlockResult = await peopleApi.query.stakingScore.stakingStartBlock(address);
|
||||
const currentBlock = Number((await peopleApi.query.system.number()).toString());
|
||||
|
||||
if (startBlockResult.isEmpty || startBlockResult.isNone) {
|
||||
return { isTracking: false, startBlock: null, currentBlock, durationBlocks: 0 };
|
||||
return { isTracking: false, hasCachedData: false, startBlock: null, currentBlock, durationBlocks: 0 };
|
||||
}
|
||||
|
||||
const startBlock = Number(startBlockResult.toString());
|
||||
const durationBlocks = currentBlock - startBlock;
|
||||
|
||||
// Check if noter has submitted cached staking data
|
||||
let hasCachedData = false;
|
||||
if (peopleApi.query.stakingScore.cachedStakingDetails) {
|
||||
try {
|
||||
const [relayResult, assetHubResult] = await Promise.all([
|
||||
peopleApi.query.stakingScore.cachedStakingDetails(address, 'RelayChain')
|
||||
.catch(() => ({ isSome: false, isEmpty: true })),
|
||||
peopleApi.query.stakingScore.cachedStakingDetails(address, 'AssetHub')
|
||||
.catch(() => ({ isSome: false, isEmpty: true })),
|
||||
]);
|
||||
hasCachedData = (relayResult.isSome || !relayResult.isEmpty) ||
|
||||
(assetHubResult.isSome || !assetHubResult.isEmpty);
|
||||
} catch {
|
||||
hasCachedData = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isTracking: true,
|
||||
hasCachedData,
|
||||
startBlock,
|
||||
currentBlock,
|
||||
durationBlocks
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching staking score status:', error);
|
||||
return { isTracking: false, startBlock: null, currentBlock: 0, durationBlocks: 0 };
|
||||
return { isTracking: false, hasCachedData: false, startBlock: null, currentBlock: 0, durationBlocks: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,8 +236,8 @@ export async function getStakingScoreStatus(
|
||||
* Start staking score tracking
|
||||
* Calls: stakingScore.startScoreTracking()
|
||||
*
|
||||
* Called on People Chain. Requires staking data to be available via
|
||||
* cachedStakingDetails (pushed from Asset Hub via XCM).
|
||||
* Called on People Chain. No stake requirement - user opts in, then a
|
||||
* noter-authorized account submits staking data via receive_staking_details().
|
||||
*/
|
||||
export async function startScoreTracking(
|
||||
peopleApi: ApiPromise,
|
||||
|
||||
+72
-37
@@ -40,6 +40,7 @@ export interface StakingInfo {
|
||||
stakingScore: number | null;
|
||||
stakingDuration: number | null; // Duration in blocks
|
||||
hasStartedScoreTracking: boolean;
|
||||
hasCachedStakingData: boolean; // Whether noter has submitted staking data to People Chain
|
||||
isValidator: boolean;
|
||||
pezRewards: PezRewardInfo | null; // PEZ rewards information
|
||||
}
|
||||
@@ -236,9 +237,10 @@ export async function getStakingInfo(
|
||||
let stakingScore: number | null = null;
|
||||
let stakingDuration: number | null = null;
|
||||
let hasStartedScoreTracking = false;
|
||||
let hasCachedStakingData = false;
|
||||
|
||||
try {
|
||||
// stakingScore pallet is on People Chain - uses cached staking data from Asset Hub via XCM
|
||||
// stakingScore pallet is on People Chain - uses cached staking data submitted by noter
|
||||
const scoreApi = peopleApi || api;
|
||||
if (scoreApi.query.stakingScore && scoreApi.query.stakingScore.stakingStartBlock) {
|
||||
// Check if user has started score tracking
|
||||
@@ -252,47 +254,79 @@ export async function getStakingInfo(
|
||||
const durationInBlocks = currentBlock - startBlock;
|
||||
stakingDuration = durationInBlocks;
|
||||
|
||||
// Calculate amount-based score (20-50 points)
|
||||
const stakedHEZ = ledger ? parseFloat(formatBalance(ledger.total)) : 0;
|
||||
let amountScore = 20; // Default
|
||||
// Check if noter has submitted cached staking data to People Chain
|
||||
// CachedStakingDetails is a DoubleMap: (AccountId, StakingSource) -> StakingDetails
|
||||
// StakingSource: RelayChain = 0, AssetHub = 1
|
||||
// StakingDetails: { staked_amount, nominations_count, unlocking_chunks_count }
|
||||
let totalCachedStakeWei = BigInt(0);
|
||||
if (scoreApi.query.stakingScore.cachedStakingDetails) {
|
||||
try {
|
||||
const [relayResult, assetHubResult] = await Promise.all([
|
||||
scoreApi.query.stakingScore.cachedStakingDetails(address, 'RelayChain')
|
||||
.catch(() => null),
|
||||
scoreApi.query.stakingScore.cachedStakingDetails(address, 'AssetHub')
|
||||
.catch(() => null),
|
||||
]);
|
||||
|
||||
if (stakedHEZ <= 100) {
|
||||
amountScore = 20;
|
||||
} else if (stakedHEZ <= 250) {
|
||||
amountScore = 30;
|
||||
} else if (stakedHEZ <= 750) {
|
||||
amountScore = 40;
|
||||
} else {
|
||||
amountScore = 50; // 751+ HEZ
|
||||
if (relayResult && !relayResult.isEmpty && relayResult.isSome) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const json = (relayResult.unwrap() as any).toJSON() as any;
|
||||
totalCachedStakeWei += BigInt(json.stakedAmount ?? json.staked_amount ?? '0');
|
||||
hasCachedStakingData = true;
|
||||
}
|
||||
if (assetHubResult && !assetHubResult.isEmpty && assetHubResult.isSome) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const json = (assetHubResult.unwrap() as any).toJSON() as any;
|
||||
totalCachedStakeWei += BigInt(json.stakedAmount ?? json.staked_amount ?? '0');
|
||||
hasCachedStakingData = true;
|
||||
}
|
||||
} catch {
|
||||
hasCachedStakingData = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate duration multiplier
|
||||
const MONTH_IN_BLOCKS = 30 * 24 * 60 * 10; // 432,000 blocks (~30 days, 6s per block)
|
||||
let durationMultiplier = 1.0;
|
||||
if (hasCachedStakingData) {
|
||||
// Use cached stake from People Chain (matches on-chain pallet calculation)
|
||||
const stakedHEZ = Number(totalCachedStakeWei / BigInt(10 ** 12));
|
||||
let amountScore = 20; // Default
|
||||
|
||||
if (durationInBlocks >= 12 * MONTH_IN_BLOCKS) {
|
||||
durationMultiplier = 2.0; // 12+ months
|
||||
} else if (durationInBlocks >= 6 * MONTH_IN_BLOCKS) {
|
||||
durationMultiplier = 1.7; // 6-11 months
|
||||
} else if (durationInBlocks >= 3 * MONTH_IN_BLOCKS) {
|
||||
durationMultiplier = 1.4; // 3-5 months
|
||||
} else if (durationInBlocks >= MONTH_IN_BLOCKS) {
|
||||
durationMultiplier = 1.2; // 1-2 months
|
||||
} else {
|
||||
durationMultiplier = 1.0; // < 1 month
|
||||
if (stakedHEZ <= 100) {
|
||||
amountScore = 20;
|
||||
} else if (stakedHEZ <= 250) {
|
||||
amountScore = 30;
|
||||
} else if (stakedHEZ <= 750) {
|
||||
amountScore = 40;
|
||||
} else {
|
||||
amountScore = 50; // 751+ HEZ
|
||||
}
|
||||
|
||||
// Calculate duration multiplier
|
||||
const MONTH_IN_BLOCKS = 30 * 24 * 60 * 10; // 432,000 blocks (~30 days, 6s per block)
|
||||
let durationMultiplier = 1.0;
|
||||
|
||||
if (durationInBlocks >= 12 * MONTH_IN_BLOCKS) {
|
||||
durationMultiplier = 2.0; // 12+ months
|
||||
} else if (durationInBlocks >= 6 * MONTH_IN_BLOCKS) {
|
||||
durationMultiplier = 1.7; // 6-11 months
|
||||
} else if (durationInBlocks >= 3 * MONTH_IN_BLOCKS) {
|
||||
durationMultiplier = 1.4; // 3-5 months
|
||||
} else if (durationInBlocks >= MONTH_IN_BLOCKS) {
|
||||
durationMultiplier = 1.2; // 1-2 months
|
||||
} else {
|
||||
durationMultiplier = 1.0; // < 1 month
|
||||
}
|
||||
|
||||
// Final score calculation (max 100)
|
||||
stakingScore = Math.min(100, Math.floor(amountScore * durationMultiplier));
|
||||
|
||||
console.log('Staking score calculated:', {
|
||||
stakedHEZ,
|
||||
amountScore,
|
||||
durationInBlocks,
|
||||
durationMultiplier,
|
||||
finalScore: stakingScore
|
||||
});
|
||||
}
|
||||
|
||||
// Final score calculation (max 100)
|
||||
// This MUST match the pallet's integer math: amount_score * multiplier_numerator / multiplier_denominator
|
||||
stakingScore = Math.min(100, Math.floor(amountScore * durationMultiplier));
|
||||
|
||||
console.log('Staking score calculated:', {
|
||||
stakedHEZ,
|
||||
amountScore,
|
||||
durationInBlocks,
|
||||
durationMultiplier,
|
||||
finalScore: stakingScore
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -315,6 +349,7 @@ export async function getStakingInfo(
|
||||
stakingScore,
|
||||
stakingDuration,
|
||||
hasStartedScoreTracking,
|
||||
hasCachedStakingData,
|
||||
isValidator,
|
||||
pezRewards
|
||||
};
|
||||
|
||||
@@ -376,11 +376,6 @@ export const StakingDashboard: React.FC = () => {
|
||||
const handleStartScoreTracking = async () => {
|
||||
if (!peopleApi || !selectedAccount) return;
|
||||
|
||||
if (!stakingInfo || parseFloat(stakingInfo.bonded) === 0) {
|
||||
toast.error('You must bond tokens before starting score tracking');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const injector = await getInjectorSigner(selectedAccount.address);
|
||||
@@ -496,23 +491,32 @@ export const StakingDashboard: React.FC = () => {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{stakingInfo?.hasStartedScoreTracking ? (
|
||||
<>
|
||||
<div className="text-2xl font-bold text-purple-500">
|
||||
{stakingInfo.stakingScore}/100
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Duration: {stakingInfo.stakingDuration
|
||||
? `${Math.floor(stakingInfo.stakingDuration / (24 * 60 * 10))} days`
|
||||
: '0 days'}
|
||||
</p>
|
||||
</>
|
||||
stakingInfo.hasCachedStakingData ? (
|
||||
<>
|
||||
<div className="text-2xl font-bold text-purple-500">
|
||||
{stakingInfo.stakingScore}/100
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Duration: {stakingInfo.stakingDuration
|
||||
? `${Math.floor(stakingInfo.stakingDuration / (24 * 60 * 10))} days`
|
||||
: '0 days'}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-lg font-bold text-yellow-500">Waiting for data...</div>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Score tracking started. A noter will submit your staking data soon.
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<div className="text-2xl font-bold text-gray-500">Not Started</div>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleStartScoreTracking}
|
||||
disabled={!stakingInfo || parseFloat(stakingInfo.bonded) === 0 || isLoading}
|
||||
disabled={!stakingInfo || isLoading}
|
||||
className="mt-2 w-full bg-purple-600 hover:bg-purple-700"
|
||||
>
|
||||
Start Score Tracking
|
||||
|
||||
Reference in New Issue
Block a user