├── .github └── workflows │ ├── ci.yml │ └── publish-docs.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── package-lock.json ├── package.json ├── spec.md ├── src ├── advertisers │ └── dht.ts ├── dcid.ts ├── index.ts ├── namers │ ├── ipns.ts │ └── w3.ts └── pinners │ └── w3.ts ├── test ├── advertisers │ ├── advertiser.ts │ └── dht.spec.ts ├── aegir.ts ├── env.d.ts ├── mocks │ └── w3name.ts ├── namers │ ├── ipns.spec.ts │ ├── namer.ts │ └── w3.spec.ts ├── pinners │ ├── pinner.ts │ └── w3.spec.ts └── utils │ ├── create-cid.ts │ ├── create-helia.ts │ ├── create-kubo.ts │ ├── create-libp2p.browser.ts │ ├── create-libp2p.ts │ ├── protocols.ts │ └── services.ts └── tsconfig.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - '**' 10 | types: 11 | - opened 12 | - synchronize 13 | - reopened 14 | - ready_for_review 15 | 16 | jobs: 17 | 18 | check: 19 | if: github.event.pull_request.draft == false 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v2 23 | - uses: actions/setup-node@v3 24 | with: 25 | node-version: 'lts/*' 26 | cache: 'npm' 27 | - run: npm ci 28 | - run: npm run lint 29 | 30 | test-node: 31 | if: github.event.pull_request.draft == false 32 | needs: check 33 | runs-on: ${{ matrix.os }} 34 | strategy: 35 | matrix: 36 | os: [ubuntu-latest] # [windows-latest, ubuntu-latest, macos-latest] 37 | node: [18] 38 | fail-fast: true 39 | steps: 40 | - uses: actions/checkout@v2 41 | - uses: actions/setup-node@v3 42 | with: 43 | node-version: ${{ matrix.node }} 44 | cache: 'npm' 45 | - run: npm ci 46 | - run: npm run test:node 47 | - uses: codecov/codecov-action@v3 48 | with: 49 | flags: node 50 | 51 | # test-chrome: 52 | # if: github.event.pull_request.draft == false 53 | # needs: check 54 | # runs-on: ${{ matrix.os }} 55 | # strategy: 56 | # matrix: 57 | # os: [ubuntu-latest] # [windows-latest, ubuntu-latest, macos-latest] 58 | # node: [18] 59 | # fail-fast: true 60 | # steps: 61 | # - uses: actions/checkout@v2 62 | # - uses: actions/setup-node@v3 63 | # with: 64 | # node-version: ${{ matrix.node }} 65 | # cache: 'npm' 66 | # - run: npm ci 67 | # - run: npm run test:chrome 68 | # - uses: codecov/codecov-action@v3 69 | # with: 70 | # flags: node 71 | 72 | release: 73 | runs-on: ubuntu-latest 74 | # needs: [test-node, test-chrome] 75 | needs: test-node 76 | if: github.event_name == 'push' && github.ref == 'refs/heads/master' && github.event.pull_request.draft == false 77 | steps: 78 | - uses: google-github-actions/release-please-action@v3 79 | id: release 80 | with: 81 | token: ${{ secrets.GITHUB_TOKEN }} 82 | release-type: node 83 | - uses: actions/checkout@v2 84 | - uses: actions/setup-node@v3 85 | with: 86 | node-version: 'lts/*' 87 | cache: 'npm' 88 | registry-url: 'https://registry.npmjs.org' 89 | - run: npm ci 90 | - run: npx aegir docs --publish 91 | env: 92 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 93 | # The logic below handles the npm publication: 94 | # these if statements ensure that a publication only occurs when 95 | # a new release is created: 96 | - run: npm publish 97 | if: ${{ steps.release.outputs.release_created }} 98 | env: 99 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 100 | - run: | 101 | npm version `node -p -e "require('./package.json').version"`-`git rev-parse --short HEAD` --no-git-tag-version 102 | npm publish --tag next --access public 103 | if: ${{ !steps.release.outputs.release_created }} 104 | env: 105 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 106 | name: release rc 107 | -------------------------------------------------------------------------------- /.github/workflows/publish-docs.yml: -------------------------------------------------------------------------------- 1 | name: publish-docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | docs: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: 'lts/*' 16 | cache: 'npm' 17 | - run: npm ci 18 | - run: npx aegir docs --publish 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | .docs 5 | .aegir.js 6 | .token 7 | 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [5.0.0](https://github.com/tabcat/zzzync/compare/v4.2.0...v5.0.0) (2024-01-09) 4 | 5 | 6 | ### ⚠ BREAKING CHANGES 7 | 8 | * Update packages. 9 | 10 | ### Miscellaneous Chores 11 | 12 | * Update packages. ([f1d2032](https://github.com/tabcat/zzzync/commit/f1d203233e81bbdf64752d81ff4e49f7e357bbd2)) 13 | 14 | ## [4.2.0](https://github.com/tabcat/zzzync/compare/v4.1.0...v4.2.0) (2023-09-16) 15 | 16 | 17 | ### Features 18 | 19 | * resolve local names using cache ([f4b18bf](https://github.com/tabcat/zzzync/commit/f4b18bf38f8e661740ce63ad824a026ada4c4c5c)) 20 | 21 | ## [4.1.0](https://github.com/tabcat/zzzync/compare/v4.0.0...v4.1.0) (2023-09-15) 22 | 23 | 24 | ### Features 25 | 26 | * **namers/w3:** local resolutions of names ([8fd4760](https://github.com/tabcat/zzzync/commit/8fd47605f3878e4e22b29e83f2a230484e5ccb10)) 27 | 28 | ## [4.0.0](https://github.com/tabcat/zzzync/compare/v3.0.0...v4.0.0) (2023-09-14) 29 | 30 | 31 | ### ⚠ BREAKING CHANGES 32 | 33 | * change advertiser interface 34 | * add pinner to zzzync interface 35 | 36 | ### Features 37 | 38 | * add pinner to zzzync interface ([1d23cff](https://github.com/tabcat/zzzync/commit/1d23cffdb8a05b41d7afd7e6da45b832596efa7a)) 39 | 40 | 41 | ### Bug Fixes 42 | 43 | * catch advertisers/dht query abort error ([d3a5aea](https://github.com/tabcat/zzzync/commit/d3a5aea6c68beefc64b6f3a81f43c9a61fa45608)) 44 | * handle sync and async code the same ([277086b](https://github.com/tabcat/zzzync/commit/277086be56a0255125ff76e6d0591b2557a2d506)) 45 | * handle w3 name resolution errors ([31ffe91](https://github.com/tabcat/zzzync/commit/31ffe910de56cc46f724fbda96cb838ad0ccf537)) 46 | * pinner/w3 uses CarWriter correctly ([3f1b3ec](https://github.com/tabcat/zzzync/commit/3f1b3ec79a8a0ddd02aa2347a381ce0bcdccfd27)) 47 | * w3 namer uses passed service option ([20e1ad3](https://github.com/tabcat/zzzync/commit/20e1ad32d6fae6225655c9f23dacc17ef23dadeb)) 48 | 49 | 50 | ### Code Refactoring 51 | 52 | * change advertiser interface ([7af6741](https://github.com/tabcat/zzzync/commit/7af674178640527e36ea7a37a2163c0d902dcb34)) 53 | 54 | ## [3.0.0](https://github.com/tabcat/zzzync/compare/v2.0.0...v3.0.0) (2023-09-04) 55 | 56 | 57 | ### ⚠ BREAKING CHANGES 58 | 59 | * rename w3name to just w3 60 | * rename w3namer and dhtAdvertiser exports 61 | * dht advertiser takes kad-dht not libp2p 62 | 63 | ### Code Refactoring 64 | 65 | * dht advertiser takes kad-dht not libp2p ([d0ee166](https://github.com/tabcat/zzzync/commit/d0ee1667d77f2c275f62e2d92fa60c00114a47ec)) 66 | * rename w3name to just w3 ([bb74965](https://github.com/tabcat/zzzync/commit/bb749659725692e1dc1cd956f4942dfcea1fc4df)) 67 | * rename w3namer and dhtAdvertiser exports ([45baedd](https://github.com/tabcat/zzzync/commit/45baedd8c46887801bfbea1e7f0a6bb64a84c7f7)) 68 | 69 | ## [2.0.0](https://github.com/tabcat/zzzync/compare/v1.1.0...v2.0.0) (2023-07-17) 70 | 71 | 72 | ### ⚠ BREAKING CHANGES 73 | 74 | * optionally stops ephemeral libp2p node 75 | 76 | ### Features 77 | 78 | * optionally stops ephemeral libp2p node ([5fddc7e](https://github.com/tabcat/zzzync/commit/5fddc7e3fec8fa2712d5baebcf8355a5b2cccacd)) 79 | 80 | ## [1.1.0](https://github.com/tabcat/zzzync/compare/v1.0.0...v1.1.0) (2023-07-07) 81 | 82 | 83 | ### Features 84 | 85 | * optionally scope the dht to lan|wan ([a835682](https://github.com/tabcat/zzzync/commit/a83568280dd201cd1f597f1332c63e12fd87dc83)) 86 | 87 | 88 | ### Bug Fixes 89 | 90 | * make dht options optional ([90c6c30](https://github.com/tabcat/zzzync/commit/90c6c30e73994851944443bbcc07328b6a9a461a)) 91 | 92 | ## 1.0.0 (2023-06-17) 93 | 94 | 95 | ### Features 96 | 97 | * add toDcid ([3a5e27e](https://github.com/tabcat/zzzync/commit/3a5e27e713c8bddebb1f15c628f33a2652d26836)) 98 | 99 | 100 | ### Bug Fixes 101 | 102 | * handle async digests in browser ([1495f6a](https://github.com/tabcat/zzzync/commit/1495f6abda16311365f4a81af5c3df1dc17a3e2f)) 103 | * ipns resolve handles ipfs prefix ([d3db515](https://github.com/tabcat/zzzync/commit/d3db515ec11ed2f14367b73154ed22281087d4f2)) 104 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | All contributions are licensed under a dual MIT/Apache-2.0 license. 2 | 3 | MIT: https://www.opensource.org/licenses/mit 4 | Apache-2.0: https://www.apache.org/licenses/LICENSE-2.0 5 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Licensed under the Apache License, Version 2.0 (the "License"); 2 | you may not use this file except in compliance with the License. 3 | You may obtain a copy of the License at 4 | 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 💤ync 2 | 3 | 4 | 5 | 6 | 7 | zĭngk 8 | 9 | 10 |
11 |
12 | 13 | sync with peers that have gone to sleep 😴 14 | 15 | --- 16 | 17 |
18 | 19 | Zzzync uses [IPLD](https://ipld.io/), [IPNS](https://docs.ipfs.tech/concepts/ipns/), and [Provider Records](https://docs.ipfs.tech/concepts/dht/) to replicate dynamic content over IPFS. Read about the design in [tabcat/dynamic-content](https://github.com/tabcat/dynamic-content). 20 | 21 | IPLD is used to store replica data 22 | IPNS is used to point to the latest version of a collaborator's local replica 23 | Provider Records are used to find the [peerIDs](https://docs.libp2p.io/concepts/fundamentals/peers/#peer-id) of collaborators, which can be turned into IPNS names 24 | 25 | ## API Docs 26 | 27 | https://tabcat.github.io/zzzync/ 28 | 29 | ## Spec 30 | 31 | https://github.com/tabcat/zzzync/blob/master/spec.md 32 | 33 | --- 34 | 35 | This work is being funded as part of a [grant](https://github.com/tabcat/rough-opal) by [Protocol Labs](https://protocol.ai) 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tabcat/zzzync", 3 | "version": "5.0.0", 4 | "description": "replication protocol over IPLD, IPNS, and IPFS Provider Records", 5 | "type": "module", 6 | "types": "./dist/src/index.d.ts", 7 | "typesVersions": { 8 | "*": { 9 | "*": [ 10 | "*", 11 | "dist/*", 12 | "dist/src/*", 13 | "dist/src/*/index" 14 | ], 15 | "src/*": [ 16 | "*", 17 | "dist/*", 18 | "dist/src/*", 19 | "dist/src/*/index" 20 | ] 21 | } 22 | }, 23 | "files": [ 24 | "dist/src", 25 | "!**/*.tsbuildinfo" 26 | ], 27 | "eslintConfig": { 28 | "extends": "ipfs", 29 | "parserOptions": { 30 | "sourceType": "module" 31 | } 32 | }, 33 | "exports": { 34 | ".": { 35 | "types": "./dist/src/index.d.ts", 36 | "import": "./dist/src/index.js" 37 | }, 38 | "./advertisers/dht": { 39 | "types": "./dist/src/advertisers/dht.d.ts", 40 | "import": "./dist/src/advertisers/dht.js" 41 | }, 42 | "./namers/w3": { 43 | "types": "./dist/src/namers/w3.d.ts", 44 | "import": "./dist/src/namers/w3.js" 45 | } 46 | }, 47 | "scripts": { 48 | "prepublishOnly": "npm run build", 49 | "clean": "aegir clean", 50 | "lint": "aegir lint", 51 | "dep-check": "aegir dep-check", 52 | "build": "rm -rf dist .aegir.js && aegir build && ln -sf ./dist/test/aegir.js ./.aegir.js", 53 | "docs": "aegir docs", 54 | "test": "npm run test:node", 55 | "test:chrome": "aegir test -t browser --cov", 56 | "test:chrome-webworker": "aegir test -t webworker", 57 | "test:firefox": "aegir test -t browser -- --browser firefox", 58 | "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", 59 | "test:node": "aegir test -t node --cov", 60 | "test:electron-main": "aegir test -t electron-main" 61 | }, 62 | "repository": { 63 | "type": "git", 64 | "url": "git+https://github.com/tabcat/zzzync.git" 65 | }, 66 | "keywords": [ 67 | "IPFS", 68 | "IPLD", 69 | "IPNS", 70 | "DHT", 71 | "replication" 72 | ], 73 | "author": "tabcat ", 74 | "license": "Apache-2.0 OR MIT", 75 | "bugs": { 76 | "url": "https://github.com/tabcat/zzzync/issues" 77 | }, 78 | "homepage": "https://github.com/tabcat/zzzync#readme", 79 | "typedoc": { 80 | "entryPoint": "./src/index.ts" 81 | }, 82 | "browser": { 83 | "./dist/test/utils/create-libp2p.js": "./dist/test/utils/create-libp2p.browser.js" 84 | }, 85 | "devDependencies": { 86 | "@chainsafe/libp2p-noise": "^14.1.0", 87 | "@chainsafe/libp2p-yamux": "^6.0.1", 88 | "@libp2p/circuit-relay-v2": "^1.0.10", 89 | "@libp2p/identify": "^1.0.9", 90 | "@libp2p/peer-id-factory": "^4.0.3", 91 | "@libp2p/tcp": "^9.0.10", 92 | "@libp2p/webrtc": "^4.0.14", 93 | "aegir": "^42.1.0", 94 | "blockstore-core": "^4.3.10", 95 | "go-ipfs": "^0.22.0", 96 | "helia": "^3.0.0", 97 | "ipfsd-ctl": "^13.0.0", 98 | "kubo-rpc-client": "^3.0.2", 99 | "merge-options": "^3.0.4", 100 | "wherearewe": "^2.0.1" 101 | }, 102 | "dependencies": { 103 | "@helia/interface": "^3.0.0", 104 | "@helia/ipns": "^4.0.0", 105 | "@ipld/car": "^5.2.5", 106 | "@libp2p/crypto": "^3.0.4", 107 | "@libp2p/interface": "^1.1.1", 108 | "@libp2p/kad-dht": "^12.0.2", 109 | "@libp2p/websockets": "^8.0.10", 110 | "@multiformats/multiaddr": "^12.1.12", 111 | "datastore-core": "^9.2.7", 112 | "interface-blockstore": "^5.2.9", 113 | "interface-blockstore-tests": "^6.1.9", 114 | "interface-datastore": "^8.2.10", 115 | "interface-store": "^5.1.7", 116 | "it-drain": "^3.0.5", 117 | "libp2p": "^1.1.1", 118 | "multiformats": "^13.0.0", 119 | "uint8arrays": "^5.0.1", 120 | "w3name": "^1.0.8", 121 | "web3.storage": "^4.5.5" 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /spec.md: -------------------------------------------------------------------------------- 1 | 2 | # Zzzync Spec Document 3 | 4 | This document is meant to provide clarity on zzzync as a protocol. 5 | 6 | Protocol Version: `1.0.0-beta` 7 | 8 | --- 9 | 10 |
11 | 12 | ## DCID 13 | 14 | DCID are a definition made by Zzzync and are not recognized as a separate format by IPFS. 15 | They are of the same format as CIDs but are created differently. 16 | DCID are created by taking the CID of a manifest/setup document for some dynamic content, then prefixing `'/dcoi/'` decoded utf-8 bytes to the CID multihash, and then hashing into a different CID. 17 | 18 | > 'dcoi' is an acronym meaning *dynamic content over ipfs* 19 | 20 | DCID format: 21 | 22 | ``` 23 | <0x01 (CIDv1)><0x55 (multicode raw)>)> 24 | ``` 25 | 26 | --- 27 | > **Q:** Why not just use `<'/dcoi/' decoded utf-8>` as the routing key. Why convert it back to a CID? 28 | 29 | > **A:** The kad-dht api in javascript makes working with CIDs easier. As the protocol matures this may be changed. 30 | --- 31 | 32 |
33 | 34 | ## PeerId 35 | 36 | A [PeerId](https://github.com/libp2p/specs/blob/master/peer-ids/peer-ids.md) is a Libp2p definition. 37 | They are used to identify nodes on the network and are made of a cryptographic keypair. 38 | They are unique to a device and must not be shared. 39 | 40 |
41 | 42 | ## Advertisers 43 | 44 | Advertisers are used to point from a DCID to PeerIds. 45 | Advertisers can use any system to do so. 46 | There may be multiple advertisers to use and defined under this protocol and they can be used together. 47 | 48 | Advertisers need only re-advertise when the system requires it, to keep the advertisements available. 49 | 50 | ### DHT Advertiser 51 | 52 | The DHT advertiser uses the IPFS DHT's Provider Records to point from DCIDs to PeerIds. 53 | Each record points to a different PeerId and records stay on the network for a maximum of 48hours. 54 | 55 | The `ADD_PROVIDER` query is used to advertise that a PeerId is the provider of a DCID. 56 | The `GET_PROVIDERS` query is used to discover PeerIds that are providing a DCID. 57 | 58 | The [IPFS DHT spec](https://github.com/libp2p/specs/tree/master/kad-dht) provides further information. 59 | 60 |
61 | 62 | ## Namers 63 | 64 | Namers are used to point from a PeerId to a CID. 65 | The CID is the latest version of some dynamic content. 66 | Namers can use any system that provides verifiable guarantees of a mutable PeerId -> CID mapping. 67 | There may be multiple namers to use and defined under this protocol 68 | 69 | Namers need only republish after a change has been made to a local replica, changing its CID. 70 | 71 | ### IPNS Namer 72 | 73 | The IPNS namer uses the Interplanetary Name System to resolve PeerIds to CIDs. 74 | 75 | [IPNS spec](https://specs.ipfs.tech/ipns/ipns-record/) 76 | 77 | IPNS Records have a value field which is encoded as bytes and can contain something other than a CID. 78 | For Zzzync's purpose there should only ever be IPFS path here, encoded utf-8. 79 | An immutable IPFS path is utf-8 encoded string that includes a multibase encoded CID with an `/ipfs/` prefix: 80 | 81 | ``` 82 | /ipfs/ 83 | ``` 84 | 85 | ### W3Name Namer 86 | 87 | The W3Name namer uses the W3Name system to resolve PeerIds to CIDs. 88 | W3Name system was built to be a substitute for IPNS. 89 | 90 | The design and records used are very similar so the IPNS Namer section on value field format also applies here. 91 | 92 | PeerIds are made of cryptographic keys. 93 | The private key of the PeerId being used must be used to sign the W3Name records. 94 | 95 |
96 | 97 | ## Replication Protocol 98 | 99 | ### Advertisement 100 | 101 | After a change has been made to a local replica and the replica data has been uploaded to another machine: 102 | 103 | 1. Use Namer to publish device unique PeerId -> CID of replica. 104 | 2. Use Advertiser to advertise DCID -> PeerId 105 | 106 | ### Discovery 107 | 108 | 1. Use Advertiser to query PeerIds for DCID. 109 | 2. Use Namer for each PeerId to resolve replica CIDs. 110 | 111 | If the replica data for the CID has been uploaded to another machine offline replication can be completed. 112 | 113 |
114 | 115 | ## Replica Hosts 116 | 117 | For offline discovery to be possible the records created by the Advertisers/Namers must be available. 118 | For offline replication the referenced replica data remain available. 119 | Without both offline collaboration cannot occur in this context. 120 | 121 | This document does not specify how data should be hosted for this purpose. 122 | It only specifies how to advertise and discover the latest versions for some dynamic content. 123 | -------------------------------------------------------------------------------- /src/advertisers/dht.ts: -------------------------------------------------------------------------------- 1 | import drain from 'it-drain' 2 | import type { Advertiser } from '../index.js' 3 | import type { ContentRouting } from '@libp2p/interface/content-routing' 4 | import type { PeerId } from '@libp2p/interface/peer-id' 5 | import type { KadDHT } from '@libp2p/kad-dht' 6 | import type { CID } from 'multiformats/cid' 7 | 8 | export interface CreateEphemeralKadDHT { 9 | (provider: PeerId): Promise<{ dht: KadDHT, stop?(): Promise }> 10 | } 11 | 12 | const collaborate = (createEphemeralKadDHT: CreateEphemeralKadDHT): Advertiser['collaborate'] => 13 | async function (dcid: CID, provider: PeerId): Promise { 14 | const { dht, stop } = await createEphemeralKadDHT(provider) 15 | 16 | try { 17 | await drain(dht.provide(dcid)) 18 | } finally { 19 | if (stop != null) { 20 | await stop() 21 | } 22 | } 23 | } 24 | 25 | const findCollaborators = (libp2p: { contentRouting: ContentRouting }): Advertiser['findCollaborators'] => 26 | async function * (dcid: CID): AsyncIterable { 27 | try { 28 | for await (const peerInfo of libp2p.contentRouting.findProviders(dcid)) { 29 | yield peerInfo.id 30 | } 31 | } catch (e) { 32 | // eslint-disable-next-line no-console 33 | console.error(e) 34 | } 35 | } 36 | 37 | export function dhtAdvertiser (libp2p: { contentRouting: ContentRouting }, createEphemeralKadDHT: CreateEphemeralKadDHT): Advertiser { 38 | return { 39 | collaborate: collaborate(createEphemeralKadDHT), 40 | findCollaborators: findCollaborators(libp2p) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/dcid.ts: -------------------------------------------------------------------------------- 1 | import { CID } from 'multiformats' 2 | import * as raw from 'multiformats/codecs/raw' 3 | import { sha256 } from 'multiformats/hashes/sha2' 4 | import { concat } from 'uint8arrays/concat' 5 | import { fromString } from 'uint8arrays/from-string' 6 | 7 | // dynamic content over ipfs 8 | const DCOI_KEY = fromString('/dcoi/') 9 | 10 | // similar to ipns routing key in js-ipns 11 | const routingKey = (cid: CID): Uint8Array => concat([DCOI_KEY, cid.multihash.bytes]) 12 | 13 | // Use CID as Routing Key to make APIs easier to work with 14 | export async function toDcid (cid: CID): Promise { 15 | const digest = await sha256.digest(routingKey(cid)) 16 | 17 | const dcid = CID.create(1, raw.code, digest) 18 | 19 | return dcid 20 | } 21 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @packageDocumentation 3 | * 4 | * @example 5 | * 6 | * ```typescript 7 | * ``` 8 | */ 9 | 10 | import type { Ed25519PeerId, PeerId } from '@libp2p/interface/peer-id' 11 | import type { Blockstore } from 'interface-blockstore' 12 | import type { CID } from 'multiformats/cid' 13 | 14 | export { toDcid } from './dcid.js' 15 | 16 | export interface Advertiser { 17 | collaborate(dcid: CID, provider: PeerId): Promise 18 | findCollaborators(dcid: CID): AsyncIterable 19 | } 20 | 21 | export interface Namer { 22 | publish(key: Ed25519PeerId, value: CID): Promise 23 | resolve(key: Ed25519PeerId): Promise 24 | } 25 | 26 | export interface Pinner extends Blockstore {} 27 | 28 | export interface Zzzync { 29 | readonly namer: Namer 30 | readonly advertiser: Advertiser 31 | readonly pinner: Pinner 32 | } 33 | 34 | class DefaultZzzync implements Zzzync { 35 | constructor ( 36 | readonly namer: Namer, 37 | readonly advertiser: Advertiser, 38 | readonly pinner: Pinner 39 | ) {} 40 | } 41 | 42 | export function zzzync ( 43 | namer: Namer, 44 | advertiser: Advertiser, 45 | pinner: Pinner 46 | ): Zzzync { 47 | return new DefaultZzzync(namer, advertiser, pinner) 48 | } 49 | -------------------------------------------------------------------------------- /src/namers/ipns.ts: -------------------------------------------------------------------------------- 1 | import { ipns, type IPNS } from '@helia/ipns' 2 | import { libp2p } from '@helia/ipns/routing' 3 | import type { Namer } from '../index.js' 4 | import type { Helia } from '@helia/interface' 5 | import type { Ed25519PeerId } from '@libp2p/interface/peer-id' 6 | import type { CID } from 'multiformats/cid' 7 | 8 | const publish = (ipns: IPNS): Namer['publish'] => 9 | async (peerId: Ed25519PeerId, value: CID) => { void ipns.publish(peerId, value) } 10 | 11 | const resolve = (ipns: IPNS): Namer['resolve'] => 12 | async (peerId: Ed25519PeerId) => ipns.resolve(peerId) 13 | 14 | export function ipnsNamer (helia: Helia): Namer { 15 | const ns = ipns(helia, { routers: [libp2p(helia)] }) 16 | 17 | return { 18 | publish: publish(ns), 19 | resolve: resolve(ns) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/namers/w3.ts: -------------------------------------------------------------------------------- 1 | import { Key } from 'interface-datastore' 2 | import { keys } from 'libp2p-crypto' 3 | import { CID } from 'multiformats/cid' 4 | import * as Name from 'w3name' 5 | import type { Namer } from '../index.js' 6 | import type { Ed25519PeerId } from '@libp2p/interface/peer-id' 7 | import type { Datastore } from 'interface-datastore' 8 | import type { Await } from 'interface-store' 9 | import type W3NameService from 'w3name/service' 10 | 11 | export interface RevisionState { 12 | get(peerId: Ed25519PeerId): Await 13 | set(peerId: Ed25519PeerId, revision: Name.Revision): Await 14 | } 15 | 16 | const ipfsPrefix = '/ipfs/' 17 | const revision2cid = (revision: string): CID => { 18 | if (!revision.startsWith(ipfsPrefix)) { 19 | throw new Error('invalid revision: missing /ipfs/ prefix') 20 | } 21 | 22 | return CID.parse(revision.slice(ipfsPrefix.length)) 23 | } 24 | const cid2revision = (cid: CID): string => ipfsPrefix + cid.toString() 25 | 26 | export const revisionState = (datastore: Datastore): RevisionState => { 27 | const get: RevisionState['get'] = async (peerId): Promise => { 28 | try { 29 | return Name.Revision.decode(await datastore.get(new Key(peerId.toString()))) 30 | } catch (e) { 31 | if (String(e) !== 'Error: Not Found') { 32 | throw e 33 | } 34 | 35 | return undefined 36 | } 37 | } 38 | 39 | const set: RevisionState['set'] = async (peerId, revision): Promise => { 40 | await datastore.put(new Key(peerId.toString()), Name.Revision.encode(revision)) 41 | } 42 | 43 | return { get, set } 44 | } 45 | 46 | const pid2Name = (peerId: Ed25519PeerId): Name.Name => 47 | new Name.Name(keys.unmarshalPublicKey(peerId.publicKey)) 48 | 49 | const publish = 50 | (service: W3NameService, revisions: RevisionState): Namer['publish'] => 51 | async (peerId: Ed25519PeerId, cid: CID) => { 52 | if (peerId.privateKey == null) { 53 | throw new Error('namers/w3: unable to publish, peerId.privateKey undefined') 54 | } 55 | 56 | const name = new Name.WritableName(await keys.unmarshalPrivateKey(peerId.privateKey)) 57 | 58 | const revisionValue = cid2revision(cid) 59 | const existing = await revisions.get(peerId) 60 | let updated: Name.Revision 61 | if (existing == null) { 62 | updated = await Name.v0(name, revisionValue) 63 | } else { 64 | updated = await Name.increment(existing, revisionValue) 65 | } 66 | await revisions.set(peerId, updated) 67 | 68 | await Name.publish(updated, name.key, service) 69 | } 70 | 71 | const resolve = 72 | (service: W3NameService, revisions: RevisionState): Namer['resolve'] => 73 | async (peerId: Ed25519PeerId) => { 74 | let revision: Name.Revision | undefined = await revisions.get(peerId) 75 | 76 | if (revision != null) { 77 | // keys must not be updated concurrently by other devices 78 | return revision2cid(revision.value) 79 | } 80 | 81 | try { 82 | revision = await Name.resolve(pid2Name(peerId), service) 83 | return revision2cid(revision.value) 84 | } catch { 85 | throw new Error('unable to resolve peerId to value') 86 | } 87 | } 88 | 89 | export function w3Namer (service: W3NameService, revisions: RevisionState): Namer { 90 | return { 91 | publish: publish(service, revisions), 92 | resolve: resolve(service, revisions) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/pinners/w3.ts: -------------------------------------------------------------------------------- 1 | import { CarWriter, CarReader } from '@ipld/car' 2 | import { CID } from 'multiformats' 3 | import type { Blockstore, Pair } from 'interface-blockstore' 4 | import type { AwaitIterable } from 'interface-store' 5 | import type { Web3Storage } from 'web3.storage' 6 | 7 | export class Web3StoragePinner implements Blockstore { 8 | constructor (readonly client: Web3Storage) {} 9 | 10 | async has (cid: CID): Promise { 11 | return this.client.status(cid.toString()) != null 12 | } 13 | 14 | async put (cid: CID, bytes: Uint8Array): Promise { 15 | const { writer, out } = CarWriter.create(cid) 16 | 17 | void writer.put({ cid, bytes }) 18 | void writer.close() 19 | 20 | const reader = await CarReader.fromIterable(out) 21 | // @ts-expect-error Types of parameters 'key' and 'key' are incompatible. 22 | await this.client.putCar(reader) 23 | 24 | return cid 25 | } 26 | 27 | async * putMany (source: AwaitIterable): AsyncIterable { 28 | for await (const { cid, block } of source) { 29 | yield this.put(cid, block) 30 | } 31 | } 32 | 33 | async get (cid: CID): Promise { 34 | const response = await this.client.get(cid.toString()) 35 | 36 | if (response == null) { 37 | throw new Error('response was null') 38 | } 39 | 40 | if (response.ok != null) { 41 | throw new Error('failed to get block ' + cid.toString()) 42 | } 43 | 44 | const carBytes = new Uint8Array(await response.arrayBuffer()) 45 | const reader = await CarReader.fromBytes(carBytes) 46 | const block = await reader.get(cid) 47 | 48 | if (block == null) { 49 | throw new Error('block for cid not found in car') 50 | } 51 | 52 | return block.bytes 53 | } 54 | 55 | async * getMany (source: AwaitIterable): AsyncIterable { 56 | for await (const cid of source) { 57 | const bytes = await this.get(cid) 58 | yield { cid, block: bytes } 59 | } 60 | } 61 | 62 | async delete (cid: CID): Promise { 63 | await this.client.delete(cid.toString()) 64 | } 65 | 66 | async * deleteMany (source: AwaitIterable): AsyncIterable { 67 | for await (const cid of source) { 68 | await this.delete(cid) 69 | yield cid 70 | } 71 | } 72 | 73 | async * getAll (): AsyncIterable { 74 | for await (const { cid: cidString } of this.client.list()) { 75 | const cid = CID.parse(cidString) 76 | yield { cid, block: await this.get(cid) } 77 | } 78 | } 79 | } 80 | 81 | export const w3Pinner = (client: Web3Storage): Web3StoragePinner => new Web3StoragePinner(client) 82 | -------------------------------------------------------------------------------- /test/advertisers/advertiser.ts: -------------------------------------------------------------------------------- 1 | import type { Advertiser } from '../../src' 2 | import type { Ed25519PeerId } from '@libp2p/interface/peer-id' 3 | import type { CID } from 'multiformats/cid' 4 | 5 | interface AdvertiserOptions { 6 | server: Ed25519PeerId 7 | provider: Ed25519PeerId 8 | dcid: CID 9 | } 10 | 11 | interface CollaborateOptions extends AdvertiserOptions { 12 | collaborate: Advertiser['collaborate'] 13 | } 14 | 15 | async function collaborate ({ collaborate, dcid, provider }: CollaborateOptions): Promise { 16 | await collaborate(dcid, provider) 17 | } 18 | 19 | interface FindCollaboratorsOptions extends AdvertiserOptions { 20 | findCollaborators: Advertiser['findCollaborators'] 21 | } 22 | 23 | async function findCollaborators ({ findCollaborators, provider, dcid }: FindCollaboratorsOptions): Promise { 24 | for await (const peerId of findCollaborators(dcid)) { 25 | if (peerId.toString() === provider.toString()) { 26 | return 27 | } 28 | } 29 | 30 | throw new Error('did not find provider') 31 | } 32 | 33 | export const spec = { 34 | collaborate, 35 | findCollaborators 36 | } 37 | -------------------------------------------------------------------------------- /test/advertisers/dht.spec.ts: -------------------------------------------------------------------------------- 1 | import { createEd25519PeerId } from '@libp2p/peer-id-factory' 2 | import { CID } from 'multiformats' 3 | import { dhtAdvertiser } from '../../src/advertisers/dht.js' 4 | import { createLibp2pNode } from '../utils/create-libp2p.js' 5 | import { lanKadProtocol } from '../utils/protocols.js' 6 | import { spec } from './advertiser.js' 7 | import type { CreateEphemeralKadDHT } from '../../src/advertisers/dht.js' 8 | import type { Advertiser } from '../../src/index.js' 9 | import type { Ed25519PeerId, PeerId } from '@libp2p/interface/peer-id' 10 | import type { KadDHT } from '@libp2p/kad-dht' 11 | import type { Multiaddr } from '@multiformats/multiaddr' 12 | import type { Libp2p } from 'libp2p' 13 | 14 | type Libp2pWithDHT = Libp2p<{ dht: KadDHT }> 15 | 16 | describe('advertisers/dht.ts', () => { 17 | let 18 | client: Libp2pWithDHT, 19 | server: Libp2pWithDHT, 20 | advertiser: Advertiser, 21 | dcid: CID, 22 | provider: Ed25519PeerId, 23 | addrs: Multiaddr[] 24 | 25 | const createEphemeralKadDHT: CreateEphemeralKadDHT = async (peerId: PeerId): ReturnType => { 26 | const libp2p = await createLibp2pNode(peerId) 27 | 28 | await libp2p.dialProtocol(addrs, lanKadProtocol) 29 | 30 | return { 31 | dht: libp2p.services.dht, 32 | stop: async () => libp2p.stop() 33 | } 34 | } 35 | 36 | before(async () => { 37 | client = await createLibp2pNode() 38 | server = await createLibp2pNode() 39 | addrs = server.getMultiaddrs() 40 | await client.dialProtocol(addrs, lanKadProtocol) 41 | advertiser = dhtAdvertiser(client, createEphemeralKadDHT) 42 | provider = await createEd25519PeerId() 43 | dcid = CID.parse('bafyreihypffwyzhujryetatiy5imqq3p4mokuz36xmgp7wfegnhnjhwrsq') 44 | }) 45 | 46 | after(async () => { 47 | await client.stop() 48 | await server.stop() 49 | }) 50 | 51 | it('advertises non-self peerId as collaborator', async () => { 52 | await spec.collaborate({ 53 | collaborate: advertiser.collaborate, 54 | server: server.peerId as Ed25519PeerId, 55 | provider, 56 | dcid 57 | }) 58 | }) 59 | 60 | it('finds non-self peerId as collaborator', async () => { 61 | await spec.findCollaborators({ 62 | findCollaborators: advertiser.findCollaborators, 63 | server: server.peerId as Ed25519PeerId, 64 | provider, 65 | dcid 66 | }) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /test/aegir.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs' 2 | import { noise } from '@chainsafe/libp2p-noise' 3 | import { yamux } from '@chainsafe/libp2p-yamux' 4 | import { circuitRelayServer } from '@libp2p/circuit-relay-v2' 5 | import { type Identify, identify } from '@libp2p/identify' 6 | import { type KadDHT, kadDHT } from '@libp2p/kad-dht' 7 | import { tcp } from '@libp2p/tcp' 8 | import { webSockets } from '@libp2p/websockets' 9 | import * as filters from '@libp2p/websockets/filters' 10 | import getPort from 'aegir/get-port' 11 | import { MemoryBlockstore } from 'blockstore-core' 12 | import { MemoryDatastore } from 'datastore-core' 13 | import { createHelia } from 'helia' 14 | import { ipnsSelector } from 'ipns/selector' 15 | import { ipnsValidator } from 'ipns/validator' 16 | import { type Libp2pOptions, createLibp2p, type Libp2p } from 'libp2p' 17 | // import w3nameServer from './mocks/w3name.js' 18 | import type { Helia } from '@helia/interface' 19 | import type { ServiceMap } from '@libp2p/interface' 20 | import type { GlobalOptions, TestOptions } from 'aegir' 21 | 22 | let WEB3_STORAGE_TOKEN: string | null = null 23 | 24 | try { 25 | WEB3_STORAGE_TOKEN = readFileSync('.token', 'utf8').trim() 26 | } catch { 27 | // eslint-disable-next-line no-console 28 | console.log('no web3.storage token provided, skipping pinner/w3 tests') 29 | } 30 | 31 | interface Services extends ServiceMap { 32 | identify: Identify 33 | dht: KadDHT 34 | } 35 | 36 | async function createLibp2pNode (): Promise> { 37 | const datastore = new MemoryDatastore() 38 | const options: Libp2pOptions = { 39 | addresses: { 40 | listen: [ 41 | '/ip4/127.0.0.1/tcp/0/ws' 42 | ] 43 | }, 44 | transports: [ 45 | tcp(), 46 | webSockets({ 47 | filter: filters.all 48 | }) 49 | ], 50 | connectionEncryption: [ 51 | noise() 52 | ], 53 | streamMuxers: [ 54 | yamux() 55 | ], 56 | datastore, 57 | services: { 58 | identify: identify(), 59 | dht: kadDHT({ 60 | validators: { ipns: ipnsValidator }, 61 | selectors: { ipns: ipnsSelector } 62 | }), 63 | relay: circuitRelayServer({ advertise: true }) 64 | } 65 | } 66 | return createLibp2p(options) 67 | } 68 | 69 | export async function createHeliaNode (): Promise>> { 70 | const blockstore = new MemoryBlockstore() 71 | const datastore = new MemoryDatastore() 72 | 73 | const libp2p = await createLibp2pNode() 74 | 75 | const helia = await createHelia>({ 76 | libp2p, 77 | blockstore, 78 | datastore 79 | }) 80 | 81 | return helia 82 | } 83 | 84 | interface BeforeResult { 85 | env: typeof process.env 86 | helia?: Helia> 87 | } 88 | 89 | export default { 90 | test: { 91 | before: async (options: GlobalOptions & TestOptions): Promise => { 92 | const W3_NAME_PORT = await getPort() 93 | // w3nameServer.listen(W3_NAME_PORT) 94 | 95 | const result: BeforeResult = { 96 | env: { 97 | W3_NAME_PORT: W3_NAME_PORT.toString() 98 | } 99 | } 100 | 101 | if (WEB3_STORAGE_TOKEN != null) { 102 | // pinner tests are broken 103 | // result.env.WEB3_STORAGE_TOKEN = WEB3_STORAGE_TOKEN 104 | } 105 | 106 | if (options.runner !== 'node') { 107 | const helia = await createHeliaNode() 108 | 109 | // const ipfsdPort = await getPort() 110 | // const ipfsdServer = await createServer({ 111 | // host: '127.0.0.1', 112 | // port: ipfsdPort 113 | // }, { 114 | // ipfsBin: (await import('go-ipfs')).default.path(), 115 | // kuboRpcModule: kuboRpcClient, 116 | // ipfsOptions: { 117 | // config: { 118 | // Addresses: { 119 | // Swarm: [ 120 | // "/ip4/0.0.0.0/tcp/0", 121 | // "/ip4/0.0.0.0/tcp/0/ws" 122 | // ] 123 | // } 124 | // } 125 | // } 126 | // }).start() 127 | 128 | result.env = { 129 | ...result.env, 130 | RELAY_MULTI_ADDR: helia.libp2p.getMultiaddrs()[0].toString() 131 | } 132 | result.helia = helia 133 | } 134 | 135 | return result 136 | }, 137 | after: async (options: GlobalOptions & TestOptions, beforeResult: BeforeResult) => { 138 | if (options.runner !== 'node') { 139 | // await beforeResult.ipfsdServer.stop() 140 | await beforeResult.helia?.stop() 141 | // w3nameServer.close() 142 | } 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /test/env.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | export interface ProcessEnv { 3 | RELAY_MULTI_ADDR?: string 4 | W3_NAME_PORT: string 5 | WEB3_STORAGE_TOKEN?: string 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/mocks/w3name.ts: -------------------------------------------------------------------------------- 1 | import http from 'http' 2 | import * as ipns from 'ipns' 3 | import * as uint8arrays from 'uint8arrays' 4 | 5 | // Memory object to mock db 6 | const db = new Map() 7 | 8 | // Make our HTTP server 9 | const server = http.createServer((req, res) => { 10 | if (req.url === undefined) { 11 | throw Error('No url passed to mock server') 12 | } 13 | 14 | const reqUrl = req.url 15 | if (reqUrl?.startsWith('/name/')) { 16 | const key = reqUrl.split('/').at(-1) 17 | 18 | if (req.method === 'POST') { 19 | // Save the data to the mocked DB. 20 | req.on('data', chunk => { 21 | db.set(key, chunk.toString()) 22 | }) 23 | 24 | req.on('end', () => { 25 | res.end() 26 | }) 27 | } 28 | 29 | if (req.method === 'GET') { 30 | let record 31 | let entry 32 | // Retrieve saved data from the mocked db. 33 | switch (key) { 34 | // Mock a JSON Error to ensure it's handled by the client. 35 | case 'json-error': 36 | res.setHeader('Content-Type', 'application/json;charset=UTF-8') 37 | res.statusCode = 500 38 | res.write( 39 | JSON.stringify({ message: 'throw an error for the tests' }) 40 | ) 41 | break 42 | 43 | // Mock a text/plain Error to ensure it's handled by the client. 44 | case 'text-error': 45 | res.setHeader('Content-Type', 'text/plain') 46 | res.statusCode = 500 47 | res.write( 48 | 'throw an error for the tests' 49 | ) 50 | break 51 | 52 | // Return the stored key from the mocked db. 53 | default: 54 | record = uint8arrays.fromString(db.get(key), 'base64pad') 55 | entry = ipns.unmarshal(record) 56 | res.write( 57 | JSON.stringify({ 58 | value: entry.value, 59 | record: db.get(key) 60 | }) 61 | ) 62 | break 63 | } 64 | 65 | res.end() 66 | } 67 | } 68 | }) 69 | 70 | export default server 71 | -------------------------------------------------------------------------------- /test/namers/ipns.spec.ts: -------------------------------------------------------------------------------- 1 | // import { kadDHT } from '@libp2p/kad-dht' 2 | import { createEd25519PeerId } from '@libp2p/peer-id-factory' 3 | import { createHelia } from 'helia' 4 | import * as ipnsNamer from '../../src/namers/ipns.js' 5 | import { createCID } from '../utils/create-cid.js' 6 | import { type WithRelay, createLibp2pNode } from '../utils/create-libp2p.js' 7 | import { lanKadProtocol } from '../utils/protocols.js' 8 | import { spec } from './namer.js' 9 | import type { Namer } from '../../src/index.js' 10 | import type { Helia } from '@helia/interface' 11 | import type { Ed25519PeerId } from '@libp2p/interface/peer-id' 12 | import type { Libp2p } from 'libp2p' 13 | import type { CID } from 'multiformats/cid' 14 | 15 | describe.skip('namers/ipns.ts', () => { 16 | let 17 | client: Helia>, 18 | server: Helia>, 19 | namer: Namer, 20 | key: Ed25519PeerId, 21 | value: CID, 22 | newValue: CID 23 | 24 | before(async () => { 25 | client = await createHelia({ 26 | libp2p: await createLibp2pNode() 27 | }) 28 | server = await createHelia({ 29 | libp2p: await createLibp2pNode() 30 | }) 31 | await client.libp2p.dialProtocol(server.libp2p.getMultiaddrs(), lanKadProtocol) 32 | namer = ipnsNamer.ipnsNamer(client) 33 | key = await createEd25519PeerId() 34 | value = await createCID() 35 | newValue = await createCID() 36 | }) 37 | 38 | after(async () => { 39 | await client.stop() 40 | await server.stop() 41 | }) 42 | 43 | it('publishes name/value pair', async () => { 44 | await spec.publish({ 45 | publish: namer.publish, 46 | key, 47 | value 48 | }) 49 | }) 50 | 51 | it('resolves name/value pair', async () => { 52 | await spec.resolve({ 53 | resolve: namer.resolve, 54 | key, 55 | value 56 | }) 57 | }) 58 | 59 | it('updates name/value pair', async () => { 60 | await spec.update({ 61 | publish: namer.publish, 62 | resolve: namer.resolve, 63 | key, 64 | value, 65 | newValue 66 | }) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /test/namers/namer.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'aegir/chai' 2 | import type { Namer } from '../../src' 3 | import type { Ed25519PeerId } from '@libp2p/interface/peer-id' 4 | import type { CID } from 'multiformats/cid' 5 | 6 | interface NamerOptions { 7 | key: Ed25519PeerId 8 | value: CID 9 | } 10 | 11 | interface PublishOptions extends NamerOptions { 12 | publish: Namer['publish'] 13 | } 14 | 15 | async function publish ({ publish, key, value }: PublishOptions): Promise { 16 | await publish(key, value) 17 | } 18 | 19 | interface ResolveOptions extends NamerOptions { 20 | resolve: Namer['resolve'] 21 | } 22 | 23 | async function resolve ({ resolve, key, value }: ResolveOptions): Promise { 24 | const cid = await resolve(key) 25 | 26 | expect(cid.equals(value)).to.equal(true) 27 | } 28 | 29 | interface UpdateOptions extends PublishOptions, ResolveOptions { 30 | newValue: CID 31 | } 32 | 33 | async function update ({ publish, resolve, key, value, newValue }: UpdateOptions): Promise { 34 | await spec.resolve({ resolve, key, value }) 35 | await spec.publish({ publish, key, value: newValue }) 36 | await spec.resolve({ resolve, key, value: newValue }) 37 | } 38 | 39 | export const spec = { 40 | publish, 41 | resolve, 42 | update 43 | } 44 | -------------------------------------------------------------------------------- /test/namers/w3.spec.ts: -------------------------------------------------------------------------------- 1 | import { createEd25519PeerId } from '@libp2p/peer-id-factory' 2 | import { MemoryDatastore } from 'datastore-core' 3 | import W3NameService from 'w3name/service' 4 | import { w3Namer, revisionState } from '../../src/namers/w3.js' 5 | import { createCID } from '../utils/create-cid.js' 6 | // import { createRateLimiter } from '../utils/create-rate-limitter.js' 7 | import { spec } from './namer.js' 8 | import type { Namer } from '../../src/index.js' 9 | import type { Ed25519PeerId } from '@libp2p/interface/peer-id' 10 | import type { CID } from 'multiformats/cid' 11 | 12 | describe('namers/w3.ts', () => { 13 | let 14 | namer: Namer, 15 | key: Ed25519PeerId, 16 | value: CID, 17 | newValue: CID 18 | 19 | // const endpoint = new URL('http://localhost:' + process.env.W3_NAME_PORT) 20 | 21 | before(async () => { 22 | // if (process?.env?.W3NS == null) { 23 | // throw new Error('W3NS env variable missing') 24 | // } 25 | // const service: W3NameService = { 26 | // endpoint: new URL(process.env.W3NS), 27 | // waitForRateLimit: createRateLimiter() 28 | // } 29 | 30 | const datastore = new MemoryDatastore() 31 | const revisions = revisionState(datastore) 32 | 33 | namer = w3Namer(new W3NameService(), revisions) 34 | key = await createEd25519PeerId() 35 | value = await createCID() 36 | newValue = await createCID() 37 | }) 38 | 39 | it('publishes name/value pair', async () => { 40 | await spec.publish({ 41 | publish: namer.publish, 42 | key, 43 | value 44 | }) 45 | }) 46 | 47 | it('resolve name/value pair', async () => { 48 | await spec.resolve({ 49 | resolve: namer.resolve, 50 | key, 51 | value 52 | }) 53 | }) 54 | 55 | it('updates name/value pair', async () => { 56 | await spec.update({ 57 | publish: namer.publish, 58 | resolve: namer.resolve, 59 | key, 60 | value, 61 | newValue 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /test/pinners/pinner.ts: -------------------------------------------------------------------------------- 1 | export * from 'interface-blockstore-tests' 2 | -------------------------------------------------------------------------------- /test/pinners/w3.spec.ts: -------------------------------------------------------------------------------- 1 | import { Web3Storage } from 'web3.storage' 2 | import { w3Pinner } from '../../src/pinners/w3.js' 3 | import { interfaceBlockstoreTests as suite } from './pinner.js' 4 | 5 | const token = process.env.WEB3_STORAGE_TOKEN 6 | 7 | describe('pinners/w3.ts', () => { 8 | if (token != null) { 9 | suite({ 10 | setup () { 11 | const client = new Web3Storage({ token }) 12 | return w3Pinner(client) 13 | }, 14 | teardown () {} 15 | }) 16 | } 17 | }) 18 | -------------------------------------------------------------------------------- /test/utils/create-cid.ts: -------------------------------------------------------------------------------- 1 | import { CID } from 'multiformats/cid' 2 | import * as raw from 'multiformats/codecs/raw' 3 | import { sha256 } from 'multiformats/hashes/sha2' 4 | 5 | export async function createCID (): Promise { 6 | return CID.create(1, raw.code, await sha256.digest(new TextEncoder().encode(Date.now().toString()))) 7 | } 8 | -------------------------------------------------------------------------------- /test/utils/create-helia.ts: -------------------------------------------------------------------------------- 1 | import { MemoryBlockstore } from 'blockstore-core' 2 | import { MemoryDatastore } from 'datastore-core' 3 | import { createHelia } from 'helia' 4 | import { createLibp2pNode, type WithRelay } from './create-libp2p.js' 5 | import type { Helia } from '@helia/interface' 6 | import type { Libp2p } from 'libp2p' 7 | 8 | export async function createHeliaNode (): Promise>> { 9 | const blockstore = new MemoryBlockstore() 10 | const datastore = new MemoryDatastore() 11 | 12 | const libp2p = await createLibp2pNode() 13 | 14 | const helia = await createHelia({ 15 | libp2p, 16 | blockstore, 17 | datastore 18 | }) 19 | 20 | return helia 21 | } 22 | -------------------------------------------------------------------------------- /test/utils/create-kubo.ts: -------------------------------------------------------------------------------- 1 | import * as goIpfs from 'go-ipfs' 2 | import { type Controller, type ControllerOptions, createController } from 'ipfsd-ctl' 3 | import * as kuboRpcClient from 'kubo-rpc-client' 4 | import mergeOptions from 'merge-options' 5 | import { isElectronMain, isNode } from 'wherearewe' 6 | 7 | export async function createKuboNode (options: ControllerOptions<'go'> = {}): Promise { 8 | const opts = mergeOptions({ 9 | kuboRpcModule: kuboRpcClient, 10 | ipfsBin: isNode || isElectronMain ? goIpfs.path() : undefined, 11 | test: true, 12 | endpoint: process.env.IPFSD_SERVER, 13 | ipfsOptions: { 14 | config: { 15 | Addresses: { 16 | Swarm: [ 17 | '/ip4/127.0.0.1/tcp/4001', 18 | '/ip4/127.0.0.1/tcp/4002/ws' 19 | ] 20 | } 21 | } 22 | } 23 | }, options) 24 | 25 | return createController(opts) 26 | } 27 | -------------------------------------------------------------------------------- /test/utils/create-libp2p.browser.ts: -------------------------------------------------------------------------------- 1 | import { noise } from '@chainsafe/libp2p-noise' 2 | import { yamux } from '@chainsafe/libp2p-yamux' 3 | import { circuitRelayTransport } from '@libp2p/circuit-relay-v2' 4 | import { webRTC, webRTCDirect } from '@libp2p/webrtc' 5 | import { webSockets } from '@libp2p/websockets' 6 | import * as filters from '@libp2p/websockets/filters' 7 | import { multiaddr } from '@multiformats/multiaddr' 8 | import { MemoryDatastore } from 'datastore-core' 9 | import { createLibp2p, type Libp2p, type Libp2pOptions } from 'libp2p' 10 | import services, { type Services } from './services.js' 11 | 12 | export async function createLibp2pNode (options?: Libp2pOptions): Promise> { 13 | const datastore = new MemoryDatastore() 14 | 15 | const libp2p = await createLibp2p({ 16 | addresses: { 17 | listen: [ 18 | '/webrtc' 19 | ] 20 | }, 21 | transports: [ 22 | webRTC(), 23 | webRTCDirect(), 24 | webSockets({ filter: filters.all }), 25 | circuitRelayTransport({ 26 | discoverRelays: 1 27 | }) 28 | ], 29 | connectionEncryption: [ 30 | noise() 31 | ], 32 | connectionGater: { 33 | denyDialMultiaddr: () => { 34 | // by default we refuse to dial local addresses from the browser since they 35 | // are usually sent by remote peers broadcasting undialable multiaddrs but 36 | // here we are explicitly connecting to a local node so do not deny dialing 37 | // any discovered address 38 | return false 39 | } 40 | }, 41 | streamMuxers: [ 42 | yamux() 43 | ], 44 | datastore, 45 | ...options, 46 | services 47 | }) 48 | 49 | await libp2p.dial(multiaddr(process.env.RELAY_MULTI_ADDR)) 50 | await new Promise(resolve => setTimeout(resolve, 1000)) 51 | 52 | return libp2p 53 | } 54 | 55 | // const node = await createLibp2p({ 56 | // addresses: { 57 | // listen: [ 58 | // '/webrtc' 59 | // ] 60 | // }, 61 | // transports: [ 62 | // webSockets({ 63 | // filter: filters.all, 64 | // }), 65 | // webRTC(), 66 | // circuitRelayTransport({ 67 | // discoverRelays: 1, 68 | // }), 69 | // ], 70 | // connectionEncryption: [noise()], 71 | // streamMuxers: [mplex()], 72 | // connectionGater: { 73 | // denyDialMultiaddr: () => { 74 | // // by default we refuse to dial local addresses from the browser since they 75 | // // are usually sent by remote peers broadcasting undialable multiaddrs but 76 | // // here we are explicitly connecting to a local node so do not deny dialing 77 | // // any discovered address 78 | // return false 79 | // } 80 | // }, 81 | -------------------------------------------------------------------------------- /test/utils/create-libp2p.ts: -------------------------------------------------------------------------------- 1 | import { noise } from '@chainsafe/libp2p-noise' 2 | import { yamux } from '@chainsafe/libp2p-yamux' 3 | import { circuitRelayServer, type CircuitRelayService } from '@libp2p/circuit-relay-v2' 4 | import { tcp } from '@libp2p/tcp' 5 | import { webSockets } from '@libp2p/websockets' 6 | import * as filters from '@libp2p/websockets/filters' 7 | import { MemoryDatastore } from 'datastore-core' 8 | import { createLibp2p, type Libp2p } from 'libp2p' 9 | import services, { type Services } from './services.js' 10 | import type { PeerId } from '@libp2p/interface/peer-id' 11 | 12 | export interface WithRelay extends Services { 13 | relay: CircuitRelayService 14 | } 15 | 16 | export async function createLibp2pNode (peerId?: PeerId): Promise> { 17 | const datastore = new MemoryDatastore() 18 | 19 | return createLibp2p({ 20 | peerId, 21 | addresses: { 22 | listen: [ 23 | '/ip4/127.0.0.1/tcp/0/ws' 24 | ] 25 | }, 26 | transports: [ 27 | tcp(), 28 | webSockets({ 29 | filter: filters.all 30 | }) 31 | ], 32 | connectionEncryption: [ 33 | noise() 34 | ], 35 | streamMuxers: [ 36 | yamux() 37 | ], 38 | datastore, 39 | services: { 40 | ...services, 41 | relay: circuitRelayServer({ advertise: true }) 42 | } 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /test/utils/protocols.ts: -------------------------------------------------------------------------------- 1 | export const kadProtocol = '/ipfs/kad/1.0.0' 2 | export const lanKadProtocol = '/ipfs/lan/kad/1.0.0' 3 | -------------------------------------------------------------------------------- /test/utils/services.ts: -------------------------------------------------------------------------------- 1 | import { ipnsSelector, ipnsValidator } from '@helia/ipns' 2 | import { type Identify, identify } from '@libp2p/identify' 3 | import { type KadDHT, kadDHT, removePublicAddressesMapper } from '@libp2p/kad-dht' 4 | import { lanKadProtocol } from './protocols.js' 5 | import type { ServiceMap } from '@libp2p/interface' 6 | 7 | export interface Services extends ServiceMap { 8 | identify: Identify 9 | dht: KadDHT 10 | } 11 | 12 | const services = { 13 | identify: identify(), 14 | 15 | dht: kadDHT({ 16 | protocol: lanKadProtocol, 17 | peerInfoMapper: removePublicAddressesMapper, 18 | validators: { ipns: ipnsValidator }, 19 | selectors: { ipns: ipnsSelector }, 20 | clientMode: false 21 | }) 22 | } 23 | 24 | export default services 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/aegir/src/config/tsconfig.aegir.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": [ 7 | "src", 8 | "test" 9 | ] 10 | } 11 | --------------------------------------------------------------------------------