feat(x402): Implemented x402 Base facilitator

This commit is contained in:
Daniel Gonzalez
2026-03-18 20:26:59 -04:00
parent e9606ecfe8
commit 1812f11f4e
29 changed files with 5179 additions and 1642 deletions
+5 -1
View File
@@ -158,5 +158,9 @@ export default {
disableNewAccounts: process.env.DISABLE_NEW_ACCOUNTS ? true : false, disableNewAccounts: process.env.DISABLE_NEW_ACCOUNTS ? true : false,
// Admin password // Admin password
adminPassword: process.env.ADMIN_PASSWORD adminPassword: process.env.ADMIN_PASSWORD,
// Facilitator Configuration
network: process.env.NETWORK || 'base-sepolia',
evmPrivateKey: process.env.EVM_PRIVATE_KEY ? process.env.EVM_PRIVATE_KEY : '0x47e17173e204a3007023e19f210b31e1398d50a1df270243df997e0b57b1208a'
} }
+7
View File
@@ -0,0 +1,7 @@
export DISABLE_IPFS=1
export NO_MONGO=1
export PORT=4022
export EVM_PRIVATE_KEY=
export NETWORK=base
npm start
+3651 -1639
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -25,10 +25,13 @@
}, },
"repository": "Permissionless-Software-Foundation/ipfs-service-provider", "repository": "Permissionless-Software-Foundation/ipfs-service-provider",
"dependencies": { "dependencies": {
"@x402/core": "^2.7.0",
"@x402/evm": "^2.7.0",
"axios": "0.27.2", "axios": "0.27.2",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"glob": "7.1.6", "glob": "7.1.6",
"helia-coord": "2.0.3", "helia-coord": "2.0.3",
"install": "^0.13.0",
"jsonrpc-lite": "2.2.0", "jsonrpc-lite": "2.2.0",
"jsonwebtoken": "8.5.1", "jsonwebtoken": "8.5.1",
"jwt-bch-lib": "1.3.0", "jwt-bch-lib": "1.3.0",
@@ -48,8 +51,10 @@
"mongoose": "5.13.14", "mongoose": "5.13.14",
"node-fetch": "npm:@achingbrain/node-fetch@2.6.7", "node-fetch": "npm:@achingbrain/node-fetch@2.6.7",
"nodemailer": "6.7.5", "nodemailer": "6.7.5",
"npm": "^11.11.1",
"passport-local": "1.0.0", "passport-local": "1.0.0",
"public-ip": "6.0.1", "public-ip": "6.0.1",
"viem": "^2.47.4",
"winston": "3.3.3", "winston": "3.3.3",
"winston-daily-rotate-file": "4.5.0" "winston-daily-rotate-file": "4.5.0"
}, },
+87
View File
@@ -0,0 +1,87 @@
import { x402Facilitator as X402Facilitator } from '@x402/core/facilitator'
import { registerExactEvmScheme } from '@x402/evm/exact/facilitator'
import { toFacilitatorEvmSigner } from '@x402/evm'
import { createWalletClient, http, publicActions } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { baseSepolia, base } from 'viem/chains'
class FacilitatorAdapter {
constructor (localConfig = {}) {
this.config = localConfig
this.facilitator = new X402Facilitator()
this.NETWORK = this.config.network || 'base'
this.EVM_PRIVATE_KEY = this.config.evmPrivateKey
this.createWalletClient = createWalletClient
this.registerExactEvmScheme = registerExactEvmScheme
this.privateKeyToAccount = privateKeyToAccount
this.toFacilitatorEvmSigner = toFacilitatorEvmSigner
this.start = this.start.bind(this)
}
async start () {
try {
if (!this.EVM_PRIVATE_KEY) {
throw new Error("Property 'EVM_PRIVATE_KEY' must be a string!")
}
const _chainId = this.NETWORK === 'base' ? base?.id : baseSepolia?.id
const chainId = `eip155:${_chainId}`
// Initialize the EVM account from private key
const evmAccount = this.privateKeyToAccount(this.EVM_PRIVATE_KEY)
console.info(`EVM Facilitator account: ${evmAccount.address}`)
// Create a Viem client with both wallet and public capabilities
const viemClient = this.createWalletClient({
account: evmAccount,
chain: this.NETWORK === 'base' ? base : baseSepolia,
transport: http()
}).extend(publicActions)
// Initialize the x402 Facilitator with EVM support
const evmSigner = this.toFacilitatorEvmSigner({
getCode: (args) => viemClient.getCode(args),
address: evmAccount.address,
readContract: (args) =>
viemClient.readContract({
...args,
args: args.args || []
}),
verifyTypedData: (args) => viemClient.verifyTypedData(args),
writeContract: (args) =>
viemClient.writeContract({
...args,
args: args.args || []
}),
sendTransaction: (args) => viemClient.sendTransaction(args),
waitForTransactionReceipt: (args) => viemClient.waitForTransactionReceipt(args)
})
this.registerExactEvmScheme(this.facilitator, {
signer: evmSigner,
networks: [chainId]
})
this.facilitator
.onBeforeVerify(async (ctx) => { console.log('Before verify', ctx) })
.onAfterVerify(async (ctx) => { console.log('After verify', ctx) })
.onVerifyFailure(async (ctx) => { console.log('Verify failure', ctx) })
.onBeforeSettle(async (ctx) => { console.log('Before settle', ctx) })
.onAfterSettle(async (ctx) => { console.log('After settle', ctx) })
.onSettleFailure(async (ctx) => { console.log('Settle failure', ctx) })
return this.facilitator
} catch (error) {
console.error('Error in facilitator.js/start()')
throw error
}
}
}
export default FacilitatorAdapter
+6 -1
View File
@@ -18,6 +18,7 @@ import JSONFiles from './json-files.js'
import FullStackJWT from './fullstack-jwt.js' import FullStackJWT from './fullstack-jwt.js'
import config from '../../config/index.js' import config from '../../config/index.js'
import Wallet from './wallet.adapter.js' import Wallet from './wallet.adapter.js'
import FacilitatorAdapter from './facilitator.js'
class Adapters { class Adapters {
constructor (localConfig = {}) { constructor (localConfig = {}) {
@@ -31,7 +32,7 @@ class Adapters {
this.bchjs = new BCHJS({ restURL: config.apiServer }) this.bchjs = new BCHJS({ restURL: config.apiServer })
this.config = config this.config = config
this.wallet = new Wallet(localConfig) this.wallet = new Wallet(localConfig)
this.facilitator = new FacilitatorAdapter(config)
// Get a valid JWT API key and instance bch-js. // Get a valid JWT API key and instance bch-js.
this.fullStackJwt = new FullStackJWT(config) this.fullStackJwt = new FullStackJWT(config)
} }
@@ -65,6 +66,10 @@ class Adapters {
console.log('Not starting IPFS node since this is an e2e test.') console.log('Not starting IPFS node since this is an e2e test.')
} }
await this.facilitator.start()
console.log('Facilitator Adapter is ready.')
console.log('Async Adapters have been started.') console.log('Async Adapters have been started.')
return true return true
+15
View File
@@ -14,6 +14,9 @@ import LogsRESTController from './logs/index.js'
import IpfsRESTController from './ipfs/index.js' import IpfsRESTController from './ipfs/index.js'
import config from '../../../config/index.js' import config from '../../../config/index.js'
import UsageRESTController from './usage/index.js' import UsageRESTController from './usage/index.js'
import SettleRouter from './settle/index.js'
import VerifyRouter from './verify/index.js'
import SupportedRouter from './supported/index.js'
class RESTControllers { class RESTControllers {
constructor (localConfig = {}) { constructor (localConfig = {}) {
@@ -54,6 +57,18 @@ class RESTControllers {
userRouter.attach(app) userRouter.attach(app)
} }
// Attach the REST API Controllers associated with the /settle route
const settleRouter = new SettleRouter(dependencies)
settleRouter.attach(app)
// Attach the REST API Controllers associated with the /verify route
const verifyRouter = new VerifyRouter(dependencies)
verifyRouter.attach(app)
// Attach the REST API Controllers associated with the /supported route
const supportedRouter = new SupportedRouter(dependencies)
supportedRouter.attach(app)
// Attach the REST API Controllers associated with the /contact route // Attach the REST API Controllers associated with the /contact route
const contactRESTController = new ContactRESTController(dependencies) const contactRESTController = new ContactRESTController(dependencies)
contactRESTController.attach(app) contactRESTController.attach(app)
@@ -0,0 +1,56 @@
/*
REST API Controller library for the /settle route
*/
class SettleRESTControllerLib {
constructor (localConfig = {}) {
// Dependency Injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of Adapters library required when instantiating /settle REST Controller.'
)
}
this.useCases = localConfig.useCases
if (!this.useCases) {
throw new Error(
'Instance of Use Cases library required when instantifying /settle REST Controller.'
)
}
this.settle = this.settle.bind(this)
this.handleError = this.handleError.bind(this)
}
async settle (ctx) {
try {
const inputObj = ctx.request.body
const result = await this.useCases.settle.settle(inputObj)
ctx.body = result
} catch (err) {
// console.log(`err.message: ${err.message}`)
// console.log('err: ', err)
// ctx.throw(422, err.message)
this.handleError(ctx, err)
}
}
// DRY error handler
handleError (ctx, err) {
// If an HTTP status is specified by the buisiness logic, use that.
if (err.status) {
if (err.message) {
ctx.throw(err.status, err.message)
} else {
ctx.throw(err.status)
}
} else {
// By default use a 422 error if the HTTP status is not specified.
ctx.throw(422, err.message)
}
}
}
export default SettleRESTControllerLib
+58
View File
@@ -0,0 +1,58 @@
/*
REST API library for /settle route.
*/
// Public npm libraries.
import Router from 'koa-router'
// Local libraries.
import SettleRESTControllerLib from './controller.js'
import config from '../../../../config/index.js'
class SettleRouter {
constructor (localConfig = {}) {
// Dependency Injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of Adapters library required when instantiating Settle Router.'
)
}
this.useCases = localConfig.useCases
if (!this.useCases) {
throw new Error(
'Instance of Use Cases library required when instantiating Settle Router.'
)
}
const dependencies = {
adapters: this.adapters,
useCases: this.useCases
}
// Encapsulate dependencies.
this.config = config
this.settleRESTController = new SettleRESTControllerLib(dependencies)
// Instantiate the router and set the base route.
const baseUrl = '/settle'
this.router = new Router({ prefix: baseUrl })
}
attach (app) {
if (!app) {
throw new Error(
'Must pass app object when attaching REST API controllers.'
)
}
// Define the routes and attach the controller.
this.router.post('/', this.settleRESTController.settle)
// Attach the Controller routes to the Koa app.
app.use(this.router.routes())
app.use(this.router.allowedMethods())
}
}
export default SettleRouter
@@ -0,0 +1,53 @@
/*
REST API Controller library for the /supported route
*/
class SupportedRESTControllerLib {
constructor (localConfig = {}) {
// Dependency Injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of Adapters library required when instantiating /supported REST Controller.'
)
}
this.useCases = localConfig.useCases
if (!this.useCases) {
throw new Error(
'Instance of Use Cases library required when instantifying /supported REST Controller.'
)
}
this.supported = this.supported.bind(this)
this.handleError = this.handleError.bind(this)
}
async supported (ctx) {
try {
const result = await this.useCases.supported.supported()
ctx.body = result
} catch (err) {
// console.log(`err.message: ${err.message}`)
// console.log('err: ', err)
// ctx.throw(422, err.message)
this.handleError(ctx, err)
}
}
// DRY error handler
handleError (ctx, err) {
// If an HTTP status is specified by the buisiness logic, use that.
if (err.status) {
if (err.message) {
ctx.throw(err.status, err.message)
} else {
ctx.throw(err.status)
}
} else {
// By default use a 422 error if the HTTP status is not specified.
ctx.throw(422, err.message)
}
}
}
export default SupportedRESTControllerLib
@@ -0,0 +1,58 @@
/*
REST API library for /supported route.
*/
// Public npm libraries.
import Router from 'koa-router'
// Local libraries.
import SupportedRESTControllerLib from './controller.js'
import config from '../../../../config/index.js'
class SupportedRouter {
constructor (localConfig = {}) {
// Dependency Injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of Adapters library required when instantiating Supported Router.'
)
}
this.useCases = localConfig.useCases
if (!this.useCases) {
throw new Error(
'Instance of Use Cases library required when instantiating Supported Router.'
)
}
const dependencies = {
adapters: this.adapters,
useCases: this.useCases
}
// Encapsulate dependencies.
this.config = config
this.supportedRESTController = new SupportedRESTControllerLib(dependencies)
// Instantiate the router and set the base route.
const baseUrl = '/supported'
this.router = new Router({ prefix: baseUrl })
}
attach (app) {
if (!app) {
throw new Error(
'Must pass app object when attaching REST API controllers.'
)
}
// Define the routes and attach the controller.
this.router.get('/', this.supportedRESTController.supported)
// Attach the Controller routes to the Koa app.
app.use(this.router.routes())
app.use(this.router.allowedMethods())
}
}
export default SupportedRouter
@@ -0,0 +1,56 @@
/*
REST API Controller library for the /verify route
*/
class VerifyRESTControllerLib {
constructor (localConfig = {}) {
// Dependency Injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of Adapters library required when instantiating /verify REST Controller.'
)
}
this.useCases = localConfig.useCases
if (!this.useCases) {
throw new Error(
'Instance of Use Cases library required when instantifying /verify REST Controller.'
)
}
this.verify = this.verify.bind(this)
this.handleError = this.handleError.bind(this)
}
async verify (ctx) {
try {
const inputObj = ctx.request.body
const result = await this.useCases.verify.verify(inputObj)
ctx.body = result
} catch (err) {
// console.log(`err.message: ${err.message}`)
// console.log('err: ', err)
// ctx.throw(422, err.message)
this.handleError(ctx, err)
}
}
// DRY error handler
handleError (ctx, err) {
// If an HTTP status is specified by the buisiness logic, use that.
if (err.status) {
if (err.message) {
ctx.throw(err.status, err.message)
} else {
ctx.throw(err.status)
}
} else {
// By default use a 422 error if the HTTP status is not specified.
ctx.throw(422, err.message)
}
}
}
export default VerifyRESTControllerLib
+58
View File
@@ -0,0 +1,58 @@
/*
REST API library for /verify route.
*/
// Public npm libraries.
import Router from 'koa-router'
// Local libraries.
import VerifyRESTControllerLib from './controller.js'
import config from '../../../../config/index.js'
class VerifyRouter {
constructor (localConfig = {}) {
// Dependency Injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of Adapters library required when instantiating Verify Router.'
)
}
this.useCases = localConfig.useCases
if (!this.useCases) {
throw new Error(
'Instance of Use Cases library required when instantiating Verify Router.'
)
}
const dependencies = {
adapters: this.adapters,
useCases: this.useCases
}
// Encapsulate dependencies.
this.config = config
this.verifyRESTController = new VerifyRESTControllerLib(dependencies)
// Instantiate the router and set the base route.
const baseUrl = '/verify'
this.router = new Router({ prefix: baseUrl })
}
attach (app) {
if (!app) {
throw new Error(
'Must pass app object when attaching REST API controllers.'
)
}
// Define the routes and attach the controller.
this.router.post('/', this.verifyRESTController.verify)
// Attach the Controller routes to the Koa app.
app.use(this.router.routes())
app.use(this.router.allowedMethods())
}
}
export default VerifyRouter
+6
View File
@@ -7,6 +7,9 @@
// Local libraries // Local libraries
import UserUseCases from './user.js' import UserUseCases from './user.js'
import { UsageUseCases } from './usage-use-cases.js' import { UsageUseCases } from './usage-use-cases.js'
import SettleUseCases from './settle.js'
import VerifyUseCases from './verify.js'
import SupportedUseCases from './supported.js'
class UseCases { class UseCases {
constructor (localConfig = {}) { constructor (localConfig = {}) {
@@ -20,6 +23,9 @@ class UseCases {
// console.log('use-cases/index.js localConfig: ', localConfig) // console.log('use-cases/index.js localConfig: ', localConfig)
this.user = new UserUseCases(localConfig) this.user = new UserUseCases(localConfig)
this.usage = new UsageUseCases(localConfig) this.usage = new UsageUseCases(localConfig)
this.settle = new SettleUseCases(localConfig)
this.verify = new VerifyUseCases(localConfig)
this.supported = new SupportedUseCases(localConfig)
} }
// Run any startup Use Cases at the start of the app. // Run any startup Use Cases at the start of the app.
+41
View File
@@ -0,0 +1,41 @@
/*
This library contains business-logic for dealing with settle x402 payment transactions.
*/
import wlogger from '../adapters/wlogger.js'
class SettleLib {
constructor (localConfig = {}) {
// console.log('User localConfig: ', localConfig)
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of adapters must be passed in when instantiating Settle Use Cases library.'
)
}
this.facilitator = this.adapters.facilitator.facilitator
}
async settle (inputObj = {}) {
try {
const { paymentPayload, paymentRequirements } = inputObj
if (!paymentPayload || !paymentRequirements) {
throw new Error('Missing paymentPayload or paymentRequirements')
}
const response = await this.facilitator.settle(
paymentPayload,
paymentRequirements
)
console.log('Payment Successfully with txId', response.transaction)
return response
} catch (err) {
// console.log('createUser() error: ', err)
wlogger.error('Error in lib/settle.js/settle()')
throw err
}
}
}
export default SettleLib
+31
View File
@@ -0,0 +1,31 @@
/*
This library contains business-logic for dealing with supported x402 payment transactions.
*/
import wlogger from '../adapters/wlogger.js'
class SupportedLib {
constructor (localConfig = {}) {
// console.log('User localConfig: ', localConfig)
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of adapters must be passed in when instantiating Supported Use Cases library.'
)
}
this.facilitator = this.adapters.facilitator.facilitator
}
async supported () {
try {
const response = this.facilitator.getSupported()
return response
} catch (err) {
// console.log('createUser() error: ', err)
wlogger.error('Error in lib/supported.js/supported()')
throw err
}
}
}
export default SupportedLib
+41
View File
@@ -0,0 +1,41 @@
/*
This library contains business-logic for dealing with verify x402 payment transactions.
*/
import wlogger from '../adapters/wlogger.js'
class VerifyLib {
constructor (localConfig = {}) {
// console.log('User localConfig: ', localConfig)
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of adapters must be passed in when instantiating Verify Use Cases library.'
)
}
this.facilitator = this.adapters.facilitator.facilitator
}
async verify (inputObj = {}) {
try {
const { paymentPayload, paymentRequirements } = inputObj
if (!paymentPayload || !paymentRequirements) {
throw new Error('Missing paymentPayload or paymentRequirements')
}
const response = await this.facilitator.verify(
paymentPayload,
paymentRequirements
)
return response
} catch (err) {
console.log('error: ', err)
// console.log('createUser() error: ', err)
wlogger.error('Error in lib/verify.js/verify()')
throw err
}
}
}
export default VerifyLib
+66
View File
@@ -0,0 +1,66 @@
import { assert } from 'chai'
import sinon from 'sinon'
import FacilitatorLib from '../../../src/adapters/facilitator.js'
let uut
let sandbox
describe('Facilitator', () => {
beforeEach(() => {
uut = new FacilitatorLib()
sandbox = sinon.createSandbox()
})
afterEach(() => sandbox.restore())
describe('start()', () => {
it('should throw error if EVM_PRIVATE_KEY property is not provided', async () => {
try {
await uut.start()
assert(false, 'Unexpected result')
} catch (err) {
assert.include(err.message, "Property 'EVM_PRIVATE_KEY' must be a string!")
}
})
it('should start the facilitator', async () => {
try {
uut.EVM_PRIVATE_KEY = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
const viewClientMock = {
getCode: sandbox.stub().resolves('0x1'),
readContract: sandbox.stub().resolves({}),
verifyTypedData: sandbox.stub().resolves(true),
writeContract: sandbox.stub().resolves('0xhash'),
sendTransaction: sandbox.stub().resolves('0xhash'),
waitForTransactionReceipt: sandbox.stub().resolves({})
}
sandbox.stub(uut, 'privateKeyToAccount').returns({
address: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
privateKey: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
})
sandbox.stub(uut, 'createWalletClient').returns({
extend: () => viewClientMock
})
const signerStub = sandbox.stub(uut, 'toFacilitatorEvmSigner').returns({ address: '0x123' })
await uut.start()
// Cover all the methods of the signer
const signerConfig = signerStub.firstCall.args[0]
await signerConfig.getCode({ address: '0x1' })
await signerConfig.readContract({ address: '0x1', abi: [], functionName: 'test' })
await signerConfig.verifyTypedData({})
await signerConfig.writeContract({ address: '0x1', abi: [], functionName: 'test' })
await signerConfig.sendTransaction({})
await signerConfig.waitForTransactionReceipt({})
} catch (err) {
assert(false, 'Unexpected result')
}
})
})
})
@@ -0,0 +1,121 @@
/*
Unit tests for the REST API handler for the /settle endpoints.
*/
// Public npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
// Local support libraries
import adapters from '../../../mocks/adapters/index.js'
import UseCasesMock from '../../../mocks/use-cases/index.js'
// const app = require('../../../mocks/app-mock')
import SettleRESTController from '../../../../../src/controllers/rest-api/settle/controller.js'
import { context as mockContext } from '../../../mocks/ctx-mock.js'
let uut
let sandbox
let ctx
describe('#Settle-REST-Controller', () => {
// const testUser = {}
beforeEach(() => {
const useCases = new UseCasesMock()
uut = new SettleRESTController({ adapters, useCases })
sandbox = sinon.createSandbox()
// Mock the context object.
ctx = mockContext()
})
afterEach(() => sandbox.restore())
describe('#constructor', () => {
it('should throw an error if adapters are not passed in', () => {
try {
uut = new SettleRESTController()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Adapters library required when instantiating /settle REST Controller.'
)
}
})
it('should throw an error if useCases are not passed in', () => {
try {
uut = new SettleRESTController({ adapters })
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Use Cases library required when instantifying /settle REST Controller.'
)
}
})
})
describe('#settle', () => {
it('should return the settle result', async () => {
// Mock dependencies
ctx.body = {}
const result = {
transaction: '1234567890'
}
sandbox.stub(uut.useCases.settle, 'settle').resolves(result)
await uut.settle(ctx)
assert.isObject(ctx.body)
assert.property(ctx.body, 'transaction')
})
it('should catch and throw useCases error', async () => {
try {
// Force an error
sandbox.stub(uut.useCases.settle, 'settle').throws(new Error('test error'))
await uut.settle(ctx)
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'test error')
}
})
})
describe('#handleError', () => {
it('should still throw error if there is no message', () => {
try {
const err = {
status: 404
}
uut.handleError(ctx, err)
} catch (err) {
assert.include(err.message, 'Not Found')
}
})
it('should throw error with message', () => {
try {
const err = {
status: 422,
message: 'test error'
}
uut.handleError(ctx, err)
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
})
@@ -0,0 +1,82 @@
/*
Unit tests for the REST API handler for the /settle endpoints.
*/
// Public npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
// Local support libraries
import adapters from '../../../mocks/adapters/index.js'
import UseCasesMock from '../../../mocks/use-cases/index.js'
// const app = require('../../../mocks/app-mock')
import SettleRouter from '../../../../../src/controllers/rest-api/settle/index.js'
let uut
let sandbox
// let ctx
// const mockContext = require('../../../../unit/mocks/ctx-mock').context
describe('#Settle-REST-Router', () => {
// const testUser = {}
beforeEach(() => {
const useCases = new UseCasesMock()
uut = new SettleRouter({ adapters, useCases })
sandbox = sinon.createSandbox()
// Mock the context object.
// ctx = mockContext()
})
afterEach(() => sandbox.restore())
describe('#constructor', () => {
it('should throw an error if adapters are not passed in', () => {
try {
uut = new SettleRouter()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Adapters library required when instantiating Settle Router.'
)
}
})
it('should throw an error if useCases are not passed in', () => {
try {
uut = new SettleRouter({ adapters })
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Use Cases library required when instantiating Settle Router.'
)
}
})
})
describe('#attach', () => {
it('should throw an error if app is not passed in.', () => {
try {
uut.attach()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Must pass app object when attaching REST API controllers.'
)
}
})
})
})
@@ -0,0 +1,123 @@
/*
Unit tests for the REST API handler for the /supported endpoints.
*/
// Public npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
// Local support libraries
import adapters from '../../../mocks/adapters/index.js'
import UseCasesMock from '../../../mocks/use-cases/index.js'
// const app = require('../../../mocks/app-mock')
import SupportedRESTController from '../../../../../src/controllers/rest-api/supported/controller.js'
import { context as mockContext } from '../../../mocks/ctx-mock.js'
let uut
let sandbox
let ctx
describe('#Supported-REST-Controller', () => {
// const testUser = {}
beforeEach(() => {
const useCases = new UseCasesMock()
uut = new SupportedRESTController({ adapters, useCases })
sandbox = sinon.createSandbox()
// Mock the context object.
ctx = mockContext()
})
afterEach(() => sandbox.restore())
describe('#constructor', () => {
it('should throw an error if adapters are not passed in', () => {
try {
uut = new SupportedRESTController()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Adapters library required when instantiating /supported REST Controller.'
)
}
})
it('should throw an error if useCases are not passed in', () => {
try {
uut = new SupportedRESTController({ adapters })
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Use Cases library required when instantifying /supported REST Controller.'
)
}
})
})
describe('#supported', () => {
it('should return the supported payment kinds and extensions', async () => {
// Mock dependencies
ctx.body = {}
const result = {
paymentKinds: ['x402'],
extensions: ['x402']
}
sandbox.stub(uut.useCases.supported, 'supported').resolves(result)
await uut.supported(ctx)
assert.isObject(ctx.body)
assert.property(ctx.body, 'paymentKinds')
assert.property(ctx.body, 'extensions')
})
it('should catch and throw useCases error', async () => {
try {
// Force an error
sandbox.stub(uut.useCases.supported, 'supported').throws(new Error('test error'))
await uut.supported(ctx)
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'test error')
}
})
})
describe('#handleError', () => {
it('should still throw error if there is no message', () => {
try {
const err = {
status: 404
}
uut.handleError(ctx, err)
} catch (err) {
assert.include(err.message, 'Not Found')
}
})
it('should throw error with message', () => {
try {
const err = {
status: 422,
message: 'test error'
}
uut.handleError(ctx, err)
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
})
@@ -0,0 +1,82 @@
/*
Unit tests for the REST API handler for the /supported endpoints.
*/
// Public npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
// Local support libraries
import adapters from '../../../mocks/adapters/index.js'
import UseCasesMock from '../../../mocks/use-cases/index.js'
// const app = require('../../../mocks/app-mock')
import SupportedRouter from '../../../../../src/controllers/rest-api/supported/index.js'
let uut
let sandbox
// let ctx
// const mockContext = require('../../../../unit/mocks/ctx-mock').context
describe('#Supported-REST-Router', () => {
// const testUser = {}
beforeEach(() => {
const useCases = new UseCasesMock()
uut = new SupportedRouter({ adapters, useCases })
sandbox = sinon.createSandbox()
// Mock the context object.
// ctx = mockContext()
})
afterEach(() => sandbox.restore())
describe('#constructor', () => {
it('should throw an error if adapters are not passed in', () => {
try {
uut = new SupportedRouter()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Adapters library required when instantiating Supported Router.'
)
}
})
it('should throw an error if useCases are not passed in', () => {
try {
uut = new SupportedRouter({ adapters })
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Use Cases library required when instantiating Supported Router.'
)
}
})
})
describe('#attach', () => {
it('should throw an error if app is not passed in.', () => {
try {
uut.attach()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Must pass app object when attaching REST API controllers.'
)
}
})
})
})
@@ -0,0 +1,121 @@
/*
Unit tests for the REST API handler for the /verify endpoints.
*/
// Public npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
// Local support libraries
import adapters from '../../../mocks/adapters/index.js'
import UseCasesMock from '../../../mocks/use-cases/index.js'
// const app = require('../../../mocks/app-mock')
import VerifyRESTController from '../../../../../src/controllers/rest-api/verify/controller.js'
import { context as mockContext } from '../../../mocks/ctx-mock.js'
let uut
let sandbox
let ctx
describe('#Verify-REST-Controller', () => {
// const testUser = {}
beforeEach(() => {
const useCases = new UseCasesMock()
uut = new VerifyRESTController({ adapters, useCases })
sandbox = sinon.createSandbox()
// Mock the context object.
ctx = mockContext()
})
afterEach(() => sandbox.restore())
describe('#constructor', () => {
it('should throw an error if adapters are not passed in', () => {
try {
uut = new VerifyRESTController()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Adapters library required when instantiating /verify REST Controller.'
)
}
})
it('should throw an error if useCases are not passed in', () => {
try {
uut = new VerifyRESTController({ adapters })
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Use Cases library required when instantifying /verify REST Controller.'
)
}
})
})
describe('#verify', () => {
it('should return the verify result', async () => {
// Mock dependencies
ctx.body = {}
const result = {
valid: true
}
sandbox.stub(uut.useCases.verify, 'verify').resolves(result)
await uut.verify(ctx)
assert.isObject(ctx.body)
assert.property(ctx.body, 'valid')
})
it('should catch and throw useCases error', async () => {
try {
// Force an error
sandbox.stub(uut.useCases.verify, 'verify').throws(new Error('test error'))
await uut.verify(ctx)
} catch (err) {
// console.log('err: ', err)
assert.include(err.message, 'test error')
}
})
})
describe('#handleError', () => {
it('should still throw error if there is no message', () => {
try {
const err = {
status: 404
}
uut.handleError(ctx, err)
} catch (err) {
assert.include(err.message, 'Not Found')
}
})
it('should throw error with message', () => {
try {
const err = {
status: 422,
message: 'test error'
}
uut.handleError(ctx, err)
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
})
@@ -0,0 +1,82 @@
/*
Unit tests for the REST API handler for the /verify endpoints.
*/
// Public npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
// Local support libraries
import adapters from '../../../mocks/adapters/index.js'
import UseCasesMock from '../../../mocks/use-cases/index.js'
// const app = require('../../../mocks/app-mock')
import VerifyRouter from '../../../../../src/controllers/rest-api/verify/index.js'
let uut
let sandbox
// let ctx
// const mockContext = require('../../../../unit/mocks/ctx-mock').context
describe('#Verify-REST-Router', () => {
// const testUser = {}
beforeEach(() => {
const useCases = new UseCasesMock()
uut = new VerifyRouter({ adapters, useCases })
sandbox = sinon.createSandbox()
// Mock the context object.
// ctx = mockContext()
})
afterEach(() => sandbox.restore())
describe('#constructor', () => {
it('should throw an error if adapters are not passed in', () => {
try {
uut = new VerifyRouter()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Adapters library required when instantiating Verify Router.'
)
}
})
it('should throw an error if useCases are not passed in', () => {
try {
uut = new VerifyRouter({ adapters })
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Use Cases library required when instantiating Verify Router.'
)
}
})
})
describe('#attach', () => {
it('should throw an error if app is not passed in.', () => {
try {
uut.attach()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Must pass app object when attaching REST API controllers.'
)
}
})
})
})
+23 -1
View File
@@ -108,4 +108,26 @@ const localdb = {
} }
} }
export default { ipfs, localdb }; const facilitator = {
facilitator: {
getSupported: async () => {
return {
paymentKinds: ['x402'],
extensions: ['x402']
}
},
settle: async () => {
return {
paymentKinds: ['x402'],
extensions: ['x402']
}
},
verify: async () => {
return {
paymentKinds: ['x402'],
extensions: ['x402']
}
},
}
}
export default { ipfs, localdb, facilitator };
+30
View File
@@ -57,6 +57,32 @@ class UsageUseCaseMock {
} }
} }
class SupportedUseCaseMock {
async supported() {
return {
paymentKinds: ['x402'],
extensions: ['x402']
}
}
}
class SettleUseCaseMock {
async settle() {
return {
paymentKinds: ['x402'],
extensions: ['x402']
}
}
}
class VerifyUseCaseMock {
async verify() {
return {
paymentKinds: ['x402'],
extensions: ['x402']
}
}
}
class UseCasesMock { class UseCasesMock {
constuctor(localConfig = {}) { constuctor(localConfig = {}) {
// this.user = new UserUseCaseMock(localConfig) // this.user = new UserUseCaseMock(localConfig)
@@ -64,6 +90,10 @@ class UseCasesMock {
user = new UserUseCaseMock() user = new UserUseCaseMock()
usage = new UsageUseCaseMock() usage = new UsageUseCaseMock()
supported = new SupportedUseCaseMock()
settle = new SettleUseCaseMock()
verify = new VerifyUseCaseMock()
} }
export default UseCasesMock; export default UseCasesMock;
+77
View File
@@ -0,0 +1,77 @@
/*
Unit tests for the src/use-cases/settle.js business logic library.
*/
// Public npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
import SettleUseCases from '../../../src/use-cases/settle.js'
import adapters from '../mocks/adapters/index.js'
describe('#settle-use-case', () => {
let uut
let sandbox
before(async () => {
})
beforeEach(() => {
sandbox = sinon.createSandbox()
uut = new SettleUseCases({ adapters })
console.log('SupportedLib adapters: ', adapters.facilitatorMock)
})
afterEach(() => sandbox.restore())
describe('#constructor', () => {
it('should throw an error if adapters are not passed in', () => {
try {
uut = new SettleUseCases()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of adapters must be passed in when instantiating Settle Use Cases library.'
)
}
})
})
describe('#settle', () => {
it('should return the supported payment kinds and extensions', async () => {
const result = await uut.settle({ paymentPayload: {}, paymentRequirements: {} })
assert.isObject(result)
})
it('should throw an error if paymentPayload are not provided', async () => {
try {
await uut.settle({})
assert.fail('Unexpected code path')
} catch (error) {
assert.include(error.message, 'Missing paymentPayload or paymentRequirements')
}
})
it('should throw an error if paymentRequirements are not provided', async () => {
try {
await uut.settle({ paymentPayload: {} })
assert.fail('Unexpected code path')
} catch (error) {
assert.include(error.message, 'Missing paymentPayload or paymentRequirements')
}
})
it('should handle facilitator errors', async () => {
try {
sandbox.stub(uut.facilitator, 'settle').throws(new Error('test error'))
await uut.settle({ paymentPayload: {}, paymentRequirements: {} })
assert.fail('Unexpected code path')
} catch (error) {
assert.include(error.message, 'test error')
}
})
})
})
+61
View File
@@ -0,0 +1,61 @@
/*
Unit tests for the src/use-cases/supported.js business logic library.
*/
// Public npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
import SupportedUseCases from '../../../src/use-cases/supported.js'
import adapters from '../mocks/adapters/index.js'
describe('#supported-use-case', () => {
let uut
let sandbox
before(async () => {
})
beforeEach(() => {
sandbox = sinon.createSandbox()
uut = new SupportedUseCases({ adapters })
console.log('SupportedLib adapters: ', adapters.facilitatorMock)
})
afterEach(() => sandbox.restore())
describe('#constructor', () => {
it('should throw an error if adapters are not passed in', () => {
try {
uut = new SupportedUseCases()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of adapters must be passed in when instantiating Supported Use Cases library.'
)
}
})
})
describe('#supported', () => {
it('should return the supported payment kinds and extensions', async () => {
const result = await uut.supported()
assert.isObject(result)
})
it('should handle errors', async () => {
try {
sandbox.stub(uut.facilitator, 'getSupported').throws(new Error('test error'))
await uut.supported()
assert.fail('Unexpected code path')
} catch (error) {
assert.include(error.message, 'test error')
}
})
})
})
+77
View File
@@ -0,0 +1,77 @@
/*
Unit tests for the src/use-cases/verify.js business logic library.
*/
// Public npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
import VerifyUseCases from '../../../src/use-cases/verify.js'
import adapters from '../mocks/adapters/index.js'
describe('#verify-use-case', () => {
let uut
let sandbox
before(async () => {
})
beforeEach(() => {
sandbox = sinon.createSandbox()
uut = new VerifyUseCases({ adapters })
console.log('SupportedLib adapters: ', adapters.facilitatorMock)
})
afterEach(() => sandbox.restore())
describe('#constructor', () => {
it('should throw an error if adapters are not passed in', () => {
try {
uut = new VerifyUseCases()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of adapters must be passed in when instantiating Verify Use Cases library.'
)
}
})
})
describe('#verify', () => {
it('should return the supported payment kinds and extensions', async () => {
const result = await uut.verify({ paymentPayload: {}, paymentRequirements: {} })
assert.isObject(result)
})
it('should throw an error if paymentPayload are not provided', async () => {
try {
await uut.verify({})
assert.fail('Unexpected code path')
} catch (error) {
assert.include(error.message, 'Missing paymentPayload or paymentRequirements')
}
})
it('should throw an error if paymentRequirements are not provided', async () => {
try {
await uut.verify({ paymentPayload: {} })
assert.fail('Unexpected code path')
} catch (error) {
assert.include(error.message, 'Missing paymentPayload or paymentRequirements')
}
})
it('should handle facilitator errors', async () => {
try {
sandbox.stub(uut.facilitator, 'verify').throws(new Error('test error'))
await uut.verify({ paymentPayload: {}, paymentRequirements: {} })
assert.fail('Unexpected code path')
} catch (error) {
assert.include(error.message, 'test error')
}
})
})
})