@bsv/wallet-toolbox
Version:
BRC100 conforming wallet, wallet storage and wallet signer components
539 lines • 26.8 kB
JavaScript
"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