@radixdlt/account
Version:
A JavaScript client library for interacting with the Radix Distributed Ledger.
312 lines (272 loc) • 8.49 kB
text/typescript
import { err, ok, Result } from 'neverthrow'
import { buffersEquals, msgFromError } from '@radixdlt/util'
import { PublicKeyT, sha256Twice } from '@radixdlt/crypto'
import { HRP, hrpFullSuffixLength, Network } from '@radixdlt/primitives'
import { Bech32, Encoding } from '../bech32'
import { ResourceIdentifierT } from './_types'
const encoding = Encoding.BECH32
const maxLength: number | undefined = undefined // arbitrarily chosen
const versionByteNativeToken = 0x01
const versionByteNonNativeToken = 0x03
const hrpSuffixFromNetwork = (network: Network) => HRP[network].RRI_suffix
const networkFromHRPSuffix = (hrp: string): Result<Network, Error> =>
hrp === HRP.mainnet.RRI_suffix
? ok(Network.MAINNET)
: hrp === HRP.stokenet.RRI_suffix
? ok(Network.STOKENET)
: hrp === HRP.localnet.RRI_suffix
? ok(Network.LOCALNET)
: hrp === HRP.releasenet.RRI_suffix
? ok(Network.RELEASENET)
: hrp === HRP.rcnet.RRI_suffix
? ok(Network.RCNET)
: hrp === HRP.milestonenet.RRI_suffix
? ok(Network.MILESTONENET)
: hrp === HRP.testnet6.RRI_suffix
? ok(Network.TESTNET6)
: hrp === HRP.sandpitnet.RRI_suffix
? ok(Network.SANDPITNET)
: err(
new Error(
`Failed to parse network from HRP ${hrp} for ValidatorAddress.`,
),
)
const __create = (input: {
hash: Buffer
name: string
network: Network
toString: () => string
}): ResourceIdentifierT => ({
...input,
__witness: 'isRRI',
equals: (other): boolean => {
if (!isResourceIdentifier(other)) return false
const same =
other.name === input.name &&
buffersEquals(other.hash, input.hash) &&
input.network === other.network
if (same) {
if (other.toString() !== input.toString()) {
const errMsg = `ResourceIdentifiers believed to be equal, but return different values when calling toString, (this)'${input.toString()}' vs other: '${other.toString()}'`
console.error(errMsg)
throw new Error(errMsg)
}
}
return same
},
})
const fromBech32String = (
bechString: string,
): Result<ResourceIdentifierT, Error> => {
// const hrpSuffix = hrpBetanetSuffix // TODO make dependent on Network!
const decodingResult = Bech32.decode({ bechString, encoding, maxLength })
if (!decodingResult.isOk()) {
const errMsg = `Failed to Bech32 decode RRI, underlying error: ${msgFromError(
decodingResult.error,
)}`
return err(new Error(errMsg))
}
const decoded = decodingResult.value
const hrp = decoded.hrp
if (
!Object.keys(HRP).some(network =>
hrp.endsWith(HRP[network as Network].RRI_suffix),
)
) {
const errMsg = `suffix found for hrp "${hrp}" not supported.`
return err(new Error(errMsg))
}
const nameToValidate = hrp.split('_')[0]
const hrpSuffix = '_' + hrp.split('_')[1]
const networkResult = networkFromHRPSuffix(hrpSuffix)
if (!networkResult.isOk()) {
const errMsg = `Expected to get network from HRP suffix '${hrpSuffix}', but failed to get it.`
return err(new Error(errMsg))
}
const network = networkResult.value
const nameValidationResult = validateCharsInName(nameToValidate)
if (!nameValidationResult.isOk()) {
return err(nameValidationResult.error)
}
const name = nameValidationResult.value
const processed = decoded.data
const combinedDataResult = Bech32.convertDataFromBech32(processed)
if (!combinedDataResult.isOk()) {
const errMsg = `Failed to convertDataFromBech32 data, underlying error: ${msgFromError(
combinedDataResult.error,
)}`
console.error(errMsg)
return err(new Error(errMsg))
}
const combinedData = combinedDataResult.value
if (combinedData.length === 0) {
const errMsg = `The data part of RRI should NEVER be empty, must at least contain 1 version byte ('${versionByteNativeToken}' for native token, or '${versionByteNonNativeToken}' for other tokens)`
console.error(errMsg)
return err(new Error(errMsg))
}
const versionByte = combinedData[0]
if (
!(
versionByte === versionByteNativeToken ||
versionByte === versionByteNonNativeToken
)
) {
const errMsg = `The version byte must be either: '${versionByteNativeToken}' for native token, or '${versionByteNonNativeToken}' for other tokens, but got: ${versionByte}, bechString: '${bechString}'`
console.error(errMsg)
return err(new Error(errMsg))
}
const isNativeToken = versionByte === versionByteNativeToken
if (isNativeToken) {
if (combinedData.length > 1) {
const errMsg = `Expected data to be empty for native token, but got: #${
combinedData.length - 1 // minus 1 because we substract the 'versionByte'
} bytes`
console.error(errMsg)
return err(new Error(errMsg))
}
} else {
if (combinedData.length <= 1) {
const errMsg = `Expected data to be non empty for non native token`
console.error(errMsg)
return err(new Error(errMsg))
}
}
return ok(
__create({
hash: combinedData,
network,
name,
toString: () => bechString,
}),
)
}
const validateCharsInName = (name: string): Result<string, Error> => {
const regexLowerAlphaNumerics = new RegExp('^[a-z0-9]+$')
if (!regexLowerAlphaNumerics.test(name)) {
const errMsg = `Illegal characters found in name`
// console.error(errMsg)
return err(new Error(errMsg))
}
return ok(name)
}
const withNameRawDataAndVersionByte = (
input: Readonly<{
hash: Buffer
network: Network
versionByte: number
name: string
}>,
): Result<ResourceIdentifierT, Error> => {
const { versionByte, hash, network } = input
const hrpSuffix = hrpSuffixFromNetwork(network)
return validateCharsInName(input.name).andThen(name => {
const hrp = `${name}${hrpSuffix}`
const combinedData = Buffer.concat([Buffer.from([versionByte]), hash])
return Bech32.convertDataToBech32(combinedData)
.andThen(processed =>
Bech32.encode({
data: processed,
hrp,
encoding,
maxLength,
}),
)
.map(bech32 =>
__create({
hash,
network,
name,
toString: () => bech32.toString(),
}),
)
})
}
const systemRRIForNetwork = (
input: Readonly<{
name: string
network: Network
}>,
): Result<ResourceIdentifierT, Error> =>
withNameRawDataAndVersionByte({
...input,
versionByte: versionByteNativeToken,
hash: Buffer.alloc(0),
})
const hashByteCount = 26
const pkToHash = (
input: Readonly<{
name: string
publicKey: PublicKeyT
}>,
): Buffer => {
const { name, publicKey } = input
const nameBytes = Buffer.from(name, 'utf8')
const pubKeyBytes = publicKey.asData({ compressed: true })
const dataToHash = Buffer.concat([pubKeyBytes, nameBytes])
const hash = sha256Twice(dataToHash)
return hash.slice(-hashByteCount) // last bytes
}
const fromPublicKeyAndNameAndNetwork = (
input: Readonly<{
publicKey: PublicKeyT
name: string
network: Network
}>,
): Result<ResourceIdentifierT, Error> =>
withNameRawDataAndVersionByte({
...input,
versionByte: versionByteNonNativeToken,
hash: pkToHash(input),
})
const fromBuffer = (buffer: Buffer): Result<ResourceIdentifierT, Error> => {
if (buffer.length === 1 && buffer[0] === 0x01) {
return systemRRIForNetwork({
name: 'xrd',
network: Network.MAINNET, // Yikes!
})
}
return err(
new Error(
'Failed to create non XRD RRI because we do not have access to the HRP.',
),
)
}
export const isResourceIdentifier = (
something: ResourceIdentifierT | unknown,
): something is ResourceIdentifierT => {
const inspection = something as ResourceIdentifierT
return (
// inspection.hash !== undefined &&
inspection.__witness !== undefined &&
inspection.__witness === 'isRRI' &&
inspection.name !== undefined &&
inspection.toString !== undefined &&
inspection.equals !== undefined
)
}
export type ResourceIdentifierUnsafeInput = string | Buffer
export const isResourceIdentifierUnsafeInput = (
something: unknown,
): something is ResourceIdentifierUnsafeInput =>
typeof something === 'string' || Buffer.isBuffer(something)
export type ResourceIdentifierOrUnsafeInput =
| ResourceIdentifierT
| ResourceIdentifierUnsafeInput
export const isResourceIdentifierOrUnsafeInput = (
something: unknown,
): something is ResourceIdentifierOrUnsafeInput =>
isResourceIdentifier(something) ||
isResourceIdentifierUnsafeInput(something)
const fromUnsafe = (
input: ResourceIdentifierOrUnsafeInput,
): Result<ResourceIdentifierT, Error> =>
isResourceIdentifier(input)
? ok(input)
: typeof input === 'string'
? fromBech32String(input)
: fromBuffer(input)
export const ResourceIdentifier = {
systemRRIForNetwork,
fromPublicKeyAndNameAndNetwork,
fromUnsafe,
}