Skip to content

Commit

Permalink
feat(staking): Support more drep formats (#3732)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaeljscript authored Nov 11, 2024
1 parent 7855269 commit d29db9b
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 37 deletions.
73 changes: 72 additions & 1 deletion packages/staking/src/governance/helpers/parsing.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {convertHexKeyHashToBech32Format} from './parsing'
import {convertHexKeyHashToBech32Format, parseDrepId} from './parsing'
import {init} from '@emurgo/cross-csl-nodejs'

describe('convertHexKeyHashToBech32Format', () => {
Expand All @@ -13,3 +13,74 @@ describe('convertHexKeyHashToBech32Format', () => {
).toBe('drep1r73ah4wa3zqhw2fpnzyyj2lnya5zwjftkakgfk094y3mkerc53c')
})
})

describe('parseDrepId', () => {
const cardano = init('global')

it('should parse a key hash in bech32 format', async () => {
const result = await parseDrepId(
'drep1jnmmkfwpta0yuwjchw0gu6csh75vy62088egy9n67d0zc7sn83m',
cardano,
)
expect(result).toStrictEqual({
hash: '94f7bb25c15f5e4e3a58bb9e8e6b10bfa8c2694f39f282167af35e2c',
type: 'key',
})
})

it('should parse a key hash in base32 format', async () => {
const result = await parseDrepId(
'drep1y2m0g4r66pyaw3p7u454wc0p4f0ygm8ueaev0mgd3tvwm7sskqwqp',
cardano,
)
expect(result).toStrictEqual({
hash: 'b6f4547ad049d7443ee5695761e1aa5e446cfccf72c7ed0d8ad8edfa',
type: 'key',
})
})

it('should parse a key hash starting with 22 in hex format', async () => {
const result = await parseDrepId(
'22b6f4547ad049d7443ee5695761e1aa5e446cfccf72c7ed0d8ad8edfa',
cardano,
)
expect(result).toStrictEqual({
hash: 'b6f4547ad049d7443ee5695761e1aa5e446cfccf72c7ed0d8ad8edfa',
type: 'key',
})
})

it('should parse a drep_vkh1 key hash in bech32 format', async () => {
const result = await parseDrepId(
'drep_vkh1km69g7ksf8t5g0h9d9tkrcd2tezxelx0wtr76rv2mrkl549k89t',
cardano,
)

expect(result).toStrictEqual({
hash: 'b6f4547ad049d7443ee5695761e1aa5e446cfccf72c7ed0d8ad8edfa',
type: 'key',
})
})

it('should parse a script hash in bech32 format', async () => {
const result = await parseDrepId(
'drep_script18cgl8kdnjculhww4n3h0a3ahc85ahjcsg53u0f93jnz9c0339av',
cardano,
)
expect(result).toStrictEqual({
hash: '3e11f3d9b39639fbb9d59c6efec7b7c1e9dbcb104523c7a4b194c45c',
type: 'script',
})
})

it('should parse a script hash starting with 23 in hex format', async () => {
const result = await parseDrepId(
'233e11f3d9b39639fbb9d59c6efec7b7c1e9dbcb104523c7a4b194c45c',
cardano,
)
expect(result).toStrictEqual({
hash: '3e11f3d9b39639fbb9d59c6efec7b7c1e9dbcb104523c7a4b194c45c',
type: 'script',
})
})
})
126 changes: 92 additions & 34 deletions packages/staking/src/governance/helpers/parsing.ts
Original file line number Diff line number Diff line change
@@ -1,85 +1,134 @@
import {CardanoTypes} from '../../types'
import {bech32 as bech32Module} from 'bech32'

