psf-bch-api 7.1.0 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env-local +2 -0
- package/package.json +3 -3
- package/production/docker/.env-local +34 -0
- package/production/docker/Dockerfile +8 -25
- package/production/docker/docker-compose.yml +4 -3
- package/production/docker/temp.js +7 -0
- package/src/config/env/common.js +1 -1
- package/src/controllers/rest-api/encryption/controller.js +100 -0
- package/src/controllers/rest-api/encryption/router.js +51 -0
- package/src/controllers/rest-api/fulcrum/controller.js +2 -1
- package/src/controllers/rest-api/index.js +4 -0
- package/src/controllers/rest-api/slp/controller.js +3 -1
- package/src/use-cases/encryption-use-cases.js +120 -0
- package/src/use-cases/fulcrum-use-cases.js +10 -2
- package/src/use-cases/index.js +7 -0
- package/src/use-cases/slp-use-cases.js +5 -1
- package/test/unit/controllers/encryption-controller-unit.js +203 -0
- package/test/unit/controllers/rest-api-index-unit.js +7 -0
- package/test/unit/use-cases/encryption-use-cases-unit.js +247 -0
- package/test/unit/use-cases/fulcrum-use-cases-unit.js +1 -1
- package/test/unit/use-cases/slp-use-cases-unit.js +1 -1
package/.env-local
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "psf-bch-api",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.2.0",
|
|
4
4
|
"main": "psf-bch-api.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -15,12 +15,12 @@
|
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"description": "REST API proxy to Bitcoin Cash infrastructure",
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@psf/bch-js": "
|
|
18
|
+
"@psf/bch-js": "7.1.0",
|
|
19
19
|
"axios": "1.7.7",
|
|
20
20
|
"cors": "2.8.5",
|
|
21
21
|
"dotenv": "16.3.1",
|
|
22
22
|
"express": "5.1.0",
|
|
23
|
-
"minimal-slp-wallet": "
|
|
23
|
+
"minimal-slp-wallet": "7.0.1",
|
|
24
24
|
"psffpp": "1.2.0",
|
|
25
25
|
"slp-token-media": "1.2.10",
|
|
26
26
|
"winston": "3.11.0",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# START INFRASTRUCTURE SETUP
|
|
2
|
+
|
|
3
|
+
# Full Node Connection
|
|
4
|
+
RPC_BASEURL=http://172.17.0.1:8332
|
|
5
|
+
RPC_USERNAME=bitcoin
|
|
6
|
+
RPC_PASSWORD=password
|
|
7
|
+
|
|
8
|
+
# Fulcrum Indexer
|
|
9
|
+
FULCRUM_API=http://172.17.0.1:3001/v1
|
|
10
|
+
|
|
11
|
+
# SLP Indexer
|
|
12
|
+
SLP_INDEXER_API=http://localhost:5010
|
|
13
|
+
|
|
14
|
+
# REST API URL for wallet operations
|
|
15
|
+
LOCAL_RESTURL=http://localhost:5942/v6
|
|
16
|
+
|
|
17
|
+
# END INFRASTRUCTURE SETUP
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# START ACCESS CONTROL
|
|
21
|
+
|
|
22
|
+
PORT=5942
|
|
23
|
+
|
|
24
|
+
# x402 payments required to access this API?
|
|
25
|
+
X402_ENABLED=true
|
|
26
|
+
SERVER_BCH_ADDRESS=bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d
|
|
27
|
+
FACILITATOR_URL=http://localhost:4345/facilitator
|
|
28
|
+
|
|
29
|
+
# Basic Authentication required to access this API?
|
|
30
|
+
USE_BASIC_AUTH=true
|
|
31
|
+
BASIC_AUTH_TOKEN=some-random-token
|
|
32
|
+
|
|
33
|
+
# END ACCESS CONTROL
|
|
34
|
+
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
#
|
|
3
3
|
|
|
4
4
|
#IMAGE BUILD COMMANDS
|
|
5
|
-
# ct-base-ubuntu = ubuntu 18.04 + nodejs v10 LTS
|
|
6
|
-
#FROM christroutner/ct-base-ubuntu
|
|
7
5
|
FROM ubuntu:22.04
|
|
8
6
|
MAINTAINER Chris Troutner <chris.troutner@gmail.com>
|
|
9
7
|
|
|
@@ -47,39 +45,24 @@ RUN runuser -l safeuser -c "npm config set prefix '~/.npm-global'"
|
|
|
47
45
|
|
|
48
46
|
# Clone the rest.bitcoin.com repository
|
|
49
47
|
WORKDIR /home/safeuser
|
|
50
|
-
RUN git clone https://github.com/
|
|
48
|
+
RUN git clone https://github.com/Permissionless-Software-Foundation/psf-bch-api
|
|
51
49
|
|
|
52
50
|
# Switch to the desired branch. `master` is usually stable,
|
|
53
51
|
# and `stage` has the most up-to-date changes.
|
|
54
|
-
WORKDIR /home/safeuser/
|
|
55
|
-
|
|
56
|
-
# For development: switch to unstable branch
|
|
57
|
-
#RUN git checkout pin-ipfs
|
|
52
|
+
WORKDIR /home/safeuser/psf-bch-api
|
|
58
53
|
|
|
59
54
|
# Install dependencies
|
|
60
55
|
RUN npm install
|
|
56
|
+
RUN npm install minimal-slp-wallet
|
|
61
57
|
|
|
62
58
|
# Generate the API docs
|
|
63
59
|
RUN npm run docs
|
|
64
60
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
# Make leveldb folders
|
|
68
|
-
#RUN mkdir leveldb
|
|
69
|
-
#WORKDIR /home/safeuser/psf-slp-indexer/leveldb
|
|
70
|
-
#RUN mkdir current
|
|
71
|
-
#RUN mkdir zips
|
|
72
|
-
#RUN mkdir backup
|
|
73
|
-
#WORKDIR /home/safeuser/psf-slp-indexer/leveldb/zips
|
|
74
|
-
#COPY restore-auto.sh restore-auto.sh
|
|
75
|
-
#WORKDIR /home/safeuser/psf-slp-indexer
|
|
61
|
+
COPY .env-local .env
|
|
76
62
|
|
|
77
|
-
# Expose the port the API will be served on.
|
|
78
|
-
#EXPOSE 5011
|
|
79
63
|
|
|
80
|
-
|
|
81
|
-
#COPY start-production.sh start-production.sh
|
|
82
|
-
VOLUME start-rest2nostr.sh
|
|
83
|
-
CMD ["./start-rest2nostr.sh"]
|
|
64
|
+
CMD ["npm", "start"]
|
|
84
65
|
|
|
85
|
-
#
|
|
66
|
+
# Used to debug the container.
|
|
67
|
+
#COPY temp.js temp.js
|
|
68
|
+
#CMD ["node", "temp.js"]
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Start the service with the command 'docker-compose up -d'
|
|
2
2
|
|
|
3
3
|
services:
|
|
4
|
-
|
|
4
|
+
psf-bch-api:
|
|
5
5
|
build: .
|
|
6
|
-
container_name:
|
|
6
|
+
container_name: psf-bch-api
|
|
7
7
|
logging:
|
|
8
8
|
driver: 'json-file'
|
|
9
9
|
options:
|
|
@@ -15,5 +15,6 @@ services:
|
|
|
15
15
|
ports:
|
|
16
16
|
- '5942:5942' # <host port>:<container port>
|
|
17
17
|
volumes:
|
|
18
|
-
|
|
18
|
+
#- ./start-rest2nostr.sh:/home/safeuser/REST2NOSTR/start-rest2nostr.sh
|
|
19
|
+
- ./.env:/home/safeuser/.env
|
|
19
20
|
restart: always
|
package/src/config/env/common.js
CHANGED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/*
|
|
2
|
+
REST API Controller for the /encryption routes.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import wlogger from '../../../adapters/wlogger.js'
|
|
6
|
+
|
|
7
|
+
class EncryptionRESTController {
|
|
8
|
+
constructor (localConfig = {}) {
|
|
9
|
+
this.adapters = localConfig.adapters
|
|
10
|
+
if (!this.adapters) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
'Instance of Adapters library required when instantiating Encryption REST Controller.'
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
this.useCases = localConfig.useCases
|
|
17
|
+
if (!this.useCases || !this.useCases.encryption) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
'Instance of Encryption use cases required when instantiating Encryption REST Controller.'
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
this.encryptionUseCases = this.useCases.encryption
|
|
24
|
+
|
|
25
|
+
// Bind functions
|
|
26
|
+
this.root = this.root.bind(this)
|
|
27
|
+
this.getPublicKey = this.getPublicKey.bind(this)
|
|
28
|
+
this.handleError = this.handleError.bind(this)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @api {get} /v6/encryption/ Service status
|
|
33
|
+
* @apiName EncryptionRoot
|
|
34
|
+
* @apiGroup Encryption
|
|
35
|
+
*
|
|
36
|
+
* @apiDescription Returns the status of the encryption service.
|
|
37
|
+
*
|
|
38
|
+
* @apiSuccess {String} status Service identifier
|
|
39
|
+
*/
|
|
40
|
+
async root (req, res) {
|
|
41
|
+
return res.status(200).json({ status: 'encryption' })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @api {get} /v6/encryption/publickey/:address Get public key for a BCH address
|
|
46
|
+
* @apiName GetPublicKey
|
|
47
|
+
* @apiGroup Encryption
|
|
48
|
+
* @apiDescription Searches the blockchain for a public key associated with a
|
|
49
|
+
* BCH address. Returns an object. If successful, the publicKey property will
|
|
50
|
+
* contain a hexadecimal representation of the public key.
|
|
51
|
+
*
|
|
52
|
+
* @apiParam {String} address BCH address (cash address or legacy format)
|
|
53
|
+
*
|
|
54
|
+
* @apiExample Example usage:
|
|
55
|
+
* curl -X GET "https://api.fullstack.cash/v6/encryption/publickey/bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf" -H "accept: application/json"
|
|
56
|
+
*
|
|
57
|
+
* @apiSuccess {Boolean} success Indicates if the operation was successful
|
|
58
|
+
* @apiSuccess {String} publicKey The public key in hexadecimal format, or "not found"
|
|
59
|
+
*/
|
|
60
|
+
async getPublicKey (req, res) {
|
|
61
|
+
try {
|
|
62
|
+
const address = req.params.address
|
|
63
|
+
|
|
64
|
+
// Reject if address is an array
|
|
65
|
+
if (Array.isArray(address)) {
|
|
66
|
+
res.status(400)
|
|
67
|
+
return res.json({
|
|
68
|
+
success: false,
|
|
69
|
+
error: 'address can not be an array.'
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Reject if address is missing
|
|
74
|
+
if (!address) {
|
|
75
|
+
res.status(400)
|
|
76
|
+
return res.json({
|
|
77
|
+
success: false,
|
|
78
|
+
error: 'address is required.'
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const result = await this.encryptionUseCases.getPublicKey({ address })
|
|
83
|
+
|
|
84
|
+
return res.status(200).json(result)
|
|
85
|
+
} catch (err) {
|
|
86
|
+
return this.handleError(err, res)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
handleError (err, res) {
|
|
91
|
+
wlogger.error('Error in EncryptionRESTController:', err)
|
|
92
|
+
|
|
93
|
+
const status = err.status || 500
|
|
94
|
+
const message = err.message || 'Internal server error'
|
|
95
|
+
|
|
96
|
+
return res.status(status).json({ success: false, error: message })
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default EncryptionRESTController
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/*
|
|
2
|
+
REST API router for /encryption routes.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import express from 'express'
|
|
6
|
+
import EncryptionRESTController from './controller.js'
|
|
7
|
+
|
|
8
|
+
class EncryptionRouter {
|
|
9
|
+
constructor (localConfig = {}) {
|
|
10
|
+
this.adapters = localConfig.adapters
|
|
11
|
+
if (!this.adapters) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
'Instance of Adapters library required when instantiating Encryption REST Router.'
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
this.useCases = localConfig.useCases
|
|
18
|
+
if (!this.useCases) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
'Instance of Use Cases library required when instantiating Encryption REST Router.'
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const dependencies = {
|
|
25
|
+
adapters: this.adapters,
|
|
26
|
+
useCases: this.useCases
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.encryptionController = new EncryptionRESTController(dependencies)
|
|
30
|
+
|
|
31
|
+
this.apiPrefix = (localConfig.apiPrefix || '').replace(/\/$/, '')
|
|
32
|
+
this.baseUrl = `${this.apiPrefix}/encryption`
|
|
33
|
+
if (!this.baseUrl.startsWith('/')) {
|
|
34
|
+
this.baseUrl = `/${this.baseUrl}`
|
|
35
|
+
}
|
|
36
|
+
this.router = express.Router()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
attach (app) {
|
|
40
|
+
if (!app) {
|
|
41
|
+
throw new Error('Must pass app object when attaching REST API controllers.')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.router.get('/', this.encryptionController.root)
|
|
45
|
+
this.router.get('/publickey/:address', this.encryptionController.getPublicKey)
|
|
46
|
+
|
|
47
|
+
app.use(this.baseUrl, this.router)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default EncryptionRouter
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import wlogger from '../../../adapters/wlogger.js'
|
|
6
6
|
import BCHJS from '@psf/bch-js'
|
|
7
|
+
import config from '../../../config/index.js'
|
|
7
8
|
|
|
8
|
-
const bchjs = new BCHJS()
|
|
9
|
+
const bchjs = new BCHJS({ restURL: config.restURL })
|
|
9
10
|
|
|
10
11
|
class FulcrumRESTController {
|
|
11
12
|
constructor (localConfig = {}) {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import BlockchainRouter from './full-node/blockchain/router.js'
|
|
11
11
|
import ControlRouter from './full-node/control/router.js'
|
|
12
12
|
import DSProofRouter from './full-node/dsproof/router.js'
|
|
13
|
+
import EncryptionRouter from './encryption/router.js'
|
|
13
14
|
import FulcrumRouter from './fulcrum/router.js'
|
|
14
15
|
import MiningRouter from './full-node/mining/router.js'
|
|
15
16
|
import PriceRouter from './price/router.js'
|
|
@@ -70,6 +71,9 @@ class RESTControllers {
|
|
|
70
71
|
const dsproofRouter = new DSProofRouter(dependencies)
|
|
71
72
|
dsproofRouter.attach(app)
|
|
72
73
|
|
|
74
|
+
const encryptionRouter = new EncryptionRouter(dependencies)
|
|
75
|
+
encryptionRouter.attach(app)
|
|
76
|
+
|
|
73
77
|
const fulcrumRouter = new FulcrumRouter(dependencies)
|
|
74
78
|
fulcrumRouter.attach(app)
|
|
75
79
|
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import wlogger from '../../../adapters/wlogger.js'
|
|
6
6
|
import BCHJS from '@psf/bch-js'
|
|
7
|
+
import config from '../../../config/index.js'
|
|
7
8
|
|
|
8
|
-
const bchjs = new BCHJS()
|
|
9
|
+
const bchjs = new BCHJS({ restURL: config.restURL })
|
|
9
10
|
|
|
10
11
|
class SlpRESTController {
|
|
11
12
|
constructor (localConfig = {}) {
|
|
@@ -201,6 +202,7 @@ class SlpRESTController {
|
|
|
201
202
|
const result = await this.slpUseCases.getTokenData({ tokenId, withTxHistory })
|
|
202
203
|
return res.status(200).json(result)
|
|
203
204
|
} catch (err) {
|
|
205
|
+
console.log('Error in /v6/slp/token/data getTokenData(): ', err)
|
|
204
206
|
return this.handleError(err, res)
|
|
205
207
|
}
|
|
206
208
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Use cases for encryption-related operations.
|
|
3
|
+
Retrieves public keys from the blockchain for BCH addresses.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Global npm libraries
|
|
7
|
+
import BCHJS from '@psf/bch-js'
|
|
8
|
+
|
|
9
|
+
// Local libraries
|
|
10
|
+
import wlogger from '../adapters/wlogger.js'
|
|
11
|
+
import config from '../config/index.js'
|
|
12
|
+
|
|
13
|
+
const bchjs = new BCHJS({ restURL: config.restURL })
|
|
14
|
+
|
|
15
|
+
class EncryptionUseCases {
|
|
16
|
+
constructor (localConfig = {}) {
|
|
17
|
+
this.adapters = localConfig.adapters
|
|
18
|
+
if (!this.adapters) {
|
|
19
|
+
throw new Error('Adapters instance required when instantiating Encryption use cases.')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
this.useCases = localConfig.useCases
|
|
23
|
+
if (!this.useCases) {
|
|
24
|
+
throw new Error('UseCases instance required when instantiating Encryption use cases.')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Allow bchjs to be injected for testing
|
|
28
|
+
this.bchjs = localConfig.bchjs || bchjs
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get the public key for a BCH address by searching the blockchain.
|
|
33
|
+
* Searches the transaction history of the address for a transaction input
|
|
34
|
+
* that contains the public key.
|
|
35
|
+
*
|
|
36
|
+
* @param {Object} params - Parameters object
|
|
37
|
+
* @param {string} params.address - BCH address (cash address or legacy format)
|
|
38
|
+
* @returns {Promise<Object>} Object with success status and publicKey if found
|
|
39
|
+
*/
|
|
40
|
+
async getPublicKey ({ address }) {
|
|
41
|
+
try {
|
|
42
|
+
// Convert to cash address format
|
|
43
|
+
const cashAddr = this.bchjs.Address.toCashAddress(address)
|
|
44
|
+
|
|
45
|
+
// Get transaction history for the address
|
|
46
|
+
const txHistory = await this.useCases.fulcrum.getTransactions({ address: cashAddr })
|
|
47
|
+
|
|
48
|
+
// Extract just the TXIDs
|
|
49
|
+
const txids = txHistory.transactions.map((elem) => elem.tx_hash)
|
|
50
|
+
|
|
51
|
+
// Throw error if there is no transaction history
|
|
52
|
+
if (!txids || txids.length === 0) {
|
|
53
|
+
throw new Error('No transaction history.')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Loop through the transaction history and search for the public key
|
|
57
|
+
for (let i = 0; i < txids.length; i++) {
|
|
58
|
+
const thisTx = txids[i]
|
|
59
|
+
|
|
60
|
+
// Get verbose transaction details
|
|
61
|
+
const txDetails = await this.useCases.rawtransactions.getRawTransaction({
|
|
62
|
+
txid: thisTx,
|
|
63
|
+
verbose: true
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const vin = txDetails.vin
|
|
67
|
+
|
|
68
|
+
// Loop through each input
|
|
69
|
+
for (let j = 0; j < vin.length; j++) {
|
|
70
|
+
const thisVin = vin[j]
|
|
71
|
+
|
|
72
|
+
// Skip if no scriptSig (e.g., coinbase transactions)
|
|
73
|
+
if (!thisVin.scriptSig || !thisVin.scriptSig.asm) {
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Extract the script signature
|
|
78
|
+
const scriptSig = thisVin.scriptSig.asm.split(' ')
|
|
79
|
+
|
|
80
|
+
// Extract the public key from the script signature (last element)
|
|
81
|
+
const pubKey = scriptSig[scriptSig.length - 1]
|
|
82
|
+
|
|
83
|
+
// Skip if pubKey is not a valid hex string (basic validation)
|
|
84
|
+
if (!pubKey || !/^[0-9a-fA-F]+$/.test(pubKey)) {
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
// Generate cash address from public key
|
|
90
|
+
const keyBuf = Buffer.from(pubKey, 'hex')
|
|
91
|
+
const ec = this.bchjs.ECPair.fromPublicKey(keyBuf)
|
|
92
|
+
const cashAddr2 = this.bchjs.ECPair.toCashAddress(ec)
|
|
93
|
+
|
|
94
|
+
// If public keys match, this is the correct public key
|
|
95
|
+
if (cashAddr === cashAddr2) {
|
|
96
|
+
return {
|
|
97
|
+
success: true,
|
|
98
|
+
publicKey: pubKey
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch (err) {
|
|
102
|
+
// Skip invalid public keys - continue searching
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Public key not found in any transaction
|
|
109
|
+
return {
|
|
110
|
+
success: false,
|
|
111
|
+
publicKey: 'not found'
|
|
112
|
+
}
|
|
113
|
+
} catch (err) {
|
|
114
|
+
wlogger.error('Error in EncryptionUseCases.getPublicKey()', err)
|
|
115
|
+
throw err
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export default EncryptionUseCases
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import wlogger from '../adapters/wlogger.js'
|
|
6
6
|
import BCHJS from '@psf/bch-js'
|
|
7
|
+
import config from '../config/index.js'
|
|
7
8
|
|
|
8
|
-
const bchjs = new BCHJS()
|
|
9
|
+
const bchjs = new BCHJS({ restURL: config.restURL })
|
|
9
10
|
|
|
10
11
|
class FulcrumUseCases {
|
|
11
12
|
constructor (localConfig = {}) {
|
|
@@ -53,7 +54,14 @@ class FulcrumUseCases {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
async getTransactionDetails ({ txid }) {
|
|
56
|
-
|
|
57
|
+
try {
|
|
58
|
+
const response = await this.fulcrum.get(`electrumx/tx/data/${txid}`)
|
|
59
|
+
console.log(`getTransactionDetails() TXID ${txid}: ${JSON.stringify(response, null, 2)}`)
|
|
60
|
+
return response
|
|
61
|
+
} catch (err) {
|
|
62
|
+
wlogger.error('Error in FulcrumUseCases.getTransactionDetails()', err)
|
|
63
|
+
throw err
|
|
64
|
+
}
|
|
57
65
|
}
|
|
58
66
|
|
|
59
67
|
async getTransactionDetailsBulk ({ txids, verbose }) {
|
package/src/use-cases/index.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import BlockchainUseCases from './full-node-blockchain-use-cases.js'
|
|
9
9
|
import ControlUseCases from './full-node-control-use-cases.js'
|
|
10
10
|
import DSProofUseCases from './full-node-dsproof-use-cases.js'
|
|
11
|
+
import EncryptionUseCases from './encryption-use-cases.js'
|
|
11
12
|
import FulcrumUseCases from './fulcrum-use-cases.js'
|
|
12
13
|
import MiningUseCases from './full-node-mining-use-cases.js'
|
|
13
14
|
import PriceUseCases from './price-use-cases.js'
|
|
@@ -31,6 +32,12 @@ class UseCases {
|
|
|
31
32
|
this.price = new PriceUseCases({ adapters: this.adapters })
|
|
32
33
|
this.rawtransactions = new RawTransactionsUseCases({ adapters: this.adapters })
|
|
33
34
|
this.slp = new SlpUseCases({ adapters: this.adapters })
|
|
35
|
+
|
|
36
|
+
// Encryption use cases require access to other use cases (fulcrum, rawtransactions)
|
|
37
|
+
this.encryption = new EncryptionUseCases({
|
|
38
|
+
adapters: this.adapters,
|
|
39
|
+
useCases: this
|
|
40
|
+
})
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
// Run any startup Use Cases at the start of the app.
|
|
@@ -9,7 +9,7 @@ import SlpTokenMedia from 'slp-token-media'
|
|
|
9
9
|
import axios from 'axios'
|
|
10
10
|
import config from '../config/index.js'
|
|
11
11
|
|
|
12
|
-
const bchjs = new BCHJS()
|
|
12
|
+
const bchjs = new BCHJS({ restURL: config.restURL })
|
|
13
13
|
|
|
14
14
|
class SlpUseCases {
|
|
15
15
|
constructor (localConfig = {}) {
|
|
@@ -259,6 +259,7 @@ class SlpUseCases {
|
|
|
259
259
|
|
|
260
260
|
return mutableCid
|
|
261
261
|
} catch (err) {
|
|
262
|
+
console.log('Error in SlpUseCases.getMutableCid()', err)
|
|
262
263
|
wlogger.error('Error in SlpUseCases.getMutableCid()', err)
|
|
263
264
|
return false
|
|
264
265
|
}
|
|
@@ -271,7 +272,9 @@ class SlpUseCases {
|
|
|
271
272
|
}
|
|
272
273
|
|
|
273
274
|
// Get transaction data
|
|
275
|
+
console.log('Decoding OP_RETURN for TXID: ', txid)
|
|
274
276
|
const txData = await this.bchjs.Electrumx.txData(txid)
|
|
277
|
+
console.log(`TXID ${txid}: ${JSON.stringify(txData, null, 2)}`)
|
|
275
278
|
let data = false
|
|
276
279
|
|
|
277
280
|
// Map the vout of the transaction in search of an OP_RETURN
|
|
@@ -291,6 +294,7 @@ class SlpUseCases {
|
|
|
291
294
|
|
|
292
295
|
return data
|
|
293
296
|
} catch (error) {
|
|
297
|
+
console.log('Error in SlpUseCases.decodeOpReturn()', error)
|
|
294
298
|
wlogger.error('Error in SlpUseCases.decodeOpReturn()', error)
|
|
295
299
|
throw error
|
|
296
300
|
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Unit tests for EncryptionRESTController.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { assert } from 'chai'
|
|
6
|
+
import sinon from 'sinon'
|
|
7
|
+
|
|
8
|
+
import EncryptionRESTController from '../../../src/controllers/rest-api/encryption/controller.js'
|
|
9
|
+
import { createMockRequest, createMockResponse, createMockRequestWithParams } from '../mocks/controller-mocks.js'
|
|
10
|
+
|
|
11
|
+
describe('#encryption-controller.js', () => {
|
|
12
|
+
let sandbox
|
|
13
|
+
let mockAdapters
|
|
14
|
+
let mockUseCases
|
|
15
|
+
let uut
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
sandbox = sinon.createSandbox()
|
|
19
|
+
mockAdapters = {}
|
|
20
|
+
mockUseCases = {
|
|
21
|
+
encryption: {
|
|
22
|
+
getPublicKey: sandbox.stub().resolves({
|
|
23
|
+
success: true,
|
|
24
|
+
publicKey: '02abc123def456789'
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
uut = new EncryptionRESTController({
|
|
30
|
+
adapters: mockAdapters,
|
|
31
|
+
useCases: mockUseCases
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
sandbox.restore()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('#constructor()', () => {
|
|
40
|
+
it('should require adapters', () => {
|
|
41
|
+
assert.throws(() => {
|
|
42
|
+
// eslint-disable-next-line no-new
|
|
43
|
+
new EncryptionRESTController({ useCases: mockUseCases })
|
|
44
|
+
}, /Adapters library required/)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should require encryption use cases', () => {
|
|
48
|
+
assert.throws(() => {
|
|
49
|
+
// eslint-disable-next-line no-new
|
|
50
|
+
new EncryptionRESTController({ adapters: mockAdapters, useCases: {} })
|
|
51
|
+
}, /Encryption use cases required/)
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe('#root()', () => {
|
|
56
|
+
it('should return encryption status', async () => {
|
|
57
|
+
const req = createMockRequest()
|
|
58
|
+
const res = createMockResponse()
|
|
59
|
+
|
|
60
|
+
await uut.root(req, res)
|
|
61
|
+
|
|
62
|
+
assert.equal(res.statusValue, 200)
|
|
63
|
+
assert.deepEqual(res.jsonData, { status: 'encryption' })
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('#getPublicKey()', () => {
|
|
68
|
+
it('should return public key on success', async () => {
|
|
69
|
+
const req = createMockRequestWithParams({
|
|
70
|
+
address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
|
|
71
|
+
})
|
|
72
|
+
const res = createMockResponse()
|
|
73
|
+
|
|
74
|
+
await uut.getPublicKey(req, res)
|
|
75
|
+
|
|
76
|
+
assert.equal(res.statusValue, 200)
|
|
77
|
+
assert.deepEqual(res.jsonData, {
|
|
78
|
+
success: true,
|
|
79
|
+
publicKey: '02abc123def456789'
|
|
80
|
+
})
|
|
81
|
+
assert.isTrue(mockUseCases.encryption.getPublicKey.calledOnce)
|
|
82
|
+
assert.isTrue(mockUseCases.encryption.getPublicKey.calledWith({
|
|
83
|
+
address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
|
|
84
|
+
}))
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('should return not found when public key is not found', async () => {
|
|
88
|
+
mockUseCases.encryption.getPublicKey.resolves({
|
|
89
|
+
success: false,
|
|
90
|
+
publicKey: 'not found'
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const req = createMockRequestWithParams({
|
|
94
|
+
address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
|
|
95
|
+
})
|
|
96
|
+
const res = createMockResponse()
|
|
97
|
+
|
|
98
|
+
await uut.getPublicKey(req, res)
|
|
99
|
+
|
|
100
|
+
assert.equal(res.statusValue, 200)
|
|
101
|
+
assert.deepEqual(res.jsonData, {
|
|
102
|
+
success: false,
|
|
103
|
+
publicKey: 'not found'
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('should reject array addresses', async () => {
|
|
108
|
+
const req = createMockRequestWithParams({
|
|
109
|
+
address: ['addr1', 'addr2']
|
|
110
|
+
})
|
|
111
|
+
const res = createMockResponse()
|
|
112
|
+
|
|
113
|
+
await uut.getPublicKey(req, res)
|
|
114
|
+
|
|
115
|
+
assert.equal(res.statusValue, 400)
|
|
116
|
+
assert.deepEqual(res.jsonData, {
|
|
117
|
+
success: false,
|
|
118
|
+
error: 'address can not be an array.'
|
|
119
|
+
})
|
|
120
|
+
assert.isFalse(mockUseCases.encryption.getPublicKey.called)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('should reject missing address', async () => {
|
|
124
|
+
const req = createMockRequestWithParams({})
|
|
125
|
+
const res = createMockResponse()
|
|
126
|
+
|
|
127
|
+
await uut.getPublicKey(req, res)
|
|
128
|
+
|
|
129
|
+
assert.equal(res.statusValue, 400)
|
|
130
|
+
assert.deepEqual(res.jsonData, {
|
|
131
|
+
success: false,
|
|
132
|
+
error: 'address is required.'
|
|
133
|
+
})
|
|
134
|
+
assert.isFalse(mockUseCases.encryption.getPublicKey.called)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('should handle errors via handleError', async () => {
|
|
138
|
+
const error = new Error('No transaction history.')
|
|
139
|
+
error.status = 400
|
|
140
|
+
mockUseCases.encryption.getPublicKey.rejects(error)
|
|
141
|
+
|
|
142
|
+
const req = createMockRequestWithParams({
|
|
143
|
+
address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
|
|
144
|
+
})
|
|
145
|
+
const res = createMockResponse()
|
|
146
|
+
|
|
147
|
+
await uut.getPublicKey(req, res)
|
|
148
|
+
|
|
149
|
+
assert.equal(res.statusValue, 400)
|
|
150
|
+
assert.deepEqual(res.jsonData, {
|
|
151
|
+
success: false,
|
|
152
|
+
error: 'No transaction history.'
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('should default to 500 status for errors without status', async () => {
|
|
157
|
+
const error = new Error('Internal error')
|
|
158
|
+
mockUseCases.encryption.getPublicKey.rejects(error)
|
|
159
|
+
|
|
160
|
+
const req = createMockRequestWithParams({
|
|
161
|
+
address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
|
|
162
|
+
})
|
|
163
|
+
const res = createMockResponse()
|
|
164
|
+
|
|
165
|
+
await uut.getPublicKey(req, res)
|
|
166
|
+
|
|
167
|
+
assert.equal(res.statusValue, 500)
|
|
168
|
+
assert.deepEqual(res.jsonData, {
|
|
169
|
+
success: false,
|
|
170
|
+
error: 'Internal error'
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe('#handleError()', () => {
|
|
176
|
+
it('should use error status and message when provided', () => {
|
|
177
|
+
const error = new Error('Custom error')
|
|
178
|
+
error.status = 422
|
|
179
|
+
const res = createMockResponse()
|
|
180
|
+
|
|
181
|
+
uut.handleError(error, res)
|
|
182
|
+
|
|
183
|
+
assert.equal(res.statusValue, 422)
|
|
184
|
+
assert.deepEqual(res.jsonData, {
|
|
185
|
+
success: false,
|
|
186
|
+
error: 'Custom error'
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('should default to 500 and Internal server error', () => {
|
|
191
|
+
const error = {}
|
|
192
|
+
const res = createMockResponse()
|
|
193
|
+
|
|
194
|
+
uut.handleError(error, res)
|
|
195
|
+
|
|
196
|
+
assert.equal(res.statusValue, 500)
|
|
197
|
+
assert.deepEqual(res.jsonData, {
|
|
198
|
+
success: false,
|
|
199
|
+
error: 'Internal server error'
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
})
|
|
@@ -9,6 +9,7 @@ import RESTControllers from '../../../src/controllers/rest-api/index.js'
|
|
|
9
9
|
import BlockchainRouter from '../../../src/controllers/rest-api/full-node/blockchain/router.js'
|
|
10
10
|
import ControlRouter from '../../../src/controllers/rest-api/full-node/control/router.js'
|
|
11
11
|
import DSProofRouter from '../../../src/controllers/rest-api/full-node/dsproof/router.js'
|
|
12
|
+
import EncryptionRouter from '../../../src/controllers/rest-api/encryption/router.js'
|
|
12
13
|
import MiningRouter from '../../../src/controllers/rest-api/full-node/mining/router.js'
|
|
13
14
|
import PriceRouter from '../../../src/controllers/rest-api/price/router.js'
|
|
14
15
|
import RawTransactionsRouter from '../../../src/controllers/rest-api/full-node/rawtransactions/router.js'
|
|
@@ -100,6 +101,9 @@ describe('#controllers/rest-api/index.js', () => {
|
|
|
100
101
|
getMutableCid: () => {},
|
|
101
102
|
decodeOpReturn: () => {},
|
|
102
103
|
getCIDData: () => {}
|
|
104
|
+
},
|
|
105
|
+
encryption: {
|
|
106
|
+
getPublicKey: () => {}
|
|
103
107
|
}
|
|
104
108
|
}
|
|
105
109
|
})
|
|
@@ -129,6 +133,7 @@ describe('#controllers/rest-api/index.js', () => {
|
|
|
129
133
|
const blockchainAttachStub = sandbox.stub(BlockchainRouter.prototype, 'attach')
|
|
130
134
|
const controlAttachStub = sandbox.stub(ControlRouter.prototype, 'attach')
|
|
131
135
|
const dsproofAttachStub = sandbox.stub(DSProofRouter.prototype, 'attach')
|
|
136
|
+
const encryptionAttachStub = sandbox.stub(EncryptionRouter.prototype, 'attach')
|
|
132
137
|
const fulcrumAttachStub = sandbox.stub(FulcrumRouter.prototype, 'attach')
|
|
133
138
|
const miningAttachStub = sandbox.stub(MiningRouter.prototype, 'attach')
|
|
134
139
|
const priceAttachStub = sandbox.stub(PriceRouter.prototype, 'attach')
|
|
@@ -148,6 +153,8 @@ describe('#controllers/rest-api/index.js', () => {
|
|
|
148
153
|
assert.equal(controlAttachStub.getCall(0).args[0], app)
|
|
149
154
|
assert.isTrue(dsproofAttachStub.calledOnce)
|
|
150
155
|
assert.equal(dsproofAttachStub.getCall(0).args[0], app)
|
|
156
|
+
assert.isTrue(encryptionAttachStub.calledOnce)
|
|
157
|
+
assert.equal(encryptionAttachStub.getCall(0).args[0], app)
|
|
151
158
|
assert.isTrue(fulcrumAttachStub.calledOnce)
|
|
152
159
|
assert.equal(fulcrumAttachStub.getCall(0).args[0], app)
|
|
153
160
|
assert.isTrue(miningAttachStub.calledOnce)
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Unit tests for EncryptionUseCases.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { assert } from 'chai'
|
|
6
|
+
import sinon from 'sinon'
|
|
7
|
+
|
|
8
|
+
import EncryptionUseCases from '../../../src/use-cases/encryption-use-cases.js'
|
|
9
|
+
|
|
10
|
+
describe('#encryption-use-cases.js', () => {
|
|
11
|
+
let sandbox
|
|
12
|
+
let mockAdapters
|
|
13
|
+
let mockUseCases
|
|
14
|
+
let mockBchjs
|
|
15
|
+
let uut
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
sandbox = sinon.createSandbox()
|
|
19
|
+
mockAdapters = {}
|
|
20
|
+
|
|
21
|
+
// Mock bchjs
|
|
22
|
+
mockBchjs = {
|
|
23
|
+
Address: {
|
|
24
|
+
toCashAddress: sandbox.stub().returns('bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf')
|
|
25
|
+
},
|
|
26
|
+
ECPair: {
|
|
27
|
+
fromPublicKey: sandbox.stub().returns({}),
|
|
28
|
+
toCashAddress: sandbox.stub().returns('bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf')
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Mock use cases
|
|
33
|
+
mockUseCases = {
|
|
34
|
+
fulcrum: {
|
|
35
|
+
getTransactions: sandbox.stub().resolves({
|
|
36
|
+
transactions: [
|
|
37
|
+
{ tx_hash: 'abc123def456' }
|
|
38
|
+
]
|
|
39
|
+
})
|
|
40
|
+
},
|
|
41
|
+
rawtransactions: {
|
|
42
|
+
getRawTransaction: sandbox.stub().resolves({
|
|
43
|
+
vin: [
|
|
44
|
+
{
|
|
45
|
+
scriptSig: {
|
|
46
|
+
asm: 'signature 02abc123def456789'
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
uut = new EncryptionUseCases({
|
|
55
|
+
adapters: mockAdapters,
|
|
56
|
+
useCases: mockUseCases,
|
|
57
|
+
bchjs: mockBchjs
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
sandbox.restore()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('#constructor()', () => {
|
|
66
|
+
it('should require adapters', () => {
|
|
67
|
+
assert.throws(() => {
|
|
68
|
+
// eslint-disable-next-line no-new
|
|
69
|
+
new EncryptionUseCases({ useCases: mockUseCases })
|
|
70
|
+
}, /Adapters instance required/)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should require useCases', () => {
|
|
74
|
+
assert.throws(() => {
|
|
75
|
+
// eslint-disable-next-line no-new
|
|
76
|
+
new EncryptionUseCases({ adapters: mockAdapters })
|
|
77
|
+
}, /UseCases instance required/)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe('#getPublicKey()', () => {
|
|
82
|
+
it('should return public key when found', async () => {
|
|
83
|
+
const result = await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
|
|
84
|
+
|
|
85
|
+
assert.isTrue(result.success)
|
|
86
|
+
assert.equal(result.publicKey, '02abc123def456789')
|
|
87
|
+
assert.isTrue(mockBchjs.Address.toCashAddress.calledOnce)
|
|
88
|
+
assert.isTrue(mockUseCases.fulcrum.getTransactions.calledOnce)
|
|
89
|
+
assert.isTrue(mockUseCases.rawtransactions.getRawTransaction.calledOnce)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should return not found when public key does not match', async () => {
|
|
93
|
+
// Make the ECPair.toCashAddress return a different address
|
|
94
|
+
mockBchjs.ECPair.toCashAddress.returns('bitcoincash:qqq000000000000000000000000000000000000000')
|
|
95
|
+
|
|
96
|
+
const result = await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
|
|
97
|
+
|
|
98
|
+
assert.isFalse(result.success)
|
|
99
|
+
assert.equal(result.publicKey, 'not found')
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('should throw error when no transaction history', async () => {
|
|
103
|
+
mockUseCases.fulcrum.getTransactions.resolves({
|
|
104
|
+
transactions: []
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
|
|
109
|
+
assert.fail('Should have thrown an error')
|
|
110
|
+
} catch (err) {
|
|
111
|
+
assert.equal(err.message, 'No transaction history.')
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('should handle transactions without scriptSig', async () => {
|
|
116
|
+
mockUseCases.rawtransactions.getRawTransaction.resolves({
|
|
117
|
+
vin: [
|
|
118
|
+
{ txid: 'coinbase' } // No scriptSig
|
|
119
|
+
]
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const result = await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
|
|
123
|
+
|
|
124
|
+
assert.isFalse(result.success)
|
|
125
|
+
assert.equal(result.publicKey, 'not found')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('should handle invalid public key hex gracefully', async () => {
|
|
129
|
+
mockUseCases.rawtransactions.getRawTransaction.resolves({
|
|
130
|
+
vin: [
|
|
131
|
+
{
|
|
132
|
+
scriptSig: {
|
|
133
|
+
asm: 'signature NOT_VALID_HEX'
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
const result = await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
|
|
140
|
+
|
|
141
|
+
assert.isFalse(result.success)
|
|
142
|
+
assert.equal(result.publicKey, 'not found')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('should handle ECPair.fromPublicKey throwing error', async () => {
|
|
146
|
+
mockBchjs.ECPair.fromPublicKey.throws(new Error('Invalid public key'))
|
|
147
|
+
|
|
148
|
+
const result = await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
|
|
149
|
+
|
|
150
|
+
assert.isFalse(result.success)
|
|
151
|
+
assert.equal(result.publicKey, 'not found')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('should search through multiple transactions', async () => {
|
|
155
|
+
// First transaction has no matching public key
|
|
156
|
+
mockUseCases.fulcrum.getTransactions.resolves({
|
|
157
|
+
transactions: [
|
|
158
|
+
{ tx_hash: 'tx1' },
|
|
159
|
+
{ tx_hash: 'tx2' }
|
|
160
|
+
]
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
// Return different data for each tx - first tx has input with non-matching key
|
|
164
|
+
mockUseCases.rawtransactions.getRawTransaction
|
|
165
|
+
.onFirstCall().resolves({
|
|
166
|
+
vin: [
|
|
167
|
+
{
|
|
168
|
+
scriptSig: {
|
|
169
|
+
asm: 'sig 02aaa111bbb222ccc'
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
]
|
|
173
|
+
})
|
|
174
|
+
.onSecondCall().resolves({
|
|
175
|
+
vin: [
|
|
176
|
+
{
|
|
177
|
+
scriptSig: {
|
|
178
|
+
asm: 'sig 02abc123def456789'
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
// First tx doesn't match, second tx matches
|
|
185
|
+
mockBchjs.ECPair.toCashAddress
|
|
186
|
+
.onFirstCall().returns('bitcoincash:qqq000000000000000000000000000000000000000')
|
|
187
|
+
.onSecondCall().returns('bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf')
|
|
188
|
+
|
|
189
|
+
const result = await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
|
|
190
|
+
|
|
191
|
+
assert.isTrue(result.success)
|
|
192
|
+
assert.equal(result.publicKey, '02abc123def456789')
|
|
193
|
+
assert.equal(mockUseCases.rawtransactions.getRawTransaction.callCount, 2)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('should search through multiple inputs in a transaction', async () => {
|
|
197
|
+
mockUseCases.rawtransactions.getRawTransaction.resolves({
|
|
198
|
+
vin: [
|
|
199
|
+
{
|
|
200
|
+
scriptSig: {
|
|
201
|
+
asm: 'sig 02aaa111bbb222ccc'
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
scriptSig: {
|
|
206
|
+
asm: 'sig 02abc123def456789'
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
]
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
// First input doesn't match, second input matches
|
|
213
|
+
mockBchjs.ECPair.toCashAddress
|
|
214
|
+
.onFirstCall().returns('bitcoincash:qqq000000000000000000000000000000000000000')
|
|
215
|
+
.onSecondCall().returns('bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf')
|
|
216
|
+
|
|
217
|
+
const result = await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
|
|
218
|
+
|
|
219
|
+
assert.isTrue(result.success)
|
|
220
|
+
assert.equal(result.publicKey, '02abc123def456789')
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('should propagate fulcrum errors', async () => {
|
|
224
|
+
const error = new Error('Fulcrum API error')
|
|
225
|
+
mockUseCases.fulcrum.getTransactions.rejects(error)
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
|
|
229
|
+
assert.fail('Should have thrown an error')
|
|
230
|
+
} catch (err) {
|
|
231
|
+
assert.equal(err.message, 'Fulcrum API error')
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('should propagate rawtransactions errors', async () => {
|
|
236
|
+
const error = new Error('RawTransactions API error')
|
|
237
|
+
mockUseCases.rawtransactions.getRawTransaction.rejects(error)
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
await uut.getPublicKey({ address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf' })
|
|
241
|
+
assert.fail('Should have thrown an error')
|
|
242
|
+
} catch (err) {
|
|
243
|
+
assert.equal(err.message, 'RawTransactions API error')
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
})
|
|
247
|
+
})
|
|
@@ -24,7 +24,7 @@ describe('#fulcrum-use-cases.js', () => {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
// Create a mock BCHJS instance with stubbed sortAllTxs method
|
|
27
|
-
const mockBchjs = new BCHJS()
|
|
27
|
+
const mockBchjs = new BCHJS({ restURL: 'http://localhost:5942/v6/' })
|
|
28
28
|
if (!mockBchjs.Electrumx) {
|
|
29
29
|
mockBchjs.Electrumx = {}
|
|
30
30
|
}
|