Implement @lamassu/lightning package
Lightning.Pub client for Nostr-native Lightning operations: Features: - Account information and balance queries - Invoice generation (BOLT11) - Invoice payment with preimage - Invoice lookup and status checking - Payment watching with callbacks - Exchange rate fetching (mock for now) - Basic invoice decoding Communication: - NIP-47 compatible request/response events - NIP-44 encrypted content - Timeout handling Types: - AccountInfo, Invoice, PaymentResult - ExchangeRate, ChannelInfo, NodeInfo Includes basic unit tests for client initialization. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f35d0330b9
commit
8fc22e5291
6 changed files with 674 additions and 0 deletions
66
lamassu-next/packages/lightning/src/__tests__/client.test.ts
Normal file
66
lamassu-next/packages/lightning/src/__tests__/client.test.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { LightningPubClient } from '../client.js'
|
||||
|
||||
describe('LightningPubClient', () => {
|
||||
const config = {
|
||||
serviceUrl: 'https://lightning.pub',
|
||||
accountPubkey: 'a'.repeat(64),
|
||||
relays: ['wss://relay.example.com'],
|
||||
}
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should create a client with config', () => {
|
||||
const client = new LightningPubClient(config)
|
||||
expect(client).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getExchangeRate', () => {
|
||||
it('should return mock exchange rate for USD', async () => {
|
||||
const client = new LightningPubClient(config)
|
||||
const rate = await client.getExchangeRate('USD')
|
||||
|
||||
expect(rate.currency).toBe('USD')
|
||||
expect(rate.satsPerUnit).toBeGreaterThan(0)
|
||||
expect(rate.timestamp).toBeDefined()
|
||||
})
|
||||
|
||||
it('should return rate for different currencies', async () => {
|
||||
const client = new LightningPubClient(config)
|
||||
|
||||
const usdRate = await client.getExchangeRate('USD')
|
||||
const eurRate = await client.getExchangeRate('EUR')
|
||||
|
||||
expect(usdRate.satsPerUnit).not.toBe(eurRate.satsPerUnit)
|
||||
})
|
||||
})
|
||||
|
||||
describe('decodeInvoice', () => {
|
||||
it('should decode a basic invoice', () => {
|
||||
const client = new LightningPubClient(config)
|
||||
|
||||
// This is a simplified test since we have basic parsing
|
||||
const result = client.decodeInvoice('lnbc100u1...')
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result.expiresAt).toBeGreaterThan(Date.now() / 1000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('initialize', () => {
|
||||
it('should require nostrClient and identity', () => {
|
||||
const client = new LightningPubClient(config)
|
||||
|
||||
// Without initialization, operations should fail
|
||||
expect(client.getBalance()).rejects.toThrow('Client not initialized')
|
||||
})
|
||||
})
|
||||
|
||||
describe('disconnect', () => {
|
||||
it('should clean up resources', () => {
|
||||
const client = new LightningPubClient(config)
|
||||
// Should not throw
|
||||
expect(() => client.disconnect()).not.toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
405
lamassu-next/packages/lightning/src/client.ts
Normal file
405
lamassu-next/packages/lightning/src/client.ts
Normal file
|
|
@ -0,0 +1,405 @@
|
|||
/**
|
||||
* Lightning.Pub Client
|
||||
*
|
||||
* Client for interacting with Lightning.Pub - a Nostr-native
|
||||
* account system that wraps LND.
|
||||
*
|
||||
* Lightning.Pub provides:
|
||||
* - Account management (sublayers)
|
||||
* - Invoice generation
|
||||
* - Payment processing
|
||||
* - CLINK protocol support
|
||||
*/
|
||||
|
||||
import type { Event } from 'nostr-tools'
|
||||
import { finalizeEvent } from 'nostr-tools'
|
||||
import type { MachineIdentity, NostrClient } from '@lamassu/nostr-client'
|
||||
import { encryptContent, decryptJSON } from '@lamassu/nostr-client'
|
||||
import type {
|
||||
LightningPubConfig,
|
||||
AccountInfo,
|
||||
Invoice,
|
||||
PaymentResult,
|
||||
ExchangeRate,
|
||||
InvoiceCallback,
|
||||
} from './types.js'
|
||||
|
||||
/** Event kinds for Lightning.Pub communication */
|
||||
const LIGHTNING_PUB_KINDS = {
|
||||
REQUEST: 23194, // NIP-47 compatible
|
||||
RESPONSE: 23195,
|
||||
}
|
||||
|
||||
/** Request types */
|
||||
type RequestMethod =
|
||||
| 'get_info'
|
||||
| 'get_balance'
|
||||
| 'make_invoice'
|
||||
| 'pay_invoice'
|
||||
| 'lookup_invoice'
|
||||
| 'list_transactions'
|
||||
|
||||
interface Request {
|
||||
method: RequestMethod
|
||||
params: Record<string, unknown>
|
||||
}
|
||||
|
||||
interface Response<T = unknown> {
|
||||
result?: T
|
||||
error?: { code: number; message: string }
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightning.Pub client for ATM operations
|
||||
*/
|
||||
export class LightningPubClient {
|
||||
private config: Required<LightningPubConfig>
|
||||
private nostrClient: NostrClient | null = null
|
||||
private identity: MachineIdentity | null = null
|
||||
private invoiceCallbacks: Map<string, InvoiceCallback> = new Map()
|
||||
private subscriptionId?: string
|
||||
|
||||
constructor(config: LightningPubConfig) {
|
||||
this.config = {
|
||||
timeout: 30000,
|
||||
...config,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the client with Nostr connection
|
||||
*/
|
||||
initialize(nostrClient: NostrClient, identity: MachineIdentity): void {
|
||||
this.nostrClient = nostrClient
|
||||
this.identity = identity
|
||||
this.startListening()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get account information
|
||||
*/
|
||||
async getAccountInfo(): Promise<AccountInfo> {
|
||||
const response = await this.sendRequest<AccountInfo>({
|
||||
method: 'get_info',
|
||||
params: {},
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current balance
|
||||
*/
|
||||
async getBalance(): Promise<{ balanceSats: number }> {
|
||||
const response = await this.sendRequest<{ balance: number }>({
|
||||
method: 'get_balance',
|
||||
params: {},
|
||||
})
|
||||
|
||||
return { balanceSats: response.balance }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Lightning invoice
|
||||
*/
|
||||
async createInvoice(params: {
|
||||
amountMsat: number
|
||||
description?: string
|
||||
expirySecs?: number
|
||||
}): Promise<Invoice> {
|
||||
const response = await this.sendRequest<{
|
||||
payment_request: string
|
||||
payment_hash: string
|
||||
expires_at: number
|
||||
}>({
|
||||
method: 'make_invoice',
|
||||
params: {
|
||||
amount: params.amountMsat,
|
||||
description: params.description || 'ATM Payment',
|
||||
expiry: params.expirySecs || 600,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
paymentRequest: response.payment_request,
|
||||
paymentHash: response.payment_hash,
|
||||
amountMsat: params.amountMsat,
|
||||
description: params.description,
|
||||
createdAt: Math.floor(Date.now() / 1000),
|
||||
expiresAt: response.expires_at,
|
||||
status: 'pending',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pay a Lightning invoice
|
||||
*/
|
||||
async payInvoice(paymentRequest: string): Promise<PaymentResult> {
|
||||
try {
|
||||
const response = await this.sendRequest<{
|
||||
preimage: string
|
||||
fee_paid?: number
|
||||
}>({
|
||||
method: 'pay_invoice',
|
||||
params: {
|
||||
invoice: paymentRequest,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
preimage: response.preimage,
|
||||
feeMsat: response.fee_paid,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Payment failed',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up an invoice by payment hash
|
||||
*/
|
||||
async lookupInvoice(paymentHash: string): Promise<Invoice | null> {
|
||||
try {
|
||||
const response = await this.sendRequest<{
|
||||
payment_request: string
|
||||
amount: number
|
||||
description: string
|
||||
created_at: number
|
||||
expires_at: number
|
||||
settled: boolean
|
||||
preimage?: string
|
||||
}>({
|
||||
method: 'lookup_invoice',
|
||||
params: {
|
||||
payment_hash: paymentHash,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
paymentRequest: response.payment_request,
|
||||
paymentHash,
|
||||
amountMsat: response.amount,
|
||||
description: response.description,
|
||||
createdAt: response.created_at,
|
||||
expiresAt: response.expires_at,
|
||||
status: response.settled ? 'paid' : 'pending',
|
||||
preimage: response.preimage,
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch for invoice payment
|
||||
*/
|
||||
watchInvoice(paymentHash: string, callback: InvoiceCallback): void {
|
||||
this.invoiceCallbacks.set(paymentHash, callback)
|
||||
|
||||
// Poll for payment status
|
||||
const pollInterval = setInterval(async () => {
|
||||
const invoice = await this.lookupInvoice(paymentHash)
|
||||
if (invoice && invoice.status === 'paid') {
|
||||
clearInterval(pollInterval)
|
||||
this.invoiceCallbacks.delete(paymentHash)
|
||||
callback(invoice)
|
||||
}
|
||||
}, 2000)
|
||||
|
||||
// Clean up after expiry
|
||||
setTimeout(
|
||||
() => {
|
||||
clearInterval(pollInterval)
|
||||
this.invoiceCallbacks.delete(paymentHash)
|
||||
},
|
||||
10 * 60 * 1000
|
||||
) // 10 minutes max
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop watching an invoice
|
||||
*/
|
||||
unwatchInvoice(paymentHash: string): void {
|
||||
this.invoiceCallbacks.delete(paymentHash)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a BOLT11 invoice
|
||||
*/
|
||||
decodeInvoice(paymentRequest: string): {
|
||||
amountMsat: number | null
|
||||
paymentHash: string
|
||||
description: string
|
||||
expiresAt: number
|
||||
} {
|
||||
// Basic invoice parsing (for production, use bolt11 library)
|
||||
// This is a simplified version
|
||||
const amountMatch = paymentRequest.match(/lnbc(\d+)([munp]?)/)
|
||||
let amountMsat: number | null = null
|
||||
|
||||
if (amountMatch) {
|
||||
const [, amount, multiplier] = amountMatch
|
||||
const baseAmount = parseInt(amount ?? '0', 10)
|
||||
switch (multiplier) {
|
||||
case 'm':
|
||||
amountMsat = baseAmount * 100_000_000
|
||||
break
|
||||
case 'u':
|
||||
amountMsat = baseAmount * 100_000
|
||||
break
|
||||
case 'n':
|
||||
amountMsat = baseAmount * 100
|
||||
break
|
||||
case 'p':
|
||||
amountMsat = baseAmount / 10
|
||||
break
|
||||
default:
|
||||
amountMsat = baseAmount * 100_000_000_000
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
amountMsat,
|
||||
paymentHash: '', // Would need proper parsing
|
||||
description: '',
|
||||
expiresAt: Math.floor(Date.now() / 1000) + 3600,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get exchange rate (via external service or Lightning.Pub)
|
||||
*/
|
||||
async getExchangeRate(currency: string): Promise<ExchangeRate> {
|
||||
// For now, return a mock rate
|
||||
// In production, integrate with price feeds
|
||||
const mockRates: Record<string, number> = {
|
||||
USD: 2500, // sats per dollar (example)
|
||||
EUR: 2700,
|
||||
GBP: 3100,
|
||||
}
|
||||
|
||||
return {
|
||||
currency,
|
||||
satsPerUnit: mockRates[currency] ?? 2500,
|
||||
timestamp: Date.now(),
|
||||
source: 'mock',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start listening for responses
|
||||
*/
|
||||
private startListening(): void {
|
||||
if (!this.nostrClient || !this.identity) return
|
||||
if (this.subscriptionId) return
|
||||
|
||||
this.subscriptionId = this.nostrClient.subscribe(
|
||||
[
|
||||
{
|
||||
kinds: [LIGHTNING_PUB_KINDS.RESPONSE],
|
||||
'#p': [this.identity.publicKey],
|
||||
},
|
||||
],
|
||||
{
|
||||
onEvent: (event) => this.handleResponse(event),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming response events
|
||||
*/
|
||||
private handleResponse(event: Event): void {
|
||||
// Handle async responses if needed
|
||||
// For invoice watching, we use polling instead
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a request to Lightning.Pub
|
||||
*/
|
||||
private async sendRequest<T>(request: Request): Promise<T> {
|
||||
if (!this.nostrClient || !this.identity) {
|
||||
throw new Error('Client not initialized')
|
||||
}
|
||||
|
||||
const content = encryptContent(this.identity, this.config.accountPubkey, request)
|
||||
|
||||
const event = finalizeEvent(
|
||||
{
|
||||
kind: LIGHTNING_PUB_KINDS.REQUEST,
|
||||
content,
|
||||
tags: [['p', this.config.accountPubkey]],
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
pubkey: this.identity.publicKey,
|
||||
},
|
||||
this.identity.privateKey
|
||||
)
|
||||
|
||||
await this.nostrClient.publish(event)
|
||||
|
||||
// Wait for response
|
||||
return this.waitForResponse<T>(event.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a response to a specific request
|
||||
*/
|
||||
private waitForResponse<T>(requestId: string): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.nostrClient || !this.identity) {
|
||||
reject(new Error('Client not initialized'))
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
this.nostrClient!.unsubscribe(subId)
|
||||
reject(new Error('Request timeout'))
|
||||
}, this.config.timeout)
|
||||
|
||||
const subId = this.nostrClient.subscribe(
|
||||
[
|
||||
{
|
||||
kinds: [LIGHTNING_PUB_KINDS.RESPONSE],
|
||||
'#e': [requestId],
|
||||
'#p': [this.identity!.publicKey],
|
||||
},
|
||||
],
|
||||
{
|
||||
onEvent: (event) => {
|
||||
clearTimeout(timeout)
|
||||
this.nostrClient!.unsubscribe(subId)
|
||||
|
||||
try {
|
||||
const response = decryptJSON<Response<T>>(this.identity!, event.pubkey, event.content)
|
||||
|
||||
if (response.error) {
|
||||
reject(new Error(response.error.message))
|
||||
} else {
|
||||
resolve(response.result as T)
|
||||
}
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect and clean up
|
||||
*/
|
||||
disconnect(): void {
|
||||
if (this.subscriptionId && this.nostrClient) {
|
||||
this.nostrClient.unsubscribe(this.subscriptionId)
|
||||
this.subscriptionId = undefined
|
||||
}
|
||||
this.invoiceCallbacks.clear()
|
||||
this.nostrClient = null
|
||||
this.identity = null
|
||||
}
|
||||
}
|
||||
60
lamassu-next/packages/lightning/src/index.ts
Normal file
60
lamassu-next/packages/lightning/src/index.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* @lamassu/lightning
|
||||
*
|
||||
* Lightning.Pub client for Nostr-native Lightning operations.
|
||||
*
|
||||
* Lightning.Pub is an account system that wraps LND and provides
|
||||
* CLINK protocol support. This package handles:
|
||||
* - Invoice generation
|
||||
* - Payment processing
|
||||
* - Balance queries
|
||||
* - Exchange rate fetching
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { LightningPubClient } from '@lamassu/lightning'
|
||||
*
|
||||
* const client = new LightningPubClient({
|
||||
* serviceUrl: 'https://lightning.pub',
|
||||
* accountPubkey: 'npub1...',
|
||||
* relays: ['wss://relay.example.com'],
|
||||
* })
|
||||
*
|
||||
* // Initialize with Nostr client
|
||||
* client.initialize(nostrClient, machineIdentity)
|
||||
*
|
||||
* // Create an invoice
|
||||
* const invoice = await client.createInvoice({
|
||||
* amountMsat: 100000, // 100 sats
|
||||
* description: 'ATM withdrawal',
|
||||
* })
|
||||
*
|
||||
* // Watch for payment
|
||||
* client.watchInvoice(invoice.paymentHash, (paid) => {
|
||||
* console.log('Invoice paid:', paid.preimage)
|
||||
* })
|
||||
*
|
||||
* // Pay an invoice
|
||||
* const result = await client.payInvoice('lnbc...')
|
||||
* if (result.success) {
|
||||
* console.log('Paid with preimage:', result.preimage)
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
// Client
|
||||
export { LightningPubClient } from './client.js'
|
||||
|
||||
// Types
|
||||
export type {
|
||||
LightningPubConfig,
|
||||
AccountInfo,
|
||||
Invoice,
|
||||
InvoiceStatus,
|
||||
PaymentResult,
|
||||
ExchangeRate,
|
||||
ChannelInfo,
|
||||
NodeInfo,
|
||||
InvoiceCallback,
|
||||
PaymentCallback,
|
||||
} from './types.js'
|
||||
113
lamassu-next/packages/lightning/src/types.ts
Normal file
113
lamassu-next/packages/lightning/src/types.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
/**
|
||||
* Lightning.Pub client type definitions
|
||||
*
|
||||
* Lightning.Pub is a Nostr-native account system that wraps LND
|
||||
* and provides CLINK payment support.
|
||||
*/
|
||||
|
||||
/** Lightning.Pub account information */
|
||||
export interface AccountInfo {
|
||||
/** Account public key */
|
||||
pubkey: string
|
||||
/** Account balance in satoshis */
|
||||
balanceSats: number
|
||||
/** Maximum send amount */
|
||||
maxSendSats: number
|
||||
/** Maximum receive amount */
|
||||
maxReceiveSats: number
|
||||
}
|
||||
|
||||
/** Invoice status */
|
||||
export type InvoiceStatus = 'pending' | 'paid' | 'expired' | 'cancelled'
|
||||
|
||||
/** Invoice details */
|
||||
export interface Invoice {
|
||||
/** BOLT11 payment request */
|
||||
paymentRequest: string
|
||||
/** Payment hash */
|
||||
paymentHash: string
|
||||
/** Amount in millisatoshis */
|
||||
amountMsat: number
|
||||
/** Invoice description */
|
||||
description?: string
|
||||
/** Creation timestamp */
|
||||
createdAt: number
|
||||
/** Expiry timestamp */
|
||||
expiresAt: number
|
||||
/** Current status */
|
||||
status: InvoiceStatus
|
||||
/** Payment preimage (if paid) */
|
||||
preimage?: string
|
||||
}
|
||||
|
||||
/** Payment result */
|
||||
export interface PaymentResult {
|
||||
/** Whether payment succeeded */
|
||||
success: boolean
|
||||
/** Payment preimage (proof of payment) */
|
||||
preimage?: string
|
||||
/** Fee paid in millisatoshis */
|
||||
feeMsat?: number
|
||||
/** Error message if failed */
|
||||
error?: string
|
||||
}
|
||||
|
||||
/** Exchange rate information */
|
||||
export interface ExchangeRate {
|
||||
/** Fiat currency code */
|
||||
currency: string
|
||||
/** Satoshis per fiat unit */
|
||||
satsPerUnit: number
|
||||
/** Timestamp of rate */
|
||||
timestamp: number
|
||||
/** Source of rate */
|
||||
source: string
|
||||
}
|
||||
|
||||
/** Lightning.Pub client configuration */
|
||||
export interface LightningPubConfig {
|
||||
/** Lightning.Pub service URL */
|
||||
serviceUrl: string
|
||||
/** Account pubkey on Lightning.Pub */
|
||||
accountPubkey: string
|
||||
/** Nostr relays for communication */
|
||||
relays: string[]
|
||||
/** Request timeout in ms */
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
/** Channel information */
|
||||
export interface ChannelInfo {
|
||||
/** Channel ID */
|
||||
channelId: string
|
||||
/** Remote pubkey */
|
||||
remotePubkey: string
|
||||
/** Local balance in sats */
|
||||
localBalanceSats: number
|
||||
/** Remote balance in sats */
|
||||
remoteBalanceSats: number
|
||||
/** Channel capacity in sats */
|
||||
capacitySats: number
|
||||
/** Whether channel is active */
|
||||
active: boolean
|
||||
}
|
||||
|
||||
/** Node information */
|
||||
export interface NodeInfo {
|
||||
/** Node public key */
|
||||
pubkey: string
|
||||
/** Node alias */
|
||||
alias: string
|
||||
/** Number of channels */
|
||||
numChannels: number
|
||||
/** Total capacity in sats */
|
||||
totalCapacitySats: number
|
||||
/** Synced to chain */
|
||||
syncedToChain: boolean
|
||||
}
|
||||
|
||||
/** Invoice callback for watching payments */
|
||||
export type InvoiceCallback = (invoice: Invoice) => void
|
||||
|
||||
/** Payment status update callback */
|
||||
export type PaymentCallback = (result: PaymentResult) => void
|
||||
22
lamassu-next/packages/lightning/tsconfig.json
Normal file
22
lamassu-next/packages/lightning/tsconfig.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||
}
|
||||
8
lamassu-next/packages/lightning/vitest.config.ts
Normal file
8
lamassu-next/packages/lightning/vitest.config.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ['src/**/*.test.ts'],
|
||||
globals: false,
|
||||
},
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue