@bsv/wallet-toolbox
Version:
BRC100 conforming wallet, wallet storage and wallet signer components
966 lines (959 loc) • 63 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.isEmptyObject = void 0;
exports.toLogString = toLogString;
exports.createActionResultToTxLogString = createActionResultToTxLogString;
exports.txToLogString = txToLogString;
exports.numberArrayToHexString = numberArrayToHexString;
const chalk_1 = __importDefault(require("chalk"));
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const sdk_1 = require("@bsv/sdk");
const TestUtilsWalletStorage_1 = require("../../utils/TestUtilsWalletStorage");
const noLog = true;
const logFilePath = path_1.default.resolve(__dirname, 'createAction2.test.ts');
function sanitizeTestName(testName) {
const cleanTestName = testName.replace(/[^a-zA-Z0-9_]/g, '_');
return cleanTestName.startsWith('LOG_') ? cleanTestName : `LOG_${cleanTestName}`;
}
describe('createAction2 nosend transactions', () => {
jest.setTimeout(99999999);
let ctxs = [];
const env = TestUtilsWalletStorage_1._tu.getEnv('test');
const testName = () => { var _a; return (_a = expect.getState().currentTestName) !== null && _a !== void 0 ? _a : 'test'; };
beforeEach(async () => {
ctxs = [];
if (env.runMySQL) {
ctxs.push(await TestUtilsWalletStorage_1._tu.createLegacyWalletMySQLCopy(testName()));
}
ctxs.push(await TestUtilsWalletStorage_1._tu.createLegacyWalletSQLiteCopy(testName()));
});
afterEach(async () => {
for (const { wallet } of ctxs)
await wallet.destroy();
});
test('1_transaction with single output checked using toLogString', async () => {
for (const { wallet } of ctxs) {
wallet.randomVals = [0.1, 0.2, 0.3, 0.7, 0.8, 0.9];
const fundingLabel = 'funding transaction for createAction';
const fundingArgs = {
outputs: [
{
basket: 'funding basket',
tags: ['funding transaction output', 'test tag'],
satoshis: 3,
lockingScript: '76a914abcdef0123456789abcdef0123456789abcdef88ac',
outputDescription: 'Funding Output'
}
],
labels: [
fundingLabel,
'this is an extra long test label that should be truncated at 80 chars when it is displayed'
],
description: 'Funding transaction',
options: { noSend: true, randomizeOutputs: false }
};
const fundingResult = await wallet.createAction(fundingArgs);
expect(fundingResult.tx).toBeDefined();
const actionsResult = await wallet.listActions({
labels: [fundingLabel],
includeInputs: true,
includeOutputs: true,
includeInputSourceLockingScripts: true,
includeInputUnlockingScripts: true,
includeOutputLockingScripts: true,
includeLabels: true
});
const rl1 = toLogString(fundingResult.tx, actionsResult);
expect(rl1.log).toBe(`transactions:3
txid:30bdac0f5c6491f130820517802ff57e20e5a50c08b5c65e6976627fb82ae930 version:1 lockTime:0 sats:-4 status:nosend
outgoing:true desc:'Funding transaction' labels:['funding transaction for createaction','this is an extra long test
label that should be truncated at 80 chars when it is...']
inputs: 1
0: sourceTXID:a3a8fe7f541c1383ff7b975af49b27284ae720af5f2705d8409baaf519190d26.2 sats:913
lock:(50)76a914f7238871139f4926cbd592a03a737981e558245d88ac
unlock:(214)483045022100cfef1f6d781af99a1de14efd6f24f2a14234a26097012f27121eb36f4e330c1d0220... seq:4294967295
outputs: 2
0: sats:3 lock:(48)76a914abcdef0123456789abcdef0123456789abcdef88ac index:0 spendable:true basket:'funding basket'
desc:'Funding Output' tags:['funding transaction output','test tag']
1: sats:909 lock:(50)76a9145947e66cdd43c70fb1780116b79e6f7d96e30e0888ac index:1 spendable:true basket:'default'`);
}
});
test('2_transaction with multiple outputs checked using toLogString', async () => {
for (const { wallet } of ctxs) {
wallet.randomVals = [0.1, 0.2, 0.3, 0.7, 0.8, 0.9];
const fundingLabel = 'funding transaction for createAction';
const fundingArgs = {
outputs: [
{
basket: 'funding basket',
tags: ['funding transaction for createAction', 'test tag'],
satoshis: 5,
lockingScript: '76a914abcdef0123456789abcdef0123456789abcdef88ac',
outputDescription: 'Funding output'
},
{
basket: 'extra basket',
tags: ['extra transaction output', 'extra test tag'],
satoshis: 6,
lockingScript: '76a914fedcba9876543210fedcba9876543210fedcba88ac',
outputDescription: 'Extra Output'
}
],
labels: [fundingLabel, 'this is the extra label'],
description: 'Funding transaction with multiple outputs',
options: { noSend: true, randomizeOutputs: false }
};
const fundingResult = await wallet.createAction(fundingArgs);
const actionsResult = await wallet.listActions({
labels: [fundingLabel],
includeInputs: true,
includeOutputs: true,
includeInputSourceLockingScripts: true,
includeInputUnlockingScripts: true,
includeOutputLockingScripts: true,
includeLabels: true
});
const rl1 = toLogString(fundingResult.tx, actionsResult);
expect(rl1.log).toBe(`transactions:3
txid:b3848f2cabf5887ec679ca60347a29f6ecad425fda738700265c2f9d22c18ab5 version:1 lockTime:0 sats:-12 status:nosend
outgoing:true desc:'Funding transaction with multiple outputs' labels:['funding transaction for createaction','this
is the extra label']
inputs: 1
0: sourceTXID:a3a8fe7f541c1383ff7b975af49b27284ae720af5f2705d8409baaf519190d26.2 sats:913
lock:(50)76a914f7238871139f4926cbd592a03a737981e558245d88ac
unlock:(212)473044022079020cc8ea5ee6b3610806286e41567147d4b4b07d16bc1341311e00ce7647b0022034... seq:4294967295
outputs: 3
0: sats:5 lock:(48)76a914abcdef0123456789abcdef0123456789abcdef88ac index:0 spendable:true basket:'funding basket'
desc:'Funding output' tags:['funding transaction for createaction','test tag']
1: sats:6 lock:(48)76a914fedcba9876543210fedcba9876543210fedcba88ac index:1 spendable:true basket:'extra basket'
desc:'Extra Output' tags:['extra transaction output','extra test tag']
2: sats:901 lock:(50)76a9145947e66cdd43c70fb1780116b79e6f7d96e30e0888ac index:2 spendable:true basket:'default'`);
}
});
test('3_transaction with explicit change check also uses toLogString on the spend', async () => {
var _a, _b, _c;
for (const { wallet } of ctxs) {
wallet.randomVals = [0.1, 0.2, 0.3, 0.7, 0.8, 0.9];
const fundingArgs = {
outputs: [
{
satoshis: 4,
lockingScript: '76a914abcdef0123456789abcdef0123456789abcdef88ac',
outputDescription: 'Funding output'
}
],
description: 'Funding transaction',
options: { noSend: true, randomizeOutputs: false }
};
const fundingResult = await wallet.createAction(fundingArgs);
expect(fundingResult.tx).toBeDefined();
expect(fundingResult.noSendChange).toBeDefined();
expect(fundingResult.noSendChange.length).toBe(1);
log(`noSendChange returned:${JSON.stringify(fundingResult.noSendChange, null, 2)}`);
const outputSatoshis = 2;
const estimatedFee = 1;
const fundingBeef = sdk_1.Beef.fromBinary(fundingResult.tx);
expect(fundingBeef).toBeDefined();
const spendingArgs = {
inputs: [
{
outpoint: `${fundingResult.txid}.0`,
unlockingScript: '47304402207f2e9a',
inputDescription: 'desc3'
}
],
inputBEEF: fundingBeef.toBinary(),
outputs: [
{
satoshis: outputSatoshis,
lockingScript: '76a914abcdef0123456789abcdef0123456789abcdef88ac',
outputDescription: 'First spending Output for check on change '
}
],
labels: ['spending transaction test'],
description: 'Explicit check on returned change',
options: {
knownTxids: [fundingResult.txid],
noSend: true,
randomizeOutputs: false,
noSendChange: []
}
};
const spendingResult = await wallet.createAction(spendingArgs);
expect(spendingResult.tx).toBeDefined();
log(`Spending transaction created:${JSON.stringify(spendingResult, null, 2)}`);
const spendingActionsResult = await wallet.listActions({
labels: ['spending transaction test'],
includeInputs: true,
includeOutputs: true,
includeInputSourceLockingScripts: true,
includeInputUnlockingScripts: true,
includeOutputLockingScripts: true,
includeLabels: true
});
const totalInputSatoshis = (_b = (_a = spendingActionsResult.actions[0]) === null || _a === void 0 ? void 0 : _a.inputs) === null || _b === void 0 ? void 0 : _b.reduce((sum, input) => sum + input.sourceSatoshis, 0);
const expectedChange = totalInputSatoshis - outputSatoshis - estimatedFee;
const outputs = ((_c = spendingActionsResult.actions[0]) === null || _c === void 0 ? void 0 : _c.outputs) || [];
const changeOutput = outputs.find(output => output.basket === 'default');
expect(changeOutput.satoshis).toBe(expectedChange);
const actualFee = totalInputSatoshis - outputSatoshis - expectedChange;
expect(actualFee).toBe(estimatedFee);
const rl1 = toLogString(spendingResult.tx, spendingActionsResult);
expect(rl1.log).toBe(`transactions:5
txid:afa6713aab0957cf5bb00dee532ad7b895e919a99564ec2016b51cb3d472d87f version:1 lockTime:0 sats:1 status:nosend
outgoing:true desc:'Explicit check on returned change' labels:['spending transaction test']
inputs: 2
0: sourceTXID:527ffe88f70d5b7de2b8b5ba9966b9c755e7da4de749d4fcd27140a03145a11d.0 sats:995
lock:(50)76a914ab2b66432503a3681fc5af1502207ca458c8752d88ac
unlock:(214)483045022100973a84555fa864e08313bda5c88e1991094db7b8d82586c899276155dabcbc9a0220... seq:4294967295
1: sourceTXID:70afdc54187a1cdb8e35f7d00e5e111cbf5c43c4dc3f1da2cc44479133c75f9e.0 sats:4 desc:'Funding output'
lock:(48)76a914abcdef0123456789abcdef0123456789abcdef88ac unlock:(16)47304402207f2e9a seq:4294967295
outputs: 2
0: sats:2 lock:(48)76a914abcdef0123456789abcdef0123456789abcdef88ac index:0 spendable:true desc:'First spending
Output for check on change '
1: sats:996 lock:(50)76a9145947e66cdd43c70fb1780116b79e6f7d96e30e0888ac index:1 spendable:true basket:'default'`);
}
});
test('4_transaction with custom options knownTxids and returnTXIDOnly false uses toLogString', async () => {
for (const { wallet } of ctxs) {
wallet.randomVals = [0.1, 0.2, 0.3, 0.7, 0.8, 0.9];
const fundingOutputSatoshis = 4;
const fundingArgs = {
outputs: [
{
satoshis: fundingOutputSatoshis,
lockingScript: '76a914abcdef0123456789abcdef0123456789abcdef88ac',
outputDescription: 'Funding output'
}
],
description: 'Funding transaction',
options: { noSend: true, randomizeOutputs: false }
};
const fundingResult = await wallet.createAction(fundingArgs);
expect(fundingResult.tx).toBeDefined();
const spendingArgs = {
description: 'Check knownTxids and returnTXIDOnly',
outputs: [
{
satoshis: 4,
lockingScript: '76a914abcdef0123456789abcdef0123456789abcdef88ac',
outputDescription: 'returnTXIDOnly false test'
}
],
labels: ['custom options test'],
options: {
knownTxids: ['tx123', 'tx456'],
returnTXIDOnly: false,
noSend: true,
randomizeOutputs: false
}
};
const spendingResult = await wallet.createAction(spendingArgs);
expect(spendingArgs.options.knownTxids).toEqual(expect.arrayContaining(['tx123', 'tx456']));
const spendingActionsResult = await wallet.listActions({
labels: ['custom options test'],
includeInputs: true,
includeOutputs: true,
includeInputSourceLockingScripts: true,
includeInputUnlockingScripts: true,
includeOutputLockingScripts: true,
includeLabels: true
});
const rl1 = toLogString(spendingResult.tx, spendingActionsResult);
expect(rl1.log).toBe(`transactions:2
txid:38ded69627603b30bd1f55eb3f88098dbf74f2ef0ff5e3cfe6a34f97ce2db9c2 version:1 lockTime:0 sats:-5 status:nosend
outgoing:true desc:'Check knownTxids and returnTXIDOnly' labels:['custom options test']
inputs: 1
0: sourceTXID:527ffe88f70d5b7de2b8b5ba9966b9c755e7da4de749d4fcd27140a03145a11d.0 sats:995
lock:(50)76a914ab2b66432503a3681fc5af1502207ca458c8752d88ac
unlock:(212)4730440220113a6f72035a6ddcd6930db7e3f3d5c70486f9aaefb095e6fa3557afa916ec37022054... seq:4294967295
outputs: 2
0: sats:4 lock:(48)76a914abcdef0123456789abcdef0123456789abcdef88ac index:0 spendable:true desc:'returnTXIDOnly
false test'
1: sats:990 lock:(50)76a9145947e66cdd43c70fb1780116b79e6f7d96e30e0888ac index:1 spendable:true basket:'default'`);
}
});
test('5_transaction with custom options knownTxids and returnTXIDOnly true', async () => {
for (const { wallet } of ctxs) {
wallet.randomVals = [0.1, 0.2, 0.3, 0.7, 0.8, 0.9];
const fundingOutputSatoshis = 4;
const fundingArgs = {
outputs: [
{
satoshis: fundingOutputSatoshis,
lockingScript: '76a914abcdef0123456789abcdef0123456789abcdef88ac',
outputDescription: 'Funding output'
}
],
description: 'Funding transaction',
options: { noSend: true }
};
const fundingResult = await wallet.createAction(fundingArgs);
expect(fundingResult.tx).toBeDefined();
const spendingArgs = {
description: 'Check knownTxids and returnTXIDOnly',
outputs: [
{
satoshis: 4,
lockingScript: '76a914abcdef0123456789abcdef0123456789abcdef88ac',
outputDescription: 'returnTXIDOnly true test'
}
],
labels: ['custom options test'],
options: {
knownTxids: ['tx123', 'tx456'],
returnTXIDOnly: true,
noSend: true
}
};
const spendingResult = await wallet.createAction(spendingArgs);
expect(spendingResult.tx).not.toBeDefined();
expect(spendingArgs.options.knownTxids).toEqual(expect.arrayContaining(['tx123', 'tx456']));
}
});
test('6_transaction with custom options knownTxids check returned BeefParty txids', async () => {
var _a, _b;
for (const { wallet } of ctxs) {
wallet.autoKnownTxids = true;
wallet.randomVals = [0.1, 0.2, 0.3, 0.7, 0.8, 0.9];
const fundingOutputSatoshis = 4;
const fundingArgs = {
outputs: [
{
satoshis: fundingOutputSatoshis,
lockingScript: '76a914abcdef0123456789abcdef0123456789abcdef88ac',
outputDescription: 'Funding Output'
}
],
description: 'Funding transaction',
options: { noSend: true }
};
const fundingResult = await wallet.createAction(fundingArgs);
expect(fundingResult.tx).toBeDefined();
const spendingArgs = {
description: 'Check knownTxids txids',
outputs: [
{
satoshis: 4,
lockingScript: '76a914abcdef0123456789abcdef0123456789abcdef88ac',
outputDescription: 'Output for check txids'
}
],
labels: ['custom options test'],
options: {
knownTxids: ['tx123', 'tx456'],
returnTXIDOnly: true,
noSend: true
}
};
const spendingResult = await wallet.createAction(spendingArgs);
expect(spendingResult).toBeDefined();
expect(spendingArgs.options.knownTxids).toEqual(expect.arrayContaining(['tx123', 'tx456']));
const fundingBeef = sdk_1.Beef.fromBinary(fundingResult.tx);
expect(fundingBeef).toBeDefined();
const BeefPartyTxids = fundingBeef.txs.map(tx => tx.txid);
const expectedTxids = ['tx123', 'tx456'];
if ((_a = spendingArgs.options) === null || _a === void 0 ? void 0 : _a.knownTxids) {
expect((_b = spendingArgs.options.knownTxids) === null || _b === void 0 ? void 0 : _b.sort()).toEqual(expectedTxids.sort());
}
}
});
test('7_transaction with custom options knownTxids check returned BeefParty txids with additional spend', async () => {
var _a, _b;
for (const { wallet } of ctxs) {
wallet.autoKnownTxids = true;
wallet.randomVals = [0.1, 0.2, 0.3, 0.7, 0.8, 0.9];
const fundingOutputSatoshis = 4;
const fundingArgs = {
outputs: [
{
satoshis: fundingOutputSatoshis,
lockingScript: '76a914abcdef0123456789abcdef0123456789abcdef88ac',
outputDescription: 'Funding Output'
}
],
description: 'Funding transaction',
options: { noSend: true }
};
const fundingResult = await wallet.createAction(fundingArgs);
expect(fundingResult.tx).toBeDefined();
const spendingArgs = {
description: 'Check knownTxids txids extra',
outputs: [
{
satoshis: 4,
lockingScript: '76a914abcdef0123456789abcdef0123456789abcdef88ac',
outputDescription: 'Output for check txids extra'
}
],
options: {
knownTxids: ['tx123', 'tx456'],
returnTXIDOnly: false,
noSend: true
}
};
const spendingResult = await wallet.createAction(spendingArgs);
expect(spendingResult).toBeDefined();
expect(spendingArgs.options.knownTxids).toEqual(expect.arrayContaining(['tx123', 'tx456']));
const fundingBeef = sdk_1.Beef.fromBinary(fundingResult.tx);
expect(fundingBeef).toBeDefined();
const partyBeefTxids = fundingBeef.txs.map(tx => tx.txid);
const expectedTxids = ['tx123', 'tx456'];
expect((_a = spendingArgs.options.knownTxids) === null || _a === void 0 ? void 0 : _a.sort()).toEqual(expectedTxids.sort());
const additionalSpendArgs = {
description: 'Extra spend transaction',
outputs: [
{
satoshis: 4,
lockingScript: '76a914abcdef0123456789abcdef0123456789abcdef88ac',
outputDescription: 'Extra spend output'
}
],
labels: ['extra spend test'],
options: {
knownTxids: spendingArgs.options.knownTxids,
returnTXIDOnly: true,
noSend: true
}
};
const additionalSpendResult = await wallet.createAction(additionalSpendArgs);
expect(additionalSpendResult).toBeDefined();
const finalBeef = sdk_1.Beef.fromBinary(spendingResult.tx);
expect(finalBeef).toBeDefined();
const finalPartyBeefTxids = finalBeef.txs.map(tx => tx.txid);
const finalExpectedTxids = [...expectedTxids];
expect((_b = additionalSpendArgs.options.knownTxids) === null || _b === void 0 ? void 0 : _b.sort()).toEqual(finalExpectedTxids.sort());
}
});
/* WIP
test('8_no-send transaction with zero satoshis output', async () => {
for (const { wallet, activeStorage: storage } of ctxs) {
const args: CreateActionArgs = {
outputs: [
{
satoshis: 0,
lockingScript: '76a914abcdef0123456789abcdef0123456789abcdef88ac',
outputDescription: 'Invalid output'
}
],
description: 'Valid transaction',
options: {
returnTXIDOnly: false,
randomizeOutputs: false,
noSend: true
}
}
const result: CreateActionResult = await wallet.createAction(args)
expect(result.tx).toBeDefined()
expect(result.signableTransaction).toBeUndefined()
}
})
test('9_no-send transaction without auth (should fail)', async () => {
for (const { wallet, activeStorage: storage } of ctxs) {
const args: CreateActionArgs = {
outputs: [
{
satoshis: 5,
lockingScript: '76a914abcdef0123456789abcdef0123456789abcdef88ac',
outputDescription: 'Valid output'
}
],
description: 'Valid transaction',
options: {
returnTXIDOnly: false,
randomizeOutputs: false,
noSend: true
}
}
await expect(wallet.createAction(args, undefined)).rejects.toThrow()
}
})
test('10_no-send transaction with malformed args (invalid destination)', async () => {
for (const { wallet, activeStorage: storage } of ctxs) {
const args: CreateActionArgs = {
outputs: [
{
satoshis: 6,
lockingScript: 'invalid_script',
outputDescription: 'Valid output'
}
],
description: 'Valid transaction',
options: {
noSend: true
}
}
await expect(wallet.createAction(args)).rejects.toThrow()
}
})
test('11_transaction with OP_RETURN', async () => {
for (const { wallet, activeStorage: storage } of ctxs) {
const args: CreateActionArgs = {
outputs: [
{
satoshis: 0,
lockingScript: '6a0c48656c6c6f20576f726c64',
outputDescription: 'OP_RETURN data'
}
],
description: 'Transaction embedding OP_RETURN data',
options: { noSend: true }
}
const result: CreateActionResult = await wallet.createAction(args)
expect(result.tx).toBeDefined()
expect(result.signableTransaction).toBeUndefined()
}
})
test('12_high fee transaction', async () => {
for (const { wallet, activeStorage: storage } of ctxs) {
const args: CreateActionArgs = {
inputs: [
{
outpoint: 'tx4.0',
unlockingScript: '47304402207f2e9a',
inputDescription: 'desc4'
}
],
outputs: [
{
satoshis: 950,
lockingScript: '76a914abcdef0123456789abcdef0123456789abcdef88ac',
outputDescription: 'Output D'
}
],
description:
'Transaction that results in high fees (insufficient change)',
options: { noSend: true }
}
await expect(wallet.createAction(args)).rejects.toThrow(
/WERR_INSUFFICIENT_FUNDS/
)
}
})
test('13_zero fee transaction', async () => {
for (const { wallet, activeStorage: storage } of ctxs) {
const args: CreateActionArgs = {
inputs: [
{
outpoint: 'tx5.0',
unlockingScript: '47304402207f2e9a',
inputDescription: 'desc5'
}
],
outputs: [
{
satoshis: 500,
lockingScript: '76a914abcdef0123456789abcdef0123456789abcdef88ac',
outputDescription: 'Output E'
}
],
description: 'Zero-fee transaction attempt',
options: { noSend: true }
}
await expect(wallet.createAction(args)).rejects.toThrow(
/WERR_INSUFFICIENT_FUNDS/
)
}
})
test('14_dust transaction', async () => {
for (const { wallet, activeStorage: storage } of ctxs) {
const args: CreateActionArgs = {
outputs: [
{
satoshis: 1,
lockingScript: '76a914abcdef0123456789abcdef0123456789abcdef88ac',
outputDescription: 'Dust output'
}
],
description: 'Transaction with dust output',
options: { noSend: true }
}
await expect(wallet.createAction(args)).rejects.toThrow(
/WERR_INVALID_PARAMETER/
)
}
})
*/
});
// Helper functions
function getExpectedLog(testName, logFilePath) {
if (!fs_1.default.existsSync(logFilePath)) {
return null;
}
const fileContent = fs_1.default.readFileSync(logFilePath, 'utf8');
const sanitizedTestName = sanitizeTestName(testName);
// Use regex to extract the correct log constant
const logRegex = new RegExp(`const\\s+${sanitizedTestName}\\s*=\\s*\\{\\s*log:\\s*['\`]([\\s\\S]*?)['\`]\\s*,\\s*logColor:\\s*['\`]([\\s\\S]*?)['\`]\\s*\\}`, 'm');
const match = fileContent.match(logRegex);
if (match) {
return { log: match[1], logColor: match[2] };
}
return null;
}
const normalizeVariableParts = (log) => {
return log
.replace(/txid:[a-f0-9]{64}/g, 'txid:PLACEHOLDER') // Replace txids
.replace(/unlock:\(\d+\)(?:483045022100[a-f0-9]{64}0220|[a-f0-9]+)/g, 'unlock:PLACEHOLDER')
.replace(/lock:\(\d+\)76a914[a-f0-9]{40}/g, 'lock:PLACEHOLDER') // Replace locking script
.replace(/index:\d+ spendable:/g, 'index:PLACEHOLDER spendable:') // Normalize index
.trim();
};
/**
* Appends logs as a constant to a test file.
* @param {string} testName - The name of the test.
* @param {{ log: string; logColor: string }} rl - The log data.
*/
function appendLogsAsConst(testName, rl) {
const normalizedTestName = testName
.replace(/[^a-zA-Z0-9_ ]/g, '')
.trim()
.replace(/\s+/g, '_');
const sanitizedTestName = sanitizeTestName(normalizedTestName);
const logFilePath = path_1.default.resolve(__dirname, 'createAction2.man.test.ts');
const logConst = `
// Auto-generated test log - ${new Date().toISOString()}
const ${sanitizedTestName} = {
log: \`${rl.log}\`,
logColor: \`${rl.logColor}\`
};
`.trim();
fs_1.default.appendFileSync(logFilePath, `\n${logConst}\n`, 'utf8');
}
/**
* Truncates a string to a maximum length of 80 characters.
* @param {string} s - The string to truncate.
* @returns {string} - The truncated string.
*/ const truncate = (s) => (s.length > 80 ? s.slice(0, 80) + '...' : s);
/**
* Formats an optional field if it has a defined value.
* @param {string} fieldName - The name of the field.
* @param {any} value - The value of the field.
* @returns {string} - The formatted field string.
*/
const formatOptionalField = (fieldName, value) => value !== undefined && value !== null && value !== '' ? ` ${fieldName}:${value}` : '';
/**
* Formats an optional field with quotes if it has a defined value.
* @param {string} fieldName - The name of the field.
* @param {any} value - The value of the field.
* @returns {string} - The formatted field string with quotes.
*/
const formatOptionalFieldWithQuotes = (fieldName, value) => value !== undefined && value !== null && value !== '' ? ` ${fieldName}:'${value}'` : '';
/**
* Formats an optional field with color if it has a defined value.
* @param {string} fieldName - The name of the field.
* @param {any} value - The value of the field.
* @param {(val: string) => string} colorFunc - The function to apply color formatting.
* @returns {string} - The formatted field string with color.
*/
const formatOptionalFieldWithColor = (fieldName, value, colorFunc) => value !== undefined && value !== null && value !== ''
? ` ${chalk_1.default.gray(fieldName + ':')}${colorFunc(typeof value === 'string' ? value : String(value))}`
: '';
/**
* Formats metadata if present.
* @param {any} metadata - The metadata object.
* @returns {string} - The formatted metadata string.
*/
const formatMetadata = (metadata) => metadata && !(0, exports.isEmptyObject)(metadata) ? `metadata:${JSON.stringify(metadata)}` : '';
/**
* Formats the Merkle path if present.
* @param {MerklePath | string} [merklePath] - The Merkle path.
* @returns {string} - The formatted Merkle path string.
*/
const formatMerklePath = (merklePath) => (merklePath ? `merklePath:${String(merklePath)}` : '');
const MAX_LOG_LINE_LENGTH = 120; // Define in the test
/**
* Wraps a log line to a specified max length.
* @param {string} text - The text to wrap.
* @param {number} indent - The indentation level.
* @param {number} [maxLength=120] - The maximum length of a line.
* @returns {string} - The wrapped log line.
*/
const wrapLogLine = (text, indent, maxLength = 120) => {
const words = text.trim().split(' ');
let wrappedText = ' '.repeat(indent);
let currentLineLength = indent * 2;
for (const word of words) {
if (currentLineLength + word.length + 1 > maxLength) {
wrappedText += '\n' + ' '.repeat(indent) + ' ' + word + ' ';
currentLineLength = indent * 2 + word.length + 1;
}
else {
wrappedText += word + ' ';
currentLineLength += word.length + 1;
}
}
return wrappedText.trimEnd();
};
/**
* Formats an indented line.
* @param {number} indent - The indentation level.
* @param {string} content - The content of the line.
* @returns {string} - The formatted indented line.
*/
const formatIndentedLineWithWrap = (indent, content, maxLength = 120) => wrapLogLine(content.trim(), indent, maxLength);
/**
* Formats a list of wallet action inputs for logging.
* @param {WalletActionInput[]} [inputs] - The list of wallet action inputs.
* @returns {{ log: string; logColor: string }[]} - An array of formatted log strings and their colorized versions.
*/
const formatInputs = (inputs) => inputs && inputs.length > 0
? inputs
.sort((a, b) => a.sourceOutpoint.localeCompare(b.sourceOutpoint))
.map((input, i) => {
let line = `${i}: sourceTXID:${input.sourceOutpoint} sats:${input.sourceSatoshis}`;
let color = `${chalk_1.default.gray(`${i}:`)} ${chalk_1.default.blue(input.sourceOutpoint)} ${chalk_1.default.green(`${input.sourceSatoshis} sats`)}`;
line += formatOptionalFieldWithQuotes('desc', input.inputDescription);
color += formatOptionalFieldWithColor('desc', input.inputDescription, chalk_1.default.white);
if (input.sourceLockingScript) {
line += ` lock:(${input.sourceLockingScript.length})${truncate(input.sourceLockingScript)}`;
color += ` ${chalk_1.default.gray('lock:')}(${input.sourceLockingScript.length})${chalk_1.default.cyan(truncate(input.sourceLockingScript))}`;
}
if (input.unlockingScript) {
line += ` unlock:(${input.unlockingScript.length})${truncate(input.unlockingScript)}`;
color += ` ${chalk_1.default.gray('unlock:')}(${input.unlockingScript.length})${chalk_1.default.cyan(truncate(input.unlockingScript))}`;
}
line += ` seq:${input.sequenceNumber}`;
color += ` ${chalk_1.default.gray('seq:')}${input.sequenceNumber}`;
return {
log: formatIndentedLineWithWrap(2, line),
logColor: formatIndentedLineWithWrap(2, color)
};
})
: [
{
log: formatIndentedLineWithWrap(2, 'No inputs'),
logColor: formatIndentedLineWithWrap(2, chalk_1.default.gray('No inputs'))
}
];
/**
* Formats a list of wallet action outputs for logging.
* @param {WalletActionOutput[]} [outputs] - The list of wallet action outputs.
* @returns {{ log: string; logColor: string }[]} - An array of formatted log strings and their colorized versions.
*/
const formatOutputs = (outputs) => outputs && outputs.length > 0
? outputs
.sort((a, b) => a.satoshis - b.satoshis)
.map((output, i) => {
var _a, _b, _c, _d, _e;
let line = `${i}: sats:${output.satoshis} lock:(${((_a = output.lockingScript) === null || _a === void 0 ? void 0 : _a.length) || ''})${(_b = truncate(output.lockingScript)) !== null && _b !== void 0 ? _b : 'N/A'}`;
let color = `${chalk_1.default.gray(`${i}:`)} ${chalk_1.default.green(`${output.satoshis} sats`)} ${chalk_1.default.gray('lock:')}(${((_c = output.lockingScript) === null || _c === void 0 ? void 0 : _c.length) || ''})${chalk_1.default.cyan((_d = truncate(output.lockingScript)) !== null && _d !== void 0 ? _d : 'N/A')}`;
line += formatOptionalField('index', output.outputIndex);
color += formatOptionalFieldWithColor('index', output.outputIndex, chalk_1.default.white);
line += formatOptionalField('spendable', output.spendable);
color += formatOptionalFieldWithColor('spendable', output.spendable, chalk_1.default.white);
line += formatOptionalFieldWithQuotes('custinst', output.customInstructions);
color += formatOptionalFieldWithColor('custinst', output.customInstructions, chalk_1.default.white);
line += formatOptionalFieldWithQuotes('basket', output.basket);
color += formatOptionalFieldWithColor('basket', output.basket, chalk_1.default.white);
line += formatOptionalFieldWithQuotes('desc', output.outputDescription);
color += formatOptionalFieldWithColor('desc', output.outputDescription, chalk_1.default.white);
if ((_e = output.tags) === null || _e === void 0 ? void 0 : _e.length) {
const tagsString = `[${output.tags.map(tag => `'${truncate(tag)}'`).join(',')}]`;
line += ` tags:${tagsString}`;
color += ` ${chalk_1.default.gray('tags:')}${chalk_1.default.white(tagsString)}`;
}
return {
log: formatIndentedLineWithWrap(2, line),
logColor: formatIndentedLineWithWrap(2, color)
};
})
: [
{
log: formatIndentedLineWithWrap(2, 'No outputs'),
logColor: formatIndentedLineWithWrap(2, chalk_1.default.gray('No outputs'))
}
];
/**
* Formats a list of labels into a string representation.
* @param {string[]} [labels] - The list of labels.
* @returns {string} - A formatted string of labels enclosed in brackets.
*/
const formatLabels = (labels) => labels && labels.length > 0 ? `[${labels.map(label => `'${truncate(label)}'`).join(',')}]` : '';
/**
* Generates a formatted log string from an AtomicBEEF object.
* @param {AtomicBEEF} atomicBeef - The AtomicBEEF object containing transaction data.
* @param {ListActionsResult} [actionsResult] - The result of listing actions, used for additional transaction metadata.
* @param {boolean} [showKey=true] - Whether to display key transaction details.
* @returns {Promise<{ log: string; logColor: string }>} - An object containing the formatted log string and a colorized version.
*/
function toLogString(atomicBeef, actionsResult, showKey = true) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
const BEEF_V1 = 4022206465;
try {
const beef = sdk_1.Beef.fromBinary(atomicBeef);
beef.version = BEEF_V1;
let log = `transactions:${beef.txs.length}`;
let logColor = chalk_1.default.gray(`transactions:${beef.txs.length}`);
if (showKey) {
logColor += ` ${chalk_1.default.gray(`key:`)} (${chalk_1.default.blue('txid/outpoint')} ${chalk_1.default.cyan('script')} ${chalk_1.default.green('sats')})`;
}
const mainTxid = beef.txs.slice(-1)[0].txid;
const mainTx = beef.findAtomicTransaction(mainTxid);
const action = actionsResult === null || actionsResult === void 0 ? void 0 : actionsResult.actions.find(a => a.txid === mainTxid);
const labelString = formatLabels(action === null || action === void 0 ? void 0 : action.labels);
const metadataString = formatMetadata(mainTx.metadata);
const merklePathString = formatMerklePath(mainTx.merklePath);
log += `\n${formatIndentedLineWithWrap(1, `txid:${mainTxid} version:${mainTx.version} lockTime:${mainTx.lockTime}${formatOptionalField('sats', action === null || action === void 0 ? void 0 : action.satoshis)}${formatOptionalField('status', action === null || action === void 0 ? void 0 : action.status)}${formatOptionalField('outgoing', action === null || action === void 0 ? void 0 : action.isOutgoing)}${formatOptionalFieldWithQuotes('desc', action === null || action === void 0 ? void 0 : action.description)}${metadataString}${merklePathString} labels:${labelString}`)}`;
logColor += `\n${formatIndentedLineWithWrap(1, [
chalk_1.default.blue(mainTxid),
` ${chalk_1.default.gray('version:')}${mainTx.version}`,
` ${chalk_1.default.gray('lockTime:')}${mainTx.lockTime}`,
` ${chalk_1.default.green(`${action === null || action === void 0 ? void 0 : action.satoshis} sats`)}`,
formatOptionalFieldWithColor('status', action === null || action === void 0 ? void 0 : action.status, chalk_1.default.white),
formatOptionalFieldWithColor('outgoing', action === null || action === void 0 ? void 0 : action.isOutgoing, chalk_1.default.white),
formatOptionalFieldWithColor('desc', action === null || action === void 0 ? void 0 : action.description, chalk_1.default.white),
metadataString ? chalk_1.default.gray(metadataString) : '',
merklePathString ? chalk_1.default.gray(merklePathString) : '',
` ${chalk_1.default.gray('labels:')}${chalk_1.default.white(labelString)}`
]
.filter(Boolean)
.join(''))}`;
log += `\n${formatIndentedLine(1, `inputs: ${(_b = (_a = action === null || action === void 0 ? void 0 : action.inputs) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0}`)}`;
logColor += `\n${formatIndentedLine(1, chalk_1.default.gray(`inputs: ${(_d = (_c = action === null || action === void 0 ? void 0 : action.inputs) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0}`))}`;
const sortedInputs = ((_e = action === null || action === void 0 ? void 0 : action.inputs) !== null && _e !== void 0 ? _e : []).sort((a, b) => a.sourceOutpoint.localeCompare(b.sourceOutpoint));
const formattedInputs = formatInputs(sortedInputs);
formattedInputs.forEach(({ log: inputLog, logColor: inputLogColor }) => {
log += `\n${formatIndentedLine(2, inputLog)}`;
logColor += `\n${formatIndentedLine(2, inputLogColor)}`;
});
log += `\n${formatIndentedLineWithWrap(1, `outputs: ${(_g = (_f = action === null || action === void 0 ? void 0 : action.outputs) === null || _f === void 0 ? void 0 : _f.length) !== null && _g !== void 0 ? _g : 0}`)}`;
logColor += `\n${formatIndentedLineWithWrap(1, chalk_1.default.gray(`outputs: ${(_j = (_h = action === null || action === void 0 ? void 0 : action.outputs) === null || _h === void 0 ? void 0 : _h.length) !== null && _j !== void 0 ? _j : 0}`))}`;
const sortedOutputs = (_k = action === null || action === void 0 ? void 0 : action.outputs) === null || _k === void 0 ? void 0 : _k.slice().sort((a, b) => a.satoshis - b.satoshis);
const formattedOutputs = formatOutputs(sortedOutputs);
formattedOutputs.forEach(({ log: outputLog, logColor: outputLogColor }) => {
log += `\n${formatIndentedLine(2, outputLog)}`;
logColor += `\n${formatIndentedLine(2, outputLogColor)}`;
});
return { log, logColor };
}
catch (error) {
return {
log: `Error parsing transaction: ${error.message}`,
logColor: chalk_1.default.red(`Error parsing transaction: ${error.message}`)
};
}
}
function createActionResultToTxLogString(createActionResult, actionsResult, showKey = false) {
const BEEF_V1 = 4022206465;
const beef = sdk_1.Beef.fromBinary(createActionResult === null || createActionResult === void 0 ? void 0 : createActionResult.tx);
beef.version = BEEF_V1;
const mainTxid = beef.txs.slice(-1)[0].txid;
return txToLogString(beef.findAtomicTransaction(mainTxid), 0, showKey, actionsResult);
}
const MAX_RECURSION_DEPTH = 3;
/**
* Truncates a TXID, replacing the middle 48 characters with '...'.
* @param {string} txid - The original transaction ID.
* @returns {string} - The truncated TXID.
*/
const truncateTxid = (txid) => {
if (txid.length <= 64) {
return txid.slice(0, 8) + '...' + txid.slice(-8);
}
return txid;
};
/**
* Formats a list of transaction outputs for logging.
* @param {TransactionOutput[]} [outputs] - The list of transaction outputs.
* @param {number} indent - The current indentation level.
* @returns {{ log: string; logColor: string }[]} - A formatted log string array.
*/
const formatTxOutputs = (outputs, indent) => outputs && outputs.length > 0
? outputs
.sort((a, b) => a.satoshis - b.satoshis)
.map((output, i) => {
let line = formatIndentedLine(indent + 4, `${i}: lock:(${output.lockingScript.toHex().length || ''})${truncate(output.lockingScript.toHex())}`);
let color = formatIndentedLine(indent + 4, `${chalk_1.default.gray(`${i}:`)} ${chalk_1.default.gray('lock:')}(${output.lockingScript.toHex().length || ''})${chalk_1.default.cyan(truncate(output.lockingScript.toHex()))}`);
if (output.satoshis) {
line += ` sats:${output.satoshis}`;
color += ` ${chalk_1.default.green(`${output.satoshis} sats`)}`;
}
return { log: line, logColor: color };
})
: [
{
log: formatIndentedLine(indent + 4, 'No outputs'),
logColor: formatIndentedLine(indent + 4, chalk_1.default.gray('No outputs'))
}
];
/**
* Formats transaction inputs with proper indentation.
* @param {TransactionInput[]} inputs - The list of transaction inputs.
* @param {number} indent - The current indentation level.
* @returns {{ log: string; logColor: string }[]} - A formatted log string array.
*/
const formatTxInputs = (inputs, indent) => inputs && inputs.length > 0
? inputs
.sort((a, b) => a.sourceTXID.localeCompare(b.sourceTXID))
.map((input, i) => {
let line = formatIndentedLine(indent + 4, `${i}: sourceTXID:${truncateTxid(input.sourceTXID)}.${input.sourceOutputIndex}`);
let color = formatIndentedLine(indent + 4, `${chalk_1.default.gray(`${i}:`)} ${chalk_1.default.blue(truncateTxid(input.sourceTXID))}.${chalk_1.default.blue(input.sourceOutputIndex)}`);
if (input.unlockingScript) {
line += `\n${formatIndentedLine(indent + 6, `unlock:(${input.unlockingScript.toHex().length})${truncate(input.unlockingScript.toHex())}`)}`;
color += `\n${formatIndentedLine(indent + 6, `${chalk_1.default.gray('unlock:')}(${input.unlockingScript.toHex().length})${chalk_1.default.cyan(truncate(input.unlockingScript.toHex()))}`)}`;
}
if (input.sequence) {
line += `\n${formatIndentedLine(indent + 6, `seq:${input.sequence}`)}`;
color += `\n${formatIndentedLine(indent + 6, `${chalk_1.default.gray('seq:')}${input.sequence}`)}`;
}
if (input.sourceTransaction) {
const { log: sourceTxLog, logColor: sourceTxLogColor } = txToLogString(input.sourceTransaction, indent + 6);
const sourceTxLogTrimed = sourceTxLog.replace(/\s+Transaction/, 'Transaction');
const sourceTxLogColorTrimed = sourceTxLogColor.replace(/\s+Transaction/, 'Transaction');
line += `\n${formatIndentedLine(indent + 6, `sourceTx:`)}${sourceTxLogTrimed}`;
color += `\n${formatIndentedLine(indent + 6, `${chalk_1.default.gray('sourceTx:')}`)}${sourceTxLogColorTrimed}`;
}
else {
line += `\n${formatIndentedLine(indent + 6, `sourceTx:Transaction [Max Depth Reached]`)}`;
color += `\n${formatIndentedLine(indent + 6, chalk_1.default.gray(`sourceTx:Transaction [Max Depth Reached]`))}`;
}
return { log: line, logColor: color };
})
: [
{
log: formatIndentedLine(indent + 4, 'No inputs'),
logColor: formatIndentedLine(indent + 4, chalk_1.default.gray('No inputs'))
}
];
/**
* Generates a formatted log string from a Transaction object.
* Ensures proper indentation and prevents recursion errors.
* @param {Transaction} tx - The Transaction object containing transaction data.
* @param {number} indent - The current indentation level.
* @param {boolean} [showKey=true] - Whether to display key transaction details.
* @param {ListActionsResult} [actionsResult] - The result of listing actions.
* @returns {{ log: string; logColor: string }} - A formatted log string and colorized version.
*/
function txToLogString(tx, indent = 0, showKey = false, actionsResult) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
try {
if (indent / 2 >= MAX_RECURSION_DEPTH) {
return {
log: formatIndentedLine(indent + 4, 'Transaction [Max Depth Reached]'),
logColor: chalk_1.default.gray(formatIndentedLine(indent + 4, 'Transaction [Max Depth Reached]'))
};
}
const beef = sdk_1.Beef.fromBinary(tx.toBEEF());
const mainTxid = beef.txs.slice(-1)[0].txid;
const metadataString = formatMetadata(tx.metadata);
const merklePathString = formatMerklePath(tx.merklePath);
let log = formatIndentedLine(indent, `Transaction:${truncateTxid(mainTxid)}`);
let logColor = formatIndentedLine(indent, `${chalk_1.default.gray('Transaction:')}${chalk_1.default.blue(truncateTxid(mainTxid))}`);
if (showKey) {
logColor += ` ${chalk_1.default.gray(`key:`)} (${chalk_1.default.blue('txid/outpoint')} ${chalk_1.default.cyan('script')} ${chalk_1.default.green('sats')})`;
}
log += `\n${formatIndentedLine(indent + 2, `version:${tx.version} lockTime:${tx.lockTime}${metadataString}${merklePathString}`)}`;
logColor += `\n${formatIndentedLine(indent + 2, `${chalk_1.default.gray('version:')}${chalk_1.default.white(tx.version)} ${chalk_1.default.gray('lockTime:')}${chalk_1.default.white(tx.lockTime)}` +
(metadataString ? chalk_1.default.gray(metadataString) : '') +
(merklePathString ? chalk_1.default.gray(merklePathString) : ''))}`;
log += `\n${formatIndentedLine(indent + 2, `inputs: ${(_b = (_a = tx === null || tx === void 0 ? void 0 : tx.inputs) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0}`)}`;
logColor += `\n${formatIndentedLine(indent + 2, chalk_1.default.gray(`inputs: ${(_d = (_c = tx === null || tx === void 0 ? void 0 : tx.inputs) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0}`))}`;
const sortedInputs = ((_e = tx === null || tx === void 0 ? void 0 : tx.inputs) !== null && _e !== void 0 ? _e : []).sort((a, b) => a.sourceTXID.localeCompare(b.sourceTXID));
const formattedInputs = formatTxInputs(sortedInputs, indent);
formattedInputs.forEach(({ log: inputLog, logColor: inputLogColor }) => {
log += `\n${inputLog}`;
logColor += `\n${inputLogCo