mirror of
https://github.com/Permissionless-Software-Foundation/ipfs-bch-wallet-consumer.git
synced 2026-09-22 09:12:03 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43025f0265 | ||
|
|
7ea17fb57c | ||
|
|
efb29104d7 | ||
|
|
971474d493 | ||
|
|
71a6de03b3 | ||
|
|
98dd904fa5 | ||
|
|
b5c7e2efb3 | ||
|
|
3c9bbb097e | ||
|
|
8956968027 | ||
|
|
14c724bf9c | ||
|
|
a9f7e609e9 | ||
|
|
fee68e0f59 | ||
|
|
86c005c205 | ||
|
|
d4d5b2eb68 | ||
|
|
32a651b7a8 | ||
|
|
7da5c5b4d6 | ||
|
|
ba5ead998d | ||
|
|
409a0f1049 | ||
|
|
bd2f27bb64 | ||
|
|
55d8d131cf | ||
|
|
5a6e3247e8 | ||
|
|
2a522775eb | ||
|
|
d677c83795 | ||
|
|
6cfaceb99e | ||
|
|
b12cc194f4 | ||
|
|
7cffd9f377 | ||
|
|
0a406cb9a4 | ||
|
|
f172a51016 | ||
|
|
00d53659b1 | ||
|
|
b0797d92f5 | ||
|
|
48484ed7af | ||
|
|
6a4aadde8a | ||
|
|
79e3aba26e | ||
|
|
bf61359bc7 | ||
|
|
1911991511 | ||
|
|
ef0179fb3f | ||
|
|
38bdfe306b | ||
|
|
5c674f4dc5 | ||
|
|
80155393d9 | ||
|
|
232417b8b1 | ||
|
|
36cb9c5734 | ||
|
|
a79d152993 | ||
|
|
ce7c3a0e2f | ||
|
|
e6b0580209 | ||
|
|
1f3c639100 | ||
|
|
e6efc4f49b | ||
|
|
dbbd321029 | ||
|
|
439dbc54ae | ||
|
|
547e59f52f | ||
|
|
a0d385de73 | ||
|
|
bec136009f | ||
|
|
98b7fa7734 | ||
|
|
d7bb476858 | ||
|
|
18c71c383f | ||
|
|
de85a933c4 | ||
|
|
16247372f1 | ||
|
|
c6c8b6418c | ||
|
|
10c4e7380f | ||
|
|
4933dce472 | ||
|
|
80c2822716 | ||
|
|
f48ff9a67e | ||
|
|
4bd2385359 | ||
|
|
be71492773 | ||
|
|
bcb70edf2b | ||
|
|
07071032bc | ||
|
|
f476e119a2 | ||
|
|
0fed232428 | ||
|
|
0273510485 | ||
|
|
a62dee37f4 | ||
|
|
8d6af3e1b7 | ||
|
|
ee0f2da641 | ||
|
|
44499d9652 | ||
|
|
365a5dcd4b | ||
|
|
7f5b456018 | ||
|
|
c7217486bd | ||
|
|
050141d31e | ||
|
|
b697f37199 | ||
|
|
ceb58caba1 | ||
|
|
2724b500ee | ||
|
|
1b84e7beb8 | ||
|
|
383900d8b5 | ||
|
|
ba806d86b5 | ||
|
|
4d7c7350a0 | ||
|
|
adc6500aed | ||
|
|
eb9fce73eb | ||
|
|
ec3ed165e4 | ||
|
|
d41d20cc81 | ||
|
|
5f965e7146 | ||
|
|
f382bb4578 | ||
|
|
ba37f87f80 | ||
|
|
eaa9c23f55 | ||
|
|
ac3730dbd9 | ||
|
|
581b8eb626 | ||
|
|
cd4ec2711e | ||
|
|
e9e3dbad0d | ||
|
|
295b361431 | ||
|
|
ad60df9d51 | ||
|
|
6ae635c21d | ||
|
|
61807a2f3d | ||
|
|
16ba5a8980 | ||
|
|
99b817549e | ||
|
|
60a8c335c8 | ||
|
|
1783768c32 | ||
|
|
b3630eaa06 | ||
|
|
d691016911 | ||
|
|
6506aead8c | ||
|
|
ddfc83d026 | ||
|
|
fe42c1e0e3 | ||
|
|
928a211a63 | ||
|
|
8bff69a4ca | ||
|
|
5fced7d501 | ||
|
|
7cdf622ad6 | ||
|
|
afd8ceac7b | ||
|
|
5d63db3662 | ||
|
|
9f6cd7dbf1 | ||
|
|
0725aa3ada | ||
|
|
3eb88d4076 | ||
|
|
1c61f150b1 | ||
|
|
4c74b287d8 | ||
|
|
264d762f74 | ||
|
|
3c02461c6b | ||
|
|
a30f2be51f | ||
|
|
0690a9fb65 | ||
|
|
4fa103765e | ||
|
|
b0d6ce40a9 | ||
|
|
c57a3d935b | ||
|
|
82b5943601 | ||
|
|
41b76967f5 | ||
|
|
e4c2ee44c1 | ||
|
|
c05f022995 | ||
|
|
7723c3a201 | ||
|
|
8e338818ca | ||
|
|
ea51aa420c | ||
|
|
bc410678b4 | ||
|
|
a42c15c969 | ||
|
|
a3102a0278 | ||
|
|
f4f142dc48 | ||
|
|
e03a70c9a3 | ||
|
|
d47b8797d4 | ||
|
|
56ba0aad02 | ||
|
|
825bc6d2de | ||
|
|
bf206107f8 | ||
|
|
18fe63a5ad | ||
|
|
d6e0e7b903 |
@@ -68,5 +68,8 @@ ipfsdata
|
||||
.ipfsdata
|
||||
run-dev.sh
|
||||
data/
|
||||
run-dev.sh
|
||||
|
||||
!README.md
|
||||
|
||||
wallet.json
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# Permissionless Software Foundation Community Contributing Guide 1.0
|
||||
|
||||
This document describes a very simple process suitable for most projects under
|
||||
the PSF umbrella. It is based on [this Medium article](https://medium.com/the-node-js-collection/healthy-open-source-967fa8be7951) and the [Node.js Community Contribution Guide](https://github.com/nodejs/TSC/blob/master/BasePolicies/CONTRIBUTING.md).
|
||||
Projects are encouraged to adopt this whether they
|
||||
are hosted under the PSF or not.
|
||||
|
||||
The goal of this document is to create a contribution process that:
|
||||
|
||||
* Encourages new contributions.
|
||||
* Encourages contributors to remain involved.
|
||||
* Avoids unnecessary processes and bureaucracy whenever possible.
|
||||
* Creates a transparent decision making process which makes it clear how
|
||||
contributors can be involved in decision making.
|
||||
|
||||
This document is based on much prior art in the Node.js community, io.js,
|
||||
and the Node.js project.
|
||||
|
||||
Additional guidance can be found at the [Permissionless Software Foundation Telegram Channel](https://t.me/permissionless_software).
|
||||
|
||||
## Vocabulary
|
||||
|
||||
* A **Contributor** is any individual creating or commenting on an issue or pull request.
|
||||
* A **Committer** is a subset of contributors who have been given write access to the repository.
|
||||
* A **TC (Technical Committee)** is a group of committers representing the required technical
|
||||
expertise to resolve rare disputes.
|
||||
|
||||
# Logging Issues
|
||||
|
||||
Log an issue for any question or problem you might have. When in doubt, log an issue,
|
||||
any additional policies about what to include will be provided in the responses. The only
|
||||
exception is security disclosures which should be sent privately.
|
||||
|
||||
Committers may direct you to another repository, ask for additional clarifications, and
|
||||
add appropriate metadata before the issue is addressed.
|
||||
|
||||
Please be courteous, respectful, and every participant is expected to follow the
|
||||
project's Code of Conduct.
|
||||
|
||||
# Contributions
|
||||
|
||||
Any change to resources in this repository must be through pull requests. This applies to all changes
|
||||
to documentation, code, binary files, etc. Even long term committers and TC members must use
|
||||
pull requests.
|
||||
|
||||
No pull request can be merged without being reviewed.
|
||||
|
||||
For non-trivial contributions, pull requests should sit for at least 36 hours to ensure that
|
||||
contributors in other timezones have time to review. Consideration should also be given to
|
||||
weekends and other holiday periods to ensure active committers all have reasonable time to
|
||||
become involved in the discussion and review process if they wish.
|
||||
|
||||
The default for each contribution is that it is accepted once no committer has an objection.
|
||||
During review committers may also request that a specific contributor who is most versed in a
|
||||
particular area gives a "LGTM" before the PR can be merged. There is no additional "sign off"
|
||||
process for contributions to land. Once all issues brought by committers are addressed it can
|
||||
be landed by any committer.
|
||||
|
||||
In the case of an objection being raised in a pull request by another committer, all involved
|
||||
committers should seek to arrive at a consensus by way of addressing concerns being expressed
|
||||
by discussion, compromise on the proposed change, or withdrawal of the proposed change.
|
||||
|
||||
If a contribution is controversial and committers cannot agree about how to get it to land
|
||||
or if it should land then it should be escalated to the TC. TC members should regularly
|
||||
discuss pending contributions in order to find a resolution. It is expected that only a
|
||||
small minority of issues be brought to the TC for resolution and that discussion and
|
||||
compromise among committers be the default resolution mechanism.
|
||||
|
||||
# Becoming a Committer
|
||||
|
||||
All contributors who land a non-trivial contribution should be on-boarded in a timely manner,
|
||||
and added as a committer, and be given write access to the repository.
|
||||
|
||||
Committers are expected to follow this policy and continue to send pull requests, go through
|
||||
proper review, and have other committers merge their pull requests.
|
||||
|
||||
# TC Process
|
||||
|
||||
The TC uses a "consensus seeking" process for issues that are escalated to the TC.
|
||||
The group tries to find a resolution that has no open objections among TC members.
|
||||
If a consensus cannot be reached that has no objections then a majority wins vote
|
||||
is called. It is also expected that the majority of decisions made by the TC are via
|
||||
a consensus seeking process and that voting is only used as a last-resort.
|
||||
|
||||
Resolution may involve returning the issue to committers with suggestions on how to
|
||||
move forward towards a consensus. It is not expected that a meeting of the TC
|
||||
will resolve all issues on its agenda during that meeting and may prefer to continue
|
||||
the discussion happening among the committers.
|
||||
|
||||
Members can be added to the TC at any time. Any committer can nominate another committer
|
||||
to the TC and the TC uses its standard consensus seeking process to evaluate whether or
|
||||
not to add this new member. Members who do not participate consistently at the level of
|
||||
a majority of the other members are expected to resign.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Pedigree
|
||||
|
||||
This repository was forked from [koa-api-boilerplate](https://github.com/christroutner/koa-api-boilerplate). Code changes from that upstream repository is frequently pulled in and merged to this repository.
|
||||
@@ -12,8 +12,8 @@ More details are available at [CashStack.info](https://CashStack.info).
|
||||
|
||||
## Requirements
|
||||
|
||||
- node **^14.18.2**
|
||||
- npm **^8.3.0**
|
||||
- node **^16.20.2**
|
||||
- npm **^8.19.4**
|
||||
- Docker **^20.10.8**
|
||||
- Docker Compose **^1.27.4**
|
||||
|
||||
@@ -44,9 +44,23 @@ docker-compose pull
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
You can bring the containers back up with `docker-compose up -d`.
|
||||
- You can bring the containers down with `docker-compose down`
|
||||
- You can bring the containers back up with `docker-compose up -d`.
|
||||
|
||||
## Code Structure
|
||||
## Configuration
|
||||
|
||||
This program is intended to started with a Bash shell script. In that script, you can set the following environment variables:
|
||||
|
||||
```bash
|
||||
# The IPFS ID of the service running ipfs-bch-wallet-service
|
||||
export PREFERRED_P2WDB_PROVIDER=12D3KooWCNpwHbHmkNSJsqu3CQVVe3iW8g6e1gn3kWTPahj2igiy
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
This app is intended to be started via a bash shell script. See the environment variables used to configure this app in the [config/env/common.js file](./config/env/common.js).
|
||||
|
||||
## File Structure
|
||||
|
||||
The file layout of this repository differs from the koa-api-boilerplate. Instead, it follows the file layout of [Clean Architecture](https://christroutner.github.io/trouts-blog/blog/clean-architecture).
|
||||
|
||||
@@ -55,14 +69,13 @@ The file layout of this repository differs from the koa-api-boilerplate. Instead
|
||||
- `npm start` Start server on live mode
|
||||
- `npm run docs` Generate API documentation
|
||||
- `npm test` Run mocha tests
|
||||
- `docker-compose build` Build a 'production' Docker container
|
||||
- `docker-compose up` Run the docker container
|
||||
|
||||
## Documentation
|
||||
|
||||
API documentation is written inline and generated by [apidoc](http://apidocjs.com/).
|
||||
API documentation is written inline and generated by [apidoc](http://apidocjs.com/). Docs can be generated with this command:
|
||||
- `npm run docs`
|
||||
|
||||
Visit `http://localhost:5000/docs/` to view docs
|
||||
Visit `http://localhost:5020/` to view docs
|
||||
|
||||
There is additional developer documentation in the [dev-docs directory](./dev-docs).
|
||||
|
||||
|
||||
Vendored
+74
-19
@@ -1,6 +1,11 @@
|
||||
/*
|
||||
This file is used to store unsecure, application-specific data common to all
|
||||
environments.
|
||||
|
||||
Additional Environent Variables:
|
||||
- CONNECT_PREF: should have a value of 'cr' (default), or 'direct'. This is
|
||||
used by helia-coord to select a connection preference between peers. Servers
|
||||
with an ip4 or ip6 address should use 'direct'.
|
||||
*/
|
||||
|
||||
/* eslint no-unneeded-ternary:0 */
|
||||
@@ -22,7 +27,7 @@ const ipfsCoordName = process.env.COORD_NAME
|
||||
|
||||
export default {
|
||||
// Configure TCP port.
|
||||
port: process.env.PORT || 5005,
|
||||
port: process.env.PORT || 5015,
|
||||
|
||||
// Password for HTML UI that displays logs.
|
||||
logPass: 'test',
|
||||
@@ -38,14 +43,43 @@ export default {
|
||||
? process.env.EMAILPASS
|
||||
: 'emailpassword',
|
||||
|
||||
// BEGIN WALLET CONFIGURATION
|
||||
|
||||
// BCH Mnemonic for generating encryption keys and payment address
|
||||
mnemonic: process.env.MNEMONIC ? process.env.MNEMONIC : '',
|
||||
|
||||
// (Optional) use a wallet file generated by psf-bch-wallet.
|
||||
walletFile: process.env.WALLET_FILE
|
||||
? process.env.WALLET_FILE
|
||||
: `${__dirname.toString()}/../../wallet.json`,
|
||||
|
||||
// Wallet Configuration
|
||||
// Default value: web3 - connect to bch-api through web 3 JSON RPC over IPFS.
|
||||
// Alternate value: web2 - connect to bch-api through a web 2 REST API over HTTP.
|
||||
walletInterface: process.env.WALLET_INTERFACE ? process.env.WALLET_INTERFACE : 'web3',
|
||||
|
||||
// Wallet API Server
|
||||
// Default web2 server: https://api.fullstack.cash/v5/
|
||||
// Default web3 server: https://free-bch.fullstack.cash/
|
||||
apiServer: process.env.WALLET_INTERFACE === 'web2'
|
||||
? (
|
||||
process.env.APISERVER
|
||||
? process.env.APISERVER
|
||||
: 'https://api.fullstack.cash/v5/'
|
||||
)
|
||||
: 'https://free-bch.fullstack.cash/',
|
||||
|
||||
// Basic Authentication Password for connecting to bch-api through a web 2
|
||||
// REST API over HTTP.
|
||||
authPass: process.env.WALLET_AUTH_PASS
|
||||
? process.env.WALLET_AUTH_PASS
|
||||
: '',
|
||||
|
||||
// FullStack.cash account information, used for automatic JWT handling.
|
||||
getJwtAtStartup: process.env.GET_JWT_AT_STARTUP ? true : false,
|
||||
authServer: process.env.AUTHSERVER
|
||||
? process.env.AUTHSERVER
|
||||
: 'https://auth.fullstack.cash',
|
||||
apiServer: process.env.APISERVER
|
||||
? process.env.APISERVER
|
||||
: 'https://api.fullstack.cash/v5/',
|
||||
fullstackLogin: process.env.FULLSTACKLOGIN
|
||||
? process.env.FULLSTACKLOGIN
|
||||
: 'demo@demo.com',
|
||||
@@ -53,8 +87,17 @@ export default {
|
||||
? process.env.FULLSTACKPASS
|
||||
: 'demo',
|
||||
|
||||
// IPFS settings.
|
||||
useIpfs: process.env.DISABLE_IPFS ? false : true, // Disable IPFS flag
|
||||
// END WALLET CONFIGURATION
|
||||
|
||||
// BEGIN IPFS CONFIGURATION
|
||||
|
||||
// Debug verbosity level of helia-coord.
|
||||
// 0 = no debug logs. 3 = maximum logs.
|
||||
debugLevel: process.env.DEBUG_LEVEL ? parseInt(process.env.DEBUG_LEVEL) : 2,
|
||||
|
||||
// Enable/Disable the IPFS node at startup. Enabled by default.
|
||||
useIpfs: process.env.DISABLE_IPFS ? false : true,
|
||||
|
||||
isCircuitRelay: process.env.ENABLE_CIRCUIT_RELAY ? true : false,
|
||||
// SSL domain used for websocket connection via browsers.
|
||||
crDomain: process.env.CR_DOMAIN ? process.env.CR_DOMAIN : '',
|
||||
@@ -68,10 +111,10 @@ export default {
|
||||
'@type': 'WebAPI',
|
||||
name: ipfsCoordName,
|
||||
version,
|
||||
protocol: 'generic-service',
|
||||
protocol: 'ipfs-bch-wallet-consumer',
|
||||
description:
|
||||
'This is a generic IPFS Serivice Provider that uses JSON RPC over IPFS to communicate with it. This instance has not been customized. Source code: https://github.com/Permissionless-Software-Foundation/ipfs-service-provider',
|
||||
documentation: 'https://ipfs-service-provider.fullstack.cash/',
|
||||
'REST API gateway for accessing BCH blockchain services over the web3 Cash Stack (https://CashStack.info)',
|
||||
documentation: 'https://ipfs-bch-wallet-consumer.fullstack.cash/',
|
||||
provider: {
|
||||
'@type': 'Organization',
|
||||
name: 'Permissionless Software Foundation',
|
||||
@@ -83,10 +126,25 @@ export default {
|
||||
ipfsTcpPort: process.env.IPFS_TCP_PORT ? process.env.IPFS_TCP_PORT : 4101,
|
||||
ipfsWsPort: process.env.IPFS_WS_PORT ? process.env.IPFS_WS_PORT : 4103,
|
||||
|
||||
// BCH Mnemonic for generating encryption keys and payment address
|
||||
mnemonic: process.env.MNEMONIC ? process.env.MNEMONIC : '',
|
||||
// Settings for production, using external go-ipfs node.
|
||||
isProduction: process.env.SVC_ENV === 'prod' ? true : false,
|
||||
ipfsHost: process.env.IPFS_HOST ? process.env.IPFS_HOST : 'localhost',
|
||||
ipfsApiPort: process.env.IPFS_API_PORT
|
||||
? parseInt(process.env.IPFS_API_PORT)
|
||||
: 5001,
|
||||
|
||||
debugLevel: process.env.DEBUG_LEVEL ? parseInt(process.env.DEBUG_LEVEL) : 2,
|
||||
chatPubSubChan: 'psf-ipfs-chat-001',
|
||||
|
||||
// This can add specific Circuit Relay v2 servers to connect to.
|
||||
bootstrapRelays: [
|
||||
// v2 Circuit Relay (Token Tiger)
|
||||
// '/ip4/137.184.93.145/tcp/8001/p2p/12D3KooWGMEKkdJfyZbwdH9EafZbRTtMn7FnhWPrE4MhRty2763g',
|
||||
|
||||
// v2 Circuit Relay server (FullStack.cash)
|
||||
// '/ip4/78.46.129.7/tcp/4001/p2p/12D3KooWFQ11GQ5NubsJGhYZ4X3wrAGimLevxfm6HPExCrMYhpSL'
|
||||
],
|
||||
|
||||
// END IPFS CONFIGURATION
|
||||
|
||||
// Preferred provider running ipfs-bch-wallet-service for BCH wallet.
|
||||
preferredProvider: process.env.PREFERRED_PROVIDER
|
||||
@@ -103,12 +161,9 @@ export default {
|
||||
? process.env.PREFERRED_P2WDB_PROVIDER
|
||||
: '',
|
||||
|
||||
// Settings for production, using external go-ipfs node.
|
||||
isProduction: process.env.CONSUMER_ENV === 'production' ? true : false,
|
||||
ipfsHost: process.env.IPFS_HOST ? process.env.IPFS_HOST : 'localhost',
|
||||
ipfsApiPort: process.env.IPFS_API_PORT
|
||||
? parseInt(process.env.IPFS_API_PORT)
|
||||
: 5001,
|
||||
// Preferred P2WDB provider
|
||||
preferredIpfsFileProvider: process.env.PREFERRED_IPFS_FILE_PROVIDER
|
||||
? process.env.PREFERRED_IPFS_FILE_PROVIDER
|
||||
: ''
|
||||
|
||||
chatPubSubChan: 'psf-ipfs-chat-001'
|
||||
}
|
||||
|
||||
@@ -14,8 +14,4 @@ if (env === 'test') {
|
||||
config = production
|
||||
}
|
||||
|
||||
// const importStr = `./env/${env}.js`
|
||||
// console.log('importStr: ', importStr)
|
||||
// import config from importStr
|
||||
|
||||
export default Object.assign({}, common, config)
|
||||
|
||||
@@ -53,7 +53,4 @@ function applyPassportMods (passport) {
|
||||
return true
|
||||
}
|
||||
|
||||
// For testing
|
||||
// export default { passport, passportCallback };
|
||||
// export default applyPassportMods
|
||||
export { applyPassportMods, passportCallback }
|
||||
|
||||
Generated
+13347
-17980
File diff suppressed because it is too large
Load Diff
+20
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ipfs-bch-wallet-consumer",
|
||||
"version": "1.0.0",
|
||||
"version": "3.1.1",
|
||||
"description": "A (local) REST API that proxies BCH wallet services delivered over IPFS (global).",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
@@ -24,12 +24,24 @@
|
||||
},
|
||||
"repository": "Permissionless-Software-Foundation/ipfs-bch-wallet-consumer",
|
||||
"dependencies": {
|
||||
"@psf/bch-js": "6.7.3",
|
||||
"@chainsafe/libp2p-gossipsub": "11.0.1",
|
||||
"@chainsafe/libp2p-noise": "14.1.0",
|
||||
"@chainsafe/libp2p-yamux": "6.0.1",
|
||||
"@helia/unixfs": "3.0.1",
|
||||
"@libp2p/bootstrap": "10.0.7",
|
||||
"@libp2p/circuit-relay-v2": "1.0.13",
|
||||
"@libp2p/identify": "1.0.6",
|
||||
"@libp2p/keychain": "4.0.7",
|
||||
"@libp2p/logger": "4.0.5",
|
||||
"@libp2p/tcp": "9.0.7",
|
||||
"@libp2p/webrtc": "4.0.17",
|
||||
"axios": "0.27.2",
|
||||
"bcryptjs": "2.4.3",
|
||||
"blockstore-fs": "1.1.8",
|
||||
"datastore-fs": "9.1.6",
|
||||
"glob": "7.1.6",
|
||||
"ipfs-coord-esm": "9.1.13",
|
||||
"ipfs-http-client": "58.0.0",
|
||||
"helia": "2.1.0",
|
||||
"helia-coord": "1.5.6",
|
||||
"jsonrpc-lite": "2.2.0",
|
||||
"jsonwebtoken": "8.5.1",
|
||||
"jwt-bch-lib": "1.3.0",
|
||||
@@ -44,13 +56,15 @@
|
||||
"koa-router": "10.0.0",
|
||||
"koa-static": "5.0.0",
|
||||
"koa2-ratelimit": "0.9.1",
|
||||
"libp2p": "1.2.1",
|
||||
"line-reader": "0.4.0",
|
||||
"minimal-slp-wallet": "5.11.1",
|
||||
"mime-types": "2.1.35",
|
||||
"minimal-slp-wallet": "5.11.2",
|
||||
"mongoose": "5.13.14",
|
||||
"node-fetch": "npm:@achingbrain/node-fetch@2.6.7",
|
||||
"nodemailer": "6.7.5",
|
||||
"passport-local": "1.0.0",
|
||||
"public-ip": "4.0.4",
|
||||
"public-ip": "6.0.1",
|
||||
"semver": "7.3.5",
|
||||
"winston": "3.3.3",
|
||||
"winston-daily-rotate-file": "4.5.0"
|
||||
|
||||
@@ -12,9 +12,12 @@ RUN apt-get update
|
||||
RUN apt-get install -y sudo git curl nano gnupg wget
|
||||
|
||||
#Install Node and NPM
|
||||
RUN curl -sL https://deb.nodesource.com/setup_16.x -o nodesource_setup.sh
|
||||
RUN bash nodesource_setup.sh
|
||||
RUN apt-get install -y nodejs build-essential
|
||||
RUN sudo apt-get install -y ca-certificates curl gnupg
|
||||
RUN sudo mkdir -p /etc/apt/keyrings
|
||||
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
|
||||
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
|
||||
RUN sudo apt-get update
|
||||
RUN sudo apt-get install -y nodejs build-essential
|
||||
|
||||
#Create the user 'safeuser' and add them to the sudo group.
|
||||
#RUN useradd -ms /bin/bash safeuser
|
||||
@@ -34,7 +37,7 @@ WORKDIR /home/safeuser
|
||||
|
||||
# Update to the latest version of npm.
|
||||
#RUN npm install -g npm@7.23.0
|
||||
RUN npm install -g npm
|
||||
#RUN npm install -g npm
|
||||
|
||||
# npm mirror to prevent direct dependency on npm.
|
||||
RUN npm set registry http://94.130.170.209:4873/
|
||||
@@ -66,7 +69,7 @@ RUN npm run docs
|
||||
VOLUME /home/safeuser/keys
|
||||
|
||||
# Expose the port the API will be served on.
|
||||
EXPOSE 5010
|
||||
#EXPOSE 5010
|
||||
|
||||
# Start the application.
|
||||
#COPY start-production.sh start-production.sh
|
||||
|
||||
@@ -7,56 +7,28 @@ services:
|
||||
image: mongo:4.2.0
|
||||
container_name: mongo-bch-consumer
|
||||
ports:
|
||||
- '5555:27017' # <host port>:<container port>
|
||||
- '5558:27017' # <host port>:<container port>
|
||||
volumes:
|
||||
- ../data/database:/data/db
|
||||
command: mongod --logpath=/dev/null # -- quiet
|
||||
restart: always
|
||||
|
||||
ipfs-bch-consumer:
|
||||
# https://github.com/christroutner/trickle-ipfs
|
||||
image: christroutner/trickle-ipfs:v1.0.1
|
||||
#build:
|
||||
# context: ./
|
||||
# dockerfile: Dockerfile
|
||||
build: .
|
||||
#image: christroutner/ipfs-service-provider:v5.0.1
|
||||
container_name: ipfs-bch-consumer
|
||||
environment:
|
||||
IPFS_DAEMON_ARGUMENTS: '--enable-pubsub-experiment --migrate=true --agent-version-suffix=docker --routing=dhtclient'
|
||||
UPLOAD_KBPS: '1000'
|
||||
DOWNLOAD_KBPS: '1000'
|
||||
logging:
|
||||
driver: 'json-file'
|
||||
options:
|
||||
max-size: '10m'
|
||||
max-file: '10'
|
||||
mem_limit: 2000mb
|
||||
ports:
|
||||
- 4001:4001
|
||||
- 4003:4003
|
||||
- 172.17.0.1:5001:5001
|
||||
- 172.17.0.1:8080:8080
|
||||
command: [
|
||||
'./start-ipfs.sh'
|
||||
]
|
||||
volumes:
|
||||
- ../data/go-ipfs/data:/root/.ipfs
|
||||
restart: always
|
||||
|
||||
bch-consumer:
|
||||
#build: .
|
||||
image: christroutner/ipfs-bch-wallet-consumer:v1.8.4
|
||||
container_name: bch-consumer
|
||||
logging:
|
||||
driver: 'json-file'
|
||||
options:
|
||||
max-size: '10m'
|
||||
max-file: '10'
|
||||
mem_limit: 500mb
|
||||
#mem_limit: 500mb
|
||||
links:
|
||||
- mongo-bch-consumer
|
||||
- ipfs-bch-consumer
|
||||
ports:
|
||||
- '5010:5010' # <host port>:<container port>
|
||||
- '5015:5015' # <host port>:<container port>
|
||||
- '4101:4101'
|
||||
- '4103:4103'
|
||||
volumes:
|
||||
- ../data/ipfsdata:/home/safeuser/ipfs-bch-wallet-consumer/.ipfsdata
|
||||
- ./start-production.sh:/home/safeuser/ipfs-bch-wallet-consumer/start-production.sh
|
||||
|
||||
@@ -16,23 +16,24 @@ export COORD_NAME=bch-consumer-generic
|
||||
# and you must forward that subdomain to the IPFS_WS_PORT.
|
||||
#export CR_DOMAIN=subdomain.yourdomain.com
|
||||
|
||||
# Debug level. 0 = minimal info. 2 = max info.
|
||||
export DEBUG_LEVEL=1
|
||||
|
||||
# END: Optional configuration settings
|
||||
|
||||
|
||||
# Production database connection string.
|
||||
export DBURL=mongodb://172.17.0.1:5555/ipfs-bch-consumer-prod
|
||||
export DBURL=mongodb://172.17.0.1:5558/ipfs-bch-consumer-prod
|
||||
|
||||
# Configure REST API port
|
||||
export PORT=5010
|
||||
export PORT=5015
|
||||
|
||||
# Production settings using external go-ipfs node.
|
||||
export CONSUMER_ENV=production
|
||||
export SVC_ENV=prod
|
||||
export IPFS_HOST=172.17.0.1
|
||||
export IPFS_API_PORT=5001
|
||||
export IPFS_TCP_PORT=4001
|
||||
export IPFS_WS_PORT=4003
|
||||
export IPFS_TCP_PORT=4101
|
||||
export IPFS_WS_PORT=4103
|
||||
|
||||
# Set the debug level for helia-coord. 0-3.
|
||||
# 0 = no debug logs. 3 = maximum debug logs.
|
||||
export DEBUG_LEVEL=0
|
||||
|
||||
npm start
|
||||
|
||||
@@ -102,7 +102,7 @@ class BchAdapter {
|
||||
return outObj
|
||||
} catch (err) {
|
||||
// console.log('createUser() error: ', err)
|
||||
wlogger.error('Error in use-cases/bch.js/getStatus()')
|
||||
wlogger.error('Error in adapters/bch.js/getStatus()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,7 @@ class BchAdapter {
|
||||
return true
|
||||
} catch (err) {
|
||||
// console.log('createUser() error: ', err)
|
||||
wlogger.error('Error in use-cases/bch.js/getStatus()')
|
||||
wlogger.error('Error in adapters/bch.js/getStatus()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -160,13 +160,17 @@ class BchAdapter {
|
||||
return data
|
||||
} catch (err) {
|
||||
// console.log('createUser() error: ', err)
|
||||
wlogger.error('Error in use-cases/bch.js/getBalances()')
|
||||
wlogger.error('Error in adapters/bch.js/getBalances()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async getUtxos (addr) {
|
||||
try {
|
||||
if (!addr) {
|
||||
throw new Error('addr required when calling getUtxos')
|
||||
}
|
||||
|
||||
// Throw an error if this IPFS node has not yet made a connection to a
|
||||
// wallet service provider.
|
||||
const selectedProvider =
|
||||
@@ -203,7 +207,7 @@ class BchAdapter {
|
||||
return data
|
||||
} catch (err) {
|
||||
// console.log('createUser() error: ', err)
|
||||
wlogger.error('Error in use-cases/bch.js/getUtxos()')
|
||||
wlogger.error('Error in adapters/bch.js/getUtxos()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -257,13 +261,17 @@ class BchAdapter {
|
||||
return data
|
||||
} catch (err) {
|
||||
// console.log('createUser() error: ', err)
|
||||
wlogger.error('Error in use-cases/bch.js/getUtxosBulk()')
|
||||
wlogger.error('Error in adapters/bch.js/getUtxosBulk()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async broadcast (hex) {
|
||||
try {
|
||||
if (!hex) {
|
||||
throw new Error('hex required when calling broadcast')
|
||||
}
|
||||
|
||||
// Throw an error if this IPFS node has not yet made a connection to a
|
||||
// wallet service provider.
|
||||
const selectedProvider =
|
||||
@@ -309,6 +317,10 @@ class BchAdapter {
|
||||
try {
|
||||
console.log('Executing getTransaction() bch adapter')
|
||||
|
||||
if (!address) {
|
||||
throw new Error('address required when calling getTransactions()')
|
||||
}
|
||||
|
||||
// Throw an error if this IPFS node has not yet made a connection to a
|
||||
// wallet service provider.
|
||||
const selectedProvider =
|
||||
@@ -354,6 +366,10 @@ class BchAdapter {
|
||||
// Get details on an array of TXIDs.
|
||||
async getTransaction (txids) {
|
||||
try {
|
||||
if (!txids) {
|
||||
throw new Error('txids required when calling getTransaction()')
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Getting txData on these txids: ${JSON.stringify(txids, null, 2)}`
|
||||
)
|
||||
@@ -401,6 +417,10 @@ class BchAdapter {
|
||||
|
||||
async getPubKey (address) {
|
||||
try {
|
||||
if (!address) {
|
||||
throw new Error('address required when calling getPubKey()')
|
||||
}
|
||||
|
||||
// Throw an error if this IPFS node has not yet made a connection to a
|
||||
// wallet service provider.
|
||||
const selectedProvider =
|
||||
@@ -443,6 +463,10 @@ class BchAdapter {
|
||||
|
||||
async utxoIsValid (utxo) {
|
||||
try {
|
||||
if (!utxo) {
|
||||
throw new Error('utxo required when calling utxoIsValid()')
|
||||
}
|
||||
|
||||
// Throw an error if this IPFS node has not yet made a connection to a
|
||||
// wallet service provider.
|
||||
const selectedProvider =
|
||||
@@ -485,6 +509,10 @@ class BchAdapter {
|
||||
|
||||
async getTokenData (tokenId, withTxHistory = false) {
|
||||
try {
|
||||
if (!tokenId) {
|
||||
throw new Error('tokenId required when calling getTokenData()')
|
||||
}
|
||||
|
||||
// Throw an error if this IPFS node has not yet made a connection to a
|
||||
// wallet service provider.
|
||||
const selectedProvider =
|
||||
@@ -529,6 +557,10 @@ class BchAdapter {
|
||||
// Get token icon and other media associated with the token.
|
||||
async getTokenData2 (tokenId, updateCache) {
|
||||
try {
|
||||
if (!tokenId) {
|
||||
throw new Error('tokenId required when calling getTokenData2()')
|
||||
}
|
||||
|
||||
// Throw an error if this IPFS node has not yet made a connection to a
|
||||
// wallet service provider.
|
||||
const selectedProvider =
|
||||
@@ -573,6 +605,10 @@ class BchAdapter {
|
||||
// Returns a promise that resolves to data when the RPC response is recieved.
|
||||
async waitForRPCResponse (rpcId) {
|
||||
try {
|
||||
if (!rpcId) {
|
||||
throw new Error('rpcId can not be false or undefined')
|
||||
}
|
||||
|
||||
// Initialize variables for tracking the return data.
|
||||
let dataFound = false
|
||||
let cnt = 0
|
||||
@@ -608,21 +644,25 @@ class BchAdapter {
|
||||
}
|
||||
|
||||
// Wait between loops.
|
||||
// await this.sleep(1000)
|
||||
await this.ipfs.ipfsCoordAdapter.bchjs.Util.sleep(2000)
|
||||
await this.sleep(2000)
|
||||
// await this.ipfs.ipfsCoordAdapter.bchjs.Util.sleep(2000)
|
||||
|
||||
cnt++
|
||||
|
||||
// Exit if data was returned, or the window for a response expires.
|
||||
} while (!dataFound && cnt < 10)
|
||||
// console.log(`dataFound: ${dataFound}, cnt: ${cnt}`)
|
||||
console.log(`dataFound: ${dataFound}, cnt: ${cnt}`)
|
||||
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('Error in waitForRPCResponse()')
|
||||
console.error('Error in waitForRPCResponse(): ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
sleep (ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
}
|
||||
|
||||
// module.exports = BchAdapter
|
||||
|
||||
+13
-2
@@ -15,12 +15,13 @@ import Passport from './passport.js'
|
||||
import Nodemailer from './nodemailer.js'
|
||||
import BCH from './bch/index.js'
|
||||
import P2WDB from './p2wdb/index.js'
|
||||
import IpfsFiles from './ipfs-files/index.js'
|
||||
|
||||
// const { wlogger } = require('./wlogger')
|
||||
import JSONFiles from './json-files.js'
|
||||
|
||||
import FullStackJWT from './fullstack-jwt.js'
|
||||
import config from '../../config/index.js'
|
||||
import Wallet from './wallet.adapter.js'
|
||||
|
||||
class Adapters {
|
||||
constructor (localConfig = {}) {
|
||||
@@ -44,6 +45,8 @@ class Adapters {
|
||||
this.config = config
|
||||
this.bch = new BCH(localConfig)
|
||||
this.p2wdb = new P2WDB(localConfig)
|
||||
this.wallet = new Wallet(localConfig)
|
||||
this.ipfsFiles = new IpfsFiles(localConfig)
|
||||
|
||||
// Get a valid JWT API key and instance bch-js.
|
||||
this.fullStackJwt = new FullStackJWT(config)
|
||||
@@ -51,14 +54,22 @@ class Adapters {
|
||||
|
||||
async start () {
|
||||
try {
|
||||
let apiToken
|
||||
if (this.config.getJwtAtStartup) {
|
||||
// Get a JWT token and instantiate bch-js with it. Then pass that instance
|
||||
// to all the rest of the apps controllers and adapters.
|
||||
await this.fullStackJwt.getJWT()
|
||||
apiToken = await this.fullStackJwt.getJWT()
|
||||
// Instantiate bch-js with the JWT token, and overwrite the placeholder for bch-js.
|
||||
this.bchjs = await this.fullStackJwt.instanceBchjs()
|
||||
}
|
||||
|
||||
// Create a default instance of minimal-slp-wallet without initializing it
|
||||
// (without retrieving the wallets UTXOs). This instance will be overwritten
|
||||
// if the operator has configured BCH payments.
|
||||
console.log('\nCreating default startup wallet. This wallet may be overwritten.')
|
||||
await this.wallet.instanceWalletWithoutInitialization({}, { apiToken })
|
||||
this.bchjs = this.wallet.bchWallet.bchjs
|
||||
|
||||
// Start the IPFS node.
|
||||
// Do not start these adapters if this is an e2e test.
|
||||
if (this.config.env !== 'test') {
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
This library contains code for interfacing to the ipfs-file-pin-service using
|
||||
JSON RPC over IPFS. Most of these functions are called by the /files REST API
|
||||
endpoints and the IPFS Use Cases library.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
import { v4 as uid } from 'uuid'
|
||||
import jsonrpc from 'jsonrpc-lite'
|
||||
|
||||
// Local libraries
|
||||
import wlogger from '../wlogger.js'
|
||||
|
||||
// let _this
|
||||
|
||||
class IpfsFilesAdapter {
|
||||
constructor (localConfig = {}) {
|
||||
// console.log('BCH localConfig: ', localConfig)
|
||||
this.ipfs = localConfig.ipfs
|
||||
if (!this.ipfs) {
|
||||
throw new Error(
|
||||
'An instance of IPFS must be passed when instantiating the IPFS Files Adapter library.'
|
||||
)
|
||||
}
|
||||
// this.eventEmitter = localConfig.eventEmitter
|
||||
// if (!this.eventEmitter) {
|
||||
// throw new Error(
|
||||
// 'An instance of an EventEmitter must be passed when instantiating the adapters.'
|
||||
// )
|
||||
// }
|
||||
|
||||
// Connect the RPC handler when the event fires with new data.
|
||||
// this.eventEmitter.on('rpcData', this.rpcHandler)
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.uid = uid
|
||||
this.jsonrpc = jsonrpc
|
||||
|
||||
// A queue for holding RPC data that has arrived.
|
||||
this.rpcDataQueue = []
|
||||
|
||||
// _this = this // Global handle on instance of this Class.
|
||||
|
||||
// Bind 'this' object to all subfunctions for this Class
|
||||
this.rpcHandler = this.rpcHandler.bind(this)
|
||||
this.getStatus = this.getStatus.bind(this)
|
||||
this.selectProvider = this.selectProvider.bind(this)
|
||||
}
|
||||
|
||||
// This handler is triggered when RPC data comes in over IPFS.
|
||||
// Handle RPC input, and add the response to the RPC queue.
|
||||
// Once in the queue, it will get processed by waitForRPCResponse()
|
||||
rpcHandler (data) {
|
||||
try {
|
||||
// Convert string input into an object.
|
||||
// const jsonData = JSON.parse(data)
|
||||
|
||||
// console.log(`JSON RPC response for ID ${data.payload.id} received.`)
|
||||
|
||||
this.rpcDataQueue.push(data)
|
||||
} catch (err) {
|
||||
console.error('Error in files/rpcHandler(): ', err)
|
||||
// Do not throw error. This is a top-level function.
|
||||
}
|
||||
}
|
||||
|
||||
// Get the status of BCH wallet services this node can talk to. Returns an
|
||||
// array of BCH wallet service providers.
|
||||
async getStatus () {
|
||||
try {
|
||||
const peerData = this.ipfs.ipfsCoordAdapter.ipfsCoord.thisNode.peerData
|
||||
// console.log(`peerData: ${JSON.stringify(peerData, null, 2)}`)
|
||||
|
||||
// const status = {
|
||||
// state: this.ipfs.ipfsCoordAdapter.state
|
||||
// }
|
||||
|
||||
// Add names to the IPFS IDs for each provider.
|
||||
const initialServiceProviders =
|
||||
this.ipfs.ipfsCoordAdapter.state.ipfsFileProviders
|
||||
const serviceProviders = []
|
||||
for (let i = 0; i < initialServiceProviders.length; i++) {
|
||||
const thisProvider = initialServiceProviders[i]
|
||||
|
||||
const providerData = peerData.filter((x) => x.from === thisProvider)
|
||||
|
||||
if (providerData.length) {
|
||||
const provObj = {
|
||||
ipfsId: thisProvider,
|
||||
name: providerData[0].data.jsonLd.name
|
||||
}
|
||||
|
||||
serviceProviders.push(provObj)
|
||||
}
|
||||
}
|
||||
|
||||
// console.log(
|
||||
// `serviceProviders: ${JSON.stringify(serviceProviders, null, 2)}`
|
||||
// )
|
||||
|
||||
const outObj = {
|
||||
serviceProviders,
|
||||
selectedProvider: this.ipfs.ipfsCoordAdapter.state.selectedIpfsFileProvider
|
||||
}
|
||||
// console.log('outObj: ', outObj)
|
||||
|
||||
return outObj
|
||||
} catch (err) {
|
||||
// console.log('createUser() error: ', err)
|
||||
wlogger.error('Error in adapters/files/getStatus()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Choose the ipfs-file-pin-service to use.
|
||||
async selectProvider (providerId) {
|
||||
try {
|
||||
this.ipfs.ipfsCoordAdapter.config.selectedIpfsFileProvider = providerId
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
// console.log('createUser() error: ', err)
|
||||
wlogger.error('Error in adapters/files/getStatus()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Given a CID, query the ipfs-file-pin-service to get the file metadata, like
|
||||
// the file name.
|
||||
async getFileMetadata (inObj = {}) {
|
||||
try {
|
||||
const { cid } = inObj
|
||||
|
||||
// Input validation.
|
||||
if (!cid || typeof cid !== 'string') {
|
||||
throw new Error('getFileMetadata() cid input hash must be a string.')
|
||||
}
|
||||
|
||||
// Throw an error if this IPFS node has not yet made a connection to a
|
||||
// wallet service provider.
|
||||
const selectedProvider =
|
||||
this.ipfs.ipfsCoordAdapter.state.selectedIpfsFileProvider
|
||||
if (!selectedProvider) {
|
||||
throw new Error('No IPFS File Pin Service provider available yet.')
|
||||
}
|
||||
|
||||
const rpcData = {
|
||||
endpoint: 'getFileMetadata',
|
||||
cid
|
||||
}
|
||||
|
||||
// Generate a UUID for the call.
|
||||
const rpcId = this.uid()
|
||||
|
||||
// Generate a JSON RPC command.
|
||||
const cmd = this.jsonrpc.request(rpcId, 'file-pin', rpcData)
|
||||
const cmdStr = JSON.stringify(cmd)
|
||||
// console.log('cmdStr: ', cmdStr)
|
||||
|
||||
// Send the RPC command to selected wallet service.
|
||||
const thisNode = this.ipfs.ipfsCoordAdapter.ipfsCoord.thisNode
|
||||
await this.ipfs.ipfsCoordAdapter.ipfsCoord.useCases.peer.sendPrivateMessage(
|
||||
selectedProvider,
|
||||
cmdStr,
|
||||
thisNode
|
||||
)
|
||||
|
||||
// Wait for data to come back from the wallet service.
|
||||
const data = await this.waitForRPCResponse(rpcId)
|
||||
// console.log('getFileMetadata() data: ', data)
|
||||
|
||||
return data
|
||||
} catch (err) {
|
||||
wlogger.error('Error in adapters/files/getFileMetadata()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a promise that resolves to data when the RPC response is recieved.
|
||||
async waitForRPCResponse (rpcId) {
|
||||
try {
|
||||
// Initialize variables for tracking the return data.
|
||||
let dataFound = false
|
||||
let cnt = 0
|
||||
|
||||
// Default return value, if the remote computer does not respond in time.
|
||||
let data = {
|
||||
success: false,
|
||||
message: 'request timed out',
|
||||
data: ''
|
||||
}
|
||||
|
||||
// Loop that waits for a response from the service provider.
|
||||
do {
|
||||
// console.log(`this.rpcDataQueue.length: ${this.rpcDataQueue.length}`)
|
||||
for (let i = 0; i < this.rpcDataQueue.length; i++) {
|
||||
const rawData = this.rpcDataQueue[i]
|
||||
// console.log(`rawData: ${JSON.stringify(rawData, null, 2)}`)
|
||||
|
||||
if (rawData.payload.id === rpcId) {
|
||||
dataFound = true
|
||||
// console.log('data was found in the queue')
|
||||
|
||||
data = rawData.payload.result.value
|
||||
|
||||
// Remove the data from the queue
|
||||
this.rpcDataQueue.splice(i, 1)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Wait between loops.
|
||||
// await this.sleep(1000)
|
||||
await this.ipfs.ipfsCoordAdapter.wallet.bchjs.Util.sleep(2000)
|
||||
|
||||
cnt++
|
||||
|
||||
// Exit if data was returned, or the window for a response expires.
|
||||
} while (!dataFound && cnt < 10)
|
||||
// console.log(`dataFound: ${dataFound}, cnt: ${cnt}`)
|
||||
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('Error in waitForRPCResponse()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// module.exports = P2wdbAdapter
|
||||
export default IpfsFilesAdapter
|
||||
+34
-24
@@ -68,13 +68,10 @@ class IPFS {
|
||||
}
|
||||
|
||||
// Get the status of this IPFS node.
|
||||
// Create an object with different metrics about this IPFS node, then return
|
||||
// the object.
|
||||
getStatus () {
|
||||
try {
|
||||
// console.log(
|
||||
// 'this.ipfsCoordAdapter.ipfsCoord.thisNode: ',
|
||||
// this.ipfsCoordAdapter.ipfsCoord.thisNode
|
||||
// )
|
||||
|
||||
const statusObj = {
|
||||
ipfsId: this.ipfsCoordAdapter.ipfsCoord.thisNode.ipfsId,
|
||||
multiAddrs: this.ipfsCoordAdapter.ipfsCoord.thisNode.ipfsMultiaddrs,
|
||||
@@ -105,25 +102,30 @@ class IPFS {
|
||||
ipfsPeers = this._removeDuplicatePeers(ipfsPeers)
|
||||
// console.log('filtered ipfsPeers: ', ipfsPeers)
|
||||
|
||||
const peerDataOut = []
|
||||
|
||||
// Loop through each IPFS peer and hydrate it with data from the peerData.
|
||||
for (let i = 0; i < ipfsPeers.length; i++) {
|
||||
const thisPeer = ipfsPeers[i]
|
||||
|
||||
if (!showAll) {
|
||||
// Delete properties that don't contain good info.
|
||||
delete thisPeer.muxer
|
||||
delete thisPeer.latency
|
||||
delete thisPeer.streams
|
||||
const thisPeer = {
|
||||
peer: ipfsPeers[i]
|
||||
}
|
||||
|
||||
// if (!showAll) {
|
||||
// // Delete properties that don't contain good info.
|
||||
// delete thisPeer.muxer
|
||||
// delete thisPeer.latency
|
||||
// delete thisPeer.streams
|
||||
// }
|
||||
|
||||
// Get the ipfs-coord peer data for this peer.
|
||||
let thisPeerData = peerData.filter((x) =>
|
||||
x.from.includes(thisPeer.peer)
|
||||
)
|
||||
thisPeerData = thisPeerData[0]
|
||||
// console.log('thisPeerData: ', thisPeerData)
|
||||
|
||||
// Skip if peerData for this IPFS peer could not be found.
|
||||
if (!thisPeerData) continue
|
||||
// if (!thisPeerData) continue
|
||||
|
||||
try {
|
||||
// console.log('thisPeerData: ', thisPeerData)
|
||||
@@ -132,25 +134,36 @@ class IPFS {
|
||||
thisPeer.name = thisPeerData.data.jsonLd.name
|
||||
thisPeer.protocol = thisPeerData.data.jsonLd.protocol
|
||||
thisPeer.version = thisPeerData.data.jsonLd.version
|
||||
thisPeer.connectionAddr = thisPeerData.data.connectionAddr
|
||||
|
||||
if (showAll) {
|
||||
// Add all the peer data.
|
||||
thisPeer.peerData = thisPeerData
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(
|
||||
`Error trying to hydrate peer ${thisPeer.peer}: ${err.message}`
|
||||
console.warn(
|
||||
`warning: ${thisPeer.peer}: ${err.message}`
|
||||
)
|
||||
}
|
||||
|
||||
peerDataOut.push(thisPeer)
|
||||
}
|
||||
|
||||
return ipfsPeers
|
||||
return peerDataOut
|
||||
} catch (err) {
|
||||
console.error('Error in getPeers(): ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Expects an array of peers and returns an array of peers with duplicates
|
||||
// removed.
|
||||
// This function is used by getPeers()
|
||||
_removeDuplicatePeers (arr) {
|
||||
// https://stackoverflow.com/questions/2218999/how-to-remove-all-duplicates-from-an-array-of-objects
|
||||
return arr.filter((v, i, a) => a.findIndex((t) => t === v) === i)
|
||||
}
|
||||
|
||||
// Get data about the known Circuit Relays. Hydrate with data from peers list.
|
||||
getRelays () {
|
||||
try {
|
||||
@@ -171,25 +184,22 @@ class IPFS {
|
||||
// If the peer couldn't be found, skip.
|
||||
if (!thisPeer.length) {
|
||||
thisRelay.name = ''
|
||||
thisRelay.description = ''
|
||||
continue
|
||||
}
|
||||
|
||||
thisRelay.name = thisPeer[0].data.jsonLd.name
|
||||
thisRelay.description = thisPeer[0].data.jsonLd.description
|
||||
}
|
||||
|
||||
return relayData
|
||||
const v1Relays = this.config.v1Relays
|
||||
|
||||
return { v2Relays: relayData, v1Relays }
|
||||
} catch (err) {
|
||||
console.error('Error in getRelays(): ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Expects an array of peers and returns an array of peers with duplicates
|
||||
// removed.
|
||||
_removeDuplicatePeers (arr) {
|
||||
// https://stackoverflow.com/questions/2218999/how-to-remove-all-duplicates-from-an-array-of-objects
|
||||
return arr.filter((v, i, a) => a.findIndex((t) => t.peer === v.peer) === i)
|
||||
}
|
||||
}
|
||||
|
||||
export default IPFS
|
||||
|
||||
+123
-13
@@ -5,12 +5,13 @@
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import IpfsCoord from 'ipfs-coord-esm'
|
||||
// import IpfsCoord from 'ipfs-coord-esm'
|
||||
import IpfsCoord from 'helia-coord'
|
||||
|
||||
// import BCHJS from '@psf/bch-js'
|
||||
import SlpWallet from 'minimal-slp-wallet'
|
||||
import publicIp from 'public-ip'
|
||||
import semver from 'semver'
|
||||
import { publicIpv4 } from 'public-ip'
|
||||
|
||||
// Local libraries
|
||||
import config from '../../../config/index.js'
|
||||
@@ -22,6 +23,8 @@ const MIN_BCH_WALLET_VERSION = '1.11.11'
|
||||
const WALLET_PROTOCOL = 'bch-wallet'
|
||||
const MIN_P2WDB_VERSION = '1.4.0'
|
||||
const P2WDB_PROTOCOL = 'p2wdb'
|
||||
const MIN_FILE_PIN_VERSION = '1.0.0'
|
||||
const FILE_PIN_PROTOCOL = 'ipfs-file-pin-service'
|
||||
|
||||
let _this
|
||||
|
||||
@@ -47,17 +50,21 @@ class IpfsCoordAdapter {
|
||||
// this.bchjs = new BCHJS()
|
||||
this.wallet = new SlpWallet()
|
||||
this.config = config
|
||||
this.publicIp = publicIp
|
||||
this.semver = semver
|
||||
this.publicIp = publicIpv4
|
||||
|
||||
// Properties of this class instance.
|
||||
this.isReady = false
|
||||
|
||||
// Periodically poll services for available wallet service providers.
|
||||
this.pollBchServiceInterval = setInterval(this.pollForBchServices, 10000)
|
||||
this.pollIpfsFileServiceInterval = setInterval(
|
||||
this.pollForIpfsFileServices,
|
||||
11000
|
||||
)
|
||||
this.pollP2wdbServiceInterval = setInterval(
|
||||
this.pollForP2wdbServices,
|
||||
11000
|
||||
12000
|
||||
)
|
||||
|
||||
// State object. TODO: Make this more robust.
|
||||
@@ -65,7 +72,9 @@ class IpfsCoordAdapter {
|
||||
serviceProviders: [],
|
||||
selectedServiceProvider: '',
|
||||
p2wdbProviders: [],
|
||||
selectedP2wdbProvider: ''
|
||||
selectedP2wdbProvider: '',
|
||||
ipfsFileProviders: [],
|
||||
selectedIpfsFileProvider: ''
|
||||
}
|
||||
|
||||
_this = this
|
||||
@@ -80,7 +89,7 @@ class IpfsCoordAdapter {
|
||||
// If configured as a Circuit Relay, get the public IP addresses for this node.
|
||||
if (this.config.isCircuitRelay) {
|
||||
try {
|
||||
const ip4 = await this.publicIp.v4()
|
||||
const ip4 = await this.publicIp()
|
||||
// const ip6 = await publicIp.v6()
|
||||
|
||||
circuitRelayInfo.ip4 = ip4
|
||||
@@ -94,24 +103,28 @@ class IpfsCoordAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
const nullLog = () => {}
|
||||
|
||||
const ipfsCoordOptions = {
|
||||
ipfs: this.ipfs,
|
||||
type: 'node.js',
|
||||
// type: 'browser',
|
||||
wallet: this.wallet,
|
||||
privateLog: console.log, // Default to console.log
|
||||
bchjs: this.bchjs,
|
||||
// privateLog: console.log, // Default to console.log
|
||||
privateLog: nullLog,
|
||||
isCircuitRelay: this.config.isCircuitRelay,
|
||||
circuitRelayInfo,
|
||||
apiInfo: this.config.apiInfo,
|
||||
announceJsonLd: this.config.announceJsonLd,
|
||||
debugLevel: this.config.debugLevel
|
||||
debugLevel: this.config.debugLevel,
|
||||
v1Relays: this.config.v1Relays,
|
||||
tcpPort: this.config.ipfsTcpPort
|
||||
}
|
||||
|
||||
// Production env uses external go-ipfs node.
|
||||
if (this.config.isProduction) {
|
||||
ipfsCoordOptions.nodeType = 'external'
|
||||
}
|
||||
// if (this.config.isProduction) {
|
||||
// ipfsCoordOptions.nodeType = 'external'
|
||||
// }
|
||||
|
||||
this.ipfsCoord = new this.IpfsCoord(ipfsCoordOptions)
|
||||
|
||||
@@ -273,6 +286,10 @@ class IpfsCoordAdapter {
|
||||
_this.config.preferredProvider &&
|
||||
thisPeer === _this.config.preferredProvider
|
||||
) {
|
||||
if (_this.state.selectedServiceProvider !== thisPeer) {
|
||||
console.log(`---->BCH wallet service switched to preferred peer: ${thisPeer}`)
|
||||
}
|
||||
|
||||
_this.state.selectedServiceProvider = thisPeer
|
||||
}
|
||||
|
||||
@@ -303,6 +320,96 @@ class IpfsCoordAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
// Poll the ipfs-coord coordination channel for available ipfs-file-pin-service
|
||||
// providers. This method is called periodically by a timer-interval.
|
||||
pollForIpfsFileServices () {
|
||||
try {
|
||||
// console.log('pollForIpfsFileServices() polling for BCH service')
|
||||
|
||||
// An array of IPFS IDs of other nodes in the coordination pubsub channel.
|
||||
const peers = _this.ipfsCoord.thisNode.peerList
|
||||
// console.log(`peers: ${JSON.stringify(peers, null, 2)}`)
|
||||
|
||||
// Array of objects. Each object is the IPFS ID of the peer and contains
|
||||
// data about that peer.
|
||||
const peerData = _this.ipfsCoord.thisNode.peerData
|
||||
// console.log(`peerData: ${JSON.stringify(peerData, null, 2)}`)
|
||||
|
||||
for (let i = 0; i < peers.length; i++) {
|
||||
const thisPeer = peers[i]
|
||||
const thisData = peerData.filter((x) => x.from === thisPeer)
|
||||
const thisPeerData = thisData[0]
|
||||
|
||||
// Create a 'fingerprint' that defines the wallet service.
|
||||
const protocol = thisPeerData.data.jsonLd.protocol
|
||||
const version = thisPeerData.data.jsonLd.version
|
||||
// console.log(
|
||||
// `debug: peer ${thisPeer} uses protocol: ${protocol} v${version}`,
|
||||
// )
|
||||
|
||||
let versionMatches = false
|
||||
if (version) {
|
||||
// versionMatches = _this.semver.gt(version, MIN_FILE_PIN_VERSION)
|
||||
versionMatches = _this.semver.satisfies(version, `>=${MIN_FILE_PIN_VERSION}`)
|
||||
}
|
||||
|
||||
// Ignore any peers that don't match the fingerprint for a BCH wallet
|
||||
// service.
|
||||
if (protocol && protocol.includes(FILE_PIN_PROTOCOL) && versionMatches) {
|
||||
// console.log('Matching peer: ', thisPeerData)
|
||||
|
||||
// Temporary business logic.
|
||||
// Use the first available wallet service detected.
|
||||
if (_this.state.ipfsFileProviders.length === 0) {
|
||||
_this.state.selectedIpfsFileProvider = thisPeer
|
||||
|
||||
// Persist the config setting, so it can be used by other commands.
|
||||
// _this.conf.set('selectedService', thisPeer)
|
||||
console.log(`---->IPFS File service selected: ${thisPeer}`)
|
||||
}
|
||||
|
||||
// console.log('preferredProvider: ', _this.config.preferredProvider)
|
||||
|
||||
// If a preferred provider is set in the config file, then connect
|
||||
// to the preferred provider when it's discovered.
|
||||
if (
|
||||
_this.config.preferredIpfsFileProvider &&
|
||||
thisPeer === _this.config.preferredIpfsFileProvider
|
||||
) {
|
||||
if (_this.state.selectedIpfsFileProvider !== thisPeer) {
|
||||
console.log(`---->IPFS File service switched to preferred peer: ${thisPeer}`)
|
||||
}
|
||||
|
||||
_this.state.selectedIpfsFileProvider = thisPeer
|
||||
}
|
||||
|
||||
// console.log('selectedServiceProvider: ', _this.state.selectedServiceProvider)
|
||||
|
||||
// Check if the peer has already been added to the list of providers.
|
||||
const alreadyExists = _this.state.ipfsFileProviders.filter(
|
||||
(x) => x === thisPeer
|
||||
)
|
||||
|
||||
// Add the peer to the list of serviceProviders if it doesn't already
|
||||
// exist in the list.
|
||||
if (!alreadyExists.length) {
|
||||
_this.state.ipfsFileProviders.push(thisPeer)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// catch and handle known failure mode.
|
||||
if (
|
||||
err.message.includes("Cannot read property 'peerList' of undefined")
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
console.error('Error in pollForIpfsFileServices(): ', err)
|
||||
// Do not throw error. This is a top-level function.
|
||||
}
|
||||
}
|
||||
|
||||
// This method handles input coming in from other IPFS peers.
|
||||
// It passes the data on to the REST API library by emitting an event.
|
||||
// peerInputHandler (data) {
|
||||
@@ -318,9 +425,12 @@ class IpfsCoordAdapter {
|
||||
|
||||
// Subscribe to the chat pubsub channel
|
||||
async subscribeToChat () {
|
||||
// TODO: Allow user to replace nullog with their own log handler at startup.
|
||||
const nullLog = () => {}
|
||||
|
||||
await this.ipfsCoord.adapters.pubsub.subscribeToPubsubChannel(
|
||||
this.config.chatPubSubChan,
|
||||
console.log,
|
||||
nullLog,
|
||||
this.ipfsCoord.thisNode
|
||||
)
|
||||
}
|
||||
|
||||
+232
-80
@@ -9,95 +9,95 @@
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
// const IPFS = require('ipfs')
|
||||
// const IPFS = require('@chris.troutner/ipfs')
|
||||
// import IPFSembedded from 'ipfs';
|
||||
|
||||
import { create } from 'ipfs-http-client'
|
||||
import { createHelia } from 'helia'
|
||||
import fs from 'fs'
|
||||
import http from 'http'
|
||||
import { FsBlockstore } from 'blockstore-fs'
|
||||
import { FsDatastore } from 'datastore-fs'
|
||||
import { createLibp2p } from 'libp2p'
|
||||
import { tcp } from '@libp2p/tcp'
|
||||
import { noise } from '@chainsafe/libp2p-noise'
|
||||
import { yamux } from '@chainsafe/libp2p-yamux'
|
||||
// import { bootstrap } from '@libp2p/bootstrap'
|
||||
// import { identifyService } from 'libp2p/identify'
|
||||
import { identify } from '@libp2p/identify'
|
||||
// import { circuitRelayServer, circuitRelayTransport } from 'libp2p/circuit-relay'
|
||||
import { circuitRelayServer, circuitRelayTransport } from '@libp2p/circuit-relay-v2'
|
||||
import { gossipsub } from '@chainsafe/libp2p-gossipsub'
|
||||
import { webSockets } from '@libp2p/websockets'
|
||||
import { publicIpv4 } from 'public-ip'
|
||||
import { multiaddr } from '@multiformats/multiaddr'
|
||||
import { webRTC } from '@libp2p/webrtc'
|
||||
import { keychain } from '@libp2p/keychain'
|
||||
import { defaultLogger } from '@libp2p/logger'
|
||||
import { unixfs } from '@helia/unixfs'
|
||||
|
||||
// Local libraries
|
||||
import config from '../../../config/index.js'
|
||||
import JsonFiles from '../json-files.js'
|
||||
|
||||
const IPFS_DIR = './.ipfsdata/ipfs'
|
||||
// Hack to get __dirname back.
|
||||
// https://blog.logrocket.com/alternatives-dirname-node-js-es-modules/
|
||||
import * as url from 'url'
|
||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
|
||||
// console.log('__dirname: ', __dirname)
|
||||
|
||||
const ROOT_DIR = `${__dirname}../../../`
|
||||
const IPFS_DIR = `${__dirname}../../../.ipfsdata/ipfs`
|
||||
|
||||
class IpfsAdapter {
|
||||
constructor (localConfig) {
|
||||
// Encapsulate dependencies
|
||||
this.config = config
|
||||
this.fs = fs
|
||||
this.create = create
|
||||
|
||||
// Choose the IPFS constructor based on the config settings.
|
||||
// this.IPFS = IPFSembedded // default
|
||||
// if (this.config.isProduction) {
|
||||
// this.IPFS = IPFSexternal
|
||||
// }
|
||||
this.createLibp2p = createLibp2p
|
||||
this.publicIp = publicIpv4
|
||||
this.multiaddr = multiaddr
|
||||
this.jsonFiles = new JsonFiles()
|
||||
this.keychain = keychain
|
||||
this.createHelia = createHelia
|
||||
|
||||
// Properties of this class instance.
|
||||
this.isReady = false
|
||||
|
||||
// Bind 'this' object to all subfunctions
|
||||
this.start = this.start.bind(this)
|
||||
this.createNode = this.createNode.bind(this)
|
||||
this.stop = this.stop.bind(this)
|
||||
this.ensureBlocksDir = this.ensureBlocksDir.bind(this)
|
||||
this.getSeed = this.getSeed.bind(this)
|
||||
this.getKeychain = this.getKeychain.bind(this)
|
||||
}
|
||||
|
||||
// Start an IPFS node.
|
||||
async start () {
|
||||
try {
|
||||
// Ipfs Options
|
||||
const ipfsOptionsEmbedded = {
|
||||
repo: IPFS_DIR,
|
||||
start: true,
|
||||
config: {
|
||||
relay: {
|
||||
enabled: true, // enable circuit relay dialer and listener
|
||||
hop: {
|
||||
enabled: config.isCircuitRelay // enable circuit relay HOP (make this node a relay)
|
||||
}
|
||||
},
|
||||
pubsub: true, // enable pubsub
|
||||
Swarm: {
|
||||
ConnMgr: {
|
||||
HighWater: 30,
|
||||
LowWater: 10
|
||||
}
|
||||
},
|
||||
Addresses: {
|
||||
Swarm: [
|
||||
`/ip4/0.0.0.0/tcp/${this.config.ipfsTcpPort}`,
|
||||
`/ip4/0.0.0.0/tcp/${this.config.ipfsWsPort}/ws`
|
||||
]
|
||||
},
|
||||
Datastore: {
|
||||
StorageMax: '2GB',
|
||||
StorageGCWatermark: 50,
|
||||
GCPeriod: '15m'
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ensure the directory structure exists that is needed by the IPFS node to store data.
|
||||
this.ensureBlocksDir()
|
||||
|
||||
const ipfsOptionsExternal = {
|
||||
host: this.config.ipfsHost,
|
||||
port: this.config.ipfsApiPort,
|
||||
agent: http.Agent({ keepAlive: true, maxSockets: 200 })
|
||||
}
|
||||
// Create an IPFS node
|
||||
const ipfs = await this.createNode()
|
||||
// console.log('ipfs: ', ipfs)
|
||||
|
||||
let ipfsOptions = ipfsOptionsEmbedded
|
||||
if (this.config.isProduction) {
|
||||
ipfsOptions = ipfsOptionsExternal
|
||||
}
|
||||
this.id = ipfs.libp2p.peerId.toString()
|
||||
console.log('IPFS ID: ', this.id)
|
||||
|
||||
// Create a new IPFS node.
|
||||
this.ipfs = await this.create(ipfsOptions)
|
||||
// Attempt to guess our ip4 IP address.
|
||||
const ip4 = await this.publicIp()
|
||||
let detectedMultiaddr = `/ip4/${ip4}/tcp/${this.config.ipfsTcpPort}/p2p/${this.id}`
|
||||
detectedMultiaddr = this.multiaddr(detectedMultiaddr)
|
||||
|
||||
// Set the 'server' profile so the node does not scan private networks.
|
||||
await this.ipfs.config.profiles.apply('server')
|
||||
// Get the multiaddrs for the node.
|
||||
const multiaddrs = ipfs.libp2p.getMultiaddrs()
|
||||
multiaddrs.push(detectedMultiaddr)
|
||||
console.log('Multiaddrs: ', multiaddrs)
|
||||
|
||||
// Debugging: Display IPFS config settings.
|
||||
// const configSettings = await this.ipfs.config.getAll()
|
||||
// console.log(`configSettings: ${JSON.stringify(configSettings, null, 2)}`)
|
||||
this.multiaddrs = multiaddrs
|
||||
|
||||
// Signal that this adapter is ready.
|
||||
this.isReady = true
|
||||
|
||||
this.ipfs = ipfs
|
||||
|
||||
return this.ipfs
|
||||
} catch (err) {
|
||||
console.error('Error in ipfs.js/start()')
|
||||
@@ -111,31 +111,183 @@ class IpfsAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
async getKeychain (datastore) {
|
||||
const keychainInit = {
|
||||
pass: await this.getSeed()
|
||||
}
|
||||
|
||||
const chain = this.keychain(keychainInit)({
|
||||
datastore,
|
||||
logger: defaultLogger()
|
||||
})
|
||||
|
||||
return chain
|
||||
}
|
||||
|
||||
// This function creates an IPFS node using Helia.
|
||||
// It returns the node as an object.
|
||||
async createNode () {
|
||||
try {
|
||||
// Create block and data stores.
|
||||
const blockstore = new FsBlockstore(`${IPFS_DIR}/blockstore`)
|
||||
const datastore = new FsDatastore(`${IPFS_DIR}/datastore`)
|
||||
|
||||
// const keychainInit = {
|
||||
// pass: await this.getSeed()
|
||||
// }
|
||||
|
||||
// Create an identity
|
||||
let peerId
|
||||
// console.log('this.keychain: ', this.keychain)
|
||||
// const chain = this.keychain(keychainInit)({
|
||||
// datastore,
|
||||
// logger: defaultLogger()
|
||||
// })
|
||||
const chain = await this.getKeychain(datastore)
|
||||
try {
|
||||
peerId = await chain.exportPeerId('myKey')
|
||||
} catch (err) {
|
||||
await chain.createKey('myKey', 'Ed25519', 4096)
|
||||
peerId = await chain.exportPeerId('myKey')
|
||||
}
|
||||
|
||||
// Configure services
|
||||
const services = {
|
||||
identify: identify(),
|
||||
pubsub: gossipsub({ allowPublishToZeroPeers: true })
|
||||
}
|
||||
if (this.config.isCircuitRelay) {
|
||||
console.log('Helia (IPFS) node IS configured as Circuit Relay')
|
||||
services.relay = circuitRelayServer({ // makes the node function as a relay server
|
||||
hopTimeout: 30 * 1000, // incoming relay requests must be resolved within this time limit
|
||||
advertise: true,
|
||||
reservations: {
|
||||
maxReservations: 15, // how many peers are allowed to reserve relay slots on this server
|
||||
reservationClearInterval: 300 * 1000, // how often to reclaim stale reservations
|
||||
applyDefaultLimit: true, // whether to apply default data/duration limits to each relayed connection
|
||||
defaultDurationLimit: 2 * 60 * 1000, // the default maximum amount of time a relayed connection can be open for
|
||||
defaultDataLimit: BigInt(2 << 7), // the default maximum number of bytes that can be transferred over a relayed connection
|
||||
maxInboundHopStreams: 32, // how many inbound HOP streams are allow simultaneously
|
||||
maxOutboundHopStreams: 64 // how many outbound HOP streams are allow simultaneously
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.log('Helia (IPFS) node IS NOT configured as Circuit Relay')
|
||||
}
|
||||
|
||||
// Configure transports
|
||||
let transports
|
||||
if (process.env.CONNECT_PREF === 'direct') {
|
||||
transports = [
|
||||
tcp(),
|
||||
webSockets(),
|
||||
webRTC()
|
||||
]
|
||||
} else {
|
||||
transports = [
|
||||
tcp(),
|
||||
webSockets(),
|
||||
circuitRelayTransport({
|
||||
discoverRelays: 3,
|
||||
reservationConcurrency: 3
|
||||
}),
|
||||
webRTC()
|
||||
]
|
||||
}
|
||||
|
||||
// libp2p is the networking layer that underpins Helia
|
||||
const libp2p = await this.createLibp2p({
|
||||
peerId,
|
||||
datastore,
|
||||
addresses: {
|
||||
listen: [
|
||||
'/ip4/127.0.0.1/tcp/0',
|
||||
`/ip4/0.0.0.0/tcp/${this.config.ipfsTcpPort}`,
|
||||
`/ip4/0.0.0.0/tcp/${this.config.ipfsWsPort}/ws`,
|
||||
'/webrtc'
|
||||
]
|
||||
},
|
||||
transports,
|
||||
connectionEncryption: [
|
||||
noise()
|
||||
],
|
||||
streamMuxers: [
|
||||
yamux()
|
||||
],
|
||||
services
|
||||
})
|
||||
|
||||
// create a Helia node
|
||||
const helia = await this.createHelia({
|
||||
blockstore,
|
||||
datastore,
|
||||
libp2p
|
||||
})
|
||||
|
||||
// Attach IPFS file system.
|
||||
const fs = unixfs(helia)
|
||||
helia.fs = fs
|
||||
|
||||
return helia
|
||||
} catch (err) {
|
||||
console.error('Error creating Helia node: ', err)
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async stop () {
|
||||
await this.ipfs.stop()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Remove the '/blocks' directory that is used to store IPFS data.
|
||||
// Dev Note: It's assumed this node is not pinning any data and that
|
||||
// everything in this directory is transient. This folder will regularly
|
||||
// fill up and prevent IPFS from starting.
|
||||
// rmBlocksDir () {
|
||||
// try {
|
||||
// const dir = `${IPFS_DIR}/blocks`
|
||||
// console.log(`Deleting ${dir} directory...`)
|
||||
//
|
||||
// this.fs.rmdirSync(dir, { recursive: true })
|
||||
//
|
||||
// console.log(`${dir} directory is deleted!`)
|
||||
//
|
||||
// return true // Signal successful execution.
|
||||
// } catch (err) {
|
||||
// console.log('Error in rmBlocksDir()')
|
||||
// throw err
|
||||
// }
|
||||
// }
|
||||
// Ensure that the directories exist to store blocks from the IPFS network.
|
||||
// This function is called at startup, before the IPFS node is started.
|
||||
ensureBlocksDir () {
|
||||
try {
|
||||
!this.fs.existsSync(`${ROOT_DIR}.ipfsdata`) && this.fs.mkdirSync(`${ROOT_DIR}.ipfsdata`)
|
||||
|
||||
!this.fs.existsSync(`${IPFS_DIR}`) && this.fs.mkdirSync(`${IPFS_DIR}`)
|
||||
|
||||
!this.fs.existsSync(`${IPFS_DIR}/blockstore`) && this.fs.mkdirSync(`${IPFS_DIR}/blockstore`)
|
||||
|
||||
!this.fs.existsSync(`${IPFS_DIR}/datastore`) && this.fs.mkdirSync(`${IPFS_DIR}/datastore`)
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('Error in adapters/ipfs.js/ensureBlocksDir(): ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// This function opens the seed used to generate the key for this IPFS peer.
|
||||
// The seed is stored in a JSON file. If it doesn't exist, a new one is created.
|
||||
async getSeed () {
|
||||
try {
|
||||
let seed
|
||||
|
||||
const filename = `${IPFS_DIR}/seed.json`
|
||||
|
||||
try {
|
||||
// Try to read the JSON file containing the seed.
|
||||
seed = await this.jsonFiles.readJSON(filename)
|
||||
} catch (err) {
|
||||
const seedNum = Math.floor(Math.random() * 1000000000000000000000)
|
||||
seed = seedNum.toString()
|
||||
|
||||
// Save the newly generated seed
|
||||
await this.jsonFiles.writeJSON(seed, filename)
|
||||
}
|
||||
|
||||
// console.log('getSeed() seed: ', seed)
|
||||
|
||||
return seed
|
||||
} catch (err) {
|
||||
console.error('Error in adapters/ipfs/ipfs.js/getSeed()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default IpfsAdapter
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
/*
|
||||
Wallet adapter. This library instantiates minimal-slp-wallet based on different
|
||||
configuration settings. That single instance of minimal-slp-wallet is then
|
||||
easily passed around the rest of this application.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import BchWallet from 'minimal-slp-wallet'
|
||||
|
||||
// Local libraries
|
||||
import config from '../../config/index.js'
|
||||
import JsonFiles from './json-files.js'
|
||||
|
||||
class Wallet {
|
||||
constructor (localConfig = {}) {
|
||||
// Encapsulate dependencies
|
||||
this.config = config
|
||||
this.BchWallet = BchWallet
|
||||
this.jsonFiles = new JsonFiles()
|
||||
|
||||
// Bind 'this' object to all subfunctions
|
||||
this.instanceWalletWithoutInitialization = this.instanceWalletWithoutInitialization.bind(this)
|
||||
}
|
||||
|
||||
// This is used for initializing the wallet, without waiting to update the wallet
|
||||
// UTXOs from the blockchain.
|
||||
// This is useful when the wallet is simply needed to make calls to the blockchain,
|
||||
// and there is no need to hydrate it with UTXO data.
|
||||
async instanceWalletWithoutInitialization (walletData = {}, advancedConfig = {}) {
|
||||
try {
|
||||
// Use the apiToken from the config settings, if one is not passed-in at
|
||||
// run-time.
|
||||
if (!advancedConfig.apiToken) {
|
||||
advancedConfig.apiToken = this.config.apiToken
|
||||
}
|
||||
|
||||
// If an authentication password is passed in the config, add it.
|
||||
if (this.config.authPass) {
|
||||
advancedConfig.authPass = this.config.authPass
|
||||
}
|
||||
|
||||
// Detect and configure different blockchain infrastructure settings.
|
||||
if (this.config.walletInterface === 'web2') {
|
||||
advancedConfig.interface = 'rest-api'
|
||||
advancedConfig.restURL = this.config.apiServer
|
||||
} else {
|
||||
// By default use the web3 Cash Stack (https://cashstack.info)
|
||||
advancedConfig.interface = 'consumer-api'
|
||||
advancedConfig.restURL = this.config.apiServer
|
||||
}
|
||||
|
||||
// console.log('advancedConfig setting when creating wallet: ', advancedConfig)
|
||||
|
||||
// Instantiate minimal-slp-wallet.
|
||||
if (walletData.mnemonic) {
|
||||
// Instance the wallet using the mnemonic passed in to this function.
|
||||
console.log('Mnemonic provided to wallet library from calling function.')
|
||||
this.bchWallet = await this._instanceWallet(walletData.mnemonic, advancedConfig)
|
||||
} else if (this.config.mnemonic) {
|
||||
// Otherwise use the mnemonic in the config setting or passed as an environment variable.
|
||||
console.log('Mnemonic retrieved from config file or environment variable.')
|
||||
this.bchWallet = await this._instanceWallet(this.config.mnemonic, advancedConfig)
|
||||
} else {
|
||||
// If no mnemonic is provided, then generate a new mnemonic to create the wallet.
|
||||
console.log('New mnemonic generated.')
|
||||
this.bchWallet = await this._instanceWallet(undefined, advancedConfig)
|
||||
}
|
||||
|
||||
// Wait for wallet to initialize.
|
||||
await this.bchWallet.walletInfoPromise
|
||||
console.log('BCH wallet initialized.')
|
||||
console.log(`Wallet address: ${this.bchWallet.walletInfo.cashAddress}`)
|
||||
console.log(`Wallet mnemonic: ${this.bchWallet.walletInfo.mnemonic}`)
|
||||
|
||||
return this.bchWallet
|
||||
} catch (err) {
|
||||
console.error('Error in instanceWalletWithoutInitialization()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// This function is used for easier mocking of unit tests.
|
||||
async _instanceWallet (mnemonic, config) {
|
||||
const wallet = new this.BchWallet(mnemonic, config)
|
||||
await wallet.walletInfoPromise
|
||||
|
||||
return wallet
|
||||
}
|
||||
|
||||
// Open the wallet file, or create one if the file doesn't exist.
|
||||
// Does not instance the wallet. The output of this function is expected to
|
||||
// be passed to instanceWallet().
|
||||
async openWallet (advancedConfig = {}) {
|
||||
try {
|
||||
let walletData
|
||||
|
||||
// Try to open the wallet.json file.
|
||||
try {
|
||||
walletData = await this.jsonFiles.readJSON(this.config.walletFile)
|
||||
walletData = walletData.wallet
|
||||
} catch (err) {
|
||||
// Create a new wallet file if one does not already exist.
|
||||
console.log('Wallet file not found. Creating new wallet.json file.')
|
||||
|
||||
// Create a new wallet.
|
||||
// No-Update flag creates wallet without making any network calls.
|
||||
const walletInstance = await this.instanceWalletWithoutInitialization({}, advancedConfig)
|
||||
|
||||
walletData = walletInstance.walletInfo
|
||||
|
||||
// Add the nextAddress property
|
||||
walletData.nextAddress = 1
|
||||
|
||||
// Write the wallet data to the JSON file.
|
||||
await this.jsonFiles.writeJSON({ wallet: walletData }, this.config.walletFile)
|
||||
}
|
||||
|
||||
// console.log('walletData: ', walletData)
|
||||
return walletData
|
||||
} catch (err) {
|
||||
console.error('Error in openWallet()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Create an instance of minimal-slp-wallet. Same as
|
||||
// instanceWalletWithoutInitialization(), but waits for the wallet to initialize
|
||||
// its UTXOs (wallet balance and tokens).
|
||||
async instanceWallet (walletData = {}, advancedConfig = {}) {
|
||||
try {
|
||||
// Instance the wallet without initialization.
|
||||
await this.instanceWalletWithoutInitialization(walletData, advancedConfig)
|
||||
|
||||
// Initialize the wallet
|
||||
await this.bchWallet.initialize()
|
||||
|
||||
return this.bchWallet
|
||||
} catch (err) {
|
||||
console.error('Error in wallet.js/instanceWallet()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Increments the 'nextAddress' property in the wallet file. This property
|
||||
// indicates the HD index that should be used to generate a key pair for
|
||||
// storing funds for Offers.
|
||||
// This function opens the wallet file, increments the nextAddress property,
|
||||
// then saves the change to the wallet file.
|
||||
async incrementNextAddress () {
|
||||
try {
|
||||
const walletData = await this.openWallet()
|
||||
|
||||
await this.instanceWalletWithoutInitialization(walletData)
|
||||
|
||||
walletData.nextAddress++
|
||||
// console.log('walletData finish: ', walletData)
|
||||
|
||||
await this.jsonFiles.writeJSON({ wallet: walletData }, this.config.walletFile)
|
||||
|
||||
// Update the working instance of the wallet.
|
||||
this.bchWallet.walletInfo.nextAddress++
|
||||
// console.log('this.bchWallet.walletInfo: ', this.bchWallet.walletInfo)
|
||||
|
||||
return walletData.nextAddress
|
||||
} catch (err) {
|
||||
console.error('Error in incrementNextAddress()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// This method returns an object that contains a private key WIF, public address,
|
||||
// and the index of the HD wallet that the key pair was generated from.
|
||||
// TODO: Allow input integer. If input is used, use that as the index. If no
|
||||
// input is provided, then call incrementNextAddress().
|
||||
async getKeyPair (hdIndex = 0) {
|
||||
try {
|
||||
if (!hdIndex) {
|
||||
// Increment the HD index and generate a new key pair.
|
||||
hdIndex = await this.incrementNextAddress()
|
||||
}
|
||||
|
||||
const mnemonic = this.bchWallet.walletInfo.mnemonic
|
||||
|
||||
// root seed buffer
|
||||
const rootSeed = await this.bchWallet.bchjs.Mnemonic.toSeed(mnemonic)
|
||||
const masterHDNode = this.bchWallet.bchjs.HDNode.fromSeed(rootSeed)
|
||||
|
||||
// HDNode of BIP44 account
|
||||
// const account = this.bchWallet.bchjs.HDNode.derivePath(masterHDNode, "m/44'/245'/0'")
|
||||
const childNode = masterHDNode.derivePath(`m/44'/245'/0'/0/${hdIndex}`)
|
||||
const cashAddress = this.bchWallet.bchjs.HDNode.toCashAddress(childNode)
|
||||
console.log('Generating a new key pair for cashAddress: ', cashAddress)
|
||||
|
||||
const wif = this.bchWallet.bchjs.HDNode.toWIF(childNode)
|
||||
|
||||
const outObj = {
|
||||
cashAddress,
|
||||
wif,
|
||||
hdIndex
|
||||
}
|
||||
return outObj
|
||||
} catch (err) {
|
||||
console.error('Error in getKeyPair()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Optimize the wallet by consolidating the UTXOs.
|
||||
async optimize () {
|
||||
const UTXO_THREASHOLD = 7
|
||||
// Do a dry-run first to see if there are enough UTXOs worth consolidating.
|
||||
const dryRunOut = await this.bchWallet.optimize(true)
|
||||
if (dryRunOut.bchUtxoCnt > UTXO_THREASHOLD) {
|
||||
// Consolidate BCH UTXOs if the count is above the threashold.
|
||||
const txids = await this.bchWallet.optimize()
|
||||
console.log(`Wallet optimized with these return values: ${JSON.stringify(txids, null, 2)}`)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Get the balance of the wallet in sats and PSF tokens.
|
||||
// This function is called by the GET /entry/balance controller.
|
||||
async getBalance () {
|
||||
const balance = await this.bchWallet.getBalance()
|
||||
// console.log('balance: ', balance)
|
||||
const tokens = await this.bchWallet.listTokens()
|
||||
// console.log('tokens: ', tokens)
|
||||
// Find the array entry for the PSF token
|
||||
const psfTokens = tokens.find(x => x.tokenId === '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0')
|
||||
// console.log('psfTokens: ', psfTokens)
|
||||
let psfBalance = 0
|
||||
if (psfTokens) {
|
||||
psfBalance = psfTokens.qty
|
||||
}
|
||||
const outObj = {
|
||||
satBalance: balance,
|
||||
psfBalance,
|
||||
success: true
|
||||
}
|
||||
return outObj
|
||||
}
|
||||
}
|
||||
|
||||
export default Wallet
|
||||
@@ -97,6 +97,7 @@ class JSONRPC {
|
||||
wlogger.info(
|
||||
`JSON RPC received from ${from}, ID: ${parsedData.payload.id}, type: ${parsedData.type}`
|
||||
)
|
||||
// console.log('parsedData: ', JSON.stringify(parsedData, null, 2))
|
||||
}
|
||||
// console.log(`parsedData: ${JSON.stringify(parsedData, null, 2)}`)
|
||||
|
||||
@@ -113,6 +114,11 @@ class JSONRPC {
|
||||
console.log('routing to BCH adapter')
|
||||
retObj = await _this.adapters.bch.rpcHandler(parsedData)
|
||||
}
|
||||
|
||||
if (parsedData.payload.result.method === 'file-pin') {
|
||||
console.log('routing to IPFS Files adapter')
|
||||
retObj = await _this.adapters.ipfsFiles.rpcHandler(parsedData)
|
||||
}
|
||||
} catch (err) {
|
||||
/* exit quietly */
|
||||
}
|
||||
|
||||
@@ -723,6 +723,33 @@ class BchRESTControllerLib {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /bch/service Get the IPFS ID for the Wallet service
|
||||
* @apiPermission public
|
||||
* @apiName getService
|
||||
* @apiGroup REST BCH
|
||||
* @apiDescription Get the IPFS ID for the ipfs-bch-wallet-service node that
|
||||
* this app is using to retrieve blockchain data from.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5015/bch/service
|
||||
*
|
||||
*/
|
||||
async getService (ctx) {
|
||||
try {
|
||||
const selectedServiceProvider = this.adapters.ipfs.ipfsCoordAdapter.state.selectedServiceProvider
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
selectedServiceProvider
|
||||
}
|
||||
} catch (err) {
|
||||
// wlogger.error('Error in ipfs/controller.js/viewFile(): ', err)
|
||||
console.log('Error in ipfs/controller.js/getService(): ', err)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// DRY error handler
|
||||
handleError (ctx, err) {
|
||||
// If an HTTP status is specified by the buisiness logic, use that.
|
||||
|
||||
@@ -63,6 +63,7 @@ class BchRouter {
|
||||
this.router.post('/utxoIsValid', this.utxoIsValid)
|
||||
this.router.post('/getTokenData', this.getTokenData)
|
||||
this.router.post('/getTokenData2', this.getTokenData2)
|
||||
this.router.get('/service', this.getService)
|
||||
|
||||
// Attach the Controller routes to the Koa app.
|
||||
app.use(this.router.routes())
|
||||
@@ -116,6 +117,10 @@ class BchRouter {
|
||||
async getTokenData2 (ctx, next) {
|
||||
await _this.bchRESTController.getTokenData2(ctx, next)
|
||||
}
|
||||
|
||||
async getService (ctx, next) {
|
||||
await _this.bchRESTController.getService(ctx, next)
|
||||
}
|
||||
}
|
||||
|
||||
// module.exports = BchRouter
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
REST API Controller library for the /ipfs route
|
||||
*/
|
||||
|
||||
import wlogger from '../../../adapters/wlogger.js'
|
||||
// Global npm libraries
|
||||
import mime from 'mime-types'
|
||||
|
||||
let _this
|
||||
// Local libraries
|
||||
import wlogger from '../../../adapters/wlogger.js'
|
||||
|
||||
class IpfsRESTControllerLib {
|
||||
constructor (localConfig = {}) {
|
||||
@@ -26,14 +28,23 @@ class IpfsRESTControllerLib {
|
||||
// this.UserModel = this.adapters.localdb.Users
|
||||
// this.userUseCases = this.useCases.user
|
||||
|
||||
_this = this
|
||||
// Bind 'this' object to all subfunctions
|
||||
this.getStatus = this.getStatus.bind(this)
|
||||
this.getPeers = this.getPeers.bind(this)
|
||||
this.getRelays = this.getRelays.bind(this)
|
||||
this.handleError = this.handleError.bind(this)
|
||||
this.connect = this.connect.bind(this)
|
||||
this.getThisNode = this.getThisNode.bind(this)
|
||||
this.viewFile = this.viewFile.bind(this)
|
||||
this.getService = this.getService.bind(this)
|
||||
this.getFileInfo = this.getFileInfo.bind(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /ipfs Get status on IPFS infrastructure
|
||||
* @apiPermission public
|
||||
* @apiName GetIpfsStatus
|
||||
* @apiGroup REST BCH
|
||||
* @apiGroup REST IPFS
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5001/ipfs
|
||||
@@ -41,13 +52,13 @@ class IpfsRESTControllerLib {
|
||||
*/
|
||||
async getStatus (ctx) {
|
||||
try {
|
||||
const status = await _this.adapters.ipfs.getStatus()
|
||||
const status = await this.adapters.ipfs.getStatus()
|
||||
|
||||
ctx.body = { status }
|
||||
} catch (err) {
|
||||
wlogger.error('Error in ipfs/controller.js/getStatus(): ')
|
||||
// ctx.throw(422, err.message)
|
||||
_this.handleError(ctx, err)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,26 +67,160 @@ class IpfsRESTControllerLib {
|
||||
try {
|
||||
const showAll = ctx.request.body.showAll
|
||||
|
||||
const peers = await _this.adapters.ipfs.getPeers(showAll)
|
||||
const peers = await this.adapters.ipfs.getPeers(showAll)
|
||||
|
||||
ctx.body = { peers }
|
||||
} catch (err) {
|
||||
wlogger.error('Error in ipfs/controller.js/getPeers(): ')
|
||||
// ctx.throw(422, err.message)
|
||||
_this.handleError(ctx, err)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Get data about the known Circuit Relays. Hydrate with data from peers list.
|
||||
async getRelays (ctx) {
|
||||
try {
|
||||
const relays = await _this.adapters.ipfs.getRelays()
|
||||
const relays = await this.adapters.ipfs.getRelays()
|
||||
|
||||
ctx.body = { relays }
|
||||
} catch (err) {
|
||||
wlogger.error('Error in ipfs/controller.js/getRelays(): ')
|
||||
// ctx.throw(422, err.message)
|
||||
_this.handleError(ctx, err)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
async connect (ctx) {
|
||||
try {
|
||||
const multiaddr = ctx.request.body.multiaddr
|
||||
const getDetails = ctx.request.body.getDetails
|
||||
|
||||
// console.log('this.adapters.ipfs.ipfsCoordAdapter.ipfsCoord.adapters.ipfs: ', this.adapters.ipfs.ipfsCoordAdapter.ipfsCoord.adapters.ipfs)
|
||||
const result = await this.adapters.ipfs.ipfsCoordAdapter.ipfsCoord.adapters.ipfs.connectToPeer({ multiaddr, getDetails })
|
||||
// console.log('result: ', result)
|
||||
|
||||
ctx.body = result
|
||||
} catch (err) {
|
||||
wlogger.error('Error in ipfs/controller.js/connect():', err)
|
||||
// ctx.throw(422, err.message)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /ipfs/node Get a copy of the thisNode object from helia-coord
|
||||
* @apiPermission public
|
||||
* @apiName GetThisNode
|
||||
* @apiGroup REST IPFS
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5001/ipfs/node
|
||||
*
|
||||
*/
|
||||
async getThisNode (ctx) {
|
||||
try {
|
||||
const thisNode = this.adapters.ipfs.ipfsCoordAdapter.ipfsCoord.thisNode
|
||||
|
||||
ctx.body = { thisNode }
|
||||
} catch (err) {
|
||||
wlogger.error('Error in ipfs/controller.js/getThisNode(): ')
|
||||
// ctx.throw(422, err.message)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /ipfs/view/:cid Retrieve and display a file via its IPFS CID
|
||||
* @apiPermission public
|
||||
* @apiName GetCidView
|
||||
* @apiGroup REST IPFS
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5001/ipfs/view/bafkreieaqtdhfywyddomswogynzymukosqqgqo7lkt5lch2zwfnc55m6om
|
||||
*
|
||||
*/
|
||||
async viewFile (ctx) {
|
||||
try {
|
||||
const { cid } = ctx.params
|
||||
|
||||
// const file = await this.adapters.ipfs.ipfs.blockstore.get(cid)
|
||||
// return file
|
||||
|
||||
// const cid = ctx.params.cid
|
||||
|
||||
const { filename, readStream } = await this.useCases.ipfs.downloadCid({ cid })
|
||||
|
||||
// ctx.body = ctx.req.pipe(readStream)
|
||||
|
||||
// Lookup the mime type from the filename.
|
||||
const contentType = mime.lookup(filename)
|
||||
|
||||
ctx.set('Content-Type', contentType)
|
||||
ctx.set(
|
||||
'Content-Disposition',
|
||||
// 'inline; filename="' + filename + '"'
|
||||
`inline; filename="${filename}"`
|
||||
)
|
||||
ctx.body = readStream
|
||||
} catch (err) {
|
||||
// wlogger.error('Error in ipfs/controller.js/viewFile(): ', err)
|
||||
console.log('Error in ipfs/controller.js/viewFile(): ', err)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /ipfs/service Get the IPFS ID for the File Pin service
|
||||
* @apiPermission public
|
||||
* @apiName GetService
|
||||
* @apiGroup REST IPFS
|
||||
* @apiDescription Get the IPFS ID for the ipfs-file-pin-service node that
|
||||
* this app is using to retrieve file data from.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5015/ipfs/service
|
||||
*
|
||||
*/
|
||||
async getService (ctx) {
|
||||
try {
|
||||
const selectedIpfsFileProvider = this.adapters.ipfs.ipfsCoordAdapter.state.selectedIpfsFileProvider
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
selectedIpfsFileProvider
|
||||
}
|
||||
} catch (err) {
|
||||
// wlogger.error('Error in ipfs/controller.js/viewFile(): ', err)
|
||||
console.log('Error in ipfs/controller.js/getService(): ', err)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /ipfs/file-info/:cid Get file pin info about a CID
|
||||
* @apiPermission public
|
||||
* @apiName GetFileInfo
|
||||
* @apiGroup REST IPFS
|
||||
* @apiDescription Get file metadata and pin status information give a CID.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5015/ipfs/file-info/bafkreieaqtdhfywyddomswogynzymukosqqgqo7lkt5lch2zwfnc55m6om
|
||||
*
|
||||
*/
|
||||
async getFileInfo (ctx) {
|
||||
try {
|
||||
const { cid } = ctx.params
|
||||
|
||||
const ipfsFiles = this.adapters.ipfsFiles
|
||||
|
||||
const metadata = await ipfsFiles.getFileMetadata({ cid })
|
||||
console.log('getCidMetadata() metadata: ', metadata)
|
||||
|
||||
ctx.body = metadata
|
||||
} catch (err) {
|
||||
// wlogger.error('Error in ipfs/controller.js/viewFile(): ', err)
|
||||
console.log('Error in ipfs/controller.js/getService(): ', err)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import Validators from '../middleware/validators.js'
|
||||
|
||||
// let _this
|
||||
|
||||
class BchRouter {
|
||||
class IpfsRouter {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
@@ -54,16 +54,17 @@ class BchRouter {
|
||||
this.router.get('/', this.ipfsRESTController.getStatus)
|
||||
this.router.post('/peers', this.ipfsRESTController.getPeers)
|
||||
this.router.post('/relays', this.ipfsRESTController.getRelays)
|
||||
this.router.post('/connect', this.ipfsRESTController.connect)
|
||||
this.router.get('/node', this.ipfsRESTController.getThisNode)
|
||||
this.router.get('/view/:cid', this.ipfsRESTController.viewFile)
|
||||
this.router.get('/service', this.ipfsRESTController.getService)
|
||||
this.router.get('/file-info/:cid', this.ipfsRESTController.getFileInfo)
|
||||
|
||||
// Attach the Controller routes to the Koa app.
|
||||
app.use(this.router.routes())
|
||||
app.use(this.router.allowedMethods())
|
||||
}
|
||||
|
||||
// async getStatus (ctx, next) {
|
||||
// await _this.ipfsRESTController.getStatus(ctx, next)
|
||||
// }
|
||||
}
|
||||
|
||||
// module.exports = BchRouter
|
||||
export default BchRouter
|
||||
export default IpfsRouter
|
||||
|
||||
@@ -48,7 +48,7 @@ class TimerControllers {
|
||||
// Replace this example function with your own timer handler.
|
||||
exampleTimerFunc (negativeTest) {
|
||||
try {
|
||||
console.log('Example timer controller executed.')
|
||||
// console.log('Example timer controller executed.')
|
||||
|
||||
if (negativeTest) throw new Error('test error')
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
https://troutsblog.com/blog/clean-architecture
|
||||
*/
|
||||
|
||||
// Local libraries
|
||||
import UserUseCases from './user.js'
|
||||
import IpfsUseCases from './ipfs-use-cases.js'
|
||||
|
||||
class UseCases {
|
||||
constructor (localConfig = {}) {
|
||||
@@ -23,19 +25,14 @@ class UseCases {
|
||||
|
||||
// console.log('use-cases/index.js localConfig: ', localConfig)
|
||||
this.user = new UserUseCases(localConfig)
|
||||
this.ipfs = new IpfsUseCases(localConfig)
|
||||
}
|
||||
|
||||
// Run any startup Use Cases at the start of the app.
|
||||
async start () {
|
||||
// try {
|
||||
console.log('Async Use Cases have been started.')
|
||||
|
||||
return true
|
||||
// } catch (err) {
|
||||
// console.error('Error in use-cases/index.js/start()')
|
||||
// // console.log(err)
|
||||
// throw err
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
Use cases for working with IPFS.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
// import Wallet from 'minimal-slp-wallet'
|
||||
// import { CID } from 'multiformats'
|
||||
// import RetryQueue from '@chris.troutner/retry-queue'
|
||||
// import { exporter } from 'ipfs-unixfs-exporter'
|
||||
import { Duplex } from 'stream'
|
||||
|
||||
// Local libraries
|
||||
// import PinEntity from '../entities/pin.js'
|
||||
// import config from '../../config/index.js'
|
||||
|
||||
// const PSF_TOKEN_ID = '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0'
|
||||
|
||||
class IpfsUseCases {
|
||||
constructor (localConfig = {}) {
|
||||
// console.log('User localConfig: ', localConfig)
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of adapters must be passed in when instantiating IPFS Use Cases library.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
// this.exporter = exporter
|
||||
|
||||
// Bind 'this' object to all class subfunctions.
|
||||
this.downloadCid = this.downloadCid.bind(this)
|
||||
// this.downloadCid2 = this.downloadCid2.bind(this)
|
||||
}
|
||||
|
||||
// Download a pinned file, given its CID.
|
||||
// Returns a readable stream.
|
||||
async downloadCid (inObj = {}) {
|
||||
try {
|
||||
const { cid } = inObj
|
||||
|
||||
if (!cid) throw new Error('CID is undefined')
|
||||
|
||||
// const Pins = this.adapters.localdb.Pins
|
||||
// let existingModel = await Pins.find({ cid })
|
||||
// existingModel = existingModel[0]
|
||||
// console.log('existingModel: ', existingModel)
|
||||
|
||||
// if (!existingModel) {
|
||||
// throw new Error(`Database model for CID ${cid} does not exist.`)
|
||||
// }
|
||||
//
|
||||
// if (!existingModel.dataPinned) {
|
||||
// throw new Error('File has not been pinned. Not available.')
|
||||
// }
|
||||
|
||||
const filename = await this.getCidMetadata({ cid })
|
||||
|
||||
const helia = this.adapters.ipfs.ipfs
|
||||
|
||||
// Convert the file to a Buffer.
|
||||
const fileChunks = []
|
||||
for await (const chunk of helia.fs.cat(cid)) {
|
||||
fileChunks.push(chunk)
|
||||
}
|
||||
const fileBuf = Buffer.concat(fileChunks)
|
||||
|
||||
// Convert the Buffer into a readable stream
|
||||
const bufferToStream = (myBuffer) => {
|
||||
const tmp = new Duplex()
|
||||
tmp.push(myBuffer)
|
||||
tmp.push(null)
|
||||
return tmp
|
||||
}
|
||||
const readStream = bufferToStream(fileBuf)
|
||||
|
||||
// const filename = 'test.jpg'
|
||||
|
||||
return { filename, readStream }
|
||||
} catch (err) {
|
||||
console.error('Error in use-cases/ipfs.js/dowloadCid()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Given a CID, this function will retrieve file metadata from the
|
||||
// ipfs-file-pin-service, using the IPFS JSON-RPC. This metadata includes
|
||||
// the filename, which can be used to infer mime-type, so that it can be
|
||||
// delivered to a web browser.
|
||||
async getCidMetadata (inObj = {}) {
|
||||
try {
|
||||
const { cid } = inObj
|
||||
|
||||
const ipfsFiles = this.adapters.ipfsFiles
|
||||
|
||||
const metadata = await ipfsFiles.getFileMetadata({ cid })
|
||||
console.log('getCidMetadata() metadata: ', metadata)
|
||||
|
||||
const filename = metadata.fileMetadata.filename
|
||||
|
||||
return filename
|
||||
} catch (err) {
|
||||
console.error('Error in getCidMetadata(): ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// async downloadCid2 (inObj = {}) {
|
||||
// try {
|
||||
// const { cid } = inObj
|
||||
//
|
||||
// // console.log(`downloadFile() retrieving this CID: ${cid}, with fileName: ${fileName}, and path: ${path}`)
|
||||
//
|
||||
// const blockstore = this.adapters.ipfs.ipfs.blockstore
|
||||
// const entry = await this.exporter(cid, blockstore)
|
||||
//
|
||||
// console.info(entry.cid) // Qmqux
|
||||
// console.log('entry: ', entry)
|
||||
// // console.info(entry.unixfs.fileSize()) // 4
|
||||
//
|
||||
// // const filePath = `${path}/${fileName}`
|
||||
// // console.log(`filePath: ${filePath}`)
|
||||
// // const writableStream = this.fs.createWriteStream(filePath)
|
||||
// //
|
||||
// // writableStream.on('error', this.writeStreamError)
|
||||
// //
|
||||
// // writableStream.on('finish', this.writeStreamFinished)
|
||||
// //
|
||||
//
|
||||
// const fileChunks = []
|
||||
// for await (const buf of entry.content()) {
|
||||
// fileChunks.push(buf)
|
||||
// }
|
||||
// const fileBuf = Buffer.concat(fileChunks)
|
||||
//
|
||||
// //
|
||||
// // writableStream.end()
|
||||
//
|
||||
// // Convert the Buffer into a readable stream
|
||||
// const bufferToStream = (myBuffer) => {
|
||||
// const tmp = new Duplex()
|
||||
// tmp.push(myBuffer)
|
||||
// tmp.push(null)
|
||||
// return tmp
|
||||
// }
|
||||
// const readStream = bufferToStream(fileBuf)
|
||||
//
|
||||
// const filename = 'test.jpg'
|
||||
//
|
||||
// return { filename, readStream }
|
||||
//
|
||||
// // return { cid }
|
||||
// } catch (err) {
|
||||
// console.error('Error in ipfs-use-cases.js/downloadCid()')
|
||||
// throw err
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
export default IpfsUseCases
|
||||
@@ -94,25 +94,12 @@ describe('#bch-use-case', () => {
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
uut.ipfs.ipfsCoordAdapter.bchjs = {
|
||||
Util: {
|
||||
sleep: () => {
|
||||
throw new Error('test error')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mock data.
|
||||
const rpcId = '123'
|
||||
uut.rpcDataQueue.push(mockData.rpcData)
|
||||
|
||||
await uut.waitForRPCResponse(rpcId)
|
||||
await uut.waitForRPCResponse()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log('error: ', err)
|
||||
assert.include(err.message, 'test error')
|
||||
assert.include(err.message, 'rpcId can not be false or undefined')
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -156,6 +143,9 @@ describe('#bch-use-case', () => {
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut, 'uid').throws(new Error('test error'))
|
||||
|
||||
await uut.getBalances()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
@@ -191,7 +181,7 @@ describe('#bch-use-case', () => {
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.message, 'test error')
|
||||
assert.equal(err.message, 'addr required when calling getUtxos')
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -267,7 +257,7 @@ describe('#bch-use-case', () => {
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.message, 'test error')
|
||||
assert.equal(err.message, 'hex required when calling broadcast')
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -297,7 +287,7 @@ describe('#bch-use-case', () => {
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.message, 'test error')
|
||||
assert.equal(err.message, 'address required when calling getTransactions()')
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -327,7 +317,7 @@ describe('#bch-use-case', () => {
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.message, 'test error')
|
||||
assert.equal(err.message, 'txids required when calling getTransaction()')
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -357,7 +347,7 @@ describe('#bch-use-case', () => {
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.message, 'test error')
|
||||
assert.equal(err.message, 'address required when calling getPubKey()')
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -387,7 +377,7 @@ describe('#bch-use-case', () => {
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.message, 'test error')
|
||||
assert.equal(err.message, 'utxo required when calling utxoIsValid()')
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -417,7 +407,7 @@ describe('#bch-use-case', () => {
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.message, 'test error')
|
||||
assert.equal(err.message, 'tokenId required when calling getTokenData()')
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -447,7 +437,7 @@ describe('#bch-use-case', () => {
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.message, 'test error')
|
||||
assert.equal(err.message, 'tokenId required when calling getTokenData2()')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -76,7 +76,7 @@ describe('#IPFS', () => {
|
||||
it('should get the public IP address if this node is a Circuit Relay', async () => {
|
||||
// Mock dependencies.
|
||||
uut.IpfsCoord = IPFSCoordMock
|
||||
sandbox.stub(uut.publicIp, 'v4').resolves('123')
|
||||
sandbox.stub(uut, 'publicIp').resolves('123')
|
||||
|
||||
// Force Circuit Relay
|
||||
uut.config.isCircuitRelay = true
|
||||
@@ -90,7 +90,7 @@ describe('#IPFS', () => {
|
||||
it('should exit quietly if this node is a Circuit Relay and there is an issue getting the IP address', async () => {
|
||||
// Mock dependencies.
|
||||
uut.IpfsCoord = IPFSCoordMock
|
||||
sandbox.stub(uut.publicIp, 'v4').rejects(new Error('test error'))
|
||||
sandbox.stub(uut, 'publicIp').rejects(new Error('test error'))
|
||||
|
||||
// Force Circuit Relay
|
||||
uut.config.isCircuitRelay = true
|
||||
|
||||
@@ -69,4 +69,155 @@ describe('#IPFS-adapter-index', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getStatus', () => {
|
||||
it('should return an object with node metrics', () => {
|
||||
// Force uut to have the needed properties
|
||||
uut.ipfsCoordAdapter = {
|
||||
ipfsCoord: {
|
||||
thisNode: {
|
||||
ipfsId: 'fake-id',
|
||||
ipfsMultiaddrs: [],
|
||||
bchAddr: 'fake-bch-addr',
|
||||
slpAddr: 'fake-slp-addr',
|
||||
pubKey: 'fake-pubkey',
|
||||
peerList: [],
|
||||
relayData: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = uut.getStatus()
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.property(result, 'ipfsId')
|
||||
assert.property(result, 'multiAddrs')
|
||||
assert.property(result, 'bchAddr')
|
||||
assert.property(result, 'slpAddr')
|
||||
assert.property(result, 'pubKey')
|
||||
assert.property(result, 'peers')
|
||||
assert.property(result, 'relays')
|
||||
})
|
||||
|
||||
it('should catch, report, and throw errors', () => {
|
||||
try {
|
||||
uut.getStatus()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log('err.message: ', err.message)
|
||||
assert.include(err.message, 'Cannot read properties')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#_removeDuplicatePeers', () => {
|
||||
it('should return the same array when there are no duplicates', () => {
|
||||
const inArray = [
|
||||
'12D3KooWPpBXhhAeoCZCGuQ3KR4xwHzzvtP57f6zLmo8P7ZFBJFE',
|
||||
'12D3KooWRBhwfeP2Y9CDkFRBAZ1pmxUadH36TKuk3KtKm5XXP8mA',
|
||||
'12D3KooWGsgHWyDLKuV4ZSfRJfsxQJj77rxx3i8Px3qXKHsLN7a2',
|
||||
'12D3KooWJyc54njjeZGbLew4D8u1ghrmZTTPyh3QpBF7dxtd3zGY',
|
||||
'12D3KooWDtj9cfj1SKuLbDNKvKRKSsGN8qivq9M8CYpLPDpcD5pu'
|
||||
]
|
||||
|
||||
const result = uut._removeDuplicatePeers(inArray)
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.equal(result.length, inArray.length)
|
||||
})
|
||||
|
||||
it('should remove duplicates from an array', () => {
|
||||
const inArray = [
|
||||
'12D3KooWPpBXhhAeoCZCGuQ3KR4xwHzzvtP57f6zLmo8P7ZFBJFE',
|
||||
'12D3KooWRBhwfeP2Y9CDkFRBAZ1pmxUadH36TKuk3KtKm5XXP8mA',
|
||||
'12D3KooWGsgHWyDLKuV4ZSfRJfsxQJj77rxx3i8Px3qXKHsLN7a2',
|
||||
'12D3KooWJyc54njjeZGbLew4D8u1ghrmZTTPyh3QpBF7dxtd3zGY',
|
||||
'12D3KooWDtj9cfj1SKuLbDNKvKRKSsGN8qivq9M8CYpLPDpcD5pu',
|
||||
'12D3KooWPpBXhhAeoCZCGuQ3KR4xwHzzvtP57f6zLmo8P7ZFBJFE',
|
||||
'12D3KooWRBhwfeP2Y9CDkFRBAZ1pmxUadH36TKuk3KtKm5XXP8mA'
|
||||
]
|
||||
|
||||
const result = uut._removeDuplicatePeers(inArray)
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.equal(result.length, inArray.length - 2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getPeers', () => {
|
||||
it('should return an array of current JSON data about each peer when showAll is true', async () => {
|
||||
// Mock dependencies
|
||||
uut.ipfsCoordAdapter.ipfsCoord = {
|
||||
thisNode: {
|
||||
peerData: [{ from: 'a', data: { jsonLd: { name: 'a', protocol: 'test', version: '1' } } }, { from: 'b' }]
|
||||
},
|
||||
adapters: {
|
||||
ipfs: {
|
||||
getPeers: async () => ['a', 'b']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = await uut.getPeers(true)
|
||||
|
||||
assert.isArray(result)
|
||||
})
|
||||
|
||||
it('should catch, report, and throw errors', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
uut.ipfsCoordAdapter.ipfsCoord = {
|
||||
thisNode: {},
|
||||
adapters: {
|
||||
ipfs: {
|
||||
getPeers: async () => { throw new Error('test error') }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await uut.getPeers()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log('err.message: ', err.message)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getRelays', () => {
|
||||
it('should return known relays', () => {
|
||||
uut.ipfsCoordAdapter.ipfsCoord = {
|
||||
thisNode: {
|
||||
peerData: [{ from: 'a', data: { jsonLd: { name: 'a', description: 'test' } } }, { from: 'b' }],
|
||||
relayData: [{ ipfsId: 'c' }, { ipfsId: 'a' }]
|
||||
}
|
||||
}
|
||||
|
||||
const result = uut.getRelays()
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.isArray(result.v2Relays)
|
||||
assert.equal(result.v2Relays.length, 2)
|
||||
assert.property(result.v2Relays[0], 'name')
|
||||
assert.property(result.v2Relays[0], 'description')
|
||||
assert.property(result.v2Relays[1], 'name')
|
||||
assert.property(result.v2Relays[1], 'description')
|
||||
})
|
||||
|
||||
it('should catch, report, and throw errors', () => {
|
||||
try {
|
||||
// Force an error
|
||||
uut.ipfsCoordAdapter.ipfsCoord = { thisNode: {} }
|
||||
|
||||
uut.getRelays()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log('err.message: ', err.message)
|
||||
assert.include(err.message, 'Cannot read')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,21 +2,29 @@
|
||||
Unit tests for the IPFS Adapter.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import { assert } from 'chai'
|
||||
|
||||
import sinon from 'sinon'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
import { peerIdFromString } from '@libp2p/peer-id'
|
||||
|
||||
// Local libraries
|
||||
import IPFSLib from '../../../src/adapters/ipfs/ipfs.js'
|
||||
import create from '../mocks/ipfs-mock.js'
|
||||
// import create from '../mocks/ipfs-mock.js'
|
||||
import config from '../../../config/index.js'
|
||||
import createHeliaLib from '../mocks/helia-mock.js'
|
||||
|
||||
// config.isProduction = true;
|
||||
describe('#IPFS-adapter', () => {
|
||||
let uut
|
||||
let sandbox
|
||||
let ipfs
|
||||
|
||||
beforeEach(() => {
|
||||
uut = new IPFSLib()
|
||||
|
||||
ipfs = cloneDeep(createHeliaLib)
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
@@ -45,35 +53,28 @@ describe('#IPFS-adapter', () => {
|
||||
describe('#start', () => {
|
||||
it('should return a promise that resolves into an instance of IPFS.', async () => {
|
||||
// Mock dependencies.
|
||||
uut.create = create
|
||||
sandbox.stub(uut, 'createNode').resolves(ipfs)
|
||||
sandbox.stub(uut, 'publicIp').resolves('192.168.2.4')
|
||||
sandbox.stub(uut, 'multiaddr').returns('/ip4/fake-multiaddr')
|
||||
|
||||
const result = await uut.start()
|
||||
// console.log('result: ', result)
|
||||
|
||||
// Assert properties of the instance are set.
|
||||
assert.equal(uut.isReady, true)
|
||||
assert.property(uut, 'multiaddrs')
|
||||
assert.property(uut, 'id')
|
||||
|
||||
assert.property(result, 'config')
|
||||
})
|
||||
|
||||
it('should return a promise that resolves into an instance of IPFS in production mode.', async () => {
|
||||
// Mock dependencies.
|
||||
uut.create = create
|
||||
uut.config.isProduction = true
|
||||
const result = await uut.start()
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.equal(uut.isReady, true)
|
||||
|
||||
assert.property(result, 'config')
|
||||
// Output should be an instance of IPFS
|
||||
assert.property(result, 'libp2p')
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut, 'create').rejects(new Error('test error'))
|
||||
sandbox.stub(uut, 'createNode').rejects(new Error('test error'))
|
||||
|
||||
await uut.start()
|
||||
|
||||
assert.fail('Unexpected code path.')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
@@ -96,24 +97,146 @@ describe('#IPFS-adapter', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// describe('#rmBlocksDir', () => {
|
||||
// it('should delete the /blocks directory', () => {
|
||||
// const result = uut.rmBlocksDir()
|
||||
//
|
||||
// assert.equal(result, true)
|
||||
// })
|
||||
//
|
||||
// it('should catch and throw an error', () => {
|
||||
// try {
|
||||
// // Force an error
|
||||
// sandbox.stub(uut.fs, 'rmdirSync').throws(new Error('test error'))
|
||||
//
|
||||
// uut.rmBlocksDir()
|
||||
//
|
||||
// assert.fail('Unexpected code path')
|
||||
// } catch (err) {
|
||||
// assert.equal(err.message, 'test error')
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
describe('#ensureBlocksDir', () => {
|
||||
it('should create directory if it does not exist', () => {
|
||||
// Force desired code path
|
||||
sandbox.stub(uut.fs, 'existsSync').returns(false)
|
||||
sandbox.stub(uut.fs, 'mkdirSync').returns(true)
|
||||
|
||||
const result = uut.ensureBlocksDir()
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
|
||||
it('should report and throw errors', () => {
|
||||
// Force an error
|
||||
sandbox.stub(uut.fs, 'existsSync').throws(new Error('test error'))
|
||||
|
||||
try {
|
||||
uut.ensureBlocksDir()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#createNode', () => {
|
||||
it('should report and throw errors', async () => {
|
||||
// Force an error
|
||||
sandbox.stub(uut, 'createLibp2p').rejects(new Error('test error'))
|
||||
|
||||
try {
|
||||
await uut.createNode()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should create an IPFS node from Helia', async () => {
|
||||
uut.config.isCircuitRelay = false
|
||||
const result = await uut.createNode()
|
||||
// console.log('result: ', result)
|
||||
|
||||
// Assert the returned IPFS node has expected properties
|
||||
assert.property(result, 'libp2p')
|
||||
assert.property(result, 'blockstore')
|
||||
|
||||
// Stop the IPFS node
|
||||
await result.stop()
|
||||
})
|
||||
|
||||
it('should create a Circuit Relay if configured', async () => {
|
||||
uut.config.isCircuitRelay = true
|
||||
const result = await uut.createNode()
|
||||
|
||||
// Assert the returned IPFS node has expected properties
|
||||
assert.property(result, 'libp2p')
|
||||
assert.property(result, 'blockstore')
|
||||
|
||||
// Stop the IPFS node
|
||||
await result.stop()
|
||||
})
|
||||
|
||||
it('should create a new private key on first run', async () => {
|
||||
uut.config.isCircuitRelay = false
|
||||
|
||||
// Mock dependencies and force desired code path.
|
||||
let beenCalled = false
|
||||
sandbox.stub(uut, 'getKeychain').resolves({
|
||||
exportPeerId: async () => {
|
||||
if (!beenCalled) {
|
||||
beenCalled = true
|
||||
throw new Error('test error')
|
||||
}
|
||||
return peerIdFromString('12D3KooWSXF1PnEfiA8bCG8SJduCvzdwHtvhVPK4WC6zzDoto2XP')
|
||||
},
|
||||
createKey: async () => {}
|
||||
})
|
||||
sandbox.stub(uut, 'createLibp2p').resolves()
|
||||
sandbox.stub(uut, 'createHelia').resolves({})
|
||||
|
||||
const result = await uut.createNode()
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.property(result, 'fs')
|
||||
})
|
||||
|
||||
it('should not use circuit relay when CONNECT_PREF is set to direct', async () => {
|
||||
process.env.CONNECT_PREF = 'direct'
|
||||
|
||||
uut.config.isCircuitRelay = false
|
||||
const result = await uut.createNode()
|
||||
// console.log('result: ', result)
|
||||
|
||||
delete process.env.CONNECT_PREF
|
||||
|
||||
// Assert the returned IPFS node has expected properties
|
||||
assert.property(result, 'libp2p')
|
||||
assert.property(result, 'blockstore')
|
||||
|
||||
// Stop the IPFS node
|
||||
await result.stop()
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getSeed', () => {
|
||||
it('should read the seed from the JSON file', async () => {
|
||||
// Mock dependencies and force desired code path
|
||||
sandbox.stub(uut.jsonFiles, 'readJSON').resolves('12345678')
|
||||
|
||||
const result = await uut.getSeed()
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.isString(result)
|
||||
})
|
||||
|
||||
it('should generate a new seed if the JSON file is not found', async () => {
|
||||
// Mock dependencies and force desired code path
|
||||
sandbox.stub(uut.jsonFiles, 'readJSON').rejects(new Error('test error'))
|
||||
sandbox.stub(uut.jsonFiles, 'writeJSON').resolves()
|
||||
|
||||
const result = await uut.getSeed()
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.isString(result)
|
||||
})
|
||||
|
||||
it('should catch, report, and throw errors', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut.jsonFiles, 'readJSON').rejects(new Error('test error'))
|
||||
sandbox.stub(uut.jsonFiles, 'writeJSON').rejects(new Error('test error'))
|
||||
|
||||
await uut.getSeed()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
/*
|
||||
Unit tests for the Wallet Adapter library.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import { assert } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import BchWallet from 'minimal-slp-wallet'
|
||||
import fs from 'fs'
|
||||
|
||||
// Local libraries
|
||||
import WalletAdapter from '../../../src/adapters/wallet.adapter.js'
|
||||
import { MockBchWallet } from '../mocks/adapters/wallet.js'
|
||||
|
||||
// Hack to get __dirname back.
|
||||
// https://blog.logrocket.com/alternatives-dirname-node-js-es-modules/
|
||||
import * as url from 'url'
|
||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
|
||||
|
||||
// Global constants
|
||||
const testWalletFile = `${__dirname.toString()}test-wallet.json`
|
||||
|
||||
describe('#wallet', () => {
|
||||
let uut
|
||||
let sandbox
|
||||
|
||||
before(() => {
|
||||
// Delete the test file if it exists.
|
||||
try {
|
||||
deleteFile(testWalletFile)
|
||||
} catch (err) { }
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
uut = new WalletAdapter()
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
after(() => {
|
||||
// Delete the test file if it exists.
|
||||
try {
|
||||
deleteFile(testWalletFile)
|
||||
} catch (err) { }
|
||||
})
|
||||
|
||||
describe('#_instanceWallet', () => {
|
||||
it('should create a wallet given a mnemonic', async () => {
|
||||
const mnemonic = 'wagon tray learn flat erase laugh lonely rug check captain jacket morning'
|
||||
const result = await uut._instanceWallet(mnemonic)
|
||||
// console.log('result: ', result)
|
||||
assert.equal(result.walletInfo.mnemonic, mnemonic)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#openWallet', () => {
|
||||
it('should create a new wallet when wallet file does not exist', async () => {
|
||||
// Mock dependencies
|
||||
uut.BchWallet = MockBchWallet
|
||||
// Ensure we open the test file, not the production wallet file.
|
||||
uut.config.walletFile = testWalletFile
|
||||
const result = await uut.openWallet()
|
||||
// console.log('result: ', result)
|
||||
assert.property(result, 'mnemonic')
|
||||
assert.property(result, 'privateKey')
|
||||
assert.property(result, 'publicKey')
|
||||
assert.property(result, 'cashAddress')
|
||||
assert.property(result, 'address')
|
||||
assert.property(result, 'slpAddress')
|
||||
assert.property(result, 'legacyAddress')
|
||||
assert.property(result, 'hdPath')
|
||||
})
|
||||
|
||||
it('should open existing wallet file', async () => {
|
||||
// This test case uses the file created in the previous test case.
|
||||
// Ensure we open the test file, not the production wallet file.
|
||||
uut.config.walletFile = testWalletFile
|
||||
const result = await uut.openWallet()
|
||||
// console.log('result: ', result)
|
||||
assert.property(result, 'mnemonic')
|
||||
assert.property(result, 'privateKey')
|
||||
assert.property(result, 'publicKey')
|
||||
assert.property(result, 'cashAddress')
|
||||
assert.property(result, 'address')
|
||||
assert.property(result, 'slpAddress')
|
||||
assert.property(result, 'legacyAddress')
|
||||
assert.property(result, 'hdPath')
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
uut.config.walletFile = ''
|
||||
uut.BchWallet = () => {
|
||||
}
|
||||
await uut.openWallet()
|
||||
// console.log('result: ', result)
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
assert.include(err.message, 'this.BchWallet is not a constructor')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#instanceWalletWithoutInitialization', () => {
|
||||
it('should create an instance of BchWallet', async () => {
|
||||
// Create a mock wallet.
|
||||
const mockWallet = new BchWallet()
|
||||
await mockWallet.walletInfoPromise
|
||||
sandbox.stub(mockWallet, 'initialize').resolves()
|
||||
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut, '_instanceWallet').resolves(mockWallet)
|
||||
uut.config.authPass = 'fake-auth-pass'
|
||||
|
||||
// Ensure we open the test file, not the production wallet file.
|
||||
uut.config.walletFile = testWalletFile
|
||||
const walletData = await uut.openWallet()
|
||||
|
||||
// console.log('walletData: ', walletData)
|
||||
const result = await uut.instanceWalletWithoutInitialization(walletData)
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.property(result, 'walletInfoPromise')
|
||||
assert.property(result, 'walletInfo')
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut, '_instanceWallet').rejects(new Error('test error'))
|
||||
|
||||
await uut.instanceWalletWithoutInitialization()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should create an instance of BchWallet using web2 infra', async () => {
|
||||
// Create a mock wallet.
|
||||
const mockWallet = new BchWallet()
|
||||
await mockWallet.walletInfoPromise
|
||||
sandbox.stub(mockWallet, 'initialize').resolves()
|
||||
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut, '_instanceWallet').resolves(mockWallet)
|
||||
|
||||
// Ensure we open the test file, not the production wallet file.
|
||||
uut.config.walletFile = testWalletFile
|
||||
const walletData = await uut.openWallet()
|
||||
// console.log('walletData: ', walletData)
|
||||
|
||||
// Force desired code path
|
||||
uut.config.walletInterface = 'web2'
|
||||
const result = await uut.instanceWalletWithoutInitialization(walletData)
|
||||
|
||||
// console.log('result: ', result)
|
||||
assert.property(result, 'walletInfoPromise')
|
||||
assert.property(result, 'walletInfo')
|
||||
})
|
||||
|
||||
it('should generate wallet from mnemonic in config', async () => {
|
||||
// Create a mock wallet.
|
||||
const mockWallet = new BchWallet()
|
||||
await mockWallet.walletInfoPromise
|
||||
sandbox.stub(mockWallet, 'initialize').resolves()
|
||||
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut, '_instanceWallet').resolves(mockWallet)
|
||||
|
||||
// Ensure we open the test file, not the production wallet file.
|
||||
uut.config.walletFile = testWalletFile
|
||||
const walletData = await uut.openWallet()
|
||||
// console.log('walletData: ', walletData)
|
||||
|
||||
const originalConfig = uut.config.mnemonic
|
||||
uut.config.mnemonic = walletData.mnemonic
|
||||
|
||||
const result = await uut.instanceWalletWithoutInitialization({})
|
||||
// console.log('result: ', result)
|
||||
|
||||
uut.config.mnemonic = originalConfig
|
||||
|
||||
assert.property(result, 'walletInfoPromise')
|
||||
assert.property(result, 'walletInfo')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#instanceWallet', () => {
|
||||
it('should create an instance of BchWallet', async () => {
|
||||
// Create a mock wallet.
|
||||
const mockWallet = new BchWallet()
|
||||
await mockWallet.walletInfoPromise
|
||||
sandbox.stub(mockWallet, 'initialize').resolves()
|
||||
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut, '_instanceWallet').resolves(mockWallet)
|
||||
|
||||
// Ensure we open the test file, not the production wallet file.
|
||||
uut.WALLET_FILE = testWalletFile
|
||||
const walletData = await uut.openWallet()
|
||||
// console.log('walletData: ', walletData)
|
||||
const result = await uut.instanceWallet(walletData)
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.property(result, 'walletInfoPromise')
|
||||
assert.property(result, 'walletInfo')
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut, 'instanceWalletWithoutInitialization').rejects(new Error('test error'))
|
||||
|
||||
await uut.instanceWallet()
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should create an instance of BchWallet using web2 infra', async () => {
|
||||
// Create a mock wallet.
|
||||
const mockWallet = new BchWallet()
|
||||
await mockWallet.walletInfoPromise
|
||||
sandbox.stub(mockWallet, 'initialize').resolves()
|
||||
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut, '_instanceWallet').resolves(mockWallet)
|
||||
|
||||
// Ensure we open the test file, not the production wallet file.
|
||||
uut.WALLET_FILE = testWalletFile
|
||||
const walletData = await uut.openWallet()
|
||||
// console.log('walletData: ', walletData)
|
||||
|
||||
// Force desired code path
|
||||
uut.config.useFullStackCash = true
|
||||
const result = await uut.instanceWallet(walletData)
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.property(result, 'walletInfoPromise')
|
||||
assert.property(result, 'walletInfo')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#incrementNextAddress', () => {
|
||||
it('should increment the nextAddress property', async () => {
|
||||
// Ensure we open the test file, not the production wallet file.
|
||||
uut.WALLET_FILE = testWalletFile
|
||||
// mock instance of minimal-slp-wallet
|
||||
uut.bchWallet = new MockBchWallet()
|
||||
const result = await uut.incrementNextAddress()
|
||||
assert.equal(result, 2)
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut, 'openWallet').rejects(new Error('test error'))
|
||||
await uut.incrementNextAddress()
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getKeyPair', () => {
|
||||
it('should return an object with a key pair', async () => {
|
||||
// Ensure we open the test file, not the production wallet file.
|
||||
// uut.WALLET_FILE = testWalletFile
|
||||
uut.config.walletFile = testWalletFile
|
||||
|
||||
const result = await uut.getKeyPair()
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.property(result, 'cashAddress')
|
||||
assert.property(result, 'wif')
|
||||
assert.property(result, 'hdIndex')
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox
|
||||
.stub(uut, 'incrementNextAddress')
|
||||
.rejects(new Error('test error'))
|
||||
await uut.getKeyPair()
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#optimize', () => {
|
||||
it('should call the wallet optimize function', async () => {
|
||||
// mock instance of minimal-slp-wallet
|
||||
uut.bchWallet = new MockBchWallet()
|
||||
sandbox.stub(uut.bchWallet, 'optimize').resolves({ bchUtxoCnt: 10 })
|
||||
const result = await uut.optimize()
|
||||
assert.equal(result, true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getBalance', () => {
|
||||
it('should get the balance for the wallet', async () => {
|
||||
// mock instance of minimal-slp-wallet
|
||||
uut.bchWallet = new MockBchWallet()
|
||||
// Mock dependencies and force desired code path
|
||||
sandbox.stub(uut.bchWallet, 'getBalance').resolves(41012)
|
||||
sandbox.stub(uut.bchWallet, 'listTokens').resolves([{
|
||||
tokenId: '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
|
||||
ticker: 'PSF',
|
||||
name: 'Permissionless Software Foundation',
|
||||
decimals: 8,
|
||||
tokenType: 1,
|
||||
url: 'psfoundation.cash',
|
||||
qty: 2
|
||||
}])
|
||||
const result = await uut.getBalance()
|
||||
// console.log('result: ', result)
|
||||
// Assert the expected properties exist and have the expected values.
|
||||
assert.equal(result.satBalance, 41012)
|
||||
assert.equal(result.psfBalance, 2)
|
||||
assert.equal(result.success, true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const deleteFile = (filepath) => {
|
||||
try {
|
||||
// Delete state if exist
|
||||
fs.unlinkSync(filepath)
|
||||
} catch (err) {
|
||||
// console.error('Error trying to delete file: ', err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
/*
|
||||
Unit tests for the REST API handler for the /ipfs endpoints.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
import { assert } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
|
||||
// Local libraries
|
||||
import IpfsApiController from '../../../../../src/controllers/rest-api/ipfs/controller.js'
|
||||
import adapters from '../../../mocks/adapters/index.js'
|
||||
import UseCasesMock from '../../../mocks/use-cases/index.js'
|
||||
|
||||
import { context as mockContext } from '../../../mocks/ctx-mock.js'
|
||||
let uut
|
||||
let sandbox
|
||||
let ctx
|
||||
|
||||
describe('#IPFS REST API', () => {
|
||||
before(async () => {
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
const useCases = new UseCasesMock()
|
||||
|
||||
uut = new IpfsApiController({ adapters, useCases })
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Mock the context object.
|
||||
ctx = mockContext()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should throw an error if adapters are not passed in', () => {
|
||||
try {
|
||||
uut = new IpfsApiController()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Adapters library required when instantiating /ipfs REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if useCases are not passed in', () => {
|
||||
try {
|
||||
uut = new IpfsApiController({ adapters })
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Use Cases library required when instantiating /ipfs REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#GET /status', () => {
|
||||
it('should return 422 status on biz logic error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut.adapters.ipfs, 'getStatus').rejects(new Error('test error'))
|
||||
|
||||
await uut.getStatus(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return 200 status on success', async () => {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.adapters.ipfs, 'getStatus').resolves({ a: 'b' })
|
||||
|
||||
await uut.getStatus(ctx)
|
||||
// console.log('ctx.body: ', ctx.body)
|
||||
|
||||
assert.property(ctx.body, 'status')
|
||||
assert.equal(ctx.body.status.a, 'b')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#POST /peers', () => {
|
||||
it('should return 422 status on biz logic error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut.adapters.ipfs, 'getPeers').rejects(new Error('test error'))
|
||||
|
||||
ctx.request.body = {
|
||||
showAll: true
|
||||
}
|
||||
|
||||
await uut.getPeers(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return 200 status on success', async () => {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.adapters.ipfs, 'getPeers').resolves({ a: 'b' })
|
||||
|
||||
ctx.request.body = {
|
||||
showAll: true
|
||||
}
|
||||
|
||||
await uut.getPeers(ctx)
|
||||
// console.log('ctx.body: ', ctx.body)
|
||||
|
||||
assert.property(ctx.body, 'peers')
|
||||
assert.equal(ctx.body.peers.a, 'b')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#POST /relays', () => {
|
||||
it('should return 422 status on biz logic error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut.adapters.ipfs, 'getRelays').rejects(new Error('test error'))
|
||||
|
||||
await uut.getRelays(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return 200 status on success', async () => {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.adapters.ipfs, 'getRelays').resolves({ a: 'b' })
|
||||
|
||||
await uut.getRelays(ctx)
|
||||
// console.log('ctx.body: ', ctx.body)
|
||||
|
||||
assert.property(ctx.body, 'relays')
|
||||
assert.equal(ctx.body.relays.a, 'b')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#POST /connect', () => {
|
||||
it('should return 422 status on biz logic error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut.adapters.ipfs.ipfsCoordAdapter.ipfsCoord.adapters.ipfs, 'connectToPeer').rejects(new Error('test error'))
|
||||
|
||||
ctx.request.body = {
|
||||
multiaddr: '/ip4/161.35.99.207/tcp/4001/p2p/12D3KooWDtj9cfj1SKuLbDNKvKRKSsGN8qivq9M8CYpLPDpcD5pu'
|
||||
}
|
||||
|
||||
await uut.connect(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return 200 status on success', async () => {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.adapters.ipfs.ipfsCoordAdapter.ipfsCoord.adapters.ipfs, 'connectToPeer').resolves({ success: true })
|
||||
|
||||
ctx.request.body = {
|
||||
multiaddr: '/ip4/161.35.99.207/tcp/4001/p2p/12D3KooWDtj9cfj1SKuLbDNKvKRKSsGN8qivq9M8CYpLPDpcD5pu'
|
||||
}
|
||||
|
||||
await uut.connect(ctx)
|
||||
// console.log('ctx.body: ', ctx.body)
|
||||
|
||||
assert.property(ctx.body, 'success')
|
||||
assert.equal(ctx.body.success, true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#handleError', () => {
|
||||
it('should still throw error if there is no message', () => {
|
||||
try {
|
||||
const err = {
|
||||
status: 404
|
||||
}
|
||||
|
||||
uut.handleError(ctx, err)
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Not Found')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw error with message', () => {
|
||||
try {
|
||||
const err = {
|
||||
status: 422,
|
||||
message: 'test error'
|
||||
}
|
||||
|
||||
uut.handleError(ctx, err)
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getThisNode', () => {
|
||||
it('should return 422 status on biz logic error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
// sandbox.stub(uut.adapters.ipfs.ipfsCoordAdapter.ipfsCoord, 'thisNode').rejects(new Error('test error'))
|
||||
uut.adapters.ipfs.ipfsCoordAdapter = {}
|
||||
|
||||
ctx.request.body = {}
|
||||
|
||||
await uut.getThisNode(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'Cannot read')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return 200 status on success', async () => {
|
||||
// Mock dependencies
|
||||
// sandbox.stub(uut.adapters.ipfs.ipfsCoordAdapter.ipfsCoord.adapters.ipfs, 'connectToPeer').resolves({ success: true })
|
||||
|
||||
uut.adapters.ipfs.ipfsCoordAdapter = {
|
||||
ipfsCoord: {
|
||||
thisNode: {}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.request.body = {}
|
||||
|
||||
await uut.getThisNode(ctx)
|
||||
|
||||
assert.property(ctx.body, 'thisNode')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
Unit tests for the REST API handler for the /ipfs endpoints.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
import { assert } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
|
||||
// Local support libraries
|
||||
import adapters from '../../../mocks/adapters/index.js'
|
||||
import UseCasesMock from '../../../mocks/use-cases/index.js'
|
||||
import IpfsRouter from '../../../../../src/controllers/rest-api/ipfs/index.js'
|
||||
// const app = require('../../../mocks/app-mock')
|
||||
|
||||
let uut
|
||||
let sandbox
|
||||
// let ctx
|
||||
|
||||
// const mockContext = require('../../../../unit/mocks/ctx-mock').context
|
||||
|
||||
describe('#IPFS-REST-Router', () => {
|
||||
// const testUser = {}
|
||||
|
||||
beforeEach(() => {
|
||||
const useCases = new UseCasesMock()
|
||||
uut = new IpfsRouter({ adapters, useCases })
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Mock the context object.
|
||||
// ctx = mockContext()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should throw an error if adapters are not passed in', () => {
|
||||
try {
|
||||
uut = new IpfsRouter()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Adapters library required when instantiating IPFS REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if useCases are not passed in', () => {
|
||||
try {
|
||||
uut = new IpfsRouter({ adapters })
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Use Cases library required when instantiating IPFS REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#attach', () => {
|
||||
it('should throw an error if app is not passed in.', () => {
|
||||
try {
|
||||
uut.attach()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Must pass app object when attaching REST API controllers.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -38,9 +38,13 @@ describe('#config', () => {
|
||||
assert.equal(config.env, 'test')
|
||||
})
|
||||
|
||||
it('Should return test environment config', async () => {
|
||||
it('Should return prod environment config', async () => {
|
||||
process.env.SVC_ENV = 'prod'
|
||||
|
||||
process.env.WALLET_INTERFACE = 'web2'
|
||||
process.env.APISERVER = 'https://api.fullstack.cash/v5/'
|
||||
|
||||
await import('../../../config/env/common.js?foo=bar2')
|
||||
const importedConfig3 = await import('../../../config/index.js?foo=bar2')
|
||||
const config = importedConfig3.default
|
||||
// console.log('config: ', config)
|
||||
|
||||
@@ -16,6 +16,11 @@ class IpfsCoordAdapter {
|
||||
constructor () {
|
||||
|
||||
this.ipfsCoord = {
|
||||
adapters: {
|
||||
ipfs: {
|
||||
connectToPeer: async () => {}
|
||||
}
|
||||
},
|
||||
useCases: {
|
||||
peer: {
|
||||
sendPrivateMessage: () => {}
|
||||
@@ -36,7 +41,10 @@ class IpfsCoordAdapter {
|
||||
|
||||
const ipfs = {
|
||||
ipfsAdapter: new IpfsAdapter(),
|
||||
ipfsCoordAdapter: new IpfsCoordAdapter()
|
||||
ipfsCoordAdapter: new IpfsCoordAdapter(),
|
||||
getStatus: async () => {},
|
||||
getPeers: async () => {},
|
||||
getRelays: async () => {}
|
||||
}
|
||||
ipfs.ipfs = ipfs.ipfsAdapter.ipfs
|
||||
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import BCHJS from "@psf/bch-js";
|
||||
|
||||
const mockWallet = {
|
||||
mnemonic: 'course abstract aerobic deer try switch turtle diet fence affair butter top',
|
||||
privateKey: 'L5D2UAam8tvo3uii5kpgaGyjvVMimdrXu8nWGQSQjuuAix6ji1YQ',
|
||||
publicKey: '0379433ffc401483ade310469953c1cba77c71af904f07c15bde330d7198b4d6dc',
|
||||
cashAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
|
||||
address: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
|
||||
slpAddress: 'simpleledger:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acq9zm7d33',
|
||||
legacyAddress: '1JQj1KcQL7GPKzc1D2PvdUSgw3MbDtrHzi',
|
||||
hdPath: "m/44'/245'/0'/0/0",
|
||||
nextAddress: 1
|
||||
};
|
||||
|
||||
class MockBchWallet {
|
||||
constructor() {
|
||||
this.walletInfoPromise = true;
|
||||
this.walletInfo = mockWallet;
|
||||
this.initialize = async () => {}
|
||||
this.bchjs = new BCHJS();
|
||||
this.burnTokens = async () => {
|
||||
return { success: true, txid: 'txid' };
|
||||
};
|
||||
this.sendTokens = async () => {
|
||||
return 'fakeTxid';
|
||||
};
|
||||
this.getUtxos = async () => { };
|
||||
this.getBalance = async () => { };
|
||||
this.listTokens = async () => { };
|
||||
this.getTxData = async () => {
|
||||
return [{
|
||||
tokenTicker: 'TROUT'
|
||||
}];
|
||||
};
|
||||
this.optimize = async () => { };
|
||||
// Environment variable is used by wallet-balance.unit.js to force an error.
|
||||
if (process.env.NO_UTXO) {
|
||||
this.utxos = {};
|
||||
}
|
||||
else {
|
||||
this.utxos = {
|
||||
utxoStore: {
|
||||
address: 'bitcoincash:qqetvdnlt0p8g27dr44cx7h057kpzly9xse9huc97z',
|
||||
bchUtxos: [
|
||||
{
|
||||
height: 700685,
|
||||
tx_hash: '1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
|
||||
tx_pos: 0,
|
||||
value: 1000,
|
||||
txid: '1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
|
||||
vout: 0,
|
||||
isValid: false
|
||||
},
|
||||
{
|
||||
height: 700685,
|
||||
tx_hash: '1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
|
||||
tx_pos: 2,
|
||||
value: 19406,
|
||||
txid: '1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
|
||||
vout: 2,
|
||||
isValid: false
|
||||
}
|
||||
],
|
||||
nullUtxos: [],
|
||||
slpUtxos: {
|
||||
type1: {
|
||||
mintBatons: [],
|
||||
tokens: [
|
||||
{
|
||||
'height': 717331,
|
||||
'tx_hash': '74889580bb1a5f8c026aa2f55118ac9917df3332f7abae72a70343daa1c29621',
|
||||
'tx_pos': 1,
|
||||
'value': 546,
|
||||
'txid': '74889580bb1a5f8c026aa2f55118ac9917df3332f7abae72a70343daa1c29621',
|
||||
'vout': 1,
|
||||
'isSlp': true,
|
||||
'type': 'token',
|
||||
'qty': '10',
|
||||
'tokenId': '600ee24d0f208aebc2bdd2c4ee1b9acb6d57343561442e8676b5bbea311d5a0f',
|
||||
'address': 'bitcoincash:qqraj35x6l2qyqhjm5l7qlt7z2245ez8l5z3dwkeq5',
|
||||
'ticker': 'FLIPS',
|
||||
'name': 'FLIPS',
|
||||
'documentUri': '',
|
||||
'documentHash': '',
|
||||
'decimals': 1,
|
||||
'qtyStr': '1'
|
||||
},
|
||||
{
|
||||
'height': 730597,
|
||||
'tx_hash': '52520faddfafc46b8f8c9548b097f3a3b82a5bf363b5095047b9c5f83247fe36',
|
||||
'tx_pos': 1,
|
||||
'value': 546,
|
||||
'txid': '52520faddfafc46b8f8c9548b097f3a3b82a5bf363b5095047b9c5f83247fe36',
|
||||
'vout': 1,
|
||||
'isSlp': true,
|
||||
'type': 'token',
|
||||
'qty': '34999991',
|
||||
'tokenId': '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
|
||||
'address': 'bitcoincash:qqraj35x6l2qyqhjm5l7qlt7z2245ez8l5z3dwkeq5',
|
||||
'ticker': 'PSF',
|
||||
'name': 'Permissionless Software Foundation',
|
||||
'documentUri': 'psfoundation.cash',
|
||||
'documentHash': '',
|
||||
'decimals': 8,
|
||||
'qtyStr': '0.34999991'
|
||||
},
|
||||
{
|
||||
'height': 730597,
|
||||
'tx_hash': '5dc7e7c91382aed1666a51212dfb74050261e12c3c4f62b6b1e57f42d6c51ee1',
|
||||
'tx_pos': 2,
|
||||
'value': 546,
|
||||
'txid': '5dc7e7c91382aed1666a51212dfb74050261e12c3c4f62b6b1e57f42d6c51ee1',
|
||||
'vout': 2,
|
||||
'isSlp': true,
|
||||
'type': 'token',
|
||||
'qty': '18898',
|
||||
'tokenId': 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
|
||||
'address': 'bitcoincash:qqraj35x6l2qyqhjm5l7qlt7z2245ez8l5z3dwkeq5',
|
||||
'ticker': 'TROUT',
|
||||
'name': "Trout's test token",
|
||||
'documentUri': 'troutsblog.com',
|
||||
'documentHash': '',
|
||||
'decimals': 2,
|
||||
'qtyStr': '188.98'
|
||||
}
|
||||
]
|
||||
},
|
||||
nft: {
|
||||
tokens: []
|
||||
},
|
||||
group: {
|
||||
tokens: [],
|
||||
mintBatons: []
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { MockBchWallet };
|
||||
|
||||
export { mockWallet };
|
||||
|
||||
export default {
|
||||
MockBchWallet,
|
||||
mockWallet
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
Mocking library for helia.
|
||||
This is used to replace the helia library when running unit tests.
|
||||
*/
|
||||
|
||||
const ipfs = {
|
||||
libp2p: {
|
||||
getMultiaddrs: () => [],
|
||||
peerId: 'fake-id'
|
||||
}
|
||||
}
|
||||
|
||||
export default ipfs
|
||||
Reference in New Issue
Block a user