Skip to content

Commit

Permalink
Remove unused check neurons code (#5183)
Browse files Browse the repository at this point in the history
# Motivation

In recent PRs I've been changing how we check if neurons need to be
refreshed or claimed.
As a reset some code has become unused.

# Changes

Remove `checkSnsNeuronBalances` and functions only used by it.

# Tests

1. Remove unit tests.
2. Add one mock behavior setup which was previously depended on via the
setup of another test.
3. Add `vi.restoreAllMocks()` to prevent tests depending on each other.

# Todos

- [ ] Add entry to changelog (if necessary).
not necessary
  • Loading branch information
dskloetd authored Jul 11, 2024
1 parent de6e4c0 commit a2ba057
Show file tree
Hide file tree
Showing 2 changed files with 3 additions and 472 deletions.
225 changes: 1 addition & 224 deletions frontend/src/lib/services/sns-neurons-check-balances.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import {
claimNeuron,
getNeuronBalance,
getSnsNeuron,
querySnsNeuron,
refreshNeuron,
} from "$lib/api/sns-governance.api";
import { MAX_NEURONS_SUBACCOUNTS } from "$lib/constants/sns-neurons.constants";
import { getAuthenticatedIdentity } from "$lib/services/auth.services";
import { loadSnsParameters } from "$lib/services/sns-parameters.services";
import { checkedNeuronSubaccountsStore } from "$lib/stores/checked-neurons.store";
Expand All @@ -17,7 +15,7 @@ import {
nextMemo,
subaccountToHexString,
} from "$lib/utils/sns-neuron.utils";
import { AnonymousIdentity, type Identity } from "@dfinity/agent";
import type { Identity } from "@dfinity/agent";
import type { Principal } from "@dfinity/principal";
import type { SnsNeuron, SnsNeuronId } from "@dfinity/sns";
import { neuronSubaccount } from "@dfinity/sns";
Expand Down Expand Up @@ -126,35 +124,6 @@ const claimAndLoadNeuron = async ({
});
};

const findNeuronBySubaccount = async ({
subaccount,
neurons,
rootCanisterId,
positiveBalance,
}: {
subaccount: Uint8Array | number[];
neurons: SnsNeuron[];
rootCanisterId: Principal;
positiveBalance: boolean;
}): Promise<SnsNeuron | undefined> => {
let maybeNeuron = neurons.find(
(neuron) =>
getSnsNeuronIdAsHexString(neuron) === subaccountToHexString(subaccount)
);
// At this point we don't know whether it's an unclaimed or transferred neuron.
if (isNullish(maybeNeuron) && positiveBalance) {
const neuronId: SnsNeuronId = { id: subaccount };
maybeNeuron = await querySnsNeuron({
identity: new AnonymousIdentity(),
rootCanisterId,
neuronId,
// No need to check with update call, worst case, a neuron will appear in the UI that shouldn't or an extra call to refressh will be made.
certified: false,
});
}
return maybeNeuron;
};

