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 | |
|---|---|---|---|
|
|
ac451fd601 | ||
|
|
6857fe0166 | ||
|
|
89a03e561a | ||
|
|
22e32a6fc1 | ||
|
|
fcf5db7fa4 | ||
|
|
f8cf903fb9 | ||
|
|
c7986d1986 | ||
|
|
87ca75425b | ||
|
|
587114e827 | ||
|
|
c3a694546d | ||
|
|
591bd618e4 | ||
|
|
166f2de20e | ||
|
|
35fbc23fe0 | ||
|
|
c65d662630 | ||
|
|
1b99f8afa1 | ||
|
|
1bba636e43 | ||
|
|
c6f0a1317c | ||
|
|
fad5f8e23d | ||
|
|
ec6e0f5e46 | ||
|
|
f1caef2c0d | ||
|
|
55bf9424f9 | ||
|
|
aa100c97ed | ||
|
|
9fa5eafe95 | ||
|
|
4d9e948e30 | ||
|
|
e5cdf28e34 | ||
|
|
1d82b36105 | ||
|
|
b910cd1e6d | ||
|
|
536861ba99 | ||
|
|
ef030e3dc6 | ||
|
|
3da46c3d6f | ||
|
|
82f5552836 | ||
|
|
4d25681c51 | ||
|
|
8fe78037c1 | ||
|
|
33a447cd61 | ||
|
|
8a70d6a169 | ||
|
|
9ae5e890ae | ||
|
|
1bf28cbc21 | ||
|
|
d7bb6e4cb2 | ||
|
|
98a1631498 | ||
|
|
8dce8cbf8d | ||
|
|
f097ee024a | ||
|
|
7d21fe02f8 | ||
|
|
0b3049138b | ||
|
|
d7136583b8 | ||
|
|
173f706a61 | ||
|
|
39fc919d35 | ||
|
|
6f0726aeb8 | ||
|
|
2568818ea1 | ||
|
|
c721da1562 | ||
|
|
3606a2d31b | ||
|
|
764905b041 | ||
|
|
556c6ec522 | ||
|
|
04395b70a6 | ||
|
|
4564090830 | ||
|
|
81521151f4 | ||
|
|
5a819c1d9c | ||
|
|
f2f3208fda | ||
|
|
7fe888cdda | ||
|
|
db3f1bf2cf | ||
|
|
512a49200d | ||
|
|
c6019df364 | ||
|
|
b7988253df | ||
|
|
5f64257dca | ||
|
|
2fbdccb883 | ||
|
|
ecc10afac3 | ||
|
|
b55afd5f93 | ||
|
|
12037a5e57 | ||
|
|
4fc54aa9c5 | ||
|
|
2bd7f0c45c | ||
|
|
900423339e | ||
|
|
d907afa6bb | ||
|
|
9b116d82b7 | ||
|
|
1d1bf50ee3 | ||
|
|
5002004697 | ||
|
|
5b0fac4301 | ||
|
|
9584810b16 | ||
|
|
f311a3b9c6 | ||
|
|
83f16709d8 | ||
|
|
fe91f4b875 | ||
|
|
2d9a2b811c | ||
|
|
9360cf6f77 | ||
|
|
514b65ed24 | ||
|
|
0c7f3088a8 | ||
|
|
aef88eb369 | ||
|
|
2f6cc72f41 | ||
|
|
12cab02300 | ||
|
|
04a908261a | ||
|
|
f16eacc3f2 | ||
|
|
83898742f1 | ||
|
|
1795918719 | ||
|
|
6894217078 | ||
|
|
3ffbed27b5 | ||
|
|
c30b697bc6 | ||
|
|
df2f69050b | ||
|
|
b49979971c | ||
|
|
7a620892d7 | ||
|
|
b372e3d260 | ||
|
|
707b385b8a | ||
|
|
6cb140a491 | ||
|
|
c43ee8c25a | ||
|
|
0a7ec11510 | ||
|
|
4a1524629c | ||
|
|
f7aaba71d1 | ||
|
|
418a2e2ad2 | ||
|
|
74248ea0c7 | ||
|
|
70a69e98bb | ||
|
|
8cffd7fae3 | ||
|
|
a1023e8224 | ||
|
|
a32ed63d47 | ||
|
|
934fb46ffc | ||
|
|
b27136d03b | ||
|
|
064ac36dc7 | ||
|
|
5e2cf86f4b | ||
|
|
425d2ada65 | ||
|
|
9837d6a7fb | ||
|
|
60a3d67c0b | ||
|
|
a03c5d8c91 | ||
|
|
0e7afb5f51 | ||
|
|
bcc90da49b | ||
|
|
b473a3803c | ||
|
|
38ed9a8dac | ||
|
|
0ca46df43e | ||
|
|
b5f09fbab9 | ||
|
|
e3e928b33d | ||
|
|
9614f186b4 | ||
|
|
0390f2d956 |
+2
-1
@@ -62,7 +62,7 @@ docs
|
||||
.nyc_output
|
||||
coverage
|
||||
database/
|
||||
system-user-*.json
|
||||
#system-user-*.json
|
||||
orbitdb
|
||||
ipfsdata
|
||||
.ipfsdata
|
||||
@@ -73,3 +73,4 @@ run-dev.sh
|
||||
!README.md
|
||||
|
||||
wallet.json
|
||||
system-user-dev.json
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2021-2023 Permissionless Software Foundation
|
||||
Copyright (c) 2021-2024 Permissionless Software Foundation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
||||
+1
-1
@@ -1,3 +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.
|
||||
This repository was forked from [ipfs-service-provider](https://github.com/Permissionless-Software-Foundation/ipfs-service-provider). Code changes from that upstream repository is frequently pulled in and merged to this repository.
|
||||
|
||||
@@ -12,9 +12,9 @@ More details are available at [CashStack.info](https://CashStack.info).
|
||||
|
||||
## Requirements
|
||||
|
||||
- node **^16.20.2**
|
||||
- npm **^8.19.4**
|
||||
- Docker **^20.10.8**
|
||||
- node **^20.16.0**
|
||||
- npm **^10.8.1**
|
||||
- Docker **^24.0.7**
|
||||
- Docker Compose **^1.27.4**
|
||||
|
||||
## Installation
|
||||
|
||||
+26
-15
@@ -25,6 +25,7 @@ import config from '../config/index.js' // this first.
|
||||
|
||||
import AdminLib from '../src/adapters/admin.js'
|
||||
import errorMiddleware from '../src/controllers/rest-api/middleware/error.js'
|
||||
import { usageMiddleware } from '../src/use-cases/usage-use-cases.js'
|
||||
import wlogger from '../src/adapters/wlogger.js'
|
||||
import Controllers from '../src/controllers/index.js'
|
||||
import { applyPassportMods } from '../config/passport.js'
|
||||
@@ -45,16 +46,18 @@ class Server {
|
||||
const app = new Koa()
|
||||
app.keys = [this.config.session]
|
||||
|
||||
// Connect to the Mongo Database.
|
||||
this.mongoose.Promise = global.Promise
|
||||
this.mongoose.set('useCreateIndex', true) // Stop deprecation warning.
|
||||
console.log(
|
||||
`Connecting to MongoDB with this connection string: ${this.config.database}`
|
||||
)
|
||||
await this.mongoose.connect(this.config.database, {
|
||||
useUnifiedTopology: true,
|
||||
useNewUrlParser: true
|
||||
})
|
||||
if (!this.config.noMongo) {
|
||||
// Connect to the Mongo Database.
|
||||
this.mongoose.Promise = global.Promise
|
||||
this.mongoose.set('useCreateIndex', true) // Stop deprecation warning.
|
||||
console.log(
|
||||
`Connecting to MongoDB with this connection string: ${this.config.database}`
|
||||
)
|
||||
await this.mongoose.connect(this.config.database, {
|
||||
useUnifiedTopology: true,
|
||||
useNewUrlParser: true
|
||||
})
|
||||
}
|
||||
|
||||
console.log(`Starting environment: ${this.config.env}`)
|
||||
console.log(`Debug level: ${this.config.debugLevel}`)
|
||||
@@ -65,6 +68,7 @@ class Server {
|
||||
app.use(bodyParser())
|
||||
app.use(session())
|
||||
app.use(errorMiddleware())
|
||||
app.use(usageMiddleware())
|
||||
|
||||
// Used to generate the docs.
|
||||
app.use(mount('/', serve(`${process.cwd()}/docs`)))
|
||||
@@ -81,7 +85,10 @@ class Server {
|
||||
// Enable CORS for testing
|
||||
// THIS IS A SECURITY RISK. COMMENT OUT FOR PRODUCTION
|
||||
// Dev Note: This line must come BEFORE controllers.attachRESTControllers()
|
||||
app.use(cors({ origin: '*' }))
|
||||
app.use(cors({
|
||||
origin: '*',
|
||||
allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH,OPTIONS'
|
||||
}))
|
||||
|
||||
// Wait for any adapters to initialize.
|
||||
await this.controllers.initAdapters()
|
||||
@@ -102,9 +109,11 @@ class Server {
|
||||
this.server = await app.listen(this.config.port)
|
||||
console.log(`Server started on ${this.config.port}`)
|
||||
|
||||
// Create the system admin user.
|
||||
const success = await this.adminLib.createSystemUser()
|
||||
if (success) console.log('System admin user created.')
|
||||
if (!this.config.noMongo) {
|
||||
// Create the system admin user.
|
||||
const success = await this.adminLib.createSystemUser()
|
||||
if (success) console.log('System admin user created.')
|
||||
}
|
||||
|
||||
// Attach the other IPFS controllers.
|
||||
// Skip if this is a test environment.
|
||||
@@ -116,7 +125,9 @@ class Server {
|
||||
console.log('\nConfiguration:')
|
||||
console.log(`Circuit Relay: ${this.config.isCircuitRelay}`)
|
||||
console.log(`IPFS TCP port: ${this.config.ipfsTcpPort}`)
|
||||
console.log(`IPFS WS port: ${this.config.ipfsWsPort}\n`)
|
||||
console.log(`IPFS WS port: ${this.config.ipfsWsPort}`)
|
||||
console.log(`IPFS WebRTC port: ${this.config.ipfsWebRtcPort}`)
|
||||
console.log(`Connection preference: ${this.config.connectPref}\n`)
|
||||
|
||||
return app
|
||||
} catch (err) {
|
||||
|
||||
Vendored
+9
-1
@@ -43,6 +43,9 @@ export default {
|
||||
? process.env.EMAILPASS
|
||||
: 'emailpassword',
|
||||
|
||||
// Enable or Disable the usage of Mongo DB.
|
||||
noMongo: process.env.NO_MONGO ? true : false,
|
||||
|
||||
// BEGIN WALLET CONFIGURATION
|
||||
|
||||
// BCH Mnemonic for generating encryption keys and payment address
|
||||
@@ -119,12 +122,17 @@ export default {
|
||||
'@type': 'Organization',
|
||||
name: 'Permissionless Software Foundation',
|
||||
url: 'https://PSFoundation.cash'
|
||||
}
|
||||
},
|
||||
|
||||
// If this node has an IP4 address or domain name used to provide a REST API.
|
||||
web2Api: process.env.WEB2_API ? process.env.WEB2_API : null
|
||||
},
|
||||
|
||||
// IPFS Ports
|
||||
ipfsTcpPort: process.env.IPFS_TCP_PORT ? process.env.IPFS_TCP_PORT : 4101,
|
||||
ipfsWsPort: process.env.IPFS_WS_PORT ? process.env.IPFS_WS_PORT : 4103,
|
||||
ipfsWebRtcPort: process.env.IPFS_WEB_RTC_PORT ? process.env.IPFS_WEB_RTC_PORT : 4105,
|
||||
connectPref: process.env.CONNECT_PREF, // Used in helia-coord to select connection preference.
|
||||
|
||||
// Settings for production, using external go-ipfs node.
|
||||
isProduction: process.env.SVC_ENV === 'prod' ? true : false,
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"password": "YsFhsCVCqussWyyLYUjM",
|
||||
"email": "system@system.com",
|
||||
"id": "67ffbea61a8de3388f82c401",
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3ZmZiZWE2MWE4ZGUzMzg4ZjgyYzQwMSIsImlhdCI6MTc0NDgxMzczNH0.a4wM-CfGudF40yi4lwTsnd_aDZHrRn87zjHYxFZmeC4"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"password": "02PjvhoMGoM7pJylp80Z",
|
||||
"email": "system@system.com",
|
||||
"id": "645e49a45a9f5a8e0a54a083",
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY0NWU0OWE0NWE5ZjVhOGUwYTU0YTA4MyIsImlhdCI6MTY4MzkwMDgzNn0.sXeT-G9kS5Nu1dnZUGsNm6FhxrcNaCHhnQ-tLqsJc9E"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"password": "RCJqq5FjgYzvTXOyS1vo",
|
||||
"email": "system@system.com",
|
||||
"id": "645e59c19d49119bbb8329e7",
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY0NWU1OWMxOWQ0OTExOWJiYjgzMjllNyIsImlhdCI6MTY4MzkwNDk2MX0.2yi-hkY31nkgMm_S1QrPs4dIm5DBnJ4GsJEPiSPYHDA"
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
import Server from './bin/server.js'
|
||||
const server = new Server()
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.log(`Handling ${reason.code} error. stack: `, reason)
|
||||
})
|
||||
|
||||
server.startServer()
|
||||
|
||||
Generated
+7614
-7380
File diff suppressed because it is too large
Load Diff
+22
-17
@@ -24,24 +24,28 @@
|
||||
},
|
||||
"repository": "Permissionless-Software-Foundation/ipfs-bch-wallet-consumer",
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"@chainsafe/libp2p-gossipsub": "14.1.0",
|
||||
"@chainsafe/libp2p-noise": "16.0.1",
|
||||
"@chainsafe/libp2p-yamux": "7.0.1",
|
||||
"@helia/unixfs": "4.0.2",
|
||||
"@libp2p/bootstrap": "11.0.26",
|
||||
"@libp2p/circuit-relay-v2": "3.2.2",
|
||||
"@libp2p/config": "1.1.0",
|
||||
"@libp2p/identify": "3.0.22",
|
||||
"@libp2p/keychain": "5.0.14",
|
||||
"@libp2p/logger": "5.1.8",
|
||||
"@libp2p/ping": "2.0.22",
|
||||
"@libp2p/tcp": "10.1.2",
|
||||
"@libp2p/webrtc": "5.2.2",
|
||||
"@libp2p/websockets": "9.2.2",
|
||||
"@multiformats/multiaddr": "12.3.5",
|
||||
"axios": "0.27.2",
|
||||
"bcryptjs": "2.4.3",
|
||||
"blockstore-fs": "1.1.8",
|
||||
"datastore-fs": "9.1.6",
|
||||
"blockstore-fs": "2.0.2",
|
||||
"datastore-fs": "10.0.2",
|
||||
"glob": "7.1.6",
|
||||
"helia": "2.1.0",
|
||||
"helia-coord": "1.5.6",
|
||||
"helia": "5.2.1",
|
||||
"helia-coord": "1.7.2",
|
||||
"jsonrpc-lite": "2.2.0",
|
||||
"jsonwebtoken": "8.5.1",
|
||||
"jwt-bch-lib": "1.3.0",
|
||||
@@ -56,14 +60,15 @@
|
||||
"koa-router": "10.0.0",
|
||||
"koa-static": "5.0.0",
|
||||
"koa2-ratelimit": "0.9.1",
|
||||
"libp2p": "1.2.1",
|
||||
"libp2p": "2.7.2",
|
||||
"line-reader": "0.4.0",
|
||||
"mime-types": "2.1.35",
|
||||
"minimal-slp-wallet": "5.11.2",
|
||||
"minimal-slp-wallet": "5.13.2",
|
||||
"mongoose": "5.13.14",
|
||||
"node-fetch": "npm:@achingbrain/node-fetch@2.6.7",
|
||||
"nodemailer": "6.7.5",
|
||||
"passport-local": "1.0.0",
|
||||
"psffpp": "1.2.0",
|
||||
"public-ip": "6.0.1",
|
||||
"semver": "7.3.5",
|
||||
"winston": "3.3.3",
|
||||
|
||||
@@ -15,7 +15,7 @@ RUN apt-get install -y sudo git curl nano gnupg wget
|
||||
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 echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.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
|
||||
|
||||
@@ -66,11 +66,6 @@ RUN npm install
|
||||
# Generate the API docs
|
||||
RUN npm run docs
|
||||
|
||||
VOLUME /home/safeuser/keys
|
||||
|
||||
# Expose the port the API will be served on.
|
||||
#EXPOSE 5010
|
||||
|
||||
# Start the application.
|
||||
#COPY start-production.sh start-production.sh
|
||||
CMD ["./start-production.sh"]
|
||||
|
||||
@@ -22,13 +22,16 @@ services:
|
||||
options:
|
||||
max-size: '10m'
|
||||
max-file: '10'
|
||||
#mem_limit: 500mb
|
||||
mem_limit: 1000mb
|
||||
links:
|
||||
- mongo-bch-consumer
|
||||
ports:
|
||||
- '5015:5015' # <host port>:<container port>
|
||||
- '4101:4101'
|
||||
- '4103:4103'
|
||||
# <host port>:<container port>
|
||||
- '5015:5015' # REST API
|
||||
- '4101:4101' # TCP
|
||||
- '4103:4103' # Websockets
|
||||
- '4105:4105' # WebRTC
|
||||
|
||||
volumes:
|
||||
- ../data/ipfsdata:/home/safeuser/ipfs-bch-wallet-consumer/.ipfsdata
|
||||
- ./start-production.sh:/home/safeuser/ipfs-bch-wallet-consumer/start-production.sh
|
||||
|
||||
@@ -31,9 +31,15 @@ export IPFS_HOST=172.17.0.1
|
||||
export IPFS_API_PORT=5001
|
||||
export IPFS_TCP_PORT=4101
|
||||
export IPFS_WS_PORT=4103
|
||||
export IPFS_WEB_RTC_PORT=4105
|
||||
|
||||
# Set the debug level for helia-coord. 0-3.
|
||||
# 0 = no debug logs. 3 = maximum debug logs.
|
||||
export DEBUG_LEVEL=0
|
||||
|
||||
# Use this if the IPFS node has a publically accessible IP address.
|
||||
#export CONNECT_PREF=direct
|
||||
# Use this if the IPFS node is behind a NAT or Firewall and cannot be accessed directly.
|
||||
export CONNECT_PREF=cr
|
||||
|
||||
npm start
|
||||
|
||||
@@ -45,6 +45,10 @@ class IpfsFilesAdapter {
|
||||
this.rpcHandler = this.rpcHandler.bind(this)
|
||||
this.getStatus = this.getStatus.bind(this)
|
||||
this.selectProvider = this.selectProvider.bind(this)
|
||||
this.getFileMetadata = this.getFileMetadata.bind(this)
|
||||
this.getPins = this.getPins.bind(this)
|
||||
this.waitForRPCResponse = this.waitForRPCResponse.bind(this)
|
||||
this.pinClaim = this.pinClaim.bind(this)
|
||||
}
|
||||
|
||||
// This handler is triggered when RPC data comes in over IPFS.
|
||||
@@ -60,7 +64,7 @@ class IpfsFilesAdapter {
|
||||
this.rpcDataQueue.push(data)
|
||||
} catch (err) {
|
||||
console.error('Error in files/rpcHandler(): ', err)
|
||||
// Do not throw error. This is a top-level function.
|
||||
// Do not throw error. This is a top-level function.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +81,7 @@ class IpfsFilesAdapter {
|
||||
|
||||
// Add names to the IPFS IDs for each provider.
|
||||
const initialServiceProviders =
|
||||
this.ipfs.ipfsCoordAdapter.state.ipfsFileProviders
|
||||
this.ipfs.ipfsCoordAdapter.state.ipfsFileProviders
|
||||
const serviceProviders = []
|
||||
for (let i = 0; i < initialServiceProviders.length; i++) {
|
||||
const thisProvider = initialServiceProviders[i]
|
||||
@@ -120,7 +124,7 @@ class IpfsFilesAdapter {
|
||||
return true
|
||||
} catch (err) {
|
||||
// console.log('createUser() error: ', err)
|
||||
wlogger.error('Error in adapters/files/getStatus()')
|
||||
wlogger.error('Error in adapters/files/selectProvider()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -139,7 +143,7 @@ class IpfsFilesAdapter {
|
||||
// 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
|
||||
this.ipfs.ipfsCoordAdapter.state.selectedIpfsFileProvider
|
||||
if (!selectedProvider) {
|
||||
throw new Error('No IPFS File Pin Service provider available yet.')
|
||||
}
|
||||
@@ -176,6 +180,51 @@ class IpfsFilesAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
// Get an array of the latest 20 pinned files.
|
||||
async getPins (inObj = {}) {
|
||||
try {
|
||||
// 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 { page } = inObj
|
||||
|
||||
const rpcData = {
|
||||
endpoint: 'getPins',
|
||||
page
|
||||
}
|
||||
|
||||
// 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/getPins()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a promise that resolves to data when the RPC response is recieved.
|
||||
async waitForRPCResponse (rpcId) {
|
||||
try {
|
||||
@@ -216,7 +265,7 @@ class IpfsFilesAdapter {
|
||||
|
||||
cnt++
|
||||
|
||||
// Exit if data was returned, or the window for a response expires.
|
||||
// Exit if data was returned, or the window for a response expires.
|
||||
} while (!dataFound && cnt < 10)
|
||||
// console.log(`dataFound: ${dataFound}, cnt: ${cnt}`)
|
||||
|
||||
@@ -226,6 +275,53 @@ class IpfsFilesAdapter {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async pinClaim (inObj = {}) {
|
||||
try {
|
||||
// Throw an error if this IPFS node has not yet made a connection to a
|
||||
// ipfs service provider.
|
||||
const selectedProvider =
|
||||
this.ipfs.ipfsCoordAdapter.state.selectedIpfsFileProvider
|
||||
if (!selectedProvider) {
|
||||
throw new Error('No IPFS File Provider Service is available yet. Try again in a few seconds.')
|
||||
}
|
||||
|
||||
const { proofOfBurnTxid, cid, claimTxid, filename, address } = inObj
|
||||
|
||||
const rpcData = {
|
||||
endpoint: 'pinClaim',
|
||||
proofOfBurnTxid,
|
||||
cid,
|
||||
claimTxid,
|
||||
filename,
|
||||
address
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('Error in pinClaim()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// module.exports = P2wdbAdapter
|
||||
|
||||
@@ -20,7 +20,7 @@ import config from '../../../config/index.js'
|
||||
|
||||
// The minimum version of ipfs-bch-wallet-service that this wallet can work with.
|
||||
const MIN_BCH_WALLET_VERSION = '1.11.11'
|
||||
const WALLET_PROTOCOL = 'bch-wallet'
|
||||
const WALLET_PROTOCOL = 'bch-wallet-service'
|
||||
const MIN_P2WDB_VERSION = '1.4.0'
|
||||
const P2WDB_PROTOCOL = 'p2wdb'
|
||||
const MIN_FILE_PIN_VERSION = '1.0.0'
|
||||
|
||||
+84
-61
@@ -19,17 +19,20 @@ 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 { identify, identifyPush } from '@libp2p/identify'
|
||||
import { circuitRelayServer, circuitRelayTransport } from '@libp2p/circuit-relay-v2'
|
||||
// import { circuitRelayServer } 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 { webRTC, webRTCDirect } from '@libp2p/webrtc'
|
||||
import { keychain } from '@libp2p/keychain'
|
||||
import { defaultLogger } from '@libp2p/logger'
|
||||
import { unixfs } from '@helia/unixfs'
|
||||
// import { generateKeyPairFromSeed } from '@libp2p/crypto/keys'
|
||||
// import crypto from 'crypto'
|
||||
import { ping } from '@libp2p/ping'
|
||||
import { loadOrCreateSelfKey } from '@libp2p/config'
|
||||
|
||||
// Local libraries
|
||||
import config from '../../../config/index.js'
|
||||
@@ -64,8 +67,8 @@ class IpfsAdapter {
|
||||
this.createNode = this.createNode.bind(this)
|
||||
this.stop = this.stop.bind(this)
|
||||
this.ensureBlocksDir = this.ensureBlocksDir.bind(this)
|
||||
// this.getPrivateKey = this.getPrivateKey.bind(this)
|
||||
this.getSeed = this.getSeed.bind(this)
|
||||
this.getKeychain = this.getKeychain.bind(this)
|
||||
}
|
||||
|
||||
// Start an IPFS node.
|
||||
@@ -111,19 +114,6 @@ 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 () {
|
||||
@@ -132,83 +122,70 @@ class IpfsAdapter {
|
||||
const blockstore = new FsBlockstore(`${IPFS_DIR}/blockstore`)
|
||||
const datastore = new FsDatastore(`${IPFS_DIR}/datastore`)
|
||||
|
||||
// const keychainInit = {
|
||||
// pass: await this.getSeed()
|
||||
// }
|
||||
// Get the private key.
|
||||
// const privateKey = await this.getPrivateKey()
|
||||
// console.log('privateKey: ', privateKey)
|
||||
|
||||
// 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')
|
||||
const keychainInit = {
|
||||
selfKey: 'myKey',
|
||||
pass: await this.getSeed()
|
||||
}
|
||||
const privateKey = await loadOrCreateSelfKey(datastore, keychainInit)
|
||||
|
||||
// Configure services
|
||||
const services = {
|
||||
identify: identify(),
|
||||
pubsub: gossipsub({ allowPublishToZeroPeers: true })
|
||||
identifyPush: identifyPush(),
|
||||
pubsub: gossipsub({ allowPublishToZeroTopicPeers: true }),
|
||||
ping: ping(),
|
||||
keychain: keychain(keychainInit)
|
||||
}
|
||||
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
|
||||
}
|
||||
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()
|
||||
]
|
||||
}
|
||||
const transports = [
|
||||
tcp(),
|
||||
webSockets(),
|
||||
circuitRelayTransport({
|
||||
discoverRelays: 3,
|
||||
reservationConcurrency: 3
|
||||
}),
|
||||
webRTC(),
|
||||
webRTCDirect()
|
||||
]
|
||||
|
||||
// libp2p is the networking layer that underpins Helia
|
||||
const libp2p = await this.createLibp2p({
|
||||
peerId,
|
||||
// peerId,
|
||||
privateKey,
|
||||
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'
|
||||
'/webrtc',
|
||||
'/p2p-circuit'
|
||||
]
|
||||
},
|
||||
transports,
|
||||
connectionEncryption: [
|
||||
connectionEncrypters: [
|
||||
noise()
|
||||
],
|
||||
streamMuxers: [
|
||||
@@ -254,6 +231,10 @@ class IpfsAdapter {
|
||||
|
||||
!this.fs.existsSync(`${IPFS_DIR}/datastore`) && this.fs.mkdirSync(`${IPFS_DIR}/datastore`)
|
||||
|
||||
!this.fs.existsSync(`${IPFS_DIR}/datastore/pkcs8`) && this.fs.mkdirSync(`${IPFS_DIR}/datastore/pkcs8`)
|
||||
|
||||
// !fs.existsSync(`${IPFS_DIR}/datastore/peers`) && fs.mkdirSync(`${IPFS_DIR}/datastore/peers`)
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('Error in adapters/ipfs.js/ensureBlocksDir(): ', err)
|
||||
@@ -288,6 +269,48 @@ class IpfsAdapter {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Get the private key from disk, or generate a new one and save it,
|
||||
// if it doesn't exist.
|
||||
// async getPrivateKey () {
|
||||
// try {
|
||||
// const filename = `${IPFS_DIR}/privkey.json`
|
||||
|
||||
// let privKeyHex
|
||||
// try {
|
||||
// // Try to read the JSON file containing the seed.
|
||||
// privKeyHex = await this.jsonFiles.readJSON(filename)
|
||||
// // console.log('saved privKeyHex: ', privKeyHex)
|
||||
// } catch (err) {
|
||||
// // Generate a new private key and save it to disk.
|
||||
|
||||
// // Generate a new private key and save it to disk.
|
||||
// const randomBuffer = crypto.randomBytes(32)
|
||||
// // console.log('randomBuffer: ', randomBuffer)
|
||||
|
||||
// privKeyHex = randomBuffer.toString('hex')
|
||||
// // console.log('new privKeyHex: ', privKeyHex)
|
||||
|
||||
// // Save the newly generated seed
|
||||
// await this.jsonFiles.writeJSON(privKeyHex, filename)
|
||||
// }
|
||||
|
||||
// // Convert the saved hex string to a buffer.
|
||||
// const privKeyBuf = Buffer.from(privKeyHex, 'hex')
|
||||
|
||||
// // Convert the buffer to a Uint8Array 'seed'
|
||||
// const seed = new Uint8Array(privKeyBuf.buffer, privKeyBuf.byteOffset, privKeyBuf.byteLength)
|
||||
// // console.log('seed: ', seed)
|
||||
|
||||
// // Generate a ED25519 key pair.
|
||||
// const privKey = await generateKeyPairFromSeed('Ed25519', seed)
|
||||
|
||||
// return privKey
|
||||
// } catch (err) {
|
||||
// console.error('Error in adapters/ipfs/ipfs.js/getPrivateKey(): ', err)
|
||||
// throw err
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
export default IpfsAdapter
|
||||
|
||||
@@ -23,6 +23,13 @@ class Controllers {
|
||||
this.useCases = new UseCases(localConfig)
|
||||
this.timerControllers = new TimerControllers({ adapters: this.adapters, useCases: this.useCases })
|
||||
this.config = config
|
||||
|
||||
// Bind 'this' object to all subfunction
|
||||
this.initAdapters = this.initAdapters.bind(this)
|
||||
this.initUseCases = this.initUseCases.bind(this)
|
||||
this.attachRESTControllers = this.attachRESTControllers.bind(this)
|
||||
this.attachControllers = this.attachControllers.bind(this)
|
||||
this.attachRPCControllers = this.attachRPCControllers.bind(this)
|
||||
}
|
||||
|
||||
// Spin up any adapter libraries that have async startup needs.
|
||||
|
||||
@@ -15,6 +15,8 @@ import BchRESTController from './bch/index.js'
|
||||
import IpfsRESTController from './ipfs/index.js'
|
||||
import P2wdbRESTController from './p2wdb/index.js'
|
||||
import PriceRESTController from './price/index.js'
|
||||
import config from '../../../config/index.js'
|
||||
import UsageRESTController from './usage/index.js'
|
||||
|
||||
class RESTControllers {
|
||||
constructor (localConfig = {}) {
|
||||
@@ -32,7 +34,11 @@ class RESTControllers {
|
||||
)
|
||||
}
|
||||
|
||||
// console.log('Controllers localConfig: ', localConfig)
|
||||
// Bind 'this' object to all subfunctions.
|
||||
this.attachRESTControllers = this.attachRESTControllers.bind(this)
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.config = config
|
||||
}
|
||||
|
||||
attachRESTControllers (app) {
|
||||
@@ -41,13 +47,15 @@ class RESTControllers {
|
||||
useCases: this.useCases
|
||||
}
|
||||
|
||||
// Attach the REST API Controllers associated with the /auth route
|
||||
const authRESTController = new AuthRESTController(dependencies)
|
||||
authRESTController.attach(app)
|
||||
if (!this.config.noMongo) {
|
||||
// Attach the REST API Controllers associated with the /auth route
|
||||
const authRESTController = new AuthRESTController(dependencies)
|
||||
authRESTController.attach(app)
|
||||
|
||||
// Attach the REST API Controllers associated with the /user route
|
||||
const userRouter = new UserRouter(dependencies)
|
||||
userRouter.attach(app)
|
||||
// Attach the REST API Controllers associated with the /user route
|
||||
const userRouter = new UserRouter(dependencies)
|
||||
userRouter.attach(app)
|
||||
}
|
||||
|
||||
// Attach the REST API Controllers associated with the /contact route
|
||||
const contactRESTController = new ContactRESTController(dependencies)
|
||||
@@ -72,6 +80,10 @@ class RESTControllers {
|
||||
// Attach the REST API Controllers associated with the /price route
|
||||
const priceRESTController = new PriceRESTController(dependencies)
|
||||
priceRESTController.attach(app)
|
||||
|
||||
// Attach the REST API Controllers associated with the /usage route
|
||||
const usageRESTController = new UsageRESTController(dependencies)
|
||||
usageRESTController.attach(app)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,10 @@ class IpfsRESTControllerLib {
|
||||
this.viewFile = this.viewFile.bind(this)
|
||||
this.getService = this.getService.bind(this)
|
||||
this.getFileInfo = this.getFileInfo.bind(this)
|
||||
this.getPins = this.getPins.bind(this)
|
||||
this.cid2json = this.cid2json.bind(this)
|
||||
this.downloadFile = this.downloadFile.bind(this)
|
||||
this.pinClaim = this.pinClaim.bind(this)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,12 +167,34 @@ class IpfsRESTControllerLib {
|
||||
)
|
||||
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/download/:cid Download a file via its IPFS CID
|
||||
* @apiPermission public
|
||||
* @apiName GetCidDownload
|
||||
* @apiGroup REST IPFS
|
||||
* @apiDescription Download a file via its IPFS CID. Returns a readable stream.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5001/ipfs/download/bafkreieaqtdhfywyddomswogynzymukosqqgqo7lkt5lch2zwfnc55m6om
|
||||
*/
|
||||
async downloadFile (ctx) {
|
||||
try {
|
||||
const { cid } = ctx.params
|
||||
|
||||
const { readStream } = await this.useCases.ipfs.downloadCid({ cid })
|
||||
|
||||
ctx.body = readStream
|
||||
} catch (err) {
|
||||
console.log('Error in ipfs/controller.js/downloadFile(): ', err)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /ipfs/service Get the IPFS ID for the File Pin service
|
||||
* @apiPermission public
|
||||
@@ -190,7 +216,7 @@ class IpfsRESTControllerLib {
|
||||
selectedIpfsFileProvider
|
||||
}
|
||||
} catch (err) {
|
||||
// wlogger.error('Error in ipfs/controller.js/viewFile(): ', err)
|
||||
// wlogger.error('Error in ipfs/controller.js/getService(): ', err)
|
||||
console.log('Error in ipfs/controller.js/getService(): ', err)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
@@ -218,14 +244,91 @@ class IpfsRESTControllerLib {
|
||||
|
||||
ctx.body = metadata
|
||||
} catch (err) {
|
||||
// wlogger.error('Error in ipfs/controller.js/viewFile(): ', err)
|
||||
console.log('Error in ipfs/controller.js/getService(): ', err)
|
||||
console.log('Error in ipfs/controller.js/getFileInfo(): ', err)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /ipfs/pins Get info on latest 20 pinned files
|
||||
* @apiPermission public
|
||||
* @apiName GetPins
|
||||
* @apiGroup REST IPFS
|
||||
* @apiDescription Returns an array of the latest 20 pinned files.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5015/ipfs/pins
|
||||
*
|
||||
*/
|
||||
async getPins (ctx) {
|
||||
try {
|
||||
const { page } = ctx.params
|
||||
|
||||
const ipfsFiles = this.adapters.ipfsFiles
|
||||
|
||||
const pinData = await ipfsFiles.getPins({ page })
|
||||
console.log('getPins() pinData: ', pinData)
|
||||
|
||||
ctx.body = pinData
|
||||
} catch (err) {
|
||||
console.log('Error in ipfs/controller.js/getPins(): ', err)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /ipfs/cid2json/:cid Given a CID, retrieve a JSON object
|
||||
* @apiPermission public
|
||||
* @apiName GetCid2Json
|
||||
* @apiGroup REST IPFS
|
||||
* @apiDescription Given a CID, retrieve a JSON object.
|
||||
* If the CID does not resolves to a JSON file, then an error is thrown.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5015/ipfs/cid2json/bafkreigbgrvpagnmrqz2vhofifrqobigsxkdvnvikf5iqrkrbwrzirazhm
|
||||
*
|
||||
*/
|
||||
async cid2json (ctx) {
|
||||
try {
|
||||
const { cid } = ctx.params
|
||||
|
||||
const json = await this.useCases.ipfs.cid2json({ cid })
|
||||
|
||||
ctx.body = json
|
||||
} catch (err) {
|
||||
console.log('Error in ipfs/controller.js/cid2json(): ', err.message)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /ipfs/pin-claim Process pin claim.
|
||||
* @apiPermission public
|
||||
* @apiName PinClaim
|
||||
* @apiGroup REST IPFS
|
||||
* @apiDescription Process pin claim.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X POST -d '{ "proofOfBurnTxid": "be4b63156c93f58ed311d403d9f756deda9abbc81d0fef8fbe5d769538b4261c", "cid": "bafybeied3zdwdiro7fqytyha2yfband4lwcrtozmf6shynylt3kexh26dq", "claimTxid": "c71e2f2cdf8658d90c61ac6183b8ffeeb359779807b317386044705d8352f0f2", "filename": "mutable-67ccefcca67097473e78ca10.json", "address": "bitcoincash:qqs2wrahl6azn9qdyrmp9ygeejqvzr8ruv7e9m30fr" }' http://localhost:5001/ipfs/pin-claim *
|
||||
*/
|
||||
async pinClaim (ctx) {
|
||||
try {
|
||||
const body = ctx.request.body
|
||||
|
||||
const result = await this.useCases.ipfs.pinClaim(body)
|
||||
|
||||
ctx.body = result
|
||||
} catch (err) {
|
||||
console.log('Error in ipfs/controller.js/cid2json(): ', err.message)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// DRY error handler
|
||||
handleError (ctx, err) {
|
||||
// console.log('handleError() err.status: ', err.status)
|
||||
// console.log('handleError() err.message: ', err.message)
|
||||
|
||||
// If an HTTP status is specified by the buisiness logic, use that.
|
||||
if (err.status) {
|
||||
if (err.message) {
|
||||
@@ -234,6 +337,7 @@ class IpfsRESTControllerLib {
|
||||
ctx.throw(err.status)
|
||||
}
|
||||
} else {
|
||||
// console.log(`handleError() err.message: ${err.message}`)
|
||||
// By default use a 422 error if the HTTP status is not specified.
|
||||
ctx.throw(422, err.message)
|
||||
}
|
||||
|
||||
@@ -57,9 +57,12 @@ class IpfsRouter {
|
||||
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('/download/:cid', this.ipfsRESTController.downloadFile)
|
||||
this.router.get('/service', this.ipfsRESTController.getService)
|
||||
this.router.get('/file-info/:cid', this.ipfsRESTController.getFileInfo)
|
||||
|
||||
this.router.get('/pins/:page', this.ipfsRESTController.getPins)
|
||||
this.router.get('/cid2json/:cid', this.ipfsRESTController.cid2json)
|
||||
this.router.post('/pin-claim', this.ipfsRESTController.pinClaim)
|
||||
// Attach the Controller routes to the Koa app.
|
||||
app.use(this.router.routes())
|
||||
app.use(this.router.allowedMethods())
|
||||
|
||||
@@ -28,7 +28,10 @@ class PriceRESTControllerLib {
|
||||
// this.UserModel = this.adapters.localdb.Users
|
||||
// this.userUseCases = this.useCases.user
|
||||
|
||||
// _this = this
|
||||
// Bind 'this' object to all subfunctions
|
||||
this.getUSD = this.getUSD.bind(this)
|
||||
this.getXecPrice = this.getXecPrice.bind(this)
|
||||
this.getPsffppWritePrice = this.getPsffppWritePrice.bind(this)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,6 +92,22 @@ class PriceRESTControllerLib {
|
||||
}
|
||||
}
|
||||
|
||||
// Get the write price in PSF tokens to pin 1MB to the PSFFPP.
|
||||
// TODO: Add API docs.
|
||||
// curl -X GET http://localhost:5015/price/psffpp
|
||||
async getPsffppWritePrice (ctx) {
|
||||
try {
|
||||
const psfPrice = await this.useCases.ipfs.getWritePrice()
|
||||
|
||||
ctx.body = { psfPrice }
|
||||
} catch (err) {
|
||||
// Write out error to error log.
|
||||
wlogger.error('Error in GET /price/getPsffppWritePrice', err)
|
||||
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// DRY error handler
|
||||
handleError (ctx, err) {
|
||||
// If an HTTP status is specified by the buisiness logic, use that.
|
||||
|
||||
@@ -53,6 +53,7 @@ class PriceRouter {
|
||||
// Define the routes and attach the controller.
|
||||
this.router.get('/usd', this.getPrice)
|
||||
this.router.get('/xecusd', this.getXecPrice)
|
||||
this.router.get('/psffpp', this.getPsffppWritePrice)
|
||||
|
||||
// Attach the Controller routes to the Koa app.
|
||||
app.use(this.router.routes())
|
||||
@@ -66,6 +67,10 @@ class PriceRouter {
|
||||
async getXecPrice (ctx, next) {
|
||||
await _this.priceRESTController.getXecPrice(ctx, next)
|
||||
}
|
||||
|
||||
async getPsffppWritePrice (ctx, next) {
|
||||
await _this.priceRESTController.getPsffppWritePrice(ctx, next)
|
||||
}
|
||||
}
|
||||
|
||||
// module.exports = PriceRouter
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
REST API Controller library for the /usage route
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
|
||||
// Local libraries
|
||||
import wlogger from '../../../adapters/wlogger.js'
|
||||
|
||||
class UsageRESTControllerLib {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating /usage REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating /usage REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
|
||||
// Bind 'this' object to all subfunctions
|
||||
this.getStatus = this.getStatus.bind(this)
|
||||
this.getTopIps = this.getTopIps.bind(this)
|
||||
this.getTopEndpoints = this.getTopEndpoints.bind(this)
|
||||
this.handleError = this.handleError.bind(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /usage Get status on IPFS infrastructure
|
||||
* @apiPermission public
|
||||
* @apiName GetUsageStatus
|
||||
* @apiGroup REST Usage
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5020/usage
|
||||
*
|
||||
*/
|
||||
getStatus (ctx) {
|
||||
try {
|
||||
// const status = await this.adapters.ipfs.getStatus()
|
||||
const status = this.useCases.usage.getRestSummary()
|
||||
|
||||
ctx.body = { status }
|
||||
} catch (err) {
|
||||
wlogger.error('Error in usage/controller.js/getStatus(): ')
|
||||
// ctx.throw(422, err.message)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /usage/ips Get top IP addresses consuming the REST API
|
||||
* @apiPermission public
|
||||
* @apiName GetUsageIPs
|
||||
* @apiGroup REST Usage
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5020/usage/ips
|
||||
*
|
||||
*/
|
||||
getTopIps (ctx) {
|
||||
try {
|
||||
const ips = this.useCases.usage.getTopIps()
|
||||
|
||||
ctx.body = { ips }
|
||||
} catch (err) {
|
||||
wlogger.error('Error in usage/controller.js/getTopIps(): ')
|
||||
// ctx.throw(422, err.message)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /usage/endpoints Get top endpoints consumed from the REST API
|
||||
* @apiPermission public
|
||||
* @apiName GetUsageEndpoints
|
||||
* @apiGroup REST Usage
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5020/usage/endpoints
|
||||
*
|
||||
*/
|
||||
getTopEndpoints (ctx) {
|
||||
try {
|
||||
const endpoints = this.useCases.usage.getTopEndpoints()
|
||||
|
||||
ctx.body = { endpoints }
|
||||
} catch (err) {
|
||||
wlogger.error('Error in usage/controller.js/getTopEndpoints(): ')
|
||||
// ctx.throw(422, err.message)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// DRY error handler
|
||||
handleError (ctx, err) {
|
||||
// If an HTTP status is specified by the buisiness logic, use that.
|
||||
if (err.status) {
|
||||
if (err.message) {
|
||||
ctx.throw(err.status, err.message)
|
||||
} else {
|
||||
ctx.throw(err.status)
|
||||
}
|
||||
} else {
|
||||
// By default use a 422 error if the HTTP status is not specified.
|
||||
ctx.throw(422, err.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// module.exports = IpfsRESTControllerLib
|
||||
export default UsageRESTControllerLib
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
REST API library for the /usage route.
|
||||
*/
|
||||
|
||||
// Public npm libraries.
|
||||
import Router from 'koa-router'
|
||||
|
||||
// Local libraries.
|
||||
import UsageRESTControllerLib from './controller.js'
|
||||
import Validators from '../middleware/validators.js'
|
||||
|
||||
// let _this
|
||||
|
||||
class UsageRouter {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating IPFS REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating IPFS REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
const dependencies = {
|
||||
adapters: this.adapters,
|
||||
useCases: this.useCases
|
||||
}
|
||||
|
||||
// Encapsulate dependencies.
|
||||
this.usageRESTController = new UsageRESTControllerLib(dependencies)
|
||||
this.validators = new Validators()
|
||||
|
||||
// Instantiate the router and set the base route.
|
||||
const baseUrl = '/usage'
|
||||
this.router = new Router({ prefix: baseUrl })
|
||||
|
||||
// _this = this
|
||||
}
|
||||
|
||||
attach (app) {
|
||||
if (!app) {
|
||||
throw new Error(
|
||||
'Must pass app object when attaching REST API controllers.'
|
||||
)
|
||||
}
|
||||
|
||||
// Define the routes and attach the controller.
|
||||
this.router.get('/', this.usageRESTController.getStatus)
|
||||
this.router.get('/ips', this.usageRESTController.getTopIps)
|
||||
this.router.get('/endpoints', this.usageRESTController.getTopEndpoints)
|
||||
|
||||
// Attach the Controller routes to the Koa app.
|
||||
app.use(this.router.routes())
|
||||
app.use(this.router.allowedMethods())
|
||||
}
|
||||
}
|
||||
|
||||
// module.exports = BchRouter
|
||||
export default UsageRouter
|
||||
@@ -28,6 +28,7 @@ class TimerControllers {
|
||||
|
||||
// Bind 'this' object to all subfunctions.
|
||||
this.exampleTimerFunc = this.exampleTimerFunc.bind(this)
|
||||
this.cleanUsage = this.cleanUsage.bind(this)
|
||||
|
||||
// this.startTimers()
|
||||
}
|
||||
@@ -36,13 +37,15 @@ class TimerControllers {
|
||||
startTimers () {
|
||||
// Any new timer control functions can be added here. They will be started
|
||||
// when the server starts.
|
||||
this.optimizeWalletHandle = setInterval(this.exampleTimerFunc, 60000 * 10)
|
||||
this.optimizeWalletHandle = setInterval(this.exampleTimerFunc, 60000 * 60)
|
||||
this.cleanUsageHandle = setInterval(this.cleanUsage, 60000 * 60) // 1 hour
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
stopTimers () {
|
||||
clearInterval(this.optimizeWalletHandle)
|
||||
clearInterval(this.cleanusageHandle)
|
||||
}
|
||||
|
||||
// Replace this example function with your own timer handler.
|
||||
@@ -60,6 +63,20 @@ class TimerControllers {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Clean the usage state so that stats reflect the last 24 hours.
|
||||
cleanUsage () {
|
||||
try {
|
||||
this.useCases.usage.cleanUsage()
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('Error in time-controller.js/cleanUsage(): ', err)
|
||||
|
||||
// Note: Do not throw an error. This is a top-level function.
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default TimerControllers
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// Local libraries
|
||||
import UserUseCases from './user.js'
|
||||
import IpfsUseCases from './ipfs-use-cases.js'
|
||||
import { UsageUseCases } from './usage-use-cases.js'
|
||||
|
||||
class UseCases {
|
||||
constructor (localConfig = {}) {
|
||||
@@ -26,6 +27,7 @@ class UseCases {
|
||||
// console.log('use-cases/index.js localConfig: ', localConfig)
|
||||
this.user = new UserUseCases(localConfig)
|
||||
this.ipfs = new IpfsUseCases(localConfig)
|
||||
this.usage = new UsageUseCases(localConfig)
|
||||
}
|
||||
|
||||
// Run any startup Use Cases at the start of the app.
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
// import RetryQueue from '@chris.troutner/retry-queue'
|
||||
// import { exporter } from 'ipfs-unixfs-exporter'
|
||||
import { Duplex } from 'stream'
|
||||
import PSFFPP from 'psffpp'
|
||||
import { CID } from 'multiformats'
|
||||
|
||||
// Local libraries
|
||||
// import PinEntity from '../entities/pin.js'
|
||||
@@ -26,18 +28,26 @@ class IpfsUseCases {
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
// this.exporter = exporter
|
||||
this.psffpp = null // placeholder
|
||||
this.CID = CID
|
||||
|
||||
// Bind 'this' object to all class subfunctions.
|
||||
this.downloadCid = this.downloadCid.bind(this)
|
||||
// this.downloadCid2 = this.downloadCid2.bind(this)
|
||||
this.getWritePrice = this.getWritePrice.bind(this)
|
||||
this.getCidMetadata = this.getCidMetadata.bind(this)
|
||||
this.cid2json = this.cid2json.bind(this)
|
||||
this.pinClaim = this.pinClaim.bind(this)
|
||||
|
||||
// State
|
||||
this.lastWritePriceUpdate = null // Used to periodically update write price.
|
||||
}
|
||||
|
||||
// Download a pinned file, given its CID.
|
||||
// Returns a readable stream.
|
||||
async downloadCid (inObj = {}) {
|
||||
try {
|
||||
const { cid } = inObj
|
||||
let { cid } = inObj
|
||||
|
||||
if (!cid) throw new Error('CID is undefined')
|
||||
|
||||
@@ -58,6 +68,47 @@ class IpfsUseCases {
|
||||
|
||||
const helia = this.adapters.ipfs.ipfs
|
||||
|
||||
const cidClass = this.CID.parse(cid)
|
||||
|
||||
// Download the file from other nodes.
|
||||
await helia.blockstore.get(cidClass)
|
||||
|
||||
// list cid content, to dermine if this is a file-based or directory-based CID.
|
||||
const contentArray = []
|
||||
for await (const file of helia.fs.ls(cid)) {
|
||||
contentArray.push(file)
|
||||
}
|
||||
|
||||
// Handle directory-based CIDs. This is used by Token Tiger for mutable and immutable token data.
|
||||
/**
|
||||
* If the cid is a directory
|
||||
* The next block of code sends a html page with a list of links with the file names into the directory.
|
||||
* Skipping this code delivers directly the first file detected in the directory.
|
||||
*/
|
||||
const isDir = contentArray[0].path.match('/') // TODO : looking for a better way to detect if is a directory
|
||||
// 'listDir' is a flag to ignore this code on /download endpoint.
|
||||
if (isDir) {
|
||||
console.log('This CID is a directory. contentArray[0]: ', contentArray[0])
|
||||
cid = `${cid}/${contentArray[0].name}`
|
||||
|
||||
// throw new Error('CID is a directory. Not supported yet.')
|
||||
// const stream = new Stream.Readable({ read () { } })
|
||||
// for (let i = 0; i < contentArray.length; i++) {
|
||||
// const cont = contentArray[i]
|
||||
// // List all paths excluding root path.
|
||||
// if (cont.path !== cid) {
|
||||
// // Add links to the gateway with the format cid/:filename
|
||||
// stream.push(`<a href='${this.config.domainName}/ipfs/view/${cid}/${cont.name}' >/${cont.name} ( CID: ${cont.cid} )</a><hr />`)
|
||||
// }
|
||||
// }
|
||||
|
||||
// stream.push(null)
|
||||
// // return fileName as html because the controller the library <mime.lookup> sends it as html
|
||||
// return { readStream: stream }
|
||||
} else {
|
||||
console.log('This CID is not a directory.')
|
||||
}
|
||||
|
||||
// Convert the file to a Buffer.
|
||||
const fileChunks = []
|
||||
for await (const chunk of helia.fs.cat(cid)) {
|
||||
@@ -83,6 +134,43 @@ class IpfsUseCases {
|
||||
}
|
||||
}
|
||||
|
||||
// Get the price (in PSF tokens) to pin 1MB to the PSFFPP network. This price
|
||||
// is set on-chain by the PSF Minting Council. PSFoundation.cash
|
||||
async getWritePrice (inObj = {}) {
|
||||
try {
|
||||
const wallet = this.adapters.wallet.bchWallet
|
||||
// console.log('getWritePrice() wallet: ', wallet)
|
||||
|
||||
// Instantiate the psffpp library if it hasn't already been.
|
||||
// Dev Note: Important to only instantiate once, since the write price
|
||||
// is cached by the psffpp library. Instantiating only once improves
|
||||
// performance of price lookup.
|
||||
if (!this.psffpp) {
|
||||
this.psffpp = new PSFFPP({ wallet })
|
||||
|
||||
const now = new Date()
|
||||
this.lastWritePriceUpdate = now.getTime()
|
||||
}
|
||||
|
||||
// Periodically re-validate the write-price (by re-instantiating the
|
||||
// PSFFPP library) in case it's changed.
|
||||
const now = new Date()
|
||||
const sixHours = now.getTime() * 1000 * 60 * 60 * 6
|
||||
if (this.lastWritePriceUpdate + sixHours < now.getTime()) {
|
||||
this.psffpp = new PSFFPP({ wallet })
|
||||
|
||||
this.lastWritePriceUpdate = now.getTime()
|
||||
}
|
||||
|
||||
const writePrice = await this.psffpp.getMcWritePrice()
|
||||
|
||||
return writePrice
|
||||
} catch (err) {
|
||||
console.error('Error in use-cases/ipfs-use-cases.js/getWritePrice()')
|
||||
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
|
||||
@@ -96,6 +184,10 @@ class IpfsUseCases {
|
||||
const metadata = await ipfsFiles.getFileMetadata({ cid })
|
||||
console.log('getCidMetadata() metadata: ', metadata)
|
||||
|
||||
if (metadata.success === false) {
|
||||
throw new Error(metadata.message)
|
||||
}
|
||||
|
||||
const filename = metadata.fileMetadata.filename
|
||||
|
||||
return filename
|
||||
@@ -105,6 +197,107 @@ class IpfsUseCases {
|
||||
}
|
||||
}
|
||||
|
||||
// Given a CID, this function will retrieve a JSON object from the
|
||||
// ipfs-file-pin-service, using the IPFS JSON-RPC.
|
||||
async cid2json (inObj = {}) {
|
||||
try {
|
||||
const { cid } = inObj
|
||||
console.log('cid2json() cid: ', cid)
|
||||
|
||||
// Throw an error if ipfs-bch-wallet-consumer has not yet connected to an instance of ipfs-file-pin-service.
|
||||
const ipfsFileProvider = this.adapters.ipfs.ipfsCoordAdapter.state.selectedIpfsFileProvider
|
||||
if (!ipfsFileProvider) {
|
||||
throw new Error('No IPFS File Provider Service is available yet. Try again in a few seconds.')
|
||||
}
|
||||
|
||||
// Throw an error if ipfs-bch-wallet-consumer can not communicate with the ipfs-file-pin-service.
|
||||
const ipfsFiles = this.adapters.ipfsFiles
|
||||
const metadata = await ipfsFiles.getFileMetadata({ cid })
|
||||
if (metadata.success === false) {
|
||||
throw new Error(`Could not communicate with instance of ipfs-file-pin-service ${ipfsFileProvider}. Try again in a few seconds.`)
|
||||
}
|
||||
|
||||
// Throw an error if the file is not pinned.
|
||||
const fileMetadata = metadata.fileMetadata
|
||||
if (!fileMetadata.dataPinned) {
|
||||
throw new Error(`CID ${cid} has not been pinned by ipfs-file-pin-service instance ${ipfsFileProvider}`)
|
||||
}
|
||||
// console.log('fileMetadata: ', fileMetadata)
|
||||
|
||||
// Throw an error if this is not a JSON file
|
||||
const filename = fileMetadata.filename
|
||||
|
||||
if (!filename.endsWith('.json')) {
|
||||
throw new Error(`CID ${cid} does not resolve to a JSON file.`)
|
||||
}
|
||||
|
||||
// Retrieve the CID content and store it in a Buffer.
|
||||
const helia = this.adapters.ipfs.ipfs
|
||||
const fileChunks = []
|
||||
|
||||
// list cid content. This is used to determine if the CID is a file or a directory.
|
||||
const contentArray = []
|
||||
for await (const file of helia.fs.ls(cid)) {
|
||||
contentArray.push(file)
|
||||
}
|
||||
// console.log('contentArray: ', contentArray)
|
||||
|
||||
try {
|
||||
// Handle CIDs that are files.
|
||||
for await (const chunk of helia.fs.cat(cid)) {
|
||||
fileChunks.push(chunk)
|
||||
}
|
||||
} catch (err) {
|
||||
// Extra logic here to look at the filename of the first file and make sure
|
||||
// it ends in .json.
|
||||
const name = contentArray[0].name
|
||||
if (!name.endsWith('.json')) {
|
||||
throw new Error(`CID ${cid} is a directory and its first file does not resolve to a valid JSON file.`)
|
||||
}
|
||||
|
||||
// Handle CIDs that are directorys containing a JSON file. (TokenTiger.com tokens)
|
||||
for await (const chunk of helia.fs.cat(`${cid}/${name}`)) {
|
||||
fileChunks.push(chunk)
|
||||
}
|
||||
}
|
||||
|
||||
const fileBuf = Buffer.concat(fileChunks)
|
||||
|
||||
// Convert the Buffer into a string.
|
||||
const jsonStr = fileBuf.toString()
|
||||
|
||||
// Parse the string into a JSON object.
|
||||
let json = null
|
||||
try {
|
||||
json = JSON.parse(jsonStr)
|
||||
} catch (err) {
|
||||
throw new Error(`CID ${cid} does not resolve to a valid JSON object.`)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
json
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error in ipfs-use-cases.js/cid2json(): ', err.message)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async pinClaim (inObj = {}) {
|
||||
try {
|
||||
// Throw an error if ipfs-bch-wallet-consumer can not communicate with the ipfs-file-pin-service.
|
||||
const metadata = await this.adapters.ipfsFiles.pinClaim(inObj)
|
||||
|
||||
return {
|
||||
success: metadata.success,
|
||||
message: metadata.message
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error in ipfs-use-cases.js/pinClaim(): ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
// async downloadCid2 (inObj = {}) {
|
||||
// try {
|
||||
// const { cid } = inObj
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
Use Case library for tracking usage. This library contains business logic
|
||||
for tracking the usage of REST API and JSON RPC calls. This library is used
|
||||
by admins to keep an eye on how many API calls were made in a 24-hour and
|
||||
1-hour time period.
|
||||
*/
|
||||
|
||||
// This global variable is used to share data between the REST middleware and
|
||||
// the Usage Use Case class instance.
|
||||
let restCalls = []
|
||||
|
||||
class UsageUseCases {
|
||||
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 Usage Use Cases library.'
|
||||
)
|
||||
}
|
||||
|
||||
// Bind 'this' object to all subfunctions
|
||||
this.cleanUsage = this.cleanUsage.bind(this)
|
||||
this.getRestSummary = this.getRestSummary.bind(this)
|
||||
this.getTopIps = this.getTopIps.bind(this)
|
||||
this.getTopEndpoints = this.getTopEndpoints.bind(this)
|
||||
|
||||
// State
|
||||
}
|
||||
|
||||
// Clean up the state by removing entries that are older than 24 hours. This
|
||||
// ensures stats reflect only the last 24 hours.
|
||||
// This function is called by a Timer Controller.
|
||||
cleanUsage () {
|
||||
try {
|
||||
const now = new Date()
|
||||
const twentyFourHoursAgo = now.getTime() - (60000 * 60 * 24)
|
||||
|
||||
restCalls = restCalls.filter(x => x.timestamp > twentyFourHoursAgo)
|
||||
return restCalls
|
||||
} catch (err) {
|
||||
console.error('Error in usage-use-cases.js/cleanUsage()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Track the calls to a REST API
|
||||
getRestSummary (inObj = {}) {
|
||||
try {
|
||||
console.log(`getRestSummary(): There have been ${restCalls.length} REST calls`)
|
||||
|
||||
return restCalls.length
|
||||
} catch (err) {
|
||||
console.error('Error in usage-use-cases.js/getRestSummary()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Get the top 20 IP addresses from the stats.
|
||||
getTopIps () {
|
||||
try {
|
||||
const ips = restCalls.map(x => x.ip)
|
||||
// Create a Map to count occurrences of each IP address string
|
||||
const countMap = new Map()
|
||||
ips.forEach(ip => {
|
||||
countMap.set(ip, (countMap.get(ip) || 0) + 1)
|
||||
})
|
||||
|
||||
// Convert the Map into an array of objects with `str` and `cnt` properties
|
||||
const result = Array.from(countMap, ([ip, cnt]) => ({ ip, cnt }))
|
||||
|
||||
// Sort the results by the `cnt` property in descending order
|
||||
result.sort((a, b) => b.cnt - a.cnt)
|
||||
|
||||
// Ensure the result has at most 20 elements
|
||||
return result.slice(0, 20)
|
||||
} catch (err) {
|
||||
console.error('Error in usage-use-cases.js/getTopIps()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Get the top 20 most consumed endpoints.
|
||||
getTopEndpoints () {
|
||||
try {
|
||||
const endpoints = restCalls.map(x => `${x.method} ${x.url}`)
|
||||
|
||||
// Create a Map to count occurrences of each IP address string
|
||||
const countMap = new Map()
|
||||
endpoints.forEach(endpoint => {
|
||||
countMap.set(endpoint, (countMap.get(endpoint) || 0) + 1)
|
||||
})
|
||||
|
||||
// Convert the Map into an array of objects with `str` and `cnt` properties
|
||||
const result = Array.from(countMap, ([endpoint, cnt]) => ({ endpoint, cnt }))
|
||||
|
||||
// Sort the results by the `cnt` property in descending order
|
||||
result.sort((a, b) => b.cnt - a.cnt)
|
||||
|
||||
// Ensure the result has at most 20 elements
|
||||
return result.slice(0, 20)
|
||||
} catch (err) {
|
||||
console.error('Error in usage-use-cases.js/getTopEndpoints()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This Koa middleware is called any time there is a REST API. It logs the
|
||||
// details from the request object.
|
||||
function usageMiddleware () {
|
||||
return async (ctx, next) => {
|
||||
try {
|
||||
await next()
|
||||
|
||||
// console.log('ctx.request: ', ctx.request)
|
||||
const now = new Date()
|
||||
|
||||
const reqObj = {
|
||||
ip: ctx.request.ip,
|
||||
url: ctx.request.url,
|
||||
method: ctx.request.method,
|
||||
timestamp: now.getTime()
|
||||
}
|
||||
// console.log('reqObj: ', reqObj)
|
||||
|
||||
restCalls.push(reqObj)
|
||||
} catch (err) {
|
||||
ctx.status = err.status || 500
|
||||
ctx.body = err.message
|
||||
ctx.app.emit('error', err, ctx)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export { UsageUseCases, usageMiddleware, restCalls }
|
||||
@@ -13,7 +13,6 @@ import axios from 'axios'
|
||||
|
||||
// Local support libraries
|
||||
import config from '../../../config/index.js'
|
||||
|
||||
import Server from '../../../bin/server.js'
|
||||
import testUtils from '../../utils/test-utils.js'
|
||||
import AdminLib from '../../../src/adapters/admin.js'
|
||||
@@ -24,105 +23,107 @@ const context = {}
|
||||
|
||||
const LOCALHOST = `http://localhost:${config.port}`
|
||||
|
||||
describe('Auth', () => {
|
||||
before(async () => {
|
||||
const app = new Server()
|
||||
if (!config.noMongo) {
|
||||
describe('Auth', () => {
|
||||
before(async () => {
|
||||
const app = new Server()
|
||||
|
||||
// This should be the first instruction. It starts the REST API server.
|
||||
await app.startServer()
|
||||
// This should be the first instruction. It starts the REST API server.
|
||||
await app.startServer()
|
||||
|
||||
// Stop the IPFS node for the rest of the e2e tests.
|
||||
// await app.controllers.adapters.ipfs.stop()
|
||||
// Stop the IPFS node for the rest of the e2e tests.
|
||||
// await app.controllers.adapters.ipfs.stop()
|
||||
|
||||
// Delete all previous users in the database.
|
||||
await testUtils.deleteAllUsers()
|
||||
// Delete all previous users in the database.
|
||||
await testUtils.deleteAllUsers()
|
||||
|
||||
// Create a new admin user.
|
||||
await adminLib.createSystemUser()
|
||||
// Create a new admin user.
|
||||
await adminLib.createSystemUser()
|
||||
|
||||
const userObj = {
|
||||
email: 'test@test.com',
|
||||
password: 'pass',
|
||||
name: 'test'
|
||||
}
|
||||
const testUser = await testUtils.createUser(userObj)
|
||||
// console.log('TestUser: ', testUser)
|
||||
|
||||
context.user = testUser.user
|
||||
context.token = testUser.token
|
||||
})
|
||||
|
||||
describe('POST /auth', () => {
|
||||
it('should throw 401 if credentials are incorrect', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'test@test.com',
|
||||
password: 'wrongpassword'
|
||||
}
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
console.log(
|
||||
`result stringified: ${JSON.stringify(result.data, null, 2)}`
|
||||
)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert(err.response.status === 401, 'Error code 401 expected.')
|
||||
const userObj = {
|
||||
email: 'test@test.com',
|
||||
password: 'pass',
|
||||
name: 'test'
|
||||
}
|
||||
const testUser = await testUtils.createUser(userObj)
|
||||
// console.log('TestUser: ', testUser)
|
||||
|
||||
context.user = testUser.user
|
||||
context.token = testUser.token
|
||||
})
|
||||
|
||||
it('should throw 401 if email is wrong format', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'wrongEmail',
|
||||
password: 'wrongpassword'
|
||||
describe('POST /auth', () => {
|
||||
it('should throw 401 if credentials are incorrect', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'test@test.com',
|
||||
password: 'wrongpassword'
|
||||
}
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
console.log(
|
||||
`result stringified: ${JSON.stringify(result.data, null, 2)}`
|
||||
)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert(err.response.status === 401, 'Error code 401 expected.')
|
||||
}
|
||||
})
|
||||
|
||||
await axios(options)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert(err.response.status === 401, 'Error code 401 expected.')
|
||||
}
|
||||
})
|
||||
|
||||
it('should auth user', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'test@test.com',
|
||||
password: 'pass'
|
||||
it('should throw 401 if email is wrong format', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'wrongEmail',
|
||||
password: 'wrongpassword'
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
// console.log(`result: ${JSON.stringify(result.data, null, 2)}`)
|
||||
|
||||
assert(result.status === 200, 'Status Code 200 expected.')
|
||||
assert(
|
||||
result.data.user.email === 'test@test.com',
|
||||
'Email of test expected'
|
||||
)
|
||||
assert(
|
||||
result.data.user.password === undefined,
|
||||
'Password expected to be omited'
|
||||
)
|
||||
} catch (err) {
|
||||
console.log(
|
||||
'Error authenticating test user: ' + JSON.stringify(err, null, 2)
|
||||
)
|
||||
throw err
|
||||
}
|
||||
await axios(options)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert(err.response.status === 401, 'Error code 401 expected.')
|
||||
}
|
||||
})
|
||||
|
||||
it('should auth user', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'test@test.com',
|
||||
password: 'pass'
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
// console.log(`result: ${JSON.stringify(result.data, null, 2)}`)
|
||||
|
||||
assert(result.status === 200, 'Status Code 200 expected.')
|
||||
assert(
|
||||
result.data.user.email === 'test@test.com',
|
||||
'Email of test expected'
|
||||
)
|
||||
assert(
|
||||
result.data.user.password === undefined,
|
||||
'Password expected to be omited'
|
||||
)
|
||||
} catch (err) {
|
||||
console.log(
|
||||
'Error authenticating test user: ' + JSON.stringify(err, null, 2)
|
||||
)
|
||||
throw err
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,8 @@ import { assert } from 'chai'
|
||||
import Admin from '../../../src/adapters/admin.js'
|
||||
import sinon from 'sinon'
|
||||
import util from 'util'
|
||||
import config from '../../../config/index.js'
|
||||
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
let sandbox
|
||||
@@ -15,101 +17,103 @@ describe('Admin', () => {
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('loginAdmin()', () => {
|
||||
it('should logind admin', async () => {
|
||||
try {
|
||||
const error = new Error('test error')
|
||||
error.response = {
|
||||
status: 422
|
||||
if (!config.noMongo) {
|
||||
describe('loginAdmin()', () => {
|
||||
it('should logind admin', async () => {
|
||||
try {
|
||||
const error = new Error('test error')
|
||||
error.response = {
|
||||
status: 422
|
||||
}
|
||||
// sandbox.stub(uut.axios, 'request').onFirstCall().throws(error)
|
||||
|
||||
const result = await uut.loginAdmin()
|
||||
const user = result.data.user
|
||||
|
||||
assert.property(user, '_id')
|
||||
assert.property(user, 'email')
|
||||
assert.property(user, 'type')
|
||||
|
||||
assert.isString(user._id)
|
||||
assert.isString(user.email)
|
||||
assert.isString(user.type)
|
||||
|
||||
assert.equal(user.type, 'admin')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
// sandbox.stub(uut.axios, 'request').onFirstCall().throws(error)
|
||||
})
|
||||
|
||||
const result = await uut.loginAdmin()
|
||||
const user = result.data.user
|
||||
it('should handle axios error', async () => {
|
||||
try {
|
||||
// Returns an erroneous password to force
|
||||
// an auth error
|
||||
sandbox.stub(uut.jsonFiles, 'readJSON').resolves({ password: 'wrong' })
|
||||
|
||||
assert.property(user, '_id')
|
||||
assert.property(user, 'email')
|
||||
assert.property(user, 'type')
|
||||
|
||||
assert.isString(user._id)
|
||||
assert.isString(user.email)
|
||||
assert.isString(user.type)
|
||||
|
||||
assert.equal(user.type, 'admin')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle axios error', async () => {
|
||||
try {
|
||||
// Returns an erroneous password to force
|
||||
// an auth error
|
||||
sandbox.stub(uut.jsonFiles, 'readJSON').resolves({ password: 'wrong' })
|
||||
|
||||
await uut.loginAdmin()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
assert.include(err.response.data, 'Unauthorized')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('createSystemUser()', () => {
|
||||
it('should create admin', async () => {
|
||||
try {
|
||||
const result = await uut.createSystemUser()
|
||||
|
||||
assert.property(result, 'email')
|
||||
assert.property(result, 'password')
|
||||
assert.property(result, 'id')
|
||||
assert.property(result, 'token')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle axios error', async () => {
|
||||
try {
|
||||
const error1 = new Error('test error')
|
||||
error1.response = {
|
||||
status: 422
|
||||
await uut.loginAdmin()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
assert.include(err.response.data, 'Unauthorized')
|
||||
}
|
||||
const error2 = new Error('test error')
|
||||
error1.response = {
|
||||
status: 500
|
||||
}
|
||||
// The loginAdmin() function in some use cases is recursive
|
||||
// after handling the 422 error, it gets called again
|
||||
sandbox
|
||||
.stub(uut.axios, 'request')
|
||||
.onFirstCall()
|
||||
.throws(error1)
|
||||
.onSecondCall()
|
||||
.throws(error2)
|
||||
|
||||
await uut.createSystemUser()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle errors when remove user', async () => {
|
||||
try {
|
||||
const error1 = new Error('test error')
|
||||
error1.response = {
|
||||
status: 422
|
||||
}
|
||||
sandbox.stub(uut.axios, 'request').throws(error1)
|
||||
sandbox.stub(uut.User, 'deleteOne').throws(new Error('test error'))
|
||||
describe('createSystemUser()', () => {
|
||||
it('should create admin', async () => {
|
||||
try {
|
||||
const result = await uut.createSystemUser()
|
||||
|
||||
await uut.createSystemUser()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
assert.property(result, 'email')
|
||||
assert.property(result, 'password')
|
||||
assert.property(result, 'id')
|
||||
assert.property(result, 'token')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle axios error', async () => {
|
||||
try {
|
||||
const error1 = new Error('test error')
|
||||
error1.response = {
|
||||
status: 422
|
||||
}
|
||||
const error2 = new Error('test error')
|
||||
error1.response = {
|
||||
status: 500
|
||||
}
|
||||
// The loginAdmin() function in some use cases is recursive
|
||||
// after handling the 422 error, it gets called again
|
||||
sandbox
|
||||
.stub(uut.axios, 'request')
|
||||
.onFirstCall()
|
||||
.throws(error1)
|
||||
.onSecondCall()
|
||||
.throws(error2)
|
||||
|
||||
await uut.createSystemUser()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle errors when remove user', async () => {
|
||||
try {
|
||||
const error1 = new Error('test error')
|
||||
error1.response = {
|
||||
status: 422
|
||||
}
|
||||
sandbox.stub(uut.axios, 'request').throws(error1)
|
||||
sandbox.stub(uut.User, 'deleteOne').throws(new Error('test error'))
|
||||
|
||||
await uut.createSystemUser()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
End-to-end tests for /usage endpoints.
|
||||
*/
|
||||
|
||||
import config from '../../../config/index.js'
|
||||
import { assert } from 'chai'
|
||||
import axios from 'axios'
|
||||
import sinon from 'sinon'
|
||||
import util from 'util'
|
||||
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
const LOCALHOST = `http://localhost:${config.port}`
|
||||
|
||||
let sandbox
|
||||
|
||||
describe('Usage', () => {
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('GET /usage', () => {
|
||||
it('should return usage status', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'get',
|
||||
url: `${LOCALHOST}/usage`
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
|
||||
assert.property(result.data, 'status')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /usage/ips', () => {
|
||||
it('should return ips', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'get',
|
||||
url: `${LOCALHOST}/usage/ips`
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
|
||||
assert.property(result.data, 'ips')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /usage/endpoints', () => {
|
||||
it('should return ips', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'get',
|
||||
url: `${LOCALHOST}/usage/endpoints`
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
|
||||
assert.property(result.data, 'endpoints')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -6,7 +6,7 @@
|
||||
import { assert } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
import { peerIdFromString } from '@libp2p/peer-id'
|
||||
// import { peerIdFromString } from '@libp2p/peer-id'
|
||||
|
||||
// Local libraries
|
||||
import IPFSLib from '../../../src/adapters/ipfs/ipfs.js'
|
||||
@@ -165,17 +165,17 @@ describe('#IPFS-adapter', () => {
|
||||
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 () => {}
|
||||
})
|
||||
// const 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({})
|
||||
|
||||
|
||||
@@ -248,4 +248,247 @@ describe('#IPFS REST API', () => {
|
||||
assert.property(ctx.body, 'thisNode')
|
||||
})
|
||||
})
|
||||
describe('#viewFile', () => {
|
||||
it('should return 422 status on biz logic error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
ctx.params = {
|
||||
cid: 'bafybeib2rphswe7clvclverw7xi7nejkpkh4mdfcurdmcgw7wcup7za6wy'
|
||||
}
|
||||
sandbox.stub(uut.useCases.ipfs, 'downloadCid').throws(new Error('test error'))
|
||||
uut.adapters.ipfs.ipfsCoordAdapter = {}
|
||||
|
||||
ctx.request.body = {}
|
||||
|
||||
await uut.viewFile(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return 200 status on success', async () => {
|
||||
ctx.params = {
|
||||
cid: 'bafybeib2rphswe7clvclverw7xi7nejkpkh4mdfcurdmcgw7wcup7za6wy'
|
||||
}
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.useCases.ipfs, 'downloadCid').resolves({ success: true, readStream: 'readStream' })
|
||||
|
||||
uut.adapters.ipfs.ipfsCoordAdapter = {
|
||||
ipfsCoord: {
|
||||
thisNode: {}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.request.body = {}
|
||||
|
||||
await uut.viewFile(ctx)
|
||||
|
||||
assert.exists(ctx.body)
|
||||
})
|
||||
})
|
||||
describe('#downloadFile', () => {
|
||||
it('should return 422 status on biz logic error', async () => {
|
||||
try {
|
||||
ctx.params = {
|
||||
cid: 'bafybeib2rphswe7clvclverw7xi7nejkpkh4mdfcurdmcgw7wcup7za6wy'
|
||||
}
|
||||
// Force an error
|
||||
sandbox.stub(uut.useCases.ipfs, 'downloadCid').throws(new Error('test error'))
|
||||
uut.adapters.ipfs.ipfsCoordAdapter = {}
|
||||
|
||||
ctx.request.body = {}
|
||||
|
||||
await uut.downloadFile(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return 200 status on success', async () => {
|
||||
ctx.params = {
|
||||
cid: 'bafybeib2rphswe7clvclverw7xi7nejkpkh4mdfcurdmcgw7wcup7za6wy'
|
||||
}
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.useCases.ipfs, 'downloadCid').resolves({ success: true, readStream: 'readStream' })
|
||||
|
||||
uut.adapters.ipfs.ipfsCoordAdapter = {
|
||||
ipfsCoord: {
|
||||
thisNode: {}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.request.body = {}
|
||||
|
||||
await uut.downloadFile(ctx)
|
||||
|
||||
assert.exists(ctx.body)
|
||||
})
|
||||
})
|
||||
describe('#getService', () => {
|
||||
it('should return 422 status on biz logic error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
uut.adapters.ipfs.ipfsCoordAdapter = {}
|
||||
|
||||
ctx.request.body = {}
|
||||
|
||||
await uut.getService(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'Cannot read properties of undefined')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return 200 status on success', async () => {
|
||||
// Mock dependencies
|
||||
|
||||
uut.adapters.ipfs.ipfsCoordAdapter = {
|
||||
state: {
|
||||
selectedIpfsFileProvider: 'provider'
|
||||
}
|
||||
}
|
||||
|
||||
ctx.request.body = {}
|
||||
|
||||
await uut.getService(ctx)
|
||||
|
||||
assert.isTrue(ctx.body.success)
|
||||
})
|
||||
})
|
||||
describe('#getFileInfo', () => {
|
||||
it('should return 422 status on biz logic error', async () => {
|
||||
try {
|
||||
ctx.params = {
|
||||
cid: 'bafybeib2rphswe7clvclverw7xi7nejkpkh4mdfcurdmcgw7wcup7za6wy'
|
||||
}
|
||||
sandbox.stub(uut.adapters.ipfsFiles, 'getFileMetadata').throws(new Error('test error'))
|
||||
|
||||
ctx.request.body = {}
|
||||
|
||||
await uut.getFileInfo(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return 200 status on success', async () => {
|
||||
ctx.params = {
|
||||
cid: 'bafybeib2rphswe7clvclverw7xi7nejkpkh4mdfcurdmcgw7wcup7za6wy'
|
||||
}
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.adapters.ipfsFiles, 'getFileMetadata').resolves({ success: true, filename: 'filename' })
|
||||
|
||||
ctx.request.body = {}
|
||||
|
||||
await uut.getFileInfo(ctx)
|
||||
|
||||
assert.isTrue(ctx.body.success)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getPins', () => {
|
||||
it('should return 422 status on biz logic error', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.adapters.ipfsFiles, 'getPins').throws(new Error('test error'))
|
||||
|
||||
ctx.request.body = {}
|
||||
ctx.params = { page: 1 }
|
||||
|
||||
await uut.getPins(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.ipfsFiles, 'getPins').resolves({ success: true })
|
||||
|
||||
ctx.request.body = {}
|
||||
ctx.params = { page: 1 }
|
||||
|
||||
await uut.getPins(ctx)
|
||||
|
||||
assert.isTrue(ctx.body.success)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#cid2json', () => {
|
||||
it('should return 422 status on biz logic error', async () => {
|
||||
try {
|
||||
ctx.params = {
|
||||
cid: 'bafybeib2rphswe7clvclverw7xi7nejkpkh4mdfcurdmcgw7wcup7za6wy'
|
||||
}
|
||||
sandbox.stub(uut.useCases.ipfs, 'cid2json').throws(new Error('test error'))
|
||||
|
||||
ctx.request.body = {}
|
||||
|
||||
await uut.cid2json(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 () => {
|
||||
ctx.params = {
|
||||
cid: 'bafybeib2rphswe7clvclverw7xi7nejkpkh4mdfcurdmcgw7wcup7za6wy'
|
||||
}
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.useCases.ipfs, 'cid2json').resolves({ success: true })
|
||||
|
||||
ctx.request.body = {}
|
||||
|
||||
await uut.cid2json(ctx)
|
||||
|
||||
assert.isTrue(ctx.body.success)
|
||||
})
|
||||
})
|
||||
describe('#pinClaim', () => {
|
||||
it('should return 422 status on biz logic error', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.useCases.ipfs, 'pinClaim').throws(new Error('test error'))
|
||||
|
||||
ctx.request.body = {}
|
||||
|
||||
await uut.pinClaim(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.useCases.ipfs, 'pinClaim').resolves({ success: true })
|
||||
|
||||
ctx.request.body = {}
|
||||
|
||||
await uut.pinClaim(ctx)
|
||||
|
||||
assert.isTrue(ctx.body.success)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
Unit tests for the REST API middleware that handle response errors.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
import { assert } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
|
||||
// Local libraries
|
||||
import errorMiddleware from '../../../../../src/controllers/rest-api/middleware/error.js'
|
||||
import { context as mockContext } from '../../../../unit/mocks/ctx-mock.js'
|
||||
|
||||
describe('#Validators', () => {
|
||||
let ctx
|
||||
let sandbox
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock the context object.
|
||||
ctx = mockContext()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#errorMiddleware', () => {
|
||||
it('should run next function', async () => {
|
||||
// Spy on next
|
||||
const next = sinon.spy(() => { })
|
||||
errorMiddleware()(ctx, next)
|
||||
|
||||
assert.isTrue(next.calledOnce)
|
||||
})
|
||||
|
||||
it('should handle unknown status error', async () => {
|
||||
try {
|
||||
const next = async () => {
|
||||
const e = new Error('test error')
|
||||
e.status = null
|
||||
throw e
|
||||
}
|
||||
|
||||
await errorMiddleware()(ctx, next)
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(ctx.status, 500)
|
||||
assert.equal(ctx.body, 'test error')
|
||||
}
|
||||
})
|
||||
it('should handle known status error', async () => {
|
||||
try {
|
||||
const next = async () => {
|
||||
const e = new Error('test error')
|
||||
e.status = 422
|
||||
throw e
|
||||
}
|
||||
|
||||
await errorMiddleware()(ctx, next)
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(ctx.status, 422)
|
||||
assert.equal(ctx.body, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
Unit tests for the REST API handler for the /usage 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 UsageController from '../../../../../src/controllers/rest-api/usage/controller.js'
|
||||
|
||||
import { context as mockContext } from '../../../mocks/ctx-mock.js'
|
||||
|
||||
let uut
|
||||
let sandbox
|
||||
let ctx
|
||||
|
||||
describe('#Usage-REST-Controller', () => {
|
||||
// const testUser = {}
|
||||
|
||||
beforeEach(() => {
|
||||
const useCases = new UseCasesMock()
|
||||
uut = new UsageController({ 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 UsageController()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Adapters library required when instantiating /usage REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if useCases are not passed in', () => {
|
||||
try {
|
||||
uut = new UsageController({ adapters })
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Use Cases library required when instantiating /usage REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#Get /usage', () => {
|
||||
it('should return 422 status on biz logic error', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.useCases.usage, 'getRestSummary').throws(new Error('test error'))
|
||||
await uut.getStatus(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return 200 status on success', async () => {
|
||||
await uut.getStatus(ctx)
|
||||
|
||||
// Assert the expected HTTP response
|
||||
assert.equal(ctx.status, 200)
|
||||
|
||||
// Assert that expected properties exist in the returned data.
|
||||
assert.property(ctx.response.body, 'status')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#Get /ips', () => {
|
||||
it('should return 422 status on biz logic error', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.useCases.usage, 'getTopIps').throws(new Error('test error'))
|
||||
await uut.getTopIps(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return 200 status on success', async () => {
|
||||
await uut.getTopIps(ctx)
|
||||
|
||||
// Assert the expected HTTP response
|
||||
assert.equal(ctx.status, 200)
|
||||
|
||||
// Assert that expected properties exist in the returned data.
|
||||
assert.property(ctx.response.body, 'ips')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#Get /endpoints', () => {
|
||||
it('should return 422 status on biz logic error', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.useCases.usage, 'getTopEndpoints').throws(new Error('test error'))
|
||||
await uut.getTopEndpoints(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return 200 status on success', async () => {
|
||||
await uut.getTopEndpoints(ctx)
|
||||
|
||||
// Assert the expected HTTP response
|
||||
assert.equal(ctx.status, 200)
|
||||
|
||||
// Assert that expected properties exist in the returned data.
|
||||
assert.property(ctx.response.body, 'endpoints')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#handleError', () => {
|
||||
it('should pass an error message', () => {
|
||||
try {
|
||||
const err = {
|
||||
status: 422,
|
||||
message: 'Unprocessable Entity'
|
||||
}
|
||||
|
||||
uut.handleError(ctx, err)
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Unprocessable Entity')
|
||||
}
|
||||
})
|
||||
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')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
Unit tests for the REST API handler for the /usage 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 UsageRouter from '../../../../../src/controllers/rest-api/usage/index.js'
|
||||
|
||||
let uut
|
||||
let sandbox
|
||||
// let ctx
|
||||
|
||||
// const mockContext = require('../../../../unit/mocks/ctx-mock').context
|
||||
|
||||
describe('#Usage-REST-Router', () => {
|
||||
beforeEach(() => {
|
||||
const useCases = new UseCasesMock()
|
||||
uut = new UsageRouter({ adapters, useCases })
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should throw an error if adapters are not passed in', () => {
|
||||
try {
|
||||
uut = new UsageRouter()
|
||||
|
||||
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 UsageRouter({ 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.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -79,4 +79,19 @@ describe('#Timer-Controllers', () => {
|
||||
assert.equal(result, false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#cleanUsage', () => {
|
||||
it('should kick off the Use Case', async () => {
|
||||
const result = await uut.cleanUsage()
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
|
||||
it('should return false on error', async () => {
|
||||
sandbox.stub(uut.useCases.usage, 'cleanUsage').throws(new Error('test error'))
|
||||
const result = await uut.cleanUsage()
|
||||
|
||||
assert.equal(result, false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
/*
|
||||
Mocks for the Adapter library.
|
||||
*/
|
||||
|
||||
class BlockstoreMock {
|
||||
constructor () {
|
||||
this.get = async () => {}
|
||||
}
|
||||
}
|
||||
class FsMock {
|
||||
constructor () {
|
||||
this.ls = async function* () {
|
||||
yield { path: 'test.txt', cid: 'QmS4ghgMgfFvqPjB4WKXHaN15ZyT4K4JYZxY5X5x5x5x5' }
|
||||
}
|
||||
this.cat = async function* () {
|
||||
yield Buffer.from('test data')
|
||||
}
|
||||
}
|
||||
}
|
||||
class IpfsAdapter {
|
||||
constructor () {
|
||||
this.ipfs = {
|
||||
blockstore: new BlockstoreMock(),
|
||||
fs: new FsMock(),
|
||||
files: {
|
||||
stat: () => {}
|
||||
}
|
||||
@@ -34,7 +50,9 @@ class IpfsCoordAdapter {
|
||||
this.peerInputHandler = () => {}
|
||||
|
||||
this.state = {
|
||||
serviceProviders: []
|
||||
serviceProviders: [],
|
||||
serviceProvidersByType: '',
|
||||
selectedIpfsFileProvider:''
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,5 +156,29 @@ class BchUseCaseMock {
|
||||
}
|
||||
}
|
||||
const bch = new BchUseCaseMock()
|
||||
const wallet = {
|
||||
bchWallet: bch
|
||||
|
||||
export default { ipfs, localdb, bch }
|
||||
}
|
||||
|
||||
const ipfsFiles = {
|
||||
pinClaim: () => {
|
||||
return {
|
||||
success: true,
|
||||
message: 'Pin claimed'
|
||||
}
|
||||
},
|
||||
getFileMetadata: () => {
|
||||
return {
|
||||
success: true,
|
||||
message: 'Files metadata'
|
||||
}
|
||||
},
|
||||
getPins: () => {
|
||||
return {
|
||||
success: true,
|
||||
message: 'Pins'
|
||||
}
|
||||
}
|
||||
}
|
||||
export default { ipfs, localdb, bch, wallet, ipfsFiles }
|
||||
|
||||
@@ -47,7 +47,7 @@ const peerData = [
|
||||
'@type': 'WebAPI',
|
||||
name: 'trout-bch-wallet-service-dev',
|
||||
version: '1.11.12',
|
||||
protocol: 'bch-wallet',
|
||||
protocol: 'bch-wallet-service',
|
||||
description:
|
||||
'IPFS service providing BCH blockchain access needed by a wallet.',
|
||||
documentation: 'https://ipfs-bch-wallet-service.fullstack.cash/',
|
||||
|
||||
@@ -49,6 +49,46 @@ class BchUseCaseMock {
|
||||
}
|
||||
}
|
||||
|
||||
class UsageUseCaseMock {
|
||||
async cleanUsage() {
|
||||
return {}
|
||||
}
|
||||
|
||||
async getRestSummary() {
|
||||
return true
|
||||
}
|
||||
|
||||
async getTopIps(params) {
|
||||
return true
|
||||
}
|
||||
|
||||
async getTopEndpoints(existingUser, newData) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
class IpfsUseCaseMock {
|
||||
async downloadCid() {
|
||||
return {}
|
||||
}
|
||||
|
||||
async cid2json() {
|
||||
return {}
|
||||
}
|
||||
|
||||
async getCidMetadata() {
|
||||
return {}
|
||||
}
|
||||
|
||||
async getService() {
|
||||
return {}
|
||||
}
|
||||
|
||||
async pinClaim() {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
class UseCasesMock {
|
||||
constuctor(localConfig = {}) {
|
||||
// this.user = new UserUseCaseMock(localConfig)
|
||||
@@ -58,6 +98,8 @@ class UseCasesMock {
|
||||
|
||||
user = new UserUseCaseMock()
|
||||
bch = new BchUseCaseMock()
|
||||
usage = new UsageUseCaseMock()
|
||||
ipfs = new IpfsUseCaseMock()
|
||||
}
|
||||
|
||||
export default UseCasesMock;
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
Unit tests for the ipfs-use-cases.js file.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
import { assert } from 'chai'
|
||||
|
||||
import sinon from 'sinon'
|
||||
|
||||
// Local support libraries
|
||||
// const testUtils = require('../../utils/test-utils')
|
||||
|
||||
// Unit under test (uut)
|
||||
import UseCases from '../../../src/use-cases/ipfs-use-cases.js'
|
||||
|
||||
import adapters from '../mocks/adapters/index.js'
|
||||
|
||||
describe('#ipfs-use-cases', () => {
|
||||
let uut
|
||||
let sandbox
|
||||
|
||||
before(async () => {
|
||||
// Delete all previous users in the database.
|
||||
// await testUtils.deleteAllUsers()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
uut = new UseCases({ adapters })
|
||||
uut.adapters.ipfs.ipfsCoordAdapter = {
|
||||
state: {
|
||||
selectedIpfsFileProvider: 'provider'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should throw an error if adapters are not passed in', () => {
|
||||
try {
|
||||
uut = new UseCases()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
|
||||
// This is here to prevent the linter from complaining.
|
||||
assert.isOk(uut)
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of adapters must be passed in when instantiating IPFS Use Cases library.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#downloadCid', () => {
|
||||
it('should download a file from IPFS', async () => {
|
||||
sandbox.stub(uut, 'getCidMetadata').resolves('file.txt')
|
||||
|
||||
const result = await uut.downloadCid({ cid: 'bafybeib2rphswe7clvclverw7xi7nejkpkh4mdfcurdmcgw7wcup7za6wy' })
|
||||
|
||||
assert.property(result, 'filename')
|
||||
assert.property(result, 'readStream')
|
||||
})
|
||||
it('should handle directory-based CIDs', async () => {
|
||||
sandbox.stub(uut, 'getCidMetadata').resolves('file.txt')
|
||||
sandbox.stub(uut.adapters.ipfs.ipfs.fs, 'ls').callsFake(async function * () {
|
||||
yield { path: 'dir1/test.txt', cid: 'QmS4ghgMgfFvqPjB4WKXHaN15ZyT4K4JYZxY5X5x5x5x5' }
|
||||
})
|
||||
const result = await uut.downloadCid({ cid: 'bafybeib2rphswe7clvclverw7xi7nejkpkh4mdfcurdmcgw7wcup7za6wy' })
|
||||
|
||||
assert.property(result, 'filename')
|
||||
assert.property(result, 'readStream')
|
||||
})
|
||||
it('should throw an error if cid is not provided ', async () => {
|
||||
try {
|
||||
await uut.downloadCid({})
|
||||
} catch (error) {
|
||||
assert.include(error.message, 'CID is undefined')
|
||||
}
|
||||
})
|
||||
})
|
||||
describe('#getWritePrice', () => {
|
||||
it('should get price', async () => {
|
||||
const result = await uut.getWritePrice()
|
||||
assert.isNumber(result)
|
||||
})
|
||||
it('should handle undefined psffpp instance', async () => {
|
||||
uut.psffpp = null
|
||||
const result = await uut.getWritePrice()
|
||||
assert.isNumber(result)
|
||||
})
|
||||
|
||||
it('should handle error ', async () => {
|
||||
try {
|
||||
uut.psffpp = {
|
||||
getMcWritePrice: () => {
|
||||
throw new Error('test error')
|
||||
}
|
||||
}
|
||||
await uut.getWritePrice()
|
||||
} catch (error) {
|
||||
assert.include(error.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
describe('#getCidMetadata', () => {
|
||||
it('should get cid metadata', async () => {
|
||||
sandbox.stub(uut.adapters.ipfsFiles, 'getFileMetadata').resolves({ success: true, fileMetadata: { dataPinned: true, filename: 'test.json' } })
|
||||
const result = await uut.getCidMetadata({ cid: 'bafybeib2rphswe7clvclverw7xi7nejkpkh4mdfcurdmcgw7wcup7za6wy' })
|
||||
assert.isString(result)
|
||||
assert.equal(result, 'test.json')
|
||||
})
|
||||
|
||||
it('should handle error ', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.adapters.ipfsFiles, 'getFileMetadata').throws(new Error('test error'))
|
||||
await uut.getCidMetadata({ cid: 'bafybeib2rphswe7clvclverw7xi7nejkpkh4mdfcurdmcgw7wcup7za6wy' })
|
||||
} catch (error) {
|
||||
assert.include(error.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle non-success response from getFileMetadata', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.adapters.ipfsFiles, 'getFileMetadata').resolves({ success: false, message: 'test error' })
|
||||
await uut.getCidMetadata({ cid: 'bafybeib2rphswe7clvclverw7xi7nejkpkh4mdfcurdmcgw7wcup7za6wy' })
|
||||
} catch (error) {
|
||||
assert.include(error.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
describe('#cid2json', () => {
|
||||
it('should get cid2json', async () => {
|
||||
uut.adapters.ipfs.ipfsCoordAdapter = {
|
||||
state: {
|
||||
selectedIpfsFileProvider: 'provider'
|
||||
}
|
||||
}
|
||||
sandbox.stub(uut.adapters.ipfsFiles, 'getFileMetadata').resolves({ fileMetadata: { dataPinned: true, filename: 'test.json' } })
|
||||
sandbox.stub(uut.adapters.ipfs.ipfs.fs, 'ls').callsFake(function * () {
|
||||
yield { name: 'test.json' }
|
||||
})
|
||||
sandbox.stub(JSON, 'parse').returns(true)
|
||||
const result = await uut.cid2json({ cid: 'bafybeib2rphswe7clvclverw7xi7nejkpkh4mdfcurdmcgw7wcup7za6wy' })
|
||||
assert.isObject(result)
|
||||
assert.property(result, 'success')
|
||||
assert.property(result, 'json')
|
||||
})
|
||||
|
||||
it('should throw error if selectedIpfsFileProvider is not defined', async () => {
|
||||
try {
|
||||
uut.adapters.ipfs.ipfsCoordAdapter = {
|
||||
state: {
|
||||
selectedIpfsFileProvider: null
|
||||
}
|
||||
}
|
||||
await uut.cid2json({ cid: 'bafybeib2rphswe7clvclverw7xi7nejkpkh4mdfcurdmcgw7wcup7za6wy' })
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.include(error.message, 'No IPFS File Provider Service is available yet. Try again in a few seconds')
|
||||
}
|
||||
})
|
||||
it('should throw error if file metadata cant be fetched', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.adapters.ipfsFiles, 'getFileMetadata').resolves({ success: false })
|
||||
await uut.cid2json({ cid: 'bafybeib2rphswe7clvclverw7xi7nejkpkh4mdfcurdmcgw7wcup7za6wy' })
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.include(error.message, 'Could not communicate with instance of ipfs-file-pin-service')
|
||||
}
|
||||
})
|
||||
it('should throw error if cid is not pinned', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.adapters.ipfsFiles, 'getFileMetadata').resolves({ success: true, fileMetadata: { dataPinned: false } })
|
||||
await uut.cid2json({ cid: 'bafybeib2rphswe7clvclverw7xi7nejkpkh4mdfcurdmcgw7wcup7za6wy' })
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.include(error.message, 'has not been pinned by ipfs-file-pin-service instance')
|
||||
}
|
||||
})
|
||||
it('should throw error if filename has no .json extension', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.adapters.ipfsFiles, 'getFileMetadata').resolves({ success: true, fileMetadata: { dataPinned: true, filename: 'test' } })
|
||||
await uut.cid2json({ cid: 'bafybeib2rphswe7clvclverw7xi7nejkpkh4mdfcurdmcgw7wcup7za6wy' })
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.include(error.message, 'does not resolve to a JSON file')
|
||||
}
|
||||
})
|
||||
it('should handle error if file cant be retrieved', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.adapters.ipfsFiles, 'getFileMetadata').resolves({ fileMetadata: { dataPinned: true, filename: 'test.json' } })
|
||||
sandbox.stub(uut.adapters.ipfs.ipfs.fs, 'cat').throws(new Error('test error'))
|
||||
sandbox.stub(uut.adapters.ipfs.ipfs.fs, 'ls').callsFake(function * () {
|
||||
yield { name: 'test.json' }
|
||||
})
|
||||
await uut.cid2json({ cid: 'bafybeib2rphswe7clvclverw7xi7nejkpkh4mdfcurdmcgw7wcup7za6wy' })
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.include(error.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle error if cid is a directory', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.adapters.ipfsFiles, 'getFileMetadata').resolves({ fileMetadata: { dataPinned: true, filename: 'test.json' } })
|
||||
sandbox.stub(uut.adapters.ipfs.ipfs.fs, 'cat')
|
||||
.onCall(0).throws(new Error('test error'))
|
||||
.onCall(1).callsFake(function * () {
|
||||
yield { name: 'test.json' }
|
||||
})
|
||||
sandbox.stub(uut.adapters.ipfs.ipfs.fs, 'ls').callsFake(function * () {
|
||||
yield { name: 'test/directory/' }
|
||||
})
|
||||
await uut.cid2json({ cid: 'bafybeib2rphswe7clvclverw7xi7nejkpkh4mdfcurdmcgw7wcup7za6wy' })
|
||||
} catch (error) {
|
||||
assert.include(error.message, 'file does not resolve to a valid JSON file')
|
||||
}
|
||||
})
|
||||
it('should get content from cid/name.json format', async () => {
|
||||
sandbox.stub(uut.adapters.ipfsFiles, 'getFileMetadata').resolves({ fileMetadata: { dataPinned: true, filename: 'test.json' } })
|
||||
sandbox.stub(uut.adapters.ipfs.ipfs.fs, 'cat')
|
||||
.onCall(0).throws(new Error('test error'))
|
||||
.onCall(1).callsFake(function * () {
|
||||
yield Buffer.from('{"test": "test"}')
|
||||
})
|
||||
sandbox.stub(uut.adapters.ipfs.ipfs.fs, 'ls').callsFake(function * () {
|
||||
yield { name: 'test/directory/test.json' }
|
||||
})
|
||||
|
||||
const result = await uut.cid2json({ cid: 'bafybeib2rphswe7clvclverw7xi7nejkpkh4mdfcurdmcgw7wcup7za6wy' })
|
||||
assert.isObject(result)
|
||||
assert.property(result, 'success')
|
||||
assert.property(result, 'json')
|
||||
})
|
||||
it('should handle if content cant be parsed', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.adapters.ipfsFiles, 'getFileMetadata').resolves({ fileMetadata: { dataPinned: true, filename: 'test.json' } })
|
||||
sandbox.stub(uut.adapters.ipfs.ipfs.fs, 'ls').callsFake(function * () {
|
||||
yield { name: 'test.json' }
|
||||
})
|
||||
sandbox.stub(JSON, 'parse').throws(new Error())
|
||||
|
||||
await uut.cid2json({ cid: 'bafybeib2rphswe7clvclverw7xi7nejkpkh4mdfcurdmcgw7wcup7za6wy' })
|
||||
} catch (error) {
|
||||
assert.include(error.message, 'not resolve to a valid JSON object')
|
||||
}
|
||||
})
|
||||
})
|
||||
describe('#pinClaim', () => {
|
||||
it('should process pinClaim', async () => {
|
||||
const result = await uut.pinClaim()
|
||||
assert.isObject(result)
|
||||
assert.property(result, 'success')
|
||||
assert.property(result, 'message')
|
||||
})
|
||||
|
||||
it('should handle error ', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.adapters.ipfsFiles, 'pinClaim').throws(new Error('test error'))
|
||||
await uut.pinClaim()
|
||||
} catch (error) {
|
||||
assert.include(error.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,255 @@
|
||||
/*
|
||||
Unit tests for the use-cases/usage-use-cases.js business logic library.
|
||||
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
import { assert } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
|
||||
// Local support libraries
|
||||
import adapters from '../mocks/adapters/index.js'
|
||||
|
||||
// Mock
|
||||
import { context as mockContext } from '../mocks/ctx-mock.js'
|
||||
|
||||
// Unit under test (uut)
|
||||
import { UsageUseCases, restCalls, usageMiddleware } from '../../../src/use-cases/usage-use-cases.js'
|
||||
|
||||
describe('#usage-use-case', () => {
|
||||
let uut
|
||||
let sandbox
|
||||
let ctx
|
||||
|
||||
before(async () => {
|
||||
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
uut = new UsageUseCases({ adapters })
|
||||
|
||||
// Set as empty array
|
||||
restCalls.splice(0, restCalls.length)
|
||||
|
||||
ctx = mockContext()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should throw an error if adapters are not passed in', () => {
|
||||
try {
|
||||
uut = new UsageUseCases()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of adapters must be passed in when instantiating Usage Use Cases library.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#cleanUsage', () => {
|
||||
it('should delete older data than 24 hours', () => {
|
||||
const now = new Date() // Mock date
|
||||
|
||||
// set older mock data
|
||||
restCalls.push({
|
||||
timestamp: now.getTime() - (60000 * 60 * 24),
|
||||
ip: '127.0.0.1'
|
||||
})
|
||||
|
||||
// Set recently mock data
|
||||
restCalls.push({
|
||||
timestamp: now.getTime(),
|
||||
ip: 'localhost'
|
||||
})
|
||||
|
||||
const result = uut.cleanUsage()
|
||||
|
||||
assert.isArray(result)
|
||||
assert.equal(result.length, 1)
|
||||
assert.equal(result[0].ip, 'localhost')
|
||||
})
|
||||
|
||||
it('should handle error', () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(restCalls, 'filter').throws(new Error('uut error'))
|
||||
|
||||
uut.cleanUsage()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(error.message, 'uut error')
|
||||
}
|
||||
})
|
||||
})
|
||||
describe('#getRestSummary', () => {
|
||||
it('should get the number of rest calls', () => {
|
||||
// Set mock data
|
||||
restCalls.push({
|
||||
ip: 'localhost'
|
||||
})
|
||||
|
||||
const result = uut.getRestSummary()
|
||||
|
||||
assert.isNumber(result)
|
||||
assert.equal(result, 1)
|
||||
})
|
||||
|
||||
it('should handle error', () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(console, 'log').throws(new Error('uut error'))
|
||||
|
||||
uut.getRestSummary()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(error.message, 'uut error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getTopIps', () => {
|
||||
it('should get top IPs', () => {
|
||||
// Set mock data
|
||||
restCalls.push({
|
||||
ip: 'localhost'
|
||||
})
|
||||
|
||||
// Set mock data
|
||||
restCalls.push({
|
||||
ip: 'localhost'
|
||||
})
|
||||
|
||||
const result = uut.getTopIps()
|
||||
|
||||
assert.isArray(result)
|
||||
|
||||
assert.property(result[0], 'ip')
|
||||
assert.property(result[0], 'cnt')
|
||||
|
||||
assert.equal(result[0].ip, 'localhost')
|
||||
assert.equal(result[0].cnt, '2')
|
||||
})
|
||||
it('should return a maximum of 20 values', () => {
|
||||
// Fill Array with 21 values
|
||||
for (let i = 0; i < 21; i++) {
|
||||
restCalls.push({
|
||||
ip: `localhost-${i}`
|
||||
})
|
||||
}
|
||||
|
||||
const result = uut.getTopIps()
|
||||
|
||||
assert.isArray(result)
|
||||
|
||||
assert.property(result[0], 'ip')
|
||||
assert.property(result[0], 'cnt')
|
||||
|
||||
assert.equal(result.length, 20)
|
||||
})
|
||||
it('should handle error', () => {
|
||||
try {
|
||||
// Set mock data
|
||||
restCalls.push(null)
|
||||
|
||||
uut.getTopIps()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.include(error.message, 'Cannot read properties')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getTopEndpoints', () => {
|
||||
it('should get top Endpoints', () => {
|
||||
// Set mock data
|
||||
restCalls.push({
|
||||
ip: 'localhost',
|
||||
url: '/api/v1/users',
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
// Set mock data
|
||||
restCalls.push({
|
||||
ip: 'localhost',
|
||||
url: '/api/v1/users',
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
const result = uut.getTopEndpoints()
|
||||
|
||||
assert.isArray(result)
|
||||
|
||||
assert.property(result[0], 'endpoint')
|
||||
assert.property(result[0], 'cnt')
|
||||
|
||||
assert.equal(result[0].endpoint, 'GET /api/v1/users')
|
||||
assert.equal(result[0].cnt, '2')
|
||||
})
|
||||
it('should return a maximum of 20 values', () => {
|
||||
// Fill Array with 21 values
|
||||
for (let i = 0; i < 21; i++) {
|
||||
restCalls.push({
|
||||
ip: 'localhost',
|
||||
url: `/api/v1/users-${i}`,
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
const result = uut.getTopEndpoints()
|
||||
|
||||
assert.isArray(result)
|
||||
|
||||
assert.property(result[0], 'endpoint')
|
||||
assert.property(result[0], 'cnt')
|
||||
|
||||
assert.equal(result.length, 20)
|
||||
})
|
||||
|
||||
it('should handle error', () => {
|
||||
try {
|
||||
// Set mock data
|
||||
restCalls.push(null)
|
||||
|
||||
uut.getTopEndpoints()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.include(error.message, 'Cannot read properties')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#usageMiddleware', () => {
|
||||
it('should update restCalls state', async () => {
|
||||
// Spy on next
|
||||
const next = sinon.spy(() => { })
|
||||
|
||||
await usageMiddleware()(ctx, next)
|
||||
|
||||
assert.equal(restCalls.length, 1)
|
||||
assert.isTrue(next.called)
|
||||
})
|
||||
|
||||
it('should handle error', async () => {
|
||||
try {
|
||||
const next = () => { throw new Error('uut error') }
|
||||
|
||||
await usageMiddleware()(ctx, next)
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (error) {
|
||||
assert.equal(error.message, 'uut error')
|
||||
assert.equal(ctx.status, 500)
|
||||
assert.equal(restCalls.length, 0)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user