Skip to content

Commit aac887c

Browse files
committed
feat: add icp staking flow
Ticket: SC-1970
1 parent 4cf7e72 commit aac887c

File tree

4 files changed

+1109
-2
lines changed

4 files changed

+1109
-2
lines changed

modules/sdk-coin-icp/src/lib/icpAgent.ts

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
11
import { Principal } from '@dfinity/principal';
22
import { HttpAgent, replica, AgentCanister } from 'ic0';
33
import utils from './utils';
4-
import { ACCOUNT_BALANCE_CALL, LEDGER_CANISTER_ID, ICRC1_FEE_KEY, METADATA_CALL, DEFAULT_SUBACCOUNT } from './iface';
4+
import {
5+
ACCOUNT_BALANCE_CALL,
6+
LEDGER_CANISTER_ID,
7+
GOVERNANCE_CANISTER_ID,
8+
ICRC1_FEE_KEY,
9+
METADATA_CALL,
10+
DEFAULT_SUBACCOUNT,
11+
NeuronInfo,
12+
ProposalInfo,
13+
ClaimNeuronParams,
14+
SetFolloweesParams,
15+
DissolveDelayParams,
16+
} from './iface';
517
import BigNumber from 'bignumber.js';
618

719
export class IcpAgent {
@@ -38,6 +50,17 @@ export class IcpAgent {
3850
return ic(Principal.fromUint8Array(LEDGER_CANISTER_ID).toText());
3951
}
4052

53+
/**
54+
* Retrieves an instance of the governance canister agent.
55+
*
56+
* @returns {AgentCanister} An agent interface for interacting with the governance canister.
57+
*/
58+
private getGovernanceCanister(): AgentCanister {
59+
const agent = this.createAgent();
60+
const ic = replica(agent, { local: true });
61+
return ic(Principal.fromUint8Array(GOVERNANCE_CANISTER_ID).toText());
62+
}
63+
4164
/**
4265
* Fetches the account balance for a given principal ID.
4366
* @param principalId - The principal ID of the account.
@@ -87,4 +110,92 @@ export class IcpAgent {
87110
throw new Error(`Error fetching transaction fee: ${errorMessage}`);
88111
}
89112
}
113+
114+
/**
115+
* Claims or refreshes a neuron from an account.
116+
*
117+
* @param {ClaimNeuronParams} params - Parameters for claiming the neuron
118+
* @returns {Promise<bigint>} The neuron ID of the claimed neuron
119+
* @throws {Error} If the neuron cannot be claimed
120+
*/
121+
public async claimNeuron(params: ClaimNeuronParams): Promise<bigint> {
122+
try {
123+
const governance = this.getGovernanceCanister();
124+
const result = await governance.call('claim_or_refresh_neuron_from_account', params);
125+
return BigInt(result.neuron_id);
126+
} catch (error) {
127+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
128+
throw new Error(`Error claiming neuron: ${errorMessage}`);
129+
}
130+
}
131+
132+
/**
133+
* Retrieves detailed information about a neuron.
134+
*
135+
* @param {bigint} neuronId - The ID of the neuron to query
136+
* @returns {Promise<NeuronInfo>} Detailed information about the neuron
137+
* @throws {Error} If the neuron information cannot be retrieved
138+
*/
139+
public async getNeuronInfo(neuronId: bigint): Promise<NeuronInfo> {
140+
try {
141+
const governance = this.getGovernanceCanister();
142+
const result = await governance.call('get_neuron_info', { neuron_id: neuronId });
143+
return result;
144+
} catch (error) {
145+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
146+
throw new Error(`Error getting neuron info for ${neuronId}: ${errorMessage}`);
147+
}
148+
}
149+
150+
/**
151+
* Sets the followees for a neuron on a specific topic.
152+
*
153+
* @param {SetFolloweesParams} params - Parameters for setting followees
154+
* @returns {Promise<void>}
155+
* @throws {Error} If the followees cannot be set
156+
*/
157+
public async setFollowees(params: SetFolloweesParams): Promise<void> {
158+
try {
159+
const governance = this.getGovernanceCanister();
160+
await governance.call('set_followees', params);
161+
} catch (error) {
162+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
163+
throw new Error(`Error setting followees for neuron ${params.neuronId}: ${errorMessage}`);
164+
}
165+
}
166+
167+
/**
168+
* Sets the dissolve delay for a neuron.
169+
*
170+
* @param {DissolveDelayParams} params - Parameters for setting dissolve delay
171+
* @returns {Promise<void>}
172+
* @throws {Error} If the dissolve delay cannot be set
173+
*/
174+
public async setDissolveDelay(params: DissolveDelayParams): Promise<void> {
175+
try {
176+
const governance = this.getGovernanceCanister();
177+
await governance.call('set_dissolve_delay', params);
178+
} catch (error) {
179+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
180+
throw new Error(`Error setting dissolve delay for neuron ${params.neuronId}: ${errorMessage}`);
181+
}
182+
}
183+
184+
/**
185+
* Retrieves information about a specific proposal.
186+
*
187+
* @param {bigint} proposalId - The ID of the proposal to query
188+
* @returns {Promise<ProposalInfo>} Information about the proposal
189+
* @throws {Error} If the proposal information cannot be retrieved
190+
*/
191+
public async getProposal(proposalId: bigint): Promise<ProposalInfo> {
192+
try {
193+
const governance = this.getGovernanceCanister();
194+
const result = await governance.call('get_proposal_info', { proposal_id: proposalId });
195+
return result;
196+
} catch (error) {
197+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
198+
throw new Error(`Error getting proposal info for ${proposalId}: ${errorMessage}`);
199+
}
200+
}
90201
}