/**
* Returns true only of neuron is present and the user has no permissions.
*
Expand All @@ -177,163 +146,6 @@ const userHasNoPermissions = ({
);
});

/**
* Checks subaccounts of identity in order to find neurons that need to be refreshed or claimed.
*
* Neuron subaccount is { sha256(0x0c . “neuron-stake” . P . i) | i ∈ ℕ }
*
* The main property of this is that it is always possible to recompute all the neurons subaccounts of a principal.
* This can be used to make the SNS UI stateless, which then means that the SNS UI can fail at any time
* without losing any important information needed to derive accounts.
*
* Neurons subaccounts of a principal must be used in order of i.
* The first account of P is sha256(0x0c . “neuron-staking” . P . 0), the second one is sha256(0x0c . “neuron-staking” . P . 1) …
* This allows any client to verify the state of neurons subaccounts that have received a transfer without checking all the ns subaccounts.
*
* A stateless SNS UI will perform the following operations every time it starts:
* - It gets the list of neurons from the SNS Governance using list_neurons. This is done via an update call to confirm the correctness of the data.
* - It calculates all its ns subaccounts and then checks their balances via query calls. This is where the ordering of ns subaccounts comes into play: the client just needs to check the ns subaccounts that are either already neuron accounts returned in 1. or have a balance different from 0.
* - The SNS UI notifies the SNS Governance of all the neuron subaccounts that the user has some permission and:
* - Have a balance different from 0 but the list of neurons doesn’t list them as neurons.
* - In this case, the client needs to claim the neuron.
* - Have a balance different from the cached_neuron_stake_e8s returned by the SNS Governance
* - In this case, the client needs to refresh the neuron.
*
* SUMMARY:
*
* -------------------------------------------------------------------
* | | Balance 0 | Balance > 0 |
* | found neuron (with permission) | check neuron stake vs balance |
* | no neuron | stop checking | claim neuron |
* -------------------------------------------------------------------
*
* @param {Object}
* @param {Principal} params.rootCanisterId
* @param {SnsNeuron[]} params.neurons neurons to check
* @param {Identity} params.identity
* @returns
*/
const checkNeuronsSubaccounts = async ({
identity,
rootCanisterId,
neurons,
neuronMinimumStake,
}: {
identity: Identity;
rootCanisterId: Principal;
neurons: SnsNeuron[];
neuronMinimumStake: bigint;
}): Promise<SnsNeuron[]> => {
const visitedSubaccounts = new Set<string>();
const controller = identity.getPrincipal();
// Visit subaccounts by order until we find a balance of 0.
// Therefore, we set the initial balance to some non-zero value.
let currentBalance = BigInt(Number.MAX_SAFE_INTEGER);
for (let index = 0; index < MAX_NEURONS_SUBACCOUNTS; index++) {
try {
// In case there is an error getting the balance, the loop stops.
currentBalance = 0n;
const subaccount = neuronSubaccount({
controller,
index,
});
// Id of an sns neuron is the subaccount of the sns governance canister where the stake is
const currentNeuronId: SnsNeuronId = { id: subaccount };
visitedSubaccounts.add(subaccountToHexString(subaccount));
// We use certified: false for performance reasons.
// A bad actor will only get that we call refresh or claim on a neuron.
currentBalance = await getNeuronBalance({
rootCanisterId,
neuronId: currentNeuronId,
certified: false,
identity,
});
const positiveBalance = currentBalance >= neuronMinimumStake;
// At this point we don't know whether it's an unclaimed or transferred neuron.
const neuron = await findNeuronBySubaccount({
subaccount,
neurons,
rootCanisterId,
positiveBalance,
});
// Skip claiming or refreshing if the user has no permission.
// This might happen if the user staked the neuron in NNS Dapp and then transferred it.
if (userHasNoPermissions({ neuron, controller })) {
continue;
}
const neuronNotFound = neuron === undefined;
// Subaccount balance >= `neuron_minimum_stake_e8s` but no neuron found, claim it.
if (positiveBalance && neuronNotFound) {
await claimAndLoadNeuron({
rootCanisterId,
identity,
controller,
memo: BigInt(index),
subaccount,
});
}
// Neuron found, check if it needs to be refreshed.
if (
neuron !== undefined &&
needsRefresh({ neuron, balanceE8s: currentBalance })
) {
// Edge case, a valid neuron should have a neuron id
const neuronId = fromNullable(neuron.id);
if (neuronId !== undefined) {
await refreshNeuron({ rootCanisterId, identity, neuronId });
await loadNeuron({
rootCanisterId,
neuronId: neuronId,
certified: true,
identity,
});
}
}
// If the balance is 0 and there is no neuron in that subaccount, stop checking.
if (currentBalance === 0n && neuronNotFound) {
break;
}
} catch (error) {
// Ignore the error, we do this in the background.
// If this fails the data might be stale but it should refreshed on the next sync.
console.error(error);
}
}
// Not all neurons that the user has some permission need to be created in NNS Dapp.
// Some of those neurons might need a refresh as well.
// That's done in another function, here we only return them.
const unvisitedNeurons = neurons.filter(
({ id }) =>
!visitedSubaccounts.has(
subaccountToHexString(fromNullable(id)?.id ?? new Uint8Array())
)
);
return unvisitedNeurons;
};

/**
* Checks neurons subaccounts and refreshes neurons that need it.
*
* @param {Object}
* @param {Principal} params.rootCanisterId
* @param {SnsNeuron[]} params.neurons neurons to check
* @param {Identity} params.identity
* @returns
*/
const checkNeurons = async ({
identity,
rootCanisterId,
neurons,
}: {
identity: Identity;
rootCanisterId: Principal;
neurons: SnsNeuron[];
}) => {
for (const neuron of neurons) {
await checkNeuron({ identity, rootCanisterId, neuron });
}
};

/**
* Checks neuron's subaccount and refreshes the neuron if needed.
* Returns true if the neuron was refreshed.
Expand Down Expand Up @@ -364,41 +176,6 @@ const checkNeuron = async ({
return true;
};

/**
* Checks a couple of things:
* - Neurons subaccounts balances and refreshes or claims neurons that need it.
* - This follows a new staking neuron algorithm explained in the helper `checkNeuronsSubaccounts`
* - Check the rest of the neurons and refreshes them if needed.
*
* It refetches the neurons that are not in sync with the subaccounts and adds them to the store.
*
* Note:
* `SnsNervousSystemParameters` should be preloaded before calling this function.
*
* @param param0
* @returns {boolean}
*/
export const checkSnsNeuronBalances = async ({
rootCanisterId,
neurons,
neuronMinimumStake,
}: {
rootCanisterId: Principal;
neurons: SnsNeuron[];
neuronMinimumStake: bigint;
}): Promise<void> => {
// TODO: Check neurons controlled by linked HW?
const identity = await getAuthenticatedIdentity();
const unvisitedNeurons = await checkNeuronsSubaccounts({
identity,
rootCanisterId,
neurons,
neuronMinimumStake,
});

await checkNeurons({ identity, rootCanisterId, neurons: unvisitedNeurons });
};

// Returns true if the neuron was refreshed.
export const refreshNeuronIfNeeded = async ({
rootCanisterId,
Expand Down
Loading

0 comments on commit a2ba057

Please sign in to comment.