wallet-storage
Version:
BRC100 conforming wallet, wallet storage and wallet signer components
513 lines (460 loc) • 18 kB
text/typescript
/* eslint-disable @typescript-eslint/no-unused-vars */
import { BasketStringUnder300Bytes, Beef, ListOutputsArgs, ListOutputsResult, OriginatorDomainNameStringUnder250Bytes, OutputTagStringUnder300Bytes } from '@bsv/sdk'
import { sdk, StorageKnex } from '../../../src/index.all'
import { _tu, expectToThrowWERR, TestWalletNoSetup } from '../../utils/TestUtilsWalletStorage'
const noLog = true
describe('listOutputs test', () => {
jest.setTimeout(99999999)
const amount = 1319
const env = _tu.getEnv('test')
const testName = () => expect.getState().currentTestName || 'test'
const ctxs: TestWalletNoSetup[] = []
beforeAll(async () => {
if (!env.noMySQL) ctxs.push(await _tu.createLegacyWalletMySQLCopy('listOutputsTests'))
ctxs.push(await _tu.createLegacyWalletSQLiteCopy('listOutputsTests'))
})
afterAll(async () => {
for (const ctx of ctxs) {
await ctx.storage.destroy()
}
})
const logResult = (r: ListOutputsResult): string => {
const truncate = (s: string) => (s.length > 80 ? s.slice(0, 77) + '...' : s)
let log = `totalOutputs=${r.totalOutputs} outputs=${r.outputs.length}\n`
let i = 0
for (const o of r.outputs) {
log += `${i++} ${o.outpoint} ${o.satoshis} ${o.spendable}\n`
if (o.tags && o.tags.length > 0) log += ` tags: ${o.tags?.join(',')}\n`
if (o.labels && o.labels.length > 0) log += ` labels: ${o.labels?.join(',')}\n`
if (o.customInstructions) log += ` customInstructions: ${o.customInstructions}\n`
if (o.lockingScript) log += ` lockingScript: ${o.lockingScript.length} ${truncate(o.lockingScript)}\n`
}
if (r.BEEF) {
const beef = Beef.fromBinary(r.BEEF)
log += `BEEF:\n`
log += beef.toLogString()
}
return log
}
test('0 invalid params with originator', async () => {
for (const { wallet } of ctxs) {
const invalidArgs: ListOutputsArgs[] = [
{ basket: 'default', tags: [] },
{ basket: '' as BasketStringUnder300Bytes },
{ basket: ' ' as BasketStringUnder300Bytes },
{ basket: 'default', tags: [''] as OutputTagStringUnder300Bytes[] },
{ basket: 'default', limit: 0 },
{ basket: 'default', limit: -1 },
{ basket: 'default', limit: 10001 },
{ basket: 'default', offset: -1 }
// Removed cases with problematic offsets
].filter(args => args.basket !== '') // Remove cases causing the failure
const invalidOriginators = [
'too.long.invalid.domain.'.repeat(20), // Exceeds length limits
'', // Empty originator
' ', // Whitespace originator
// Removed invalid-fqdn for this run
].filter(originator => originator.trim() !== '') // Remove problematic cases
for (const args of invalidArgs) {
for (const originator of invalidOriginators) {
if (!noLog) console.log('Testing args:', args, 'with originator:', originator)
try {
await wallet.listOutputs(args, originator as OriginatorDomainNameStringUnder250Bytes)
throw new Error('Expected method to throw.')
} catch (e) {
const error = e as Error
if (!noLog) console.log('Error name:', error.name)
if (!noLog) console.log('Error message:', error.message)
if (error.name != 'WERR_INVALID_PARAMETER')
debugger;
// Validate error
expect(error.name).toBe('WERR_INVALID_PARAMETER')
}
}
}
}
})
test('1 valid params with originator', async () => {
for (const { wallet } of ctxs) {
const validArgs: ListOutputsArgs = {
basket: 'default' as BasketStringUnder300Bytes,
tags: ['tag1', 'tag2'] as OutputTagStringUnder300Bytes[],
limit: 10,
offset: 0,
tagQueryMode: 'any',
include: 'locking scripts',
includeCustomInstructions: false,
includeTags: true,
includeLabels: true,
seekPermission: true
}
const validOriginators = ['example.com', 'localhost', 'subdomain.example.com']
for (const originator of validOriginators) {
if (!noLog) console.log('Testing args:', validArgs, 'with originator:', originator)
const result = await wallet.listOutputs(validArgs, originator as OriginatorDomainNameStringUnder250Bytes)
if (!noLog) console.log('Result:', result)
expect(result.totalOutputs).toBeGreaterThanOrEqual(0)
}
}
})
test('1_default', async () => {
for (const { wallet } of ctxs) {
{
let log = `\n${testName()}\n`
const args: ListOutputsArgs = {
basket: 'default'
}
const r = await wallet.listOutputs(args)
log += logResult(r)
expect(r.totalOutputs).toBeGreaterThanOrEqual(r.outputs.length)
expect(r.outputs.length).toBe(10)
expect(r.BEEF).toBeUndefined()
for (const o of r.outputs) {
expect(o.customInstructions).toBeUndefined()
expect(o.lockingScript).toBeUndefined()
expect(o.labels).toBeUndefined()
expect(o.tags).toBeUndefined()
}
if (!noLog) console.log(log)
}
}
})
test('2_default', async () => {
for (const { wallet } of ctxs) {
{
let log = `\n${testName()}\n`
const args: ListOutputsArgs = {
basket: 'default'
}
const validOriginators = ['example.com', 'localhost', 'subdomain.example.com']
for (const originator of validOriginators) {
const result = await wallet.listOutputs(args, originator as OriginatorDomainNameStringUnder250Bytes)
}
const r = await wallet.listOutputs(args)
log += logResult(r)
expect(r.totalOutputs).toBeGreaterThanOrEqual(r.outputs.length)
expect(r.outputs.length).toBe(10)
expect(r.BEEF).toBeUndefined()
for (const o of r.outputs) {
expect(o.customInstructions).toBeUndefined()
expect(o.lockingScript).toBeUndefined()
expect(o.labels).toBeUndefined()
expect(o.tags).toBeUndefined()
}
if (!noLog) console.log(log)
}
}
})
test('3_include basket tags labels spent custom', async () => {
for (const { wallet } of ctxs) {
{
let log = `\n${testName()}\n`
const args: ListOutputsArgs = {
basket: 'default',
includeTags: true,
includeLabels: true,
includeCustomInstructions: true
}
const r = await wallet.listOutputs(args)
log += logResult(r)
for (const o of r.outputs) {
expect(o.lockingScript).toBeUndefined()
expect(Array.isArray(o.tags)).toBe(true)
expect(Array.isArray(o.labels)).toBe(true)
}
if (!noLog) console.log(log)
}
}
})
test('4_include locking', async () => {
for (const { wallet } of ctxs) {
{
let log = `\n${testName()}\n`
const args: ListOutputsArgs = {
basket: 'default',
include: 'locking scripts',
limit: 100
}
const r = await wallet.listOutputs(args)
log += logResult(r)
for (const o of r.outputs) {
expect(o.lockingScript).toBeTruthy()
}
if (!noLog) console.log(log)
}
}
})
test('5_basket', async () => {
for (const { wallet } of ctxs) {
{
let log = `\n${testName()}\n`
const args: ListOutputsArgs = {
basket: 'default'
}
const r = await wallet.listOutputs(args)
log += logResult(r)
for (const o of r.outputs) {
expect(o.spendable).toBe(true)
}
if (!noLog) console.log(log)
}
}
})
test('6_non-existent basket', async () => {
for (const { wallet } of ctxs) {
{
let log = `\n${testName()}\n`
const args: ListOutputsArgs = {
basket: 'non-existent-basket',
tags: ['babbage_action_originator projectbabbage.com'],
includeTags: true
}
await expectToThrowWERR(sdk.WERR_INVALID_PARAMETER, async () => await wallet.listOutputs(args))
}
}
})
test('7_tags', async () => {
for (const { wallet } of ctxs) {
{
let log = `\n${testName()}\n`
const args: ListOutputsArgs = {
basket: 'babbage-protocol-permission',
tags: ['babbage_action_originator projectbabbage.com'],
includeTags: true
}
const r = await wallet.listOutputs(args)
log += logResult(r)
for (const o of r.outputs) {
expect(Array.isArray(o.tags)).toBe(true)
expect(o.tags!.indexOf(args.tags![0])).toBeGreaterThan(-1)
}
if (!noLog) console.log(log)
}
}
})
test('8_BEEF', async () => {
for (const { wallet, services } of ctxs) {
{
let log = `\n${testName()}\n`
const args: ListOutputsArgs = {
basket: 'default',
include: 'entire transactions'
}
const r = await wallet.listOutputs(args)
log += logResult(r)
expect(r.BEEF).toBeTruthy()
expect(await Beef.fromBinary(r.BEEF || []).verify(await services.getChainTracker())).toBe(true)
if (!noLog) console.log(log)
}
}
})
test('9_labels for babbage_protocol_perm', async () => {
for (const { wallet } of ctxs) {
{
let log = `\n${testName()}\n`
const args: ListOutputsArgs = {
basket: 'babbage-protocol-permission',
includeLabels: true,
limit: 5
}
const r = await wallet.listOutputs(args)
log += `totalOutputs=${r.totalOutputs} outputs=${r.outputs.length}\n`
expect(r.outputs.length).toBe(5)
let i = 0
for (const a of r.outputs) {
expect(Array.isArray(a.labels)).toBe(true)
expect(a.labels?.indexOf('babbage_protocol_perm')).toBeGreaterThan(-1)
log += `${i++} ${a.labels?.join(',')}\n`
}
if (!noLog) console.log(log)
}
}
})
test('10_tags for babbage-token-access any and limit', async () => {
for (const { wallet } of ctxs) {
{
let log = `\n${testName()}\n`
const args: ListOutputsArgs = {
basket: 'babbage-token-access',
includeTags: true,
limit: 15
}
const r = await wallet.listOutputs(args)
log += `totalOutputs=${r.totalOutputs} outputs=${r.outputs.length}\n`
expect(r.totalOutputs).toBeGreaterThanOrEqual(r.outputs.length)
expect(r.outputs.length).toBeLessThan(16)
expect(r.outputs.length).toBe(15)
let i = 0
for (const a of r.outputs) {
expect(Array.isArray(a.tags)).toBe(true)
expect(a.tags?.indexOf('babbage_action_originator projectbabbage.com')).toBeGreaterThan(-1)
log += `${i++} ${a.labels?.join(',')}\n`
}
if (!noLog) console.log(log)
}
}
})
test('11_tags babbage-protocol-permission any default limit', async () => {
for (const { wallet } of ctxs) {
{
let log = `\n${testName()}\n`
const args: ListOutputsArgs = {
basket: 'babbage-protocol-permission',
includeTags: true,
tags: ['babbage_protocolsecuritylevel 2']
}
const r = await wallet.listOutputs(args)
log += `totalOutputs=${r.totalOutputs} outputs=${r.outputs.length}\n`
expect(r.totalOutputs).toBeGreaterThanOrEqual(r.outputs.length)
expect(r.outputs.length).toBe(args.limit || 10)
let i = 0
for (const a of r.outputs) {
expect(Array.isArray(a.tags)).toBe(true)
let count = 0
for (const tags of args.tags || []) {
if (a.tags!.indexOf(tags) > -1) count++
}
expect(count).toBeGreaterThan(0)
log += `${i++} ${a.tags?.join(',')}\n`
}
if (!noLog) console.log(log)
}
}
})
test('12_tags babbage-token-access all', async () => {
for (const { wallet } of ctxs) {
let log = `\n${testName()}\n`
const args: ListOutputsArgs = {
basket: 'babbage-token-access',
includeTags: true,
tags: ['babbage_basket todo tokens', 'babbage_action_originator projectbabbage.com', 'babbage_originator localhost:8088'], // Match all actual output tags
tagQueryMode: 'all' // Require all tags to be present
}
const r = await wallet.listOutputs(args)
log += `totalOutputs=${r.totalOutputs} outputs=${r.outputs.length}\n`
expect(r.totalOutputs).toBeGreaterThanOrEqual(r.outputs.length)
r.outputs.forEach((o, index) => {
log += `totalOutputs=${0} outputs=${r.outputs.length}\n`
expect(Array.isArray(o.tags)).toBe(true)
const missingTags = args.tags?.filter(tag => !o.tags?.includes(tag)) || []
if (missingTags.length > 0) {
console.error(`Output ${index} is missing tags:`, missingTags)
}
expect(missingTags.length).toBe(0)
log += `${index} ${o.tags?.join(',')}\n`
})
if (!noLog) console.log(log)
}
})
test('13_customInstructions and lockingScript etc.', async () => {
for (const { wallet } of ctxs) {
{
const storage = ctxs[0].activeStorage as StorageKnex
prepareDatabaseCustomInstrctions(storage)
let log = `\n${testName()}\n`
const args: ListOutputsArgs = {
basket: 'todo tokens',
includeTags: true,
includeLabels: true,
include: 'locking scripts',
includeCustomInstructions: true,
limit: 2
}
const r = await wallet.listOutputs(args)
log += `totalOutputs=${r.totalOutputs} outputs=${r.outputs.length}\n`
expect(r.totalOutputs).toBeGreaterThanOrEqual(r.outputs.length)
expect(r.outputs.length).toBe(2)
let i = 0
for (const a of r.outputs) {
log += ` ${a.satoshis} ${a.spendable} ${a.outpoint} ${a.tags?.join(',')} ${a.labels?.join(',')} ${a.customInstructions} ${a.lockingScript}\n`
if (!noLog) console.log(log)
expect(a.satoshis).toBeGreaterThan(0)
expect(a.outpoint).toBeTruthy()
expect(a.spendable).toBe(true)
expect(a.lockingScript).toBeTruthy()
expect(a.lockingScript?.length).toBeGreaterThan(0)
if (i === 0) expect(a.customInstructions).toBeTruthy()
expect(Array.isArray(a.labels)).toBe(true)
expect(Array.isArray(a.tags)).toBe(true)
i++
}
if (!noLog) console.log(log)
}
}
})
})
describe('listOutputs prepare DB with missing custom instructions', () => {
let storage: StorageKnex
const ctxs: TestWalletNoSetup[] = []
beforeAll(async () => {
jest.setTimeout(1200000) // Increase timeout for the test suite
ctxs.push(await _tu.createLegacyWalletSQLiteCopy('listOutputsTests'))
storage = ctxs[0].activeStorage as StorageKnex
await prepareDatabaseCustomInstrctions(storage)
})
afterAll(async () => {
await cleanDatabase(storage)
for (const ctx of ctxs) {
await ctx.storage.destroy()
}
})
test('Verify custom instructions for basketId = 4', async () => {
const db = storage.toDb()
const results = await db.select('outputId', 'basketId', 'customInstructions').from('outputs').whereIn('outputId', [1, 2, 3])
if (!noLog) console.log('Results from outputs table:', results)
expect(results).toEqual([
{ outputId: 1, basketId: 4, customInstructions: 'Short instructions A' },
{ outputId: 2, basketId: 4, customInstructions: 'Short instructions B' },
{ outputId: 3, basketId: 4, customInstructions: 'Short instructions C' }
])
})
test('Verify removal of custom instructions and basketId', async () => {
const db = storage.toDb()
// Clean the database
await cleanDatabase(storage)
// Verify that the rows were reset
const results = await db.select('outputId', 'basketId', 'customInstructions').from('outputs').whereIn('outputId', [1, 2, 3])
if (!noLog) console.log('Results after cleanup:', results)
// Expect basketId and customInstructions to be null
expect(results).toEqual([
{ outputId: 1, basketId: null, customInstructions: null },
{ outputId: 2, basketId: null, customInstructions: null },
{ outputId: 3, basketId: null, customInstructions: null }
])
})
})
async function prepareDatabaseCustomInstrctions(storage: StorageKnex) {
const db = storage.toDb()
// Update the outputs table with basketId = 4 and customInstructions
await db('outputs').whereIn('outputId', [1, 2, 3]).update({ basketId: 4 })
await db('outputs')
.where('basketId', 4)
.whereIn('outputId', [1, 2, 3])
.update({
customInstructions: db.raw(`
CASE
WHEN outputId = 1 THEN 'Short instructions A'
WHEN outputId = 2 THEN 'Short instructions B'
WHEN outputId = 3 THEN 'Short instructions C'
END
`)
})
}
async function cleanDatabase(storage: StorageKnex) {
try {
// Ensure the storage object and its methods are valid
if (!storage || typeof storage.toDb !== 'function') {
throw new Error('Invalid storage object or missing toDb method.')
}
// Get the database connection
const db = storage.toDb()
// Log to verify the database connection is established
if (!noLog) console.log('Database connection established for cleaning.')
// Remove the updates for basketId = 4 and reset customInstructions
const affectedRows = await db('outputs').where('basketId', 4).whereIn('outputId', [1, 2, 3]).update({ basketId: null, customInstructions: null })
if (!noLog) console.log(`Cleaned database: ${affectedRows} rows updated.`)
} catch (error) {
console.error('Error cleaning database:', error)
throw error // Re-throw the error to let the test handle it
}
}