Neuro: A Motoko package for basic staking and neuron management

Neuro is a a Motoko package that provides helper classes to simplify the code needed to stake and control your neurons in your motoko canisters.

https://mops.one/neuro

Recently, canisters on ICP have been given the permission to stake and control neurons on the NNS (pending protocol implementation). I want to avail of this feature in my own projects across different canisters and given the significant amount of boilerplate code required to set up seamless staking and management of neurons in Motoko, creating a package to abstract this process became necessary.

This package does not use HTTP outcalls; rather, it directly calls the governance interfaces. Currently, this only works with the SNS and has been tested with the OpenChat SNS. Once the NNS is updated, which I expect to be soon, the package will be updated to provide the same helper classes for staking neurons on the NNS. The functionality is already present in the code, but it is awaiting that release.

Package features:

  • Class-based design simplifies the code :white_check_mark:
  • Enables staking neurons in canisters with a single line of code :white_check_mark:
  • Interfaces for interacting with the governance frameworks :white_check_mark:
  • Interfaces for interacting with neurons :white_check_mark:
  • Stake neurons on the NNS :x: (Available, pending protocol implementation)
  • Control neurons on the NNS :x: (Available, pending protocol implementation)
  • Stake neurons on the SNS :white_check_mark:
  • Control neurons on the SNS :white_check_mark:

Code examples:

What’s nice about this package is that it also has helper functions to stake a neuron in a single line of code:

import { SNS } "mo:neuro";

// Stake a neuron on an SNS:
public func stake_sns_neuron() : async Result.Result<Blob, Text> {
  let sns = SNS.Governance({
    canister_id = Principal.fromActor(thisCanister);
    sns_canister_id = Principal.fromText("2jvtu-yqaaa-aaaaq-aaama-cai");
    sns_ledger_canister_id = Principal.fromText("2ouva-viaaa-aaaaq-aaamq-cai");
  });

  switch (await sns.stake({ amount_e8s = 400_000_000 })) {
    case (#ok result) {
      return #ok(result);
    };
    case (#err result) {
      return #err(result);
    };
  };
};

Below is a before-and-after example of interacting with a neuron using Neuro (illustrative purposes):

Before neuro:

        let SnsGovernance = actor ("2jvtu-yqaaa-aaaaq-aaama-cai") : SnsGovernanceInterface.Self;

        public func disburseMaturity({ neuron_id : NeuronId; percentage_to_disburse : Nat32; to_account :Types.SnsAccount }) : async SnsDisburseMaturityResult {
            let { command } = await SnsGovernance.manage_neuron({
                subaccount = neuron_id;
                command = ? #DisburseMaturity({
                    to_account = to_account;
                    percentage_to_disburse = percentage_to_disburse;
                });
            });

            let ?commandList = command else return #err("Failed to execute neuron command. Neuron ID: " # debug_show neuron_id);

            switch (commandList) {
                case (#DisburseMaturity result) {
                    return #ok(result);
                };
                case _ {
                    return #err("Command failed: " # debug_show commandList);
                };
            };
        };

After neuro:

public func disburse_maturity(id: Blob) : async Result.Result<NeuroTypes.SnsNeuronInformation, Text> {
  let neuron = SNS.Neuron({
    neuron_id = id;
    sns_canister_id = Principal.fromText("2jvtu-yqaaa-aaaaq-aaama-cai");
  });

  return await neuron.disburseMaturity({
            percentage_to_disburse = 100;
            to_account = ?<some account>;
        });
};

The goal:

The goal of this package is to provide the basic and necessary functions to help you stake and control neurons in canisters. It is not intended to include complex functionalities such as staking neurons on behalf of different users or trading neurons. However, you can fork or build upon this package for your own use cases.

It is also not designed to be a comprehensive governance interface and some governance and neuron management functions are intentionally missing. This is why the package is named “neuro” and not “neuron” — allowing someone else to create a more fully-featured package under that name. If you have suggestions or would like to contribute, pull requests are welcome.

Disclaimer

This package is a work in progress and has not undergone extensive testing and I expect some things to change. It is recommended to conduct your own research and thoroughly test the package before using it in your projects. Use this package at your own risk for the staking and management of neurons.

Feedback welcome!

I’m excited to use this package in my own projects and any feedback is welcome. The package has just been released on Mops and can be used on the SNS.

4 Likes

Thank you for expanding the Motoko ecosystem with this. Looks great

1 Like

Nice! I’m sure this will help anyone looking to take advantage of the unrestricted control of neurons. Thank you for the work.

1 Like

Since the proposal https://dashboard.internetcomputer.org/proposal/130732 has now passed. I’ve updated the neuro mops package. It now includes tools for staking and managing NNS neurons. Below is a basic example of how it can make your life easier:

NNS staking example:

...

import { NNS } "mo:neuro";
import NeuroTypes "mo:neuro/types";

...

// Stake a neuron on the NNS:
public func stake_nns_neuron() : async Result.Result<Nat64, Text> {
  let nns = NNS.Governance({
    canister_id = Principal.fromActor(thisCanister);
    nns_canister_id = Principal.fromText("rrkah-fqaaa-aaaaa-aaaaq-cai");
    icp_ledger_canister_id = Principal.fromText("ryjl3-tyaaa-aaaaa-aaaba-cai");
  });

  switch (await nns.stake({ amount_e8s = 100_000_000 })) {
    case (#ok result) {
      return #ok(result);
    };
    case (#err result) {
      return #err(result);
    };
  };
};

...

// Interact with the neuron
public func get_nns_neuron_information(id: Nat64) : async NeuroTypes.NnsInformationResult {
  let neuron = NNS.Neuron({
    neuron_id = id;
    nns_canister_id = Principal.fromText("rrkah-fqaaa-aaaaa-aaaaq-cai");
  });

  return await neuron.getInformation();
};

...

Canister controlled neurons unleashed :rocket:

1 Like

Does this mean SNS projects can now stake their ICP treasury and start earning maturity?

Nice one on developing that Neuro package! :ok_hand: :ok_hand: :clap: How does the disburse_to method handle partial disbursements or transactions with multiple recipients in terms of error handling and validation?

Ye they can stake their treasuries now if they want.

Thank you! I haven’t added the #DisburseToNeuron method if that’s what you mean? I could look at adding it if it’s something people want. The goal was to add the basic neuron management tools first.

The #Disburse method is of course there and you can pass in an account and an amount. In terms of error handling and validation the package returns an empty #ok on successful results and returns a verbose Text error if something fails, the error messages come from the NNS. Heres a snippet from the types:

    // result types for commands and configuration are bare bones
    // only the error is shown, handle the ok result how you want in your code
    public type Result<X, Y> = Result.Result<X, Y>;

    public type ConfigureResult = Result<(), Text>;

    public type CommandResult = Result<(), Text>;
1 Like