@@ -11,11 +11,13 @@ type BlockfrostNetwork =
1111 | 'preprod'
1212 | 'sanchonet' ;
1313// prefixes based on CIP5 https://github.com/cardano-foundation/CIPs/blob/master/CIP-0005/CIP-0005.md
14- const Prefixes = Object . freeze ( {
14+ const PREFIXES = Object . freeze ( {
1515 ADDR : 'addr' ,
1616 ADDR_TEST : 'addr_test' ,
1717 STAKE : 'stake' ,
1818 STAKE_TEST : 'stake_test' ,
19+ STAKE_KEY_HASH : 'stake_vkh' ,
20+ STAKE_KEY : 'stake_vk' ,
1921 PAYMENT_KEY_HASH : 'addr_vkh' ,
2022 PAYMENT_KEY : 'addr_vk' ,
2123 POOL : 'pool' ,
@@ -47,8 +49,8 @@ export const validateStakeAddress = (
4749 const bech32Info = bech32 . decode ( input , 1000 ) ;
4850
4951 if (
50- ( bech32Info . prefix === Prefixes . STAKE && network === 'mainnet' ) ||
51- ( bech32Info . prefix === Prefixes . STAKE_TEST && network !== 'mainnet' )
52+ ( bech32Info . prefix === PREFIXES . STAKE && network === 'mainnet' ) ||
53+ ( bech32Info . prefix === PREFIXES . STAKE_TEST && network !== 'mainnet' )
5254 )
5355 return true ;
5456 else {
@@ -70,8 +72,8 @@ export const convertStakeAddress = (
7072 // if it's in hex, we'll convert it to Bech32
7173
7274 return network === 'mainnet'
73- ? bech32 . encode ( Prefixes . STAKE , words )
74- : bech32 . encode ( Prefixes . STAKE_TEST , words ) ;
75+ ? bech32 . encode ( PREFIXES . STAKE , words )
76+ : bech32 . encode ( PREFIXES . STAKE_TEST , words ) ;
7577 } catch {
7678 return undefined ;
7779 }
@@ -89,7 +91,7 @@ export const validateAndConvertPool = (input: string): string | undefined => {
8991 } else {
9092 const bech32Info = bech32 . decode ( input , 1000 ) ;
9193
92- return bech32Info . prefix === Prefixes . POOL ? input : undefined ;
94+ return bech32Info . prefix === PREFIXES . POOL ? input : undefined ;
9395 }
9496 } catch {
9597 return undefined ;
@@ -107,20 +109,20 @@ export const paymentCredFromBech32Address = (
107109 // compute paymentCred
108110 try {
109111 const bech32Info = bech32 . decode ( input , 1000 ) ;
110- if ( bech32Info . prefix === Prefixes . PAYMENT_KEY_HASH ) {
112+ if ( bech32Info . prefix === PREFIXES . PAYMENT_KEY_HASH ) {
111113 // valid payment_cred
112114 const payload = bech32 . fromWords ( bech32Info . words ) ;
113115 const paymentCred = `\\x${ Buffer . from ( payload ) . toString ( 'hex' ) } ` ;
114116
115117 return { paymentCred, prefix : bech32Info . prefix } ;
116- } else if ( bech32Info . prefix === Prefixes . PAYMENT_KEY ) {
118+ } else if ( bech32Info . prefix === PREFIXES . PAYMENT_KEY ) {
117119 // valid payment_cred
118120 const payload = bech32 . fromWords ( bech32Info . words ) ;
119121 const pubKey = PublicKey . from_hex ( Buffer . from ( payload ) . toString ( 'hex' ) ) ;
120122 const paymentKeyHash = `\\x${ pubKey . hash ( ) . to_hex ( ) } ` ;
121123 pubKey . free ( ) ;
122124 return { paymentCred : paymentKeyHash , prefix : bech32Info . prefix } ;
123- } else if ( bech32Info . prefix === Prefixes . SCRIPT ) {
125+ } else if ( bech32Info . prefix === PREFIXES . SCRIPT ) {
124126 const payload = bech32 . fromWords ( bech32Info . words ) ;
125127 const payloadHex = Buffer . from ( payload ) . toString ( 'hex' ) ;
126128 const paymentCred = `\\x${ payloadHex } ` ;
@@ -147,8 +149,8 @@ export const paymentCredToBech32Address = (
147149 const words = bech32 . toWords ( Buffer . from ( input , 'hex' ) ) ;
148150
149151 switch ( prefix ) {
150- case Prefixes . PAYMENT_KEY_HASH :
151- case Prefixes . SCRIPT :
152+ case PREFIXES . PAYMENT_KEY_HASH :
153+ case PREFIXES . SCRIPT :
152154 // add prefix to payment cred and encode it as bech32
153155 return bech32 . encode ( prefix , words ) ;
154156 default :
@@ -174,14 +176,14 @@ export const detectAndValidateAddressType = (
174176 // check if it's not shelley (also check network mismatch i.e. mainnet/testnet)
175177 const bech32Info = bech32 . decode ( input , 1000 ) ;
176178 if (
177- ( bech32Info . prefix === Prefixes . ADDR && network === 'mainnet' ) ||
178- ( bech32Info . prefix === Prefixes . ADDR_TEST && network !== 'mainnet' )
179+ ( bech32Info . prefix === PREFIXES . ADDR && network === 'mainnet' ) ||
180+ ( bech32Info . prefix === PREFIXES . ADDR_TEST && network !== 'mainnet' )
179181 ) {
180182 // valid shelley - addr1 for mainnet or addr_test1 for testnet
181183 return 'shelley' ;
182184 } else if (
183- bech32Info . prefix === Prefixes . PAYMENT_KEY_HASH ||
184- bech32Info . prefix === Prefixes . SCRIPT
185+ bech32Info . prefix === PREFIXES . PAYMENT_KEY_HASH ||
186+ bech32Info . prefix === PREFIXES . SCRIPT
185187 ) {
186188 // valid shelley - payment_cred
187189 return 'shelley' ;
@@ -239,6 +241,190 @@ export const scriptHashFromBech32Address = (
239241 }
240242} ;
241243
244+ const getStakeAddressHeaderByte = (
245+ type : 'keyHash' | 'scriptHash' ,
246+ network : BlockfrostNetwork ,
247+ ) => {
248+ const headerAddrType = type === 'keyHash' ? 0b1110 : 0b1111 ; // header for stake key hash/script hash
249+ const headerMainnet = 0b0001 ;
250+ const headerTestnet = 0b0000 ;
251+ const header =
252+ ( headerAddrType << 4 ) |
253+ ( network === 'mainnet' ? headerMainnet : headerTestnet ) ; // Combine nibbles
254+
255+ const headerBuff = Buffer . alloc ( 1 ) ; // Allocate a 1-byte buffer (adjust size as needed)
256+ headerBuff . writeUInt8 ( header , 0 ) ;
257+ return headerBuff ;
258+ } ;
259+
260+ /**
261+ * Constructs a Cardano stake address in db-sync format (bech32) from a stake credential.
262+ *
263+ * The function prepends the appropriate header byte (indicating key hash or script hash and network)
264+ * to the provided stake credential, then encodes the result as a bech32 stake address.
265+ *
266+ * @param stakeCred - Hex-encoded stake credential (key hash or script hash)
267+ * @param type - Type of credential: 'keyHash' or 'scriptHash'
268+ * @param network - Cardano network ('mainnet', 'testnet', etc.)
269+ * @returns Bech32-encoded stake address suitable for db-sync
270+ *
271+ * @example
272+ * getDbSyncStakeAddress('cda3khwqv60360rp5m7akt50m6ttapacs8rqhn5w342z7r35m37', 'scriptHash', 'mainnet');
273+ * // => 'stake1...'
274+ */
275+ export const getDbSyncStakeAddress = (
276+ stakeCred : string ,
277+ type : 'keyHash' | 'scriptHash' ,
278+ network : BlockfrostNetwork ,
279+ ) : string => {
280+ const headerBuff = getStakeAddressHeaderByte ( type , network ) ;
281+ const keyWithHeader = Buffer . concat ( [
282+ headerBuff ,
283+ Buffer . from ( stakeCred , 'hex' ) ,
284+ ] ) ;
285+
286+ const dbSyncAddr = bech32 . encode (
287+ network === 'mainnet' ? PREFIXES . STAKE : PREFIXES . STAKE_TEST ,
288+ bech32 . toWords ( keyWithHeader ) ,
289+ ) ;
290+ return dbSyncAddr ;
291+ } ;
292+
293+ /**
294+ * Validates and extracts the stake credential from a Cardano stake address or stake credential.
295+ *
296+ * Supported input formats:
297+ * - Stake address (bech32): stake1..., stake_test1...
298+ * - Stake key (bech32): stake_vk...
299+ * - Stake key hash (bech32): stake_vkh...
300+ * - Script hash (bech32): script...
301+ *
302+ * For stake addresses, the function checks the header byte to determine if the credential is a key hash or script hash.
303+ * For stake keys, it derives the key hash from the public key.
304+ * For script hashes, it extracts the hash directly.
305+ *
306+ * Returns an object containing:
307+ * - stakeCred: Hex-encoded stake credential (key hash or script hash)
308+ * - prefix: Bech32 prefix of the input
309+ * - type: 'keyHash' or 'scriptHash'
310+ * - dbSyncAddr: Stake address in db-sync format (bech32)
311+ *
312+ * Returns undefined if the input is invalid or not recognized.
313+ *
314+ * @param input - Bech32-encoded stake address or credential
315+ * @param network - Cardano network ('mainnet', 'testnet', etc.)
316+ * @returns Object with stakeCred, prefix, type, dbSyncAddr, or undefined if invalid
317+ */
318+ export const validateStakeCred = (
319+ input : string ,
320+ network : BlockfrostNetwork ,
321+ ) => {
322+ // Supported formats: stake, stake_test, stake_vkh, stake_vk, script
323+
324+ try {
325+ const { prefix, words } = bech32 . decode ( input , 1000 ) ;
326+
327+ switch ( prefix ) {
328+ case PREFIXES . STAKE :
329+ case PREFIXES . STAKE_TEST : {
330+ // valid payment_cred
331+ const payload = bech32 . fromWords ( words ) ;
332+
333+ // 1110.... stake key hash
334+ // 1111.... stake script hash
335+ const firstByte = payload [ 0 ] ;
336+ const addrTypeNibble = ( firstByte & 0xf0 ) >> 4 ; // Get first 4 bits
337+ let type = null ;
338+ switch ( addrTypeNibble ) {
339+ case 0b1110 :
340+ // stake key hash
341+ type = 'keyHash' ;
342+ break ;
343+ case 0b1111 :
344+ // stake script hash
345+ type = 'scriptHash' ;
346+ break ;
347+ default :
348+ return ;
349+ }
350+
351+ const stakeCred = Buffer . from ( payload ) . slice ( 1 ) . toString ( 'hex' ) ;
352+
353+ return { prefix : prefix , type, stakeCred, dbSyncAddr : input } ;
354+ }
355+ case PREFIXES . STAKE_KEY : {
356+ const payload = bech32 . fromWords ( words ) ;
357+ const pubKey = PublicKey . from_hex ( Buffer . from ( payload ) . toString ( 'hex' ) ) ;
358+ const stakeCredKeyHash = pubKey . hash ( ) . to_hex ( ) ;
359+ pubKey . free ( ) ;
360+
361+ const headerBuff = getStakeAddressHeaderByte ( 'keyHash' , network ) ;
362+ const keyWithHeader = Buffer . concat ( [
363+ headerBuff ,
364+ Buffer . from ( stakeCredKeyHash , 'hex' ) ,
365+ ] ) ;
366+
367+ const dbSyncAddr = bech32 . encode (
368+ network === 'mainnet' ? PREFIXES . STAKE : PREFIXES . STAKE_TEST ,
369+ bech32 . toWords ( keyWithHeader ) ,
370+ ) ;
371+
372+ return {
373+ stakeCred : stakeCredKeyHash ,
374+ prefix : prefix ,
375+ type : 'keyHash' ,
376+ dbSyncAddr,
377+ } ;
378+ }
379+ case PREFIXES . STAKE_KEY_HASH : {
380+ const payload = bech32 . fromWords ( words ) ;
381+ const stakeCred = Buffer . from ( payload ) . toString ( 'hex' ) ;
382+
383+ const headerBuff = getStakeAddressHeaderByte ( 'keyHash' , network ) ;
384+ const keyWithHeader = Buffer . concat ( [ headerBuff , Buffer . from ( payload ) ] ) ;
385+
386+ const dbSyncAddr = bech32 . encode (
387+ network === 'mainnet' ? PREFIXES . STAKE : PREFIXES . STAKE_TEST ,
388+ bech32 . toWords ( keyWithHeader ) ,
389+ ) ;
390+
391+ return {
392+ stakeCred : stakeCred ,
393+ prefix : prefix ,
394+ type : 'keyHash' ,
395+ dbSyncAddr,
396+ } ;
397+ }
398+ case PREFIXES . SCRIPT : {
399+ const payload = bech32 . fromWords ( words ) ;
400+ const stakeCred = Buffer . from ( payload ) . toString ( 'hex' ) ;
401+
402+ const headerBuff = getStakeAddressHeaderByte ( 'scriptHash' , network ) ;
403+ const keyWithHeader = Buffer . concat ( [ headerBuff , Buffer . from ( payload ) ] ) ;
404+
405+ const dbSyncAddr = bech32 . encode (
406+ network === 'mainnet' ? PREFIXES . STAKE : PREFIXES . STAKE_TEST ,
407+ bech32 . toWords ( keyWithHeader ) ,
408+ ) ;
409+
410+ return {
411+ stakeCred : stakeCred ,
412+ prefix : prefix ,
413+ type : 'scriptHash' ,
414+ dbSyncAddr,
415+ } ;
416+ }
417+ default : {
418+ return undefined ;
419+ }
420+ }
421+ } catch ( error ) {
422+ // Uncomment for awesome debug hack!
423+ console . error ( error ) ;
424+ return ;
425+ }
426+ } ;
427+
242428export const validatePositiveInRangeSignedInt = (
243429 possiblePositiveInt : string | number | undefined ,
244430) : boolean => {
0 commit comments