mirror of
https://github.com/Permissionless-Software-Foundation/psf-bch-api-base.git
synced 2026-09-21 16:52:00 -07:00
feat(x402): Added x402 Base blockchain support
This commit is contained in:
+19
-13
@@ -1,35 +1,41 @@
|
|||||||
# START INFRASTRUCTURE SETUP
|
# START INFRASTRUCTURE SETUP
|
||||||
|
|
||||||
# Full Node Connection
|
# Full Node Connection
|
||||||
RPC_BASEURL=http://172.17.0.1:8332
|
export RPC_BASEURL=http://172.17.0.1:8332
|
||||||
RPC_USERNAME=bitcoin
|
export RPC_USERNAME=bitcoin
|
||||||
RPC_PASSWORD=password
|
export RPC_PASSWORD=password
|
||||||
|
|
||||||
# Fulcrum Indexer
|
# Fulcrum Indexer
|
||||||
FULCRUM_API=http://172.17.0.1:3001/v1
|
export FULCRUM_API=http://172.17.0.1:3001/v1
|
||||||
|
|
||||||
# SLP Indexer
|
# SLP Indexer
|
||||||
SLP_INDEXER_API=http://localhost:5010
|
export SLP_INDEXER_API=http://localhost:5010
|
||||||
|
|
||||||
# REST API URL for wallet operations
|
# REST API URL for wallet operations
|
||||||
LOCAL_RESTURL=http://localhost:5942/v6
|
export LOCAL_RESTURL=http://localhost:5942/v6
|
||||||
|
|
||||||
# END INFRASTRUCTURE SETUP
|
# END INFRASTRUCTURE SETUP
|
||||||
|
|
||||||
|
|
||||||
# START ACCESS CONTROL
|
# START ACCESS CONTROL
|
||||||
|
|
||||||
PORT=5942
|
export PORT=5942
|
||||||
|
|
||||||
# x402 payments required to access this API?
|
# x402 payments required to access this API?
|
||||||
X402_ENABLED=true
|
# Facilitators https://docs.cdp.coinbase.com/x402/quickstart-for-sellers
|
||||||
SERVER_BCH_ADDRESS=bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d
|
export X402_ENABLED=true
|
||||||
FACILITATOR_URL=http://localhost:4345/facilitator
|
export SERVER_BASE_ADDRESS=0xd32585CE60815654C50CAf350e18de8096061e63
|
||||||
X402_PRICE_SAT=200
|
export FACILITATOR_URL=https://x402.org/facilitator #Testnet Facilitator
|
||||||
|
export X402_PRICE_USDC=0.01
|
||||||
|
|
||||||
|
# Mainnet facilitator access keys
|
||||||
|
export FACILITATOR_KEY_ID=
|
||||||
|
export FACILITATOR_SECRET_KEY=
|
||||||
|
|
||||||
# Basic Authentication required to access this API?
|
# Basic Authentication required to access this API?
|
||||||
USE_BASIC_AUTH=true
|
export USE_BASIC_AUTH=true
|
||||||
BASIC_AUTH_TOKEN=some-random-token
|
export BASIC_AUTH_TOKEN=some-random-token
|
||||||
|
|
||||||
# END ACCESS CONTROL
|
# END ACCESS CONTROL
|
||||||
|
|
||||||
|
npm start
|
||||||
@@ -3,3 +3,4 @@ node_modules/
|
|||||||
logs/
|
logs/
|
||||||
docs/
|
docs/
|
||||||
coverage/
|
coverage/
|
||||||
|
start.sh
|
||||||
|
|||||||
+26
-9
@@ -7,7 +7,8 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import cors from 'cors'
|
import cors from 'cors'
|
||||||
import dotenv from 'dotenv'
|
import dotenv from 'dotenv'
|
||||||
import { paymentMiddleware as x402PaymentMiddleware } from 'x402-bch-express'
|
import { paymentMiddleware as x402PaymentMiddleware } from 'x402-express'
|
||||||
|
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
import { dirname, join } from 'path'
|
import { dirname, join } from 'path'
|
||||||
|
|
||||||
@@ -105,11 +106,19 @@ class Server {
|
|||||||
if (x402Settings.enabled && basicAuthSettings.enabled) {
|
if (x402Settings.enabled && basicAuthSettings.enabled) {
|
||||||
// X402_ENABLED=true AND USE_BASIC_AUTH=true: Apply x402 conditionally
|
// X402_ENABLED=true AND USE_BASIC_AUTH=true: Apply x402 conditionally
|
||||||
const routes = buildX402Routes(this.config.apiPrefix)
|
const routes = buildX402Routes(this.config.apiPrefix)
|
||||||
const facilitatorOptions = x402Settings.facilitatorUrl
|
let facilitatorOptions = x402Settings.facilitatorUrl
|
||||||
? { url: x402Settings.facilitatorUrl }
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
wlogger.info(`x402 middleware enabled with basic auth bypass; enforcing ${x402Settings.priceSat} satoshis per request (unless basic auth provided)`)
|
if (facilitatorOptions) {
|
||||||
|
facilitatorOptions = {
|
||||||
|
url: x402Settings.facilitatorUrl,
|
||||||
|
/** TODO : Auth headers for request mainnet facilitator needed */
|
||||||
|
createAuthHeaders: async () => {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wlogger.info(`x402 middleware enabled with basic auth bypass; enforcing ${x402Settings.priceUSDC} satoshis per request (unless basic auth provided)`)
|
||||||
|
|
||||||
// Create conditional x402 middleware that bypasses if basic auth is valid
|
// Create conditional x402 middleware that bypasses if basic auth is valid
|
||||||
const conditionalX402Middleware = (req, res, next) => {
|
const conditionalX402Middleware = (req, res, next) => {
|
||||||
@@ -130,11 +139,19 @@ class Server {
|
|||||||
} else if (x402Settings.enabled && !basicAuthSettings.enabled) {
|
} else if (x402Settings.enabled && !basicAuthSettings.enabled) {
|
||||||
// X402_ENABLED=true AND USE_BASIC_AUTH=false: Apply x402 unconditionally (no basic auth bypass)
|
// X402_ENABLED=true AND USE_BASIC_AUTH=false: Apply x402 unconditionally (no basic auth bypass)
|
||||||
const routes = buildX402Routes(this.config.apiPrefix)
|
const routes = buildX402Routes(this.config.apiPrefix)
|
||||||
const facilitatorOptions = x402Settings.facilitatorUrl
|
let facilitatorOptions = x402Settings.facilitatorUrl
|
||||||
? { url: x402Settings.facilitatorUrl }
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
wlogger.info(`x402 middleware enabled (basic auth disabled); enforcing ${x402Settings.priceSat} satoshis per request`)
|
if (facilitatorOptions) {
|
||||||
|
facilitatorOptions = {
|
||||||
|
url: x402Settings.facilitatorUrl,
|
||||||
|
/** TODO : Auth headers for request mainnet facilitator needed */
|
||||||
|
createAuthHeaders: async () => {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wlogger.info(`x402 middleware enabled (basic auth disabled); enforcing ${x402Settings.priceUSDC} satoshis per request`)
|
||||||
|
|
||||||
// Apply x402 middleware unconditionally - no basic auth bypass
|
// Apply x402 middleware unconditionally - no basic auth bypass
|
||||||
app.use(x402PaymentMiddleware(
|
app.use(x402PaymentMiddleware(
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import { withPaymentInterceptor } from 'x402-axios'
|
||||||
|
import { mnemonicToAccount } from 'viem/accounts'
|
||||||
|
import { baseSepolia } from 'viem/chains'
|
||||||
|
import { createWalletClient, http } from 'viem'
|
||||||
|
|
||||||
|
const baseURL = 'http://localhost:5942'
|
||||||
|
const endpointPath = '/v6/full-node/blockchain/getBlockchainInfo'
|
||||||
|
const mnemonic = process.env.MNEMONIC || ''
|
||||||
|
|
||||||
|
if (!mnemonic) throw new Error('MNEMONIC env required!')
|
||||||
|
|
||||||
|
// Create a wallet client
|
||||||
|
const account = mnemonicToAccount(mnemonic)
|
||||||
|
const client = createWalletClient({
|
||||||
|
account,
|
||||||
|
transport: http(),
|
||||||
|
chain: baseSepolia
|
||||||
|
})
|
||||||
|
|
||||||
|
const api = withPaymentInterceptor(
|
||||||
|
axios.create({
|
||||||
|
baseURL
|
||||||
|
}),
|
||||||
|
client
|
||||||
|
)
|
||||||
|
|
||||||
|
const request = async () => {
|
||||||
|
console.log('\n\nStep 1: Making first call, expecting a 402 error returned.')
|
||||||
|
try {
|
||||||
|
const response = await axios.get(baseURL + endpointPath)
|
||||||
|
console.log(response.data)
|
||||||
|
console.log('Step 1 failed. Expected a 402 error.')
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`Status code: ${err?.response?.status}`)
|
||||||
|
console.log(`Error data: ${JSON.stringify(err.response.data, null, 2)}`)
|
||||||
|
console.log('\n\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n\nStep 2: Making second call with a payment.')
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Call the same endpoint path with a payment.
|
||||||
|
const paidRes = await api.get(endpointPath)
|
||||||
|
console.log('Data returned after payment: ', paidRes.data)
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Step 2 failed. Expected a 200 success status code.')
|
||||||
|
console.log(`Status code: ${err?.response?.status}`)
|
||||||
|
console.log(`Error data: ${JSON.stringify(err.response.data, null, 2)}`)
|
||||||
|
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request()
|
||||||
Generated
+6422
-180
File diff suppressed because it is too large
Load Diff
+3
-2
@@ -9,7 +9,7 @@
|
|||||||
"lint": "standard --env mocha --fix",
|
"lint": "standard --env mocha --fix",
|
||||||
"test": "npm run lint && TEST=unit c8 mocha 'test/unit/**/*.js' --exit",
|
"test": "npm run lint && TEST=unit c8 mocha 'test/unit/**/*.js' --exit",
|
||||||
"test:integration": "mocha --timeout 25000 'test/integration/**/*.js' --exit",
|
"test:integration": "mocha --timeout 25000 'test/integration/**/*.js' --exit",
|
||||||
"coverage": "c8 --reporter=html mocha 'test/unit/**/*.js' --exit"
|
"coverage": "export NODE_ENV=test && c8 --reporter=html mocha 'test/unit/**/*.js' --exit"
|
||||||
},
|
},
|
||||||
"author": "Chris Troutner <chris.troutner@gmail.com>",
|
"author": "Chris Troutner <chris.troutner@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -25,7 +25,8 @@
|
|||||||
"slp-token-media": "1.2.10",
|
"slp-token-media": "1.2.10",
|
||||||
"winston": "3.11.0",
|
"winston": "3.11.0",
|
||||||
"winston-daily-rotate-file": "4.7.1",
|
"winston-daily-rotate-file": "4.7.1",
|
||||||
"x402-bch-express": "2.0.0"
|
"x402-axios": "1.1.0",
|
||||||
|
"x402-express": "1.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"apidoc": "1.2.0",
|
"apidoc": "1.2.0",
|
||||||
|
|||||||
Vendored
+6
-4
@@ -28,14 +28,16 @@ const normalizeBoolean = (value, defaultValue) => {
|
|||||||
|
|
||||||
// By default, the price per API call is 200 satoshis.
|
// By default, the price per API call is 200 satoshis.
|
||||||
// But the user can override this value by setting the X402_PRICE_SAT environment variable.
|
// But the user can override this value by setting the X402_PRICE_SAT environment variable.
|
||||||
const parsedPriceSat = Number(process.env.X402_PRICE_SAT)
|
const parsedPrice = Number(process.env.X402_PRICE_USDC)
|
||||||
const priceSat = Number.isFinite(parsedPriceSat) && parsedPriceSat > 0 ? parsedPriceSat : 200
|
const priceUSDC = Number.isFinite(parsedPrice) && parsedPrice > 0 ? parsedPrice : 200
|
||||||
|
|
||||||
const x402Defaults = {
|
const x402Defaults = {
|
||||||
enabled: normalizeBoolean(process.env.X402_ENABLED, true),
|
enabled: normalizeBoolean(process.env.X402_ENABLED, true),
|
||||||
facilitatorUrl: process.env.FACILITATOR_URL || 'http://localhost:4345/facilitator',
|
facilitatorUrl: process.env.FACILITATOR_URL || 'http://localhost:4345/facilitator',
|
||||||
serverAddress: process.env.SERVER_BCH_ADDRESS || 'bitcoincash:qqsrke9lh257tqen99dkyy2emh4uty0vky9y0z0lsr',
|
serverAddress: process.env.SERVER_BASE_ADDRESS || 'bitcoincash:qqsrke9lh257tqen99dkyy2emh4uty0vky9y0z0lsr',
|
||||||
priceSat
|
facilitatorKeyId: process.env.FACILITATOR_KEY_ID || '',
|
||||||
|
facilitatorSecretKey: process.env.FACILITATOR_SECRET_KEY || '',
|
||||||
|
priceUSDC
|
||||||
}
|
}
|
||||||
|
|
||||||
const basicAuthDefaults = {
|
const basicAuthDefaults = {
|
||||||
|
|||||||
+7
-5
@@ -2,7 +2,7 @@ import config from './index.js'
|
|||||||
|
|
||||||
const DEFAULT_DESCRIPTION = 'Access to protected psf-bch-api resources'
|
const DEFAULT_DESCRIPTION = 'Access to protected psf-bch-api resources'
|
||||||
const DEFAULT_TIMEOUT_SECONDS = 60
|
const DEFAULT_TIMEOUT_SECONDS = 60
|
||||||
const NETWORK = 'bch'
|
const NETWORK = 'base-sepolia'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a route configuration map for x402-bch middleware.
|
* Builds a route configuration map for x402-bch middleware.
|
||||||
@@ -23,10 +23,10 @@ export function buildX402Routes (apiPrefix = '/v6') {
|
|||||||
return {
|
return {
|
||||||
network: NETWORK,
|
network: NETWORK,
|
||||||
[routeKey]: {
|
[routeKey]: {
|
||||||
price: config.x402.priceSat,
|
price: config.x402.priceUSDC,
|
||||||
network: NETWORK,
|
network: NETWORK,
|
||||||
config: {
|
config: {
|
||||||
description: `${DEFAULT_DESCRIPTION} (${config.x402.priceSat} satoshis)`,
|
description: `${DEFAULT_DESCRIPTION} (${config.x402.priceUSDC} USDC)`,
|
||||||
maxTimeoutSeconds: DEFAULT_TIMEOUT_SECONDS
|
maxTimeoutSeconds: DEFAULT_TIMEOUT_SECONDS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,9 +36,11 @@ export function buildX402Routes (apiPrefix = '/v6') {
|
|||||||
export function getX402Settings () {
|
export function getX402Settings () {
|
||||||
return {
|
return {
|
||||||
enabled: Boolean(config.x402?.enabled),
|
enabled: Boolean(config.x402?.enabled),
|
||||||
facilitatorUrl: config.x402?.facilitatorUrl,
|
facilitatorUrl: config.x402?.facilitatorUrl, // https://docs.cdp.coinbase.com/x402/quickstart-for-sellers
|
||||||
|
facilitatorKeyId: config.x402?.facilitatorKeyId,
|
||||||
|
facilitatorSecretKey: config.x402?.facilitatorSecretKey,
|
||||||
serverAddress: config.x402?.serverAddress,
|
serverAddress: config.x402?.serverAddress,
|
||||||
priceSat: config.x402?.priceSat
|
priceUSDC: config.x402?.priceUSDC
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user