export const parseDrepId = async (
drepId: string,
cardano: CardanoTypes.Wasm,
): Promise<{type: 'key'; hash: string} | {type: 'script'; hash: string}> => {
const keyPrefix = drepId.startsWith('drep1')
const scriptPrefix = drepId.startsWith('drep_script1')
const isPotentiallyValidHex = /^(22|23)[0-9a-fA-F]{56}$/.test(drepId)

if (!keyPrefix && !scriptPrefix)
throw new Error(
'Invalid DRep ID. Must have a valid key or script bech32 format',
)
if (
drepId.startsWith('drep_vkh1') &&
(await isValidBech32KeyHash(drepId, cardano))
) {
return {
type: 'key',
hash: await convertBech32KeyHashToHex(drepId, cardano),
}
}

if (keyPrefix) {
const isValidDrepKeyHashBech32Format = await getIsValidDrepKeyBech32Format(
drepId,
cardano,
)
if (
drepId.startsWith('drep1') &&
(await isValidBech32KeyHash(drepId, cardano))
) {
return {
type: 'key',
hash: await convertBech32KeyHashToHex(drepId, cardano),
}
}

if (!isValidDrepKeyHashBech32Format)
throw new Error('Invalid key DRep ID. Must have a valid bech32 format')
if (
drepId.startsWith('drep_script1') &&
(await isValidBech32ScriptHash(drepId, cardano))
) {
return {
type: 'script',
hash: await convertBech32ScriptHashToHex(drepId, cardano),
}
}

const keyHash = await convertBech32ToKeyHash(drepId, cardano)
const type = 'key'
if (drepId.startsWith('drep1') && drepId.length === 58) {
const base32Parsed = base32ToHex(drepId)
if (!base32Parsed) {
throw new Error('Invalid key DRep ID. Must have 58 hex characters')
}
return parseDrepId(base32Parsed, cardano)
}

if (
isPotentiallyValidHex &&
drepId.startsWith('22') &&
(await isValidHexKeyHash(drepId.substr(2), cardano))
) {
return {
type,
hash: keyHash,
type: 'key',
hash: drepId.substr(2),
}
}

const isValidDrepScriptBech32Format = await getIsValidDrepScriptBech32Format(
drepId,
cardano,
)
if (
isPotentiallyValidHex &&
drepId.startsWith('23') &&
(await isValidHexScriptHash(drepId.substr(2), cardano))
) {
return {
type: 'script',
hash: drepId.substr(2),
}
}

if (!isValidDrepScriptBech32Format)
throw new Error('Invalid script DRep ID. Must have a valid bech32 format')
throw new Error(
'Invalid DRep ID. Must have a valid key or script bech32 format',
)
}

const scriptHash = await convertBech32ToScriptHash(drepId, cardano)
const type = 'script'
const isValidBech32KeyHash = async (
drepId: string,
cardano: CardanoTypes.Wasm,
): Promise<boolean> => {
try {
await cardano.Ed25519KeyHash.fromBech32(drepId)
return true
} catch (e) {
return false
}
}

return {
type,
hash: scriptHash,
const isValidBech32ScriptHash = async (
drepId: string,
cardano: CardanoTypes.Wasm,
): Promise<boolean> => {
try {
await cardano.ScriptHash.fromBech32(drepId)
return true
} catch (e) {
return false
}
}

const getIsValidDrepKeyBech32Format = async (
const isValidHexScriptHash = async (
drepId: string,
cardano: CardanoTypes.Wasm,
): Promise<boolean> => {
try {
await cardano.Ed25519KeyHash.fromBech32(drepId)
await cardano.ScriptHash.fromHex(drepId)
return true
} catch (e) {
return false
}
}

const getIsValidDrepScriptBech32Format = async (
const isValidHexKeyHash = async (
drepId: string,
cardano: CardanoTypes.Wasm,
): Promise<boolean> => {
try {
await cardano.ScriptHash.fromBech32(drepId)
await cardano.Ed25519KeyHash.fromHex(drepId)
return true
} catch (e) {
return false
}
}

const convertBech32ToKeyHash = async (
const convertBech32KeyHashToHex = async (
drepId: string,
cardano: CardanoTypes.Wasm,
): Promise<string> => {
const keyHash = await cardano.Ed25519KeyHash.fromBech32(drepId)
return await keyHash.toHex()
}

const convertBech32ToScriptHash = async (
const convertBech32ScriptHashToHex = async (
drepId: string,
cardano: CardanoTypes.Wasm,
): Promise<string> => {
Expand All @@ -94,3 +143,12 @@ export const convertHexKeyHashToBech32Format = async (
const keyHash = await cardano.Ed25519KeyHash.fromHex(drepId)
return await keyHash.toBech32('drep')
}

const base32ToHex = (base32: string): string | null => {
const base32Words = bech32Module.decodeUnsafe(base32, base32.length)
return base32Words?.words ? convertBase32ToHex(base32Words.words) : null
}

const convertBase32ToHex = (words: number[]): string => {
return Buffer.from(bech32Module.fromWords(words)).toString('hex')
}
4 changes: 2 additions & 2 deletions packages/staking/src/governance/manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ describe('createGovernanceManager', () => {
'drep_script1r73ah4wa3zqhw2fpnzyyj2lnya5zwjftkakgfk094y3mkerc53c'

const errorMessage =
'Invalid script DRep ID. Must have a valid bech32 format'
'Invalid DRep ID. Must have a valid key or script bech32 format'

await expect(() =>
governanceManager.validateDRepID(invalidbech32Address),
Expand All @@ -118,7 +118,7 @@ describe('createGovernanceManager', () => {
'drep1r73ah4wa3zqhw2fpnzyyj2lnya5zwjftkakgfk094y3mkerc53cs'

const errorMessage =
'Invalid key DRep ID. Must have a valid bech32 format'
'Invalid DRep ID. Must have a valid key or script bech32 format'

await expect(() =>
governanceManager.validateDRepID(invalidbech32Address),
Expand Down

0 comments on commit d29db9b

Please sign in to comment.