11import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs' ;
2- import { Transaction } from '@emurgo/cardano-serialization-lib-nodejs' ;
3- import { mnemonicToEntropy , generateMnemonic } from 'bip39' ;
2+ import {
3+ Address ,
4+ BigNum ,
5+ Certificate ,
6+ Certificates ,
7+ Ed25519KeyHash ,
8+ LinearFee , PublicKey ,
9+ RewardAddress ,
10+ StakeCredential ,
11+ StakeDelegation ,
12+ StakeRegistration ,
13+ Transaction ,
14+ TransactionBuilder ,
15+ TransactionBuilderConfigBuilder ,
16+ TransactionOutput ,
17+ TransactionOutputs ,
18+ Value , Vkey , VRFVKey ,
19+ } from '@emurgo/cardano-serialization-lib-nodejs' ;
20+ import { mnemonicToEntropy } from 'bip39' ;
421import { Service } from "./service" ;
522import { AdaStakeOptions , InternalAdaConfig , UTXO } from "../types/ada" ;
623import {
@@ -13,6 +30,10 @@ const CARDANO_PARAMS = {
1330 COINS_PER_UTXO_WORD : '34482' ,
1431 MAX_TX_SIZE : 16384 ,
1532 MAX_VALUE_SIZE : 5000 ,
33+ MIN_FEE_A : '44' ,
34+ MIN_FEE_B : '155381' ,
35+ POOL_DEPOSIT : '500000000' ,
36+ KEY_DEPOSIT : '2000000' ,
1637} ;
1738
1839export class AdaService extends Service {
@@ -34,41 +55,78 @@ export class AdaService extends Service {
3455 walletAddress : string ,
3556 options ?: AdaStakeOptions ,
3657 ) : Promise < Transaction > {
58+ const poolId = this . testnet ? 'pool1xjt9ylq7rvsd2mxf6njkzhrkhgjzrtflz039vxs66ntvv82rdky' : 'pool10rdglgh4pzvkf936p2m669qzarr9dusrhmmz9nultm3uvq4eh5k' ;
3759 const poolHash = this . testnet ? '3496527c1e1b20d56cc9d4e5615c76ba2421ad3f13e2561a1ad4d6c6' : '78da8fa2f5089964963a0ab7ad1402e8c656f203bef622cf9f5ee3c6' ;
38- const wasmWalletAddress = CardanoWasm . Address . from_bech32 ( walletAddress ) ;
39- const baseWalletAddress = CardanoWasm . BaseAddress . from_address ( wasmWalletAddress ) ;
40-
41- if ( ! baseWalletAddress ) {
42- throw new Error ( 'Could not generate base wallet address' ) ;
60+ const poolKeyHash = Ed25519KeyHash . from_hex ( poolHash ) ;
4361
44- }
45- let utxo : UTXO = [ ] ;
4662 try {
47- utxo = await this . client . addressesUtxosAll ( walletAddress ) ;
48- } catch ( error ) {
49- if ( error instanceof BlockfrostServerError && error . status_code === 404 ) {
50- throw new Error ( `You should send ADA to ${ walletAddress } to have enough funds to sent a transaction` ) ;
51- } else {
52- throw error ;
63+ const utxos = await this . getUtxos ( walletAddress ) ;
64+ const outputs = this . prepareTx ( CARDANO_PARAMS . KEY_DEPOSIT , walletAddress ) ;
65+ const address = await this . client . addresses ( walletAddress ) ;
66+ if ( ! address . stake_address ) {
67+ throw Error ( 'No stake address' ) ;
68+ }
69+ const stakeKeyHash = await this . getStakeKeyHash ( address . stake_address ) ;
70+ if ( ! stakeKeyHash ) {
71+ throw Error ( 'Could not hash stake key' ) ;
72+ }
73+ const certificates = Certificates . new ( ) ;
74+
75+ const registrations = await this . client . accountsRegistrations ( address . stake_address ) ;
76+ // const pool = await this.client.poolsById(poolId);
77+
78+ // Register stake key if not done already
79+ if ( registrations . length === 0 ) {
80+ certificates . add (
81+ Certificate . new_stake_registration (
82+ StakeRegistration . new (
83+ StakeCredential . from_keyhash (
84+ Ed25519KeyHash . from_bytes ( stakeKeyHash ) ,
85+ ) ,
86+ ) ,
87+ ) ,
88+ ) ;
5389 }
90+ certificates . add (
91+ Certificate . new_stake_delegation (
92+ StakeDelegation . new (
93+ StakeCredential . from_keyhash (
94+ Ed25519KeyHash . from_bytes ( stakeKeyHash ) ,
95+ ) ,
96+ poolKeyHash ,
97+ ) ,
98+ ) ,
99+ ) ;
100+ return await this . buildTx ( walletAddress , utxos , outputs , certificates ) ;
101+ } catch ( error ) {
102+ throw error ;
54103 }
104+ }
55105
56- const latestBlock = await this . client . blocksLatest ( ) ;
57- const currentSlot = latestBlock . slot ;
58- if ( ! currentSlot ) {
59- throw Error ( 'Failed to fetch slot number' ) ;
60- }
106+ private prepareTx ( lovelaceValue : string , paymentAddress : string ) : TransactionOutputs {
107+ const outputs = TransactionOutputs . new ( ) ;
61108
62- const txBuilder = CardanoWasm . TransactionBuilder . new (
63- CardanoWasm . TransactionBuilderConfigBuilder . new ( )
109+ outputs . add (
110+ TransactionOutput . new (
111+ Address . from_bech32 ( paymentAddress ) ,
112+ Value . new ( BigNum . from_str ( lovelaceValue ) ) ,
113+ ) ,
114+ ) ;
115+
116+ return outputs ;
117+ }
118+
119+ private async buildTx ( changeAddress : string , utxos : UTXO , outputs : TransactionOutputs , certificates : Certificates | null = null ) {
120+ const txBuilder = TransactionBuilder . new (
121+ TransactionBuilderConfigBuilder . new ( )
64122 . fee_algo (
65- CardanoWasm . LinearFee . new (
66- CardanoWasm . BigNum . from_str ( '44' ) ,
67- CardanoWasm . BigNum . from_str ( '155381' ) ,
123+ LinearFee . new (
124+ BigNum . from_str ( CARDANO_PARAMS . MIN_FEE_A ) ,
125+ BigNum . from_str ( CARDANO_PARAMS . MIN_FEE_B ) ,
68126 ) ,
69127 )
70- . pool_deposit ( CardanoWasm . BigNum . from_str ( '500000000' ) )
71- . key_deposit ( CardanoWasm . BigNum . from_str ( '2000000' ) )
128+ . pool_deposit ( CardanoWasm . BigNum . from_str ( CARDANO_PARAMS . POOL_DEPOSIT ) )
129+ . key_deposit ( CardanoWasm . BigNum . from_str ( CARDANO_PARAMS . KEY_DEPOSIT ) )
72130 . coins_per_utxo_word (
73131 CardanoWasm . BigNum . from_str ( CARDANO_PARAMS . COINS_PER_UTXO_WORD ) ,
74132 )
@@ -77,40 +135,15 @@ export class AdaService extends Service {
77135 . build ( ) ,
78136 ) ;
79137
80- // Set TTL to +2h from currentSlot
81- // If the transaction is not included in a block before that slot it will be cancelled.
82- const ttl = currentSlot + 7200 ;
83- txBuilder . set_ttl ( ttl ) ;
138+ if ( certificates ) {
139+ txBuilder . set_certs ( certificates ) ;
140+ }
84141
85- // const addresses = await this.client.addresses(walletAddress);
86- //
87- // // Add output
88- // txBuilder.add_output(
89- // CardanoWasm.TransactionOutput.new(
90- // wasmWalletAddress,
91- // CardanoWasm.Value.new(CardanoWasm.BigNum.from_str(addresses.amount[0].quantity.toString())),
92- // ),
93- // );
94-
95- // Add delegation (stake registration + stake delegation certificates)
96- const poolKeyHash = CardanoWasm . Ed25519KeyHash . from_hex ( poolHash ) ;
97- const stakeCredentials = baseWalletAddress . stake_cred ( ) ;
98- console . log ( stakeCredentials ) ;
99- const certificates = CardanoWasm . Certificates . new ( ) ;
100- const stakeRegistration = CardanoWasm . StakeRegistration . new ( stakeCredentials ) ;
101- const stakeDelegation = CardanoWasm . StakeDelegation . new ( stakeCredentials , poolKeyHash ) ;
102- const stakeRegistrationCertificate = CardanoWasm . Certificate . new_stake_registration ( stakeRegistration ) ;
103- const delegationCertificate = CardanoWasm . Certificate . new_stake_delegation ( stakeDelegation ) ;
104- certificates . add ( stakeRegistrationCertificate ) ;
105- certificates . add ( delegationCertificate ) ;
106- txBuilder . set_certs ( certificates ) ;
107-
108- // Filter out multi asset utxo to keep this simple
109- const lovelaceUtxos = utxo . filter (
142+ // Inputs
143+ const lovelaceUtxos = utxos . filter (
110144 ( u : any ) => ! u . amount . find ( ( a : any ) => a . unit !== 'lovelace' ) ,
111145 ) ;
112146
113- // Create TransactionUnspentOutputs from utxos fetched from Blockfrost
114147 const unspentOutputs = CardanoWasm . TransactionUnspentOutputs . new ( ) ;
115148 for ( const utxo of lovelaceUtxos ) {
116149 const amount = utxo . amount . find (
@@ -127,7 +160,7 @@ export class AdaService extends Service {
127160 CardanoWasm . TransactionHash . from_bytes ( Buffer . from ( utxo . tx_hash , 'hex' ) ) ,
128161 utxo . output_index ,
129162 ) ;
130- const output = CardanoWasm . TransactionOutput . new ( wasmWalletAddress , inputValue ) ;
163+ const output = CardanoWasm . TransactionOutput . new ( Address . from_bech32 ( changeAddress ) , inputValue ) ;
131164 unspentOutputs . add ( CardanoWasm . TransactionUnspentOutput . new ( input , output ) ) ;
132165 }
133166
@@ -136,14 +169,59 @@ export class AdaService extends Service {
136169 CardanoWasm . CoinSelectionStrategyCIP2 . LargestFirst ,
137170 ) ;
138171
172+ // Outputs
173+ txBuilder . add_output ( outputs . get ( 0 ) ) ;
174+
175+
176+ const latestBlock = await this . client . blocksLatest ( ) ;
177+ const currentSlot = latestBlock . slot ;
178+ if ( ! currentSlot ) {
179+ throw Error ( 'Failed to fetch slot number' ) ;
180+ }
181+ // Current slot + 2h
182+ const ttl = currentSlot + 7200 ;
183+ txBuilder . set_ttl ( ttl ) ;
184+
139185 // Adds a change output if there are more ADA in utxo than we need for the transaction,
140186 // these coins will be returned to change address
141- txBuilder . add_change_if_needed ( wasmWalletAddress ) ;
187+ txBuilder . add_change_if_needed ( Address . from_bech32 ( changeAddress ) ) ;
142188
143- // Build transaction
144189 return txBuilder . build_tx ( ) ;
145190 }
146191
192+ private adaToLovelace ( value : string ) {
193+ return ( parseFloat ( value || '1' ) * 1000000 ) . toFixed ( ) ;
194+ }
195+
196+ private hexToBytes ( string : string ) {
197+ return Buffer . from ( string , 'hex' ) ;
198+ }
199+
200+ private hexToBech32 ( address : string ) {
201+ return Address . from_bytes ( this . hexToBytes ( address ) ) . to_bech32 ( ) ;
202+ }
203+
204+ private async getUtxos ( walletAddress : string ) {
205+ let utxo : UTXO = [ ] ;
206+ try {
207+ utxo = await this . client . addressesUtxosAll ( walletAddress ) ;
208+ } catch ( error ) {
209+ if ( error instanceof BlockfrostServerError && error . status_code === 404 ) {
210+ throw new Error ( `You should send ADA to ${ walletAddress } to have enough funds to sent a transaction` ) ;
211+ } else {
212+ throw error ;
213+ }
214+ }
215+ return utxo ;
216+ }
217+
218+ private getStakeKeyHash ( stakeKey : string ) {
219+ const rewardAddress = RewardAddress . from_address ( Address . from_bech32 ( stakeKey ) ) ;
220+ const paymentCred = rewardAddress ?. payment_cred ( ) ;
221+ const hash = paymentCred ?. to_keyhash ( ) ;
222+ return hash ?. to_bytes ( ) ;
223+ }
224+
147225 /**
148226 * Sign transaction with given integration
149227 * @param integration
@@ -172,11 +250,11 @@ export class AdaService extends Service {
172250 {
173251 "content" : message ,
174252 } ,
175- ]
253+ ] ,
176254 } ,
177255 inputsSelection : {
178256 inputsToSpend : JSON . parse ( transaction . body ( ) . inputs ( ) . to_json ( ) ) ,
179- }
257+ } ,
180258 } ;
181259
182260 const fbTx = await this . fbSigner . signWithFB ( payload , this . testnet ? 'ADA_TEST' : 'ADA' ) ;
@@ -202,19 +280,19 @@ export class AdaService extends Service {
202280 } catch ( error ) {
203281 // submit could fail if the transactions is rejected by cardano node
204282 if ( error instanceof BlockfrostServerError && error . status_code === 400 ) {
205- console . log ( error . message ) ;
283+ console . log ( error . stack , error . error ) ;
206284 } else {
207285 // rethrow other errors
208286 throw error ;
209287 }
210288 }
211289 }
212290
213- private harden ( num : number ) : number {
291+ private harden ( num : number ) : number {
214292 return 0x80000000 + num ;
215293 } ;
216294
217- private deriveAddressPrvKey (
295+ private deriveAddressPrvKey (
218296 bipPrvKey : CardanoWasm . Bip32PrivateKey ,
219297 testnet : boolean ,
220298 ) : {
@@ -254,7 +332,7 @@ export class AdaService extends Service {
254332 return { signKey : utxoKey . to_raw_key ( ) , address : address } ;
255333 } ;
256334
257- private mnemonicToPrivateKey (
335+ private mnemonicToPrivateKey (
258336 mnemonic : string ,
259337 ) : CardanoWasm . Bip32PrivateKey {
260338 const entropy = mnemonicToEntropy ( mnemonic ) ;
0 commit comments