UNPKG

iso-filecoin

Version:

Isomorphic filecoin abstractions for RPC, signatures, address, token and wallet

334 lines (303 loc) 8.23 kB
import { blake2b } from '@noble/hashes/blake2b' import { keccak_256 } from '@noble/hashes/sha3' import { utf8 } from 'iso-base/utf8' import { concat } from 'iso-base/utils' import { KV } from 'iso-kv' import { MemoryDriver } from 'iso-kv/drivers/memory.js' import { mainnet, testnet } from './chains.js' /** * @typedef {import('./types').NetworkPrefix} NetworkPrefix */ /** * Signature types filecoin network has to sign transactions */ export const SIGNATURES = /** @type {const} */ ({ SECP256K1: 1, BLS: 3, }) /** * Filecoin network prefixes */ export const NETWORKS = /** @type {const} */ ({ mainnet: 'f', testnet: 't', }) /** * Get network prefix from network * * @param {import("./types.js").Network} network * @example * ```ts twoslash * import { getNetworkPrefix } from 'iso-filecoin/utils' * * const prefix = getNetworkPrefix('mainnet') * // => 'f' */ export function getNetworkPrefix(network) { return network === 'mainnet' ? 'f' : 't' } /** * Get network from prefix * * @param {NetworkPrefix} networkPrefix * @returns {import('./types').Network} * @example * ```ts twoslash * import { getNetwork } from 'iso-filecoin/utils' * * const network = getNetwork('f') * // => 'mainnet' */ export function getNetwork(networkPrefix) { return networkPrefix === 'f' ? 'mainnet' : 'testnet' } /** * Returns the third position from derivation path * * @param {string} path - path to parse * @returns {import('./types.js').Network} * @example * ```ts twoslash * import { getNetworkFromPath } from 'iso-filecoin/utils' * * const network = getNetworkFromPath("m/44'/461'/0'/0/0") * // => 'testnet' */ export function getNetworkFromPath(path) { const type = parseDerivationPath(path).coinType if (type === 1) { return 'testnet' } return 'mainnet' } /** * Get network from any chain designation * * @param {number | string} chainId */ export function getNetworkFromChainId(chainId) { switch (chainId) { case testnet.id: case testnet.chainId: case testnet.caipNetworkId: case 'testnet': return 'testnet' case mainnet.id: case mainnet.chainId: case mainnet.caipNetworkId: case 'mainnet': return 'mainnet' default: throw new Error(`Unknown chain id: ${chainId}`) } } /** * Derivation path from chain * * @param {import('./types').Network} network * @param {number} [index=0] - Account index (default 0) * @example * ```ts twoslash * import { pathFromNetwork } from 'iso-filecoin/utils' * * const path = pathFromNetwork('mainnet') * // => 'm/44'/461'/0'/0/0' */ export function pathFromNetwork(network, index = 0) { switch (network) { case 'mainnet': return `m/44'/461'/0'/0/${index}` case 'testnet': return `m/44'/1'/0'/0/${index}` default: throw new Error(`Unknown network: ${network}`) } } /** * Checks if the prefix is a valid network prefix * * @param {string} prefix * @returns {prefix is NetworkPrefix} * @example * ```ts twoslash * import { checkNetworkPrefix } from 'iso-filecoin/utils' * * checkNetworkPrefix('f') // true * checkNetworkPrefix('t') // true * checkNetworkPrefix('x') // false * ``` */ export function checkNetworkPrefix(prefix) { return Object.values(NETWORKS).includes(/** @type {NetworkPrefix} */ (prefix)) } export const BIP_32_PATH_REGEX = /^\d+'?$/u /** * Parse a derivation path into its components * * @see https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#path-levels * @param {string} path - The derivation path to parse * @returns {import('./types').DerivationPathComponents} An object containing the derivation path components * @example * ```ts twoslash * import { parseDerivationPath } from 'iso-filecoin/utils' * * const components = parseDerivationPath("m/44'/461'/0'/0/0") * // { * // purpose: 44, * // coinType: 461, * // account: 0, * // change: 0, * // addressIndex: 0 * // } * ``` */ export function parseDerivationPath(path) { const parts = path.split('/') if (parts.length !== 6) { throw new Error( "Invalid derivation path: depth must be 5 \"m / purpose' / coin_type' / account' / change / address_index\"" ) } if (parts[0] !== 'm') { throw new Error('Invalid derivation path: depth 0 must be "m"') } if (parts[1] !== "44'") { throw new Error( 'Invalid derivation path: The "purpose" node (depth 1) must be the string "44\'"' ) } if (!BIP_32_PATH_REGEX.test(parts[2]) || !parts[2].endsWith("'")) { throw new Error( 'Invalid derivation path: The "coin_type" node (depth 2) must be a hardened BIP-32 node.' ) } if (!BIP_32_PATH_REGEX.test(parts[3]) || !parts[3].endsWith("'")) { throw new Error( 'Invalid derivation path: The "account" node (depth 3) must be a hardened BIP-32 node.' ) } if (!BIP_32_PATH_REGEX.test(parts[4])) { throw new Error( 'Invalid derivation path: The "change" node (depth 4) must be a BIP-32 node.' ) } if (!BIP_32_PATH_REGEX.test(parts[5])) { throw new Error( 'Invalid derivation path: The "address_index" node (depth 5) must be a BIP-32 node.' ) } const purpose = Number.parseInt(parts[1], 10) const coinType = Number.parseInt(parts[2], 10) const account = Number.parseInt(parts[3], 10) const change = Number.parseInt(parts[4], 10) const addressIndex = Number.parseInt(parts[5], 10) if ( Number.isNaN(purpose) || Number.isNaN(coinType) || Number.isNaN(account) || Number.isNaN(change) || Number.isNaN(addressIndex) ) { throw new TypeError( 'Invalid derivation path: some of the components cannot be parsed as numbers' ) } return { purpose, coinType, account, change, addressIndex } } /** * Checksum ethereum address * * @param {string} address - Ethereum address * @returns {string} Checksummed ethereum address * @example * ```ts twoslash * import { checksumEthAddress } from 'iso-filecoin/utils' * * const address = '0xfb6916095ca1df60bb79ce92ce3ea74c37c5d359' * const checksummed = checksumEthAddress(address) * // => '0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359' * ``` */ export function checksumEthAddress(address) { const hexAddress = address.substring(2).toLowerCase() const hash = keccak_256(utf8.decode(hexAddress)) const addressArr = hexAddress.split('') for (let i = 0; i < 40; i += 2) { if (hash[i >> 1] >> 4 >= 8 && addressArr[i]) { addressArr[i] = addressArr[i].toUpperCase() } if ((hash[i >> 1] & 0x0f) >= 8 && addressArr[i + 1]) { addressArr[i + 1] = addressArr[i + 1].toUpperCase() } } const result = `0x${addressArr.join('')}` return result } const defaultDriver = new MemoryDriver() /** * Get cache instance from cache config * * @param {import('./types').Cache} cache - Cache config * @returns {import('iso-kv').KV} * @example * ```js * import { getCache } from 'iso-filecoin' * import { MemoryDriver } from 'iso-kv/drivers/memory.js' * * // use default memory driver * const cache = getCache(true) * * // use custom driver * const customCache = getCache(new MemoryDriver()) * ``` */ export function getCache(cache) { let kv if (cache === true || cache === undefined) { kv = new KV({ driver: defaultDriver }) } if ( typeof cache === 'object' && 'get' in cache && 'has' in cache && 'set' in cache ) { kv = new KV({ driver: cache }) } return kv ?? new KV({ driver: defaultDriver }) } /** * Create a Lotus CID from a BufferSource * * @param {Uint8Array} data * @example * ```js * import { lotusCid } from 'iso-filecoin/utils' * * const data = new Uint8Array([1, 2, 3]) * const cid = lotusCid(data) * ``` */ export function lotusCid(data) { return concat([ // cidv1 1byte + dag-cbor 1byte + blake2b-256 4bytes Uint8Array.from([0x01, 0x71, 0xa0, 0xe4, 0x02, 0x20]), blake2b(data, { dkLen: 32, }), ]) } /** * Check if an error is a ZodError * * @param {unknown} err * @returns {err is import('zod').ZodError} */ export function isZodErrorLike(err) { return ( err instanceof Error && err.name === 'ZodError' && 'issues' in err && Array.isArray(err.issues) ) }