@bigmi/core
Version:
TypeScript library for Bitcoin apps.
150 lines (123 loc) • 3.12 kB
text/typescript
import { sha256 } from '@noble/hashes/sha256'
import { type Decoded, bech32, bech32m } from 'bech32'
import bs58 from 'bs58'
import type { Address } from '../types/address.ts'
export enum UTXONetwork {
Mainnet = 'mainnet',
Testnet = 'testnet',
Regtest = 'regtest',
}
export enum UTXOAddressType {
p2pkh = 'p2pkh',
p2sh = 'p2sh',
p2wpkh = 'p2wpkh',
p2wsh = 'p2wsh',
p2tr = 'p2tr',
}
export type UTXOAddress = {
bech32: boolean
network: UTXONetwork
address: Address
type: UTXOAddressType
}
const addressTypes: {
[key: number]: { type: UTXOAddressType; network: UTXONetwork }
} = {
0: {
type: UTXOAddressType.p2pkh,
network: UTXONetwork.Mainnet,
},
111: {
type: UTXOAddressType.p2pkh,
network: UTXONetwork.Testnet,
},
5: {
type: UTXOAddressType.p2sh,
network: UTXONetwork.Mainnet,
},
196: {
type: UTXOAddressType.p2sh,
network: UTXONetwork.Testnet,
},
}
const parseBech32 = (address: Address): UTXOAddress => {
let decoded: Decoded
try {
if (
address.startsWith('bc1p') ||
address.startsWith('tb1p') ||
address.startsWith('bcrt1p')
) {
decoded = bech32m.decode(address)
} else {
decoded = bech32.decode(address)
}
} catch (_error) {
throw new Error('Invalid address')
}
const mapPrefixToNetwork: { [key: string]: UTXONetwork } = {
bc: UTXONetwork.Mainnet,
tb: UTXONetwork.Testnet,
bcrt: UTXONetwork.Regtest,
}
const network: UTXONetwork = mapPrefixToNetwork[decoded.prefix]
if (network === undefined) {
throw new Error('Invalid address')
}
const witnessVersion = decoded.words[0]
if (witnessVersion < 0 || witnessVersion > 16) {
throw new Error('Invalid address')
}
const data = bech32.fromWords(decoded.words.slice(1))
let type: UTXOAddressType
if (data.length === 20) {
type = UTXOAddressType.p2wpkh
} else if (witnessVersion === 1) {
type = UTXOAddressType.p2tr
} else {
type = UTXOAddressType.p2wsh
}
return {
bech32: true,
network,
address,
type,
}
}
export const getUTXOAddress = (address: Address): UTXOAddress => {
let decoded: Uint8Array
const prefix = address.substring(0, 2).toLowerCase()
if (prefix === 'bc' || prefix === 'tb') {
return parseBech32(address)
}
try {
decoded = bs58.decode(address)
} catch (_error) {
throw new Error('Invalid address')
}
const { length } = decoded
if (length !== 25) {
throw new Error('Invalid address')
}
const version = decoded[0]
const checksum = decoded.slice(length - 4, length)
const body = decoded.slice(0, length - 4)
const expectedChecksum = sha256(sha256(body)).slice(0, 4)
if (
checksum.some(
(value: number, index: number) => value !== expectedChecksum[index]
)
) {
throw new Error('Invalid address')
}
const validVersions = Object.keys(addressTypes).map(Number)
if (!validVersions.includes(version)) {
throw new Error('Invalid address')
}
const addressType = addressTypes[version]
return {
...addressType,
address,
bech32: false,
}
}