@bsv/message-box-client
Version:
A client for P2P messaging and payments
246 lines (208 loc) • 8.54 kB
text/typescript
/* eslint-env jest */
import { PeerPayClient } from '../PeerPayClient.js'
import { WalletClient, CreateHmacResult, PrivateKey } from '@bsv/sdk'
import { jest } from '@jest/globals'
const toArray = (msg: any, enc?: 'hex' | 'utf8' | 'base64'): any[] => {
if (Array.isArray(msg)) return msg.slice()
if (msg === undefined) return []
if (typeof msg !== 'string') {
return Array.from(msg, (item: any) => item | 0)
}
switch (enc) {
case 'hex': {
const matches = msg.match(/.{1,2}/g)
return matches != null ? matches.map(byte => parseInt(byte, 16)) : []
}
case 'base64':
return Array.from(Buffer.from(msg, 'base64'))
default:
return Array.from(Buffer.from(msg, 'utf8'))
}
}
// Mock dependencies
jest.mock('@bsv/sdk', () => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const actualSDK = jest.requireActual('@bsv/sdk') as any
return {
...actualSDK,
WalletClient: jest.fn().mockImplementation(() => ({
getPublicKey: jest.fn(),
createAction: jest.fn(),
internalizeAction: jest.fn(),
createHmac: jest.fn<() => Promise<CreateHmacResult>>().mockResolvedValue({
hmac: [1, 2, 3, 4, 5]
})
}))
}
})
describe('PeerPayClient Unit Tests', () => {
let peerPayClient: PeerPayClient
let mockWalletClient: jest.Mocked<WalletClient>
beforeEach(() => {
jest.clearAllMocks()
mockWalletClient = new WalletClient() as jest.Mocked<WalletClient>
// Ensure a valid compressed public key (33 bytes, hex format)
mockWalletClient.getPublicKey.mockResolvedValue({
publicKey: PrivateKey.fromRandom().toPublicKey().toString()
})
mockWalletClient.createAction.mockResolvedValue({
tx: toArray('mockedTransaction', 'utf8')
})
peerPayClient = new PeerPayClient({
messageBoxHost: 'https://messagebox.babbage.systems',
walletClient: mockWalletClient
})
})
describe('createPaymentToken', () => {
it('should create a valid payment token', async () => {
mockWalletClient.getPublicKey.mockResolvedValue({
publicKey: PrivateKey.fromRandom().toPublicKey().toString()
})
mockWalletClient.createAction.mockResolvedValue({ tx: toArray('mockedTransaction', 'utf8') })
const payment = { recipient: PrivateKey.fromRandom().toPublicKey().toString(), amount: 5 }
const token = await peerPayClient.createPaymentToken(payment)
expect(token).toHaveProperty('amount', 5)
expect(mockWalletClient.getPublicKey).toHaveBeenCalledWith(expect.any(Object), undefined)
expect(mockWalletClient.createAction).toHaveBeenCalledWith(expect.any(Object), undefined)
})
it('should throw an error if recipient public key cannot be derived', async () => {
mockWalletClient.getPublicKey.mockResolvedValue({ publicKey: '' }) // Empty key
await expect(peerPayClient.createPaymentToken({ recipient: 'invalid', amount: 5 }))
.rejects.toThrow('Failed to derive recipient’s public key')
})
it('should throw an error if amount is <= 0', async () => {
(mockWalletClient.getPublicKey as jest.MockedFunction<typeof mockWalletClient.getPublicKey>)
.mockResolvedValue({
publicKey: PrivateKey.fromRandom().toPublicKey().toString()
})
await expect(peerPayClient.createPaymentToken({
recipient: PrivateKey.fromRandom().toPublicKey().toString(),
amount: 0
}))
.rejects.toThrow('Invalid payment details: recipient and valid amount are required')
})
})
// Test: sendPayment
describe('sendPayment', () => {
it('should call sendMessage with valid payment', async () => {
const sendMessageSpy = jest.spyOn(peerPayClient, 'sendMessage').mockResolvedValue({
status: 'success',
messageId: 'mockedMessageId'
})
const payment = { recipient: 'recipientKey', amount: 3 }
console.log('[TEST] Calling sendPayment...')
await peerPayClient.sendPayment(payment)
console.log('[TEST] sendPayment finished.')
expect(sendMessageSpy).toHaveBeenCalledWith({
recipient: 'recipientKey',
messageBox: 'payment_inbox',
body: expect.any(String)
}, undefined)
}, 10000)
})
// Test: sendLivePayment
describe('sendLivePayment', () => {
it('should call createPaymentToken and sendLiveMessage with correct parameters', async () => {
jest.spyOn(peerPayClient, 'createPaymentToken').mockResolvedValue({
customInstructions: {
derivationPrefix: 'prefix',
derivationSuffix: 'suffix'
},
transaction: Array.from(new Uint8Array([1, 2, 3, 4, 5])),
amount: 2
})
jest.spyOn(peerPayClient, 'sendLiveMessage').mockResolvedValue({
status: 'success',
messageId: 'mockedMessageId'
})
const payment = { recipient: 'recipientKey', amount: 2 }
await peerPayClient.sendLivePayment(payment)
expect(peerPayClient.createPaymentToken).toHaveBeenCalledWith(payment)
expect(peerPayClient.sendLiveMessage).toHaveBeenCalledWith({
recipient: 'recipientKey',
messageBox: 'payment_inbox',
body: '{"customInstructions":{"derivationPrefix":"prefix","derivationSuffix":"suffix"},"transaction":[1,2,3,4,5],"amount":2}'
}, undefined)
})
})
// Test: acceptPayment
describe('acceptPayment', () => {
it('should call internalizeAction and acknowledgeMessage', async () => {
mockWalletClient.internalizeAction.mockResolvedValue({ accepted: true })
jest.spyOn(peerPayClient, 'acknowledgeMessage').mockResolvedValue('acknowledged')
const payment = {
messageId: '123',
sender: 'senderKey',
token: {
customInstructions: { derivationPrefix: 'prefix', derivationSuffix: 'suffix' },
transaction: toArray('mockedTransaction', 'utf8'),
amount: 6
}
}
await peerPayClient.acceptPayment(payment)
expect(mockWalletClient.internalizeAction).toHaveBeenCalled()
expect(peerPayClient.acknowledgeMessage).toHaveBeenCalledWith({ messageIds: ['123'] })
})
})
// Test: rejectPayment
describe('rejectPayment', () => {
it('should refund payment minus fee', async () => {
jest.spyOn(peerPayClient, 'acceptPayment').mockResolvedValue(undefined)
jest.spyOn(peerPayClient, 'sendPayment').mockResolvedValue(undefined)
jest.spyOn(peerPayClient, 'acknowledgeMessage').mockResolvedValue('acknowledged')
const payment = {
messageId: '123',
sender: 'senderKey',
token: {
customInstructions: { derivationPrefix: 'prefix', derivationSuffix: 'suffix' },
transaction: toArray('mockedTransaction', 'utf8'),
amount: 2000
}
}
await peerPayClient.rejectPayment(payment)
expect(peerPayClient.acceptPayment).toHaveBeenCalledWith(payment)
expect(peerPayClient.sendPayment).toHaveBeenCalledWith({
recipient: 'senderKey',
amount: 1000 // Deduct satoshi fee
})
expect(peerPayClient.acknowledgeMessage).toHaveBeenCalledWith({
messageIds: ['123']
})
})
})
// Test: listIncomingPayments
describe('listIncomingPayments', () => {
it('should return parsed payment messages', async () => {
jest.spyOn(peerPayClient, 'listMessages').mockResolvedValue([
{
messageId: '1',
sender: 'sender1',
created_at: '2025-03-05T12:00:00Z',
updated_at: '2025-03-05T12:05:00Z',
body: JSON.stringify({
customInstructions: { derivationPrefix: 'prefix1', derivationSuffix: 'suffix1' },
transaction: toArray('mockedTransaction1', 'utf8'),
amount: 3
})
},
{
messageId: '2',
sender: 'sender2',
created_at: '2025-03-05T12:10:00Z',
updated_at: '2025-03-05T12:15:00Z',
body: JSON.stringify({
customInstructions: { derivationPrefix: 'prefix2', derivationSuffix: 'suffix2' },
transaction: toArray('mockedTransaction2', 'utf8'),
amount: 9
})
}
])
const payments = await peerPayClient.listIncomingPayments()
expect(payments).toHaveLength(2)
expect(payments[0]).toHaveProperty('sender', 'sender1')
expect(payments[0].token.amount).toBe(3)
expect(payments[1]).toHaveProperty('sender', 'sender2')
expect(payments[1].token.amount).toBe(9)
})
})
})