psf-bch-api 7.1.0 → 7.2.1

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 CHANGED
@@ -19,6 +19,8 @@ LOCAL_RESTURL=http://localhost:5942/v6
19
19
 
20
20
  # START ACCESS CONTROL
21
21
 
22
+ PORT=5942
23
+
22
24
  # x402 payments required to access this API?
23
25
  X402_ENABLED=true
24
26
  SERVER_BCH_ADDRESS=bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "psf-bch-api",
3
- "version": "7.1.0",
3
+ "version": "7.2.1",
4
4
  "main": "psf-bch-api.js",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -15,13 +15,13 @@
15
15
  "license": "MIT",
16
16
  "description": "REST API proxy to Bitcoin Cash infrastructure",
17
17
  "dependencies": {
18
- "@psf/bch-js": "6.8.3",
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": "5.13.3",
24
- "psffpp": "1.2.0",
23
+ "minimal-slp-wallet": "7.0.1",
24
+ "psffpp": "1.2.1",
25
25
  "slp-token-media": "1.2.10",
26
26
  "winston": "3.11.0",
27
27
  "winston-daily-rotate-file": "4.7.1",
@@ -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/christroutner/REST2NOSTR
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/REST2NOSTR
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
- #VOLUME /home/safeuser/keys
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
- # Start the application.
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
- #CMD ["npm", "start"]
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
- rest2nostr:
4
+ psf-bch-api:
5
5
  build: .
6
- container_name: rest2nostr
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
- - ./start-rest2nostr.sh:/home/safeuser/REST2NOSTR/start-rest2nostr.sh
18
+ #- ./start-rest2nostr.sh:/home/safeuser/REST2NOSTR/start-rest2nostr.sh
19
+ - ./.env:/home/safeuser/.env
19
20
  restart: always
@@ -0,0 +1,7 @@
1
+ // Simple Node.js app that prints 'hello world' every 10 seconds
2
+
3
+ setInterval(() => {
4
+ console.log('hello world')
5
+ }, 10000)
6
+
7
+ console.log('Timer started. Printing "hello world" every 10 seconds...')
@@ -45,7 +45,7 @@ const basicAuthDefaults = {
45
45
 
46
46
  export default {
47
47
  // Server port
48
- port: process.env.PORT || 5942,
48
+ port: parseInt(process.env.PORT, 10) || 5942,
49
49
 
50
50
  // Environment
51
51
  env: process.env.NODE_ENV || 'development',
@@ -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
- return this.fulcrum.get(`electrumx/tx/data/${txid}`)
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 }) {
@@ -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
  }
@@ -32,7 +32,7 @@ describe('#slp-use-cases.js', () => {
32
32
  }
33
33
 
34
34
  // Create mock BCHJS
35
- mockBchjs = new BCHJS()
35
+ mockBchjs = new BCHJS({ restURL: 'http://localhost:5942/v6/' })
36
36
  mockBchjs.Electrumx = {
37
37
  txData: sandbox.stub().resolves({
38
38
  details: {