diff --git a/packages/staking/src/governance/helpers/parsing.test.ts b/packages/staking/src/governance/helpers/parsing.test.ts index 9a572c40a9..b8f4bcf76d 100644 --- a/packages/staking/src/governance/helpers/parsing.test.ts +++ b/packages/staking/src/governance/helpers/parsing.test.ts @@ -1,4 +1,4 @@ -import {convertHexKeyHashToBech32Format} from './parsing' +import {convertHexKeyHashToBech32Format, parseDrepId} from './parsing' import {init} from '@emurgo/cross-csl-nodejs' describe('convertHexKeyHashToBech32Format', () => { @@ -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', + }) + }) +}) diff --git a/packages/staking/src/governance/helpers/parsing.ts b/packages/staking/src/governance/helpers/parsing.ts index deecabf961..69a17fd0de 100644 --- a/packages/staking/src/governance/helpers/parsing.ts +++ b/packages/staking/src/governance/helpers/parsing.ts @@ -1,77 +1,126 @@ 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 => { + 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 => { + try { + await cardano.ScriptHash.fromBech32(drepId) + return true + } catch (e) { + return false } } -const getIsValidDrepKeyBech32Format = async ( +const isValidHexScriptHash = async ( drepId: string, cardano: CardanoTypes.Wasm, ): Promise => { 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 => { 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 => { @@ -79,7 +128,7 @@ const convertBech32ToKeyHash = async ( return await keyHash.toHex() } -const convertBech32ToScriptHash = async ( +const convertBech32ScriptHashToHex = async ( drepId: string, cardano: CardanoTypes.Wasm, ): Promise => { @@ -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') +} diff --git a/packages/staking/src/governance/manager.test.ts b/packages/staking/src/governance/manager.test.ts index c274193001..d6d38f39f6 100644 --- a/packages/staking/src/governance/manager.test.ts +++ b/packages/staking/src/governance/manager.test.ts @@ -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), @@ -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),