UNPKG

@bsv/wallet-toolbox

Version:

BRC100 conforming wallet, wallet storage and wallet signer components

966 lines (959 loc) 63 kB
"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