Skip to content

Commit

Permalink
Merge neurons (#539)
Browse files Browse the repository at this point in the history
# Motivation

The governance canister supports merging neurons provided that they are controlled by the same principal.
But currently there is no way to use this functionality via the NNS frontend Dapp.
This PR makes it easy for users to merge neurons by adding a new 'Merge Neurons' button to the neurons tab.

# Changes

* Added `merge` to ServiceApi.ts which calls `manage_neuron` on the governance canister using the `merge` command
* Give users the option to merge neurons via a new 'Merge Neurons' button on the neurons tab
  • Loading branch information
anishh2003 committed Mar 15, 2022
1 parent 9d644ee commit 1179461
Show file tree
Hide file tree
Showing 15 changed files with 1,661 additions and 876 deletions.
3 changes: 3 additions & 0 deletions frontend/dart/lib/ic_api/platform_ic_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ abstract class AbstractPlatformICApi {
required ICP amount,
required String? toAccountId});

Future<Result<Unit, Exception>> merge(
{required Neuron neuron1, required Neuron neuron2});

Future<Result<Unit, Exception>> mergeMaturity(
{required Neuron neuron, required int percentageToMerge});

Expand Down
13 changes: 13 additions & 0 deletions frontend/dart/lib/ic_api/web/service_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ class ServiceApi {
@JS("disburseToNeuron")
external Promise<dynamic> disburseToNeuron(dynamic request);

@JS("merge")
external Promise<dynamic> merge(dynamic identity, dynamic request);

@JS("mergeMaturity")
external Promise<dynamic> mergeMaturity(dynamic identity, dynamic request);

Expand Down Expand Up @@ -223,6 +226,16 @@ class DisburseNeuronRequest {
{dynamic neuronId, dynamic amount, String? toAccountId});
}

@JS()
@anonymous
class MergeRequest {
external dynamic neuronId;
external dynamic sourceNeuronId;

external factory MergeRequest(
{dynamic neuronId, dynamic sourceNeuronId});
}

@JS()
@anonymous
class MergeMaturityRequest {
Expand Down
53 changes: 53 additions & 0 deletions frontend/dart/lib/ic_api/web/web_ic_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,59 @@ class PlatformICApi extends AbstractPlatformICApi {
}
}

@override
Future<Result<Unit, Exception>> merge(
{required Neuron neuron1, required Neuron neuron2}) async {
try {
final identity1 = (await getIdentityByNeuron(neuron1)).unwrap();
final identity2 = (await getIdentityByNeuron(neuron2)).unwrap();

if (identity1.getPrincipal().toString() != identity2.getPrincipal().toString()) {
return Result.err(Exception("The neurons being merged must both have the same controller"));
}

// To ensure any built up age bonus is always preserved, if one neuron is
// locked and the other is not, then we merge the neuron which is not
// locked into the locked neuron.
// In all other cases we merge the smaller neuron into the
// larger one.

final neuron1Locked = neuron1.state == NeuronState.LOCKED;
final neuron2Locked = neuron2.state == NeuronState.LOCKED;

Neuron targetNeuron;
Neuron sourceNeuron;
if (neuron1Locked != neuron2Locked) {
if (neuron1Locked) {
targetNeuron = neuron1;
sourceNeuron = neuron2;
} else {
targetNeuron = neuron2;
sourceNeuron = neuron1;
}
} else {
if (neuron1.cachedNeuronStake.asE8s() >= neuron2.cachedNeuronStake.asE8s()) {
targetNeuron = neuron1;
sourceNeuron = neuron2;
} else {
targetNeuron = neuron2;
sourceNeuron = neuron1;
}
}

await promiseToFuture(serviceApi!.merge(
identity1,
MergeRequest(
neuronId: targetNeuron.id.toBigInt.toJS,
sourceNeuronId: sourceNeuron.id.toBigInt.toJS)));
await fetchNeuron(neuronId: targetNeuron.id.toBigInt);
neuronSyncService!.removeNeuron(sourceNeuron.id.toString());
return Result.ok(unit);
} catch (err) {
return Result.err(Exception(err));
}
}

