UNPKG

@bsv/wallet-toolbox

Version:

BRC100 conforming wallet, wallet storage and wallet signer components

539 lines 26.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); const bsv = __importStar(require("@bsv/sdk")); const src_1 = require("../../../../../src"); const TestUtilsWalletStorage_1 = require("../../../../../test/utils/TestUtilsWalletStorage"); const EntityTransaction_1 = require("../EntityTransaction"); describe('Transaction class method tests', () => { jest.setTimeout(99999999); const env = TestUtilsWalletStorage_1.TestUtilsWalletStorage.getEnv('test'); const ctxs = []; const ctxs2 = []; beforeAll(async () => { if (env.runMySQL) { ctxs.push(await TestUtilsWalletStorage_1.TestUtilsWalletStorage.createLegacyWalletMySQLCopy('transactionTests')); ctxs2.push(await TestUtilsWalletStorage_1.TestUtilsWalletStorage.createLegacyWalletMySQLCopy('transactionTests2')); } ctxs.push(await TestUtilsWalletStorage_1.TestUtilsWalletStorage.createLegacyWalletSQLiteCopy('transactionTests')); ctxs2.push(await TestUtilsWalletStorage_1.TestUtilsWalletStorage.createLegacyWalletSQLiteCopy('transactionTests2')); }); afterAll(async () => { // Destroy both sets of database contexts for (const ctx of ctxs) { await ctx.storage.destroy(); } for (const ctx of ctxs2) { await ctx.storage.destroy(); } }); // Test: Constructor with default values test('0_creates_instance_with_default_values', () => { const tx = new EntityTransaction_1.EntityTransaction(); const now = new Date(); expect(tx.transactionId).toBe(0); expect(tx.userId).toBe(0); expect(tx.txid).toBe(''); expect(tx.status).toBe('unprocessed'); expect(tx.reference).toBe(''); expect(tx.satoshis).toBe(0); expect(tx.description).toBe(''); expect(tx.isOutgoing).toBe(false); expect(tx.rawTx).toBeUndefined(); expect(tx.inputBEEF).toBeUndefined(); expect(tx.created_at).toBeInstanceOf(Date); expect(tx.updated_at).toBeInstanceOf(Date); expect(tx.created_at.getTime()).toBeLessThanOrEqual(now.getTime()); expect(tx.updated_at.getTime()).toBeLessThanOrEqual(now.getTime()); }); // Test: Constructor with provided API object test('1_creates_instance_with_provided_api_object', () => { const now = new Date(); const apiObject = { transactionId: 123, userId: 456, txid: 'testTxid', status: 'completed', reference: 'testReference', satoshis: 789, description: 'testDescription', isOutgoing: true, rawTx: [1, 2, 3], inputBEEF: [4, 5, 6], created_at: now, updated_at: now }; const tx = new EntityTransaction_1.EntityTransaction(apiObject); expect(tx.transactionId).toBe(123); expect(tx.userId).toBe(456); expect(tx.txid).toBe('testTxid'); expect(tx.status).toBe('completed'); expect(tx.reference).toBe('testReference'); expect(tx.satoshis).toBe(789); expect(tx.description).toBe('testDescription'); expect(tx.isOutgoing).toBe(true); expect(tx.rawTx).toEqual([1, 2, 3]); expect(tx.inputBEEF).toEqual([4, 5, 6]); expect(tx.created_at).toBe(now); expect(tx.updated_at).toBe(now); }); // Test: Getters and setters test('2_getters_and_setters_work_correctly', () => { const tx = new EntityTransaction_1.EntityTransaction(); const now = new Date(); tx.transactionId = 123; tx.userId = 456; tx.txid = 'testTxid'; tx.status = 'processed'; tx.reference = 'testReference'; tx.satoshis = 789; tx.description = 'testDescription'; tx.isOutgoing = true; tx.rawTx = [1, 2, 3]; tx.inputBEEF = [4, 5, 6]; tx.created_at = now; tx.updated_at = now; // New setters tx.version = 2; tx.lockTime = 5000; expect(tx.transactionId).toBe(123); expect(tx.userId).toBe(456); expect(tx.txid).toBe('testTxid'); expect(tx.status).toBe('processed'); expect(tx.reference).toBe('testReference'); expect(tx.satoshis).toBe(789); expect(tx.description).toBe('testDescription'); expect(tx.isOutgoing).toBe(true); expect(tx.rawTx).toEqual([1, 2, 3]); expect(tx.inputBEEF).toEqual([4, 5, 6]); expect(tx.created_at).toBe(now); expect(tx.updated_at).toBe(now); // Check new properties expect(tx.version).toBe(2); // Ensure version is set correctly expect(tx.lockTime).toBe(5000); // Ensure lockTime is set correctly }); // Test: `getBsvTx` returns parsed transaction test('3_getBsvTx_returns_parsed_transaction', () => { const rawTx = Uint8Array.from([1, 2, 3]); const tx = new EntityTransaction_1.EntityTransaction({ rawTx: Array.from(rawTx) }); const bsvTx = tx.getBsvTx(); expect(bsvTx).toBeInstanceOf(bsv.Transaction); }); // Test: `getBsvTx` returns undefined if rawTx is not set test('4_getBsvTx_returns_undefined_if_no_rawTx', () => { const tx = new EntityTransaction_1.EntityTransaction(); const bsvTx = tx.getBsvTx(); expect(bsvTx).toBeUndefined(); }); // Test: `getBsvTxIns` returns parsed inputs test('5_getBsvTxIns_returns_inputs', () => { const rawTx = Uint8Array.from([1, 2, 3]); const tx = new EntityTransaction_1.EntityTransaction({ rawTx: Array.from(rawTx) }); const inputs = tx.getBsvTxIns(); expect(inputs).toBeInstanceOf(Array); }); // Test: getInputs combines spentBy and rawTx inputs test('6_getInputs_combines_spentBy_and_rawTx_inputs', async () => { for (const { activeStorage } of ctxs) { // Insert the transaction into the database const txData = await TestUtilsWalletStorage_1.TestUtilsWalletStorage.insertTestTransaction(activeStorage, undefined, true); const tx = new EntityTransaction_1.EntityTransaction(txData.tx); // Assign rawTx to simulate transaction inputs const rawTx = Uint8Array.from([1, 2, 3]); tx.rawTx = Array.from(rawTx); // Insert test outputs with spentBy linked to the transaction const output1 = await TestUtilsWalletStorage_1.TestUtilsWalletStorage.insertTestOutput(activeStorage, tx, 0, 100); await activeStorage.updateOutput(output1.outputId, { spentBy: tx.transactionId }); const output2 = await TestUtilsWalletStorage_1.TestUtilsWalletStorage.insertTestOutput(activeStorage, tx, 1, 200); await activeStorage.updateOutput(output2.outputId, { spentBy: tx.transactionId }); // Debugging: Log inserted outputs const outputs = await activeStorage.findOutputs({ partial: { spentBy: tx.transactionId } }); //console.log('Inserted Outputs:', outputs) // Get inputs from the transaction const inputs = await tx.getInputs(activeStorage); //console.log('Transaction Inputs:', inputs) // Validate the inputs expect(inputs).toHaveLength(2); expect(inputs).toEqual(expect.arrayContaining([ expect.objectContaining({ vout: 0, satoshis: 100 }), expect.objectContaining({ vout: 1, satoshis: 200 }) ])); } }); // Test: 'mergeExisting' updates when ei updated at is newer /*****************************************************************************************************/ // Actually currently fails because mergeExisting is currently setting the date to the current date // rather than the udpated_at from the incoming entity /*****************************************************************************************************/ test('9_mergeExisting_updates_when_ei_updated_at_is_newer', async () => { var _a, _b; for (const { activeStorage } of ctxs) { // Insert a test transaction into the database const txData = await TestUtilsWalletStorage_1.TestUtilsWalletStorage.insertTestTransaction(activeStorage, undefined, true); // Create the `Transaction` instance with an earlier updated_at timestamp const tx = new EntityTransaction_1.EntityTransaction({ ...txData.tx, updated_at: new Date(2022, 1, 1) }); // Create an incoming entity object (`ei`) with a newer updated_at timestamp const ei = { ...txData.tx, updated_at: new Date(2023, 1, 1), txid: 'newTxId' }; const syncMap = (0, src_1.createSyncMap)(); syncMap.transaction.idMap = { 456: 123 }; syncMap.transaction.count = 1; const expectedMergeUpdatedAt = Math.max(ei.updated_at.getTime(), tx.updated_at.getTime()); // Execute `mergeExisting` const result = await tx.mergeExisting(activeStorage, new Date(), ei, syncMap); // Validate the result and check that the transaction was updated in the database expect(result).toBe(true); const updatedTx = await activeStorage.findTransactions({ partial: { transactionId: tx.transactionId } }); expect((_a = updatedTx[0]) === null || _a === void 0 ? void 0 : _a.txid).toBe('newTxId'); // Currently expecting current time and date, but should be the updated_at from the incoming entity const updatedAtTime = (_b = updatedTx[0]) === null || _b === void 0 ? void 0 : _b.updated_at.getTime(); expect(updatedAtTime).toBe(expectedMergeUpdatedAt); } }); // Test: getBsvTx handles undefined rawTx test('10_getBsvTx_handles_undefined_rawTx', () => { const tx = new EntityTransaction_1.EntityTransaction(); // No rawTx provided const bsvTx = tx.getBsvTx(); expect(bsvTx).toBeUndefined(); }); // Test: getInputs handles storage lookups and input merging test('11_getInputs_handles_storage_lookups_and_input_merging', async () => { for (const { activeStorage } of ctxs) { // Insert a test transaction into the database const { tx } = await TestUtilsWalletStorage_1.TestUtilsWalletStorage.insertTestTransaction(activeStorage, undefined, true); // Create a Transaction instance with the inserted transaction's data const transaction = new EntityTransaction_1.EntityTransaction(tx); // Insert known inputs into the database and set the `spentBy` column to the transaction ID const input1 = await TestUtilsWalletStorage_1.TestUtilsWalletStorage.insertTestOutput(activeStorage, tx, 0, 100); // vout = 0 const input2 = await TestUtilsWalletStorage_1.TestUtilsWalletStorage.insertTestOutput(activeStorage, tx, 2, 200); // vout = 2 input1.spentBy = tx.transactionId; input2.spentBy = tx.transactionId; // Update the outputs in the database to reflect `spentBy` await activeStorage.updateOutput(input1.outputId, input1); await activeStorage.updateOutput(input2.outputId, input2); // Simulate external input for rawTx parsing const externalInput = await TestUtilsWalletStorage_1.TestUtilsWalletStorage.insertTestOutput(activeStorage, tx, // Reference the same transaction 3, // vout = 3 150 // Satoshis ); // Assign rawTx to the transaction and simulate `getBsvTxIns` behavior transaction.rawTx = Array.from(Uint8Array.from([1, 2, 3])); transaction.getBsvTxIns = () => [ { sourceTXID: externalInput.txid, sourceOutputIndex: 3 } ]; // Call `getInputs` to retrieve and merge inputs const inputs = await transaction.getInputs(activeStorage); // Validate the merged inputs expect(inputs).toHaveLength(3); // Known inputs + external input expect(inputs).toEqual(expect.arrayContaining([ expect.objectContaining({ outputId: input1.outputId }), expect.objectContaining({ outputId: input2.outputId }), expect.objectContaining({ txid: externalInput.txid, vout: 3 }) ])); } }); // Test: getProvendTx retrieves proven transaction test('15_getProvenTx_retrieves_proven_transaction', async () => { for (const { activeStorage } of ctxs) { // Insert a test proven transaction into the storage const provenTx = await TestUtilsWalletStorage_1.TestUtilsWalletStorage.insertTestProvenTx(activeStorage); // Create a Transaction instance with a valid provenTxId const tx = new EntityTransaction_1.EntityTransaction({ provenTxId: provenTx.provenTxId }); // Retrieve the ProvenTx using the getProvenTx method const retrievedProvenTx = await tx.getProvenTx(activeStorage); // Assert the retrieved ProvenTx is defined and matches the expected values expect(retrievedProvenTx).toBeDefined(); expect(retrievedProvenTx === null || retrievedProvenTx === void 0 ? void 0 : retrievedProvenTx.provenTxId).toBe(provenTx.provenTxId); } }); // Test: getProvenTx returns undefined when provenTxId is not set test('16_getProvenTx_returns_undefined_when_provenTxId_is_not_set', async () => { for (const { activeStorage } of ctxs) { // Create a Transaction instance with no provenTxId const tx = new EntityTransaction_1.EntityTransaction(); // Attempt to retrieve a ProvenTx const retrievedProvenTx = await tx.getProvenTx(activeStorage); // Assert the result is undefined expect(retrievedProvenTx).toBeUndefined(); } }); // Test: getProvenTx returns undefined when no matching ProvenTx is found test('17_getProvenTx_returns_undefined_when_no_matching_ProvenTx_is_found', async () => { for (const { activeStorage } of ctxs) { // Create a Transaction instance with a provenTxId that doesn't exist in storage const tx = new EntityTransaction_1.EntityTransaction({ provenTxId: 9999 }); // Use an ID unlikely to exist // Attempt to retrieve a ProvenTx const retrievedProvenTx = await tx.getProvenTx(activeStorage); // Assert the result is undefined expect(retrievedProvenTx).toBeUndefined(); } }); // Test: getInputs merges known inputs correctly test('18_getInputs_merges_known_inputs_correctly', async () => { for (const { activeStorage } of ctxs) { // Step 1: Insert a Transaction const { tx } = await TestUtilsWalletStorage_1.TestUtilsWalletStorage.insertTestTransaction(activeStorage, undefined, true); // Step 2: Insert outputs associated with the transaction const output1 = await TestUtilsWalletStorage_1.TestUtilsWalletStorage.insertTestOutput(activeStorage, tx, 0, 100); // vout = 0 const output2 = await TestUtilsWalletStorage_1.TestUtilsWalletStorage.insertTestOutput(activeStorage, tx, 1, 200); // vout = 1 // Step 3: Create a Transaction instance with rawTx const rawTx = Uint8Array.from([1, 2, 3]); // Example raw transaction const transaction = new EntityTransaction_1.EntityTransaction({ ...tx, rawTx: Array.from(rawTx) }); // Step 4: Simulate rawTx inputs transaction.getBsvTxIns = () => [ { sourceTXID: output1.txid, sourceOutputIndex: output1.vout }, { sourceTXID: output2.txid, sourceOutputIndex: output2.vout } ]; // Step 5: Call `getInputs` to retrieve and merge known inputs const inputs = await transaction.getInputs(activeStorage); // Step 6: Assertions expect(inputs).toHaveLength(2); // Ensure both outputs are retrieved expect(inputs).toEqual(expect.arrayContaining([ expect.objectContaining({ outputId: output1.outputId }), // vout = 0 expect.objectContaining({ outputId: output2.outputId }) // vout = 1 ])); } }); // Test: getVersion returns API version test('19_get_version_returns_api_version', () => { const tx = new EntityTransaction_1.EntityTransaction({ version: 2 }); expect(tx.version).toBe(2); }); // Test: getLockTime returns API lockTime test('20_get_lockTime_returns_api_lockTime', () => { const tx = new EntityTransaction_1.EntityTransaction({ lockTime: 500 }); expect(tx.lockTime).toBe(500); }); // Test: set id updates transactionId test('21_set_id_updates_transactionId', () => { const tx = new EntityTransaction_1.EntityTransaction(); tx.id = 123; expect(tx.transactionId).toBe(123); }); // Test: get entityName returns correct value test('22_get_entityName_returns_correct_value', () => { const tx = new EntityTransaction_1.EntityTransaction(); expect(tx.entityName).toBe('transaction'); }); // Test: get entityTable returns correct value test('23_get_entityTable_returns_correct_value', () => { const tx = new EntityTransaction_1.EntityTransaction(); expect(tx.entityTable).toBe('transactions'); }); // Test: `equals` returns false for mismatched other properties test('25_equals_returns_false_for_mismatched_other_properties', async () => { for (const { activeStorage } of ctxs) { // Insert a test transaction to use as the baseline const txData = await TestUtilsWalletStorage_1.TestUtilsWalletStorage.insertTestTransaction(activeStorage, undefined, true); const syncMap = (0, src_1.createSyncMap)(); syncMap.transaction.idMap = { [txData.tx.transactionId]: txData.tx.transactionId }; syncMap.transaction.count = 1; const tx = new EntityTransaction_1.EntityTransaction({ ...txData.tx, // Base transaction version: 2, lockTime: 500, satoshis: 789, txid: 'txid1', rawTx: [1, 2, 3], inputBEEF: [4, 5, 6], description: 'desc1', status: 'completed', reference: 'ref1' }); const other = { transactionId: txData.tx.transactionId, // Matching transactionId reference: 'ref1', // Matching reference version: 1, // Different version lockTime: 100, // Different lockTime status: 'unprocessed', // Different status satoshis: 100, // Different satoshis description: 'desc2', // Different description txid: 'txid2', // Different txid rawTx: [7, 8, 9], // Different rawTx inputBEEF: [10, 11, 12] // Different inputBEEF }; expect(tx.equals(other, syncMap)).toBe(false); // Should return false due to mismatched properties } }); // Test: `getInputs` handles known and unknown inputs test('26_getInputs_handles_known_and_unknown_inputs', async () => { for (const { activeStorage } of ctxs) { // Step 1: Insert a Transaction into the database const { tx } = await TestUtilsWalletStorage_1.TestUtilsWalletStorage.insertTestTransaction(activeStorage, undefined, true); // Step 2: Insert test outputs associated with the transaction const output1 = await TestUtilsWalletStorage_1.TestUtilsWalletStorage.insertTestOutput(activeStorage, tx, 0, 100); // vout = 0, satoshis = 100 const output2 = await TestUtilsWalletStorage_1.TestUtilsWalletStorage.insertTestOutput(activeStorage, tx, 1, 200); // vout = 1, satoshis = 200 // Step 3: Simulate rawTx inputs directly without creating a Transaction instance const rawTxInputs = [ { sourceTXID: tx.txid, sourceOutputIndex: 0 }, { sourceTXID: tx.txid, sourceOutputIndex: 1 } ]; // Step 4: Query the inputs from storage individually const inputs = await Promise.all(rawTxInputs.map(input => activeStorage.findOutputs({ partial: { transactionId: tx.transactionId, vout: input.sourceOutputIndex } }))); // Flatten the array of inputs (since `findOutputs` likely returns arrays for each query) const flattenedInputs = inputs.flat(); // Step 5: Assertions expect(flattenedInputs).toHaveLength(2); // Ensure both outputs are retrieved expect(flattenedInputs).toEqual(expect.arrayContaining([ expect.objectContaining({ outputId: output1.outputId }), // vout = 0 expect.objectContaining({ outputId: output2.outputId }) // vout = 1 ])); } }); // Test: equals identifies matching entities test('27_equals_identifies_matching_entities', async () => { const ctx1 = ctxs[0]; const ctx2 = ctxs2[0]; // Insert a transaction into the first database const tx1 = new EntityTransaction_1.EntityTransaction({ transactionId: 405, userId: 1, txid: 'txid1', created_at: new Date('2023-01-01'), updated_at: new Date('2023-01-02'), status: 'completed', reference: 'ref1', isOutgoing: true, satoshis: 789, description: 'desc1', version: 2, lockTime: 500, rawTx: [1, 2, 3], inputBEEF: [4, 5, 6] }); await ctx1.activeStorage.insertTransaction(tx1.toApi()); // Insert a matching transaction into the second database const tx2 = new EntityTransaction_1.EntityTransaction({ transactionId: 405, userId: 1, // Matching userId txid: 'txid1', // Matching txid created_at: new Date('2023-01-01'), updated_at: new Date('2023-01-02'), status: 'completed', // Matching status reference: 'ref1', // Matching reference isOutgoing: true, // Matching isOutgoing satoshis: 789, // Matching satoshis description: 'desc1', // Matching description version: 2, // Matching version lockTime: 500, // Matching lockTime rawTx: [1, 2, 3], // Matching rawTx inputBEEF: [4, 5, 6] // Matching inputBEEF }); await ctx2.activeStorage.insertTransaction(tx2.toApi()); const syncMap = (0, src_1.createSyncMap)(); syncMap.transaction.idMap = { [tx1.transactionId]: tx2.transactionId }; syncMap.transaction.count = 1; // Verify the transactions match expect(tx1.equals(tx2.toApi(), syncMap)).toBe(true); }); // Test: `equals` identifies non-matching entities test('28_equals_identifies_non_matching_entities', async () => { const ctx1 = ctxs[0]; const ctx2 = ctxs2[0]; // Insert a transaction into the first database const tx1 = new EntityTransaction_1.EntityTransaction({ transactionId: 303, userId: 1, txid: 'tx456', created_at: new Date('2023-01-01'), updated_at: new Date('2023-01-02'), status: 'unprocessed', // Default status reference: 'ref125', isOutgoing: false, // Default isOutgoing satoshis: 0, // Default satoshis description: '' // Default description }); await ctx1.activeStorage.insertTransaction(tx1.toApi()); // Insert a non-matching transaction into the second database const tx2 = new EntityTransaction_1.EntityTransaction({ transactionId: 304, userId: 1, txid: 'tx789', created_at: new Date('2023-01-01'), updated_at: new Date('2023-01-02'), status: 'unprocessed', // Default status reference: 'ref126', isOutgoing: false, // Default isOutgoing satoshis: 0, // Default satoshis description: '' // Default description }); await ctx2.activeStorage.insertTransaction(tx2.toApi()); const syncMap = (0, src_1.createSyncMap)(); syncMap.transaction.idMap = { [tx1.transactionId]: tx2.transactionId }; syncMap.transaction.count = 1; // Verify the transactions do not match expect(tx1.equals(tx2.toApi(), syncMap)).toBe(false); }); }); //# sourceMappingURL=TransactionTests.test.js.map