Skip to content

Commit a75f1e3

Browse files
committed
chore: add claimNeuronBuilder and tests
Ticket: SC-1970
1 parent aac887c commit a75f1e3

File tree

6 files changed

+363
-1011
lines changed

6 files changed

+363
-1011
lines changed

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

Lines changed: 0 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,6 @@ import {
88
ICRC1_FEE_KEY,
99
METADATA_CALL,
1010
DEFAULT_SUBACCOUNT,
11-
NeuronInfo,
12-
ProposalInfo,
13-
ClaimNeuronParams,
14-
SetFolloweesParams,
15-
DissolveDelayParams,
1611
} from './iface';
1712
import BigNumber from 'bignumber.js';
1813

@@ -110,92 +105,4 @@ export class IcpAgent {
110105
throw new Error(`Error fetching transaction fee: ${errorMessage}`);
111106
}
112107
}
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-
}
201108
}

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

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -264,23 +264,6 @@ export interface TransactionHexParams {
264264
signableHex?: string;
265265
}
266266

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-
284267
export interface ProposalInfo {
285268
proposalId: bigint;
286269
title: string;
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
2+
import { TransactionBuilder } from '../transactionBuilder';
3+
import { BaseKey, BaseTransaction, BuildTransactionError } from '@bitgo/sdk-core';
4+
import { Principal } from '@dfinity/principal';
5+
import { createHash } from 'crypto';
6+
import utils, { Utils } from '../utils';
7+
import { Transaction } from '../transaction';
8+
import { UnsignedTransactionBuilder } from '../unsignedTransactionBuilder';
9+
import {
10+
GOVERNANCE_CANISTER_ID,
11+
IcpTransaction,
12+
IcpTransactionData,
13+
OperationType,
14+
CurveType,
15+
IcpPublicKey,
16+
IcpOperation,
17+
ClaimNeuronParams,
18+
} from '../iface';
19+
20+
export class ClaimNeuronBuilder extends TransactionBuilder {
21+
private _neuronMemo: bigint;
22+
private utils: Utils;
23+
24+
constructor(_coinConfig: Readonly<CoinConfig>) {
25+
super(_coinConfig);
26+
this._neuronMemo = 0n;
27+
this.utils = new Utils();
28+
}
29+
30+
/**
31+
* Set the neuron memo for staking
32+
*
33+
* @param {bigint} memo - The memo to use for neuron identification
34+
* @returns {this} The builder instance
35+
*/
36+
public neuronMemo(memo: bigint): this {
37+
this.utils.validateMemo(memo);
38+
this._neuronMemo = memo;
39+
return this;
40+
}
41+
42+
/**
43+
* Generate the neuron subaccount based on controller principal and memo
44+
*
45+
* @param {Principal} controllerPrincipal - The principal ID of the controller
46+
* @param {bigint} memo - The memo value
47+
* @returns {Uint8Array} The generated subaccount
48+
*/
49+
private getNeuronSubaccount(controllerPrincipal: Principal, memo: bigint): Uint8Array {
50+
const nonceBuf = Buffer.alloc(8);
51+
nonceBuf.writeBigUInt64BE(memo);
52+
const domainSeparator = Buffer.from([0x0c]);
53+
const context = Buffer.from('neuron-stake', 'utf8');
54+
const principalBytes = controllerPrincipal.toUint8Array();
55+
56+
const hashInput = Buffer.concat([domainSeparator, context, principalBytes, nonceBuf]);
57+
58+
return new Uint8Array(createHash('sha256').update(hashInput).digest());
59+
}
60+
61+
/** @inheritdoc */
62+
protected async buildImplementation(): Promise<Transaction> {
63+
if (!this._sender || !this._publicKey) {
64+
throw new BuildTransactionError('Sender address and public key are required');
65+
}
66+
if (!this._amount) {
67+
throw new BuildTransactionError('Staking amount is required');
68+
}
69+
70+
// Get controller principal from public key
71+
const controllerPrincipal = this.utils.derivePrincipalFromPublicKey(this._publicKey);
72+
73+
// Generate neuron subaccount
74+
const subaccount = this.getNeuronSubaccount(controllerPrincipal, this._neuronMemo);
75+
76+
// Set receiver as governance canister with neuron subaccount
77+
const governancePrincipal = Principal.fromUint8Array(GOVERNANCE_CANISTER_ID);
78+
this._receiverId = this.utils.fromPrincipal(governancePrincipal, subaccount);
79+
80+
const publicKey: IcpPublicKey = {
81+
hex_bytes: this._publicKey,
82+
curve_type: CurveType.SECP256K1,
83+
};
84+
85+
const senderOperation: IcpOperation = {
86+
type: OperationType.TRANSACTION,
87+
account: { address: this._sender },
88+
amount: {
89+
value: `-${this._amount}`,
90+
currency: {
91+
symbol: this._coinConfig.family,
92+
decimals: this._coinConfig.decimalPlaces,
93+
},
94+
},
95+
};
96+
97+
const receiverOperation: IcpOperation = {
98+
type: OperationType.TRANSACTION,
99+
account: { address: this._receiverId },
100+
amount: {
101+
value: this._amount,
102+
currency: {
103+
symbol: this._coinConfig.family,
104+
decimals: this._coinConfig.decimalPlaces,
105+
},
106+
},
107+
};
108+
109+
const feeOperation: IcpOperation = {
110+
type: OperationType.FEE,
111+
account: { address: this._sender },
112+
amount: {
113+
value: this.utils.feeData(),
114+
currency: {
115+
symbol: this._coinConfig.family,
116+
decimals: this._coinConfig.decimalPlaces,
117+
},
118+
},
119+
};
120+
121+
const createdTimestamp = this._transaction.createdTimestamp;
122+
const { metaData, ingressEndTime } = this.utils.getMetaData(
123+
Number(this._neuronMemo),
124+
createdTimestamp,
125+
this._ingressEnd
126+
);
127+
128+
const icpTransaction: IcpTransaction = {
129+
public_keys: [publicKey],
130+
operations: [senderOperation, receiverOperation, feeOperation],
131+
metadata: metaData,
132+
};
133+
134+
const icpTransactionData: IcpTransactionData = {
135+
senderAddress: this._sender,
136+
receiverAddress: this._receiverId,
137+
amount: this._amount,
138+
fee: this.utils.feeData(),
139+
senderPublicKeyHex: this._publicKey,
140+
transactionType: OperationType.TRANSACTION,
141+
expiryTime: ingressEndTime,
142+
memo: Number(this._neuronMemo),
143+
};
144+
145+
this._transaction.icpTransactionData = icpTransactionData;
146+
this._transaction.icpTransaction = icpTransaction;
147+
148+
const unsignedTransactionBuilder = new UnsignedTransactionBuilder(this._transaction.icpTransaction);
149+
this._transaction.payloadsData = await unsignedTransactionBuilder.getUnsignedTransaction();
150+
return this._transaction;
151+
}
152+
153+
/** @inheritdoc */
154+
protected signImplementation(key: BaseKey): BaseTransaction {
155+
const signatures = utils.getSignatures(this._transaction.payloadsData, this._publicKey, key.key);
156+
this._transaction.addSignature(signatures);
157+
return this._transaction;
158+
}
159+
160+
/**
161+
* Get the parameters needed for claiming the neuron after the staking transaction is confirmed.
162+
* This allows the consumer to handle the network calls themselves.
163+
*
164+
* @returns {ClaimNeuronParams} Parameters needed for claiming the neuron
165+
* @throws {BuildTransactionError} If required fields are missing
166+
*/
167+
public getClaimNeuronParams(): ClaimNeuronParams {
168+
if (!this._publicKey) {
169+
throw new BuildTransactionError('Public key is required');
170+
}
171+
172+
const controllerPrincipal = this.utils.derivePrincipalFromPublicKey(this._publicKey);
173+
return {
174+
controller: controllerPrincipal,
175+
memo: this._neuronMemo,
176+
};
177+
}
178+
}

0 commit comments

Comments
 (0)