modules/sdk-coin-icp/src/lib/iface.ts

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import {
22
TransactionExplanation as BaseTransactionExplanation,
33
TransactionType as BitGoTransactionType,
44
} from '@bitgo/sdk-core';
5+
import { Principal } from '@dfinity/principal';
56

67
export const MAX_INGRESS_TTL = 5 * 60 * 1000_000_000; // 5 minutes in nanoseconds
78
export const PERMITTED_DRIFT = 60 * 1000_000_000; // 60 seconds in nanoseconds
8-
export const LEDGER_CANISTER_ID = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 2, 1, 1]); // Uint8Array value for "00000000000000020101" and the string value is "ryjl3-tyaaa-aaaaa-aaaba-cai"
9+
export const LEDGER_CANISTER_ID = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 2, 1, 1]); // ryjl3-tyaaa-aaaaa-aaaba-cai
10+
export const GOVERNANCE_CANISTER_ID = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); // rrkah-fqaaa-aaaaa-aaaaq-cai
911
export const ROOT_PATH = 'm/0';
1012
export const ACCOUNT_BALANCE_CALL = 'icrc1_balance_of';
1113
export const PUBLIC_NODE_REQUEST_ENDPOINT = '/api/v3/canister/';
@@ -39,6 +41,25 @@ export enum Network {
3941
ID = '00000000000000020101', // ICP does not have different network IDs for mainnet and testnet
4042
}
4143

44+
export enum Topic {
45+
Unspecified = 0,
46+
Governance = 1,
47+
SnsAndCommunityFund = 2,
48+
NetworkEconomics = 3,
49+
Node = 4,
50+
ParticipantManagement = 5,
51+
SubnetManagement = 6,
52+
NetworkCanisterManagement = 7,
53+
KYC = 8,
54+
NodeProviderRewards = 9,
55+
}
56+
57+
export enum Vote {
58+
Unspecified = 0,
59+
Yes = 1,
60+
No = 2,
61+
}
62+
4263
export interface IcpTransactionData {
4364
senderAddress: string;
4465
receiverAddress: string;
@@ -194,6 +215,37 @@ export interface RecoveryTransaction {
194215
tx: string;
195216
}
196217

218+
export enum HotkeyPermission {
219+
VOTE = 'VOTE',
220+
FOLLOW = 'FOLLOW',
221+
DISSOLVE = 'DISSOLVE',
222+
CONFIGURE = 'CONFIGURE',
223+
}
224+
225+
export interface HotkeyStatus {
226+
principal: Principal;
227+
permissions: HotkeyPermission[];
228+
addedAt: number;
229+
lastUsed?: number;
230+
}
231+
232+
export interface FullNeuron {
233+
controller: Principal;
234+
hotKeys: Principal[];
235+
hotkeyDetails?: HotkeyStatus[];
236+
dissolveDelaySeconds: number;
237+
votingPower: bigint;
238+
state: string;
239+
}
240+
241+
export interface NeuronInfo {
242+
neuronId: bigint;
243+
fullNeuron?: FullNeuron;
244+
dissolveDelaySeconds: number;
245+
votingPower: bigint;
246+
state: string;
247+
}
248+
197249
export interface UnsignedSweepRecoveryTransaction {
198250
txHex: string;
199251
coin: string;
@@ -211,3 +263,46 @@ export interface TransactionHexParams {
211263
transactionHex: string;
212264
signableHex?: string;
213265
}
266+
267+
export interface NeuronInfo {
268+
neuronId: bigint;
269+
controller: Principal;
270+
dissolveDelaySeconds: number;
271+
votingPower: bigint;
272+
followees: Record<Topic, bigint[]>;
273+
recentBallots: Ballot[];
274+
createdTimestamp: number;
275+
state: string;
276+
stakedAmount: bigint;
277+
}
278+
279+
export interface Ballot {
280+
proposalId: bigint;
281+
vote: Vote;
282+
}
283+
284+
export interface ProposalInfo {
285+
proposalId: bigint;
286+
title: string;
287+
topic: Topic;
288+
status: string;
289+
summary: string;
290+
proposer: Principal;
291+
created: number;
292+
}
293+
294+
export interface ClaimNeuronParams {
295+
controller: Principal;
296+
memo: bigint;
297+
}
298+
299+
export interface SetFolloweesParams {
300+
neuronId: bigint;
301+
topic: Topic;
302+
followees: bigint[];
303+
}
304+
305+
export interface DissolveDelayParams {
306+
neuronId: bigint;
307+
dissolveDelaySeconds: number;
308+
}

0 commit comments

Comments
 (0)