UNPKG

y-fdp-storage

Version:
519 lines (510 loc) 16.6 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { FdpStoragePersistence: () => FdpStoragePersistence, Feeds: () => feed_exports, SequentialFeed: () => SequentialFeed, TOPIC_BYTES_LENGTH: () => TOPIC_BYTES_LENGTH, TOPIC_HEX_LENGTH: () => TOPIC_HEX_LENGTH, UNCOMPRESSED_RECOVERY_ID: () => UNCOMPRESSED_RECOVERY_ID, assertSigner: () => assertSigner, defaultSign: () => defaultSign, isObject: () => isObject, isStrictlyObject: () => isStrictlyObject, makeBytes: () => makeBytes, makePrivateKeySigner: () => makePrivateKeySigner, makeSigner: () => makeSigner, readUint64BigEndian: () => readUint64BigEndian, serializeBytes: () => serializeBytes, writeUint64BigEndian: () => writeUint64BigEndian }); module.exports = __toCommonJS(src_exports); // src/adapter.ts var Y = __toESM(require("yjs")); // src/storage.ts var import_utils = require("ethers/lib/utils"); var FeedStorage = class { constructor(bee, feed, signer, topic, postageBatchId) { this.bee = bee; this.feed = feed; this.signer = signer; this.topic = topic; this.postageBatchId = postageBatchId; } /** * Reads the last state from the feed. * @returns contract state */ async storageRead() { const feedR = this.feed.makeFeedR(this.topic, this.signer.address); const last = await feedR.getLastUpdate(); const state = await this.bee.downloadData(last.reference); return { ...JSON.parse(state.text()), ...last }; } /** * Writes the state to the feed. * @param state The state to be written. * @returns void */ async storageWrite(state) { const feedRW = this.feed.makeFeedRW(this.topic, this.signer); const block = { state: (0, import_utils.hexlify)(state), timestamp: Date.now() }; const reference = await this.bee.uploadData(this.postageBatchId, JSON.stringify(block)); return feedRW.setLastUpdate(this.postageBatchId, reference.reference); } /** * Writes the state to the feed. * @param schemaFn The schema function to be used to encode the state. * @param state The state to be written. * @returns void */ async storageWriteWithSchema(schemaFn, state) { const feedRW = this.feed.makeFeedRW(this.topic, this.signer); let block; if (state) { block = schemaFn((0, import_utils.hexlify)(state)); } else { block = schemaFn(""); } const reference = await this.bee.uploadData(this.postageBatchId, JSON.stringify(block)); return feedRW.setLastUpdate(this.postageBatchId, reference.reference); } }; // src/adapter.ts var import_ethers = require("ethers"); // src/feeds/feed.ts var feed_exports = {}; __export(feed_exports, { FEED_TYPES: () => FEED_TYPES, assembleSocPayload: () => assembleSocPayload, extractDataFromSocPayload: () => extractDataFromSocPayload, fetchIndexToInt: () => fetchIndexToInt, makeTopic: () => makeTopic, mapSocToFeed: () => mapSocToFeed }); var import_bee_js2 = require("@ethersphere/bee-js"); // src/feeds/utils.ts var import_bee_js = require("@ethersphere/bee-js"); var import_elliptic = __toESM(require("elliptic")); var EC = import_elliptic.default.ec; var { assertBytes, hexToBytes, makeHexString, isBytes } = import_bee_js.Utils; var TOPIC_BYTES_LENGTH = 32; var TOPIC_HEX_LENGTH = 64; var UNCOMPRESSED_RECOVERY_ID = 27; function makeBytes(length) { return new Uint8Array(length); } function writeUint64BigEndian(value, bytes = makeBytes(8)) { const dataView = new DataView(bytes.buffer); const valueLower32 = value & 4294967295; dataView.setUint32(0, 0); dataView.setUint32(4, valueLower32); return bytes; } function publicKeyToAddress(pubKey) { const pubBytes = pubKey.encode("array", false); return import_bee_js.Utils.keccak256Hash(pubBytes.slice(1)).slice(12); } function hashWithEthereumPrefix(data) { const ethereumSignedMessagePrefix = `Ethereum Signed Message: ${data.length}`; const prefixBytes = new TextEncoder().encode(ethereumSignedMessagePrefix); return import_bee_js.Utils.keccak256Hash(prefixBytes, data); } function defaultSign(data, privateKey) { const curve = new EC("secp256k1"); const keyPair = curve.keyFromPrivate(privateKey); const hashedDigest = hashWithEthereumPrefix(data); const sigRaw = curve.sign(hashedDigest, keyPair, { canonical: true, pers: void 0 }); if (sigRaw.recoveryParam === null) { throw new Error("signDigest recovery param was null"); } const signature = new Uint8Array([ ...sigRaw.r.toArray("be", 32), ...sigRaw.s.toArray("be", 32), sigRaw.recoveryParam + UNCOMPRESSED_RECOVERY_ID ]); return signature; } function makeSigner(signer) { if (typeof signer === "string") { const hexKey = makeHexString(signer, 64); const keyBytes = hexToBytes(hexKey); return makePrivateKeySigner(keyBytes); } else if (signer instanceof Uint8Array) { assertBytes(signer, 32); return makePrivateKeySigner(signer); } assertSigner(signer); return signer; } function assertSigner(signer) { if (!isStrictlyObject(signer)) { throw new TypeError("Signer must be an object!"); } const typedSigner = signer; if (!isBytes(typedSigner.address, 20)) { throw new TypeError("Signer's address must be Uint8Array with 20 bytes!"); } if (typeof typedSigner.sign !== "function") { throw new TypeError("Signer sign property needs to be function!"); } } function makePrivateKeySigner(privateKey) { const curve = new EC("secp256k1"); const keyPair = curve.keyFromPrivate(privateKey); const address = publicKeyToAddress(keyPair.getPublic()); return { sign: (digest) => defaultSign(digest, privateKey), address }; } function readUint64BigEndian(bytes) { const dataView = new DataView(bytes.buffer); return dataView.getUint32(4); } function serializeBytes(...arrays) { const length = arrays.reduce((prev, curr) => prev + curr.length, 0); const buffer = new Uint8Array(length); let offset = 0; arrays.forEach((arr) => { buffer.set(arr, offset); offset += arr.length; }); return buffer; } function isStrictlyObject(value) { return isObject(value) && !Array.isArray(value); } function isObject(value) { return value !== null && typeof value === "object"; } // src/feeds/feed.ts var { assertBytes: assertBytes2, bytesToHex, hexToBytes: hexToBytes2, makeHexString: makeHexString2 } = import_bee_js2.Utils; var FEED_TYPES = ["sequential", "fault-tolerant-stream"]; function extractDataFromSocPayload(payload) { const timestamp = readUint64BigEndian(payload.slice(0, 8)); const p = payload.slice(8); if (p.length === 32 || p.length === 64) { return { timestamp, reference: p }; } throw new Error("NotImplemented: payload is longer than expected"); } function mapSocToFeed(socChunk, index) { const { reference, timestamp } = extractDataFromSocPayload(socChunk.payload()); return { ...socChunk, index, timestamp, reference: bytesToHex(reference) }; } function assembleSocPayload(reference, options) { const at = options?.at ?? Date.now() / 1e3; const timestamp = writeUint64BigEndian(at); return serializeBytes(timestamp, reference); } function fetchIndexToInt(fetchIndex) { const indexBytes = hexToBytes2(fetchIndex); let index = 0; for (let i = indexBytes.length - 1; i >= 0; i--) { const byte = indexBytes[i]; if (byte === 0) break; index += byte; } return index; } function makeTopic(topic) { if (typeof topic === "string") { return makeHexString2(topic, TOPIC_HEX_LENGTH); } else if (topic instanceof Uint8Array) { assertBytes2(topic, TOPIC_BYTES_LENGTH); return bytesToHex(topic, TOPIC_HEX_LENGTH); } throw new TypeError("invalid topic"); } // src/feeds/sequential-feed.ts var import_bee_js3 = require("@ethersphere/bee-js"); var { makeHexEthAddress, hexToBytes: hexToBytes3 } = import_bee_js3.Utils; var SequentialFeed = class { constructor(bee) { this.bee = bee; this.type = "sequential"; } /** * Creates a sequential feed reader * @param topic a swarm topic * @param owner owner * @returns a sequential feed reader */ makeFeedR(topic, owner) { const socReader = this.bee.makeSOCReader(owner); const topicHex = makeTopic(topic); const topicBytes = hexToBytes3(topicHex); const ownerHex = makeHexEthAddress(owner); const feedReader = this.bee.makeFeedReader("sequence", topicHex, owner); const getLastUpdate = async () => { const lastUpdate = await feedReader.download(); return lastUpdate; }; const getLastIndex = async () => { let index; try { const lastUpdate = await feedReader.download(); const { feedIndex } = lastUpdate; index = fetchIndexToInt(feedIndex); } catch (e) { index = -1; } return index; }; const findLastUpdate = async () => { const index = await getLastIndex(); const id = this.getIdentifier(topic, index); const socChunk = await socReader.download(id); return mapSocToFeed(socChunk, index); }; const getUpdate = async (index) => { const socChunk = await socReader.download(this.getIdentifier(topic, index)); return mapSocToFeed(socChunk, index); }; const getUpdates = async (indices) => { const promises = []; for (const index of indices) { promises.push(socReader.download(this.getIdentifier(topic, index))); } const socs = await Promise.all(promises); const feeds = socs.map((soc, orderIndex) => { return mapSocToFeed(soc, indices[orderIndex]); }); return feeds; }; return { type: "sequential", owner: ownerHex, topic: topicHex, findLastUpdate, getUpdate, getUpdates, getLastIndex, getLastUpdate }; } /** * Creates a sequential feed reader / writer * @param topic a swarm topic * @param signer signer * @returns a sequential feed reader / writer */ makeFeedRW(topic, signer) { const canonicalSigner = makeSigner(signer); const topicHex = makeTopic(topic); const topicBytes = hexToBytes3(topicHex); const feedR = this.makeFeedR(topic, canonicalSigner.address); const socWriter = this.bee.makeSOCWriter(canonicalSigner); const setUpdate = async (index, postageBatchId, reference) => { const identifier = this.getIdentifier(topicBytes, index); return socWriter.upload( postageBatchId, identifier, assembleSocPayload(hexToBytes3(reference)) //TODO metadata ); }; const setLastUpdate = async (postageBatchId, reference) => { let index; try { const lastIndex = await feedR.getLastIndex(); index = lastIndex + 1; } catch (e) { index = 0; } return setUpdate(index, postageBatchId, reference); }; return { ...feedR, setUpdate, setLastUpdate }; } /** * Get Single Owner Chunk identifier * @param topic a swarm topic, bytes 32 length * @param index the chunk index * @returns a bytes 32 */ getIdentifier(topic, index) { const indexBytes = writeUint64BigEndian(index); return import_bee_js3.Utils.keccak256Hash(topic, indexBytes); } }; // src/adapter.ts var import_utils4 = require("ethers/lib/utils"); var import_rxjs = require("rxjs"); var FdpStoragePersistence = class { /** * @param bee Bee instance * @param signer Signer instance * @param topic Topic * @param postageBatchId Postage batch id */ constructor(bee, signer, topic, postageBatchId) { this.bee = bee; this.signer = signer; this.topic = topic; this.postageBatchId = postageBatchId; this.onUpdates = new import_rxjs.Subject(); const hash = import_ethers.ethers.utils.id(topic).slice(2); const feed = new SequentialFeed(bee); this.stateStorage = new FeedStorage(bee, feed, signer, hash, postageBatchId); } /** * Writes the state to the feed. * @param schemaFn The schema function to be used to encode the state. * @param state The state to be written. * @returns void */ async storeWithSchema(schemaFn, state) { return this.stateStorage.storageWriteWithSchema(schemaFn, state); } /** * Converts a hex string to a Uint8Array. * @param hexstring hex string * @returns Uint8Array * */ fromHexToUint8Array(hexstring) { return (0, import_utils4.arrayify)(hexstring); } /** * Converts a Uint8Array to a hex string. * @param arr Uint8Array * @returns hex string * */ fromUint8ArrayToHex(arr) { return (0, import_utils4.hexlify)(arr); } /** * Writes the update to the feed. * @param update The update to be written. Handles exceptions internally. * @returns void */ async storeUpdate(update) { try { const current = await this.stateStorage.storageRead(); const temp = (0, import_utils4.arrayify)(current.state); const merged = Y.mergeUpdates([temp, update]); await this.stateStorage.storageWrite(merged); } catch (e) { await this.stateStorage.storageWrite(update); } } /** * Writes the update to the feed. * @param update The update to be written. Errors will be thrown if the update is not valid. * @returns void */ async store(update) { await this.stateStorage.storageWrite(update); } /** * Reads the last state from the feed. * @returns Uint8Array */ async read() { const updates = await this.stateStorage.storageRead(); return updates.state; } /** * Reads the last state from the feed. * @returns contract state */ async getYDoc() { const updates = await this.stateStorage.storageRead(); const doc = new Y.Doc(); Y.applyUpdate(doc, (0, import_utils4.arrayify)(updates.state)); return doc; } /** * Subscribes to the feed and emits updates. * @returns void * @emits {update: Uint8Array, timestamp: number, reference: string} **/ subscribe(doc, interval = 15e3) { const temp = setInterval(async () => { const updates = await this.stateStorage.storageRead(); Y.applyUpdate(doc, (0, import_utils4.arrayify)(updates.state)); this.onUpdates.next({ update: (0, import_utils4.arrayify)(updates.state), ...updates }); }, interval); return () => clearInterval(temp); } /** * Auto updates the feed. * @returns void * @emits {update: Uint8Array, timestamp: number, reference: string} **/ autoUpdate(doc, interval = 5e3) { const temp = setInterval(async () => { const update = Y.encodeStateAsUpdate(doc); await this.stateStorage.storageWrite(update); }, interval); return () => clearInterval(temp); } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { FdpStoragePersistence, Feeds, SequentialFeed, TOPIC_BYTES_LENGTH, TOPIC_HEX_LENGTH, UNCOMPRESSED_RECOVERY_ID, assertSigner, defaultSign, isObject, isStrictlyObject, makeBytes, makePrivateKeySigner, makeSigner, readUint64BigEndian, serializeBytes, writeUint64BigEndian }); //# sourceMappingURL=index.js.map