mirror of
https://github.com/Permissionless-Software-Foundation/psf-bch-api.git
synced 2026-09-21 16:52:00 -07:00
feat(blockchain): Adding blockchain full-node endpoints
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
# Full Node Connection
|
||||||
|
RPC_BASEURL=http://172.17.0.1:8332
|
||||||
|
RPC_USERNAME=bitcoin
|
||||||
|
RPC_PASSWORD=password
|
||||||
+5
-5
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "REST2NOSTR Proxy API",
|
"name": "psf-bch-api REST API",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "REST API proxy for Nostr WebSocket protocol",
|
"description": "REST API proxy to Bitcoin Cash infrastructure",
|
||||||
"title": "REST2NOSTR Proxy API",
|
"title": "psf-bch-api REST API",
|
||||||
"url": "https://nostr-relay-api.psfoundation.info",
|
"url": "http://localhost:5942",
|
||||||
"sampleUrl": "https://nostr-relay-api.psfoundation.info"
|
"sampleUrl": "http://localhost:5942"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+102
-2
@@ -9,6 +9,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "1.7.7",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"dotenv": "16.3.1",
|
"dotenv": "16.3.1",
|
||||||
"express": "5.1.0",
|
"express": "5.1.0",
|
||||||
@@ -1469,6 +1470,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/available-typed-arrays": {
|
"node_modules/available-typed-arrays": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
||||||
@@ -1485,6 +1492,17 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.7.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
|
||||||
|
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.6",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@@ -1938,6 +1956,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
"version": "10.0.1",
|
"version": "10.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
|
||||||
@@ -2156,6 +2186,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/depd": {
|
"node_modules/depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
@@ -2458,7 +2497,6 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
@@ -3352,6 +3390,26 @@
|
|||||||
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
|
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||||
|
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/for-each": {
|
"node_modules/for-each": {
|
||||||
"version": "0.3.5",
|
"version": "0.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||||
@@ -3385,6 +3443,43 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data/node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data/node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/forwarded": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@@ -3759,7 +3854,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"has-symbols": "^1.0.3"
|
"has-symbols": "^1.0.3"
|
||||||
@@ -5849,6 +5943,12 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/pstree.remy": {
|
"node_modules/pstree.remy": {
|
||||||
"version": "1.1.8",
|
"version": "1.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "REST API proxy to Bitcoin Cash infrastructure",
|
"description": "REST API proxy to Bitcoin Cash infrastructure",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "1.7.7",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"dotenv": "16.3.1",
|
"dotenv": "16.3.1",
|
||||||
"express": "5.1.0",
|
"express": "5.1.0",
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
/*
|
||||||
|
Adapter library for interacting with a BCH full node over JSON-RPC.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
|
import wlogger from './wlogger.js'
|
||||||
|
import config from '../config/index.js'
|
||||||
|
|
||||||
|
class FullNodeRPCAdapter {
|
||||||
|
constructor (localConfig = {}) {
|
||||||
|
this.config = localConfig.config || config
|
||||||
|
|
||||||
|
if (!this.config.fullNode || !this.config.fullNode.rpcBaseUrl) {
|
||||||
|
throw new Error('Full node RPC configuration is required')
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
rpcBaseUrl,
|
||||||
|
rpcUsername,
|
||||||
|
rpcPassword,
|
||||||
|
rpcTimeoutMs = 15000
|
||||||
|
} = this.config.fullNode
|
||||||
|
console.log('this.config.fullNode', this.config.fullNode)
|
||||||
|
|
||||||
|
this.requestIdPrefix = this.config.fullNode.rpcRequestIdPrefix || 'psf-bch-api'
|
||||||
|
|
||||||
|
this.http = axios.create({
|
||||||
|
baseURL: rpcBaseUrl,
|
||||||
|
timeout: rpcTimeoutMs,
|
||||||
|
auth: {
|
||||||
|
username: rpcUsername,
|
||||||
|
password: rpcPassword
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.defaultRequestPayload = {
|
||||||
|
jsonrpc: '1.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async call (method, params = [], requestId) {
|
||||||
|
const id = requestId || `${this.requestIdPrefix}-${method}`
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.http.post('', {
|
||||||
|
...this.defaultRequestPayload,
|
||||||
|
id,
|
||||||
|
method,
|
||||||
|
params
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.data && response.data.error) {
|
||||||
|
const rpcError = this._formatError(response.data.error.message, 400)
|
||||||
|
throw rpcError
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data.result
|
||||||
|
} catch (err) {
|
||||||
|
throw this._handleError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleError (err) {
|
||||||
|
const { status, message } = this.decodeError(err)
|
||||||
|
const error = new Error(message)
|
||||||
|
error.status = status
|
||||||
|
error.originalError = err
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
|
decodeError (err) {
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
err.response &&
|
||||||
|
err.response.data &&
|
||||||
|
err.response.data.error &&
|
||||||
|
err.response.data.error.message
|
||||||
|
) {
|
||||||
|
return this._formatError(err.response.data.error.message, 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err.response && err.response.data) {
|
||||||
|
return this._formatError(err.response.data, err.response.status || 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err.message) {
|
||||||
|
if (err.message.includes('ENOTFOUND') || err.message.includes('ENETUNREACH') || err.message.includes('EAI_AGAIN')) {
|
||||||
|
return this._formatError(
|
||||||
|
'Network error: Could not communicate with full node or other external service.',
|
||||||
|
503
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err.code && (err.code === 'ECONNABORTED' || err.code === 'ECONNREFUSED')) {
|
||||||
|
return this._formatError(
|
||||||
|
'Network error: Could not communicate with full node or other external service.',
|
||||||
|
503
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err.error && typeof err.error === 'string' && err.error.includes('429')) {
|
||||||
|
return this._formatError('429 Too Many Requests', 429)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err.message) {
|
||||||
|
return this._formatError(err.message, err.status || 422)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._formatError('Unhandled full node error', 500)
|
||||||
|
} catch (decodeError) {
|
||||||
|
wlogger.error('Unhandled error in FullNodeRPCAdapter.decodeError()', decodeError)
|
||||||
|
return this._formatError('Internal server error', 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validateArraySize (length, options = {}) {
|
||||||
|
const { isProUser = false } = options
|
||||||
|
const freemiumLimit = Number(this.config.fullNode?.freemiumArrayLimit || 20)
|
||||||
|
const proLimit = Number(this.config.fullNode?.proArrayLimit || freemiumLimit)
|
||||||
|
|
||||||
|
const limit = isProUser ? proLimit : freemiumLimit
|
||||||
|
return length <= limit
|
||||||
|
}
|
||||||
|
|
||||||
|
_formatError (message, status = 500) {
|
||||||
|
return {
|
||||||
|
message: message || 'Internal server error',
|
||||||
|
status: status || 500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FullNodeRPCAdapter
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
// Load individual adapter libraries.
|
// Load individual adapter libraries.
|
||||||
// import NostrRelayAdapter from './nostr-relay.js'
|
// import NostrRelayAdapter from './nostr-relay.js'
|
||||||
|
import FullNodeRPCAdapter from './full-node-rpc.js'
|
||||||
import config from '../config/index.js'
|
import config from '../config/index.js'
|
||||||
|
|
||||||
class Adapters {
|
class Adapters {
|
||||||
@@ -30,6 +31,8 @@ class Adapters {
|
|||||||
// Maintain backward compatibility: expose first relay as nostrRelay
|
// Maintain backward compatibility: expose first relay as nostrRelay
|
||||||
// This allows existing code to work during transition
|
// This allows existing code to work during transition
|
||||||
// this.nostrRelay = this.nostrRelays[0]
|
// this.nostrRelay = this.nostrRelays[0]
|
||||||
|
|
||||||
|
this.fullNode = new FullNodeRPCAdapter({ config: this.config })
|
||||||
}
|
}
|
||||||
|
|
||||||
async start () {
|
async start () {
|
||||||
|
|||||||
Vendored
+12
@@ -3,10 +3,13 @@
|
|||||||
environments.
|
environments.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
|
||||||
// Hack to get __dirname back.
|
// Hack to get __dirname back.
|
||||||
// https://blog.logrocket.com/alternatives-dirname-node-js-es-modules/
|
// https://blog.logrocket.com/alternatives-dirname-node-js-es-modules/
|
||||||
import * as url from 'url'
|
import * as url from 'url'
|
||||||
import { readFileSync } from 'fs'
|
import { readFileSync } from 'fs'
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
|
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
|
||||||
const pkgInfo = JSON.parse(readFileSync(`${__dirname.toString()}/../../../package.json`))
|
const pkgInfo = JSON.parse(readFileSync(`${__dirname.toString()}/../../../package.json`))
|
||||||
@@ -47,6 +50,15 @@ export default {
|
|||||||
return ['wss://nostr-relay.psfoundation.info', 'wss://relay.damus.io']
|
return ['wss://nostr-relay.psfoundation.info', 'wss://relay.damus.io']
|
||||||
})(),
|
})(),
|
||||||
|
|
||||||
|
// Full node RPC configuration
|
||||||
|
fullNode: {
|
||||||
|
rpcBaseUrl: process.env.RPC_BASEURL || 'http://127.0.0.1:8332',
|
||||||
|
rpcUsername: process.env.RPC_USERNAME || '',
|
||||||
|
rpcPassword: process.env.RPC_PASSWORD || '',
|
||||||
|
rpcTimeoutMs: Number(process.env.RPC_TIMEOUT_MS || 15000),
|
||||||
|
rpcRequestIdPrefix: process.env.RPC_REQUEST_ID_PREFIX || 'psf-bch-api'
|
||||||
|
},
|
||||||
|
|
||||||
// Version
|
// Version
|
||||||
version
|
version
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,553 @@
|
|||||||
|
/*
|
||||||
|
REST API Controller for the /full-node/blockchain routes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import wlogger from '../../../../adapters/wlogger.js'
|
||||||
|
|
||||||
|
class BlockchainRESTController {
|
||||||
|
constructor (localConfig = {}) {
|
||||||
|
this.adapters = localConfig.adapters
|
||||||
|
if (!this.adapters) {
|
||||||
|
throw new Error(
|
||||||
|
'Instance of Adapters library required when instantiating Blockchain REST Controller.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.useCases = localConfig.useCases
|
||||||
|
if (!this.useCases || !this.useCases.blockchain) {
|
||||||
|
throw new Error(
|
||||||
|
'Instance of Blockchain use cases required when instantiating Blockchain REST Controller.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.blockchainUseCases = this.useCases.blockchain
|
||||||
|
|
||||||
|
// Bind functions
|
||||||
|
this.root = this.root.bind(this)
|
||||||
|
this.getBestBlockHash = this.getBestBlockHash.bind(this)
|
||||||
|
this.getBlockchainInfo = this.getBlockchainInfo.bind(this)
|
||||||
|
this.getBlockCount = this.getBlockCount.bind(this)
|
||||||
|
this.getBlockHeaderSingle = this.getBlockHeaderSingle.bind(this)
|
||||||
|
this.getBlockHeaderBulk = this.getBlockHeaderBulk.bind(this)
|
||||||
|
this.getChainTips = this.getChainTips.bind(this)
|
||||||
|
this.getDifficulty = this.getDifficulty.bind(this)
|
||||||
|
this.getMempoolEntrySingle = this.getMempoolEntrySingle.bind(this)
|
||||||
|
this.getMempoolEntryBulk = this.getMempoolEntryBulk.bind(this)
|
||||||
|
this.getMempoolAncestorsSingle = this.getMempoolAncestorsSingle.bind(this)
|
||||||
|
this.getMempoolInfo = this.getMempoolInfo.bind(this)
|
||||||
|
this.getRawMempool = this.getRawMempool.bind(this)
|
||||||
|
this.getTxOut = this.getTxOut.bind(this)
|
||||||
|
this.getTxOutPost = this.getTxOutPost.bind(this)
|
||||||
|
this.getTxOutProofSingle = this.getTxOutProofSingle.bind(this)
|
||||||
|
this.getTxOutProofBulk = this.getTxOutProofBulk.bind(this)
|
||||||
|
this.verifyTxOutProofSingle = this.verifyTxOutProofSingle.bind(this)
|
||||||
|
this.verifyTxOutProofBulk = this.verifyTxOutProofBulk.bind(this)
|
||||||
|
this.getBlock = this.getBlock.bind(this)
|
||||||
|
this.getBlockHash = this.getBlockHash.bind(this)
|
||||||
|
this.handleError = this.handleError.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /full-node/blockchain/ Service status
|
||||||
|
* @apiName BlockchainRoot
|
||||||
|
* @apiGroup Blockchain
|
||||||
|
*
|
||||||
|
* @apiDescription Returns the status of the blockchain service.
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} status Service identifier
|
||||||
|
*/
|
||||||
|
async root (req, res) {
|
||||||
|
return res.status(200).json({ status: 'blockchain' })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /full-node/blockchain/getBestBlockHash Get best block hash
|
||||||
|
* @apiName GetBestBlockHash
|
||||||
|
* @apiGroup Blockchain
|
||||||
|
* @apiDescription Returns the hash of the best (tip) block in the longest block chain.
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -X GET "https://api.fullstack.cash/v5/blockchain/getBestBlockHash" -H "accept: application/json"
|
||||||
|
*
|
||||||
|
* @apiSuccess {String} bestBlockHash Hash of the best block
|
||||||
|
*/
|
||||||
|
async getBestBlockHash (req, res) {
|
||||||
|
try {
|
||||||
|
const result = await this.blockchainUseCases.getBestBlockHash()
|
||||||
|
return res.status(200).json(result)
|
||||||
|
} catch (err) {
|
||||||
|
return this.handleError(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /full-node/blockchain/getBlockchainInfo Get blockchain info
|
||||||
|
* @apiName GetBlockchainInfo
|
||||||
|
* @apiGroup Blockchain
|
||||||
|
* @apiDescription Returns various state info regarding blockchain processing.
|
||||||
|
*/
|
||||||
|
async getBlockchainInfo (req, res) {
|
||||||
|
try {
|
||||||
|
const result = await this.blockchainUseCases.getBlockchainInfo()
|
||||||
|
return res.status(200).json(result)
|
||||||
|
} catch (err) {
|
||||||
|
return this.handleError(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /full-node/blockchain/getBlockCount Get block count
|
||||||
|
* @apiName GetBlockCount
|
||||||
|
* @apiGroup Blockchain
|
||||||
|
* @apiDescription Returns the number of blocks in the longest blockchain.
|
||||||
|
*/
|
||||||
|
async getBlockCount (req, res) {
|
||||||
|
try {
|
||||||
|
const result = await this.blockchainUseCases.getBlockCount()
|
||||||
|
return res.status(200).json(result)
|
||||||
|
} catch (err) {
|
||||||
|
return this.handleError(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /full-node/blockchain/getBlockHeader/:hash Get single block header
|
||||||
|
* @apiName GetSingleBlockHeader
|
||||||
|
* @apiGroup Blockchain
|
||||||
|
* @apiDescription Returns serialized block header data.
|
||||||
|
*
|
||||||
|
* @apiParam {String} hash Block hash
|
||||||
|
* @apiParam {Boolean} verbose Return verbose data (default false)
|
||||||
|
*/
|
||||||
|
async getBlockHeaderSingle (req, res) {
|
||||||
|
try {
|
||||||
|
const hash = req.params.hash
|
||||||
|
if (!hash) {
|
||||||
|
return res.status(400).json({ error: 'hash can not be empty' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const verbose = req.query.verbose?.toString() === 'true'
|
||||||
|
const result = await this.blockchainUseCases.getBlockHeader({ hash, verbose })
|
||||||
|
|
||||||
|
return res.status(200).json(result)
|
||||||
|
} catch (err) {
|
||||||
|
return this.handleError(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /full-node/blockchain/getBlockHeader Get multiple block headers
|
||||||
|
* @apiName GetBulkBlockHeader
|
||||||
|
* @apiGroup Blockchain
|
||||||
|
* @apiDescription Returns serialized block header data for multiple hashes.
|
||||||
|
*
|
||||||
|
* @apiParam {String[]} hashes Block hashes
|
||||||
|
* @apiParam {Boolean} verbose Return verbose data (default false)
|
||||||
|
*/
|
||||||
|
async getBlockHeaderBulk (req, res) {
|
||||||
|
try {
|
||||||
|
const hashes = req.body.hashes
|
||||||
|
const verbose = !!req.body.verbose
|
||||||
|
|
||||||
|
if (!Array.isArray(hashes)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'hashes needs to be an array. Use GET for single hash.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.adapters.fullNode.validateArraySize(hashes.length, { isProUser: Boolean(req.locals?.proLimit) })) {
|
||||||
|
return res.status(400).json({ error: 'Array too large.' })
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const hash of hashes) {
|
||||||
|
if (!hash || hash.length !== 64) {
|
||||||
|
return res.status(400).json({ error: `This is not a hash: ${hash}` })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.blockchainUseCases.getBlockHeaders({ hashes, verbose })
|
||||||
|
return res.status(200).json(result)
|
||||||
|
} catch (err) {
|
||||||
|
return this.handleError(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /full-node/blockchain/getChainTips Get chain tips
|
||||||
|
* @apiName GetChainTips
|
||||||
|
* @apiGroup Blockchain
|
||||||
|
* @apiDescription Returns information about known tips in the block tree.
|
||||||
|
*/
|
||||||
|
async getChainTips (req, res) {
|
||||||
|
try {
|
||||||
|
const result = await this.blockchainUseCases.getChainTips()
|
||||||
|
return res.status(200).json(result)
|
||||||
|
} catch (err) {
|
||||||
|
return this.handleError(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /full-node/blockchain/getDifficulty Get difficulty
|
||||||
|
* @apiName GetDifficulty
|
||||||
|
* @apiGroup Blockchain
|
||||||
|
* @apiDescription Returns the current difficulty value.
|
||||||
|
*/
|
||||||
|
async getDifficulty (req, res) {
|
||||||
|
try {
|
||||||
|
const result = await this.blockchainUseCases.getDifficulty()
|
||||||
|
return res.status(200).json(result)
|
||||||
|
} catch (err) {
|
||||||
|
return this.handleError(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /full-node/blockchain/getMempoolEntry/:txid Get single mempool entry
|
||||||
|
* @apiName GetMempoolEntry
|
||||||
|
* @apiGroup Blockchain
|
||||||
|
* @apiDescription Returns mempool data for a transaction.
|
||||||
|
*/
|
||||||
|
async getMempoolEntrySingle (req, res) {
|
||||||
|
try {
|
||||||
|
const txid = req.params.txid
|
||||||
|
if (!txid) {
|
||||||
|
return res.status(400).json({ error: 'txid can not be empty' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.blockchainUseCases.getMempoolEntry({ txid })
|
||||||
|
return res.status(200).json(result)
|
||||||
|
} catch (err) {
|
||||||
|
return this.handleError(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /full-node/blockchain/getMempoolEntry Get bulk mempool entry
|
||||||
|
* @apiName GetMempoolEntryBulk
|
||||||
|
* @apiGroup Blockchain
|
||||||
|
* @apiDescription Returns mempool data for multiple transactions.
|
||||||
|
*/
|
||||||
|
async getMempoolEntryBulk (req, res) {
|
||||||
|
try {
|
||||||
|
const txids = req.body.txids
|
||||||
|
|
||||||
|
if (!Array.isArray(txids)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'txids needs to be an array. Use GET for single txid.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.adapters.fullNode.validateArraySize(txids.length, { isProUser: Boolean(req.locals?.proLimit) })) {
|
||||||
|
return res.status(400).json({ error: 'Array too large.' })
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const txid of txids) {
|
||||||
|
if (!txid || txid.length !== 64) {
|
||||||
|
return res.status(400).json({ error: 'This is not a txid' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.blockchainUseCases.getMempoolEntries({ txids })
|
||||||
|
return res.status(200).json(result)
|
||||||
|
} catch (err) {
|
||||||
|
return this.handleError(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /full-node/blockchain/getMempoolAncestors/:txid Get mempool ancestors
|
||||||
|
* @apiName GetMempoolAncestors
|
||||||
|
* @apiGroup Blockchain
|
||||||
|
* @apiDescription Returns mempool ancestor data for a transaction.
|
||||||
|
*/
|
||||||
|
async getMempoolAncestorsSingle (req, res) {
|
||||||
|
try {
|
||||||
|
const txid = req.params.txid
|
||||||
|
if (!txid) {
|
||||||
|
return res.status(400).json({ error: 'txid can not be empty' })
|
||||||
|
}
|
||||||
|
|
||||||
|
let verbose = false
|
||||||
|
if (req.query.verbose && req.query.verbose.toString() === 'true') {
|
||||||
|
verbose = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.blockchainUseCases.getMempoolAncestors({ txid, verbose })
|
||||||
|
return res.status(200).json(result)
|
||||||
|
} catch (err) {
|
||||||
|
return this.handleError(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /full-node/blockchain/getMempoolInfo Get mempool info
|
||||||
|
* @apiName GetMempoolInfo
|
||||||
|
* @apiGroup Blockchain
|
||||||
|
* @apiDescription Returns details on the state of the mempool.
|
||||||
|
*/
|
||||||
|
async getMempoolInfo (req, res) {
|
||||||
|
try {
|
||||||
|
const result = await this.blockchainUseCases.getMempoolInfo()
|
||||||
|
return res.status(200).json(result)
|
||||||
|
} catch (err) {
|
||||||
|
return this.handleError(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /full-node/blockchain/getRawMempool Get raw mempool
|
||||||
|
* @apiName GetRawMempool
|
||||||
|
* @apiGroup Blockchain
|
||||||
|
* @apiDescription Returns all transaction ids in the mempool.
|
||||||
|
*
|
||||||
|
* @apiParam {Boolean} verbose Return verbose data (default false)
|
||||||
|
*/
|
||||||
|
async getRawMempool (req, res) {
|
||||||
|
try {
|
||||||
|
const verbose = req.query.verbose === 'true'
|
||||||
|
const result = await this.blockchainUseCases.getRawMempool({ verbose })
|
||||||
|
return res.status(200).json(result)
|
||||||
|
} catch (err) {
|
||||||
|
return this.handleError(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /full-node/blockchain/getTxOut/:txid/:n Get transaction output
|
||||||
|
* @apiName GetTxOut
|
||||||
|
* @apiGroup Blockchain
|
||||||
|
* @apiDescription Returns details about an unspent transaction output.
|
||||||
|
*/
|
||||||
|
async getTxOut (req, res) {
|
||||||
|
try {
|
||||||
|
const txid = req.params.txid
|
||||||
|
if (!txid) {
|
||||||
|
return res.status(400).json({ error: 'txid can not be empty' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const nRaw = req.params.n
|
||||||
|
if (nRaw === undefined || nRaw === '') {
|
||||||
|
return res.status(400).json({ error: 'n can not be empty' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const n = parseInt(nRaw)
|
||||||
|
const includeMempool = req.query.includeMempool === 'true'
|
||||||
|
|
||||||
|
const result = await this.blockchainUseCases.getTxOut({
|
||||||
|
txid,
|
||||||
|
n,
|
||||||
|
includeMempool
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.status(200).json(result)
|
||||||
|
} catch (err) {
|
||||||
|
return this.handleError(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /full-node/blockchain/getTxOut Validate a UTXO
|
||||||
|
* @apiName GetTxOutPost
|
||||||
|
* @apiGroup Blockchain
|
||||||
|
* @apiDescription Returns details about an unspent transaction output.
|
||||||
|
*/
|
||||||
|
async getTxOutPost (req, res) {
|
||||||
|
try {
|
||||||
|
const txid = req.body.txid
|
||||||
|
if (!txid) {
|
||||||
|
return res.status(400).json({ error: 'txid can not be empty' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const voutRaw = req.body.vout
|
||||||
|
if (voutRaw === undefined || voutRaw === '') {
|
||||||
|
return res.status(400).json({ error: 'vout can not be empty' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const n = parseInt(voutRaw)
|
||||||
|
const mempool = req.body.mempool !== undefined ? !!req.body.mempool : true
|
||||||
|
|
||||||
|
const result = await this.blockchainUseCases.getTxOut({
|
||||||
|
txid,
|
||||||
|
n,
|
||||||
|
includeMempool: mempool
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.status(200).json(result)
|
||||||
|
} catch (err) {
|
||||||
|
return this.handleError(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /full-node/blockchain/getTxOutProof/:txid Get TxOut proof
|
||||||
|
* @apiName GetTxOutProofSingle
|
||||||
|
* @apiGroup Blockchain
|
||||||
|
* @apiDescription Returns a hex-encoded proof that the transaction was included in a block.
|
||||||
|
*/
|
||||||
|
async getTxOutProofSingle (req, res) {
|
||||||
|
try {
|
||||||
|
const txid = req.params.txid
|
||||||
|
if (!txid) {
|
||||||
|
return res.status(400).json({ error: 'txid can not be empty' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.blockchainUseCases.getTxOutProof({ txid })
|
||||||
|
return res.status(200).json(result)
|
||||||
|
} catch (err) {
|
||||||
|
return this.handleError(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /full-node/blockchain/getTxOutProof Get TxOut proofs
|
||||||
|
* @apiName GetTxOutProofBulk
|
||||||
|
* @apiGroup Blockchain
|
||||||
|
* @apiDescription Returns hex-encoded proofs for transactions.
|
||||||
|
*/
|
||||||
|
async getTxOutProofBulk (req, res) {
|
||||||
|
try {
|
||||||
|
const txids = req.body.txids
|
||||||
|
|
||||||
|
if (!Array.isArray(txids)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'txids needs to be an array. Use GET for single txid.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.adapters.fullNode.validateArraySize(txids.length, { isProUser: Boolean(req.locals?.proLimit) })) {
|
||||||
|
return res.status(400).json({ error: 'Array too large.' })
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const txid of txids) {
|
||||||
|
if (!txid || txid.length !== 64) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: `Invalid txid. Double check your txid is valid: ${txid}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.blockchainUseCases.getTxOutProofs({ txids })
|
||||||
|
return res.status(200).json(result)
|
||||||
|
} catch (err) {
|
||||||
|
return this.handleError(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /full-node/blockchain/verifyTxOutProof/:proof Verify TxOut proof
|
||||||
|
* @apiName VerifyTxOutProofSingle
|
||||||
|
* @apiGroup Blockchain
|
||||||
|
* @apiDescription Verifies a hex-encoded proof was included in a block.
|
||||||
|
*/
|
||||||
|
async verifyTxOutProofSingle (req, res) {
|
||||||
|
try {
|
||||||
|
const proof = req.params.proof
|
||||||
|
if (!proof) {
|
||||||
|
return res.status(400).json({ error: 'proof can not be empty' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.blockchainUseCases.verifyTxOutProof({ proof })
|
||||||
|
return res.status(200).json(result)
|
||||||
|
} catch (err) {
|
||||||
|
return this.handleError(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /full-node/blockchain/verifyTxOutProof Verify TxOut proofs
|
||||||
|
* @apiName VerifyTxOutProofBulk
|
||||||
|
* @apiGroup Blockchain
|
||||||
|
* @apiDescription Verifies hex-encoded proofs were included in blocks.
|
||||||
|
*/
|
||||||
|
async verifyTxOutProofBulk (req, res) {
|
||||||
|
try {
|
||||||
|
const proofs = req.body.proofs
|
||||||
|
|
||||||
|
if (!Array.isArray(proofs)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'proofs needs to be an array. Use GET for single proof.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.adapters.fullNode.validateArraySize(proofs.length, { isProUser: Boolean(req.locals?.proLimit) })) {
|
||||||
|
return res.status(400).json({ error: 'Array too large.' })
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const proof of proofs) {
|
||||||
|
if (!proof) {
|
||||||
|
return res.status(400).json({ error: `proof can not be empty: ${proof}` })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.blockchainUseCases.verifyTxOutProofs({ proofs })
|
||||||
|
const flattened = result.map(entry => Array.isArray(entry) ? entry[0] : entry)
|
||||||
|
|
||||||
|
return res.status(200).json(flattened)
|
||||||
|
} catch (err) {
|
||||||
|
return this.handleError(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /full-node/blockchain/getBlock Get block details
|
||||||
|
* @apiName GetBlock
|
||||||
|
* @apiGroup Blockchain
|
||||||
|
* @apiDescription Returns block details for a hash.
|
||||||
|
*/
|
||||||
|
async getBlock (req, res) {
|
||||||
|
try {
|
||||||
|
const blockhash = req.body.blockhash
|
||||||
|
if (!blockhash) {
|
||||||
|
return res.status(400).json({ error: 'blockhash can not be empty' })
|
||||||
|
}
|
||||||
|
|
||||||
|
let verbosity = req.body.verbosity
|
||||||
|
if (verbosity === undefined || verbosity === null) {
|
||||||
|
verbosity = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.blockchainUseCases.getBlock({
|
||||||
|
blockhash,
|
||||||
|
verbosity
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.status(200).json(result)
|
||||||
|
} catch (err) {
|
||||||
|
return this.handleError(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /full-node/blockchain/getBlockHash/:height Get block hash
|
||||||
|
* @apiName GetBlockHash
|
||||||
|
* @apiGroup Blockchain
|
||||||
|
* @apiDescription Returns the hash of a block by height.
|
||||||
|
*/
|
||||||
|
async getBlockHash (req, res) {
|
||||||
|
try {
|
||||||
|
const heightRaw = req.params.height
|
||||||
|
if (!heightRaw) {
|
||||||
|
return res.status(400).json({ error: 'height can not be empty' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const height = parseInt(heightRaw)
|
||||||
|
const result = await this.blockchainUseCases.getBlockHash({ height })
|
||||||
|
|
||||||
|
return res.status(200).json(result)
|
||||||
|
} catch (err) {
|
||||||
|
return this.handleError(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleError (err, res) {
|
||||||
|
wlogger.error('Error in BlockchainRESTController:', err)
|
||||||
|
|
||||||
|
const status = err.status || 500
|
||||||
|
const message = err.message || 'Internal server error'
|
||||||
|
|
||||||
|
return res.status(status).json({ error: message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlockchainRESTController
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
REST API router for /full-node/blockchain routes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import express from 'express'
|
||||||
|
import BlockchainRESTController from './controller.js'
|
||||||
|
|
||||||
|
class BlockchainRouter {
|
||||||
|
constructor (localConfig = {}) {
|
||||||
|
this.adapters = localConfig.adapters
|
||||||
|
if (!this.adapters) {
|
||||||
|
throw new Error(
|
||||||
|
'Instance of Adapters library required when instantiating Blockchain REST Router.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.useCases = localConfig.useCases
|
||||||
|
if (!this.useCases) {
|
||||||
|
throw new Error(
|
||||||
|
'Instance of Use Cases library required when instantiating Blockchain REST Router.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dependencies = {
|
||||||
|
adapters: this.adapters,
|
||||||
|
useCases: this.useCases
|
||||||
|
}
|
||||||
|
|
||||||
|
this.blockchainController = new BlockchainRESTController(dependencies)
|
||||||
|
|
||||||
|
this.baseUrl = '/full-node/blockchain'
|
||||||
|
this.router = express.Router()
|
||||||
|
}
|
||||||
|
|
||||||
|
attach (app) {
|
||||||
|
if (!app) {
|
||||||
|
throw new Error('Must pass app object when attaching REST API controllers.')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.router.get('/', this.blockchainController.root)
|
||||||
|
this.router.get('/getBestBlockHash', this.blockchainController.getBestBlockHash)
|
||||||
|
this.router.get('/getBlockchainInfo', this.blockchainController.getBlockchainInfo)
|
||||||
|
this.router.get('/getBlockCount', this.blockchainController.getBlockCount)
|
||||||
|
this.router.get('/getBlockHeader/:hash', this.blockchainController.getBlockHeaderSingle)
|
||||||
|
this.router.post('/getBlockHeader', this.blockchainController.getBlockHeaderBulk)
|
||||||
|
this.router.get('/getChainTips', this.blockchainController.getChainTips)
|
||||||
|
this.router.get('/getDifficulty', this.blockchainController.getDifficulty)
|
||||||
|
this.router.get('/getMempoolEntry/:txid', this.blockchainController.getMempoolEntrySingle)
|
||||||
|
this.router.post('/getMempoolEntry', this.blockchainController.getMempoolEntryBulk)
|
||||||
|
this.router.get('/getMempoolAncestors/:txid', this.blockchainController.getMempoolAncestorsSingle)
|
||||||
|
this.router.get('/getMempoolInfo', this.blockchainController.getMempoolInfo)
|
||||||
|
this.router.get('/getRawMempool', this.blockchainController.getRawMempool)
|
||||||
|
this.router.get('/getTxOut/:txid/:n', this.blockchainController.getTxOut)
|
||||||
|
this.router.post('/getTxOut', this.blockchainController.getTxOutPost)
|
||||||
|
this.router.get('/getTxOutProof/:txid', this.blockchainController.getTxOutProofSingle)
|
||||||
|
this.router.post('/getTxOutProof', this.blockchainController.getTxOutProofBulk)
|
||||||
|
this.router.get('/verifyTxOutProof/:proof', this.blockchainController.verifyTxOutProofSingle)
|
||||||
|
this.router.post('/verifyTxOutProof', this.blockchainController.verifyTxOutProofBulk)
|
||||||
|
this.router.post('/getBlock', this.blockchainController.getBlock)
|
||||||
|
this.router.get('/getBlockHash/:height', this.blockchainController.getBlockHash)
|
||||||
|
|
||||||
|
app.use(this.baseUrl, this.router)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlockchainRouter
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
// Local libraries
|
// Local libraries
|
||||||
// import EventRouter from './event/index.js'
|
// import EventRouter from './event/index.js'
|
||||||
// import ReqRouter from './req/index.js'
|
// import ReqRouter from './req/index.js'
|
||||||
|
import BlockchainRouter from './full-node/blockchain/index.js'
|
||||||
import config from '../../config/index.js'
|
import config from '../../config/index.js'
|
||||||
|
|
||||||
class RESTControllers {
|
class RESTControllers {
|
||||||
@@ -33,10 +34,10 @@ class RESTControllers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
attachRESTControllers (app) {
|
attachRESTControllers (app) {
|
||||||
// const dependencies = {
|
const dependencies = {
|
||||||
// adapters: this.adapters,
|
adapters: this.adapters,
|
||||||
// useCases: this.useCases
|
useCases: this.useCases
|
||||||
// }
|
}
|
||||||
|
|
||||||
// Attach the REST API Controllers associated with the /event route
|
// Attach the REST API Controllers associated with the /event route
|
||||||
// const eventRouter = new EventRouter(dependencies)
|
// const eventRouter = new EventRouter(dependencies)
|
||||||
@@ -45,6 +46,9 @@ class RESTControllers {
|
|||||||
// Attach the REST API Controllers associated with the /req route
|
// Attach the REST API Controllers associated with the /req route
|
||||||
// const reqRouter = new ReqRouter(dependencies)
|
// const reqRouter = new ReqRouter(dependencies)
|
||||||
// reqRouter.attach(app)
|
// reqRouter.attach(app)
|
||||||
|
|
||||||
|
const blockchainRouter = new BlockchainRouter(dependencies)
|
||||||
|
blockchainRouter.attach(app)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
/*
|
||||||
|
Use cases for interacting with the BCH full node blockchain RPC interface.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import wlogger from '../../adapters/wlogger.js'
|
||||||
|
|
||||||
|
class BlockchainUseCases {
|
||||||
|
constructor (localConfig = {}) {
|
||||||
|
this.adapters = localConfig.adapters
|
||||||
|
|
||||||
|
if (!this.adapters) {
|
||||||
|
throw new Error('Adapters instance required when instantiating Blockchain use cases.')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fullNode = this.adapters.fullNode
|
||||||
|
if (!this.fullNode) {
|
||||||
|
throw new Error('Full node adapter required when instantiating Blockchain use cases.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBestBlockHash () {
|
||||||
|
return this.fullNode.call('getbestblockhash')
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBlockchainInfo () {
|
||||||
|
return this.fullNode.call('getblockchaininfo')
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBlockCount () {
|
||||||
|
return this.fullNode.call('getblockcount')
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBlockHeader ({ hash, verbose = false }) {
|
||||||
|
return this.fullNode.call('getblockheader', [hash, verbose])
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBlockHeaders ({ hashes, verbose = false }) {
|
||||||
|
try {
|
||||||
|
const promises = hashes.map(hash =>
|
||||||
|
this.fullNode.call('getblockheader', [hash, verbose], `getblockheader-${hash}`)
|
||||||
|
)
|
||||||
|
|
||||||
|
return await Promise.all(promises)
|
||||||
|
} catch (err) {
|
||||||
|
wlogger.error('Error in BlockchainUseCases.getBlockHeaders()', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getChainTips () {
|
||||||
|
return this.fullNode.call('getchaintips')
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDifficulty () {
|
||||||
|
return this.fullNode.call('getdifficulty')
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMempoolEntry ({ txid }) {
|
||||||
|
return this.fullNode.call('getmempoolentry', [txid])
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMempoolEntries ({ txids }) {
|
||||||
|
try {
|
||||||
|
const promises = txids.map(txid =>
|
||||||
|
this.fullNode.call('getmempoolentry', [txid], `getmempoolentry-${txid}`)
|
||||||
|
)
|
||||||
|
|
||||||
|
return await Promise.all(promises)
|
||||||
|
} catch (err) {
|
||||||
|
wlogger.error('Error in BlockchainUseCases.getMempoolEntries()', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMempoolAncestors ({ txid, verbose = false }) {
|
||||||
|
return this.fullNode.call('getmempoolancestors', [txid, verbose])
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMempoolInfo () {
|
||||||
|
return this.fullNode.call('getmempoolinfo')
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRawMempool ({ verbose = false }) {
|
||||||
|
return this.fullNode.call('getrawmempool', [verbose])
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTxOut ({ txid, n, includeMempool }) {
|
||||||
|
return this.fullNode.call('gettxout', [txid, n, includeMempool])
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTxOutProof ({ txid }) {
|
||||||
|
return this.fullNode.call('gettxoutproof', [[txid]])
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTxOutProofs ({ txids }) {
|
||||||
|
try {
|
||||||
|
const promises = txids.map(txid =>
|
||||||
|
this.fullNode.call('gettxoutproof', [[txid]], `gettxoutproof-${txid}`)
|
||||||
|
)
|
||||||
|
|
||||||
|
return await Promise.all(promises)
|
||||||
|
} catch (err) {
|
||||||
|
wlogger.error('Error in BlockchainUseCases.getTxOutProofs()', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyTxOutProof ({ proof }) {
|
||||||
|
return this.fullNode.call('verifytxoutproof', [proof])
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyTxOutProofs ({ proofs }) {
|
||||||
|
try {
|
||||||
|
const promises = proofs.map(proof =>
|
||||||
|
this.fullNode.call('verifytxoutproof', [proof], `verifytxoutproof-${proof.slice(0, 16)}`)
|
||||||
|
)
|
||||||
|
|
||||||
|
return await Promise.all(promises)
|
||||||
|
} catch (err) {
|
||||||
|
wlogger.error('Error in BlockchainUseCases.verifyTxOutProofs()', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBlock ({ blockhash, verbosity }) {
|
||||||
|
return this.fullNode.call('getblock', [blockhash, verbosity])
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBlockHash ({ height }) {
|
||||||
|
return this.fullNode.call('getblockhash', [height])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlockchainUseCases
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
// import PublishEventUseCase from './publish-event.js'
|
// import PublishEventUseCase from './publish-event.js'
|
||||||
// import QueryEventsUseCase from './query-events.js'
|
// import QueryEventsUseCase from './query-events.js'
|
||||||
// import ManageSubscriptionUseCase from './manage-subscription.js'
|
// import ManageSubscriptionUseCase from './manage-subscription.js'
|
||||||
|
import BlockchainUseCases from './blockchain/index.js'
|
||||||
|
|
||||||
class UseCases {
|
class UseCases {
|
||||||
constructor (localConfig = {}) {
|
constructor (localConfig = {}) {
|
||||||
@@ -21,6 +22,7 @@ class UseCases {
|
|||||||
// this.publishEvent = new PublishEventUseCase({ adapters: this.adapters })
|
// this.publishEvent = new PublishEventUseCase({ adapters: this.adapters })
|
||||||
// this.queryEvents = new QueryEventsUseCase({ adapters: this.adapters })
|
// this.queryEvents = new QueryEventsUseCase({ adapters: this.adapters })
|
||||||
// this.manageSubscription = new ManageSubscriptionUseCase({ adapters: this.adapters })
|
// this.manageSubscription = new ManageSubscriptionUseCase({ adapters: this.adapters })
|
||||||
|
this.blockchain = new BlockchainUseCases({ adapters: this.adapters })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run any startup Use Cases at the start of the app.
|
// Run any startup Use Cases at the start of the app.
|
||||||
|
|||||||
Reference in New Issue
Block a user