@override
Future<Result<Unit, Exception>> mergeMaturity(
{required Neuron neuron, required int percentageToMerge}) async {
Expand Down
19 changes: 11 additions & 8 deletions frontend/dart/lib/ui/neurons/tab/neuron_row.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@ import '../../../nns_dapp.dart';
class NeuronRow extends StatelessWidget {
final Neuron neuron;
final bool showsWarning;
final Function? onTap;

const NeuronRow(
{Key? key, required this.neuron, this.onTap, this.showsWarning = false})
const NeuronRow({Key? key, required this.neuron, this.showsWarning = false})
: super(key: key);

@override
Expand Down Expand Up @@ -89,15 +87,20 @@ class NeuronRow extends StatelessWidget {
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
"${neuron.dissolveDelay.yearsDayHourMinuteSecondFormatted()}",
style: context.textTheme.subtitle2),
Text(" Dissolve Delay", style: context.textTheme.subtitle2)
Expanded(
child: Text(
"${neuron.dissolveDelay.yearsDayHourMinuteSecondFormatted()}",
style: context.textTheme.subtitle2),
),
Expanded(
flex: 2,
child: Text(" Dissolve Delay",
style: context.textTheme.subtitle2))
],
),
SizedBox(
height: 5,
)
),
]
],
);
Expand Down
90 changes: 56 additions & 34 deletions frontend/dart/lib/ui/neurons/tab/neurons_tab_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:nns_dapp/ui/_components/footer_gradient_button.dart';
import 'package:nns_dapp/ui/_components/form_utils.dart';
import 'package:nns_dapp/ui/_components/page_button.dart';
import 'package:nns_dapp/ui/_components/tab_title_and_content.dart';
import 'package:nns_dapp/ui/transaction/wallet/merge_neuron_accounts_page.dart';
import 'package:nns_dapp/ui/transaction/wallet/select_source_wallet_page.dart';
import 'package:nns_dapp/ui/transaction/wizard_overlay.dart';
import '../../../nns_dapp.dart';
Expand All @@ -16,63 +17,84 @@ class NeuronsPage extends StatefulWidget {
class _NeuronsPageState extends State<NeuronsPage> {
@override
Widget build(BuildContext context) {
return FooterGradientButton(
footerHeight: null,
body: ConstrainWidthAndCenter(
child: StreamBuilder<void>(
stream: context.boxes.neurons.changes,
builder: (context, snapshot) {
return TabTitleAndContent(
return StreamBuilder<void>(
stream: context.boxes.neurons.changes,
builder: (context, snapshot) {
return FooterGradientButton(
footerHeight: null,
body: ConstrainWidthAndCenter(
child: TabTitleAndContent(
title: "Neurons",
subtitle:
'''Earn rewards by staking your ICP in neurons. Neurons allow you to participate in governance on the Internet Computer by voting on Network Nervous System (NNS) proposals.
Your principal id is "${context.icApi.getPrincipal()}"''',
children: [
SmallFormDivider(),
...(context.boxes.neurons.values
?.sortedByDescending((element) =>
element.createdTimestampSeconds.toBigInt)
?.sortedByDescending((element) => element.createdTimestampSeconds.toBigInt)
.mapToList((e) => Card(
child: TextButton(
onPressed: () {
context.nav.push(
neuronPageDef.createPageConfig(e));
context.nav.push(neuronPageDef.createPageConfig(e));
},
child: Padding(
padding: const EdgeInsets.all(16.0),
child: NeuronRow(
neuron: e,
showsWarning: true,
onTap: () {
context.nav.push(
neuronPageDef.createPageConfig(e));
},
),
),
),
)) ??
[]),
SizedBox(height: 150)
],
);
}),
),
footer: Align(
alignment: Alignment.bottomCenter,
child: PageButton(
title: "Stake Neuron",
onPress: () {
OverlayBaseWidget.show(
context,
WizardOverlay(
rootTitle: "Select Source Account",
rootWidget: SelectSourceWallet(isStakeNeuron: true),
),
);
},
),
),
);
),
footer: Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: PageButton(
title: "Stake Neuron",
onPress: () {
OverlayBaseWidget.show(
context,
WizardOverlay(
rootTitle: "Select Source Account",
rootWidget: SelectSourceWallet(isStakeNeuron: true),
),
);
},
),
),
Flexible(
child: PageButton(
title: "Merge Neurons",
onPress: () {
OverlayBaseWidget.show(
context,
WizardOverlay(
rootTitle: "Select Two neurons to merge",
rootWidget: MergeNeuronSourceAccount(
onCompleteAction: (context) {
OverlayBaseWidget.of(context)?.dismiss();
},
),
),
);
}.takeIf((e) => context.boxes.neurons.values.length >= 2)),
),
],
),
),
),
);
});
}
}

0 comments on commit 1179461

Please sign in to comment.