Skip to content

Commit f1f3f7d

Browse files
nooxxloicttn
authored andcommitted
cardano: craft stake tx / withdraw tx / unstake tx
1 parent e7f6e5b commit f1f3f7d

File tree

2 files changed

+122
-46
lines changed

2 files changed

+122
-46
lines changed

examples/ada.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,15 @@ const f = async () => {
1919
],
2020
});
2121

22-
const tx = await k.ada.craftWithdrawRewardsTx(
22+
// const tx = await k.ada.craftStakeTx(
23+
// '376acfff-e35d-4b7c-90da-c6acb8ea7197',
24+
// 'addr_test1qpy358g8glafrucevf0rjpmzx2k5esn5uvjh7dzuakpdhv4g2egyt3y3qw6jrguz0lmyhxygjdg2ytaf5z6ueaety7dsmpcee5',
25+
// );
26+
// const tx = await k.ada.craftWithdrawRewardsTx(
27+
// 'addr_test1qpy358g8glafrucevf0rjpmzx2k5esn5uvjh7dzuakpdhv4g2egyt3y3qw6jrguz0lmyhxygjdg2ytaf5z6ueaety7dsmpcee5',
28+
// );
29+
30+
const tx = await k.ada.craftUnstakeTx(
2331
'addr_test1qpy358g8glafrucevf0rjpmzx2k5esn5uvjh7dzuakpdhv4g2egyt3y3qw6jrguz0lmyhxygjdg2ytaf5z6ueaety7dsmpcee5',
2432
);
2533
const txSigned = await k.ada.sign('vault1', tx);

src/services/ada.ts

Lines changed: 113 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
BigNum,
44
Certificate,
55
Certificates,
6-
CoinSelectionStrategyCIP2,
76
Ed25519KeyHash,
87
Ed25519Signature,
98
hash_transaction,
@@ -12,6 +11,7 @@ import {
1211
RewardAddress,
1312
StakeCredential,
1413
StakeDelegation,
14+
StakeDeregistration,
1515
StakeRegistration,
1616
Transaction,
1717
TransactionBuilder,
@@ -20,13 +20,12 @@ import {
2020
TransactionInput,
2121
TransactionOutput,
2222
TransactionOutputs,
23-
TransactionUnspentOutput,
24-
TransactionUnspentOutputs,
2523
TransactionWitnessSet,
2624
Value,
2725
Vkey,
2826
Vkeywitness,
29-
Vkeywitnesses, Withdrawals,
27+
Vkeywitnesses,
28+
Withdrawals,
3029
} from '@emurgo/cardano-serialization-lib-nodejs';
3130
import { Service } from "./service";
3231
import { AdaStakeOptions, InternalAdaConfig, UTXO } from "../types/ada";
@@ -46,6 +45,7 @@ const CARDANO_PARAMS = {
4645
POOL_DEPOSIT: '500000000',
4746
KEY_DEPOSIT: '2000000',
4847
MIN_UTXO_VALUE_ADA_ONLY: 1000000,
48+
DEFAULT_NATIVE_FEES: 300000, // Over-estimate (0.3 ADA)
4949
};
5050

5151
export class AdaService extends Service {
@@ -73,10 +73,9 @@ export class AdaService extends Service {
7373

7474
try {
7575
const utxos = await this.getUtxos(walletAddress);
76-
const outputs = this.prepareTx(CARDANO_PARAMS.KEY_DEPOSIT, walletAddress);
7776
const address = await this.client.addresses(walletAddress);
7877
if (!address.stake_address) {
79-
throw Error('No stake address');
78+
throw Error('Could not fetch stake address');
8079
}
8180

8281
const stakeKeyHash = await this.getStakeKeyHash(address.stake_address);
@@ -85,12 +84,13 @@ export class AdaService extends Service {
8584
}
8685
const certificates = Certificates.new();
8786

88-
const registrations = await this.client.accountsRegistrations(address.stake_address);
87+
const registrations = await this.client.accountsRegistrationsAll(address.stake_address);
88+
const lastRegistration = registrations.length > 0 ? registrations[registrations.length - 1] : undefined;
8989
const pool = await this.client.poolsById(poolId);
9090
const poolKeyHash = Ed25519KeyHash.from_hex(pool.hex);
9191

92-
// Register stake key if not done already
93-
if (registrations.length === 0) {
92+
// Register stake key if not done already or if last registration was a deregister action
93+
if (!lastRegistration || lastRegistration.action === 'deregistered') {
9494
certificates.add(
9595
Certificate.new_stake_registration(
9696
StakeRegistration.new(
@@ -111,6 +111,10 @@ export class AdaService extends Service {
111111
),
112112
),
113113
);
114+
115+
const walletBalance = this.getWalletBalance(utxos);
116+
const outAmount = (walletBalance - CARDANO_PARAMS.DEFAULT_NATIVE_FEES - Number(CARDANO_PARAMS.KEY_DEPOSIT)).toString();
117+
const outputs = this.prepareTx(outAmount, walletAddress);
114118
return await this.buildTx(walletAddress, utxos, outputs, certificates);
115119
} catch (error) {
116120
throw error;
@@ -147,27 +151,69 @@ export class AdaService extends Service {
147151
throw Error('Could not retrieve rewards address');
148152
}
149153

150-
const rewardsHistory = await this.client.accountsRewardsAll(address.stake_address);
151-
let totalRewards: number = 0;
152-
for(const rewards of rewardsHistory){
153-
totalRewards += Number(rewards.amount);
154+
const availableRewards = await this.getAvailableRewards(address.stake_address);
155+
const amountToWithdrawLovelace = amountToWithdraw ? this.adaToLovelace(amountToWithdraw.toString()) : availableRewards.toString();
156+
withdrawals.insert(rewardAddress, BigNum.from_str(amountToWithdrawLovelace));
157+
158+
const walletBalance = this.getWalletBalance(utxos);
159+
const outAmount = (walletBalance - CARDANO_PARAMS.DEFAULT_NATIVE_FEES + availableRewards).toString();
160+
const outputs = this.prepareTx(outAmount, walletAddress);
161+
162+
return await this.buildTx(walletAddress, utxos, outputs, null, withdrawals);
163+
} catch (error) {
164+
throw error;
165+
}
166+
}
167+
168+
/**
169+
* Craft ada undelegate transaction
170+
* @param walletAddress wallet delegating that will receive the rewards
171+
*/
172+
async craftUnstakeTx(
173+
walletAddress: string,
174+
): Promise<Transaction> {
175+
176+
177+
try {
178+
const utxos = await this.getUtxos(walletAddress);
179+
const address = await this.client.addresses(walletAddress);
180+
if (!address.stake_address) {
181+
throw Error('No stake address');
154182
}
155183

156-
const amountToWithdrawLovelace = amountToWithdraw ? this.adaToLovelace(amountToWithdraw.toString()) : totalRewards.toString();
157-
withdrawals.insert(rewardAddress, BigNum.from_str(amountToWithdrawLovelace));
184+
const stakeKeyHash = await this.getStakeKeyHash(address.stake_address);
185+
if (!stakeKeyHash) {
186+
throw Error('Could not hash stake key');
187+
}
158188

159-
let walletBalance = 0;
160-
for(const utxo of utxos){
161-
if(utxo.amount.length > 0 && utxo.amount[0].unit === 'lovelace'){
162-
walletBalance += Number(utxo.amount[0].quantity);
163-
}
189+
const withdrawals = Withdrawals.new();
190+
const rewardAddress = RewardAddress.from_address(Address.from_bech32(address.stake_address));
191+
192+
if (!rewardAddress) {
193+
throw Error('Could not retrieve rewards address');
164194
}
165-
// Not sure about this value (might need to be BALANCE + REWARDS + FEES)
166-
const outAmount = (CARDANO_PARAMS.MIN_UTXO_VALUE_ADA_ONLY + totalRewards).toString();
195+
196+
197+
const availableRewards = await this.getAvailableRewards(address.stake_address);
198+
withdrawals.insert(rewardAddress, BigNum.from_str(availableRewards.toString()));
199+
200+
const walletBalance = this.getWalletBalance(utxos);
201+
const outAmount = (walletBalance - CARDANO_PARAMS.DEFAULT_NATIVE_FEES + Number(CARDANO_PARAMS.KEY_DEPOSIT) + availableRewards).toString();
167202
const outputs = this.prepareTx(outAmount, walletAddress);
168203

169-
const tx = await this.buildTx(walletAddress, utxos, outputs, null, withdrawals);
170-
return tx;
204+
// Deregister certificate
205+
const certificates = Certificates.new();
206+
certificates.add(
207+
Certificate.new_stake_deregistration(
208+
StakeDeregistration.new(
209+
StakeCredential.from_keyhash(
210+
Ed25519KeyHash.from_bytes(stakeKeyHash),
211+
),
212+
),
213+
),
214+
);
215+
216+
return await this.buildTx(walletAddress, utxos, outputs, certificates, withdrawals);
171217
} catch (error) {
172218
throw error;
173219
}
@@ -176,15 +222,15 @@ export class AdaService extends Service {
176222
/**
177223
* Prepare outputs (destination addresses and amounts) for a transaction
178224
* @param lovelaceValue
179-
* @param paymentAddress
225+
* @param toAddress
180226
* @private
181227
*/
182-
private prepareTx(lovelaceValue: string, paymentAddress: string): TransactionOutputs {
228+
private prepareTx(lovelaceValue: string, toAddress: string): TransactionOutputs {
183229
const outputs = TransactionOutputs.new();
184230

185231
outputs.add(
186232
TransactionOutput.new(
187-
Address.from_bech32(paymentAddress),
233+
Address.from_bech32(toAddress),
188234
Value.new(BigNum.from_str(lovelaceValue)),
189235
),
190236
);
@@ -194,15 +240,15 @@ export class AdaService extends Service {
194240

195241
/**
196242
* Build transaction with correct fees, inputs, outputs and certificates
197-
* @param changeAddress
243+
* @param inputAddress
198244
* @param utxos
199245
* @param outputs
200246
* @param certificates
201247
* @param withdrawals
202248
* @private
203249
*/
204250
private async buildTx(
205-
changeAddress: string,
251+
inputAddress: string,
206252
utxos: UTXO,
207253
outputs: TransactionOutputs,
208254
certificates: Certificates | null = null,
@@ -230,7 +276,7 @@ export class AdaService extends Service {
230276
txBuilder.set_certs(certificates);
231277
}
232278

233-
if(withdrawals){
279+
if (withdrawals) {
234280
txBuilder.set_withdrawals(withdrawals);
235281
}
236282

@@ -239,12 +285,8 @@ export class AdaService extends Service {
239285
(u: any) => !u.amount.find((a: any) => a.unit !== 'lovelace'),
240286
);
241287

242-
const unspentOutputs = TransactionUnspentOutputs.new();
243288
for (const utxo of lovelaceUtxos) {
244-
const amount = utxo.amount.find(
245-
(a: any) => a.unit === 'lovelace',
246-
)?.quantity;
247-
289+
const amount = utxo.amount.find(a => a.unit === 'lovelace')?.quantity;
248290
if (!amount) continue;
249291

250292
const inputValue = Value.new(
@@ -255,15 +297,9 @@ export class AdaService extends Service {
255297
TransactionHash.from_bytes(Buffer.from(utxo.tx_hash, 'hex')),
256298
utxo.output_index,
257299
);
258-
const output = TransactionOutput.new(Address.from_bech32(changeAddress), inputValue);
259-
unspentOutputs.add(TransactionUnspentOutput.new(input, output));
300+
txBuilder.add_input(Address.from_bech32(inputAddress), input, inputValue);
260301
}
261302

262-
txBuilder.add_inputs_from(
263-
unspentOutputs,
264-
CoinSelectionStrategyCIP2.LargestFirst,
265-
);
266-
267303
// Outputs
268304
txBuilder.add_output(outputs.get(0));
269305

@@ -276,9 +312,7 @@ export class AdaService extends Service {
276312
const ttl = currentSlot + 7200;
277313
txBuilder.set_ttl(ttl);
278314

279-
// Adds a change output if there are more ADA in utxo than we need for the transaction,
280-
// these coins will be returned to change address
281-
txBuilder.add_change_if_needed(Address.from_bech32(changeAddress));
315+
txBuilder.set_fee(BigNum.from_str(CARDANO_PARAMS.DEFAULT_NATIVE_FEES.toString()));
282316

283317
return txBuilder.build_tx();
284318
}
@@ -306,6 +340,40 @@ export class AdaService extends Service {
306340
return utxo;
307341
}
308342

343+
/**
344+
* Calculate wallet total balance from given utxo
345+
* @param utxos
346+
* @private
347+
*/
348+
private getWalletBalance(utxos: UTXO): number {
349+
let walletBalance = 0;
350+
for (const utxo of utxos) {
351+
if (utxo.amount.length > 0 && utxo.amount[0].unit === 'lovelace') {
352+
walletBalance += Number(utxo.amount[0].quantity);
353+
}
354+
}
355+
return walletBalance;
356+
}
357+
358+
/**
359+
* Get available rewards for given stake address
360+
* @param stakeAddress
361+
* @private
362+
*/
363+
private async getAvailableRewards(stakeAddress: string): Promise<number> {
364+
let availableRewards = 0;
365+
const rewardsHistory = await this.client.accountsRewardsAll(stakeAddress);
366+
for (const rewards of rewardsHistory) {
367+
availableRewards += Number(rewards.amount);
368+
}
369+
370+
const withdrawalsHistory = await this.client.accountsWithdrawalsAll(stakeAddress);
371+
for (const withdrawal of withdrawalsHistory) {
372+
availableRewards -= Number(withdrawal.amount);
373+
}
374+
return availableRewards;
375+
}
376+
309377
/**
310378
* Get stake key keyhash
311379
* @param stakeKey

0 commit comments

Comments
 (0)