diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index de2528a8..4c742230 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -140,16 +140,13 @@ jobs: - uses: ipfs/aegir/actions/cache-node-modules@master - if: ${{ steps.release.outputs.release_created }} name: Run release version - run: npm publish + run: npm run --if-present release env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - if: ${{ steps.release.outputs.release_created }} - name: Publish docs - run: npm run --if-present docs + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - if: ${{ !steps.release.outputs.release_created }} name: Run release rc run: | - npm version `node -p -e "require('./package.json').version"`-`git rev-parse --short HEAD` --no-git-tag-version - npm publish --tag next + npm run --if-present release:rc env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 910f6339..7ad9e674 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ dist node_modules package-lock.json yarn.lock +.vscode diff --git a/.npmrc b/.npmrc deleted file mode 100644 index c5ebf5e2..00000000 --- a/.npmrc +++ /dev/null @@ -1,2 +0,0 @@ -; package-lock with tarball deps breaks lerna/nx - remove when https://github.com/semantic-release/github/pull/487 is merged -package-lock=false diff --git a/README.md b/README.md index d1cd9f5d..42c9e08b 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,48 @@ -# Helia — an implementation of IPFS in JavaScript +helia logo -### Project status +# helia -This project is pre-alpha and is currently in development. An initial v1 release is planned for [late Q1 2023](/ROADMAP.md#late-q1-march). Helia is being built in the open; community contributors are welcome! - -The core of IPFS is the [Files API](https://github.com/ipfs/js-ipfs/blob/master/docs/core-api/FILES.md), which will likewise be implemented in Helia. These initial building blocks are in development now; have a look at this repo's PR(s). For more info about Helia, please see the [Roadmap](https://github.com/ipfs/helia/issues/5) and the [Manifesto](MANIFESTO.md). +[![ipfs.tech](https://img.shields.io/badge/project-IPFS-blue.svg?style=flat-square)](https://ipfs.tech) +[![Discuss](https://img.shields.io/discourse/https/discuss.ipfs.tech/posts.svg?style=flat-square)](https://discuss.ipfs.tech) +[![codecov](https://img.shields.io/codecov/c/github/ipfs/helia.svg?style=flat-square)](https://codecov.io/gh/ipfs/helia) +[![CI](https://img.shields.io/github/actions/workflow/status/ipfs/helia/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipfs/helia/actions/workflows/js-test-and-release.yml?query=branch%3Amain) -We are also sharing about the progress so far, and discussing how you can get involved, at [Helia Demo Day](https://lu.ma/helia) every couple weeks. We'd love to see you there! +> An implementation of IPFS in JavaScript -## Table of Contents +## Table of contents +- [Structure](#structure) +- [Project status](#project-status) - [Name](#name) - [Background](#background) - [Roadmap](#roadmap) -- [Contribute](#contribute) +- [API Docs](#api-docs) - [License](#license) - - [Contribution](#contribution) +- [Contribute](#contribute) + +## Structure + +- [`/packages/helia`](./packages/helia) An implementation of IPFS in JavaScript +- [`/packages/interface`](./packages/interface) The Helia API +- [`/packages/interop`](./packages/interop) Interop tests for Helia + +## Project status + +This project is pre-alpha and is currently in development. An initial v1 release is planned for [late Q1 2023](/ROADMAP.md#late-q1-march). Helia is being built in the open; community contributors are welcome! + +The core of IPFS is the [Files API](https://github.com/ipfs/js-ipfs/blob/master/docs/core-api/FILES.md), which will likewise be implemented in Helia. These initial building blocks are in development now; have a look at this repo's PR(s). For more info about Helia, please see the [Roadmap](https://github.com/ipfs/helia/issues/5) and the [Manifesto](MANIFESTO.md). + +We are also sharing about the progress so far, and discussing how you can get involved, at [Helia Demo Day](https://lu.ma/helia) every couple weeks. We'd love to see you there! ## Name -Helia (_HEE-lee-ah_) is the Latin spelling of Ἡλιη -- in Greek mythology, one of the [Heliades](https://www.wikidata.org/wiki/Q12656412): the daughters of the sun god Helios. When their brother Phaethon died trying to drive the sun chariot across the sky, their tears of mourning fell to earth as amber, which is yellow (sort of), and so is JavaScript. They were then turned into [poplar](https://en.wiktionary.org/wiki/poplar) trees and, well, JavaScript is quite popular. +Helia (*HEE-lee-ah*) is the Latin spelling of Ἡλιη -- in Greek mythology, one of the [Heliades](https://www.wikidata.org/wiki/Q12656412): the daughters of the sun god Helios. When their brother Phaethon died trying to drive the sun chariot across the sky, their tears of mourning fell to earth as amber, which is yellow (sort of), and so is JavaScript. They were then turned into [poplar](https://en.wiktionary.org/wiki/poplar) trees and, well, JavaScript is quite popular. In Oct–Dec 2022, IP Stewards [sought](https://github.com/ipfs/pomegranate/issues/3) community input for the name of this project. After considering 20 suggestions and holding a couple of polls, the name **Helia** was chosen. Here's [why it's not named IPFS](https://github.com/ipfs/ipfs/issues/470). ## Background -This project aims to build a lean, modular, modern implementation of IPFS, the Interplanetary File System. +This project aims to build a lean, modular, and modern implementation of IPFS, the InterPlanetary File System. For more information, see the [State of IPFS in JS (blog post)](https://blog.ipfs.tech/state-of-ipfs-in-js/). @@ -33,17 +50,27 @@ For more information, see the [State of IPFS in JS (blog post)](https://blog.ipf Please find and comment on [the Roadmap here](https://github.com/ipfs/helia/issues/5). -## Contribute - This IPFS implementation in JavaScript is a work in progress. [Here are some ways you can help](https://blog.ipfs.tech/state-of-ipfs-in-js/#%F0%9F%A4%9D-ways-you-can-help)! - + +## API Docs + +- + ## License Licensed under either of - * Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / http://www.apache.org/licenses/LICENSE-2.0) - * MIT ([LICENSE-MIT](LICENSE-MIT) / http://opensource.org/licenses/MIT) +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribute + +Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia/issues). + +Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general. -### Contribution +Please be aware that all interactions related to this repo are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. + +[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) diff --git a/assets/helia.idraw b/assets/helia.idraw new file mode 100644 index 00000000..0afaddbf Binary files /dev/null and b/assets/helia.idraw differ diff --git a/assets/helia.png b/assets/helia.png new file mode 100644 index 00000000..bfde8c54 Binary files /dev/null and b/assets/helia.png differ diff --git a/lerna.json b/lerna.json deleted file mode 100644 index 9301e47a..00000000 --- a/lerna.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "lerna": "6.1.0", - "useWorkspaces": true, - "version": "independent", - "command": { - "run": { - "stream": true - } - } -} diff --git a/package.json b/package.json index 2f782fe3..a2ce5243 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "helia-monorepo", + "name": "helia", "version": "0.0.0", "description": "An implementation of IPFS in JavaScript", "license": "Apache-2.0 OR MIT", @@ -20,29 +20,30 @@ }, "private": true, "scripts": { - "reset": "lerna run clean && rimraf ./node_modules ./package-lock.json packages/*/node_modules packages/*/package-lock.json packages/*/dist", - "test": "lerna run --concurrency 1 test -- --", - "test:node": "lerna run --concurrency 1 test:node -- --", - "test:chrome": "lerna run --concurrency 1 test:chrome -- --", - "test:chrome-webworker": "lerna --concurrency 1 run test:chrome-webworker -- --", - "test:firefox": "lerna run --concurrency 1 test:firefox -- --", - "test:firefox-webworker": "lerna run --concurrency 1 test:firefox-webworker -- --", - "test:electron-main": "lerna run --concurrency 1 test:electron-main -- --", - "test:electron-renderer": "lerna run --concurrency 1 test:electron-renderer -- --", - "clean": "lerna run clean", - "generate": "lerna run generate", - "build": "lerna run build", - "lint": "lerna run lint", + "reset": "aegir run clean && aegir clean **/node_modules **/package-lock.json", + "test": "aegir run test", + "test:node": "aegir run test:node", + "test:chrome": "aegir run test:chrome", + "test:chrome-webworker": "aegir run test:chrome-webworker", + "test:firefox": "aegir run test:firefox", + "test:firefox-webworker": "aegir run test:firefox-webworker", + "test:electron-main": "aegir run test:electron-main", + "test:electron-renderer": "aegir run test:electron-renderer", + "clean": "aegir run clean", + "generate": "aegir run generate", + "build": "aegir run build", + "lint": "aegir run lint", "docs": "NODE_OPTIONS=--max_old_space_size=4096 aegir docs", "docs:no-publish": "npm run docs -- --publish false", - "dep-check": "lerna run dep-check", - "release": "npm run docs:no-publish && lerna run --concurrency 1 release && npm run docs" + "dep-check": "aegir run dep-check", + "release": "npm run docs:no-publish && npm run release:npm && npm run docs", + "release:npm": "aegir exec npm -- publish", + "release:rc": "aegir release-rc" }, "dependencies": { - "aegir": "^38.1.0", - "lerna": "^6.1.0", - "rimraf": "^4.1.1" + "aegir": "^38.1.0" }, + "type": "module", "workspaces": [ "packages/*" ] diff --git a/packages/helia/LICENSE b/packages/helia/LICENSE new file mode 100644 index 00000000..20ce483c --- /dev/null +++ b/packages/helia/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/helia/LICENSE-APACHE b/packages/helia/LICENSE-APACHE new file mode 100644 index 00000000..14478a3b --- /dev/null +++ b/packages/helia/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/helia/LICENSE-MIT b/packages/helia/LICENSE-MIT new file mode 100644 index 00000000..72dc60d8 --- /dev/null +++ b/packages/helia/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/helia/README.md b/packages/helia/README.md new file mode 100644 index 00000000..7be60f02 --- /dev/null +++ b/packages/helia/README.md @@ -0,0 +1,53 @@ +# helia + +[![ipfs.tech](https://img.shields.io/badge/project-IPFS-blue.svg?style=flat-square)](https://ipfs.tech) +[![Discuss](https://img.shields.io/discourse/https/discuss.ipfs.tech/posts.svg?style=flat-square)](https://discuss.ipfs.tech) +[![codecov](https://img.shields.io/codecov/c/github/ipfs/helia.svg?style=flat-square)](https://codecov.io/gh/ipfs/helia) +[![CI](https://img.shields.io/github/actions/workflow/status/ipfs/helia/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipfs/helia/actions/workflows/js-test-and-release.yml?query=branch%3Amain) + +> An implementation of IPFS in JavaScript + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribute + +Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia/issues). + +Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general. + +Please be aware that all interactions related to this repo are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. + +[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) diff --git a/packages/helia/package.json b/packages/helia/package.json new file mode 100644 index 00000000..3f9fdc1c --- /dev/null +++ b/packages/helia/package.json @@ -0,0 +1,165 @@ +{ + "name": "helia", + "version": "0.0.0", + "description": "An implementation of IPFS in JavaScript", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/ipfs/helia/tree/master/packages/helia#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/ipfs/helia.git" + }, + "bugs": { + "url": "https://github.com/ipfs/helia/issues" + }, + "keywords": [ + "IPFS" + ], + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "release": { + "branches": [ + "main" + ], + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "conventionalcommits", + "releaseRules": [ + { + "breaking": true, + "release": "major" + }, + { + "revert": true, + "release": "patch" + }, + { + "type": "feat", + "release": "minor" + }, + { + "type": "fix", + "release": "patch" + }, + { + "type": "docs", + "release": "patch" + }, + { + "type": "test", + "release": "patch" + }, + { + "type": "deps", + "release": "patch" + }, + { + "scope": "no-release", + "release": false + } + ] + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits", + "presetConfig": { + "types": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "chore", + "section": "Trivial Changes" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "deps", + "section": "Dependencies" + }, + { + "type": "test", + "section": "Tests" + } + ] + } + } + ], + "@semantic-release/changelog", + "@semantic-release/npm", + "@semantic-release/github", + "@semantic-release/git" + ] + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main", + "release": "aegir release" + }, + "dependencies": { + "@helia/interface": "~0.0.0", + "@libp2p/interface-libp2p": "^1.1.0", + "@libp2p/interfaces": "^3.3.1", + "blockstore-core": "^3.0.0", + "interface-blockstore": "^4.0.1", + "interface-datastore": "^7.0.3", + "interface-store": "^3.0.4", + "ipfs-bitswap": "^16.0.0", + "it-filter": "^2.0.0", + "it-merge": "^2.0.0", + "it-pushable": "^3.1.2", + "multiformats": "^11.0.1" + }, + "devDependencies": { + "@chainsafe/libp2p-noise": "^11.0.0", + "@chainsafe/libp2p-yamux": "^3.0.5", + "@libp2p/websockets": "^5.0.3", + "aegir": "^38.1.0", + "datastore-core": "^8.0.4", + "libp2p": "^0.42.2" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/helia/src/helia.ts b/packages/helia/src/helia.ts new file mode 100644 index 00000000..621db2e8 --- /dev/null +++ b/packages/helia/src/helia.ts @@ -0,0 +1,68 @@ +import type { Helia, InfoResponse } from '@helia/interface' +import type { Libp2p } from '@libp2p/interface-libp2p' +import type { Blockstore } from 'interface-blockstore' +import type { Datastore } from 'interface-datastore' +import { identity } from 'multiformats/hashes/identity' +import { sha256, sha512 } from 'multiformats/hashes/sha2' +import type { MultihashHasher } from 'multiformats/hashes/interface' +import type { HeliaInit } from '.' +import { Bitswap, createBitswap } from 'ipfs-bitswap' +import { BlockStorage } from './storage.js' + +export class HeliaImpl implements Helia { + public libp2p: Libp2p + public blockstore: Blockstore + public datastore: Datastore + + #bitswap: Bitswap + + constructor (init: HeliaInit) { + const hashers: MultihashHasher[] = [ + sha256, + sha512, + identity, + ...(init.hashers ?? []) + ] + + this.#bitswap = createBitswap(init.libp2p, init.blockstore, { + hashLoader: { + getHasher: async (codecOrName: string | number) => { + const hasher = hashers.find(hasher => { + return hasher.code === codecOrName || hasher.name === codecOrName + }) + + if (hasher != null) { + return await Promise.resolve(hasher) + } + + throw new Error(`Could not load hasher for code/name "${codecOrName}"`) + } + } + }) + + this.libp2p = init.libp2p + this.blockstore = new BlockStorage(init.blockstore, this.#bitswap) + this.datastore = init.datastore + } + + async start (): Promise { + this.#bitswap.start() + await this.libp2p.start() + } + + async stop (): Promise { + this.#bitswap.stop() + await this.libp2p.stop() + } + + async info (): Promise { + return { + peerId: this.libp2p.peerId, + multiaddrs: this.libp2p.getMultiaddrs(), + agentVersion: this.libp2p.identifyService.host.agentVersion, + protocolVersion: this.libp2p.identifyService.host.protocolVersion, + protocols: this.libp2p.getProtocols(), + status: this.libp2p.isStarted() ? 'running' : 'stopped' + } + } +} diff --git a/packages/helia/src/index.ts b/packages/helia/src/index.ts new file mode 100644 index 00000000..419b036f --- /dev/null +++ b/packages/helia/src/index.ts @@ -0,0 +1,78 @@ +/** + * @packageDocumentation + * + * Create a Helia node. + * + * @example + * + * ```typescript + * import { createLibp2p } from 'libp2p' + * import { MemoryDatastore } from 'datastore-core' + * import { MemoryBlockstore } from 'blockstore-core' + * import { createHelia } from 'helia' + * import { unixfs } from '@helia/unixfs' + * import { CID } from 'multiformats/cid' + * + * const node = await createHelia({ + * blockstore: new MemoryBlockstore(), + * datastore: new MemoryDatastore(), + * libp2p: await createLibp2p({ + * //... libp2p options + * }) + * }) + * const fs = unixfs(node) + * fs.cat(CID.parse('bafyFoo')) + * ``` + */ + +import type { Helia } from '@helia/interface' +import type { Libp2p } from '@libp2p/interface-libp2p' +import type { Blockstore } from 'interface-blockstore' +import type { Datastore } from 'interface-datastore' +import type { MultihashHasher } from 'multiformats/hashes/interface' +import { HeliaImpl } from './helia.js' + +/** + * Options used to create a Helia node. + */ +export interface HeliaInit { + /** + * A libp2p node is required to perform network operations + */ + libp2p: Libp2p + + /** + * The blockstore is where blocks are stored + */ + blockstore: Blockstore + + /** + * The datastore is where data is stored + */ + datastore: Datastore + + /** + * By default sha256, sha512 and identity hashes are supported for + * bitswap operations. To bitswap blocks with CIDs using other hashes + * pass appropriate MultihashHashers here. + */ + hashers?: MultihashHasher[] + + /** + * Pass `false` to not start the helia node + */ + start?: boolean +} + +/** + * Create and return a Helia node + */ +export async function createHelia (init: HeliaInit): Promise { + const helia = new HeliaImpl(init) + + if (init.start !== false) { + await helia.start() + } + + return helia +} diff --git a/packages/helia/src/storage.ts b/packages/helia/src/storage.ts new file mode 100644 index 00000000..59f8b7c3 --- /dev/null +++ b/packages/helia/src/storage.ts @@ -0,0 +1,138 @@ +import { BaseBlockstore } from 'blockstore-core' +import merge from 'it-merge' +import { pushable } from 'it-pushable' +import filter from 'it-filter' +import type { Blockstore, KeyQuery, Query } from 'interface-blockstore' +import type { Bitswap } from 'ipfs-bitswap' +import type { CID } from 'multiformats/cid' +import type { AbortOptions } from '@libp2p/interfaces' +import type { AwaitIterable } from 'interface-store' + +export interface BlockStorageOptions extends AbortOptions { + progress?: (evt: Event) => void +} + +/** + * BlockStorage is a hybrid blockstore that puts/gets blocks from a configured + * blockstore (that may be on disk, s3, or something else). If the blocks are + * not present Bitswap will be used to fetch them from network peers. + */ +export class BlockStorage extends BaseBlockstore implements Blockstore { + private readonly child: Blockstore + private readonly bitswap: Bitswap + + /** + * Create a new BlockStorage + */ + constructor (blockstore: Blockstore, bitswap: Bitswap) { + super() + + this.child = blockstore + this.bitswap = bitswap + } + + async open (): Promise { + await this.child.open() + } + + async close (): Promise { + await this.child.close() + } + + unwrap (): Blockstore { + return this.child + } + + /** + * Put a block to the underlying datastore + */ + async put (cid: CID, block: Uint8Array, options: AbortOptions = {}): Promise { + if (await this.has(cid)) { + return + } + + if (this.bitswap.isStarted()) { + await this.bitswap.put(cid, block, options) + } else { + await this.child.put(cid, block, options) + } + } + + /** + * Put a multiple blocks to the underlying datastore + */ + async * putMany (blocks: AwaitIterable<{ key: CID, value: Uint8Array }>, options: AbortOptions = {}): AsyncGenerator<{ key: CID, value: Uint8Array }, void, undefined> { + const missingBlocks = filter(blocks, async ({ key }) => { return !(await this.has(key)) }) + + if (this.bitswap.isStarted()) { + yield * this.bitswap.putMany(missingBlocks, options) + } else { + yield * this.child.putMany(missingBlocks, options) + } + } + + /** + * Get a block by cid + */ + async get (cid: CID, options: BlockStorageOptions = {}): Promise { + if (!(await this.has(cid)) && this.bitswap.isStarted()) { + return await this.bitswap.get(cid, options) + } else { + return await this.child.get(cid, options) + } + } + + /** + * Get multiple blocks back from an array of cids + */ + async * getMany (cids: AwaitIterable, options: BlockStorageOptions = {}): AsyncGenerator { + const getFromBitswap = pushable({ objectMode: true }) + const getFromChild = pushable({ objectMode: true }) + + void Promise.resolve().then(async () => { + for await (const cid of cids) { + if (!(await this.has(cid)) && this.bitswap.isStarted()) { + getFromBitswap.push(cid) + } else { + getFromChild.push(cid) + } + } + + getFromBitswap.end() + getFromChild.end() + }).catch(err => { + getFromBitswap.throw(err) + }) + + yield * merge( + this.bitswap.getMany(getFromBitswap, options), + this.child.getMany(getFromChild, options) + ) + } + + /** + * Delete a block from the blockstore + */ + async delete (cid: CID, options: AbortOptions = {}): Promise { + await this.child.delete(cid, options) + } + + /** + * Delete multiple blocks from the blockstore + */ + async * deleteMany (cids: AwaitIterable, options: AbortOptions = {}): AsyncGenerator { + yield * this.child.deleteMany(cids, options) + } + + async has (cid: CID, options: AbortOptions = {}): Promise { + return await this.child.has(cid, options) + } + + async * query (q: Query, options: AbortOptions = {}): AsyncGenerator<{ key: CID, value: Uint8Array }, void, undefined> { + yield * this.child.query(q, options) + } + + async * queryKeys (q: KeyQuery, options: AbortOptions = {}): AsyncGenerator { + yield * this.child.queryKeys(q, options) + } +} diff --git a/packages/helia/test/index.spec.ts b/packages/helia/test/index.spec.ts new file mode 100644 index 00000000..bb9a5ff9 --- /dev/null +++ b/packages/helia/test/index.spec.ts @@ -0,0 +1,99 @@ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' +import { MemoryBlockstore } from 'blockstore-core' +import { MemoryDatastore } from 'datastore-core' +import { createLibp2p } from 'libp2p' +import { webSockets } from '@libp2p/websockets' +import { noise } from '@chainsafe/libp2p-noise' +import { yamux } from '@chainsafe/libp2p-yamux' +import { createHelia } from '../src/index.js' +import type { Helia } from '@helia/interface' + +describe('helia', () => { + let helia: Helia + + beforeEach(async () => { + helia = await createHelia({ + datastore: new MemoryDatastore(), + blockstore: new MemoryBlockstore(), + libp2p: await createLibp2p({ + transports: [ + webSockets() + ], + connectionEncryption: [ + noise() + ], + streamMuxers: [ + yamux() + ] + }) + }) + }) + + afterEach(async () => { + if (helia != null) { + await helia.stop() + } + }) + + it('stops and starts', async () => { + const startedInfo = await helia.info() + + expect(startedInfo).to.have.property('status', 'running') + expect(startedInfo).to.have.property('protocols') + .with.property('length').that.is.greaterThan(0) + + await helia.stop() + + const stoppedInfo = await helia.info() + + expect(stoppedInfo).to.have.property('status', 'stopped') + expect(stoppedInfo).to.have.property('protocols') + .with.lengthOf(0) + }) + + it('returns node information', async () => { + const info = await helia.info() + + expect(info).to.have.property('peerId').that.is.ok() + expect(info).to.have.property('multiaddrs').that.is.an('array') + expect(info).to.have.property('agentVersion').that.is.a('string') + expect(info).to.have.property('protocolVersion').that.is.a('string') + }) + + it('should have a blockstore', async () => { + expect(helia).to.have.property('blockstore').that.is.ok() + }) + + it('should have a datastore', async () => { + expect(helia).to.have.property('datastore').that.is.ok() + }) + + it('should have a libp2p', async () => { + expect(helia).to.have.property('libp2p').that.is.ok() + }) + + it('allows creating offline node', async () => { + const helia = await createHelia({ + start: false, + datastore: new MemoryDatastore(), + blockstore: new MemoryBlockstore(), + libp2p: await createLibp2p({ + start: false, + transports: [ + webSockets() + ], + connectionEncryption: [ + noise() + ], + streamMuxers: [ + yamux() + ] + }) + }) + + const info = await helia.info() + + expect(info).to.have.property('status', 'stopped') + }) +}) diff --git a/packages/helia/tsconfig.json b/packages/helia/tsconfig.json new file mode 100644 index 00000000..4c0bdf77 --- /dev/null +++ b/packages/helia/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface" + } + ] +} diff --git a/packages/interface/LICENSE b/packages/interface/LICENSE new file mode 100644 index 00000000..20ce483c --- /dev/null +++ b/packages/interface/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/interface/LICENSE-APACHE b/packages/interface/LICENSE-APACHE new file mode 100644 index 00000000..14478a3b --- /dev/null +++ b/packages/interface/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/interface/LICENSE-MIT b/packages/interface/LICENSE-MIT new file mode 100644 index 00000000..72dc60d8 --- /dev/null +++ b/packages/interface/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/interface/README.md b/packages/interface/README.md new file mode 100644 index 00000000..94d30350 --- /dev/null +++ b/packages/interface/README.md @@ -0,0 +1,44 @@ +# @helia/interface + +[![ipfs.tech](https://img.shields.io/badge/project-IPFS-blue.svg?style=flat-square)](https://ipfs.tech) +[![Discuss](https://img.shields.io/discourse/https/discuss.ipfs.tech/posts.svg?style=flat-square)](https://discuss.ipfs.tech) +[![codecov](https://img.shields.io/codecov/c/github/ipfs/helia.svg?style=flat-square)](https://codecov.io/gh/ipfs/helia) +[![CI](https://img.shields.io/github/actions/workflow/status/ipfs/helia/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipfs/helia/actions/workflows/js-test-and-release.yml?query=branch%3Amain) + +> The Helia API + +## Table of contents + +- [Install](#install) +- [API Docs](#api-docs) +- [License](#license) +- [Contribute](#contribute) + +## Install + +```console +$ npm i @helia/interface +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribute + +Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia/issues). + +Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general. + +Please be aware that all interactions related to this repo are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. + +[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) diff --git a/packages/interface/package.json b/packages/interface/package.json new file mode 100644 index 00000000..96140295 --- /dev/null +++ b/packages/interface/package.json @@ -0,0 +1,167 @@ +{ + "name": "@helia/interface", + "version": "0.0.0", + "description": "The Helia API", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/ipfs/helia/tree/master/packages/interface#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/ipfs/helia.git" + }, + "bugs": { + "url": "https://github.com/ipfs/helia/issues" + }, + "keywords": [ + "IPFS" + ], + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ], + "src/*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ] + } + }, + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + }, + "./errors": { + "types": "./dist/src/errors.d.ts", + "import": "./dist/src/errors.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "release": { + "branches": [ + "main" + ], + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "conventionalcommits", + "releaseRules": [ + { + "breaking": true, + "release": "major" + }, + { + "revert": true, + "release": "patch" + }, + { + "type": "feat", + "release": "minor" + }, + { + "type": "fix", + "release": "patch" + }, + { + "type": "docs", + "release": "patch" + }, + { + "type": "test", + "release": "patch" + }, + { + "type": "deps", + "release": "patch" + }, + { + "scope": "no-release", + "release": false + } + ] + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits", + "presetConfig": { + "types": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "chore", + "section": "Trivial Changes" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "deps", + "section": "Dependencies" + }, + { + "type": "test", + "section": "Tests" + } + ] + } + } + ], + "@semantic-release/changelog", + "@semantic-release/npm", + "@semantic-release/github", + "@semantic-release/git" + ] + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "release": "aegir release" + }, + "dependencies": { + "@libp2p/interface-libp2p": "^1.1.0", + "@libp2p/interface-peer-id": "^2.0.1", + "@libp2p/interfaces": "^3.3.1", + "@multiformats/multiaddr": "^11.1.5", + "interface-blockstore": "^4.0.1", + "interface-datastore": "^7.0.3" + }, + "devDependencies": { + "aegir": "^38.1.0" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/interface/src/index.ts b/packages/interface/src/index.ts new file mode 100644 index 00000000..82239f2a --- /dev/null +++ b/packages/interface/src/index.ts @@ -0,0 +1,108 @@ +/** + * @packageDocumentation + * + * The API defined by a Helia node + * + * @example + * + * ```typescript + * import type { Helia } from '@helia/interface' + * + * export function doSomething(helia: Helia) { + * // use helia node functions here + * } + * ``` + */ + +import type { Libp2p } from '@libp2p/interface-libp2p' +import type { Blockstore } from 'interface-blockstore' +import type { AbortOptions } from '@libp2p/interfaces' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { Datastore } from 'interface-datastore' + +/** + * The API presented by a Helia node. + */ +export interface Helia { + /** + * The underlying libp2p node + */ + libp2p: Libp2p + + /** + * Where the blocks are stored + */ + blockstore: Blockstore + + /** + * A key/value store + */ + datastore: Datastore + + /** + * Returns information about this node + * + * @example + * + * ```typescript + * import { createHelia } from 'helia' + * + * const node = await createHelia() + * const id = await node.info() + * console.info(id) + * // { peerId: PeerId(12D3Foo), ... } + * ``` + */ + info: (options?: InfoOptions) => Promise + + /** + * Starts the Helia node + */ + start: () => Promise + + /** + * Stops the Helia node + */ + stop: () => Promise +} + +export interface InfoOptions extends AbortOptions { + /** + * If passed, return information about this PeerId, defaults + * to the ID of the current node. + */ + peerId?: PeerId +} + +export interface InfoResponse { + /** + * The ID of the peer this info is about + */ + peerId: PeerId + + /** + * The multiaddrs the peer is listening on + */ + multiaddrs: Multiaddr[] + + /** + * The peer's reported agent version + */ + agentVersion: string + + /** + * The peer's reported protocol version + */ + protocolVersion: string + + /** + * The protocols the peer supports + */ + protocols: string[] + + /** + * The status of the node + */ + status: 'running' | 'stopped' +} diff --git a/packages/interface/tsconfig.json b/packages/interface/tsconfig.json new file mode 100644 index 00000000..13a35996 --- /dev/null +++ b/packages/interface/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ] +} diff --git a/packages/interop/.aegir.js b/packages/interop/.aegir.js new file mode 100644 index 00000000..c6e4ebd3 --- /dev/null +++ b/packages/interop/.aegir.js @@ -0,0 +1,45 @@ +import getPort from 'aegir/get-port' +import { createServer } from 'ipfsd-ctl' +import * as kuboRpcClient from 'kubo-rpc-client' + +/** @type {import('aegir').PartialOptions} */ +export default { + test: { + before: async (options) => { + if (options.runner !== 'node') { + const ipfsdPort = await getPort() + const ipfsdServer = await createServer({ + host: '127.0.0.1', + port: ipfsdPort + }, { + ipfsBin: (await import('go-ipfs')).default.path(), + kuboRpcModule: kuboRpcClient, + ipfsOptions: { + config: { + Addresses: { + Swarm: [ + "/ip4/0.0.0.0/tcp/0", + "/ip4/0.0.0.0/tcp/0/ws" + ] + } + } + } + }).start() + + return { + env: { + IPFSD_SERVER: `http://127.0.0.1:${ipfsdPort}` + }, + ipfsdServer + } + } + + return {} + }, + after: async (options, beforeResult) => { + if (options.runner !== 'node') { + await beforeResult.ipfsdServer.stop() + } + } + } +} diff --git a/packages/interop/LICENSE b/packages/interop/LICENSE new file mode 100644 index 00000000..20ce483c --- /dev/null +++ b/packages/interop/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/interop/LICENSE-APACHE b/packages/interop/LICENSE-APACHE new file mode 100644 index 00000000..14478a3b --- /dev/null +++ b/packages/interop/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/interop/LICENSE-MIT b/packages/interop/LICENSE-MIT new file mode 100644 index 00000000..72dc60d8 --- /dev/null +++ b/packages/interop/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/interop/README.md b/packages/interop/README.md new file mode 100644 index 00000000..2e3eed6c --- /dev/null +++ b/packages/interop/README.md @@ -0,0 +1,53 @@ +# @helia/interop + +[![ipfs.tech](https://img.shields.io/badge/project-IPFS-blue.svg?style=flat-square)](https://ipfs.tech) +[![Discuss](https://img.shields.io/discourse/https/discuss.ipfs.tech/posts.svg?style=flat-square)](https://discuss.ipfs.tech) +[![codecov](https://img.shields.io/codecov/c/github/ipfs/helia.svg?style=flat-square)](https://codecov.io/gh/ipfs/helia) +[![CI](https://img.shields.io/github/actions/workflow/status/ipfs/helia/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipfs/helia/actions/workflows/js-test-and-release.yml?query=branch%3Amain) + +> Interop tests for Helia + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribute + +Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia/issues). + +Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general. + +Please be aware that all interactions related to this repo are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. + +[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) diff --git a/packages/interop/package.json b/packages/interop/package.json new file mode 100644 index 00000000..d5802dc6 --- /dev/null +++ b/packages/interop/package.json @@ -0,0 +1,167 @@ +{ + "name": "@helia/interop", + "version": "0.0.0", + "description": "Interop tests for Helia", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/ipfs/helia/tree/master/packages/interop#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/ipfs/helia.git" + }, + "bugs": { + "url": "https://github.com/ipfs/helia/issues" + }, + "keywords": [ + "IPFS" + ], + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "release": { + "branches": [ + "main" + ], + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "conventionalcommits", + "releaseRules": [ + { + "breaking": true, + "release": "major" + }, + { + "revert": true, + "release": "patch" + }, + { + "type": "feat", + "release": "minor" + }, + { + "type": "fix", + "release": "patch" + }, + { + "type": "docs", + "release": "patch" + }, + { + "type": "test", + "release": "patch" + }, + { + "type": "deps", + "release": "patch" + }, + { + "scope": "no-release", + "release": false + } + ] + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits", + "presetConfig": { + "types": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "chore", + "section": "Trivial Changes" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "deps", + "section": "Dependencies" + }, + { + "type": "test", + "section": "Tests" + } + ] + } + } + ], + "@semantic-release/changelog", + "@semantic-release/npm", + "@semantic-release/github", + "@semantic-release/git" + ] + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main", + "release": "aegir release" + }, + "devDependencies": { + "@chainsafe/libp2p-noise": "^11.0.0", + "@chainsafe/libp2p-yamux": "^3.0.5", + "@helia/interface": "~0.0.0", + "@libp2p/tcp": "^6.1.2", + "@libp2p/websockets": "^5.0.3", + "@multiformats/sha3": "^2.0.15", + "aegir": "^38.1.0", + "blockstore-core": "^3.0.0", + "datastore-core": "^8.0.4", + "go-ipfs": "^0.18.1", + "helia": "~0.0.0", + "ipfsd-ctl": "^13.0.0", + "it-to-buffer": "^3.0.0", + "kubo-rpc-client": "^3.0.0", + "libp2p": "^0.42.2", + "multiformats": "^11.0.1" + }, + "browser": { + "./dist/test/fixtures/create-helia.js": "./dist/test/fixtures/create-helia.browser.js", + "./dist/test/fixtures/create-kubo.js": "./dist/test/fixtures/create-kubo.browser.js", + "go-ipfs": false + }, + "private": true, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/interop/src/index.ts b/packages/interop/src/index.ts new file mode 100644 index 00000000..336ce12b --- /dev/null +++ b/packages/interop/src/index.ts @@ -0,0 +1 @@ +export {} diff --git a/packages/interop/test/blockstore.spec.ts b/packages/interop/test/blockstore.spec.ts new file mode 100644 index 00000000..31375fdf --- /dev/null +++ b/packages/interop/test/blockstore.spec.ts @@ -0,0 +1,56 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import { createHeliaNode } from './fixtures/create-helia.js' +import { createKuboNode } from './fixtures/create-kubo.js' +import type { Helia } from '@helia/interface' +import type { Controller } from 'ipfsd-ctl' +import toBuffer from 'it-to-buffer' +import { sha256 } from 'multiformats/hashes/sha2' +import { CID } from 'multiformats/cid' +import * as raw from 'multiformats/codecs/raw' + +describe('blockstore', () => { + let helia: Helia + let kubo: Controller + + beforeEach(async () => { + helia = await createHeliaNode() + kubo = await createKuboNode() + + // connect the two nodes + await helia.libp2p.peerStore.addressBook.add(kubo.peer.id, kubo.peer.addresses) + await helia.libp2p.dial(kubo.peer.id) + }) + + afterEach(async () => { + if (helia != null) { + await helia.stop() + } + + if (kubo != null) { + await kubo.stop() + } + }) + + it('should be able to send a block', async () => { + const input = Uint8Array.from([0, 1, 2, 3, 4]) + const digest = await sha256.digest(input) + const cid = CID.createV1(raw.code, digest) + await helia.blockstore.put(cid, input) + const output = await toBuffer(kubo.api.cat(cid)) + + expect(output).to.equalBytes(input) + }) + + it('should be able to receive a block', async () => { + const input = Uint8Array.from([0, 1, 2, 3, 4]) + const { cid } = await kubo.api.add({ content: input }, { + cidVersion: 1, + rawLeaves: true + }) + const output = await helia.blockstore.get(cid) + + expect(output).to.equalBytes(input) + }) +}) diff --git a/packages/interop/test/fixtures/create-helia.browser.ts b/packages/interop/test/fixtures/create-helia.browser.ts new file mode 100644 index 00000000..da35e7f2 --- /dev/null +++ b/packages/interop/test/fixtures/create-helia.browser.ts @@ -0,0 +1,45 @@ +import { createHelia, HeliaInit } from 'helia' +import { createLibp2p } from 'libp2p' +import { webSockets } from '@libp2p/websockets' +import { all } from '@libp2p/websockets/filters' +import { noise } from '@chainsafe/libp2p-noise' +import { yamux } from '@chainsafe/libp2p-yamux' +import { MemoryBlockstore } from 'blockstore-core' +import { MemoryDatastore } from 'datastore-core' +import type { Helia } from '@helia/interface' + +export async function createHeliaNode (init?: Partial): Promise { + const blockstore = new MemoryBlockstore() + const datastore = new MemoryDatastore() + + // dial-only in the browser until webrtc browser-to-browser arrives + const libp2p = await createLibp2p({ + transports: [ + webSockets({ + filter: all + }) + ], + connectionEncryption: [ + noise() + ], + streamMuxers: [ + yamux() + ], + datastore, + nat: { + enabled: false + }, + relay: { + enabled: false + } + }) + + const helia = await createHelia({ + libp2p, + blockstore, + datastore, + ...init + }) + + return helia +} diff --git a/packages/interop/test/fixtures/create-helia.ts b/packages/interop/test/fixtures/create-helia.ts new file mode 100644 index 00000000..28fb7899 --- /dev/null +++ b/packages/interop/test/fixtures/create-helia.ts @@ -0,0 +1,46 @@ +import { createHelia, HeliaInit } from 'helia' +import { createLibp2p } from 'libp2p' +import { tcp } from '@libp2p/tcp' +import { noise } from '@chainsafe/libp2p-noise' +import { yamux } from '@chainsafe/libp2p-yamux' +import { MemoryBlockstore } from 'blockstore-core' +import { MemoryDatastore } from 'datastore-core' +import type { Helia } from '@helia/interface' + +export async function createHeliaNode (init?: Partial): Promise { + const blockstore = new MemoryBlockstore() + const datastore = new MemoryDatastore() + + const libp2p = await createLibp2p({ + addresses: { + listen: [ + '/ip4/0.0.0.0/tcp/0' + ] + }, + transports: [ + tcp() + ], + connectionEncryption: [ + noise() + ], + streamMuxers: [ + yamux() + ], + datastore, + nat: { + enabled: false + }, + relay: { + enabled: false + } + }) + + const helia = await createHelia({ + libp2p, + blockstore, + datastore, + ...init + }) + + return helia +} diff --git a/packages/interop/test/fixtures/create-kubo.browser.ts b/packages/interop/test/fixtures/create-kubo.browser.ts new file mode 100644 index 00000000..f374075e --- /dev/null +++ b/packages/interop/test/fixtures/create-kubo.browser.ts @@ -0,0 +1,20 @@ +import { Controller, createController } from 'ipfsd-ctl' +import * as kuboRpcClient from 'kubo-rpc-client' + +export async function createKuboNode (): Promise { + return await createController({ + kuboRpcModule: kuboRpcClient, + test: true, + endpoint: process.env.IPFSD_SERVER, + ipfsOptions: { + config: { + Addresses: { + Swarm: [ + '/ip4/0.0.0.0/tcp/0', + '/ip4/0.0.0.0/tcp/0/ws' + ] + } + } + } + }) +} diff --git a/packages/interop/test/fixtures/create-kubo.ts b/packages/interop/test/fixtures/create-kubo.ts new file mode 100644 index 00000000..925f3e70 --- /dev/null +++ b/packages/interop/test/fixtures/create-kubo.ts @@ -0,0 +1,22 @@ +// @ts-expect-error no types +import * as goIpfs from 'go-ipfs' +import { Controller, createController } from 'ipfsd-ctl' +import * as kuboRpcClient from 'kubo-rpc-client' + +export async function createKuboNode (): Promise { + return await createController({ + kuboRpcModule: kuboRpcClient, + ipfsBin: goIpfs.path(), + test: true, + ipfsOptions: { + config: { + Addresses: { + Swarm: [ + '/ip4/0.0.0.0/tcp/0', + '/ip4/0.0.0.0/tcp/0/ws' + ] + } + } + } + }) +} diff --git a/packages/interop/test/hashes.spec.ts b/packages/interop/test/hashes.spec.ts new file mode 100644 index 00000000..1362f1e2 --- /dev/null +++ b/packages/interop/test/hashes.spec.ts @@ -0,0 +1,63 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import { createHeliaNode } from './fixtures/create-helia.js' +import { createKuboNode } from './fixtures/create-kubo.js' +import type { Helia } from '@helia/interface' +import type { Controller } from 'ipfsd-ctl' +import toBuffer from 'it-to-buffer' +import { CID } from 'multiformats/cid' +import * as raw from 'multiformats/codecs/raw' +// @ts-expect-error type config is broken +import { sha3512 } from '@multiformats/sha3' + +describe('hashes', () => { + let helia: Helia + let kubo: Controller + + beforeEach(async () => { + helia = await createHeliaNode({ + hashers: [ + sha3512 + ] + }) + kubo = await createKuboNode() + + // connect the two nodes + await helia.libp2p.peerStore.addressBook.add(kubo.peer.id, kubo.peer.addresses) + await helia.libp2p.dial(kubo.peer.id) + }) + + afterEach(async () => { + if (helia != null) { + await helia.stop() + } + + if (kubo != null) { + await kubo.stop() + } + }) + + it('should be able to send a block with a non-default hash', async () => { + const input = Uint8Array.from([0, 1, 2, 3, 4]) + const digest = await sha3512.digest(input) + const cid = CID.createV1(raw.code, digest) + await helia.blockstore.put(cid, input) + const output = await toBuffer(kubo.api.cat(cid)) + + expect(output).to.equalBytes(input) + }) + + it('should be able to receive a block with a non-default hash', async () => { + const input = Uint8Array.from([0, 1, 2, 3, 4]) + const { cid } = await kubo.api.add({ content: input }, { + cidVersion: 1, + rawLeaves: true, + hashAlg: 'sha3-512' + }) + expect(cid.multihash.code).to.equal(sha3512.code) + const output = await helia.blockstore.get(cid) + + expect(output).to.equalBytes(input) + }) +}) diff --git a/packages/interop/tsconfig.json b/packages/interop/tsconfig.json new file mode 100644 index 00000000..4eb21e77 --- /dev/null +++ b/packages/interop/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../helia" + }, + { + "path": "../interface" + } + ] +}