Compare commits

...
118 Commits
Author SHA1 Message Date
Chris Troutner 173f706a61 Merge pull request #63 from Permissionless-Software-Foundation/ct-unstable
Syncing with upstream ipfs-service-provider
2025-01-12 17:14:50 -07:00
Chris Troutner 39fc919d35 fix(pedigree): Updating pedigree doc 2025-01-12 17:12:15 -07:00
Chris Troutner 6f0726aeb8 Synced with upstream ipfs-service-provider 2025-01-12 17:11:09 -07:00
Chris Troutner 2568818ea1 Merge pull request #62 from Permissionless-Software-Foundation/ct-unstable
fix(CORS): Adding OPTIONS as an allowed method
2025-01-12 17:03:02 -07:00
Chris Troutner c721da1562 Merge pull request #165 from Permissionless-Software-Foundation/usage
Usage
2024-12-07 21:28:57 -08:00
Chris Troutner 3606a2d31b code cleanup 2024-12-07 21:27:41 -08:00
Chris Troutner 764905b041 fix(usage): Adding endpoints to analyize 24 hour usage of the REST API 2024-12-07 21:25:10 -08:00
Chris Troutner 556c6ec522 fix(usage): Adding Usage REST API 2024-12-07 20:48:08 -08:00
Chris Troutner 04395b70a6 Syncing with ct-unstable. Fixing merge conflict 2024-12-07 18:08:25 -08:00
Chris Troutner 4564090830 fix(CORS): Adding OPTIONS as an allowed method 2024-10-01 08:17:17 -07:00
Chris Troutner 81521151f4 Merge pull request #164 from Permissionless-Software-Foundation/ct-unstable
fix(ipfs): Disabling webRTC again
2024-09-18 12:02:22 -07:00
Chris Troutner 5a819c1d9c linting 2024-09-18 12:01:10 -07:00
Chris Troutner f2f3208fda fix(ipfs): Disabling webRTC again 2024-09-18 12:00:56 -07:00
Chris Troutner 7fe888cdda Merge pull request #163 from Permissionless-Software-Foundation/ct-unstable
fix(webRTC): Renabling webRTC & circuit relays
2024-09-17 07:50:59 -07:00
Chris Troutner db3f1bf2cf fix(webRTC): Renabling webRTC & circuit relays 2024-09-17 07:49:43 -07:00
Chris Troutner 512a49200d Merge pull request #61 from Permissionless-Software-Foundation/ct-unstable
Syncing with upstream ipfs-service-provider
2024-09-13 10:12:32 -07:00
Chris Troutner c6019df364 Merge remote-tracking branch 'upstream/master' into ct-unstable 2024-09-13 10:11:21 -07:00
Chris Troutner b7988253df Adding system-user-dev.json to gitignore 2024-09-13 10:11:08 -07:00
Chris Troutner 5f64257dca Merge pull request #162 from Permissionless-Software-Foundation/ct-unstable
fix(webrtc): Disabling webRTC due to bug
2024-09-13 10:07:53 -07:00
Chris Troutner 2fbdccb883 fix(webrtc): Disabling webRTC due to bug 2024-09-13 10:06:19 -07:00
Chris Troutner ecc10afac3 linting 2024-09-13 10:05:05 -07:00
Chris Troutner b55afd5f93 Merge pull request #60 from Permissionless-Software-Foundation/ct-unstable
Syncing with upstream ipfs-service-provider
2024-09-12 17:32:46 -07:00
Chris Troutner 12037a5e57 Syncing with upstream ipfs-service-provider 2024-09-12 17:31:28 -07:00
Chris Troutner 4fc54aa9c5 Merge pull request #161 from Permissionless-Software-Foundation/ct-unstable
fix(helia-coord): Updating to v1.5.14
2024-09-12 16:59:23 -07:00
Chris Troutner 2bd7f0c45c Merge branch 'master' into ct-unstable 2024-09-12 16:57:00 -07:00
Chris Troutner 900423339e fix(helia-coord): Updating to v1.5.14 2024-09-12 16:56:44 -07:00
Chris Troutner d907afa6bb Merge pull request #160 from Permissionless-Software-Foundation/ct-unstable
fix(helia-coord): Updating to v1.5.13
2024-09-12 16:48:03 -07:00
Chris Troutner 9b116d82b7 fix(helia-coord): Updating to v1.5.13 2024-09-12 16:42:43 -07:00
Chris Troutner 1d1bf50ee3 Merge pull request #159 from Permissionless-Software-Foundation/ct-unstable
Updating dependencies
2024-09-09 18:15:03 -07:00
Chris Troutner 5002004697 fix(minimal-slp-wallet): Upgraded to v5.12.0 2024-09-09 18:13:36 -07:00
Chris Troutner 5b0fac4301 Merge pull request #59 from Permissionless-Software-Foundation/ct-unstable
Updating dependencies
2024-09-05 10:10:29 -07:00
Chris Troutner 9584810b16 fix(deps): Updating dependencies 2024-09-05 10:06:11 -07:00
Chris Troutner f311a3b9c6 fix(webRTC): Upgrading to new package, adding required config 2024-08-29 11:00:07 -07:00
Chris Troutner 83f16709d8 Merge remote-tracking branch 'upstream/master' into ct-unstable 2024-08-24 08:17:31 -07:00
Chris Troutner fe91f4b875 Merge pull request #158 from Permissionless-Software-Foundation/ct-unstable
fix(helia-coord): Updating to v1.5.12
2024-08-23 10:30:27 -07:00
Chris Troutner 2d9a2b811c fix(helia-coord): Updating to v1.5.12 2024-08-23 10:24:21 -07:00
Chris Troutner 9360cf6f77 Syncing with upstream, updating minimal-slp-wallet to v5.11.3 2024-08-23 07:24:10 -07:00
Chris Troutner 514b65ed24 Merge pull request #157 from Permissionless-Software-Foundation/ct-unstable
fix(minimal-slp-wallet): Updated to v5.11.3
2024-08-23 07:02:19 -07:00
Chris Troutner 0c7f3088a8 fix(minimal-slp-wallet): Updated to v5.11.3 2024-08-23 06:59:58 -07:00
Chris Troutner aef88eb369 Merge pull request #58 from Permissionless-Software-Foundation/ct-unstable
Upgrading helia to v4.2.5
2024-08-20 08:17:49 -07:00
Chris Troutner 2f6cc72f41 Adding system user .json files 2024-08-20 08:16:37 -07:00
Chris Troutner 12cab02300 Merge pull request #156 from Permissionless-Software-Foundation/ct-unstable
Upgrading helia to v4.2.5
2024-08-20 07:53:53 -07:00
Chris Troutner 04a908261a Updating requirements in README 2024-08-20 07:32:30 -07:00
Chris Troutner f16eacc3f2 feat(helia): Updated helia and libp2p by syncing with upstream ipfs-service-provider 2024-08-19 13:22:08 -07:00
Chris Troutner 83898742f1 fix(webRTC): Falling back from v4.1.7 to v4.0.34 2024-08-19 07:35:59 -07:00
Chris Troutner 1795918719 Using node.js v20 for docker container 2024-08-19 06:33:31 -07:00
Chris Troutner 6894217078 fix(production): Limiting docker to 1GB of memory 2024-08-19 06:29:16 -07:00
Chris Troutner 3ffbed27b5 fix(helia-coord): Got running with v1.5.11 2024-08-18 08:41:28 -07:00
Chris Troutner c30b697bc6 linting 2024-07-26 11:29:10 -07:00
Chris Troutner df2f69050b feat(/price/psffpp): New REST endpoint for retrieving PSFFPP write price 2024-07-26 11:29:00 -07:00
Chris Troutner b49979971c lib added, but write price error defaults to hard-coded value 2024-07-25 07:35:49 -07:00
Chris Troutner 7a620892d7 Merge pull request #57 from Permissionless-Software-Foundation/ct-unstable
Updating helia-coord
2024-07-21 15:54:31 -07:00
Chris Troutner b372e3d260 Merge pull request #155 from Permissionless-Software-Foundation/ct-unstable
fix(Mongo): Skipping some tests when noMongo is true
2024-06-09 09:12:31 -07:00
Chris Troutner 707b385b8a linting 2024-06-09 09:11:09 -07:00
Chris Troutner 6cb140a491 fix(Mongo): Skipping some tests when noMongo is true 2024-06-09 09:10:55 -07:00
Chris Troutner c43ee8c25a Merge pull request #154 from Permissionless-Software-Foundation/ct-unstable
feat(Mongo): Config variable can disable MongoDB usage and dependency in Docker
2024-06-09 08:38:00 -07:00
Chris Troutner 0a7ec11510 feat(Mongo): Config variable can disable MongoDB usage and dependency in Docker 2024-06-09 08:28:40 -07:00
Chris Troutner 4a1524629c Merge remote-tracking branch 'upstream/master' into ct-unstable 2024-05-21 20:33:37 -07:00
Chris Troutner f7aaba71d1 Merge pull request #153 from Permissionless-Software-Foundation/ct-unstable
fix(helia-coord): Updating to v1.5.8
2024-05-04 09:38:28 -07:00
Chris Troutner 418a2e2ad2 fix(helia-coord): Updating to v1.5.8 2024-05-04 09:37:10 -07:00
Chris Troutner 74248ea0c7 Merge pull request #56 from Permissionless-Software-Foundation/ct-unstable
Improvements from production testing
2024-04-28 20:40:25 -07:00
Chris Troutner 70a69e98bb fix(pollForBchServices()): filtering for only wallet-services, not consumers 2024-04-28 16:53:34 -07:00
Chris Troutner 8cffd7fae3 Merge remote-tracking branch 'upstream/master' into ct-unstable 2024-04-27 13:22:07 -07:00
Chris Troutner a1023e8224 Merge pull request #152 from Permissionless-Software-Foundation/ct-unstable
fix(Docker): Setting memory limit at 4GB
2024-04-27 13:20:20 -07:00
Chris Troutner a32ed63d47 fix(Docker): Setting memory limit at 4GB 2024-04-27 13:17:57 -07:00
Chris Troutner 934fb46ffc Merge pull request #151 from Permissionless-Software-Foundation/ct-unstable
fix(Docker): Removing Volume statement
2024-04-26 14:40:41 -07:00
Chris Troutner b27136d03b fix(Docker): Removing Volume statement 2024-04-26 14:39:18 -07:00
Chris Troutner 064ac36dc7 linting 2024-04-26 14:38:17 -07:00
Chris Troutner 5e2cf86f4b Checking in code before switching branches 2024-04-26 14:38:03 -07:00
Chris Troutner 425d2ada65 Binding 'this' object to some subfunctions 2024-04-20 07:14:17 -07:00
Chris Troutner 9837d6a7fb Merge remote-tracking branch 'upstream/master' into ct-unstable 2024-04-13 07:15:57 -07:00
Chris Troutner 60a3d67c0b Merge pull request #150 from Permissionless-Software-Foundation/ct-unstable
fix(config): web2Api can be set with env var
2024-04-13 07:11:20 -07:00
Chris Troutner a03c5d8c91 fix(config): web2Api can be set with env var 2024-04-13 07:09:45 -07:00
Chris Troutner 0e7afb5f51 Merge remote-tracking branch 'upstream/master' into ct-unstable 2024-04-13 07:07:44 -07:00
Chris Troutner bcc90da49b Merge pull request #149 from Permissionless-Software-Foundation/ct-unstable
fix(config): Adding web2Api property to jsonLd config
2024-04-13 07:04:02 -07:00
Chris Troutner b473a3803c fix(config): Adding web2Api property to jsonLd config 2024-04-13 07:02:32 -07:00
Chris Troutner 38ed9a8dac Merge pull request #55 from Permissionless-Software-Foundation/ct-unstable
fix(getPins): Adding getPins endpoint for File Pin Service
2024-04-12 10:46:01 -07:00
Chris Troutner 0ca46df43e fix(getPins): Adding getPins endpoint for File Pin Service 2024-04-12 10:44:03 -07:00
Chris Troutner b5f09fbab9 Sycing with upstream ipfs-service-provider 2024-04-12 08:35:49 -07:00
Chris Troutner e3e928b33d Adding getPins endpoint for File Pin Service 2024-04-12 08:34:39 -07:00
Chris Troutner 9614f186b4 Merge pull request #148 from Permissionless-Software-Foundation/ct-unstable
fix(helia-coord): Updating to v1.5.7
2024-04-12 08:32:13 -07:00
Chris Troutner 0390f2d956 fix(helia-coord): Updating to v1.5.7 2024-04-12 08:30:14 -07:00
Chris Troutner 43025f0265 Merge pull request #54 from Permissionless-Software-Foundation/ct-unstable
Adding endpoints needed for metrics
2024-04-11 05:36:56 -07:00
Chris Troutner 7ea17fb57c Removing example timer controller 2024-04-11 05:34:25 -07:00
Chris Troutner efb29104d7 feat(/ipfs/file-info/:cid): GET endpoint to get CID pin metadata from ipfs-file-pin-server over JSON RPC 2024-04-10 19:41:43 -07:00
Chris Troutner 971474d493 linting 2024-04-10 11:05:55 -07:00
Chris Troutner 71a6de03b3 feat(/bch/service): GET endpoint to get ipfs-bch-wallet-service consumed by this app 2024-04-10 11:05:46 -07:00
Chris Troutner 98dd904fa5 feat(/ipfs/service): GET endpoint to retrieve File Pin service used by this app 2024-04-10 10:52:07 -07:00
Chris Troutner b5c7e2efb3 Using filename from file metadata 2024-04-09 22:00:58 -07:00
Chris Troutner 3c9bbb097e Successfully recieving result from file pin JSON RPC call 2024-04-09 20:56:40 -07:00
Chris Troutner 8956968027 Commenting out downloadCid2() 2024-04-09 08:08:53 -07:00
Chris Troutner 14c724bf9c Prototyping IPFS file download 2024-04-09 06:36:23 -07:00
Chris Troutner a9f7e609e9 Merge pull request #53 from Permissionless-Software-Foundation/ct-unstable
fix(docker): Changing ports to comply with cashstack.info
2024-04-07 14:41:49 -07:00
Chris Troutner fee68e0f59 fix(docker): Changing ports to comply with cashstack.info 2024-04-07 14:38:40 -07:00
Chris Troutner 86c005c205 Merge pull request #52 from Permissionless-Software-Foundation/ct-unstable
fix(jsonLd): Updating jsonLd details passed to other nodes
2024-04-03 09:36:10 -07:00
Chris Troutner d4d5b2eb68 fix(jsonLd): Updating jsonLd details passed to other nodes 2024-04-03 09:34:43 -07:00
Chris Troutner 32a651b7a8 Merge pull request #51 from Permissionless-Software-Foundation/ct-unstable
Syncing with upstream
2024-04-03 08:19:26 -07:00
Chris Troutner 7da5c5b4d6 Fixing merge conflicts 2024-04-03 08:16:52 -07:00
Chris Troutner ba5ead998d Merge pull request #147 from Permissionless-Software-Foundation/ct-unstable
fix(docker): Tweaking some defaults
2024-04-03 06:02:17 -07:00
Chris Troutner 409a0f1049 fix(docker): Tweaking some defaults 2024-04-03 05:54:24 -07:00
Chris Troutner bd2f27bb64 Merge pull request #146 from Permissionless-Software-Foundation/ct-unstable
fix(incrementNextAddress): fixing bug
2024-03-29 12:44:43 -07:00
Chris Troutner 55d8d131cf Removing commented code 2024-03-29 12:43:15 -07:00
Chris Troutner 5a6e3247e8 fix(incrementNextAddress): fixing bug 2024-03-29 12:43:02 -07:00
Chris Troutner 2a522775eb Adding wallet.json to gitignore 2024-03-29 11:36:58 -07:00
Chris Troutner d677c83795 Removing wallet.json file 2024-03-29 11:36:35 -07:00
Chris Troutner 6cfaceb99e Merge pull request #50 from Permissionless-Software-Foundation/ct-unstable
Syncing with upstream, fixing flaky tests
2024-03-24 08:25:20 -07:00
Chris Troutner b12cc194f4 fix(tests): Fixed fragile unit tests 2024-03-24 08:24:08 -07:00
Chris Troutner 7cffd9f377 Merge pull request #145 from Permissionless-Software-Foundation/ct-unstable
fix(helia-coord): Updating to v1.5.6, auto-retry of bootstrap list
2024-03-24 07:44:46 -07:00
Chris Troutner 0a406cb9a4 fix(helia-coord): Updating to v1.5.6, auto-retry of bootstrap list 2024-03-24 07:43:34 -07:00
Chris Troutner f172a51016 Merge pull request #144 from Permissionless-Software-Foundation/ct-unstable
Integrating minimal-slp-wallet. Wallet creation at startup
2024-03-23 17:46:55 -07:00
Chris Troutner 00d53659b1 Updating README 2024-03-23 17:44:41 -07:00
Chris Troutner b0797d92f5 Increased test coverage to 100% 2024-03-23 17:42:59 -07:00
Chris Troutner 48484ed7af Fixing typo with web3 wallet 2024-03-23 17:23:07 -07:00
Chris Troutner 6a4aadde8a Refactoring config file 2024-03-23 17:14:26 -07:00
Chris Troutner 79e3aba26e Adding default startup wallet 2024-03-23 16:18:10 -07:00
Chris Troutner bf61359bc7 feat(wallet): Added adapter library for consuming minimal-slp-wallet 2024-03-23 11:16:45 -07:00
Chris Troutner 1911991511 linting 2024-03-23 08:18:47 -07:00
Chris Troutner ef0179fb3f fix(README): Adding config variables to README 2024-03-23 08:18:32 -07:00
43 changed files with 10439 additions and 7128 deletions
+4 -1
View File
@@ -62,7 +62,7 @@ docs
.nyc_output
coverage
database/
system-user-*.json
#system-user-*.json
orbitdb
ipfsdata
.ipfsdata
@@ -71,3 +71,6 @@ data/
run-dev.sh
!README.md
wallet.json
system-user-dev.json
+1 -1
View File
@@ -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
View File
@@ -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.
+17 -4
View File
@@ -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
@@ -47,7 +47,20 @@ docker-compose up -d
- You can bring the containers down with `docker-compose down`
- You can bring the containers back up with `docker-compose up -d`.
## Code Structure
## Configuration
This program is intended to started with a Bash shell script. In that script, you can set the following environment variables:
```bash
# The IPFS ID of the service running ipfs-bch-wallet-service
export PREFERRED_P2WDB_PROVIDER=12D3KooWCNpwHbHmkNSJsqu3CQVVe3iW8g6e1gn3kWTPahj2igiy
```
### Configuration
This app is intended to be started via a bash shell script. See the environment variables used to configure this app in the [config/env/common.js file](./config/env/common.js).
## File Structure
The file layout of this repository differs from the koa-api-boilerplate. Instead, it follows the file layout of [Clean Architecture](https://christroutner.github.io/trouts-blog/blog/clean-architecture).
+23 -14
View File
@@ -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.
+78 -31
View File
@@ -27,7 +27,7 @@ const ipfsCoordName = process.env.COORD_NAME
export default {
// Configure TCP port.
port: process.env.PORT || 5005,
port: process.env.PORT || 5015,
// Password for HTML UI that displays logs.
logPass: 'test',
@@ -43,14 +43,46 @@ 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
mnemonic: process.env.MNEMONIC ? process.env.MNEMONIC : '',
// (Optional) use a wallet file generated by psf-bch-wallet.
walletFile: process.env.WALLET_FILE
? process.env.WALLET_FILE
: `${__dirname.toString()}/../../wallet.json`,
// Wallet Configuration
// Default value: web3 - connect to bch-api through web 3 JSON RPC over IPFS.
// Alternate value: web2 - connect to bch-api through a web 2 REST API over HTTP.
walletInterface: process.env.WALLET_INTERFACE ? process.env.WALLET_INTERFACE : 'web3',
// Wallet API Server
// Default web2 server: https://api.fullstack.cash/v5/
// Default web3 server: https://free-bch.fullstack.cash/
apiServer: process.env.WALLET_INTERFACE === 'web2'
? (
process.env.APISERVER
? process.env.APISERVER
: 'https://api.fullstack.cash/v5/'
)
: 'https://free-bch.fullstack.cash/',
// Basic Authentication Password for connecting to bch-api through a web 2
// REST API over HTTP.
authPass: process.env.WALLET_AUTH_PASS
? process.env.WALLET_AUTH_PASS
: '',
// FullStack.cash account information, used for automatic JWT handling.
getJwtAtStartup: process.env.GET_JWT_AT_STARTUP ? true : false,
authServer: process.env.AUTHSERVER
? process.env.AUTHSERVER
: 'https://auth.fullstack.cash',
apiServer: process.env.APISERVER
? process.env.APISERVER
: 'https://api.fullstack.cash/v5/',
fullstackLogin: process.env.FULLSTACKLOGIN
? process.env.FULLSTACKLOGIN
: 'demo@demo.com',
@@ -58,8 +90,17 @@ export default {
? process.env.FULLSTACKPASS
: 'demo',
// IPFS settings.
useIpfs: process.env.DISABLE_IPFS ? false : true, // Disable IPFS flag
// END WALLET CONFIGURATION
// BEGIN IPFS CONFIGURATION
// Debug verbosity level of helia-coord.
// 0 = no debug logs. 3 = maximum logs.
debugLevel: process.env.DEBUG_LEVEL ? parseInt(process.env.DEBUG_LEVEL) : 2,
// Enable/Disable the IPFS node at startup. Enabled by default.
useIpfs: process.env.DISABLE_IPFS ? false : true,
isCircuitRelay: process.env.ENABLE_CIRCUIT_RELAY ? true : false,
// SSL domain used for websocket connection via browsers.
crDomain: process.env.CR_DOMAIN ? process.env.CR_DOMAIN : '',
@@ -73,41 +114,24 @@ export default {
'@type': 'WebAPI',
name: ipfsCoordName,
version,
protocol: 'generic-service',
protocol: 'ipfs-bch-wallet-consumer',
description:
'This is a generic IPFS Serivice Provider that uses JSON RPC over IPFS to communicate with it. This instance has not been customized. Source code: https://github.com/Permissionless-Software-Foundation/ipfs-service-provider',
documentation: 'https://ipfs-service-provider.fullstack.cash/',
'REST API gateway for accessing BCH blockchain services over the web3 Cash Stack (https://CashStack.info)',
documentation: 'https://ipfs-bch-wallet-consumer.fullstack.cash/',
provider: {
'@type': 'Organization',
name: 'Permissionless Software Foundation',
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,
// BCH Mnemonic for generating encryption keys and payment address
mnemonic: process.env.MNEMONIC ? process.env.MNEMONIC : '',
debugLevel: process.env.DEBUG_LEVEL ? parseInt(process.env.DEBUG_LEVEL) : 2,
// Preferred provider running ipfs-bch-wallet-service for BCH wallet.
preferredProvider: process.env.PREFERRED_PROVIDER
? process.env.PREFERRED_PROVIDER
: '',
// If true, users will not be able to change the provider via a REST API call.
freezeProvider: process.env.FREEZE_PROVIDER ? true : false,
// : 'QmdBGB8S6sEr19gaAxJYjhRbz5ZmMiH5a1JyyMmaxcRKnY'
// : 'QmUTx6KqYKVZbKpKxR7vGDUgZFYVvVVyEWDeCYq4GwBCff'
// Used for development
// Preferred P2WDB provider
preferredP2wdbProvider: process.env.PREFERRED_P2WDB_PROVIDER
? process.env.PREFERRED_P2WDB_PROVIDER
: '',
// Settings for production, using external go-ipfs node.
isProduction: process.env.SVC_ENV === 'prod' ? true : false,
ipfsHost: process.env.IPFS_HOST ? process.env.IPFS_HOST : 'localhost',
@@ -124,5 +148,28 @@ export default {
// v2 Circuit Relay server (FullStack.cash)
// '/ip4/78.46.129.7/tcp/4001/p2p/12D3KooWFQ11GQ5NubsJGhYZ4X3wrAGimLevxfm6HPExCrMYhpSL'
]
],
// END IPFS CONFIGURATION
// Preferred provider running ipfs-bch-wallet-service for BCH wallet.
preferredProvider: process.env.PREFERRED_PROVIDER
? process.env.PREFERRED_PROVIDER
: '',
// If true, users will not be able to change the provider via a REST API call.
freezeProvider: process.env.FREEZE_PROVIDER ? true : false,
// : 'QmdBGB8S6sEr19gaAxJYjhRbz5ZmMiH5a1JyyMmaxcRKnY'
// : 'QmUTx6KqYKVZbKpKxR7vGDUgZFYVvVVyEWDeCYq4GwBCff'
// Used for development
// Preferred P2WDB provider
preferredP2wdbProvider: process.env.PREFERRED_P2WDB_PROVIDER
? process.env.PREFERRED_P2WDB_PROVIDER
: '',
// Preferred P2WDB provider
preferredIpfsFileProvider: process.env.PREFERRED_IPFS_FILE_PROVIDER
? process.env.PREFERRED_IPFS_FILE_PROVIDER
: ''
}
+6
View File
@@ -0,0 +1,6 @@
{
"password": "RPWjFwYWyJ9S8ByjRDbi",
"email": "system@system.com",
"id": "678459df8ec91cd6c3973b63",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3ODQ1OWRmOGVjOTFjZDZjMzk3M2I2MyIsImlhdCI6MTczNjcyNzAwN30.AHLNjmznprA5GzQ_z8GbhrYHOJUIlMzVC64SGRy-Xvs"
}
+6
View File
@@ -0,0 +1,6 @@
{
"password": "02PjvhoMGoM7pJylp80Z",
"email": "system@system.com",
"id": "645e49a45a9f5a8e0a54a083",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY0NWU0OWE0NWE5ZjVhOGUwYTU0YTA4MyIsImlhdCI6MTY4MzkwMDgzNn0.sXeT-G9kS5Nu1dnZUGsNm6FhxrcNaCHhnQ-tLqsJc9E"
}
+6
View File
@@ -0,0 +1,6 @@
{
"password": "RCJqq5FjgYzvTXOyS1vo",
"email": "system@system.com",
"id": "645e59c19d49119bbb8329e7",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY0NWU1OWMxOWQ0OTExOWJiYjgzMjllNyIsImlhdCI6MTY4MzkwNDk2MX0.2yi-hkY31nkgMm_S1QrPs4dIm5DBnJ4GsJEPiSPYHDA"
}
+7433 -6128
View File
File diff suppressed because it is too large Load Diff
+22 -18
View File
@@ -1,6 +1,6 @@
{
"name": "ipfs-bch-wallet-consumer",
"version": "3.0.0",
"version": "3.1.1",
"description": "A (local) REST API that proxies BCH wallet services delivered over IPFS (global).",
"main": "index.js",
"type": "module",
@@ -24,24 +24,26 @@
},
"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": "13.1.1",
"@chainsafe/libp2p-noise": "15.1.1",
"@chainsafe/libp2p-yamux": "6.0.2",
"@helia/unixfs": "3.0.7",
"@libp2p/bootstrap": "10.1.5",
"@libp2p/circuit-relay-v2": "1.1.5",
"@libp2p/identify": "2.1.5",
"@libp2p/keychain": "4.1.5",
"@libp2p/logger": "4.0.20",
"@libp2p/tcp": "9.1.5",
"@libp2p/webrtc": "4.1.7",
"@libp2p/websockets": "8.2.0",
"@multiformats/multiaddr": "12.3.0",
"axios": "0.27.2",
"bcryptjs": "2.4.3",
"blockstore-fs": "1.1.8",
"datastore-fs": "9.1.6",
"blockstore-fs": "2.0.0",
"datastore-fs": "9.1.7",
"glob": "7.1.6",
"helia": "2.1.0",
"helia-coord": "1.5.5",
"helia": "4.2.5",
"helia-coord": "1.5.15",
"jsonrpc-lite": "2.2.0",
"jsonwebtoken": "8.5.1",
"jwt-bch-lib": "1.3.0",
@@ -56,13 +58,15 @@
"koa-router": "10.0.0",
"koa-static": "5.0.0",
"koa2-ratelimit": "0.9.1",
"libp2p": "1.2.1",
"libp2p": "1.9.1",
"line-reader": "0.4.0",
"minimal-slp-wallet": "5.11.1",
"mime-types": "2.1.35",
"minimal-slp-wallet": "5.12.0",
"mongoose": "5.13.14",
"node-fetch": "npm:@achingbrain/node-fetch@2.6.7",
"nodemailer": "6.7.5",
"passport-local": "1.0.0",
"psffpp": "1.1.9",
"public-ip": "6.0.1",
"semver": "7.3.5",
"winston": "3.3.3",
+2 -4
View File
@@ -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,10 +66,8 @@ 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
#EXPOSE 5010
# Start the application.
#COPY start-production.sh start-production.sh
+5 -5
View File
@@ -7,7 +7,7 @@ services:
image: mongo:4.2.0
container_name: mongo-bch-consumer
ports:
- '5555:27017' # <host port>:<container port>
- '5558:27017' # <host port>:<container port>
volumes:
- ../data/database:/data/db
command: mongod --logpath=/dev/null # -- quiet
@@ -22,13 +22,13 @@ services:
options:
max-size: '10m'
max-file: '10'
mem_limit: 500mb
mem_limit: 1000mb
links:
- mongo-bch-consumer
ports:
- '5010:5010' # <host port>:<container port>
- '4001:4001'
- '4003:4003'
- '5015:5015' # <host port>:<container port>
- '4101:4101'
- '4103:4103'
volumes:
- ../data/ipfsdata:/home/safeuser/ipfs-bch-wallet-consumer/.ipfsdata
- ./start-production.sh:/home/safeuser/ipfs-bch-wallet-consumer/start-production.sh
+4 -7
View File
@@ -16,24 +16,21 @@ export COORD_NAME=bch-consumer-generic
# and you must forward that subdomain to the IPFS_WS_PORT.
#export CR_DOMAIN=subdomain.yourdomain.com
# Debug level. 0 = minimal info. 2 = max info.
export DEBUG_LEVEL=1
# END: Optional configuration settings
# Production database connection string.
export DBURL=mongodb://172.17.0.1:5555/ipfs-bch-consumer-prod
export DBURL=mongodb://172.17.0.1:5558/ipfs-bch-consumer-prod
# Configure REST API port
export PORT=5010
export PORT=5015
# Production settings using external go-ipfs node.
export SVC_ENV=prod
export IPFS_HOST=172.17.0.1
export IPFS_API_PORT=5001
export IPFS_TCP_PORT=4001
export IPFS_WS_PORT=4003
export IPFS_TCP_PORT=4101
export IPFS_WS_PORT=4103
# Set the debug level for helia-coord. 0-3.
# 0 = no debug logs. 3 = maximum debug logs.
+49 -9
View File
@@ -102,7 +102,7 @@ class BchAdapter {
return outObj
} catch (err) {
// console.log('createUser() error: ', err)
wlogger.error('Error in use-cases/bch.js/getStatus()')
wlogger.error('Error in adapters/bch.js/getStatus()')
throw err
}
}
@@ -115,7 +115,7 @@ class BchAdapter {
return true
} catch (err) {
// console.log('createUser() error: ', err)
wlogger.error('Error in use-cases/bch.js/getStatus()')
wlogger.error('Error in adapters/bch.js/getStatus()')
throw err
}
}
@@ -160,13 +160,17 @@ class BchAdapter {
return data
} catch (err) {
// console.log('createUser() error: ', err)
wlogger.error('Error in use-cases/bch.js/getBalances()')
wlogger.error('Error in adapters/bch.js/getBalances()')
throw err
}
}
async getUtxos (addr) {
try {
if (!addr) {
throw new Error('addr required when calling getUtxos')
}
// Throw an error if this IPFS node has not yet made a connection to a
// wallet service provider.
const selectedProvider =
@@ -203,7 +207,7 @@ class BchAdapter {
return data
} catch (err) {
// console.log('createUser() error: ', err)
wlogger.error('Error in use-cases/bch.js/getUtxos()')
wlogger.error('Error in adapters/bch.js/getUtxos()')
throw err
}
}
@@ -257,13 +261,17 @@ class BchAdapter {
return data
} catch (err) {
// console.log('createUser() error: ', err)
wlogger.error('Error in use-cases/bch.js/getUtxosBulk()')
wlogger.error('Error in adapters/bch.js/getUtxosBulk()')
throw err
}
}
async broadcast (hex) {
try {
if (!hex) {
throw new Error('hex required when calling broadcast')
}
// Throw an error if this IPFS node has not yet made a connection to a
// wallet service provider.
const selectedProvider =
@@ -309,6 +317,10 @@ class BchAdapter {
try {
console.log('Executing getTransaction() bch adapter')
if (!address) {
throw new Error('address required when calling getTransactions()')
}
// Throw an error if this IPFS node has not yet made a connection to a
// wallet service provider.
const selectedProvider =
@@ -354,6 +366,10 @@ class BchAdapter {
// Get details on an array of TXIDs.
async getTransaction (txids) {
try {
if (!txids) {
throw new Error('txids required when calling getTransaction()')
}
console.log(
`Getting txData on these txids: ${JSON.stringify(txids, null, 2)}`
)
@@ -401,6 +417,10 @@ class BchAdapter {
async getPubKey (address) {
try {
if (!address) {
throw new Error('address required when calling getPubKey()')
}
// Throw an error if this IPFS node has not yet made a connection to a
// wallet service provider.
const selectedProvider =
@@ -443,6 +463,10 @@ class BchAdapter {
async utxoIsValid (utxo) {
try {
if (!utxo) {
throw new Error('utxo required when calling utxoIsValid()')
}
// Throw an error if this IPFS node has not yet made a connection to a
// wallet service provider.
const selectedProvider =
@@ -485,6 +509,10 @@ class BchAdapter {
async getTokenData (tokenId, withTxHistory = false) {
try {
if (!tokenId) {
throw new Error('tokenId required when calling getTokenData()')
}
// Throw an error if this IPFS node has not yet made a connection to a
// wallet service provider.
const selectedProvider =
@@ -529,6 +557,10 @@ class BchAdapter {
// Get token icon and other media associated with the token.
async getTokenData2 (tokenId, updateCache) {
try {
if (!tokenId) {
throw new Error('tokenId required when calling getTokenData2()')
}
// Throw an error if this IPFS node has not yet made a connection to a
// wallet service provider.
const selectedProvider =
@@ -573,6 +605,10 @@ class BchAdapter {
// Returns a promise that resolves to data when the RPC response is recieved.
async waitForRPCResponse (rpcId) {
try {
if (!rpcId) {
throw new Error('rpcId can not be false or undefined')
}
// Initialize variables for tracking the return data.
let dataFound = false
let cnt = 0
@@ -608,21 +644,25 @@ class BchAdapter {
}
// Wait between loops.
// await this.sleep(1000)
await this.ipfs.ipfsCoordAdapter.bchjs.Util.sleep(2000)
await this.sleep(2000)
// await this.ipfs.ipfsCoordAdapter.bchjs.Util.sleep(2000)
cnt++
// Exit if data was returned, or the window for a response expires.
} while (!dataFound && cnt < 10)
// console.log(`dataFound: ${dataFound}, cnt: ${cnt}`)
console.log(`dataFound: ${dataFound}, cnt: ${cnt}`)
return data
} catch (err) {
console.error('Error in waitForRPCResponse()')
console.error('Error in waitForRPCResponse(): ', err)
throw err
}
}
sleep (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
}
// module.exports = BchAdapter
+13 -2
View File
@@ -15,12 +15,13 @@ import Passport from './passport.js'
import Nodemailer from './nodemailer.js'
import BCH from './bch/index.js'
import P2WDB from './p2wdb/index.js'
import IpfsFiles from './ipfs-files/index.js'
// const { wlogger } = require('./wlogger')
import JSONFiles from './json-files.js'
import FullStackJWT from './fullstack-jwt.js'
import config from '../../config/index.js'
import Wallet from './wallet.adapter.js'
class Adapters {
constructor (localConfig = {}) {
@@ -44,6 +45,8 @@ class Adapters {
this.config = config
this.bch = new BCH(localConfig)
this.p2wdb = new P2WDB(localConfig)
this.wallet = new Wallet(localConfig)
this.ipfsFiles = new IpfsFiles(localConfig)
// Get a valid JWT API key and instance bch-js.
this.fullStackJwt = new FullStackJWT(config)
@@ -51,14 +54,22 @@ class Adapters {
async start () {
try {
let apiToken
if (this.config.getJwtAtStartup) {
// Get a JWT token and instantiate bch-js with it. Then pass that instance
// to all the rest of the apps controllers and adapters.
await this.fullStackJwt.getJWT()
apiToken = await this.fullStackJwt.getJWT()
// Instantiate bch-js with the JWT token, and overwrite the placeholder for bch-js.
this.bchjs = await this.fullStackJwt.instanceBchjs()
}
// Create a default instance of minimal-slp-wallet without initializing it
// (without retrieving the wallets UTXOs). This instance will be overwritten
// if the operator has configured BCH payments.
console.log('\nCreating default startup wallet. This wallet may be overwritten.')
await this.wallet.instanceWalletWithoutInitialization({}, { apiToken })
this.bchjs = this.wallet.bchWallet.bchjs
// Start the IPFS node.
// Do not start these adapters if this is an e2e test.
if (this.config.env !== 'test') {
+274
View File
@@ -0,0 +1,274 @@
/*
This library contains code for interfacing to the ipfs-file-pin-service using
JSON RPC over IPFS. Most of these functions are called by the /files REST API
endpoints and the IPFS Use Cases library.
*/
// Public npm libraries
import { v4 as uid } from 'uuid'
import jsonrpc from 'jsonrpc-lite'
// Local libraries
import wlogger from '../wlogger.js'
// let _this
class IpfsFilesAdapter {
constructor (localConfig = {}) {
// console.log('BCH localConfig: ', localConfig)
this.ipfs = localConfig.ipfs
if (!this.ipfs) {
throw new Error(
'An instance of IPFS must be passed when instantiating the IPFS Files Adapter library.'
)
}
// this.eventEmitter = localConfig.eventEmitter
// if (!this.eventEmitter) {
// throw new Error(
// 'An instance of an EventEmitter must be passed when instantiating the adapters.'
// )
// }
// Connect the RPC handler when the event fires with new data.
// this.eventEmitter.on('rpcData', this.rpcHandler)
// Encapsulate dependencies
this.uid = uid
this.jsonrpc = jsonrpc
// A queue for holding RPC data that has arrived.
this.rpcDataQueue = []
// _this = this // Global handle on instance of this Class.
// Bind 'this' object to all subfunctions for this Class
this.rpcHandler = this.rpcHandler.bind(this)
this.getStatus = this.getStatus.bind(this)
this.selectProvider = this.selectProvider.bind(this)
}
// This handler is triggered when RPC data comes in over IPFS.
// Handle RPC input, and add the response to the RPC queue.
// Once in the queue, it will get processed by waitForRPCResponse()
rpcHandler (data) {
try {
// Convert string input into an object.
// const jsonData = JSON.parse(data)
// console.log(`JSON RPC response for ID ${data.payload.id} received.`)
this.rpcDataQueue.push(data)
} catch (err) {
console.error('Error in files/rpcHandler(): ', err)
// Do not throw error. This is a top-level function.
}
}
// Get the status of BCH wallet services this node can talk to. Returns an
// array of BCH wallet service providers.
async getStatus () {
try {
const peerData = this.ipfs.ipfsCoordAdapter.ipfsCoord.thisNode.peerData
// console.log(`peerData: ${JSON.stringify(peerData, null, 2)}`)
// const status = {
// state: this.ipfs.ipfsCoordAdapter.state
// }
// Add names to the IPFS IDs for each provider.
const initialServiceProviders =
this.ipfs.ipfsCoordAdapter.state.ipfsFileProviders
const serviceProviders = []
for (let i = 0; i < initialServiceProviders.length; i++) {
const thisProvider = initialServiceProviders[i]
const providerData = peerData.filter((x) => x.from === thisProvider)
if (providerData.length) {
const provObj = {
ipfsId: thisProvider,
name: providerData[0].data.jsonLd.name
}
serviceProviders.push(provObj)
}
}
// console.log(
// `serviceProviders: ${JSON.stringify(serviceProviders, null, 2)}`
// )
const outObj = {
serviceProviders,
selectedProvider: this.ipfs.ipfsCoordAdapter.state.selectedIpfsFileProvider
}
// console.log('outObj: ', outObj)
return outObj
} catch (err) {
// console.log('createUser() error: ', err)
wlogger.error('Error in adapters/files/getStatus()')
throw err
}
}
// Choose the ipfs-file-pin-service to use.
async selectProvider (providerId) {
try {
this.ipfs.ipfsCoordAdapter.config.selectedIpfsFileProvider = providerId
return true
} catch (err) {
// console.log('createUser() error: ', err)
wlogger.error('Error in adapters/files/getStatus()')
throw err
}
}
// Given a CID, query the ipfs-file-pin-service to get the file metadata, like
// the file name.
async getFileMetadata (inObj = {}) {
try {
const { cid } = inObj
// Input validation.
if (!cid || typeof cid !== 'string') {
throw new Error('getFileMetadata() cid input hash must be a string.')
}
// Throw an error if this IPFS node has not yet made a connection to a
// wallet service provider.
const selectedProvider =
this.ipfs.ipfsCoordAdapter.state.selectedIpfsFileProvider
if (!selectedProvider) {
throw new Error('No IPFS File Pin Service provider available yet.')
}
const rpcData = {
endpoint: 'getFileMetadata',
cid
}
// Generate a UUID for the call.
const rpcId = this.uid()
// Generate a JSON RPC command.
const cmd = this.jsonrpc.request(rpcId, 'file-pin', rpcData)
const cmdStr = JSON.stringify(cmd)
// console.log('cmdStr: ', cmdStr)
// Send the RPC command to selected wallet service.
const thisNode = this.ipfs.ipfsCoordAdapter.ipfsCoord.thisNode
await this.ipfs.ipfsCoordAdapter.ipfsCoord.useCases.peer.sendPrivateMessage(
selectedProvider,
cmdStr,
thisNode
)
// Wait for data to come back from the wallet service.
const data = await this.waitForRPCResponse(rpcId)
// console.log('getFileMetadata() data: ', data)
return data
} catch (err) {
wlogger.error('Error in adapters/files/getFileMetadata()')
throw err
}
}
// 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 rpcData = {
endpoint: 'getPins'
}
// 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 {
// Initialize variables for tracking the return data.
let dataFound = false
let cnt = 0
// Default return value, if the remote computer does not respond in time.
let data = {
success: false,
message: 'request timed out',
data: ''
}
// Loop that waits for a response from the service provider.
do {
// console.log(`this.rpcDataQueue.length: ${this.rpcDataQueue.length}`)
for (let i = 0; i < this.rpcDataQueue.length; i++) {
const rawData = this.rpcDataQueue[i]
// console.log(`rawData: ${JSON.stringify(rawData, null, 2)}`)
if (rawData.payload.id === rpcId) {
dataFound = true
// console.log('data was found in the queue')
data = rawData.payload.result.value
// Remove the data from the queue
this.rpcDataQueue.splice(i, 1)
break
}
}
// Wait between loops.
// await this.sleep(1000)
await this.ipfs.ipfsCoordAdapter.wallet.bchjs.Util.sleep(2000)
cnt++
// Exit if data was returned, or the window for a response expires.
} while (!dataFound && cnt < 10)
// console.log(`dataFound: ${dataFound}, cnt: ${cnt}`)
return data
} catch (err) {
console.error('Error in waitForRPCResponse()')
throw err
}
}
}
// module.exports = P2wdbAdapter
export default IpfsFilesAdapter
+105 -3
View File
@@ -20,9 +20,11 @@ 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'
const FILE_PIN_PROTOCOL = 'ipfs-file-pin-service'
let _this
@@ -56,9 +58,13 @@ class IpfsCoordAdapter {
// Periodically poll services for available wallet service providers.
this.pollBchServiceInterval = setInterval(this.pollForBchServices, 10000)
this.pollIpfsFileServiceInterval = setInterval(
this.pollForIpfsFileServices,
11000
)
this.pollP2wdbServiceInterval = setInterval(
this.pollForP2wdbServices,
11000
12000
)
// State object. TODO: Make this more robust.
@@ -66,7 +72,9 @@ class IpfsCoordAdapter {
serviceProviders: [],
selectedServiceProvider: '',
p2wdbProviders: [],
selectedP2wdbProvider: ''
selectedP2wdbProvider: '',
ipfsFileProviders: [],
selectedIpfsFileProvider: ''
}
_this = this
@@ -278,6 +286,10 @@ class IpfsCoordAdapter {
_this.config.preferredProvider &&
thisPeer === _this.config.preferredProvider
) {
if (_this.state.selectedServiceProvider !== thisPeer) {
console.log(`---->BCH wallet service switched to preferred peer: ${thisPeer}`)
}
_this.state.selectedServiceProvider = thisPeer
}
@@ -308,6 +320,96 @@ class IpfsCoordAdapter {
}
}
// Poll the ipfs-coord coordination channel for available ipfs-file-pin-service
// providers. This method is called periodically by a timer-interval.
pollForIpfsFileServices () {
try {
// console.log('pollForIpfsFileServices() polling for BCH service')
// An array of IPFS IDs of other nodes in the coordination pubsub channel.
const peers = _this.ipfsCoord.thisNode.peerList
// console.log(`peers: ${JSON.stringify(peers, null, 2)}`)
// Array of objects. Each object is the IPFS ID of the peer and contains
// data about that peer.
const peerData = _this.ipfsCoord.thisNode.peerData
// console.log(`peerData: ${JSON.stringify(peerData, null, 2)}`)
for (let i = 0; i < peers.length; i++) {
const thisPeer = peers[i]
const thisData = peerData.filter((x) => x.from === thisPeer)
const thisPeerData = thisData[0]
// Create a 'fingerprint' that defines the wallet service.
const protocol = thisPeerData.data.jsonLd.protocol
const version = thisPeerData.data.jsonLd.version
// console.log(
// `debug: peer ${thisPeer} uses protocol: ${protocol} v${version}`,
// )
let versionMatches = false
if (version) {
// versionMatches = _this.semver.gt(version, MIN_FILE_PIN_VERSION)
versionMatches = _this.semver.satisfies(version, `>=${MIN_FILE_PIN_VERSION}`)
}
// Ignore any peers that don't match the fingerprint for a BCH wallet
// service.
if (protocol && protocol.includes(FILE_PIN_PROTOCOL) && versionMatches) {
// console.log('Matching peer: ', thisPeerData)
// Temporary business logic.
// Use the first available wallet service detected.
if (_this.state.ipfsFileProviders.length === 0) {
_this.state.selectedIpfsFileProvider = thisPeer
// Persist the config setting, so it can be used by other commands.
// _this.conf.set('selectedService', thisPeer)
console.log(`---->IPFS File service selected: ${thisPeer}`)
}
// console.log('preferredProvider: ', _this.config.preferredProvider)
// If a preferred provider is set in the config file, then connect
// to the preferred provider when it's discovered.
if (
_this.config.preferredIpfsFileProvider &&
thisPeer === _this.config.preferredIpfsFileProvider
) {
if (_this.state.selectedIpfsFileProvider !== thisPeer) {
console.log(`---->IPFS File service switched to preferred peer: ${thisPeer}`)
}
_this.state.selectedIpfsFileProvider = thisPeer
}
// console.log('selectedServiceProvider: ', _this.state.selectedServiceProvider)
// Check if the peer has already been added to the list of providers.
const alreadyExists = _this.state.ipfsFileProviders.filter(
(x) => x === thisPeer
)
// Add the peer to the list of serviceProviders if it doesn't already
// exist in the list.
if (!alreadyExists.length) {
_this.state.ipfsFileProviders.push(thisPeer)
}
}
}
} catch (err) {
// catch and handle known failure mode.
if (
err.message.includes("Cannot read property 'peerList' of undefined")
) {
return
}
console.error('Error in pollForIpfsFileServices(): ', err)
// Do not throw error. This is a top-level function.
}
}
// This method handles input coming in from other IPFS peers.
// It passes the data on to the REST API library by emitting an event.
// peerInputHandler (data) {
+17 -25
View File
@@ -20,13 +20,13 @@ import { yamux } from '@chainsafe/libp2p-yamux'
// import { bootstrap } from '@libp2p/bootstrap'
// import { identifyService } from 'libp2p/identify'
import { identify } from '@libp2p/identify'
// import { circuitRelayServer, circuitRelayTransport } from 'libp2p/circuit-relay'
import { circuitRelayServer, circuitRelayTransport } from '@libp2p/circuit-relay-v2'
// import { 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 } from '@libp2p/webrtc'
import { keychain } from '@libp2p/keychain'
import { defaultLogger } from '@libp2p/logger'
import { unixfs } from '@helia/unixfs'
@@ -154,7 +154,7 @@ class IpfsAdapter {
// Configure services
const services = {
identify: identify(),
pubsub: gossipsub({ allowPublishToZeroPeers: true })
pubsub: gossipsub({ allowPublishToZeroTopicPeers: true })
}
if (this.config.isCircuitRelay) {
console.log('Helia (IPFS) node IS configured as Circuit Relay')
@@ -175,25 +175,15 @@ class IpfsAdapter {
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()
]
// libp2p is the networking layer that underpins Helia
const libp2p = await this.createLibp2p({
@@ -203,8 +193,8 @@ class IpfsAdapter {
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'
`/ip4/0.0.0.0/tcp/${this.config.ipfsWsPort}/ws`
// '/webrtc'
]
},
transports,
@@ -254,6 +244,8 @@ class IpfsAdapter {
!this.fs.existsSync(`${IPFS_DIR}/datastore`) && this.fs.mkdirSync(`${IPFS_DIR}/datastore`)
// !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)
+244
View File
@@ -0,0 +1,244 @@
/*
Wallet adapter. This library instantiates minimal-slp-wallet based on different
configuration settings. That single instance of minimal-slp-wallet is then
easily passed around the rest of this application.
*/
// Global npm libraries
import BchWallet from 'minimal-slp-wallet'
// Local libraries
import config from '../../config/index.js'
import JsonFiles from './json-files.js'
class Wallet {
constructor (localConfig = {}) {
// Encapsulate dependencies
this.config = config
this.BchWallet = BchWallet
this.jsonFiles = new JsonFiles()
// Bind 'this' object to all subfunctions
this.instanceWalletWithoutInitialization = this.instanceWalletWithoutInitialization.bind(this)
}
// This is used for initializing the wallet, without waiting to update the wallet
// UTXOs from the blockchain.
// This is useful when the wallet is simply needed to make calls to the blockchain,
// and there is no need to hydrate it with UTXO data.
async instanceWalletWithoutInitialization (walletData = {}, advancedConfig = {}) {
try {
// Use the apiToken from the config settings, if one is not passed-in at
// run-time.
if (!advancedConfig.apiToken) {
advancedConfig.apiToken = this.config.apiToken
}
// If an authentication password is passed in the config, add it.
if (this.config.authPass) {
advancedConfig.authPass = this.config.authPass
}
// Detect and configure different blockchain infrastructure settings.
if (this.config.walletInterface === 'web2') {
advancedConfig.interface = 'rest-api'
advancedConfig.restURL = this.config.apiServer
} else {
// By default use the web3 Cash Stack (https://cashstack.info)
advancedConfig.interface = 'consumer-api'
advancedConfig.restURL = this.config.apiServer
}
// console.log('advancedConfig setting when creating wallet: ', advancedConfig)
// Instantiate minimal-slp-wallet.
if (walletData.mnemonic) {
// Instance the wallet using the mnemonic passed in to this function.
console.log('Mnemonic provided to wallet library from calling function.')
this.bchWallet = await this._instanceWallet(walletData.mnemonic, advancedConfig)
} else if (this.config.mnemonic) {
// Otherwise use the mnemonic in the config setting or passed as an environment variable.
console.log('Mnemonic retrieved from config file or environment variable.')
this.bchWallet = await this._instanceWallet(this.config.mnemonic, advancedConfig)
} else {
// If no mnemonic is provided, then generate a new mnemonic to create the wallet.
console.log('New mnemonic generated.')
this.bchWallet = await this._instanceWallet(undefined, advancedConfig)
}
// Wait for wallet to initialize.
await this.bchWallet.walletInfoPromise
console.log('BCH wallet initialized.')
console.log(`Wallet address: ${this.bchWallet.walletInfo.cashAddress}`)
console.log(`Wallet mnemonic: ${this.bchWallet.walletInfo.mnemonic}`)
return this.bchWallet
} catch (err) {
console.error('Error in instanceWalletWithoutInitialization()')
throw err
}
}
// This function is used for easier mocking of unit tests.
async _instanceWallet (mnemonic, config) {
const wallet = new this.BchWallet(mnemonic, config)
await wallet.walletInfoPromise
return wallet
}
// Open the wallet file, or create one if the file doesn't exist.
// Does not instance the wallet. The output of this function is expected to
// be passed to instanceWallet().
async openWallet (advancedConfig = {}) {
try {
let walletData
// Try to open the wallet.json file.
try {
walletData = await this.jsonFiles.readJSON(this.config.walletFile)
walletData = walletData.wallet
} catch (err) {
// Create a new wallet file if one does not already exist.
console.log('Wallet file not found. Creating new wallet.json file.')
// Create a new wallet.
// No-Update flag creates wallet without making any network calls.
const walletInstance = await this.instanceWalletWithoutInitialization({}, advancedConfig)
walletData = walletInstance.walletInfo
// Add the nextAddress property
walletData.nextAddress = 1
// Write the wallet data to the JSON file.
await this.jsonFiles.writeJSON({ wallet: walletData }, this.config.walletFile)
}
// console.log('walletData: ', walletData)
return walletData
} catch (err) {
console.error('Error in openWallet()')
throw err
}
}
// Create an instance of minimal-slp-wallet. Same as
// instanceWalletWithoutInitialization(), but waits for the wallet to initialize
// its UTXOs (wallet balance and tokens).
async instanceWallet (walletData = {}, advancedConfig = {}) {
try {
// Instance the wallet without initialization.
await this.instanceWalletWithoutInitialization(walletData, advancedConfig)
// Initialize the wallet
await this.bchWallet.initialize()
return this.bchWallet
} catch (err) {
console.error('Error in wallet.js/instanceWallet()')
throw err
}
}
// Increments the 'nextAddress' property in the wallet file. This property
// indicates the HD index that should be used to generate a key pair for
// storing funds for Offers.
// This function opens the wallet file, increments the nextAddress property,
// then saves the change to the wallet file.
async incrementNextAddress () {
try {
const walletData = await this.openWallet()
await this.instanceWalletWithoutInitialization(walletData)
walletData.nextAddress++
// console.log('walletData finish: ', walletData)
await this.jsonFiles.writeJSON({ wallet: walletData }, this.config.walletFile)
// Update the working instance of the wallet.
this.bchWallet.walletInfo.nextAddress++
// console.log('this.bchWallet.walletInfo: ', this.bchWallet.walletInfo)
return walletData.nextAddress
} catch (err) {
console.error('Error in incrementNextAddress()')
throw err
}
}
// This method returns an object that contains a private key WIF, public address,
// and the index of the HD wallet that the key pair was generated from.
// TODO: Allow input integer. If input is used, use that as the index. If no
// input is provided, then call incrementNextAddress().
async getKeyPair (hdIndex = 0) {
try {
if (!hdIndex) {
// Increment the HD index and generate a new key pair.
hdIndex = await this.incrementNextAddress()
}
const mnemonic = this.bchWallet.walletInfo.mnemonic
// root seed buffer
const rootSeed = await this.bchWallet.bchjs.Mnemonic.toSeed(mnemonic)
const masterHDNode = this.bchWallet.bchjs.HDNode.fromSeed(rootSeed)
// HDNode of BIP44 account
// const account = this.bchWallet.bchjs.HDNode.derivePath(masterHDNode, "m/44'/245'/0'")
const childNode = masterHDNode.derivePath(`m/44'/245'/0'/0/${hdIndex}`)
const cashAddress = this.bchWallet.bchjs.HDNode.toCashAddress(childNode)
console.log('Generating a new key pair for cashAddress: ', cashAddress)
const wif = this.bchWallet.bchjs.HDNode.toWIF(childNode)
const outObj = {
cashAddress,
wif,
hdIndex
}
return outObj
} catch (err) {
console.error('Error in getKeyPair()')
throw err
}
}
// Optimize the wallet by consolidating the UTXOs.
async optimize () {
const UTXO_THREASHOLD = 7
// Do a dry-run first to see if there are enough UTXOs worth consolidating.
const dryRunOut = await this.bchWallet.optimize(true)
if (dryRunOut.bchUtxoCnt > UTXO_THREASHOLD) {
// Consolidate BCH UTXOs if the count is above the threashold.
const txids = await this.bchWallet.optimize()
console.log(`Wallet optimized with these return values: ${JSON.stringify(txids, null, 2)}`)
}
return true
}
// Get the balance of the wallet in sats and PSF tokens.
// This function is called by the GET /entry/balance controller.
async getBalance () {
const balance = await this.bchWallet.getBalance()
// console.log('balance: ', balance)
const tokens = await this.bchWallet.listTokens()
// console.log('tokens: ', tokens)
// Find the array entry for the PSF token
const psfTokens = tokens.find(x => x.tokenId === '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0')
// console.log('psfTokens: ', psfTokens)
let psfBalance = 0
if (psfTokens) {
psfBalance = psfTokens.qty
}
const outObj = {
satBalance: balance,
psfBalance,
success: true
}
return outObj
}
}
export default Wallet
+7
View File
@@ -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.
+6
View File
@@ -97,6 +97,7 @@ class JSONRPC {
wlogger.info(
`JSON RPC received from ${from}, ID: ${parsedData.payload.id}, type: ${parsedData.type}`
)
// console.log('parsedData: ', JSON.stringify(parsedData, null, 2))
}
// console.log(`parsedData: ${JSON.stringify(parsedData, null, 2)}`)
@@ -113,6 +114,11 @@ class JSONRPC {
console.log('routing to BCH adapter')
retObj = await _this.adapters.bch.rpcHandler(parsedData)
}
if (parsedData.payload.result.method === 'file-pin') {
console.log('routing to IPFS Files adapter')
retObj = await _this.adapters.ipfsFiles.rpcHandler(parsedData)
}
} catch (err) {
/* exit quietly */
}
@@ -723,6 +723,33 @@ class BchRESTControllerLib {
}
}
/**
* @api {get} /bch/service Get the IPFS ID for the Wallet service
* @apiPermission public
* @apiName getService
* @apiGroup REST BCH
* @apiDescription Get the IPFS ID for the ipfs-bch-wallet-service node that
* this app is using to retrieve blockchain data from.
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5015/bch/service
*
*/
async getService (ctx) {
try {
const selectedServiceProvider = this.adapters.ipfs.ipfsCoordAdapter.state.selectedServiceProvider
ctx.body = {
success: true,
selectedServiceProvider
}
} catch (err) {
// wlogger.error('Error in ipfs/controller.js/viewFile(): ', err)
console.log('Error in ipfs/controller.js/getService(): ', err)
this.handleError(ctx, err)
}
}
// DRY error handler
handleError (ctx, err) {
// If an HTTP status is specified by the buisiness logic, use that.
+5
View File
@@ -63,6 +63,7 @@ class BchRouter {
this.router.post('/utxoIsValid', this.utxoIsValid)
this.router.post('/getTokenData', this.getTokenData)
this.router.post('/getTokenData2', this.getTokenData2)
this.router.get('/service', this.getService)
// Attach the Controller routes to the Koa app.
app.use(this.router.routes())
@@ -116,6 +117,10 @@ class BchRouter {
async getTokenData2 (ctx, next) {
await _this.bchRESTController.getTokenData2(ctx, next)
}
async getService (ctx, next) {
await _this.bchRESTController.getService(ctx, next)
}
}
// module.exports = BchRouter
+19 -7
View File
@@ -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)
}
}
+130 -2
View File
@@ -3,6 +3,7 @@
*/
// Global npm libraries
import mime from 'mime-types'
// Local libraries
import wlogger from '../../../adapters/wlogger.js'
@@ -34,13 +35,17 @@ class IpfsRESTControllerLib {
this.handleError = this.handleError.bind(this)
this.connect = this.connect.bind(this)
this.getThisNode = this.getThisNode.bind(this)
this.viewFile = this.viewFile.bind(this)
this.getService = this.getService.bind(this)
this.getFileInfo = this.getFileInfo.bind(this)
this.getPins = this.getPins.bind(this)
}
/**
* @api {get} /ipfs Get status on IPFS infrastructure
* @apiPermission public
* @apiName GetIpfsStatus
* @apiGroup REST BCH
* @apiGroup REST IPFS
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5001/ipfs
@@ -107,7 +112,7 @@ class IpfsRESTControllerLib {
* @api {get} /ipfs/node Get a copy of the thisNode object from helia-coord
* @apiPermission public
* @apiName GetThisNode
* @apiGroup REST BCH
* @apiGroup REST IPFS
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5001/ipfs/node
@@ -125,6 +130,129 @@ class IpfsRESTControllerLib {
}
}
/**
* @api {get} /ipfs/view/:cid Retrieve and display a file via its IPFS CID
* @apiPermission public
* @apiName GetCidView
* @apiGroup REST IPFS
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5001/ipfs/view/bafkreieaqtdhfywyddomswogynzymukosqqgqo7lkt5lch2zwfnc55m6om
*
*/
async viewFile (ctx) {
try {
const { cid } = ctx.params
// const file = await this.adapters.ipfs.ipfs.blockstore.get(cid)
// return file
// const cid = ctx.params.cid
const { filename, readStream } = await this.useCases.ipfs.downloadCid({ cid })
// ctx.body = ctx.req.pipe(readStream)
// Lookup the mime type from the filename.
const contentType = mime.lookup(filename)
ctx.set('Content-Type', contentType)
ctx.set(
'Content-Disposition',
// 'inline; filename="' + filename + '"'
`inline; filename="${filename}"`
)
ctx.body = readStream
} catch (err) {
// wlogger.error('Error in ipfs/controller.js/viewFile(): ', err)
console.log('Error in ipfs/controller.js/viewFile(): ', err)
this.handleError(ctx, err)
}
}
/**
* @api {get} /ipfs/service Get the IPFS ID for the File Pin service
* @apiPermission public
* @apiName GetService
* @apiGroup REST IPFS
* @apiDescription Get the IPFS ID for the ipfs-file-pin-service node that
* this app is using to retrieve file data from.
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5015/ipfs/service
*
*/
async getService (ctx) {
try {
const selectedIpfsFileProvider = this.adapters.ipfs.ipfsCoordAdapter.state.selectedIpfsFileProvider
ctx.body = {
success: true,
selectedIpfsFileProvider
}
} catch (err) {
// wlogger.error('Error in ipfs/controller.js/viewFile(): ', err)
console.log('Error in ipfs/controller.js/getService(): ', err)
this.handleError(ctx, err)
}
}
/**
* @api {get} /ipfs/file-info/:cid Get file pin info about a CID
* @apiPermission public
* @apiName GetFileInfo
* @apiGroup REST IPFS
* @apiDescription Get file metadata and pin status information give a CID.
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5015/ipfs/file-info/bafkreieaqtdhfywyddomswogynzymukosqqgqo7lkt5lch2zwfnc55m6om
*
*/
async getFileInfo (ctx) {
try {
const { cid } = ctx.params
const ipfsFiles = this.adapters.ipfsFiles
const metadata = await ipfsFiles.getFileMetadata({ cid })
console.log('getCidMetadata() metadata: ', metadata)
ctx.body = metadata
} catch (err) {
// wlogger.error('Error in ipfs/controller.js/viewFile(): ', err)
console.log('Error in ipfs/controller.js/getService(): ', err)
this.handleError(ctx, err)
}
}
/**
* @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 { cid } = ctx.params
const ipfsFiles = this.adapters.ipfsFiles
const pinData = await ipfsFiles.getPins()
console.log('getPins() pinData: ', pinData)
ctx.body = pinData
} catch (err) {
// wlogger.error('Error in ipfs/controller.js/viewFile(): ', err)
console.log('Error in ipfs/controller.js/getPins(): ', err)
this.handleError(ctx, err)
}
}
// DRY error handler
handleError (ctx, err) {
// If an HTTP status is specified by the buisiness logic, use that.
+4
View File
@@ -56,6 +56,10 @@ class IpfsRouter {
this.router.post('/relays', this.ipfsRESTController.getRelays)
this.router.post('/connect', this.ipfsRESTController.connect)
this.router.get('/node', this.ipfsRESTController.getThisNode)
this.router.get('/view/:cid', this.ipfsRESTController.viewFile)
this.router.get('/service', this.ipfsRESTController.getService)
this.router.get('/file-info/:cid', this.ipfsRESTController.getFileInfo)
this.router.get('/pins', this.ipfsRESTController.getPins)
// Attach the Controller routes to the Koa app.
app.use(this.router.routes())
+20 -1
View File
@@ -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.
+5
View File
@@ -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,121 @@
/*
REST API Controller library for the /ipfs 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 /ipfs REST Controller.'
)
}
this.useCases = localConfig.useCases
if (!this.useCases) {
throw new Error(
'Instance of Use Cases library required when instantiating /ipfs REST Controller.'
)
}
// Encapsulate dependencies
// this.UserModel = this.adapters.localdb.Users
// this.userUseCases = this.useCases.user
// 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
+65
View File
@@ -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
+17 -2
View File
@@ -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,19 +37,21 @@ 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.
exampleTimerFunc (negativeTest) {
try {
console.log('Example timer controller executed.')
// console.log('Example timer controller executed.')
if (negativeTest) throw new Error('test error')
@@ -60,6 +63,18 @@ class TimerControllers {
return false
}
}
// Clean the usage state so that stats reflect the last 24 hours.
cleanUsage () {
try {
this.useCases.usage.cleanUsage()
} 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
+5
View File
@@ -4,7 +4,10 @@
https://troutsblog.com/blog/clean-architecture
*/
// 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 = {}) {
@@ -23,6 +26,8 @@ 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.
+206
View File
@@ -0,0 +1,206 @@
/*
Use cases for working with IPFS.
*/
// Global npm libraries
// import Wallet from 'minimal-slp-wallet'
// import { CID } from 'multiformats'
// import RetryQueue from '@chris.troutner/retry-queue'
// import { exporter } from 'ipfs-unixfs-exporter'
import { Duplex } from 'stream'
import PSFFPP from 'psffpp'
// Local libraries
// import PinEntity from '../entities/pin.js'
// import config from '../../config/index.js'
// const PSF_TOKEN_ID = '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0'
class IpfsUseCases {
constructor (localConfig = {}) {
// console.log('User localConfig: ', localConfig)
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of adapters must be passed in when instantiating IPFS Use Cases library.'
)
}
// Encapsulate dependencies
this.psffpp = null // placeholder
// 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)
// 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
if (!cid) throw new Error('CID is undefined')
// const Pins = this.adapters.localdb.Pins
// let existingModel = await Pins.find({ cid })
// existingModel = existingModel[0]
// console.log('existingModel: ', existingModel)
// if (!existingModel) {
// throw new Error(`Database model for CID ${cid} does not exist.`)
// }
//
// if (!existingModel.dataPinned) {
// throw new Error('File has not been pinned. Not available.')
// }
const filename = await this.getCidMetadata({ cid })
const helia = this.adapters.ipfs.ipfs
// Convert the file to a Buffer.
const fileChunks = []
for await (const chunk of helia.fs.cat(cid)) {
fileChunks.push(chunk)
}
const fileBuf = Buffer.concat(fileChunks)
// Convert the Buffer into a readable stream
const bufferToStream = (myBuffer) => {
const tmp = new Duplex()
tmp.push(myBuffer)
tmp.push(null)
return tmp
}
const readStream = bufferToStream(fileBuf)
// const filename = 'test.jpg'
return { filename, readStream }
} catch (err) {
console.error('Error in use-cases/ipfs.js/dowloadCid()')
throw err
}
}
// 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
// delivered to a web browser.
async getCidMetadata (inObj = {}) {
try {
const { cid } = inObj
const ipfsFiles = this.adapters.ipfsFiles
const metadata = await ipfsFiles.getFileMetadata({ cid })
console.log('getCidMetadata() metadata: ', metadata)
if (metadata.success === false) {
throw new Error(metadata.message)
}
const filename = metadata.fileMetadata.filename
return filename
} catch (err) {
console.error('Error in getCidMetadata(): ', err)
throw err
}
}
// async downloadCid2 (inObj = {}) {
// try {
// const { cid } = inObj
//
// // console.log(`downloadFile() retrieving this CID: ${cid}, with fileName: ${fileName}, and path: ${path}`)
//
// const blockstore = this.adapters.ipfs.ipfs.blockstore
// const entry = await this.exporter(cid, blockstore)
//
// console.info(entry.cid) // Qmqux
// console.log('entry: ', entry)
// // console.info(entry.unixfs.fileSize()) // 4
//
// // const filePath = `${path}/${fileName}`
// // console.log(`filePath: ${filePath}`)
// // const writableStream = this.fs.createWriteStream(filePath)
// //
// // writableStream.on('error', this.writeStreamError)
// //
// // writableStream.on('finish', this.writeStreamFinished)
// //
//
// const fileChunks = []
// for await (const buf of entry.content()) {
// fileChunks.push(buf)
// }
// const fileBuf = Buffer.concat(fileChunks)
//
// //
// // writableStream.end()
//
// // Convert the Buffer into a readable stream
// const bufferToStream = (myBuffer) => {
// const tmp = new Duplex()
// tmp.push(myBuffer)
// tmp.push(null)
// return tmp
// }
// const readStream = bufferToStream(fileBuf)
//
// const filename = 'test.jpg'
//
// return { filename, readStream }
//
// // return { cid }
// } catch (err) {
// console.error('Error in ipfs-use-cases.js/downloadCid()')
// throw err
// }
// }
}
export default IpfsUseCases
+135
View File
@@ -0,0 +1,135 @@
/*
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.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)
} 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 }
+89 -88
View File
@@ -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
+93 -89
View File
@@ -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')
}
})
})
})
}
})
+13 -23
View File
@@ -94,25 +94,12 @@ describe('#bch-use-case', () => {
it('should catch and throw an error', async () => {
try {
// Force an error
uut.ipfs.ipfsCoordAdapter.bchjs = {
Util: {
sleep: () => {
throw new Error('test error')
}
}
}
// Mock data.
const rpcId = '123'
uut.rpcDataQueue.push(mockData.rpcData)
await uut.waitForRPCResponse(rpcId)
await uut.waitForRPCResponse()
assert.fail('Unexpected code path')
} catch (err) {
// console.log('error: ', err)
assert.include(err.message, 'test error')
assert.include(err.message, 'rpcId can not be false or undefined')
}
})
})
@@ -156,6 +143,9 @@ describe('#bch-use-case', () => {
it('should catch and throw an error', async () => {
try {
// Force an error
sandbox.stub(uut, 'uid').throws(new Error('test error'))
await uut.getBalances()
assert.fail('Unexpected code path')
@@ -191,7 +181,7 @@ describe('#bch-use-case', () => {
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.equal(err.message, 'test error')
assert.equal(err.message, 'addr required when calling getUtxos')
}
})
})
@@ -267,7 +257,7 @@ describe('#bch-use-case', () => {
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.equal(err.message, 'test error')
assert.equal(err.message, 'hex required when calling broadcast')
}
})
})
@@ -297,7 +287,7 @@ describe('#bch-use-case', () => {
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.equal(err.message, 'test error')
assert.equal(err.message, 'address required when calling getTransactions()')
}
})
})
@@ -327,7 +317,7 @@ describe('#bch-use-case', () => {
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.equal(err.message, 'test error')
assert.equal(err.message, 'txids required when calling getTransaction()')
}
})
})
@@ -357,7 +347,7 @@ describe('#bch-use-case', () => {
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.equal(err.message, 'test error')
assert.equal(err.message, 'address required when calling getPubKey()')
}
})
})
@@ -387,7 +377,7 @@ describe('#bch-use-case', () => {
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.equal(err.message, 'test error')
assert.equal(err.message, 'utxo required when calling utxoIsValid()')
}
})
})
@@ -417,7 +407,7 @@ describe('#bch-use-case', () => {
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.equal(err.message, 'test error')
assert.equal(err.message, 'tokenId required when calling getTokenData()')
}
})
})
@@ -447,7 +437,7 @@ describe('#bch-use-case', () => {
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.equal(err.message, 'test error')
assert.equal(err.message, 'tokenId required when calling getTokenData2()')
}
})
})
+344
View File
@@ -0,0 +1,344 @@
/*
Unit tests for the Wallet Adapter library.
*/
// Global npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
import BchWallet from 'minimal-slp-wallet'
import fs from 'fs'
// Local libraries
import WalletAdapter from '../../../src/adapters/wallet.adapter.js'
import { MockBchWallet } from '../mocks/adapters/wallet.js'
// Hack to get __dirname back.
// https://blog.logrocket.com/alternatives-dirname-node-js-es-modules/
import * as url from 'url'
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
// Global constants
const testWalletFile = `${__dirname.toString()}test-wallet.json`
describe('#wallet', () => {
let uut
let sandbox
before(() => {
// Delete the test file if it exists.
try {
deleteFile(testWalletFile)
} catch (err) { }
})
beforeEach(() => {
uut = new WalletAdapter()
sandbox = sinon.createSandbox()
})
afterEach(() => sandbox.restore())
after(() => {
// Delete the test file if it exists.
try {
deleteFile(testWalletFile)
} catch (err) { }
})
describe('#_instanceWallet', () => {
it('should create a wallet given a mnemonic', async () => {
const mnemonic = 'wagon tray learn flat erase laugh lonely rug check captain jacket morning'
const result = await uut._instanceWallet(mnemonic)
// console.log('result: ', result)
assert.equal(result.walletInfo.mnemonic, mnemonic)
})
})
describe('#openWallet', () => {
it('should create a new wallet when wallet file does not exist', async () => {
// Mock dependencies
uut.BchWallet = MockBchWallet
// Ensure we open the test file, not the production wallet file.
uut.config.walletFile = testWalletFile
const result = await uut.openWallet()
// console.log('result: ', result)
assert.property(result, 'mnemonic')
assert.property(result, 'privateKey')
assert.property(result, 'publicKey')
assert.property(result, 'cashAddress')
assert.property(result, 'address')
assert.property(result, 'slpAddress')
assert.property(result, 'legacyAddress')
assert.property(result, 'hdPath')
})
it('should open existing wallet file', async () => {
// This test case uses the file created in the previous test case.
// Ensure we open the test file, not the production wallet file.
uut.config.walletFile = testWalletFile
const result = await uut.openWallet()
// console.log('result: ', result)
assert.property(result, 'mnemonic')
assert.property(result, 'privateKey')
assert.property(result, 'publicKey')
assert.property(result, 'cashAddress')
assert.property(result, 'address')
assert.property(result, 'slpAddress')
assert.property(result, 'legacyAddress')
assert.property(result, 'hdPath')
})
it('should catch and throw an error', async () => {
try {
// Force an error
uut.config.walletFile = ''
uut.BchWallet = () => {
}
await uut.openWallet()
// console.log('result: ', result)
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'this.BchWallet is not a constructor')
}
})
})
describe('#instanceWalletWithoutInitialization', () => {
it('should create an instance of BchWallet', async () => {
// Create a mock wallet.
const mockWallet = new BchWallet()
await mockWallet.walletInfoPromise
sandbox.stub(mockWallet, 'initialize').resolves()
// Mock dependencies
sandbox.stub(uut, '_instanceWallet').resolves(mockWallet)
uut.config.authPass = 'fake-auth-pass'
// Ensure we open the test file, not the production wallet file.
uut.config.walletFile = testWalletFile
const walletData = await uut.openWallet()
// console.log('walletData: ', walletData)
const result = await uut.instanceWalletWithoutInitialization(walletData)
// console.log('result: ', result)
assert.property(result, 'walletInfoPromise')
assert.property(result, 'walletInfo')
})
it('should catch and throw an error', async () => {
try {
// Force an error
sandbox.stub(uut, '_instanceWallet').rejects(new Error('test error'))
await uut.instanceWalletWithoutInitialization()
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'test error')
}
})
it('should create an instance of BchWallet using web2 infra', async () => {
// Create a mock wallet.
const mockWallet = new BchWallet()
await mockWallet.walletInfoPromise
sandbox.stub(mockWallet, 'initialize').resolves()
// Mock dependencies
sandbox.stub(uut, '_instanceWallet').resolves(mockWallet)
// Ensure we open the test file, not the production wallet file.
uut.config.walletFile = testWalletFile
const walletData = await uut.openWallet()
// console.log('walletData: ', walletData)
// Force desired code path
uut.config.walletInterface = 'web2'
const result = await uut.instanceWalletWithoutInitialization(walletData)
// console.log('result: ', result)
assert.property(result, 'walletInfoPromise')
assert.property(result, 'walletInfo')
})
it('should generate wallet from mnemonic in config', async () => {
// Create a mock wallet.
const mockWallet = new BchWallet()
await mockWallet.walletInfoPromise
sandbox.stub(mockWallet, 'initialize').resolves()
// Mock dependencies
sandbox.stub(uut, '_instanceWallet').resolves(mockWallet)
// Ensure we open the test file, not the production wallet file.
uut.config.walletFile = testWalletFile
const walletData = await uut.openWallet()
// console.log('walletData: ', walletData)
const originalConfig = uut.config.mnemonic
uut.config.mnemonic = walletData.mnemonic
const result = await uut.instanceWalletWithoutInitialization({})
// console.log('result: ', result)
uut.config.mnemonic = originalConfig
assert.property(result, 'walletInfoPromise')
assert.property(result, 'walletInfo')
})
})
describe('#instanceWallet', () => {
it('should create an instance of BchWallet', async () => {
// Create a mock wallet.
const mockWallet = new BchWallet()
await mockWallet.walletInfoPromise
sandbox.stub(mockWallet, 'initialize').resolves()
// Mock dependencies
sandbox.stub(uut, '_instanceWallet').resolves(mockWallet)
// Ensure we open the test file, not the production wallet file.
uut.WALLET_FILE = testWalletFile
const walletData = await uut.openWallet()
// console.log('walletData: ', walletData)
const result = await uut.instanceWallet(walletData)
// console.log('result: ', result)
assert.property(result, 'walletInfoPromise')
assert.property(result, 'walletInfo')
})
it('should catch and throw an error', async () => {
try {
// Force an error
sandbox.stub(uut, 'instanceWalletWithoutInitialization').rejects(new Error('test error'))
await uut.instanceWallet()
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'test error')
}
})
it('should create an instance of BchWallet using web2 infra', async () => {
// Create a mock wallet.
const mockWallet = new BchWallet()
await mockWallet.walletInfoPromise
sandbox.stub(mockWallet, 'initialize').resolves()
// Mock dependencies
sandbox.stub(uut, '_instanceWallet').resolves(mockWallet)
// Ensure we open the test file, not the production wallet file.
uut.WALLET_FILE = testWalletFile
const walletData = await uut.openWallet()
// console.log('walletData: ', walletData)
// Force desired code path
uut.config.useFullStackCash = true
const result = await uut.instanceWallet(walletData)
// console.log('result: ', result)
assert.property(result, 'walletInfoPromise')
assert.property(result, 'walletInfo')
})
})
describe('#incrementNextAddress', () => {
it('should increment the nextAddress property', async () => {
// Ensure we open the test file, not the production wallet file.
uut.WALLET_FILE = testWalletFile
// mock instance of minimal-slp-wallet
uut.bchWallet = new MockBchWallet()
const result = await uut.incrementNextAddress()
assert.equal(result, 2)
})
it('should catch and throw an error', async () => {
try {
// Force an error
sandbox.stub(uut, 'openWallet').rejects(new Error('test error'))
await uut.incrementNextAddress()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#getKeyPair', () => {
it('should return an object with a key pair', async () => {
// Ensure we open the test file, not the production wallet file.
// uut.WALLET_FILE = testWalletFile
uut.config.walletFile = testWalletFile
const result = await uut.getKeyPair()
// console.log('result: ', result)
assert.property(result, 'cashAddress')
assert.property(result, 'wif')
assert.property(result, 'hdIndex')
})
it('should catch and throw an error', async () => {
try {
// Force an error
sandbox
.stub(uut, 'incrementNextAddress')
.rejects(new Error('test error'))
await uut.getKeyPair()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#optimize', () => {
it('should call the wallet optimize function', async () => {
// mock instance of minimal-slp-wallet
uut.bchWallet = new MockBchWallet()
sandbox.stub(uut.bchWallet, 'optimize').resolves({ bchUtxoCnt: 10 })
const result = await uut.optimize()
assert.equal(result, true)
})
})
describe('#getBalance', () => {
it('should get the balance for the wallet', async () => {
// mock instance of minimal-slp-wallet
uut.bchWallet = new MockBchWallet()
// Mock dependencies and force desired code path
sandbox.stub(uut.bchWallet, 'getBalance').resolves(41012)
sandbox.stub(uut.bchWallet, 'listTokens').resolves([{
tokenId: '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
ticker: 'PSF',
name: 'Permissionless Software Foundation',
decimals: 8,
tokenType: 1,
url: 'psfoundation.cash',
qty: 2
}])
const result = await uut.getBalance()
// console.log('result: ', result)
// Assert the expected properties exist and have the expected values.
assert.equal(result.satBalance, 41012)
assert.equal(result.psfBalance, 2)
assert.equal(result.success, true)
})
})
})
const deleteFile = (filepath) => {
try {
// Delete state if exist
fs.unlinkSync(filepath)
} catch (err) {
// console.error('Error trying to delete file: ', err)
}
}
+5 -1
View File
@@ -38,9 +38,13 @@ describe('#config', () => {
assert.equal(config.env, 'test')
})
it('Should return test environment config', async () => {
it('Should return prod environment config', async () => {
process.env.SVC_ENV = 'prod'
process.env.WALLET_INTERFACE = 'web2'
process.env.APISERVER = 'https://api.fullstack.cash/v5/'
await import('../../../config/env/common.js?foo=bar2')
const importedConfig3 = await import('../../../config/index.js?foo=bar2')
const config = importedConfig3.default
// console.log('config: ', config)
+1 -1
View File
@@ -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/',
+149
View File
@@ -0,0 +1,149 @@
import BCHJS from "@psf/bch-js";
const mockWallet = {
mnemonic: 'course abstract aerobic deer try switch turtle diet fence affair butter top',
privateKey: 'L5D2UAam8tvo3uii5kpgaGyjvVMimdrXu8nWGQSQjuuAix6ji1YQ',
publicKey: '0379433ffc401483ade310469953c1cba77c71af904f07c15bde330d7198b4d6dc',
cashAddress: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
address: 'bitcoincash:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acv7fqtd00',
slpAddress: 'simpleledger:qzl0d3gcqeypv4cy7gh8rgdszxa9vvm2acq9zm7d33',
legacyAddress: '1JQj1KcQL7GPKzc1D2PvdUSgw3MbDtrHzi',
hdPath: "m/44'/245'/0'/0/0",
nextAddress: 1
};
class MockBchWallet {
constructor() {
this.walletInfoPromise = true;
this.walletInfo = mockWallet;
this.initialize = async () => {}
this.bchjs = new BCHJS();
this.burnTokens = async () => {
return { success: true, txid: 'txid' };
};
this.sendTokens = async () => {
return 'fakeTxid';
};
this.getUtxos = async () => { };
this.getBalance = async () => { };
this.listTokens = async () => { };
this.getTxData = async () => {
return [{
tokenTicker: 'TROUT'
}];
};
this.optimize = async () => { };
// Environment variable is used by wallet-balance.unit.js to force an error.
if (process.env.NO_UTXO) {
this.utxos = {};
}
else {
this.utxos = {
utxoStore: {
address: 'bitcoincash:qqetvdnlt0p8g27dr44cx7h057kpzly9xse9huc97z',
bchUtxos: [
{
height: 700685,
tx_hash: '1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
tx_pos: 0,
value: 1000,
txid: '1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
vout: 0,
isValid: false
},
{
height: 700685,
tx_hash: '1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
tx_pos: 2,
value: 19406,
txid: '1fc577caaff5626a8477162581e57bae1b19dc6aa6c10638013c2b1ba14dc654',
vout: 2,
isValid: false
}
],
nullUtxos: [],
slpUtxos: {
type1: {
mintBatons: [],
tokens: [
{
'height': 717331,
'tx_hash': '74889580bb1a5f8c026aa2f55118ac9917df3332f7abae72a70343daa1c29621',
'tx_pos': 1,
'value': 546,
'txid': '74889580bb1a5f8c026aa2f55118ac9917df3332f7abae72a70343daa1c29621',
'vout': 1,
'isSlp': true,
'type': 'token',
'qty': '10',
'tokenId': '600ee24d0f208aebc2bdd2c4ee1b9acb6d57343561442e8676b5bbea311d5a0f',
'address': 'bitcoincash:qqraj35x6l2qyqhjm5l7qlt7z2245ez8l5z3dwkeq5',
'ticker': 'FLIPS',
'name': 'FLIPS',
'documentUri': '',
'documentHash': '',
'decimals': 1,
'qtyStr': '1'
},
{
'height': 730597,
'tx_hash': '52520faddfafc46b8f8c9548b097f3a3b82a5bf363b5095047b9c5f83247fe36',
'tx_pos': 1,
'value': 546,
'txid': '52520faddfafc46b8f8c9548b097f3a3b82a5bf363b5095047b9c5f83247fe36',
'vout': 1,
'isSlp': true,
'type': 'token',
'qty': '34999991',
'tokenId': '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
'address': 'bitcoincash:qqraj35x6l2qyqhjm5l7qlt7z2245ez8l5z3dwkeq5',
'ticker': 'PSF',
'name': 'Permissionless Software Foundation',
'documentUri': 'psfoundation.cash',
'documentHash': '',
'decimals': 8,
'qtyStr': '0.34999991'
},
{
'height': 730597,
'tx_hash': '5dc7e7c91382aed1666a51212dfb74050261e12c3c4f62b6b1e57f42d6c51ee1',
'tx_pos': 2,
'value': 546,
'txid': '5dc7e7c91382aed1666a51212dfb74050261e12c3c4f62b6b1e57f42d6c51ee1',
'vout': 2,
'isSlp': true,
'type': 'token',
'qty': '18898',
'tokenId': 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
'address': 'bitcoincash:qqraj35x6l2qyqhjm5l7qlt7z2245ez8l5z3dwkeq5',
'ticker': 'TROUT',
'name': "Trout's test token",
'documentUri': 'troutsblog.com',
'documentHash': '',
'decimals': 2,
'qtyStr': '188.98'
}
]
},
nft: {
tokens: []
},
group: {
tokens: [],
mintBatons: []
}
}
}
};
}
}
}
export { MockBchWallet };
export { mockWallet };
export default {
MockBchWallet,
mockWallet
};