mirror of
https://github.com/Permissionless-Software-Foundation/ipfs-bch-wallet-consumer.git
synced 2026-09-22 01:02:04 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
173f706a61 | ||
|
|
39fc919d35 | ||
|
|
6f0726aeb8 | ||
|
|
2568818ea1 | ||
|
|
c721da1562 | ||
|
|
3606a2d31b | ||
|
|
764905b041 | ||
|
|
556c6ec522 | ||
|
|
04395b70a6 | ||
|
|
4564090830 | ||
|
|
81521151f4 | ||
|
|
5a819c1d9c | ||
|
|
f2f3208fda | ||
|
|
7fe888cdda | ||
|
|
db3f1bf2cf | ||
|
|
512a49200d | ||
|
|
c6019df364 | ||
|
|
b7988253df | ||
|
|
5f64257dca | ||
|
|
2fbdccb883 | ||
|
|
ecc10afac3 | ||
|
|
b55afd5f93 | ||
|
|
12037a5e57 | ||
|
|
4fc54aa9c5 | ||
|
|
2bd7f0c45c | ||
|
|
900423339e | ||
|
|
d907afa6bb | ||
|
|
9b116d82b7 | ||
|
|
1d1bf50ee3 | ||
|
|
5002004697 | ||
|
|
5b0fac4301 | ||
|
|
9584810b16 | ||
|
|
f311a3b9c6 | ||
|
|
83f16709d8 | ||
|
|
fe91f4b875 | ||
|
|
2d9a2b811c | ||
|
|
9360cf6f77 | ||
|
|
514b65ed24 | ||
|
|
0c7f3088a8 | ||
|
|
aef88eb369 | ||
|
|
2f6cc72f41 | ||
|
|
12cab02300 | ||
|
|
04a908261a | ||
|
|
f16eacc3f2 | ||
|
|
83898742f1 | ||
|
|
1795918719 | ||
|
|
6894217078 | ||
|
|
3ffbed27b5 | ||
|
|
c30b697bc6 | ||
|
|
df2f69050b | ||
|
|
b49979971c | ||
|
|
7a620892d7 | ||
|
|
b372e3d260 | ||
|
|
707b385b8a | ||
|
|
6cb140a491 | ||
|
|
c43ee8c25a | ||
|
|
0a7ec11510 | ||
|
|
4a1524629c | ||
|
|
f7aaba71d1 | ||
|
|
418a2e2ad2 | ||
|
|
74248ea0c7 | ||
|
|
70a69e98bb | ||
|
|
8cffd7fae3 | ||
|
|
a1023e8224 | ||
|
|
a32ed63d47 | ||
|
|
934fb46ffc | ||
|
|
b27136d03b | ||
|
|
064ac36dc7 | ||
|
|
5e2cf86f4b | ||
|
|
425d2ada65 | ||
|
|
9837d6a7fb | ||
|
|
60a3d67c0b | ||
|
|
a03c5d8c91 | ||
|
|
0e7afb5f51 | ||
|
|
bcc90da49b | ||
|
|
b473a3803c | ||
|
|
38ed9a8dac | ||
|
|
0ca46df43e | ||
|
|
b5f09fbab9 | ||
|
|
e3e928b33d | ||
|
|
9614f186b4 | ||
|
|
0390f2d956 | ||
|
|
43025f0265 | ||
|
|
7ea17fb57c | ||
|
|
efb29104d7 | ||
|
|
971474d493 | ||
|
|
71a6de03b3 | ||
|
|
98dd904fa5 | ||
|
|
b5c7e2efb3 | ||
|
|
3c9bbb097e | ||
|
|
8956968027 | ||
|
|
14c724bf9c | ||
|
|
a9f7e609e9 | ||
|
|
fee68e0f59 | ||
|
|
86c005c205 | ||
|
|
d4d5b2eb68 | ||
|
|
32a651b7a8 | ||
|
|
7da5c5b4d6 | ||
|
|
ba5ead998d | ||
|
|
409a0f1049 | ||
|
|
bd2f27bb64 | ||
|
|
55d8d131cf | ||
|
|
5a6e3247e8 | ||
|
|
2a522775eb | ||
|
|
d677c83795 | ||
|
|
6cfaceb99e | ||
|
|
b12cc194f4 | ||
|
|
7cffd9f377 | ||
|
|
0a406cb9a4 | ||
|
|
f172a51016 | ||
|
|
00d53659b1 | ||
|
|
b0797d92f5 | ||
|
|
48484ed7af | ||
|
|
6a4aadde8a | ||
|
|
79e3aba26e | ||
|
|
bf61359bc7 | ||
|
|
1911991511 | ||
|
|
ef0179fb3f |
+4
-1
@@ -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
@@ -1,5 +1,5 @@
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2021-2023 Permissionless Software Foundation
|
||||
Copyright (c) 2021-2024 Permissionless Software Foundation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
# Pedigree
|
||||
|
||||
This repository was forked from [koa-api-boilerplate](https://github.com/christroutner/koa-api-boilerplate). Code changes from that upstream repository is frequently pulled in and merged to this repository.
|
||||
This repository was forked from [ipfs-service-provider](https://github.com/Permissionless-Software-Foundation/ipfs-service-provider). Code changes from that upstream repository is frequently pulled in and merged to this repository.
|
||||
|
||||
@@ -12,9 +12,9 @@ More details are available at [CashStack.info](https://CashStack.info).
|
||||
|
||||
## Requirements
|
||||
|
||||
- node **^16.20.2**
|
||||
- npm **^8.19.4**
|
||||
- Docker **^20.10.8**
|
||||
- node **^20.16.0**
|
||||
- npm **^10.8.1**
|
||||
- Docker **^24.0.7**
|
||||
- Docker Compose **^1.27.4**
|
||||
|
||||
## Installation
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Vendored
+78
-31
@@ -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
|
||||
: ''
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"password": "RPWjFwYWyJ9S8ByjRDbi",
|
||||
"email": "system@system.com",
|
||||
"id": "678459df8ec91cd6c3973b63",
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3ODQ1OWRmOGVjOTFjZDZjMzk3M2I2MyIsImlhdCI6MTczNjcyNzAwN30.AHLNjmznprA5GzQ_z8GbhrYHOJUIlMzVC64SGRy-Xvs"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"password": "02PjvhoMGoM7pJylp80Z",
|
||||
"email": "system@system.com",
|
||||
"id": "645e49a45a9f5a8e0a54a083",
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY0NWU0OWE0NWE5ZjVhOGUwYTU0YTA4MyIsImlhdCI6MTY4MzkwMDgzNn0.sXeT-G9kS5Nu1dnZUGsNm6FhxrcNaCHhnQ-tLqsJc9E"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"password": "RCJqq5FjgYzvTXOyS1vo",
|
||||
"email": "system@system.com",
|
||||
"id": "645e59c19d49119bbb8329e7",
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY0NWU1OWMxOWQ0OTExOWJiYjgzMjllNyIsImlhdCI6MTY4MzkwNDk2MX0.2yi-hkY31nkgMm_S1QrPs4dIm5DBnJ4GsJEPiSPYHDA"
|
||||
}
|
||||
Generated
+7433
-6128
File diff suppressed because it is too large
Load Diff
+22
-18
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -102,7 +102,7 @@ class BchAdapter {
|
||||
return outObj
|
||||
} catch (err) {
|
||||
// console.log('createUser() error: ', err)
|
||||
wlogger.error('Error in use-cases/bch.js/getStatus()')
|
||||
wlogger.error('Error in adapters/bch.js/getStatus()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,7 @@ class BchAdapter {
|
||||
return true
|
||||
} catch (err) {
|
||||
// console.log('createUser() error: ', err)
|
||||
wlogger.error('Error in use-cases/bch.js/getStatus()')
|
||||
wlogger.error('Error in adapters/bch.js/getStatus()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -160,13 +160,17 @@ class BchAdapter {
|
||||
return data
|
||||
} catch (err) {
|
||||
// console.log('createUser() error: ', err)
|
||||
wlogger.error('Error in use-cases/bch.js/getBalances()')
|
||||
wlogger.error('Error in adapters/bch.js/getBalances()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async getUtxos (addr) {
|
||||
try {
|
||||
if (!addr) {
|
||||
throw new Error('addr required when calling getUtxos')
|
||||
}
|
||||
|
||||
// Throw an error if this IPFS node has not yet made a connection to a
|
||||
// wallet service provider.
|
||||
const selectedProvider =
|
||||
@@ -203,7 +207,7 @@ class BchAdapter {
|
||||
return data
|
||||
} catch (err) {
|
||||
// console.log('createUser() error: ', err)
|
||||
wlogger.error('Error in use-cases/bch.js/getUtxos()')
|
||||
wlogger.error('Error in adapters/bch.js/getUtxos()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -257,13 +261,17 @@ class BchAdapter {
|
||||
return data
|
||||
} catch (err) {
|
||||
// console.log('createUser() error: ', err)
|
||||
wlogger.error('Error in use-cases/bch.js/getUtxosBulk()')
|
||||
wlogger.error('Error in adapters/bch.js/getUtxosBulk()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async broadcast (hex) {
|
||||
try {
|
||||
if (!hex) {
|
||||
throw new Error('hex required when calling broadcast')
|
||||
}
|
||||
|
||||
// Throw an error if this IPFS node has not yet made a connection to a
|
||||
// wallet service provider.
|
||||
const selectedProvider =
|
||||
@@ -309,6 +317,10 @@ class BchAdapter {
|
||||
try {
|
||||
console.log('Executing getTransaction() bch adapter')
|
||||
|
||||
if (!address) {
|
||||
throw new Error('address required when calling getTransactions()')
|
||||
}
|
||||
|
||||
// Throw an error if this IPFS node has not yet made a connection to a
|
||||
// wallet service provider.
|
||||
const selectedProvider =
|
||||
@@ -354,6 +366,10 @@ class BchAdapter {
|
||||
// Get details on an array of TXIDs.
|
||||
async getTransaction (txids) {
|
||||
try {
|
||||
if (!txids) {
|
||||
throw new Error('txids required when calling getTransaction()')
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Getting txData on these txids: ${JSON.stringify(txids, null, 2)}`
|
||||
)
|
||||
@@ -401,6 +417,10 @@ class BchAdapter {
|
||||
|
||||
async getPubKey (address) {
|
||||
try {
|
||||
if (!address) {
|
||||
throw new Error('address required when calling getPubKey()')
|
||||
}
|
||||
|
||||
// Throw an error if this IPFS node has not yet made a connection to a
|
||||
// wallet service provider.
|
||||
const selectedProvider =
|
||||
@@ -443,6 +463,10 @@ class BchAdapter {
|
||||
|
||||
async utxoIsValid (utxo) {
|
||||
try {
|
||||
if (!utxo) {
|
||||
throw new Error('utxo required when calling utxoIsValid()')
|
||||
}
|
||||
|
||||
// Throw an error if this IPFS node has not yet made a connection to a
|
||||
// wallet service provider.
|
||||
const selectedProvider =
|
||||
@@ -485,6 +509,10 @@ class BchAdapter {
|
||||
|
||||
async getTokenData (tokenId, withTxHistory = false) {
|
||||
try {
|
||||
if (!tokenId) {
|
||||
throw new Error('tokenId required when calling getTokenData()')
|
||||
}
|
||||
|
||||
// Throw an error if this IPFS node has not yet made a connection to a
|
||||
// wallet service provider.
|
||||
const selectedProvider =
|
||||
@@ -529,6 +557,10 @@ class BchAdapter {
|
||||
// Get token icon and other media associated with the token.
|
||||
async getTokenData2 (tokenId, updateCache) {
|
||||
try {
|
||||
if (!tokenId) {
|
||||
throw new Error('tokenId required when calling getTokenData2()')
|
||||
}
|
||||
|
||||
// Throw an error if this IPFS node has not yet made a connection to a
|
||||
// wallet service provider.
|
||||
const selectedProvider =
|
||||
@@ -573,6 +605,10 @@ class BchAdapter {
|
||||
// Returns a promise that resolves to data when the RPC response is recieved.
|
||||
async waitForRPCResponse (rpcId) {
|
||||
try {
|
||||
if (!rpcId) {
|
||||
throw new Error('rpcId can not be false or undefined')
|
||||
}
|
||||
|
||||
// Initialize variables for tracking the return data.
|
||||
let dataFound = false
|
||||
let cnt = 0
|
||||
@@ -608,21 +644,25 @@ class BchAdapter {
|
||||
}
|
||||
|
||||
// Wait between loops.
|
||||
// await this.sleep(1000)
|
||||
await this.ipfs.ipfsCoordAdapter.bchjs.Util.sleep(2000)
|
||||
await this.sleep(2000)
|
||||
// await this.ipfs.ipfsCoordAdapter.bchjs.Util.sleep(2000)
|
||||
|
||||
cnt++
|
||||
|
||||
// Exit if data was returned, or the window for a response expires.
|
||||
} while (!dataFound && cnt < 10)
|
||||
// console.log(`dataFound: ${dataFound}, cnt: ${cnt}`)
|
||||
console.log(`dataFound: ${dataFound}, cnt: ${cnt}`)
|
||||
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('Error in waitForRPCResponse()')
|
||||
console.error('Error in waitForRPCResponse(): ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
sleep (ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
}
|
||||
|
||||
// module.exports = BchAdapter
|
||||
|
||||
+13
-2
@@ -15,12 +15,13 @@ import Passport from './passport.js'
|
||||
import Nodemailer from './nodemailer.js'
|
||||
import BCH from './bch/index.js'
|
||||
import P2WDB from './p2wdb/index.js'
|
||||
import IpfsFiles from './ipfs-files/index.js'
|
||||
|
||||
// const { wlogger } = require('./wlogger')
|
||||
import JSONFiles from './json-files.js'
|
||||
|
||||
import FullStackJWT from './fullstack-jwt.js'
|
||||
import config from '../../config/index.js'
|
||||
import Wallet from './wallet.adapter.js'
|
||||
|
||||
class Adapters {
|
||||
constructor (localConfig = {}) {
|
||||
@@ -44,6 +45,8 @@ class Adapters {
|
||||
this.config = config
|
||||
this.bch = new BCH(localConfig)
|
||||
this.p2wdb = new P2WDB(localConfig)
|
||||
this.wallet = new Wallet(localConfig)
|
||||
this.ipfsFiles = new IpfsFiles(localConfig)
|
||||
|
||||
// Get a valid JWT API key and instance bch-js.
|
||||
this.fullStackJwt = new FullStackJWT(config)
|
||||
@@ -51,14 +54,22 @@ class Adapters {
|
||||
|
||||
async start () {
|
||||
try {
|
||||
let apiToken
|
||||
if (this.config.getJwtAtStartup) {
|
||||
// Get a JWT token and instantiate bch-js with it. Then pass that instance
|
||||
// to all the rest of the apps controllers and adapters.
|
||||
await this.fullStackJwt.getJWT()
|
||||
apiToken = await this.fullStackJwt.getJWT()
|
||||
// Instantiate bch-js with the JWT token, and overwrite the placeholder for bch-js.
|
||||
this.bchjs = await this.fullStackJwt.instanceBchjs()
|
||||
}
|
||||
|
||||
// Create a default instance of minimal-slp-wallet without initializing it
|
||||
// (without retrieving the wallets UTXOs). This instance will be overwritten
|
||||
// if the operator has configured BCH payments.
|
||||
console.log('\nCreating default startup wallet. This wallet may be overwritten.')
|
||||
await this.wallet.instanceWalletWithoutInitialization({}, { apiToken })
|
||||
this.bchjs = this.wallet.bchWallet.bchjs
|
||||
|
||||
// Start the IPFS node.
|
||||
// Do not start these adapters if this is an e2e test.
|
||||
if (this.config.env !== 'test') {
|
||||
|
||||
@@ -0,0 +1,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
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -97,6 +97,7 @@ class JSONRPC {
|
||||
wlogger.info(
|
||||
`JSON RPC received from ${from}, ID: ${parsedData.payload.id}, type: ${parsedData.type}`
|
||||
)
|
||||
// console.log('parsedData: ', JSON.stringify(parsedData, null, 2))
|
||||
}
|
||||
// console.log(`parsedData: ${JSON.stringify(parsedData, null, 2)}`)
|
||||
|
||||
@@ -113,6 +114,11 @@ class JSONRPC {
|
||||
console.log('routing to BCH adapter')
|
||||
retObj = await _this.adapters.bch.rpcHandler(parsedData)
|
||||
}
|
||||
|
||||
if (parsedData.payload.result.method === 'file-pin') {
|
||||
console.log('routing to IPFS Files adapter')
|
||||
retObj = await _this.adapters.ipfsFiles.rpcHandler(parsedData)
|
||||
}
|
||||
} catch (err) {
|
||||
/* exit quietly */
|
||||
}
|
||||
|
||||
@@ -723,6 +723,33 @@ class BchRESTControllerLib {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /bch/service Get the IPFS ID for the Wallet service
|
||||
* @apiPermission public
|
||||
* @apiName getService
|
||||
* @apiGroup REST BCH
|
||||
* @apiDescription Get the IPFS ID for the ipfs-bch-wallet-service node that
|
||||
* this app is using to retrieve blockchain data from.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5015/bch/service
|
||||
*
|
||||
*/
|
||||
async getService (ctx) {
|
||||
try {
|
||||
const selectedServiceProvider = this.adapters.ipfs.ipfsCoordAdapter.state.selectedServiceProvider
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
selectedServiceProvider
|
||||
}
|
||||
} catch (err) {
|
||||
// wlogger.error('Error in ipfs/controller.js/viewFile(): ', err)
|
||||
console.log('Error in ipfs/controller.js/getService(): ', err)
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// DRY error handler
|
||||
handleError (ctx, err) {
|
||||
// If an HTTP status is specified by the buisiness logic, use that.
|
||||
|
||||
@@ -63,6 +63,7 @@ class BchRouter {
|
||||
this.router.post('/utxoIsValid', this.utxoIsValid)
|
||||
this.router.post('/getTokenData', this.getTokenData)
|
||||
this.router.post('/getTokenData2', this.getTokenData2)
|
||||
this.router.get('/service', this.getService)
|
||||
|
||||
// Attach the Controller routes to the Koa app.
|
||||
app.use(this.router.routes())
|
||||
@@ -116,6 +117,10 @@ class BchRouter {
|
||||
async getTokenData2 (ctx, next) {
|
||||
await _this.bchRESTController.getTokenData2(ctx, next)
|
||||
}
|
||||
|
||||
async getService (ctx, next) {
|
||||
await _this.bchRESTController.getService(ctx, next)
|
||||
}
|
||||
}
|
||||
|
||||
// module.exports = BchRouter
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -28,7 +28,10 @@ class PriceRESTControllerLib {
|
||||
// this.UserModel = this.adapters.localdb.Users
|
||||
// this.userUseCases = this.useCases.user
|
||||
|
||||
// _this = this
|
||||
// Bind 'this' object to all subfunctions
|
||||
this.getUSD = this.getUSD.bind(this)
|
||||
this.getXecPrice = this.getXecPrice.bind(this)
|
||||
this.getPsffppWritePrice = this.getPsffppWritePrice.bind(this)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,6 +92,22 @@ class PriceRESTControllerLib {
|
||||
}
|
||||
}
|
||||
|
||||
// Get the write price in PSF tokens to pin 1MB to the PSFFPP.
|
||||
// TODO: Add API docs.
|
||||
// curl -X GET http://localhost:5015/price/psffpp
|
||||
async getPsffppWritePrice (ctx) {
|
||||
try {
|
||||
const psfPrice = await this.useCases.ipfs.getWritePrice()
|
||||
|
||||
ctx.body = { psfPrice }
|
||||
} catch (err) {
|
||||
// Write out error to error log.
|
||||
wlogger.error('Error in GET /price/getPsffppWritePrice', err)
|
||||
|
||||
this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// DRY error handler
|
||||
handleError (ctx, err) {
|
||||
// If an HTTP status is specified by the buisiness logic, use that.
|
||||
|
||||
@@ -53,6 +53,7 @@ class PriceRouter {
|
||||
// Define the routes and attach the controller.
|
||||
this.router.get('/usd', this.getPrice)
|
||||
this.router.get('/xecusd', this.getXecPrice)
|
||||
this.router.get('/psffpp', this.getPsffppWritePrice)
|
||||
|
||||
// Attach the Controller routes to the Koa app.
|
||||
app.use(this.router.routes())
|
||||
@@ -66,6 +67,10 @@ class PriceRouter {
|
||||
async getXecPrice (ctx, next) {
|
||||
await _this.priceRESTController.getXecPrice(ctx, next)
|
||||
}
|
||||
|
||||
async getPsffppWritePrice (ctx, next) {
|
||||
await _this.priceRESTController.getPsffppWritePrice(ctx, next)
|
||||
}
|
||||
}
|
||||
|
||||
// module.exports = PriceRouter
|
||||
|
||||
@@ -0,0 +1,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
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
REST API library for the /usage route.
|
||||
*/
|
||||
|
||||
// Public npm libraries.
|
||||
import Router from 'koa-router'
|
||||
|
||||
// Local libraries.
|
||||
import UsageRESTControllerLib from './controller.js'
|
||||
import Validators from '../middleware/validators.js'
|
||||
|
||||
// let _this
|
||||
|
||||
class UsageRouter {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating IPFS REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating IPFS REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
const dependencies = {
|
||||
adapters: this.adapters,
|
||||
useCases: this.useCases
|
||||
}
|
||||
|
||||
// Encapsulate dependencies.
|
||||
this.usageRESTController = new UsageRESTControllerLib(dependencies)
|
||||
this.validators = new Validators()
|
||||
|
||||
// Instantiate the router and set the base route.
|
||||
const baseUrl = '/usage'
|
||||
this.router = new Router({ prefix: baseUrl })
|
||||
|
||||
// _this = this
|
||||
}
|
||||
|
||||
attach (app) {
|
||||
if (!app) {
|
||||
throw new Error(
|
||||
'Must pass app object when attaching REST API controllers.'
|
||||
)
|
||||
}
|
||||
|
||||
// Define the routes and attach the controller.
|
||||
this.router.get('/', this.usageRESTController.getStatus)
|
||||
this.router.get('/ips', this.usageRESTController.getTopIps)
|
||||
this.router.get('/endpoints', this.usageRESTController.getTopEndpoints)
|
||||
|
||||
// Attach the Controller routes to the Koa app.
|
||||
app.use(this.router.routes())
|
||||
app.use(this.router.allowedMethods())
|
||||
}
|
||||
}
|
||||
|
||||
// module.exports = BchRouter
|
||||
export default UsageRouter
|
||||
@@ -28,6 +28,7 @@ class TimerControllers {
|
||||
|
||||
// Bind 'this' object to all subfunctions.
|
||||
this.exampleTimerFunc = this.exampleTimerFunc.bind(this)
|
||||
this.cleanUsage = this.cleanUsage.bind(this)
|
||||
|
||||
// this.startTimers()
|
||||
}
|
||||
@@ -36,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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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 }
|
||||
@@ -13,7 +13,6 @@ import axios from 'axios'
|
||||
|
||||
// Local support libraries
|
||||
import config from '../../../config/index.js'
|
||||
|
||||
import Server from '../../../bin/server.js'
|
||||
import testUtils from '../../utils/test-utils.js'
|
||||
import AdminLib from '../../../src/adapters/admin.js'
|
||||
@@ -24,105 +23,107 @@ const context = {}
|
||||
|
||||
const LOCALHOST = `http://localhost:${config.port}`
|
||||
|
||||
describe('Auth', () => {
|
||||
before(async () => {
|
||||
const app = new Server()
|
||||
if (!config.noMongo) {
|
||||
describe('Auth', () => {
|
||||
before(async () => {
|
||||
const app = new Server()
|
||||
|
||||
// This should be the first instruction. It starts the REST API server.
|
||||
await app.startServer()
|
||||
// This should be the first instruction. It starts the REST API server.
|
||||
await app.startServer()
|
||||
|
||||
// Stop the IPFS node for the rest of the e2e tests.
|
||||
// await app.controllers.adapters.ipfs.stop()
|
||||
// Stop the IPFS node for the rest of the e2e tests.
|
||||
// await app.controllers.adapters.ipfs.stop()
|
||||
|
||||
// Delete all previous users in the database.
|
||||
await testUtils.deleteAllUsers()
|
||||
// Delete all previous users in the database.
|
||||
await testUtils.deleteAllUsers()
|
||||
|
||||
// Create a new admin user.
|
||||
await adminLib.createSystemUser()
|
||||
// Create a new admin user.
|
||||
await adminLib.createSystemUser()
|
||||
|
||||
const userObj = {
|
||||
email: 'test@test.com',
|
||||
password: 'pass',
|
||||
name: 'test'
|
||||
}
|
||||
const testUser = await testUtils.createUser(userObj)
|
||||
// console.log('TestUser: ', testUser)
|
||||
|
||||
context.user = testUser.user
|
||||
context.token = testUser.token
|
||||
})
|
||||
|
||||
describe('POST /auth', () => {
|
||||
it('should throw 401 if credentials are incorrect', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'test@test.com',
|
||||
password: 'wrongpassword'
|
||||
}
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
console.log(
|
||||
`result stringified: ${JSON.stringify(result.data, null, 2)}`
|
||||
)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert(err.response.status === 401, 'Error code 401 expected.')
|
||||
const userObj = {
|
||||
email: 'test@test.com',
|
||||
password: 'pass',
|
||||
name: 'test'
|
||||
}
|
||||
const testUser = await testUtils.createUser(userObj)
|
||||
// console.log('TestUser: ', testUser)
|
||||
|
||||
context.user = testUser.user
|
||||
context.token = testUser.token
|
||||
})
|
||||
|
||||
it('should throw 401 if email is wrong format', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'wrongEmail',
|
||||
password: 'wrongpassword'
|
||||
describe('POST /auth', () => {
|
||||
it('should throw 401 if credentials are incorrect', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'test@test.com',
|
||||
password: 'wrongpassword'
|
||||
}
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
console.log(
|
||||
`result stringified: ${JSON.stringify(result.data, null, 2)}`
|
||||
)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert(err.response.status === 401, 'Error code 401 expected.')
|
||||
}
|
||||
})
|
||||
|
||||
await axios(options)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert(err.response.status === 401, 'Error code 401 expected.')
|
||||
}
|
||||
})
|
||||
|
||||
it('should auth user', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'test@test.com',
|
||||
password: 'pass'
|
||||
it('should throw 401 if email is wrong format', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'wrongEmail',
|
||||
password: 'wrongpassword'
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
// console.log(`result: ${JSON.stringify(result.data, null, 2)}`)
|
||||
|
||||
assert(result.status === 200, 'Status Code 200 expected.')
|
||||
assert(
|
||||
result.data.user.email === 'test@test.com',
|
||||
'Email of test expected'
|
||||
)
|
||||
assert(
|
||||
result.data.user.password === undefined,
|
||||
'Password expected to be omited'
|
||||
)
|
||||
} catch (err) {
|
||||
console.log(
|
||||
'Error authenticating test user: ' + JSON.stringify(err, null, 2)
|
||||
)
|
||||
throw err
|
||||
}
|
||||
await axios(options)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert(err.response.status === 401, 'Error code 401 expected.')
|
||||
}
|
||||
})
|
||||
|
||||
it('should auth user', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'test@test.com',
|
||||
password: 'pass'
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
// console.log(`result: ${JSON.stringify(result.data, null, 2)}`)
|
||||
|
||||
assert(result.status === 200, 'Status Code 200 expected.')
|
||||
assert(
|
||||
result.data.user.email === 'test@test.com',
|
||||
'Email of test expected'
|
||||
)
|
||||
assert(
|
||||
result.data.user.password === undefined,
|
||||
'Password expected to be omited'
|
||||
)
|
||||
} catch (err) {
|
||||
console.log(
|
||||
'Error authenticating test user: ' + JSON.stringify(err, null, 2)
|
||||
)
|
||||
throw err
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,8 @@ import { assert } from 'chai'
|
||||
import Admin from '../../../src/adapters/admin.js'
|
||||
import sinon from 'sinon'
|
||||
import util from 'util'
|
||||
import config from '../../../config/index.js'
|
||||
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
let sandbox
|
||||
@@ -15,101 +17,103 @@ describe('Admin', () => {
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('loginAdmin()', () => {
|
||||
it('should logind admin', async () => {
|
||||
try {
|
||||
const error = new Error('test error')
|
||||
error.response = {
|
||||
status: 422
|
||||
if (!config.noMongo) {
|
||||
describe('loginAdmin()', () => {
|
||||
it('should logind admin', async () => {
|
||||
try {
|
||||
const error = new Error('test error')
|
||||
error.response = {
|
||||
status: 422
|
||||
}
|
||||
// sandbox.stub(uut.axios, 'request').onFirstCall().throws(error)
|
||||
|
||||
const result = await uut.loginAdmin()
|
||||
const user = result.data.user
|
||||
|
||||
assert.property(user, '_id')
|
||||
assert.property(user, 'email')
|
||||
assert.property(user, 'type')
|
||||
|
||||
assert.isString(user._id)
|
||||
assert.isString(user.email)
|
||||
assert.isString(user.type)
|
||||
|
||||
assert.equal(user.type, 'admin')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
// sandbox.stub(uut.axios, 'request').onFirstCall().throws(error)
|
||||
})
|
||||
|
||||
const result = await uut.loginAdmin()
|
||||
const user = result.data.user
|
||||
it('should handle axios error', async () => {
|
||||
try {
|
||||
// Returns an erroneous password to force
|
||||
// an auth error
|
||||
sandbox.stub(uut.jsonFiles, 'readJSON').resolves({ password: 'wrong' })
|
||||
|
||||
assert.property(user, '_id')
|
||||
assert.property(user, 'email')
|
||||
assert.property(user, 'type')
|
||||
|
||||
assert.isString(user._id)
|
||||
assert.isString(user.email)
|
||||
assert.isString(user.type)
|
||||
|
||||
assert.equal(user.type, 'admin')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle axios error', async () => {
|
||||
try {
|
||||
// Returns an erroneous password to force
|
||||
// an auth error
|
||||
sandbox.stub(uut.jsonFiles, 'readJSON').resolves({ password: 'wrong' })
|
||||
|
||||
await uut.loginAdmin()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
assert.include(err.response.data, 'Unauthorized')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('createSystemUser()', () => {
|
||||
it('should create admin', async () => {
|
||||
try {
|
||||
const result = await uut.createSystemUser()
|
||||
|
||||
assert.property(result, 'email')
|
||||
assert.property(result, 'password')
|
||||
assert.property(result, 'id')
|
||||
assert.property(result, 'token')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle axios error', async () => {
|
||||
try {
|
||||
const error1 = new Error('test error')
|
||||
error1.response = {
|
||||
status: 422
|
||||
await uut.loginAdmin()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
assert.include(err.response.data, 'Unauthorized')
|
||||
}
|
||||
const error2 = new Error('test error')
|
||||
error1.response = {
|
||||
status: 500
|
||||
}
|
||||
// The loginAdmin() function in some use cases is recursive
|
||||
// after handling the 422 error, it gets called again
|
||||
sandbox
|
||||
.stub(uut.axios, 'request')
|
||||
.onFirstCall()
|
||||
.throws(error1)
|
||||
.onSecondCall()
|
||||
.throws(error2)
|
||||
|
||||
await uut.createSystemUser()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle errors when remove user', async () => {
|
||||
try {
|
||||
const error1 = new Error('test error')
|
||||
error1.response = {
|
||||
status: 422
|
||||
}
|
||||
sandbox.stub(uut.axios, 'request').throws(error1)
|
||||
sandbox.stub(uut.User, 'deleteOne').throws(new Error('test error'))
|
||||
describe('createSystemUser()', () => {
|
||||
it('should create admin', async () => {
|
||||
try {
|
||||
const result = await uut.createSystemUser()
|
||||
|
||||
await uut.createSystemUser()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
assert.property(result, 'email')
|
||||
assert.property(result, 'password')
|
||||
assert.property(result, 'id')
|
||||
assert.property(result, 'token')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle axios error', async () => {
|
||||
try {
|
||||
const error1 = new Error('test error')
|
||||
error1.response = {
|
||||
status: 422
|
||||
}
|
||||
const error2 = new Error('test error')
|
||||
error1.response = {
|
||||
status: 500
|
||||
}
|
||||
// The loginAdmin() function in some use cases is recursive
|
||||
// after handling the 422 error, it gets called again
|
||||
sandbox
|
||||
.stub(uut.axios, 'request')
|
||||
.onFirstCall()
|
||||
.throws(error1)
|
||||
.onSecondCall()
|
||||
.throws(error2)
|
||||
|
||||
await uut.createSystemUser()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle errors when remove user', async () => {
|
||||
try {
|
||||
const error1 = new Error('test error')
|
||||
error1.response = {
|
||||
status: 422
|
||||
}
|
||||
sandbox.stub(uut.axios, 'request').throws(error1)
|
||||
sandbox.stub(uut.User, 'deleteOne').throws(new Error('test error'))
|
||||
|
||||
await uut.createSystemUser()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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()')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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/',
|
||||
|
||||
@@ -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
|
||||
};
|
||||
Reference in New Issue
Block a user