mirror of
https://github.com/Permissionless-Software-Foundation/psf-bch-api-base.git
synced 2026-09-21 16:52:00 -07:00
Merge pull request #3 from Permissionless-Software-Foundation/ct-unstable
Updating README and examples
This commit is contained in:
@@ -1,30 +1,188 @@
|
||||
# psf-bch-api
|
||||
# psf-bch-api-base
|
||||
|
||||
This is a REST API for communicating with Bitcoin Cash infrastructure. It replaces [bch-api](https://github.com/Permissionless-Software-Foundation/bch-api), and it implements [x402-bch protocol](https://github.com/x402-bch/x402-bch) to handle payments to access the API.
|
||||
[](https://github.com/Permissionless-Software-Foundation/psf-bch-api/blob/master/LICENSE.md)
|
||||
[](https://github.com/feross/standard)
|
||||
|
||||
This project is a fork of `psf-bch-api` with one major change: paid access now uses the `x402` protocol on the Base ecosystem, with pricing in USDC, instead of `x402-bch` on BCH.
|
||||
|
||||
The API surface remains focused on BCH infrastructure (BCH full node, Fulcrum, and SLP indexer), but monetization and payment verification are handled through x402-compatible middleware and facilitator endpoints.
|
||||
|
||||
## What Changed In This Fork
|
||||
|
||||
- Switched from `x402-bch` middleware to `x402-express`.
|
||||
- Added Base/EVM payment support (`@x402/evm`) and `x402-axios` client example.
|
||||
- Pricing is configured in `X402_PRICE_USDC` (USDC amount), not BCH satoshis.
|
||||
- x402 network is configured using CAIP-2 format (for example `eip155:8453` for Base mainnet and `eip155:84532` for Base Sepolia).
|
||||
- Facilitator auth headers are generated using Coinbase CDP JWT auth (`FACILITATOR_KEY_ID` and `FACILITATOR_SECRET_KEY`) when using CDP endpoints.
|
||||
|
||||
## Architecture
|
||||
|
||||
The server is a Node.js + Express REST API following a Clean Architecture style, and it still depends on:
|
||||
|
||||
- BCH full node JSON-RPC
|
||||
- Fulcrum API
|
||||
- SLP indexer API
|
||||
|
||||
The access-control layer now supports:
|
||||
|
||||
- open access (no auth, no payment)
|
||||
- bearer-token auth
|
||||
- x402 paid access on Base + USDC
|
||||
- optional bearer bypass for trusted clients when x402 is enabled
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Install dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Create a local env file:
|
||||
|
||||
```bash
|
||||
cp .env-example .env
|
||||
```
|
||||
|
||||
3. Edit `.env` for your infrastructure and access-control mode.
|
||||
|
||||
4. Start the server:
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
By default, the API runs on `http://localhost:5942` and controllers are mounted under `/v6`.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
All configuration is loaded from environment variables (typically from `.env`).
|
||||
|
||||
### Core Server and BCH Infrastructure
|
||||
|
||||
- `PORT` (default: `5942`)
|
||||
- `NODE_ENV` (default: `development`)
|
||||
- `API_PREFIX` (default: `/v6`)
|
||||
- `LOG_LEVEL` (default: `info`)
|
||||
- `RPC_BASEURL` (default: `http://127.0.0.1:8332`)
|
||||
- `RPC_USERNAME`
|
||||
- `RPC_PASSWORD`
|
||||
- `RPC_TIMEOUT_MS` (default: `15000`)
|
||||
- `RPC_REQUEST_ID_PREFIX` (default: `psf-bch-api`)
|
||||
- `FULCRUM_API`
|
||||
- `FULCRUM_TIMEOUT_MS` (default: `15000`)
|
||||
- `SLP_INDEXER_API`
|
||||
- `SLP_INDEXER_TIMEOUT_MS` (default: `15000`)
|
||||
- `REST_URL` or `LOCAL_RESTURL` (default fallback: `http://127.0.0.1:5942/v6/`)
|
||||
- `IPFS_GATEWAY` (default: `p2wdb-gateway-678.fullstack.cash`)
|
||||
- `SERVER_KEEPALIVE_TIMEOUT_MS` (default: `3000`)
|
||||
- `SERVER_HEADERS_TIMEOUT_MS` (default: `65000`)
|
||||
- `SERVER_REQUEST_TIMEOUT_MS` (default: `120000`)
|
||||
|
||||
### x402 + Base + USDC Settings
|
||||
|
||||
- `X402_ENABLED` (default: `true`)
|
||||
- `SERVER_BASE_ADDRESS` (EVM address receiving x402 settlements)
|
||||
- `X402_PRICE_USDC` (USDC charged per request)
|
||||
- `x402_NETWORK` (CAIP-2 chain ID; e.g. `eip155:8453` or `eip155:84532`)
|
||||
- `x402_FACILITATOR_URL` (default: `https://api.cdp.coinbase.com/platform/v2/x402`)
|
||||
- `FACILITATOR_KEY_ID` (required for CDP facilitator auth)
|
||||
- `FACILITATOR_SECRET_KEY` (required for CDP facilitator auth)
|
||||
|
||||
### Optional Bearer Auth
|
||||
|
||||
- `USE_BASIC_AUTH` (default: `false`)
|
||||
- `BASIC_AUTH_TOKEN`
|
||||
|
||||
## Access Control Modes
|
||||
|
||||
Behavior is controlled by `X402_ENABLED` and `USE_BASIC_AUTH`.
|
||||
|
||||
### 1) Open Access
|
||||
|
||||
```bash
|
||||
X402_ENABLED=false
|
||||
USE_BASIC_AUTH=false
|
||||
```
|
||||
|
||||
No payment and no auth checks.
|
||||
|
||||
### 2) Bearer Auth Only
|
||||
|
||||
```bash
|
||||
X402_ENABLED=false
|
||||
USE_BASIC_AUTH=true
|
||||
BASIC_AUTH_TOKEN=my-secret-token
|
||||
```
|
||||
|
||||
Requests (except `/` and `/health`) must send:
|
||||
|
||||
```text
|
||||
Authorization: Bearer my-secret-token
|
||||
```
|
||||
|
||||
### 3) x402 Payments on Base (USDC)
|
||||
|
||||
```bash
|
||||
X402_ENABLED=true
|
||||
USE_BASIC_AUTH=false
|
||||
SERVER_BASE_ADDRESS=0xYourBaseAddress
|
||||
X402_PRICE_USDC=0.1
|
||||
x402_NETWORK=eip155:8453
|
||||
x402_FACILITATOR_URL=https://api.cdp.coinbase.com/platform/v2/x402
|
||||
FACILITATOR_KEY_ID=your_key_id
|
||||
FACILITATOR_SECRET_KEY=your_secret
|
||||
```
|
||||
|
||||
Protected endpoints return `402 Payment Required` when no valid payment is attached. x402-capable clients can pay and retry automatically.
|
||||
|
||||
### 4) x402 + Bearer Bypass
|
||||
|
||||
```bash
|
||||
X402_ENABLED=true
|
||||
USE_BASIC_AUTH=true
|
||||
BASIC_AUTH_TOKEN=my-secret-token
|
||||
```
|
||||
|
||||
Bearer-authenticated requests bypass payment; all others must pay via x402.
|
||||
|
||||
## Client Example: Axios + x402
|
||||
|
||||
Use `examples/01-x402-axios-client.js` to test the payment flow end-to-end.
|
||||
|
||||
What it does:
|
||||
|
||||
1. Calls a protected endpoint without payment and expects `402`.
|
||||
2. Repeats the call using `x402-axios` + an EVM wallet and expects success.
|
||||
|
||||
Run it with an EVM private key:
|
||||
|
||||
```bash
|
||||
PRIVATE_KEY=0x... node examples/01-x402-axios-client.js
|
||||
```
|
||||
|
||||
The script is configured for Base mainnet by default (`viem/chains` `base`). You can switch it to Base Sepolia by using the commented testnet import in the file.
|
||||
|
||||
## Coinbase CDP Credentials
|
||||
|
||||
When using Coinbase's hosted facilitator (`https://api.cdp.coinbase.com/platform/v2/x402`), you must provide:
|
||||
|
||||
- `FACILITATOR_KEY_ID`
|
||||
- `FACILITATOR_SECRET_KEY`
|
||||
|
||||
These are CDP Secret API Key credentials from the Coinbase Developer Platform API Keys dashboard and are used to generate JWT auth headers for `/verify` and `/settle`.
|
||||
|
||||
## Development
|
||||
|
||||
Common commands:
|
||||
|
||||
```bash
|
||||
npm start
|
||||
npm test
|
||||
npm run test:integration
|
||||
npm run docs
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[MIT](./LICENSE.md)
|
||||
|
||||
## x402-bch Payments
|
||||
|
||||
All REST endpoints exposed under the `/v6` prefix are protected by the [`x402-bch-express`](https://www.npmjs.com/package/x402-bch-express) middleware. Each API call requires a BCH payment authorization for **200 satoshis**. The middleware advertises payment requirements via HTTP 402 responses and validates incoming `X-PAYMENT` headers with a configured Facilitator.
|
||||
|
||||
### Configuration
|
||||
|
||||
Environment variables control the payment flow:
|
||||
|
||||
- `X402_ENABLED` — set to `false` (case-insensitive) to disable the middleware. Defaults to enabled.
|
||||
- `SERVER_BCH_ADDRESS` — BCH cash address that receives funding transactions. Defaults to `bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d`.
|
||||
- `FACILITATOR_URL` — Root URL of the facilitator service (e.g., `http://localhost:4345/facilitator`).
|
||||
- `X402_PRICE_SAT` — Optional; override the satoshi price per call (defaults to `200`).
|
||||
|
||||
When `X402_ENABLED=false`, the server continues to operate without payment headers for local development or trusted deployments.
|
||||
|
||||
### Manual Verification
|
||||
|
||||
1. Start or point to an `x402-bch` facilitator service (the example facilitator listens at `http://localhost:4345/facilitator`).
|
||||
2. Run the API server with the default configuration: `npm start`.
|
||||
3. Call a protected endpoint without an `X-PAYMENT` header, e.g. `curl -i http://localhost:5942/v6/full-node/control/getNetworkInfo`. The server will respond with HTTP `402` and include payment requirements.
|
||||
4. Restart the server with `X402_ENABLED=false npm start` to confirm that the same request now bypasses the middleware (useful for local development without payments).
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
+10
-1
@@ -60,6 +60,7 @@ class Server {
|
||||
try {
|
||||
// Create an Express instance.
|
||||
const app = express()
|
||||
app.set('trust proxy', true)
|
||||
|
||||
const x402Settings = getX402Settings()
|
||||
const basicAuthSettings = getBasicAuthSettings()
|
||||
@@ -195,7 +196,10 @@ class Server {
|
||||
|
||||
// Request logging middleware
|
||||
app.use((req, res, next) => {
|
||||
wlogger.info(`${req.method} ${req.path}`)
|
||||
wlogger.info(`${req.method} ${req.path}`, {
|
||||
client_ip: req.ip,
|
||||
remote_address: req.socket?.remoteAddress || null
|
||||
})
|
||||
next()
|
||||
})
|
||||
|
||||
@@ -258,6 +262,11 @@ class Server {
|
||||
wlogger.info(`Server started on port ${this.config.port}`)
|
||||
})
|
||||
|
||||
// Explicit timeout settings reduce stale keep-alive socket reuse races.
|
||||
this.server.keepAliveTimeout = this.config.serverKeepAliveTimeoutMs
|
||||
this.server.headersTimeout = this.config.serverHeadersTimeoutMs
|
||||
this.server.requestTimeout = this.config.serverRequestTimeoutMs
|
||||
|
||||
this.server.on('error', (err) => {
|
||||
console.error('Server error:', err)
|
||||
wlogger.error('Server error:', err)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# 2026-03-16 Update Log
|
||||
|
||||
## Summary
|
||||
|
||||
Reduced noisy error logging for common SLP transaction misses in the `/v6/slp/txid` path.
|
||||
|
||||
## Changes Made
|
||||
|
||||
- Updated `src/use-cases/slp-use-cases.js` in `getTxid()`:
|
||||
- Added a guard for expected missing-record errors (`404` + `Key not found in database`).
|
||||
- Skips `wlogger.error()` for that specific, common case.
|
||||
- Still rethrows the error so API response behavior is unchanged.
|
||||
- Updated `src/controllers/rest-api/slp/controller.js` in `handleError()`:
|
||||
- Added the same guard to suppress duplicate error-level logs for the same expected case.
|
||||
- Keeps normal error logging for all other errors.
|
||||
|
||||
## Outcome
|
||||
|
||||
- The common "Key not found in database" case no longer pollutes error logs.
|
||||
- Unexpected failures continue to be logged at error level.
|
||||
- Client-facing status and error message behavior remains unchanged.
|
||||
@@ -0,0 +1,76 @@
|
||||
# 2026-03-17 Update Log
|
||||
|
||||
## Summary
|
||||
|
||||
Enhanced REST request logging to capture client network identity in Winston logs, enabled proxy-aware IP resolution, and reduced Fulcrum error-log noise for expected missing-transaction requests.
|
||||
|
||||
## Changes Made
|
||||
|
||||
- Updated `bin/server.js`:
|
||||
- Set Express proxy handling with `app.set('trust proxy', true)`.
|
||||
- Kept the current Winston request message format (`"${req.method} ${req.path}"`).
|
||||
- Added structured Winston metadata fields to request logs:
|
||||
- `client_ip` from `req.ip`
|
||||
- `remote_address` from `req.socket.remoteAddress`
|
||||
- Updated `src/adapters/fulcrum-api.js`:
|
||||
- Added parsing helpers to normalize Fulcrum error messages from multiple response shapes.
|
||||
- Mapped common daemon missing-TX error (`No such mempool or blockchain transaction`) to:
|
||||
- status `404`
|
||||
- message `Transaction not found`
|
||||
- Updated `src/use-cases/fulcrum-use-cases.js`:
|
||||
- Removed duplicate error logging in `getTransactionDetails()` and now rethrows adapter errors without a second error-level log.
|
||||
- Updated `src/controllers/rest-api/fulcrum/controller.js`:
|
||||
- Added TXID validation (`64`-character hex) for `GET /v6/fulcrum/tx/data/:txid`.
|
||||
- Updated `handleError()` logging policy:
|
||||
- `Transaction not found` (`404`) logs at `info`
|
||||
- other `4xx` logs at `warn`
|
||||
- `5xx` logs at `error`
|
||||
|
||||
## Useful Fields Available for REST Request Logging
|
||||
|
||||
- Routing and request basics:
|
||||
- `method` (`req.method`)
|
||||
- `path` (`req.path`)
|
||||
- `original_url` (`req.originalUrl`)
|
||||
- `query` (`req.query`)
|
||||
- Client network identity:
|
||||
- `client_ip` (`req.ip`)
|
||||
- `forwarded_ips` (`req.ips`, when behind one or more proxies)
|
||||
- `remote_address` (`req.socket.remoteAddress`)
|
||||
- HTTP and transport:
|
||||
- `protocol` (`req.protocol`)
|
||||
- `secure` (`req.secure`)
|
||||
- `http_version` (`req.httpVersion`)
|
||||
- `host` (`req.get('host')`)
|
||||
- `origin` (`req.get('origin')`)
|
||||
- `referer` (`req.get('referer')`)
|
||||
- `user_agent` (`req.get('user-agent')`)
|
||||
- Request/response performance and size:
|
||||
- `status_code` (`res.statusCode`, from `res.on('finish')`)
|
||||
- `duration_ms` (elapsed time between request start and response finish)
|
||||
- `request_size_bytes` (`req.get('content-length')`)
|
||||
- `response_size_bytes` (`res.getHeader('content-length')`)
|
||||
- App-specific request context in this codebase:
|
||||
- `basic_auth_valid` (`req.locals.basicAuthValid`)
|
||||
- x402 decision/bypass status (derived from middleware path and config)
|
||||
|
||||
## Already Logging
|
||||
|
||||
- In Winston request logs:
|
||||
- `message` with method + path (for example `GET /v6/full-node/blockchain/getBlockCount`)
|
||||
- `client_ip`
|
||||
- `remote_address`
|
||||
- `timestamp` (from Winston timestamp formatter)
|
||||
- `level`
|
||||
- In console endpoint logs:
|
||||
- Request line with method, path, and `req.ip`
|
||||
- Response line with method, path, and final `res.statusCode`
|
||||
|
||||
## Outcome
|
||||
|
||||
- Request logs now preserve existing behavior while adding IP attribution fields.
|
||||
- `trust proxy` ensures `req.ip` is proxy-aware when the server is deployed behind a reverse proxy.
|
||||
- The project now has a documented list of high-value request fields for future logging expansion.
|
||||
- Fulcrum missing-transaction lookups now return cleaner API semantics (`404 Transaction not found`).
|
||||
- Duplicate error logs for a single missing TX lookup were removed.
|
||||
- Invalid TXIDs are rejected early with a `400` validation error.
|
||||
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
Example script creating a key-pair for a Nostr account and publishing profile metadata.
|
||||
Refactored to use REST API instead of WebSocket.
|
||||
|
||||
Run the server with `npm start` in the main directory, before running this example.
|
||||
*/
|
||||
|
||||
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure'
|
||||
import * as nip19 from 'nostr-tools/nip19'
|
||||
import { bytesToHex } from '@noble/hashes/utils.js'
|
||||
|
||||
const API_URL = process.env.API_URL || 'http://localhost:5942'
|
||||
|
||||
// Generate keys
|
||||
const sk = generateSecretKey() // `sk` is a Uint8Array
|
||||
const nsec = nip19.nsecEncode(sk)
|
||||
const skHex = bytesToHex(sk)
|
||||
|
||||
const pk = getPublicKey(sk) // `pk` is a hex string
|
||||
const npub = nip19.npubEncode(pk)
|
||||
|
||||
console.log('private key:', skHex)
|
||||
console.log('encoded private key:', nsec)
|
||||
console.log()
|
||||
console.log('public key:', pk)
|
||||
console.log('encoded public key:', npub)
|
||||
console.log()
|
||||
|
||||
// Create profile metadata event (kind 0)
|
||||
const profileMetadata = {
|
||||
name: 'Alice',
|
||||
about: 'Hello, I am Alice!',
|
||||
picture: 'https://example.com/alice.jpg'
|
||||
}
|
||||
|
||||
const eventTemplate = {
|
||||
kind: 0,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [],
|
||||
content: JSON.stringify(profileMetadata)
|
||||
}
|
||||
|
||||
// Sign the event
|
||||
const signedEvent = finalizeEvent(eventTemplate, sk)
|
||||
console.log('Signed event:', JSON.stringify(signedEvent, null, 2))
|
||||
|
||||
// Publish to REST API
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/event`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(signedEvent)
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
console.log('Publish result:', result)
|
||||
|
||||
if (result.accepted) {
|
||||
console.log('Profile metadata published successfully!')
|
||||
} else {
|
||||
console.error('Failed to publish:', result.message)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error publishing event:', err)
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
Example script for reading posts (kind 1 events) from a relay.
|
||||
Refactored to use REST API instead of WebSocket.
|
||||
|
||||
Run the server with `npm start` in the main directory, before running this example.
|
||||
*/
|
||||
|
||||
const API_URL = process.env.API_URL || 'http://localhost:5942'
|
||||
|
||||
// JB55's public key
|
||||
const jb55 = '32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245'
|
||||
|
||||
// Create subscription ID
|
||||
const subId = 'read-posts-' + Date.now()
|
||||
|
||||
// Create filters - read posts from JB55
|
||||
const filters = {
|
||||
limit: 2,
|
||||
kinds: [1],
|
||||
authors: [jb55]
|
||||
}
|
||||
|
||||
try {
|
||||
// Query events using GET /req/:subId
|
||||
const filtersJson = encodeURIComponent(JSON.stringify([filters]))
|
||||
const url = `${API_URL}/req/${subId}?filters=${filtersJson}`
|
||||
|
||||
console.log(`Querying events from ${API_URL}`)
|
||||
console.log('Filters:', JSON.stringify(filters, null, 2))
|
||||
|
||||
const response = await fetch(url)
|
||||
const events = await response.json()
|
||||
|
||||
console.log(`\nReceived ${events.length} events:`)
|
||||
events.forEach((ev, index) => {
|
||||
console.log(`\nEvent ${index + 1}:`)
|
||||
console.log(' ID:', ev.id)
|
||||
console.log(' Author:', ev.pubkey)
|
||||
console.log(' Created:', new Date(ev.created_at * 1000).toISOString())
|
||||
console.log(' Content:', ev.content)
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Error reading posts:', err)
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
Example script for writing a post to a relay.
|
||||
Refactored to use REST API instead of WebSocket.
|
||||
|
||||
Run the server with `npm start` in the main directory, before running this example.
|
||||
*/
|
||||
|
||||
import { finalizeEvent, getPublicKey } from 'nostr-tools/pure'
|
||||
import { hexToBytes } from '@noble/hashes/utils.js'
|
||||
|
||||
const API_URL = process.env.API_URL || 'http://localhost:5942'
|
||||
|
||||
// Alice is our user making the post.
|
||||
const alicePrivKeyHex = '3292a48aa331aeccce003d50d70fbd79617ba91860abbd2c78fa4a8301e36bc0'
|
||||
const alicePrivKeyBin = hexToBytes(alicePrivKeyHex)
|
||||
const alicePubKey = getPublicKey(alicePrivKeyBin)
|
||||
console.log(`Alice Public Key: ${alicePubKey}`)
|
||||
|
||||
const now = new Date()
|
||||
|
||||
// Generate a post.
|
||||
const eventTemplate = {
|
||||
kind: 1,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [],
|
||||
content: `This is a test message posted at ${now.toLocaleString()}`
|
||||
}
|
||||
console.log(`eventTemplate: ${JSON.stringify(eventTemplate, null, 2)}`)
|
||||
|
||||
// Sign the post
|
||||
const signedEvent = finalizeEvent(eventTemplate, alicePrivKeyBin)
|
||||
console.log('signedEvent:', JSON.stringify(signedEvent, null, 2))
|
||||
|
||||
// Publish to REST API
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/event`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(signedEvent)
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
console.log('result:', result)
|
||||
|
||||
if (result.accepted) {
|
||||
console.log('Post published successfully!')
|
||||
console.log('Event ID:', result.eventId)
|
||||
} else {
|
||||
console.error('Failed to publish:', result.message)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error publishing post:', err)
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
Example script for reading posts from user Alice.
|
||||
Refactored to use REST API instead of WebSocket.
|
||||
|
||||
Run the server with `npm start` in the main directory, before running this example.
|
||||
*/
|
||||
|
||||
import { getPublicKey } from 'nostr-tools/pure'
|
||||
import { hexToBytes } from '@noble/hashes/utils.js'
|
||||
|
||||
const API_URL = process.env.API_URL || 'http://localhost:5942'
|
||||
|
||||
// Alice is our user.
|
||||
const alicePrivKeyHex = '3292a48aa331aeccce003d50d70fbd79617ba91860abbd2c78fa4a8301e36bc0'
|
||||
const alicePrivKeyBin = hexToBytes(alicePrivKeyHex)
|
||||
const alicePubKey = getPublicKey(alicePrivKeyBin)
|
||||
console.log(`Alice Public Key: ${alicePubKey}`)
|
||||
|
||||
// Create subscription ID
|
||||
const subId = 'read-alice-posts-' + Date.now()
|
||||
|
||||
// Create filters - read posts from Alice
|
||||
const filters = {
|
||||
limit: 2,
|
||||
kinds: [1],
|
||||
authors: [alicePubKey]
|
||||
}
|
||||
|
||||
try {
|
||||
// Query events using GET /req/:subId
|
||||
const filtersJson = encodeURIComponent(JSON.stringify([filters]))
|
||||
const url = `${API_URL}/req/${subId}?filters=${filtersJson}`
|
||||
|
||||
console.log(`Querying events from ${API_URL}`)
|
||||
console.log('Filters:', JSON.stringify(filters, null, 2))
|
||||
|
||||
const response = await fetch(url)
|
||||
const events = await response.json()
|
||||
|
||||
console.log(`\nReceived ${events.length} events from Alice:`)
|
||||
events.forEach((ev, index) => {
|
||||
console.log(`\nEvent ${index + 1}:`)
|
||||
console.log(' ID:', ev.id)
|
||||
console.log(' Created:', new Date(ev.created_at * 1000).toISOString())
|
||||
console.log(' Content:', ev.content)
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Error reading Alice posts:', err)
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
Example script for getting a follow list (kind 3 events).
|
||||
Refactored to use REST API instead of WebSocket.
|
||||
|
||||
Run the server with `npm start` in the main directory, before running this example.
|
||||
*/
|
||||
|
||||
import { getPublicKey } from 'nostr-tools/pure'
|
||||
import { hexToBytes } from '@noble/hashes/utils.js'
|
||||
|
||||
const API_URL = process.env.API_URL || 'http://localhost:5942'
|
||||
|
||||
// Alice is our user to get the follow list.
|
||||
const alicePrivKeyHex = '3292a48aa331aeccce003d50d70fbd79617ba91860abbd2c78fa4a8301e36bc0'
|
||||
const alicePrivKeyBin = hexToBytes(alicePrivKeyHex)
|
||||
const alicePubKey = getPublicKey(alicePrivKeyBin)
|
||||
console.log(`Alice Public Key: ${alicePubKey}`)
|
||||
|
||||
// Create subscription ID
|
||||
const subId = 'get-follow-list-' + Date.now()
|
||||
|
||||
// Create filters - get follow list (kind 3) from Alice
|
||||
const filters = {
|
||||
limit: 5,
|
||||
kinds: [3],
|
||||
authors: [alicePubKey]
|
||||
}
|
||||
|
||||
try {
|
||||
// Query events using GET /req/:subId
|
||||
const filtersJson = encodeURIComponent(JSON.stringify([filters]))
|
||||
const url = `${API_URL}/req/${subId}?filters=${filtersJson}`
|
||||
|
||||
console.log(`Querying follow list from ${API_URL}`)
|
||||
console.log('Filters:', JSON.stringify(filters, null, 2))
|
||||
|
||||
const response = await fetch(url)
|
||||
const events = await response.json()
|
||||
|
||||
if (events.length > 0) {
|
||||
// Get the most recent follow list (kind 3 events are replaceable)
|
||||
const followListEvent = events[0]
|
||||
const aliceFollowList = followListEvent.tags.filter(tag => tag[0] === 'p')
|
||||
console.log(`\nAlice Follow list (${aliceFollowList.length} followed users):`)
|
||||
aliceFollowList.forEach((tag, index) => {
|
||||
console.log(` ${index + 1}. ${tag[1]}${tag[3] ? ` (${tag[3]})` : ''}`)
|
||||
})
|
||||
} else {
|
||||
console.log('\nNo follow list found for Alice')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error getting follow list:', err)
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
Example to update the follow list with a new list of people to follow.
|
||||
Refactored to use REST API instead of WebSocket.
|
||||
|
||||
Run the server with `npm start` in the main directory, before running this example.
|
||||
*/
|
||||
|
||||
import { finalizeEvent, getPublicKey } from 'nostr-tools/pure'
|
||||
import { hexToBytes } from '@noble/hashes/utils.js'
|
||||
|
||||
const API_URL = process.env.API_URL || 'http://localhost:5942'
|
||||
|
||||
// Alice wants to update her follow list
|
||||
const alicePrivKeyHex = '3292a48aa331aeccce003d50d70fbd79617ba91860abbd2c78fa4a8301e36bc0'
|
||||
const alicePrivKeyBin = hexToBytes(alicePrivKeyHex)
|
||||
const alicePubKey = getPublicKey(alicePrivKeyBin)
|
||||
console.log(`Alice Public Key: ${alicePubKey}`)
|
||||
|
||||
// Bob is the person to be added to the new follow list
|
||||
const bobPrivKeyHex = 'd2e71a977bc3900d6b0f787421e3d1a666cd12ca625482b0d9eeffd23489c99f'
|
||||
const bobPrivKeyBin = hexToBytes(bobPrivKeyHex)
|
||||
const bobPubKey = getPublicKey(bobPrivKeyBin)
|
||||
console.log(`Bob Public Key: ${bobPubKey}`)
|
||||
|
||||
const psf = 'wss://nostr-relay.psfoundation.info'
|
||||
|
||||
const followList = [
|
||||
['p', bobPubKey, psf, 'bob']
|
||||
]
|
||||
|
||||
// Generate a follow list event (kind 3)
|
||||
const eventTemplate = {
|
||||
kind: 3,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: followList,
|
||||
content: ''
|
||||
}
|
||||
console.log(`eventTemplate: ${JSON.stringify(eventTemplate, null, 2)}`)
|
||||
|
||||
// Sign the event
|
||||
const signedEvent = finalizeEvent(eventTemplate, alicePrivKeyBin)
|
||||
|
||||
// Publish to REST API
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/event`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(signedEvent)
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
console.log('Publish result:', result)
|
||||
|
||||
if (result.accepted) {
|
||||
console.log('Follow list updated successfully!')
|
||||
} else {
|
||||
console.error('Failed to update follow list:', result.message)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error updating follow list:', err)
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
Example script for adding a reaction (like) to an event.
|
||||
Refactored to use REST API instead of WebSocket.
|
||||
https://github.com/nostr-protocol/nips/blob/master/25.md
|
||||
|
||||
Run the server with `npm start` in the main directory, before running this example.
|
||||
*/
|
||||
|
||||
import { hexToBytes } from '@noble/hashes/utils.js'
|
||||
import { finalizeEvent, getPublicKey } from 'nostr-tools/pure'
|
||||
|
||||
const API_URL = process.env.API_URL || 'http://localhost:5942'
|
||||
|
||||
const bobPrivKeyHex = 'd2e71a977bc3900d6b0f787421e3d1a666cd12ca625482b0d9eeffd23489c99f'
|
||||
const bobPrivKeyBin = hexToBytes(bobPrivKeyHex)
|
||||
const bobPubKey = getPublicKey(bobPrivKeyBin)
|
||||
|
||||
const psf = 'wss://nostr-relay.psfoundation.info'
|
||||
|
||||
const evIdToLike = 'd09b4c5da59be3cd2768aa53fa78b77bf4859084c94f3bf26d401f004a9c8167'
|
||||
const evIdAuthorPubKey = '2c7e76c0f8dc1dca9d0197c7d19be580a8d074ccada6a2f6ebe056ae41092e92'
|
||||
|
||||
// Generate like event (kind 7)
|
||||
const likeEventTemplate = {
|
||||
kind: 7,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
pubkey: bobPubKey,
|
||||
tags: [
|
||||
['e', evIdToLike, psf], // "e" tag includes event id, relay reference
|
||||
['p', evIdAuthorPubKey, psf] // "p" tag includes author pubkey, relay reference
|
||||
],
|
||||
content: '+'
|
||||
}
|
||||
|
||||
// Sign the event
|
||||
const signedEvent = finalizeEvent(likeEventTemplate, bobPrivKeyBin)
|
||||
console.log('signedEvent:', JSON.stringify(signedEvent, null, 2))
|
||||
|
||||
// Publish to REST API
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/event`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(signedEvent)
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
console.log('result:', result)
|
||||
|
||||
if (result.accepted) {
|
||||
console.log('Like published successfully!')
|
||||
} else {
|
||||
console.error('Failed to publish like:', result.message)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error publishing like:', err)
|
||||
}
|
||||
+4
-87
@@ -1,90 +1,7 @@
|
||||
# REST2NOSTR Examples
|
||||
# x402 Examples
|
||||
|
||||
This directory contains examples refactored from the `nostr-sandbox/` directory to use the REST API instead of WebSocket connections.
|
||||
This example uses the Base blockchain to talk to the psf-bch-api server at https://x402.fullstack.cash.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
npm install nostr-tools @noble/hashes
|
||||
```
|
||||
|
||||
2. Start the REST2NOSTR proxy server:
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
3. Set the API_URL environment variable if the server is not running on localhost:3000:
|
||||
```bash
|
||||
export API_URL=http://localhost:3000
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### 01-create-account.js
|
||||
Creates a new Nostr account keypair and publishes profile metadata (kind 0 event).
|
||||
|
||||
```bash
|
||||
node examples/01-create-account.js
|
||||
```
|
||||
|
||||
### 02-read-posts.js
|
||||
Reads posts (kind 1 events) from a specific author using GET /req/:subId.
|
||||
|
||||
```bash
|
||||
node examples/02-read-posts.js
|
||||
```
|
||||
|
||||
### 03-write-post.js
|
||||
Publishes a text post (kind 1 event) using POST /event.
|
||||
|
||||
```bash
|
||||
node examples/03-write-post.js
|
||||
```
|
||||
|
||||
### 04-read-alice-posts.js
|
||||
Reads posts from Alice's account using GET /req/:subId with author filter.
|
||||
|
||||
```bash
|
||||
node examples/04-read-alice-posts.js
|
||||
```
|
||||
|
||||
### 14-get-follow-list.js
|
||||
Retrieves a user's follow list (kind 3 event) using GET /req/:subId.
|
||||
|
||||
```bash
|
||||
node examples/05-get-follow-list.js
|
||||
```
|
||||
|
||||
### 15-update-follow-list.js
|
||||
Updates a user's follow list (kind 3 event) using POST /event.
|
||||
|
||||
```bash
|
||||
node examples/06-update-follow-list.js
|
||||
```
|
||||
|
||||
### 17-liking-event.js
|
||||
Adds a reaction/like to an event (kind 7 event) using POST /event.
|
||||
|
||||
```bash
|
||||
node examples/07-liking-event.js
|
||||
```
|
||||
|
||||
## API Endpoints Used
|
||||
|
||||
- **POST /event**: Publish events to the relay
|
||||
- **GET /req/:subId**: Stateless query for events (returns immediately)
|
||||
|
||||
## Differences from WebSocket Examples
|
||||
|
||||
1. **No WebSocket connections**: All communication is via HTTP REST API
|
||||
2. **Stateless queries**: GET /req/:subId returns events immediately rather than streaming
|
||||
3. **Event publishing**: POST /event returns immediately with acceptance status
|
||||
4. **No subscription management**: For stateless queries, subscriptions are automatically closed after EOSE
|
||||
|
||||
## Notes
|
||||
|
||||
- These examples use the same private keys as the original sandbox examples for consistency
|
||||
- The REST API handles WebSocket connections to relays internally
|
||||
- For real-time streaming, use POST /req/:subId which supports Server-Sent Events (SSE)
|
||||
Get a private key for an Base address that contains some USDC. Then execute the example like this:
|
||||
|
||||
- `PRIVATE_KEY=0x8bff4962a50d43fe93ac2d7df4da12e6bd701bd1bf12b66d391365ae03058d4f node 08-x402-axios-client.js`
|
||||
|
||||
Generated
+725
-506
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -16,14 +16,14 @@
|
||||
"description": "REST API proxy to Bitcoin Cash infrastructure",
|
||||
"dependencies": {
|
||||
"@coinbase/cdp-sdk": "1.45.0",
|
||||
"@psf/bch-js": "7.1.11",
|
||||
"@psf/bch-js": "7.1.14",
|
||||
"@x402/core": "2.6.0",
|
||||
"@x402/evm": "2.6.0",
|
||||
"axios": "1.7.7",
|
||||
"cors": "2.8.5",
|
||||
"dotenv": "16.3.1",
|
||||
"express": "5.1.0",
|
||||
"minimal-slp-wallet": "7.1.4",
|
||||
"minimal-slp-wallet": "7.1.5",
|
||||
"psffpp": "1.2.1",
|
||||
"slp-token-media": "1.2.10",
|
||||
"winston": "3.11.0",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -9,7 +9,7 @@ RPC_PASSWORD=password
|
||||
FULCRUM_API=http://172.17.0.1:3001/v1
|
||||
|
||||
# SLP Indexer
|
||||
SLP_INDEXER_API=http://172.17.0.1:5010
|
||||
SLP_INDEXER_API=http://172.17.0.1:5020
|
||||
|
||||
# REST API URL for wallet operations
|
||||
LOCAL_RESTURL=http://172.17.0.1:5942/v6
|
||||
|
||||
@@ -17,4 +17,6 @@ services:
|
||||
volumes:
|
||||
#- ./start-rest2nostr.sh:/home/safeuser/REST2NOSTR/start-rest2nostr.sh
|
||||
- ./.env:/home/safeuser/.env
|
||||
- ../data:/home/safeuser/psf-bch-api/production/data
|
||||
- ../data/logs:/home/safeuser/psf-bch-api/logs
|
||||
restart: always
|
||||
@@ -65,17 +65,28 @@ class FulcrumAPIAdapter {
|
||||
// Attempt to extract error message from response data
|
||||
if (err.response && err.response.data) {
|
||||
const data = err.response.data
|
||||
const status = err.response.status || 400
|
||||
const message = this._extractErrorMessage(data)
|
||||
|
||||
if (this._isCommonMissingTxError(message)) {
|
||||
return this._formatError('Transaction not found', 404)
|
||||
}
|
||||
|
||||
if (message) {
|
||||
return this._formatError(message, status)
|
||||
}
|
||||
|
||||
// Handle structured error responses
|
||||
if (data.error) {
|
||||
return this._formatError(data.error, err.response.status || 400)
|
||||
return this._formatError(data.error, status)
|
||||
}
|
||||
// Handle string error messages
|
||||
if (typeof data === 'string') {
|
||||
return this._formatError(data, err.response.status || 400)
|
||||
return this._formatError(data, status)
|
||||
}
|
||||
// Handle object responses that might contain error info
|
||||
if (typeof data === 'object' && data.message) {
|
||||
return this._formatError(data.message, err.response.status || 400)
|
||||
return this._formatError(data.message, status)
|
||||
}
|
||||
// Fallback to returning the status
|
||||
return this._formatError('Fulcrum API error', err.response.status || 500)
|
||||
@@ -119,6 +130,25 @@ class FulcrumAPIAdapter {
|
||||
status: status || 500
|
||||
}
|
||||
}
|
||||
|
||||
_extractErrorMessage (data) {
|
||||
if (!data) return ''
|
||||
|
||||
if (typeof data === 'string') return data
|
||||
|
||||
if (typeof data === 'object') {
|
||||
if (typeof data.error === 'string') return data.error
|
||||
if (data.error && typeof data.error === 'object' && data.error.message) return data.error.message
|
||||
if (data.message) return data.message
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
_isCommonMissingTxError (message = '') {
|
||||
return typeof message === 'string' &&
|
||||
message.includes('No such mempool or blockchain transaction')
|
||||
}
|
||||
}
|
||||
|
||||
export default FulcrumAPIAdapter
|
||||
|
||||
Vendored
+5
@@ -57,6 +57,11 @@ export default {
|
||||
// Server port
|
||||
port: parseInt(process.env.PORT, 10) || 5942,
|
||||
|
||||
// HTTP server connection lifecycle configuration.
|
||||
serverKeepAliveTimeoutMs: Number(process.env.SERVER_KEEPALIVE_TIMEOUT_MS || 3000),
|
||||
serverHeadersTimeoutMs: Number(process.env.SERVER_HEADERS_TIMEOUT_MS || 65000),
|
||||
serverRequestTimeoutMs: Number(process.env.SERVER_REQUEST_TIMEOUT_MS || 120000),
|
||||
|
||||
// Environment
|
||||
env: process.env.NODE_ENV || 'development',
|
||||
|
||||
|
||||
@@ -87,6 +87,16 @@ class FulcrumRESTController {
|
||||
return cashAddr
|
||||
}
|
||||
|
||||
_isValidTxid (txid) {
|
||||
return typeof txid === 'string' && /^[a-fA-F0-9]{64}$/.test(txid)
|
||||
}
|
||||
|
||||
_isCommonMissingTxError (err) {
|
||||
return err?.status === 404 &&
|
||||
typeof err?.message === 'string' &&
|
||||
err.message.includes('Transaction not found')
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /v6/fulcrum/balance/:address Get balance for a single address
|
||||
* @apiName GetBalance
|
||||
@@ -239,10 +249,10 @@ class FulcrumRESTController {
|
||||
try {
|
||||
const txid = req.params.txid
|
||||
|
||||
if (typeof txid !== 'string') {
|
||||
if (!this._isValidTxid(txid)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'txid must be a string'
|
||||
error: 'txid must be a 64-character hex string'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -424,7 +434,20 @@ class FulcrumRESTController {
|
||||
|
||||
const cashAddr = this._validateAndConvertAddress(address)
|
||||
|
||||
const result = await this.fulcrumUseCases.getTransactions({ address: cashAddr, allTxs })
|
||||
// Extract bearer token from request header if present
|
||||
let bearerToken = null
|
||||
if (req.headers && req.headers.authorization) {
|
||||
const parts = req.headers.authorization.split(' ')
|
||||
if (parts.length === 2 && parts[0] === 'Bearer') {
|
||||
bearerToken = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.fulcrumUseCases.getTransactions({
|
||||
address: cashAddr,
|
||||
allTxs,
|
||||
bearerToken
|
||||
})
|
||||
return res.status(200).json(result)
|
||||
} catch (err) {
|
||||
return this.handleError(err, res)
|
||||
@@ -470,9 +493,19 @@ class FulcrumRESTController {
|
||||
}
|
||||
}
|
||||
|
||||
// Extract bearer token from request header if present
|
||||
let bearerToken = null
|
||||
if (req.headers && req.headers.authorization) {
|
||||
const parts = req.headers.authorization.split(' ')
|
||||
if (parts.length === 2 && parts[0] === 'Bearer') {
|
||||
bearerToken = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.fulcrumUseCases.getTransactionsBulk({
|
||||
addresses: validatedAddresses,
|
||||
allTxs
|
||||
allTxs,
|
||||
bearerToken
|
||||
})
|
||||
return res.status(200).json(result)
|
||||
} catch (err) {
|
||||
@@ -552,9 +585,17 @@ class FulcrumRESTController {
|
||||
}
|
||||
|
||||
handleError (err, res) {
|
||||
wlogger.error('Error in FulcrumRESTController:', err)
|
||||
|
||||
const status = err.status || 500
|
||||
const isCommonMissingTxError = this._isCommonMissingTxError(err)
|
||||
|
||||
if (isCommonMissingTxError) {
|
||||
wlogger.info(`Fulcrum transaction not found: ${err.message}`)
|
||||
} else if (status >= 500) {
|
||||
wlogger.error('Error in FulcrumRESTController:', err)
|
||||
} else {
|
||||
wlogger.warn(`Fulcrum client error (${status}): ${err.message}`)
|
||||
}
|
||||
|
||||
const message = err.message || 'Internal server error'
|
||||
|
||||
return res.status(status).json({ error: message })
|
||||
|
||||
@@ -208,7 +208,14 @@ class SlpRESTController {
|
||||
}
|
||||
|
||||
handleError (err, res) {
|
||||
wlogger.error('Error in SlpRESTController:', err)
|
||||
const isCommonMissingTxError =
|
||||
err?.status === 404 &&
|
||||
typeof err?.message === 'string' &&
|
||||
err.message.includes('Key not found in database')
|
||||
|
||||
if (!isCommonMissingTxError) {
|
||||
wlogger.error('Error in SlpRESTController:', err)
|
||||
}
|
||||
|
||||
const status = err.status || 500
|
||||
const message = err.message || 'Internal server error'
|
||||
|
||||
@@ -6,9 +6,14 @@ import wlogger from '../adapters/wlogger.js'
|
||||
import BCHJS from '@psf/bch-js'
|
||||
import config from '../config/index.js'
|
||||
|
||||
// Use RESTURL (from test) or REST_URL (from psf-bch-api config) or fallback to config
|
||||
const restURL = process.env.RESTURL || process.env.REST_URL || process.env.LOCAL_RESTURL || config.restURL
|
||||
// Use BCHJSBEARERTOKEN (from test) or BASIC_AUTH_TOKEN (from psf-bch-api config) or fallback to config
|
||||
const bearerToken = process.env.BCHJSBEARERTOKEN || process.env.BASIC_AUTH_TOKEN || config.basicAuth.token
|
||||
|
||||
const bchjs = new BCHJS({
|
||||
restURL: config.restURL,
|
||||
bearerToken: config.basicAuth.token
|
||||
restURL,
|
||||
bearerToken
|
||||
})
|
||||
|
||||
class FulcrumUseCases {
|
||||
@@ -57,14 +62,9 @@ class FulcrumUseCases {
|
||||
}
|
||||
|
||||
async getTransactionDetails ({ txid }) {
|
||||
try {
|
||||
const response = await this.fulcrum.get(`electrumx/tx/data/${txid}`)
|
||||
// console.log(`getTransactionDetails() TXID ${txid}: ${JSON.stringify(response, null, 2)}`)
|
||||
return response
|
||||
} catch (err) {
|
||||
wlogger.error('Error in FulcrumUseCases.getTransactionDetails()', err)
|
||||
throw err
|
||||
}
|
||||
const response = await this.fulcrum.get(`electrumx/tx/data/${txid}`)
|
||||
// console.log(`getTransactionDetails() TXID ${txid}: ${JSON.stringify(response, null, 2)}`)
|
||||
return response
|
||||
}
|
||||
|
||||
async getTransactionDetailsBulk ({ txids, verbose }) {
|
||||
@@ -101,13 +101,24 @@ class FulcrumUseCases {
|
||||
}
|
||||
}
|
||||
|
||||
async getTransactions ({ address, allTxs }) {
|
||||
async getTransactions ({ address, allTxs, bearerToken = null }) {
|
||||
try {
|
||||
const response = await this.fulcrum.get(`electrumx/transactions/${address}`)
|
||||
|
||||
// Sort transactions in descending order, so that newest transactions are first.
|
||||
if (response.transactions && Array.isArray(response.transactions)) {
|
||||
response.transactions = await this.bchjs.Electrumx.sortAllTxs(response.transactions, 'DESCENDING')
|
||||
// Use bearer token from request if provided, otherwise use the default bchjs instance
|
||||
let bchjsInstance = this.bchjs
|
||||
if (bearerToken) {
|
||||
// Create a temporary bchjs instance with the bearer token from the request
|
||||
const restURL = process.env.RESTURL || process.env.REST_URL || process.env.LOCAL_RESTURL || config.restURL
|
||||
bchjsInstance = new BCHJS({
|
||||
restURL,
|
||||
bearerToken
|
||||
})
|
||||
}
|
||||
|
||||
response.transactions = await bchjsInstance.Electrumx.sortAllTxs(response.transactions, 'DESCENDING')
|
||||
|
||||
if (!allTxs) {
|
||||
// Return only the first 100 transactions of the history.
|
||||
@@ -122,16 +133,31 @@ class FulcrumUseCases {
|
||||
}
|
||||
}
|
||||
|
||||
async getTransactionsBulk ({ addresses, allTxs }) {
|
||||
async getTransactionsBulk ({ addresses, allTxs, bearerToken = null }) {
|
||||
try {
|
||||
const response = await this.fulcrum.post('electrumx/transactions/', { addresses })
|
||||
|
||||
// Sort transactions in descending order for each address entry.
|
||||
if (response.transactions && Array.isArray(response.transactions)) {
|
||||
// Use bearer token from request if provided, otherwise use the default bchjs instance
|
||||
let bchjsInstance = this.bchjs
|
||||
|
||||
// console.log('getTransactionsBulk() bearerToken: ', bearerToken)
|
||||
if (bearerToken) {
|
||||
// Create a temporary bchjs instance with the bearer token from the request
|
||||
const restURL = config.restURL
|
||||
|
||||
// console.log('getTransactionsBulk() restURL: ', restURL)
|
||||
bchjsInstance = new BCHJS({
|
||||
restURL,
|
||||
bearerToken
|
||||
})
|
||||
}
|
||||
|
||||
for (let i = 0; i < response.transactions.length; i++) {
|
||||
const thisEntry = response.transactions[i]
|
||||
if (thisEntry.transactions && Array.isArray(thisEntry.transactions)) {
|
||||
thisEntry.transactions = await this.bchjs.Electrumx.sortAllTxs(thisEntry.transactions, 'DESCENDING')
|
||||
thisEntry.transactions = await bchjsInstance.Electrumx.sortAllTxs(thisEntry.transactions, 'DESCENDING')
|
||||
|
||||
if (!allTxs && thisEntry.transactions.length > 100) {
|
||||
// Extract only the first 100 transactions.
|
||||
|
||||
@@ -98,7 +98,14 @@ class SlpUseCases {
|
||||
try {
|
||||
return await this.slpIndexer.post('slp/tx/', { txid })
|
||||
} catch (err) {
|
||||
wlogger.error('Error in SlpUseCases.getTxid()', err)
|
||||
const isCommonMissingTxError =
|
||||
err?.status === 404 &&
|
||||
typeof err?.message === 'string' &&
|
||||
err.message.includes('Key not found in database')
|
||||
|
||||
if (!isCommonMissingTxError) {
|
||||
wlogger.error('Error in SlpUseCases.getTxid()', err)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user