diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 7579c3f..46aeb85 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -19,6 +19,7 @@ module.exports = { plugins: ['@typescript-eslint', 'simple-import-sort'], rules: { '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/explicit-function-return-type': 'error', '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-unused-vars': [ 'warn', diff --git a/jest.config.ts b/jest.config.ts index f2420da..2c4c014 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -4,6 +4,11 @@ module.exports = { transform: { '^.+\\.tsx?$': 'ts-jest', }, - testMatch: ['**/tests/**/*.test.ts'], + rootDir: 'tests', + testMatch: ['**/tests/**/*.spec.ts'], moduleFileExtensions: ['ts', 'js'], + testTimeout: 50000, + globals: { + window: {}, + }, }; diff --git a/package-lock.json b/package-lock.json index 2ebf6f6..3515b74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,9 @@ "license": "MIT", "dependencies": { "@ethersphere/bee-js": "^8.3.1", - "@solarpunkltd/mantaray-js": "1.1.0" + "@solarpunkltd/mantaray-js": "1.1.0", + "@upcoming/bee-js": "^0.3.0", + "cafe-utility": "^21.5.0" }, "devDependencies": { "@babel/preset-env": "^7.26.0", @@ -42,50 +44,6 @@ "node": ">=14" } }, - "../mantaray-js": { - "name": "@solarpunkltd/mantaray-js", - "version": "1.1.0", - "license": "BSD-3-Clause", - "dependencies": { - "get-random-values": "^1.2.2", - "js-sha3": "^0.8.0" - }, - "devDependencies": { - "@babel/core": "^7.26.0", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-transform-runtime": "^7.25.9", - "@babel/plugin-transform-typescript": "^7.26.5", - "@babel/preset-env": "^7.26.0", - "@babel/preset-typescript": "^7.26.0", - "@ethersphere/bee-factory": "^0.5.2", - "@ethersphere/bee-js": "^8.3.1", - "@jest/types": "^29.6.3", - "@types/jest": "^29.5.14", - "@types/terser-webpack-plugin": "^5.0.4", - "@types/webpack-bundle-analyzer": "^4.7.0", - "@typescript-eslint/eslint-plugin": "^6.14.0", - "@typescript-eslint/parser": "^6.14.0", - "babel-jest": "^29.7.0", - "babel-loader": "^9.2.1", - "eslint": "^8.55.0", - "eslint-config-prettier": "^8.10.0", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-jest": "^28.10.0", - "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-react-refresh": "^0.4.5", - "eslint-plugin-simple-import-sort": "^10.0.0", - "eslint-plugin-unused-imports": "^4.1.4", - "jest": "^29.7.0", - "prettier": "^2.8.8", - "rimraf": "^6.0.1", - "ts-jest": "^29.2.5", - "ts-node": "^10.9.2", - "typescript": "^5.2.2", - "webpack": "^5.97.1", - "webpack-bundle-analyzer": "^4.10.2", - "webpack-cli": "^6.0.1" - } - }, "node_modules/@adraffy/ens-normalize": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", @@ -123,9 +81,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.3.tgz", - "integrity": "sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz", + "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==", "dev": true, "license": "MIT", "engines": { @@ -164,14 +122,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", - "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", + "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.3", - "@babel/types": "^7.26.3", + "@babel/parser": "^7.26.5", + "@babel/types": "^7.26.5", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -194,13 +152,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", - "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.25.9", + "@babel/compat-data": "^7.26.5", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -327,9 +285,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", - "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", "dev": true, "license": "MIT", "engines": { @@ -355,15 +313,15 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", - "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz", + "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/traverse": "^7.26.5" }, "engines": { "node": ">=6.9.0" @@ -446,13 +404,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", - "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.5.tgz", + "integrity": "sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.26.3" + "@babel/types": "^7.26.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -883,13 +841,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz", - "integrity": "sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz", + "integrity": "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.26.5" }, "engines": { "node": ">=6.9.0" @@ -1303,13 +1261,13 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.9.tgz", - "integrity": "sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog==", + "version": "7.26.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz", + "integrity": "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.26.5" }, "engines": { "node": ">=6.9.0" @@ -1601,15 +1559,15 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.26.3.tgz", - "integrity": "sha512-6+5hpdr6mETwSKjmJUdYw0EIkATiQhnELWlE3kJFBwSg/BGIVwVaVbX+gOXBCdc7Ln1RXZxyWGecIXhUfnl7oA==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.26.5.tgz", + "integrity": "sha512-GJhPO0y8SD5EYVCy2Zr+9dSZcEgaSmq5BLR0Oc25TOEhC+ba49vUAGZFjy8v79z9E1mdldq4x9d1xgh4L1d5dQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-plugin-utils": "^7.26.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", "@babel/plugin-syntax-typescript": "^7.25.9" }, @@ -1835,17 +1793,17 @@ } }, "node_modules/@babel/traverse": { - "version": "7.26.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", - "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.5.tgz", + "integrity": "sha512-rkOSPOw+AXbgtwUga3U4u8RpoK9FEFWBNAlTpcnkLFjL5CT+oyHNuUUC/xx6XefEJ16r38r8Bc/lfp6rYuHeJQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.3", - "@babel/parser": "^7.26.3", + "@babel/generator": "^7.26.5", + "@babel/parser": "^7.26.5", "@babel/template": "^7.25.9", - "@babel/types": "^7.26.3", + "@babel/types": "^7.26.5", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -1854,9 +1812,9 @@ } }, "node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.5.tgz", + "integrity": "sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==", "dev": true, "license": "MIT", "dependencies": { @@ -2033,6 +1991,12 @@ "beeApiVersion": "7.1.0" } }, + "node_modules/@ethersphere/bee-js/node_modules/cafe-utility": { + "version": "23.12.0", + "resolved": "https://registry.npmjs.org/cafe-utility/-/cafe-utility-23.12.0.tgz", + "integrity": "sha512-B8MHryv6dDTw8GRfJxHLy4zzhewEEYulPAXiSRqkNCeqXFoQAk8THhlU00Yk7dvc8bppnHoS7FaQ468dfGfe6A==", + "license": "MIT" + }, "node_modules/@ethersphere/bee-js/node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -2668,8 +2632,14 @@ } }, "node_modules/@solarpunkltd/mantaray-js": { - "resolved": "../mantaray-js", - "link": true + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@solarpunkltd/mantaray-js/-/mantaray-js-1.1.0.tgz", + "integrity": "sha512-NU56cNHYwzqrKQXwhkl97y/GRYB1L7ZtcaI34nhiaCjM9dK26C6oSBGqTcw6lnUoGRVt+JtHfifYyCHNsJwenQ==", + "license": "BSD-3-Clause", + "dependencies": { + "get-random-values": "^1.2.2", + "js-sha3": "^0.8.0" + } }, "node_modules/@tsconfig/node10": { "version": "1.0.11", @@ -2817,9 +2787,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", - "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", + "version": "22.10.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz", + "integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==", "dev": true, "license": "MIT", "dependencies": { @@ -3101,6 +3071,41 @@ "dev": true, "license": "ISC" }, + "node_modules/@upcoming/bee-js": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@upcoming/bee-js/-/bee-js-0.3.0.tgz", + "integrity": "sha512-4ut4g+D3VEhyCoDLyYkG0FsOE7pt1EAW97M6v+FP2NmjB6gp7zANkDxV2yiHfKRpWG0uCkWc4/dKBqz+jsAJxQ==", + "license": "BSD-3-Clause", + "dependencies": { + "axios": "^0.28.1", + "cafe-utility": "^27.6.0", + "isomorphic-ws": "^4.0.1", + "semver": "^7.3.5", + "ws": "^8.7.0" + }, + "engines": { + "bee": "2.4.0-390a402e", + "beeApiVersion": "7.2.0" + } + }, + "node_modules/@upcoming/bee-js/node_modules/cafe-utility": { + "version": "27.6.0", + "resolved": "https://registry.npmjs.org/cafe-utility/-/cafe-utility-27.6.0.tgz", + "integrity": "sha512-+AlzGhUkCywTfHnwSKSGwTLzqUyrJaCBGdmLgujgX/pYTqqEgh1sYyiEWTNQt3PIoR887rO550XKm3/AapSxWQ==", + "license": "MIT" + }, + "node_modules/@upcoming/bee-js/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -3664,9 +3669,9 @@ "license": "MIT" }, "node_modules/cafe-utility": { - "version": "23.12.0", - "resolved": "https://registry.npmjs.org/cafe-utility/-/cafe-utility-23.12.0.tgz", - "integrity": "sha512-B8MHryv6dDTw8GRfJxHLy4zzhewEEYulPAXiSRqkNCeqXFoQAk8THhlU00Yk7dvc8bppnHoS7FaQ468dfGfe6A==", + "version": "21.5.0", + "resolved": "https://registry.npmjs.org/cafe-utility/-/cafe-utility-21.5.0.tgz", + "integrity": "sha512-5u+9cf7fvcH3j2Q3jrd7nA3bUITUBj8b9Arg4eA6almqeA5+dwQA6NKba4GnW6zS9uL1iVCEQqM3z3tQVs2Xjw==", "license": "MIT" }, "node_modules/call-bind": { @@ -3740,9 +3745,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001692", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz", - "integrity": "sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==", + "version": "1.0.30001695", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001695.tgz", + "integrity": "sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==", "dev": true, "funding": [ { @@ -4152,6 +4157,11 @@ "node": ">=6.0.0" } }, + "node_modules/dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4184,9 +4194,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.80", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.80.tgz", - "integrity": "sha512-LTrKpW0AqIuHwmlVNV+cjFYTnXtM9K37OGhpe0ZI10ScPSxqVSryZHIY3WnCS5NSYbBODRTZyhRMS2h5FAEqAw==", + "version": "1.5.83", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.83.tgz", + "integrity": "sha512-LcUDPqSt+V0QmI47XLzZrz5OqILSMGsPFkDYus22rIbgorSvBYEFqq854ltTmUdHkY92FSdAAvsh4jWEULMdfQ==", "dev": true, "license": "ISC" }, @@ -4322,9 +4332,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, "license": "MIT", "dependencies": { @@ -4858,9 +4868,9 @@ } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.16.tgz", - "integrity": "sha512-slterMlxAhov/DZO8NScf6mEeMBBXodFUolijDvrtTxyezyLoTQaa73FyYus/VbTdftd8wBgBxPMRk3poleXNQ==", + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.18.tgz", + "integrity": "sha512-IRGEoFn3OKalm3hjfolEWGqoF/jPqeEYFp+C8B0WMzwGwBMvlRDQd06kghDhF0C61uJ6WfSDhEZE/sAQjduKgw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -5399,21 +5409,6 @@ "dev": true, "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -5524,6 +5519,18 @@ "node": ">= 0.4" } }, + "node_modules/get-random-values": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-random-values/-/get-random-values-1.2.2.tgz", + "integrity": "sha512-lMyPjQyl0cNNdDf2oR+IQ/fM3itDvpoHy45Ymo2r0L1EjazeSl13SfbKZs7KtZ/3MDCeueiaJiuOEfKqRTsSgA==", + "license": "MIT", + "dependencies": { + "global": "^4.4.0" + }, + "engines": { + "node": "10 || 12 || >=14" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -5614,6 +5621,16 @@ "node": "*" } }, + "node_modules/global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "license": "MIT", + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -7380,6 +7397,14 @@ "node": ">=6" } }, + "node_modules/min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", + "dependencies": { + "dom-walk": "^0.1.0" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -7925,6 +7950,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", diff --git a/package.json b/package.json index 18b2774..509fae3 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "dist/fileManager.js", "scripts": { "build": "tsc", - "test": "node --loader ts-node/esm node_modules/jest/bin/jest.js", + "test": "jest --config=jest.config.ts --runInBand --verbose --detectOpenHandles", "test:coverage": "jest --coverage", "start": "npm run build && node dist/index.js", "lint": "eslint . --ext ts --report-unused-disable-directives", @@ -23,7 +23,9 @@ "license": "MIT", "dependencies": { "@ethersphere/bee-js": "^8.3.1", - "@solarpunkltd/mantaray-js": "1.1.0" + "@upcoming/bee-js": "^0.3.0", + "@solarpunkltd/mantaray-js": "1.1.0", + "cafe-utility": "^21.5.0" }, "devDependencies": { "@babel/preset-env": "^7.26.0", diff --git a/src/constants.ts b/src/constants.ts index f16eb2c..34b5494 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,5 +1,14 @@ -const feedTypes = ['sequence', 'epoch'] as const; -export type FeedType = (typeof feedTypes)[number]; +import { FeedType } from './types'; + export const DEFAULT_FEED_TYPE: FeedType = 'sequence'; -//export const STAMP_LIST_TOPIC = 'stamps';//not good -export const STAMP_LIST_TOPIC = '0000000000000000000000000000000000000000000000000000000000000000'; +export const REFERENCE_LIST_TOPIC = 'reference-list'; +export const SHARED_INBOX_TOPIC = 'shared-inbox'; +export const SHARED_WTIHME_TOPIC = 'shared-with-me'; +export const OWNER_FEED_STAMP_LABEL = 'owner-stamp'; +export const ROOT_PATH = '/'; +export const FILEIINFO_NAME = 'fileinfo.json'; +export const FILEIINFO_PATH = ROOT_PATH + 'fileinfo.json'; +export const FILEINFO_HISTORY_NAME = 'history'; +export const FILEINFO_HISTORY_PATH = ROOT_PATH + 'history'; +export const INVALID_STMAP = '0'.repeat(64); +export const SWARM_ZERO_ADDRESS = '0'.repeat(64); diff --git a/src/fileManager.ts b/src/fileManager.ts index 89f60fd..3fe3736 100644 --- a/src/fileManager.ts +++ b/src/fileManager.ts @@ -1,104 +1,279 @@ -import { BatchId, Bee, PostageBatch, Reference, Utils } from '@ethersphere/bee-js'; -import { MantarayNode, MetadataMapping } from '@solarpunkltd/mantaray-js'; +import { + BatchId, + Bee, + BeeRequestOptions, + Data, + GetGranteesResult, + GranteesResult, + PostageBatch, + PssSubscription, + RedundancyLevel, + Reference, + Signer, + STAMPS_DEPTH_MAX, + Topic, + Utils, +} from '@ethersphere/bee-js'; +import { initManifestNode, loadAllNodes, MantarayNode, MetadataMapping } from '@solarpunkltd/mantaray-js'; import { Wallet } from 'ethers'; import { readFileSync } from 'fs'; import path from 'path'; -import { DEFAULT_FEED_TYPE, STAMP_LIST_TOPIC } from './constants'; -import { FileWithMetadata, StampList, StampWithMetadata } from './types'; -import { decodeBytesToPath, encodePathToBytes, getContentType } from './utils'; +import { + DEFAULT_FEED_TYPE, + FILEIINFO_NAME, + FILEIINFO_PATH, + FILEINFO_HISTORY_PATH, + OWNER_FEED_STAMP_LABEL, + REFERENCE_LIST_TOPIC, + SHARED_INBOX_TOPIC, + SWARM_ZERO_ADDRESS, +} from './constants'; +import { FetchFeedUpdateResponse, FileInfo, ReferenceWithHistory, ShareItem, WrappedMantarayFeed } from './types'; +import { + assertFileInfo, + assertReference, + assertReferenceWithHistory, + assertShareItem, + assertTopic, + assertWrappedMantarayFeed, + decodeBytesToPath, + encodePathToBytes, + getContentType, + getRandomTopicHex, + isNotFoundError, + makeBeeRequestOptions, + makeNumericIndex, + numberToFeedIndex, +} from './utils'; export class FileManager { - // TODO: private vars - public bee: Bee; - public mantaray: MantarayNode; - public importedFiles: FileWithMetadata[]; - - private stampList: StampWithMetadata[]; - private nextStampFeedIndex: string; + private bee: Bee; private wallet: Wallet; - private privateKey: string; - private address: string; - private topic: string; - - constructor(beeUrl: string, privateKey: string) { - if (!beeUrl) { - throw new Error('Bee URL is required for initializing the FileManager.'); - } - if (!privateKey) { - throw new Error('privateKey is required for initializing the FileManager.'); - } + private signer: Signer; + private importedReferences: string[]; + private stampList: PostageBatch[]; + private mantarayFeedList: WrappedMantarayFeed[]; + private fileInfoList: FileInfo[]; + private nextOwnerFeedIndex: number; + private sharedWithMe: ShareItem[]; + private sharedSubscription: PssSubscription | undefined; + private ownerFeedTopic: Topic; + + constructor(bee: Bee, privateKey: string) { console.log('Initializing Bee client...'); - this.bee = new Bee(beeUrl); - this.stampList = []; - this.nextStampFeedIndex = ''; - this.privateKey = privateKey; + this.bee = bee; + this.sharedSubscription = undefined; this.wallet = new Wallet(privateKey); - this.address = this.wallet.address; - this.topic = Utils.bytesToHex(Utils.keccak256Hash(STAMP_LIST_TOPIC)); - - this.mantaray = new MantarayNode(); - this.importedFiles = []; + this.signer = { + address: Utils.hexToBytes(this.wallet.address.slice(2)), + sign: async (data: Data): Promise => { + return await this.wallet.signMessage(data); + }, + }; + this.stampList = []; + this.importedReferences = []; + this.fileInfoList = []; + this.mantarayFeedList = []; + this.nextOwnerFeedIndex = 0; + this.ownerFeedTopic = this.bee.makeFeedTopic(SWARM_ZERO_ADDRESS); + this.sharedWithMe = []; } + // Start init methods // TODO: use allSettled for file fetching and only save the ones that are successful - async initialize(items: any | undefined) { - console.log('Importing stamps and references...'); - try { - await this.initStamps(); - if (this.stampList.length > 0) { - console.log('Using stamp list for initialization.'); - for (const elem of this.stampList) { - if (elem.fileReferences !== undefined && elem.fileReferences.length > 0) { - await this.importReferences(elem.fileReferences as Reference[], elem.stamp.batchID); - } - } + async initialize(items?: any): Promise { + await this.initStamps(); + await this.initOwnerFeedTopic(); + await this.initMantarayFeedList(); + await this.initFileInfoList(); + + // try { + // if (items) { + // console.log('Using provided items for initialization.'); + // await this.importLocalReferences(items); + // } else { + // console.log('Fetching all pinned references for initialization.'); + // await this.importPinnedReferences(); + // } + // console.log('References imported successfully.'); + // } catch (error: any) { + // console.error(`[ERROR] Failed to import references: ${error.message}`); + // } + } + + private async initOwnerFeedTopic(): Promise { + const referenceListTopicHex = this.bee.makeFeedTopic(REFERENCE_LIST_TOPIC); + const feedTopicData = await this.getFeedData(referenceListTopicHex, this.wallet.address, 0); + + if (feedTopicData.reference === SWARM_ZERO_ADDRESS) { + const ownerFeedStamp = this.getOwnerFeedStamp(); + if (ownerFeedStamp === undefined) { + throw 'Owner stamp not found'; } - } catch (error: any) { - console.error(`[ERROR] Failed to initialize stamps: ${error.message}`); - throw error; + + this.ownerFeedTopic = getRandomTopicHex(); + const topicDataRes = await this.bee.uploadData(ownerFeedStamp.batchID, this.ownerFeedTopic, { act: true }); + const fw = this.bee.makeFeedWriter(DEFAULT_FEED_TYPE, referenceListTopicHex, this.signer); + await fw.upload(ownerFeedStamp.batchID, topicDataRes.reference, { index: numberToFeedIndex(0) }); + await fw.upload(ownerFeedStamp.batchID, topicDataRes.historyAddress as Reference, { + index: numberToFeedIndex(1), + }); + } else { + const topicHistory = await this.getFeedData(referenceListTopicHex, this.wallet.address, 1); + const publicKey = (await this.bee.getNodeAddresses()).publicKey; // TODO: init pubkey once + const options = makeBeeRequestOptions(topicHistory.reference, publicKey); + + const topicHex = (await this.bee.downloadData(feedTopicData.reference, options)).text(); + assertTopic(topicHex); + this.ownerFeedTopic = topicHex; } + } + private async initStamps(): Promise { try { - if (items) { - console.log('Using provided items for initialization.'); - await this.importLocalReferences(items); - } else { - console.log('Fetching all pinned references for initialization.'); - await this.importPinnedReferences(); - } - console.log('References imported successfully.'); + this.stampList = await this.getUsableStamps(); + console.log('Usable stamps fetched successfully.'); } catch (error: any) { - console.error(`[ERROR] Failed to import references: ${error.message}`); - throw error; + console.error(`Failed to fetch stamps: ${error}`); + } + } + + private async initMantarayFeedList(): Promise { + const latestFeedData = await this.getFeedData(this.ownerFeedTopic); + if (latestFeedData.reference === SWARM_ZERO_ADDRESS) { + console.log("Owner mantaray feed doesn't exist yet."); + return; + } + + this.nextOwnerFeedIndex = makeNumericIndex(latestFeedData.feedIndexNext); + const refWithHistory = latestFeedData as unknown as ReferenceWithHistory; + assertReferenceWithHistory(refWithHistory); + // const ownerFeedRawData = await this.bee.downloadData(latestFeedData.reference); + // const ownerFeedData = JSON.parse(ownerFeedRawData.text()); + // assertReferenceWithHistory(ownerFeedData); + + const publicKey = (await this.bee.getNodeAddresses()).publicKey; + const options = makeBeeRequestOptions(refWithHistory.historyRef, publicKey); + const mantarayFeedListRawData = await this.bee.downloadData(refWithHistory.reference, options); + const mantarayFeedListData: WrappedMantarayFeed[] = JSON.parse(mantarayFeedListRawData.text()); + + for (const wmf of mantarayFeedListData) { + try { + assertWrappedMantarayFeed(wmf); + this.mantarayFeedList.push(wmf); + } catch (error: any) { + console.error(`Invalid WrappedMantarayFeed item, skipping it: ${error}`); + } + } + + console.log('Mantaray feed list fetched successfully.'); + } + + private async downloadFork(mantaray: MantarayNode, forkPAth: string, options?: BeeRequestOptions): Promise { + const fork = mantaray.getForkAtPath(encodePathToBytes(forkPAth)); + const entry = fork?.node.getEntry; + if (entry === undefined) { + throw `fork entry at ${forkPAth} is undefined`; + } + + return (await this.bee.downloadData(entry, options)).text(); + } + + // TODO: at this point we already have the efilerRef, so we can use it to fetch the data + // TODO: loadallnodes and deserialize works but load() doesn't work -> why ? + // TODO: already unwrapped historyRef by bee ? + private async initFileInfoList(): Promise { + // TODO: leave publickey get out of the for loop + const publicKey = (await this.bee.getNodeAddresses()).publicKey; + for (const mantaryFeedItem of this.mantarayFeedList) { + console.log('bagoy mantaryFeedItem: ', mantaryFeedItem); + const feedOptions = makeBeeRequestOptions(mantaryFeedItem.historyRef, publicKey); + const wrappedMantarayData = await this.getFeedData( + mantaryFeedItem.reference, + this.wallet.address, + 0, // TODO: if index is provided then it calls the chunk api, if undefined then it calls the feed api to lookup + // feedOptions, // TODO: commented out the act options because it can download without it but whyy ? it was uploaded via act + ); + try { + console.log('bagoy wrappedMantarayData: ', wrappedMantarayData); + assertReference(wrappedMantarayData.reference); + } catch (error: any) { + console.error(`Invalid wrappedMantarayData reference: ${wrappedMantarayData.reference}`); + continue; + } + + if (wrappedMantarayData.reference === SWARM_ZERO_ADDRESS) { + console.log(`mantaryFeedItem not found, skipping it, reference: ${mantaryFeedItem.reference}`); + continue; + } + + // let options = makeBeeRequestOptions(mantaryFeedItem.historyRef, publicKey); + const rootMantaray = (await this.bee.downloadData(wrappedMantarayData.reference)).hex(); + console.log('bagoy initFileInfoList rootMantaray: ', rootMantaray); + const mantaray = await this.loadAllMantarayNodes(Buffer.from(rootMantaray, 'hex')); + const historyRef = await this.downloadFork(mantaray, FILEINFO_HISTORY_PATH); + try { + assertReference(historyRef); + } catch (error: any) { + console.error(`Invalid history reference: ${historyRef}`); + continue; + } + console.log('bagoy historyRef: ', historyRef); + + const options = makeBeeRequestOptions(historyRef, publicKey); + const fileInfoRawData = await this.downloadFork(mantaray, FILEIINFO_PATH, options); + const fileInfoData: FileInfo = JSON.parse(fileInfoRawData); + console.log('bagoy fileInfoData: ', fileInfoData); + + try { + assertFileInfo(fileInfoData); + this.fileInfoList.push(fileInfoData); + } catch (error: any) { + console.error(`Invalid FileInfo item, skipping it: ${error}`); + } } + + console.log('File info list fetched successfully.'); } - async initializeFeed(stamp: string | BatchId) { + // End init methods + + // Start getter methods + getFileInfoList(): FileInfo[] { + return this.fileInfoList; + } + + getSharedWithMe(): ShareItem[] { + return this.sharedWithMe; + } + // End getter methods + + private async initializeFeed(batchId: string | BatchId, mantaray: MantarayNode): Promise { console.log('Initializing wallet and checking for existing feed...'); - const reader = this.bee.makeFeedReader('sequence', this.topic, this.wallet.address); + const reader = this.bee.makeFeedReader('sequence', this.ownerFeedTopic, this.wallet.address); try { const { reference } = await reader.download(); console.log(`Existing feed found. Reference: ${reference}`); const manifestData = await this.bee.downloadData(reference); - this.mantaray.deserialize(Buffer.from(manifestData)); + mantaray.deserialize(Buffer.from(manifestData)); console.log('Mantaray structure initialized from feed.'); } catch (error) { console.log('No existing feed found. Initializing new Mantaray structure...'); - this.mantaray = new MantarayNode(); - await this.saveFeed(stamp); + mantaray = new MantarayNode(); + await this.saveFeed(batchId, mantaray); } } - async saveFeed(stamp: string | BatchId) { + private async saveFeed(batchId: string | BatchId, mantaray: MantarayNode): Promise { console.log('Saving Mantaray structure to feed...'); // Save the Mantaray structure and get the manifest reference (Uint8Array) - const manifestReference = await this.mantaray.save(async (data) => { - const uploadResponse = await this.bee.uploadData(stamp, data); + const manifestReference = await mantaray.save(async (data) => { + const uploadResponse = await this.bee.uploadData(batchId, data); return uploadResponse.reference; // Ensure 64-byte reference }); @@ -106,19 +281,16 @@ export class FileManager { const hexManifestReference = manifestReference; // Create a feed writer and upload the manifest reference - const writer = this.bee.makeFeedWriter('sequence', this.topic, this.wallet.privateKey); - await writer.upload(stamp, hexManifestReference as Reference); // Explicitly cast to Reference + const writer = this.bee.makeFeedWriter('sequence', this.ownerFeedTopic, this.signer); + await writer.upload(batchId, hexManifestReference as Reference); // Explicitly cast to Reference console.log(`Feed updated with reference: ${hexManifestReference}`); } - async fetchFeed() { + private async fetchFeed(): Promise { console.log('Fetching the latest feed reference...'); - if (!this.wallet) { - throw new Error('Wallet not initialized. Please call initializeFeed first.'); - } - const reader = this.bee.makeFeedReader('sequence', this.topic, this.wallet.address); + const reader = this.bee.makeFeedReader(DEFAULT_FEED_TYPE, this.ownerFeedTopic, this.wallet.address); try { const { reference } = await reader.download(); console.log(`Latest feed reference fetched: ${reference}`); @@ -129,88 +301,28 @@ export class FileManager { } } - // TODO: method to list new stamp with files - // TODO: encrypt - // TODO: how and how long to store the stamps feed data ? - async updateStampData(stamp: string | BatchId, privateKey: string): Promise { - const feedWriter = this.bee.makeFeedWriter( - DEFAULT_FEED_TYPE, - STAMP_LIST_TOPIC, - privateKey /*, { headers: { encrypt: "true" } }*/, - ); - try { - const data = JSON.stringify({ filesOfStamps: this.stampList.map((s) => [s.stamp.batchID, s.fileReferences]) }); - const stampListDataRef = await this.bee.uploadData(stamp, data); - const writeResult = await feedWriter.upload(stamp, stampListDataRef.reference, { - index: this.nextStampFeedIndex, - }); - console.log('Stamp feed updated: ', writeResult.reference); - } catch (error: any) { - console.error(`Failed to download feed update: ${error}`); - return; - } - } - - // TODO: fetch usable stamps or read from feed - // TODO: import other stamps in order to topup: owner(s) ? - async initStamps(): Promise { - try { - this.stampList = await this.getUsableStamps(); - console.log('Usable stamps fetched successfully.'); - } catch (error: any) { - console.error(`Failed to update stamps: ${error}`); - throw error; - } - - // TODO: stamps of other users -> feature to fetch other nodes' stamp data - const topicHex = this.bee.makeFeedTopic(STAMP_LIST_TOPIC); - const feedReader = this.bee.makeFeedReader(DEFAULT_FEED_TYPE, topicHex, this.address); - try { - const latestFeedData = await feedReader.download(); - this.nextStampFeedIndex = latestFeedData.feedIndexNext; - const stampListData = (await this.bee.downloadData(latestFeedData.reference)).text(); - const stampList = JSON.parse(stampListData) as StampList; - for (const [batchId, fileRefs] of stampList.filesOfStamps) { - // if (this.stampList.find((s) => s.stamp.batchID === stamp.stamp.batchID) === undefined) { - // await this.fetchStamp(stamp.stamp.batchID); - // } - const stampIx = this.stampList.findIndex((s) => s.stamp.batchID === batchId); - if (stampIx !== -1) { - if (fileRefs.length > 0) { - this.stampList[stampIx].fileReferences = [...fileRefs]; - } - } - } - console.log('File referene list fetched from feed.'); - } catch (error: any) { - console.error(`Failed to fetch file reference list from feed: ${error}`); - return; - } - } - - async getUsableStamps(): Promise { + // Start stamp methods + private async getUsableStamps(): Promise { try { - const stamps = (await this.bee.getAllPostageBatch()).filter((s) => s.usable); - // TOOD: files as importedFiles - return stamps.map((s) => ({ stamp: s, files: [] })); + return (await this.bee.getAllPostageBatch()).filter((s) => s.usable); } catch (error: any) { console.error(`Failed to get usable stamps: ${error}`); return []; } } - async filterBatches(ttl?: number, utilization?: number, capacity?: number): Promise { + async filterBatches(ttl?: number, utilization?: number, capacity?: number): Promise { // TODO: clarify depth vs capacity return this.stampList.filter((s) => { - if (utilization !== undefined && s.stamp.utilization <= utilization) { + if (utilization !== undefined && s.utilization <= utilization) { return false; } - if (capacity !== undefined && s.stamp.depth <= capacity) { + if (capacity !== undefined && s.depth <= capacity) { return false; } - if (ttl !== undefined && s.stamp.batchTTL <= ttl) { + if (ttl !== undefined && s.batchTTL <= ttl) { return false; } @@ -218,8 +330,16 @@ export class FileManager { }); } - async getLocalStamp(batchId: string | BatchId): Promise { - return this.stampList.find((s) => s.stamp.batchID === batchId); + async getStamps(): Promise { + return this.stampList; + } + + getOwnerFeedStamp(): PostageBatch | undefined { + return this.stampList.find((s) => s.label === OWNER_FEED_STAMP_LABEL); + } + + getCachedStamp(batchId: string | BatchId): PostageBatch | undefined { + return this.stampList.find((s) => s.batchID === batchId); } async fetchStamp(batchId: string | { batchID: string }): Promise { @@ -227,28 +347,54 @@ export class FileManager { const id = typeof batchId === 'string' ? batchId : batchId.batchID; const newStamp = await this.bee.getPostageBatch(id); if (newStamp?.exists && newStamp.usable) { - this.stampList.push({ stamp: newStamp }); + this.stampList.push(newStamp); return newStamp; } - return undefined; } catch (error: any) { console.error(`Failed to get stamp with batchID ${batchId}: ${error.message}`); - return undefined; } } - async getStamps(): Promise { - return this.stampList; + async destroyVolume(batchId: string | BatchId): Promise { + if (batchId === this.getOwnerFeedStamp()?.batchID) { + throw 'Cannot destroy owner stamp'; + } + + await this.bee.diluteBatch(batchId, STAMPS_DEPTH_MAX); + + for (let i = 0; i < this.stampList.length; i++) { + if (this.stampList[i].batchID === batchId) { + this.stampList.splice(i, 1); + break; + } + } + + for (let i = 0; i < this.fileInfoList.length, ++i; ) { + const fileInfo = this.fileInfoList[i]; + if (fileInfo.batchId === batchId) { + this.fileInfoList.splice(i, 1); + const mfIx = this.mantarayFeedList.findIndex((mf) => mf.eFileRef === fileInfo.eFileRef); + if (mfIx !== -1) { + this.mantarayFeedList.splice(mfIx, 1); + } + } + } + + this.saveMantarayFeedList(); + + console.log(`Volume destroyed: ${batchId}`); } + // End stamp methods - async importReferences(referenceList: Reference[], batchId?: string, isLocal = false) { + private async importReferences(referenceList: Reference[], isLocal = false): Promise { const processPromises = referenceList.map(async (item: any) => { const reference: Reference = isLocal ? item.hash : item; try { console.log(`Processing reference: ${reference}`); // Download the file to extract its metadata - const fileData = await this.bee.downloadFile(reference); + const path = '/rootmetadata.json'; + const fileData = await this.bee.downloadFile(reference, path); const content = Buffer.from(fileData.data.toString() || ''); const fileName = fileData.name || `pinned-${reference.substring(0, 6)}`; const contentType = fileData.contentType || 'application/octet-stream'; @@ -269,10 +415,11 @@ export class FileManager { console.log(`Adding Reference: ${reference} as ${fileName}`); // Add the file to the Mantaray node with enriched metadata - this.addToMantaray(undefined, reference, metadata); + const mantaray = initManifestNode(); + this.addToMantaray(mantaray, reference, metadata); // Track imported files - this.importedFiles.push({ reference: reference, name: fileName, batchId: batchId || '' }); + this.importedReferences.push(reference); } catch (error: any) { console.error(`[ERROR] Failed to process reference ${reference}: ${error.message}`); } @@ -281,17 +428,21 @@ export class FileManager { await Promise.all(processPromises); // Wait for all references to be processed } - async importPinnedReferences() { + private async importPinnedReferences(): Promise { const allPins = await this.bee.getAllPins(); await this.importReferences(allPins); } - async importLocalReferences(items: any) { - await this.importReferences(items, undefined, true); + private async importLocalReferences(items: any): Promise { + await this.importReferences(items, undefined); } - async downloadFile(mantaray: MantarayNode, filePath: string, onlyMetadata = false) { - mantaray = mantaray || this.mantaray; + async downloadFile( + mantaray: MantarayNode, + filePath: string, + onlyMetadata = false, + options?: BeeRequestOptions, + ): Promise { console.log(`Downloading file: ${filePath}`); const normalizedPath = path.normalize(filePath); const segments = normalizedPath.split(path.sep); @@ -324,7 +475,7 @@ export class FileManager { console.log(`Downloading file with reference: ${fileReference}`); try { - const fileData = await this.bee.downloadFile(fileReference); + const fileData = await this.bee.downloadFile(fileReference, 'encryptedfilepath', options); return { data: fileData.data ? Buffer.from(fileData.data).toString('utf-8').trim() : '', metadata, @@ -335,8 +486,7 @@ export class FileManager { } } - async downloadFiles(mantaray: MantarayNode) { - mantaray = mantaray || this.mantaray; + async downloadFiles(mantaray: MantarayNode): Promise { console.log('Downloading all files from Mantaray...'); const forks = mantaray.forks; if (!forks) { @@ -385,74 +535,156 @@ export class FileManager { return validResults; // Return successful download results } + // @upcoming/bee-js 0.03 + // TODO: use all the params of the currentfileinfo is exists? + async upload( + batchId: string | BatchId, + mantaray: MantarayNode, + file: string, + currentFileInfo?: FileInfo, + customMetadata?: Record, + ): Promise { + const redundancyLevel = currentFileInfo?.redundancyLevel || RedundancyLevel.MEDIUM; + const uploadFileRes = await this.uploadFile(batchId, file, currentFileInfo); + + // TODO: store feed index in fileinfo to speed up upload? -> undifined == 0, index otherwise + const fileInfo: FileInfo = { + eFileRef: uploadFileRes.reference, + batchId: batchId, + fileName: path.basename(file), + owner: this.wallet.address, + shared: false, + historyRef: uploadFileRes.historyRef, + timestamp: new Date().getTime(), + redundancyLevel: redundancyLevel, + customMetadata: customMetadata, + }; - async uploadFile( + const fileInfoRes = await this.uploadFileInfo(batchId, fileInfo); + mantaray.addFork(encodePathToBytes(FILEIINFO_PATH), fileInfoRes.reference as Reference, { + 'Content-Type': 'application/json', + Filename: FILEIINFO_NAME, + }); + // TODO: is fileinfo ACT needed? + const uploadHistoryRes = await this.uploadFileInfoHistory(batchId, fileInfoRes.historyRef, redundancyLevel); + mantaray.addFork(encodePathToBytes(FILEINFO_HISTORY_PATH), uploadHistoryRes.historyRef as Reference); + + const wrappedMantarayRef = await this.saveMantaray(batchId, mantaray); + console.log('bagoy saveMantaray wrappedMantarayRef: ', wrappedMantarayRef); + const topic = currentFileInfo?.topic || getRandomTopicHex(); + console.log('bagoy wrapped mantaray feed topic: ', topic); + assertTopic(topic); + const wrappedFeedUpdateRes = await this.updateWrappedMantarayFeed(batchId, wrappedMantarayRef, topic); + + const feedUpdate: WrappedMantarayFeed = { + reference: topic, + historyRef: wrappedFeedUpdateRes.historyRef, + eFileRef: fileInfoRes.reference, // TODO: why fileInfoRes.reference instead of eFileRef ? + }; + const ix = this.mantarayFeedList.findIndex((f) => f.reference === feedUpdate.reference); + if (ix !== -1) { + this.mantarayFeedList[ix] = { ...feedUpdate, eGranteeRef: this.mantarayFeedList[ix].eGranteeRef }; + } else { + this.mantarayFeedList.push(feedUpdate); + } + + await this.saveMantarayFeedList(); + } + + private async uploadFile( + batchId: string | BatchId, file: string, - mantaray: MantarayNode | undefined, - stamp: string | BatchId, - customMetadata = {}, - redundancyLevel = '1', - save = true, - ) { - mantaray = mantaray || this.mantaray; + currentFileInfo: FileInfo | undefined = undefined, + ): Promise { console.log(`Uploading file: ${file}`); - const fileData = readFileSync(file); + const filePath = path.resolve(__dirname, file); + const fileData = new Uint8Array(readFileSync(filePath)); const fileName = path.basename(file); const contentType = getContentType(file); - const metadata = { - 'Content-Type': contentType, - 'Content-Size': fileData.length.toString(), - 'Time-Uploaded': new Date().toISOString(), - Filename: fileName, - 'Custom-Metadata': JSON.stringify(customMetadata), - }; - - const uploadHeaders = { - contentType, - headers: { - 'swarm-redundancy-level': redundancyLevel, - }, - }; + try { + const options = makeBeeRequestOptions(currentFileInfo?.historyRef); + const uploadFileRes = await this.bee.uploadFile( + batchId, + fileData, + fileName, + { + act: true, + redundancyLevel: currentFileInfo?.redundancyLevel || RedundancyLevel.MEDIUM, + contentType: contentType, + }, + options, + ); + + console.log(`File uploaded successfully: ${file}, Reference: ${uploadFileRes.reference}`); + return { reference: uploadFileRes.reference, historyRef: uploadFileRes.historyAddress }; + } catch (error: any) { + throw `Failed to upload file ${file}: ${error}`; + } + } + private async uploadFileInfo(batchId: string | BatchId, fileInfo: FileInfo): Promise { try { - const uploadResponse = await this.bee.uploadFile(stamp, fileData, fileName, uploadHeaders); - this.addToMantaray(mantaray, uploadResponse.reference, metadata); + const uploadInfoRes = await this.bee.uploadData(batchId, JSON.stringify(fileInfo), { + act: true, + redundancyLevel: fileInfo.redundancyLevel, + }); + console.log('Fileinfo updated: ', uploadInfoRes.reference); - if (save) { - console.log('Saving Mantaray node...'); - await this.saveMantaray(mantaray, stamp); - } + this.fileInfoList.push(fileInfo); - // TODO: handle stamplist and filelist here - const stampIx = this.stampList.findIndex((s) => s.stamp.batchID === stamp); - if (stampIx === -1) { - const newStamp = await this.fetchStamp(stamp); - // TODO: what to do here ? batch should alreade be usable - if (newStamp === undefined) { - throw new Error(`Stamp not found: ${stamp}`); - } + return { reference: uploadInfoRes.reference, historyRef: uploadInfoRes.historyAddress }; + } catch (error: any) { + throw `Failed to save fileinfo: ${error}`; + } + } - this.stampList.push({ stamp: newStamp, fileReferences: [uploadResponse.reference] }); - } else if (this.stampList[stampIx].fileReferences === undefined) { - this.stampList[stampIx].fileReferences = [uploadResponse.reference]; - } else { - this.stampList[stampIx].fileReferences.push(uploadResponse.reference); - } + private async uploadFileInfoHistory( + batchId: string | BatchId, + hisoryRef: string, + redundancyLevel: RedundancyLevel = RedundancyLevel.MEDIUM, + ): Promise { + try { + const uploadHistoryRes = await this.bee.uploadData(batchId, hisoryRef, { + redundancyLevel: redundancyLevel, + }); - await this.updateStampData(stamp, this.privateKey); + console.log('Fileinfo history updated: ', uploadHistoryRes.reference); - console.log(`File uploaded successfully: ${file}, Reference: ${uploadResponse.reference}`); - return uploadResponse.reference; + return { reference: uploadHistoryRes.reference, historyRef: uploadHistoryRes.reference }; } catch (error: any) { - console.error(`[ERROR] Failed to upload file ${file}: ${error.message}`); - throw error; + throw `Failed to save fileinfo history: ${error}`; } } - addToMantaray(mantaray: MantarayNode | undefined, reference: string, metadata: MetadataMapping = {}) { - mantaray = mantaray || this.mantaray; + private async updateWrappedMantarayFeed( + batchId: string | BatchId, + wrappedMantarayRef: Reference, + topic: Topic, + ): Promise { + try { + const fw = this.bee.makeFeedWriter(DEFAULT_FEED_TYPE, topic, this.signer); + // const wrappedMantarayData = await this.bee.uploadData(batchId, wrappedMantarayRef, { act: true }); + // const uploadRes = await fw.upload(batchId, wrappedMantarayData.reference, { + // index: undefined, // todo: keep track of the latest index ?? + // }); + // const wrappedMantarayData = await this.bee.uploadData(batchId, wrappedMantarayRef); + const uploadRes = await fw.upload(batchId, wrappedMantarayRef, { + index: undefined, // todo: keep track of the latest index ?? + act: true, // bagoy: this shall call to /soc post api + }); + console.log('bagoy updateWrappedMantarayFeed uploadres: ', uploadRes); + // console.log('bagoy updateWrappedMantarayFeed wrappedMantarayData: ', wrappedMantarayData); + console.log('bagoy updateWrappedMantarayFeed wrappedMantarayRef: ', wrappedMantarayRef); + // return { reference: uploadRes.reference, historyRef: wrappedMantarayData.historyAddress }; + return { reference: uploadRes.reference, historyRef: uploadRes.historyAddress }; + } catch (error: any) { + throw `Failed to wrapped mantaray feed: ${error}`; + } + } + + private addToMantaray(mantaray: MantarayNode, reference: string, metadata: MetadataMapping = {}): void { const filePath = metadata.fullPath || metadata.Filename || 'file'; const originalFileName = metadata.originalFileName || path.basename(filePath); @@ -466,35 +698,49 @@ export class FileManager { mantaray.addFork(bytesPath, reference as Reference, metadataWithOriginalName); } - async saveMantaray(mantaray: MantarayNode | undefined, stamp: string | BatchId) { - mantaray = mantaray || this.mantaray; - console.log('Saving Mantaray manifest...'); - + private async saveMantaray(batchId: string | BatchId, mantaray: MantarayNode): Promise { const saveFunction = async (data: Uint8Array): Promise => { - const fileName = 'manifest'; - const contentType = 'application/json'; - const uploadResponse = await this.bee.uploadFile(stamp, data, fileName, { contentType }); + const uploadResponse = await this.bee.uploadData(batchId, data); return uploadResponse.reference; }; - const manifestReference = await mantaray.save(saveFunction); + return mantaray.save(saveFunction); + } + + private async loadMantaray(manifestReference: Reference, mantaray: MantarayNode): Promise { + const loadFunction = async (address: Reference): Promise => { + return this.bee.downloadData(address); + }; + + mantaray.load(loadFunction, manifestReference); + } - const hexReference = manifestReference; - console.log(`Mantaray manifest saved with reference: ${hexReference}`); - return hexReference; + // TODO: is obfuscationKey needed? + private async loadAllMantarayNodes(data: Uint8Array): Promise { + // const mantaray = initManifestNode({ + // obfuscationKey: Utils.hexToBytes(getRandomTopicHex()), + // }); + const mantaray = new MantarayNode(); + mantaray.deserialize(data); + await loadAllNodes(async (address: Reference): Promise => { + return this.bee.downloadData(address); + }, mantaray); + + return mantaray; } - searchFilesByName(fileNameQuery: string, includeMetadata = false) { + searchFilesByName(fileNameQuery: string, mantaray: MantarayNode, includeMetadata = false): any { console.log(`Searching for files by name: ${fileNameQuery}`); - const allFiles = this.listFiles(this.mantaray, includeMetadata); + const allFiles = this.listFiles(mantaray, includeMetadata); - const filteredFiles = allFiles.filter((file) => file.path.includes(fileNameQuery)); + const filteredFiles = allFiles.filter((file: any) => file.path.includes(fileNameQuery)); return filteredFiles; } searchFiles( + mantaray: MantarayNode, { fileName, directory, @@ -511,19 +757,19 @@ export class FileManager { extension?: string; }, includeMetadata = false, - ) { - let results = this.listFiles(this.mantaray, true); + ): any { + let results = this.listFiles(mantaray, true); if (fileName) { - results = results.filter((file) => path.posix.basename(file.path).includes(fileName)); + results = results.filter((file: any) => path.posix.basename(file.path).includes(fileName)); } if (directory) { - results = results.filter((file) => path.posix.dirname(file.path).includes(directory)); + results = results.filter((file: any) => path.posix.dirname(file.path).includes(directory)); } if (metadata) { - results = results.filter((file) => { + results = results.filter((file: any) => { for (const [key, value] of Object.entries(metadata)) { if (file.metadata?.[key] !== value) { return false; @@ -534,7 +780,7 @@ export class FileManager { } if (minSize !== undefined && maxSize !== undefined) { - results = results.filter((file) => { + results = results.filter((file: any) => { const size = parseInt(file.metadata?.['Content-Size'] ?? '0', 10); // Default to '0' if undefined return size >= minSize && size <= maxSize; }); @@ -542,17 +788,16 @@ export class FileManager { if (extension) { const normalizedExtension = extension.startsWith('.') ? extension : `.${extension}`; - results = results.filter((file) => { + results = results.filter((file: any) => { const cleanPath = file.path.split('\x00').join(''); // Clean up any null characters return path.posix.extname(cleanPath) === normalizedExtension; }); } - return results.map((file) => (includeMetadata ? file : { path: file.path })); + return results.map((file: any) => (includeMetadata ? file : { path: file.path })); } - listFiles(mantaray: MantarayNode | undefined, includeMetadata = false) { - mantaray = mantaray || this.mantaray; + listFiles(mantaray: MantarayNode, includeMetadata = false): any { console.log('Listing files in Mantaray...'); const fileList = []; @@ -596,8 +841,7 @@ export class FileManager { return fileList; } - getDirectoryStructure(mantaray: MantarayNode | undefined, rootDirName: string) { - mantaray = mantaray || this.mantaray; + private getDirectoryStructure(mantaray: MantarayNode, rootDirName: string): any { console.log('Building directory structure from Mantaray...'); const structure = this.buildDirectoryStructure(mantaray); @@ -609,8 +853,7 @@ export class FileManager { return wrappedStructure; } - buildDirectoryStructure(mantaray: MantarayNode) { - mantaray = mantaray || this.mantaray; + private buildDirectoryStructure(mantaray: MantarayNode): any { console.log('Building raw directory structure...'); const structure: { [key: string]: any } = {}; @@ -641,9 +884,7 @@ export class FileManager { return structure; } - getContentsOfDirectory(targetPath: string, mantaray: MantarayNode | undefined, rootDirName: string) { - mantaray = mantaray || this.mantaray; - + getContentsOfDirectory(targetPath: string, mantaray: MantarayNode, rootDirName: string): any { const directoryStructure: { [key: string]: any } = this.getDirectoryStructure(mantaray, rootDirName); if (targetPath === rootDirName || targetPath === '.') { @@ -690,4 +931,193 @@ export class FileManager { return contents; } + + // Start owner mantaray feed handler methods + private async saveMantarayFeedList(): Promise { + const ownerFeedStamp = this.getOwnerFeedStamp(); + if (!ownerFeedStamp) { + throw 'Owner feed stamp is not found.'; + } + + try { + const mantarayFeedListData = await this.bee.uploadData( + ownerFeedStamp.batchID, + JSON.stringify(this.mantarayFeedList), + { + act: true, + }, + ); + + const ownerFeedData: ReferenceWithHistory = { + reference: mantarayFeedListData.reference, + historyRef: mantarayFeedListData.historyAddress, + }; + console.log('bagoy first init ownerFeedData.reference: ', ownerFeedData.reference); + console.log('bagoy first init ownerFeedData.historyRef: ', ownerFeedData.historyRef); + + const ownerFeedWriter = this.bee.makeFeedWriter(DEFAULT_FEED_TYPE, this.ownerFeedTopic, this.signer); + const ownerFeedRawData = await this.bee.uploadData(ownerFeedStamp.batchID, JSON.stringify(ownerFeedData)); + const writeResult = await ownerFeedWriter.upload(ownerFeedStamp.batchID, ownerFeedRawData.reference, { + index: this.nextOwnerFeedIndex, + }); + // const checkData = await ownerFeedWriter.download(); + // console.log('bagoy checkData: ', checkData); + + console.log('bagoy first init ownerFeedRawData.reference: ', ownerFeedRawData.reference); + this.nextOwnerFeedIndex += 1; + console.log('Owner feed list updated: ', writeResult.reference); + } catch (error: any) { + throw `Failed to update owner feed list: ${error}`; + } + } + // End owner mantaray feed handler methods + + // Start grantee handler methods + // fetches the list of grantees who can access the file reference + async getGranteesOfFile(eFileRef: string): Promise { + const mf = this.mantarayFeedList.find((f) => f.eFileRef === eFileRef); + if (mf?.eGranteeRef === undefined) { + throw `Grantee list not found for file reference: ${eFileRef}`; + } + + return this.bee.getGrantees(mf.eGranteeRef); + } + + // TODO: only add is supported + // updates the list of grantees who can access the file reference under the history reference + private async handleGrantees( + fileInfo: FileInfo, + grantees: { + add?: string[]; + revoke?: string[]; + }, + eGlRef?: string | Reference, + ): Promise { + console.log('Granting access to file: ', fileInfo.eFileRef); + const fIx = this.fileInfoList.findIndex((f) => f.eFileRef === fileInfo.eFileRef); + if (fIx === -1) { + throw `Provided file reference not found: ${fileInfo.eFileRef}`; + } + + let grantResult: GranteesResult; + if (eGlRef !== undefined) { + // TODO: history ref should be optional in bee-js + grantResult = await this.bee.patchGrantees( + fileInfo.batchId, + eGlRef, + fileInfo.historyRef || SWARM_ZERO_ADDRESS, + grantees, + ); + console.log('Access patched, grantee list reference: ', grantResult.ref); + } else { + if (grantees.add === undefined || grantees.add.length === 0) { + throw `No grantees specified.`; + } + + grantResult = await this.bee.createGrantees(fileInfo.batchId, grantees.add); + console.log('Access granted, new grantee list reference: ', grantResult.ref); + } + + return grantResult; + } + + // End grantee handler methods + + // Start share methods + subscribeToSharedInbox(topic: string, callback?: (data: ShareItem) => void): PssSubscription { + console.log('Subscribing to shared inbox, topic: ', topic); + this.sharedSubscription = this.bee.pssSubscribe(topic, { + onMessage: (message) => { + console.log('Received shared inbox message: ', message); + assertShareItem(message); + this.sharedWithMe.push(message); + if (callback) { + callback(message); + } + }, + onError: (e) => { + console.log('Error received in shared inbox: ', e.message); + throw e; + }, + }); + + return this.sharedSubscription; + } + + unsubscribeFromSharedInbox(): void { + if (this.sharedSubscription) { + console.log('Unsubscribed from shared inbox, topic: ', this.sharedSubscription.topic); + this.sharedSubscription.cancel(); + } + } + + async shareItem(fileInfo: FileInfo, targetOverlays: string[], recipients: string[], message?: string): Promise { + const mfIx = this.mantarayFeedList.findIndex((mf) => mf.reference === fileInfo.eFileRef); + if (mfIx === -1) { + console.log('File reference not found in mantaray feed list.'); + return; + } + + const grantResult = await this.handleGrantees( + fileInfo, + { add: recipients }, + this.mantarayFeedList[mfIx].eGranteeRef, + ); + + this.mantarayFeedList[mfIx] = { + ...this.mantarayFeedList[mfIx], + eGranteeRef: grantResult.ref, + }; + + this.saveMantarayFeedList(); + + const item: ShareItem = { + fileInfo: fileInfo, + timestamp: Date.now(), + message: message, + }; + + this.sendShareMessage(targetOverlays, item, recipients); + } + + // recipient is optional, if not provided the message will be broadcasted == pss public key + private async sendShareMessage(targetOverlays: string[], item: ShareItem, recipients: string[]): Promise { + if (recipients.length === 0 || recipients.length !== targetOverlays.length) { + console.log('Invalid recipients or targetoverlays specified for sharing.'); + return; + } + + for (let i = 0; i < recipients.length; i++) { + try { + // TODO: mining will take too long, 2 bytes are enough + const target = Utils.makeMaxTarget(targetOverlays[i]); + const msgData = new Uint8Array(Buffer.from(JSON.stringify(item))); + this.bee.pssSend(item.fileInfo.batchId, SHARED_INBOX_TOPIC, target, msgData, recipients[i]); + } catch (error: any) { + console.log(`Failed to share item with recipient: ${recipients[i]}\n `, error); + } + } + } + // End share methods + + // TODO: upload /soc wiht ACT and download: do not upload mantaraymanifref with ACT as data! + public async getFeedData( + topic: string, + address?: string, + index?: number, + options?: BeeRequestOptions, + ): Promise { + try { + const feedReader = this.bee.makeFeedReader(DEFAULT_FEED_TYPE, topic, address || this.wallet.address, options); + if (index !== undefined) { + return await feedReader.download({ index: numberToFeedIndex(index) }); + } + return await feedReader.download(); + } catch (error) { + if (isNotFoundError(error)) { + return { feedIndex: -1, feedIndexNext: (0).toString(), reference: SWARM_ZERO_ADDRESS as Reference }; + } + throw error; + } + } } diff --git a/src/index.ts b/src/index.ts index 3d8df8c..f5c5525 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,7 +12,7 @@ const wallet = { privateKey: 'af829bfc5c90818776f0b3d587c14c8e7a4222b781e9f7ebc6e527f4dfd117c2', }; -async function main() { +async function main(): Promise { try { console.log('### Simulation: FileManager Operations ###'); diff --git a/src/types.ts b/src/types.ts index 7bfb221..9ec0c94 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,20 +1,49 @@ -import { BatchId, PostageBatch, Reference } from '@ethersphere/bee-js'; +import { BatchId, RedundancyLevel, Reference, ReferenceResponse, Topic } from '@ethersphere/bee-js'; -export interface FileWithMetadata { - reference: string | Reference; - name: string; +export interface FileInfo { batchId: string | BatchId; + eFileRef: string | Reference; + topic?: string | Topic; + historyRef?: string | Reference; + owner?: string; + fileName?: string; timestamp?: number; - uploader?: string; + shared?: boolean; + preview?: string; + redundancyLevel?: RedundancyLevel; + customMetadata?: Record; } -export interface StampWithMetadata { - stamp: PostageBatch; - fileReferences?: string[] | Reference[]; - feedReference?: string | Reference; - nextIndex?: number; +export interface ReferenceWithHistory { + reference: string | Reference; + historyRef: string | Reference; } -export interface StampList { - filesOfStamps: Map; +// TODO: consider using a completely seprarate type for the manifestfeed because of topic === reference +export interface WrappedMantarayFeed extends ReferenceWithHistory { + eFileRef?: string | Reference; + eGranteeRef?: string | Reference; +} + +export interface ShareItem { + fileInfo: FileInfo; + timestamp?: number; + message?: string; +} + +export interface Bytes extends Uint8Array { + readonly length: Length; +} +export type IndexBytes = Bytes<8>; +export interface Epoch { + time: number; + level: number; +} +export interface FeedUpdateHeaders { + feedIndex: Index; + feedIndexNext: string; } +export interface FetchFeedUpdateResponse extends ReferenceResponse, FeedUpdateHeaders {} +export type Index = number | Epoch | IndexBytes | string; +const feedTypes = ['sequence', 'epoch'] as const; +export type FeedType = (typeof feedTypes)[number]; diff --git a/src/utils.ts b/src/utils.ts index c63355d..6f4b517 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,21 @@ +import { + BeeRequestOptions, + ENCRYPTED_REFERENCE_HEX_LENGTH, + Reference, + REFERENCE_BYTES_LENGTH, + REFERENCE_HEX_LENGTH, + Topic, + TOPIC_BYTES_LENGTH, + TOPIC_HEX_LENGTH, + Utils, +} from '@ethersphere/bee-js'; +import { Binary } from 'cafe-utility'; +import { randomBytes } from 'crypto'; import path from 'path'; -export function getContentType(filePath: string) { +import { Bytes, FileInfo, Index, ReferenceWithHistory, ShareItem, WrappedMantarayFeed } from './types'; + +export function getContentType(filePath: string): string { const ext = path.extname(filePath).toLowerCase(); const contentTypes: Map = new Map([ ['.txt', 'text/plain'], @@ -13,27 +28,213 @@ export function getContentType(filePath: string) { return contentTypes.get(ext) || 'application/octet-stream'; } -export function pathToBytes(s: string) { - return new TextEncoder().encode(s); +export function assertReference(value: unknown): asserts value is Reference { + try { + Utils.assertHexString(value, REFERENCE_HEX_LENGTH); + } catch (e) { + Utils.assertHexString(value, ENCRYPTED_REFERENCE_HEX_LENGTH); + } } -export function hexStringToReference(reference: string) { - const bytes = new Uint8Array(Buffer.from(reference, 'hex')); - if (bytes.length !== 32 && bytes.length !== 64) { - throw new Error('Invalid reference length'); +export function assertTopic(value: unknown): asserts value is Topic { + if (!Utils.isHexString(value, TOPIC_HEX_LENGTH)) { + throw `Invalid feed topic: ${value}`; } - return bytes; } -export function encodePathToBytes(pathString: string) { - return new TextEncoder().encode(pathString); +export function isObject(value: unknown): value is Record { + return value !== null && typeof value === 'object'; } -export function decodeBytesToPath(bytes: Uint8Array) { - if (bytes.length !== 32) { - const paddedBytes = new Uint8Array(32); - paddedBytes.set(bytes.slice(0, 32)); // Truncate or pad the input to ensure it's 32 bytes +export function isStrictlyObject(value: unknown): value is Record { + return isObject(value) && !Array.isArray(value); +} + +export function isRecord(value: Record | string[]): value is Record { + return typeof value === 'object' && 'key' in value; +} + +export function assertFileInfo(value: unknown): asserts value is FileInfo { + if (!isStrictlyObject(value)) { + throw new TypeError('FileInfo has to be object!'); + } + + const fi = value as unknown as FileInfo; + + assertReference(fi.eFileRef); + + if (fi.batchId === undefined || typeof fi.batchId !== 'string') { + throw new TypeError('batchId property of FileInfo has to be string!'); + } + + if (fi.historyRef !== undefined) { + assertReference(fi.historyRef); + } + + if (fi.topic !== undefined) { + assertTopic(fi.topic); + } + + if (fi.customMetadata !== undefined && !isRecord(fi.customMetadata)) { + throw new TypeError('FileInfo customMetadata has to be object!'); + } + + if (fi.timestamp !== undefined && typeof fi.timestamp !== 'number') { + throw new TypeError('timestamp property of FileInfo has to be number!'); + } + + if (fi.owner !== undefined && !Utils.isHexEthAddress(fi.owner)) { + throw new TypeError('owner property of FileInfo has to be string!'); + } + + if (fi.fileName !== undefined && typeof fi.fileName !== 'string') { + throw new TypeError('fileName property of FileInfo has to be string!'); + } + + if (fi.preview !== undefined && typeof fi.preview !== 'string') { + throw new TypeError('preview property of FileInfo has to be string!'); + } + + if (fi.shared !== undefined && typeof fi.shared !== 'boolean') { + throw new TypeError('shared property of FileInfo has to be boolean!'); + } + + if (fi.redundancyLevel !== undefined && typeof fi.redundancyLevel !== 'number') { + throw new TypeError('redundancyLevel property of FileInfo has to be number!'); + } +} + +export function assertShareItem(value: unknown): asserts value is ShareItem { + if (!isStrictlyObject(value)) { + throw new TypeError('ShareItem has to be object!'); + } + + const item = value as unknown as ShareItem; + + if (!isStrictlyObject(item.fileInfo)) { + throw new TypeError('ShareItem fileInfo has to be object!'); + } + + if (item.timestamp !== undefined && typeof item.timestamp !== 'number') { + throw new TypeError('timestamp property of ShareItem has to be number!'); + } + + if (item.message !== undefined && typeof item.message !== 'string') { + throw new TypeError('message property of ShareItem has to be string!'); + } +} + +export function assertReferenceWithHistory(value: unknown): asserts value is ReferenceWithHistory { + if (!isStrictlyObject(value)) { + throw new TypeError('ReferenceWithHistory has to be object!'); + } + + const rwh = value as unknown as ReferenceWithHistory; + + assertReference(rwh.historyRef); + + assertReference(rwh.reference); +} + +export function assertWrappedMantarayFeed(value: unknown): asserts value is WrappedMantarayFeed { + if (!isStrictlyObject(value)) { + throw new TypeError('WrappedMantarayFeed has to be object!'); + } + + assertReferenceWithHistory(value); + + const wmf = value as unknown as WrappedMantarayFeed; + + if (wmf.eFileRef !== undefined) { + assertReference(wmf.eFileRef); + } + + if (wmf.eGranteeRef !== undefined) { + assertReference(wmf.eGranteeRef); + } +} + +export function decodeBytesToPath(bytes: Uint8Array): string { + if (bytes.length !== REFERENCE_BYTES_LENGTH) { + const paddedBytes = new Uint8Array(REFERENCE_BYTES_LENGTH); + paddedBytes.set(bytes.slice(0, REFERENCE_BYTES_LENGTH)); // Truncate or pad the input to ensure it's 32 bytes bytes = paddedBytes; } return new TextDecoder().decode(bytes); } + +export function encodePathToBytes(pathString: string): Uint8Array { + return new TextEncoder().encode(pathString); +} + +export function makeBeeRequestOptions( + historyRef?: string, + publisher?: string, + timestamp?: number, + act?: boolean, +): BeeRequestOptions { + const options: BeeRequestOptions = {}; + if (historyRef !== undefined) { + options.headers = { 'swarm-act-history-address': historyRef }; + } + if (publisher !== undefined) { + options.headers = { + ...options.headers, + 'swarm-act-publisher': publisher, + }; + } + if (timestamp !== undefined) { + options.headers = { ...options.headers, 'swarm-act-timestamp': timestamp.toString() }; + } + if (act) { + options.headers = { ...options.headers, 'swarm-act': 'true' }; + } + + return options; +} + +export function numberToFeedIndex(index: number | undefined): string | undefined { + if (index === undefined) { + return undefined; + } + const bytes = new Uint8Array(8); + const dv = new DataView(bytes.buffer); + dv.setUint32(4, index); + + return Utils.bytesToHex(bytes); +} + +export function makeNumericIndex(index: Index): number { + if (index instanceof Uint8Array) { + return Binary.uint64BEToNumber(index); + } + + if (typeof index === 'string') { + const base = 16; + const ix = parseInt(index, base); + if (isNaN(ix)) { + throw new TypeError(`Invalid index: ${index}`); + } + return ix; + } + + if (typeof index === 'number') { + return index; + } + + throw new TypeError(`Unknown type of index: ${index}`); +} + +// status is undefined in the error object +// Determines if the error is about 'Not Found' +export function isNotFoundError(error: any): boolean { + return error.stack.includes('404') || error.message.includes('Not Found') || error.message.includes('404'); +} + +export function getRandomTopicHex(): Topic { + return Utils.bytesToHex(getRandomBytes(TOPIC_BYTES_LENGTH), TOPIC_HEX_LENGTH); +} + +export function getRandomBytes(len: number): Buffer { + return randomBytes(len); +} diff --git a/tests/fileManager.test.ts b/tests/fileManager.spec.ts similarity index 99% rename from tests/fileManager.test.ts rename to tests/fileManager.spec.ts index ad1d726..13cce59 100644 --- a/tests/fileManager.test.ts +++ b/tests/fileManager.spec.ts @@ -7,6 +7,7 @@ import { FileManager } from '../src/fileManager'; import { encodePathToBytes } from '../src/utils'; import { createMockBee, createMockMantarayNode } from './mockHelpers'; +import { BEE_URL } from './utils'; jest.mock('@solarpunkltd/mantaray-js', () => { const mockMantarayNode = jest.fn(() => createMockMantarayNode()); @@ -24,10 +25,9 @@ jest.mock('fs', () => { describe('FileManager - Setup', () => { it('should initialize with a valid Bee URL', () => { + const bee = new Bee(BEE_URL); const validPrivateKey = '0x'.padEnd(66, 'a'); // 64-character hex string padded with 'a' - const fileManager = new FileManager('http://localhost:1633', validPrivateKey); - expect(fileManager.bee).toBeTruthy(); - expect(fileManager.mantaray).toBeTruthy(); + const fileManager = new FileManager(bee, validPrivateKey); }); }); @@ -39,7 +39,7 @@ describe('FileManager - Initialize', () => { beforeEach(() => { mockBee = createMockBee(); fileManager = new FileManager('http://localhost:1633', privateKey); - fileManager.importedFiles = []; + fileManager.importedFileInfoList = []; jest.clearAllMocks(); }); diff --git a/tests/files/test.txt b/tests/files/test.txt new file mode 100644 index 0000000..c57eff5 --- /dev/null +++ b/tests/files/test.txt @@ -0,0 +1 @@ +Hello World! \ No newline at end of file diff --git a/tests/integration/fileManager-class.spec.ts b/tests/integration/fileManager-class.spec.ts new file mode 100644 index 0000000..8173cfc --- /dev/null +++ b/tests/integration/fileManager-class.spec.ts @@ -0,0 +1,124 @@ +import { Bee, Topic } from '@ethersphere/bee-js'; +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; + +import { OWNER_FEED_STAMP_LABEL, REFERENCE_LIST_TOPIC, SWARM_ZERO_ADDRESS } from '../../src/constants'; +import { FileManager } from '../../src/fileManager'; +import { FileInfo } from '../../src/types'; +import { makeBeeRequestOptions } from '../../src/utils'; +import { + BEE_URL, + buyStamp, + getTestFile, + initTestMantarayNode, + MOCK_PRIV_KEY, + MOCK_WALLET, + OTHER_BEE_URL, + OTHER_MOCK_PRIV_KEY, +} from '../utils'; + +describe('FileManager initialization', () => { + beforeEach(async () => { + const bee = new Bee(BEE_URL); + await buyStamp(bee, OWNER_FEED_STAMP_LABEL); + + jest.resetAllMocks(); + }); + + it('should create and initialize a new instance', async () => { + const bee = new Bee(OTHER_BEE_URL); + const fileManager = new FileManager(bee, OTHER_MOCK_PRIV_KEY); + try { + await fileManager.initialize(); + } catch (error: any) { + expect(error).toEqual('Owner stamp not found'); + } + const stamps = await fileManager.getStamps(); + expect(stamps).toEqual([]); + expect(fileManager.getFileInfoList()).toEqual([]); + expect(fileManager.getSharedWithMe()).toEqual([]); + }); + + it('should fetch the owner stamp and initialize the owner feed and topic', async () => { + const bee = new Bee(BEE_URL); + const batchId = await buyStamp(bee, OWNER_FEED_STAMP_LABEL); + const fileManager = new FileManager(bee, MOCK_PRIV_KEY); + await fileManager.initialize(); + const stamps = await fileManager.getStamps(); + const mockPubKey = (await bee.getNodeAddresses()).publicKey; + + expect(stamps[0].batchID).toEqual(batchId); + expect(stamps[0].label).toEqual(OWNER_FEED_STAMP_LABEL); + expect(fileManager.getCachedStamp(batchId)).toEqual(stamps[0]); + expect(fileManager.getFileInfoList()).toEqual([]); + expect(fileManager.getSharedWithMe()).toEqual([]); + + const referenceListTopicHex = bee.makeFeedTopic(REFERENCE_LIST_TOPIC); + const feedTopicData = await fileManager.getFeedData(referenceListTopicHex, MOCK_WALLET.address, 0); + const topicHistory = await fileManager.getFeedData(referenceListTopicHex, MOCK_WALLET.address, 1); + const options = makeBeeRequestOptions(topicHistory.reference, mockPubKey); + const topicHex = (await bee.downloadData(feedTopicData.reference, options)).text() as Topic; + + expect(topicHex).not.toEqual(SWARM_ZERO_ADDRESS); + // test re-initialization + await fileManager.initialize(); + const reinitTopicHex = (await bee.downloadData(feedTopicData.reference, options)).text() as Topic; + + expect(topicHex).toEqual(reinitTopicHex); + }); + + it('should throw an error if someone else than the owner tries to read the owner feed', async () => { + const bee = new Bee(BEE_URL); + const otherBee = new Bee(OTHER_BEE_URL); + const fileManager = new FileManager(bee, MOCK_PRIV_KEY); + await fileManager.initialize(); + const mockOtherPubKey = (await otherBee.getNodeAddresses()).publicKey; + + const referenceListTopicHex = bee.makeFeedTopic(REFERENCE_LIST_TOPIC); + const feedTopicData = await fileManager.getFeedData(referenceListTopicHex, MOCK_WALLET.address, 0); + const topicHistory = await fileManager.getFeedData(referenceListTopicHex, MOCK_WALLET.address, 1); + const otherOptions = makeBeeRequestOptions(topicHistory.reference, mockOtherPubKey); + + try { + await bee.downloadData(feedTopicData.reference, otherOptions); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect((error as Error).stack?.includes('404')).toBeTruthy(); + } + }); + + it('should upload a file and save it on swarm', async () => { + const expectedFileData = getTestFile('files/test.txt'); + const bee = new Bee(BEE_URL); + const mockPubKey = (await bee.getNodeAddresses()).publicKey; + const testStampId = await buyStamp(bee, 'testStamp'); + let actualFileInfo: FileInfo; + { + const fileManager = new FileManager(bee, MOCK_PRIV_KEY); + await fileManager.initialize(); + + const testMantaray = initTestMantarayNode(); + await fileManager.upload(testStampId, testMantaray, '../tests/files/test.txt', undefined, undefined); + + const fileInfoList = fileManager.getFileInfoList(); + console.log('baogy test fileInfoList: ', fileInfoList); + expect(fileInfoList.length).toEqual(1); + + actualFileInfo = fileInfoList[0]; + const options = makeBeeRequestOptions(actualFileInfo.historyRef, mockPubKey); + const actualFileData = await bee.downloadFile(actualFileInfo.eFileRef, undefined, options); + + expect(actualFileData.data.text()).toEqual(expectedFileData); + } + // re-init fileManager after it goes out of scope to test if the file is saved on the feed + const fileManager = new FileManager(bee, MOCK_PRIV_KEY); + await fileManager.initialize(); + const fileInfoList = fileManager.getFileInfoList(); + const downloadedFileInfo = fileInfoList[0]; + expect(fileInfoList.length).toEqual(1); + expect(downloadedFileInfo).toEqual(actualFileInfo); + + const options = makeBeeRequestOptions(downloadedFileInfo.historyRef, mockPubKey); + const actualFileData = await bee.downloadFile(downloadedFileInfo.eFileRef, undefined, options); + expect(actualFileData.data.text()).toEqual(expectedFileData); + }); +}); diff --git a/tests/mockHelpers.ts b/tests/mockHelpers.ts index ebb7e22..33cc032 100644 --- a/tests/mockHelpers.ts +++ b/tests/mockHelpers.ts @@ -1,5 +1,4 @@ -import { Bee, Reference, Topic } from '@ethersphere/bee-js'; -import { TextDecoder, TextEncoder } from 'util'; +import { Bee, Reference, Topic, Utils } from '@ethersphere/bee-js'; export function createMockBee(): Partial { return { @@ -111,11 +110,11 @@ export function createMockBee(): Partial { export function createMockMantarayNode(customForks: Record = {}, excludeDefaultForks = false): any { const defaultForks: Record = { file: { - prefix: encodePathToBytes('file'), + prefix: Utils.hexToBytes('file'), node: { forks: { '1.txt': { - prefix: encodePathToBytes('1.txt'), + prefix: Utils.hexToBytes('1.txt'), node: { isValueType: () => true, getEntry: 'a'.repeat(64), // Valid Uint8Array @@ -126,7 +125,7 @@ export function createMockMantarayNode(customForks: Record = {}, ex }, }, '2.txt': { - prefix: encodePathToBytes('2.txt'), + prefix: Utils.hexToBytes('2.txt'), node: { isValueType: () => true, getEntry: 'b'.repeat(64), // Valid Uint8Array @@ -148,7 +147,7 @@ export function createMockMantarayNode(customForks: Record = {}, ex return { forks, addFork: jest.fn((path: Uint8Array, reference: Uint8Array) => { - const decodedPath = new TextDecoder().decode(path); + const decodedPath = Utils.bytesToHex(path); console.log(`Mock addFork called with path: ${decodedPath}`); forks[decodedPath] = { prefix: path, @@ -162,7 +161,3 @@ export function createMockMantarayNode(customForks: Record = {}, ex }), }; } - -function encodePathToBytes(path: string): Uint8Array { - return path ? new TextEncoder().encode(path) : new Uint8Array(); -} diff --git a/tests/utils.ts b/tests/utils.ts new file mode 100644 index 0000000..841f187 --- /dev/null +++ b/tests/utils.ts @@ -0,0 +1,45 @@ +import { BatchId, Bee, Data, TOPIC_BYTES_LENGTH, Utils } from '@ethersphere/bee-js'; +import { initManifestNode, MantarayNode } from '@solarpunkltd/mantaray-js'; +import { randomBytes } from 'crypto'; +import { Wallet } from 'ethers'; +import { readFileSync } from 'fs'; +import path from 'path'; + +import { Bytes } from '../src/types'; + +export const BEE_URL = 'http://localhost:1633'; +export const OTHER_BEE_URL = 'http://localhost:1733'; +export const DEFAULT_BATCH_DEPTH = 21; +export const DEFAULT_BATCH_AMOUNT = '500000000'; +export const MOCK_PRIV_KEY = '634fb5a872396d9693e5c9f9d7233cfa93f395c093371017ff44aa9ae6564cdd'; +export const MOCK_WALLET = new Wallet(MOCK_PRIV_KEY); +export const OTHER_MOCK_PRIV_KEY = '734fb5a872396d9693e5c9f9d7233cfa93f395c093371017ff44aa9ae6564cd7'; +export const OTHER_MOCK_WALLET = new Wallet(OTHER_MOCK_PRIV_KEY); +export const MOCK_SIGNER = { + address: Utils.hexToBytes(MOCK_WALLET.address.slice(2)), + sign: async (data: Data): Promise => { + return await MOCK_WALLET.signMessage(data); + }, +}; + +export async function buyStamp(bee: Bee, label?: string): Promise { + const ownerStamp = (await bee.getAllPostageBatch()).find(async (b) => { + b.label === label; + }); + if (ownerStamp && ownerStamp.usable) { + return ownerStamp.batchID; + } + + return await bee.createPostageBatch(DEFAULT_BATCH_AMOUNT, DEFAULT_BATCH_DEPTH, { + waitForUsable: true, + label: label, + }); +} + +export function initTestMantarayNode(): MantarayNode { + return initManifestNode({ obfuscationKey: randomBytes(TOPIC_BYTES_LENGTH) as Bytes<32> }); +} + +export function getTestFile(relativePath: string): string { + return readFileSync(path.resolve(__dirname, relativePath), 'utf-8'); +} diff --git a/tsconfig.json b/tsconfig.json index b07cf0d..2311fa7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,8 +13,8 @@ "removeComments": true, "outDir": "./dist", "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, + "noUnusedLocals": false, + "noUnusedParameters": false, "noFallthroughCasesInSwitch": true, "strictPropertyInitialization": false, "isolatedModules": true,