├── .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 |
--------------------------------------------------------------------------------