Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/utils/poolId.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {bech32} from 'bech32'

export const POOL_KEY_HASH_BYTES = 28
export const HEX_POOL_ID_LENGTH = POOL_KEY_HASH_BYTES * 2

export const classifyPoolId = (raw) => {
const value = typeof raw === 'string' ? raw.trim() : ''
if (!value) {
throw new Error('Pool ID is required')
}

const hexCandidate = value.replace(/^0x/i, '')
if (/^[0-9a-fA-F]+$/.test(hexCandidate)) {
if (hexCandidate.length !== HEX_POOL_ID_LENGTH) {
throw new Error('Hex pool key hash must be 56 characters')
}
return {format: 'hex', value: hexCandidate.toLowerCase()}
}

const bech32Candidate = value.toLowerCase()
if (bech32Candidate.startsWith('pool1')) {
let decoded
try {
decoded = bech32.decode(bech32Candidate)
} catch {
throw new Error('Bech32 pool ID is not a valid pool1… encoding')
}
if (decoded.prefix !== 'pool') {
throw new Error('Bech32 pool ID must use the pool prefix')
}
const payloadLength = bech32.fromWords(decoded.words).length
if (payloadLength !== POOL_KEY_HASH_BYTES) {
throw new Error('Bech32 pool ID must encode a 28-byte pool key hash')
}
return {format: 'bech32', value: bech32Candidate}
}
Comment thread
cursor[bot] marked this conversation as resolved.

throw new Error('Pool ID must be a bech32 pool1… ID or a 56-character hex pool key hash')
}
45 changes: 45 additions & 0 deletions src/utils/poolId.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {Buffer} from 'buffer'
import {bech32} from 'bech32'
import {classifyPoolId, HEX_POOL_ID_LENGTH} from './poolId'

const hex = 'deadbeef01234567890abcdef01234567890abcdef01234567890abc'
const validBech32 = bech32.encode('pool', bech32.toWords(Buffer.from(hex, 'hex')))

describe('classifyPoolId', () => {
it('rejects empty and whitespace-only input', () => {
expect(() => classifyPoolId('')).toThrow(/required/)
expect(() => classifyPoolId(' ')).toThrow(/required/)
expect(() => classifyPoolId(null)).toThrow(/required/)
})

it('accepts a 56-character hex pool key hash', () => {
expect(hex).toHaveLength(HEX_POOL_ID_LENGTH)
expect(classifyPoolId(hex)).toEqual({format: 'hex', value: hex})
})

it('strips a 0x prefix and lowercases hex', () => {
expect(classifyPoolId('0xDEADBEEF01234567890ABCDEF01234567890ABCDEF01234567890ABC')).toEqual({
format: 'hex',
value: hex,
})
})

it('rejects hex of the wrong length', () => {
expect(() => classifyPoolId('deadbeef')).toThrow(/56 characters/)
expect(() => classifyPoolId(`${'ab'.repeat(29)}`)).toThrow(/56 characters/)
})

it('accepts a valid pool1 bech32 ID', () => {
expect(classifyPoolId(` ${validBech32.toUpperCase()} `)).toEqual({format: 'bech32', value: validBech32})
})

it('rejects truncated, padded and checksum-invalid pool1 strings', () => {
expect(() => classifyPoolId('pool1qqqq')).toThrow(/valid pool1/)
expect(() => classifyPoolId(`${validBech32}q`)).toThrow(/valid pool1/)
expect(() => classifyPoolId('pool1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq')).toThrow(/valid pool1/)
})

it('rejects unrelated strings', () => {
expect(() => classifyPoolId('addr1xyz')).toThrow(/bech32 pool1/)
})
})
Loading