import * as dagJson from '@ipld/dag-json' import { sha256 } from '@noble/hashes/sha2.js' import { Logger } from 'besonders-logger' import { CID, digest as Digest } from 'multiformats' import { encode as multiformatsEncode } from 'multiformats/block' // import { encode } from 'multiformats/block'; import { Applog, ApplogEncNoCid, ApplogNoCid, ApplogOfSomeSort, CidString, IpnsString, isEncryptedApplog } from '../applog/datom-types.ts' import { base36 } from 'multiformats/bases/base36' import { sha256 as sha265Hasher } from 'multiformats/hashes/sha2' /* THIS FILE SHOULD NOT DEPEND ON UI STUFF, SO THAT TESTS CAN RUN WITH MINIMAL DEPENDENCIES */ const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO) // eslint-disable-line no-unused-vars export const MULTICODEC_IPNS_KEY = 0x72 export function prepareForPub(log: ApplogOfSomeSort, without: string[] = ['cid']) { if (!log) throw ERROR('falsy log', log) let cid = (log as Applog).cid if (isEncryptedApplog(log)) { if (!cid) cid = getCidSync(encodeBlock(log as ApplogEncNoCid).bytes).toString() WARN('preparing an encrypted applog - really?') return { log, cid } } const logWithout = {} for (let [key, val] of Object.entries(log)) { if (val === undefined) { WARN(`log.${key} is undefined, which is not allowed - encoding as null`, log) val = null } if (!without.includes(key)) { logWithout[key] = val // && key === 'pv' ? CID.parse(val) : val //HACK: disabled until clarified: https://discuss.ipfs.tech/t/pin-dag-with-open-ends/17612 } else { VERBOSE('excluding app log', { key, val }) } } return { log: logWithout as Applog, cid } } export function encodeApplogAndGetCid(log: ApplogNoCid) { return getCidSync(encodeApplog(log).bytes) } export function encodeApplog(log: ApplogNoCid | ApplogEncNoCid): { bytes: dagJson.ByteView; cid: CID } { return encodeBlock(prepareForPub(log)?.log) } export function getCidSync(bytes: dagJson.ByteView) { // Hacky way to use a sync sha265 lib to create a CID - code inspired by https://github.com/multiformats/js-multiformats#multihash-hashers const hash = sha256(bytes) const digest = Digest.create(sha265Hasher.code, hash) const cid = CID.create(1, dagJson.code, digest) VERBOSE(`[getCidSync]`, { bytes, hash, digest, cid }) return cid } /** encode the json object into an IPLD block */ export function encodeBlock(jsonObject: any): { bytes: dagJson.ByteView; cid: CID } { DEBUG('[encodeBlock]', jsonObject) try { const byteView = dagJson.encode(jsonObject) return { bytes: byteView, cid: getCidSync(byteView) } } catch (err) { throw ERROR('[encodeBlock] failed to encode:', jsonObject, err) } } export async function encodeBlockOriginal(jsonObject: any) { // HACK re-added this to verify the sync variant is sane const encoded = await multiformatsEncode({ value: jsonObject, codec: dagJson, hasher: sha265Hasher }) const syncVariant = encodeBlock(jsonObject) if (syncVariant.cid.toString() !== encoded.cid.toString()) { ERROR(`[encodeBlockOriginal] sync cid mismatch`, { jsonObject, encoded, syncVariant }) } return encoded } export function tryParseCID(cidString: CidString) { let cid: CID | null = null let errors = [] try { cid = CID.parse(cidString) } catch (err) { VERBOSE(`[retrieveThread] couldn't parse pubID with default base`) errors.push(err) } if (!cid) { try { cid = CID.parse(cidString, base36) // e.g. for IPNS } catch (err) { VERBOSE(`[retrieveThread] couldn't parse pubID with base36`) errors.push(err) } } return { cid, errors: cid ? null : errors, // we only care about errors if we failed to parse isIpns: cid && isIpnsKeyCid(cid), } } export function isIpnsKeyCid(cid: CID) { return cid.code === MULTICODEC_IPNS_KEY } export function cidToString(cid: CID) { if (cid.code == MULTICODEC_IPNS_KEY) { return toIpnsString(cid) } else { return cid.toString() } } export function toIpnsString(cid: CID) { if (cid.code !== MULTICODEC_IPNS_KEY) throw ERROR(`Not an IPNS cid (${cid.code}):`, cid.toString()) return cid.toString(base36) as IpnsString } export function ensureValidCIDinstance(cidOrStringA: CID | CidString) { return typeof cidOrStringA === 'string' ? CID.parse(cidOrStringA) : typeof cidOrStringA.toV1 != 'function' ? CID.decode(cidOrStringA.bytes) : cidOrStringA } export function areCidsEqual(cidOrStringA: CID | CidString, cidOrStringB: CID | CidString) { if (!cidOrStringA || !cidOrStringB) throw new Error(`[areCidsEqual] invalid params: ${cidOrStringA}, ${cidOrStringB}`) if (cidOrStringA === cidOrStringB) return true // shortcut if both are strings const cidA = ensureValidCIDinstance(cidOrStringA) const cidB = ensureValidCIDinstance(cidOrStringB) return cidA.toV1().toString() === cidB.toV1().toString() } export function containsCid(list: (CID | CidString)[] | Set, needle: CID | CidString) { if (list instanceof Set) return list.has(typeof needle === 'string' ? needle : needle.toV1().toString()) // ? what if the CidString is a different form? (parse and format would cost performance) return list.some(cidOrString => areCidsEqual(cidOrString, needle)) }