Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: process tx after push #813

Open
wants to merge 25 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
94e96e8
feat: process tx after push
r4mmer Feb 4, 2025
8b978c4
chore: add comments explaining steps
r4mmer Feb 5, 2025
8d9b598
chore: linter changes
r4mmer Feb 5, 2025
b55524f
feat: add token being created on storage
r4mmer Feb 6, 2025
7882a17
feat: create token txs should use the uid as output.token
r4mmer Feb 7, 2025
457dc12
Merge remote-tracking branch 'origin/master' into feat/push-tx-and-se…
r4mmer Feb 7, 2025
32f21f6
feat: fetch tx from api when it is not in storage
r4mmer Feb 10, 2025
cfe92eb
chore: linter changes
r4mmer Feb 10, 2025
d153f63
chore: remove typing on api
r4mmer Feb 10, 2025
6a23eb6
chore: adding array bounds to error message
r4mmer Feb 11, 2025
53b0eb1
feat: change index test
r4mmer Feb 11, 2025
a72b608
feat: check we already have the token first
r4mmer Feb 11, 2025
f48b51f
Merge remote-tracking branch 'origin/master' into feat/push-tx-and-se…
r4mmer Feb 21, 2025
e893e7c
chore: review changes
r4mmer Feb 21, 2025
9abd019
chore: linter changes
r4mmer Feb 24, 2025
e5cef34
tests: transaction util test
r4mmer Feb 24, 2025
3371335
Merge branch 'master' into feat/push-tx-and-send-to-storage
r4mmer Feb 24, 2025
d8f50a0
Merge remote-tracking branch 'origin/master' into feat/push-tx-and-se…
r4mmer Feb 27, 2025
aabb513
chore: fix type name on import
r4mmer Feb 27, 2025
2073342
chore: refactor to use hydrate util
r4mmer Feb 28, 2025
2723011
tests: fix expected error message
r4mmer Feb 28, 2025
d461387
chore: prefer storage method
r4mmer Feb 28, 2025
dddbedf
tests: fix mockup calls
r4mmer Feb 28, 2025
c16860f
chore: linter changed
r4mmer Feb 28, 2025
9dda85d
feat: create token array should use the created token
r4mmer Feb 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions __tests__/integration/helpers/wallet.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ export async function stopAllWallets() {
*
* @return {Promise<CreateNewTokenResponse>}
*/
export async function createTokenHelper(hWallet, name, symbol, amount, options) {
export async function createTokenHelper(hWallet, name, symbol, amount, options = {}) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unrelated to line
All changes in this file are related to typing, some jsdoc types that were not converted into ts types.
This removes some typing alerts from the tests.
Obs: We don't have alerts on the CI because we exclude tests from tsc

const newTokenResponse = await hWallet.createNewToken(name, symbol, amount, options);
const tokenUid = newTokenResponse.hash;
await waitForTxReceived(hWallet, tokenUid);
Expand Down Expand Up @@ -271,10 +271,9 @@ export function waitForWalletReady(hWallet) {
* Waits for the wallet to receive the transaction from a websocket message
* and process the history
*
* @param {HathorWallet} hWallet
* @param {string} txId
* @param {number} [timeout] Timeout in milisseconds. Default value defined on test-constants.
* @returns {Promise<IHistoryTx>}
* @param hWallet
* @param txId
* @param [timeout] Timeout in milisseconds. Default value defined on test-constants.
*/
export async function waitForTxReceived(
hWallet: HathorWallet,
Expand All @@ -289,7 +288,7 @@ export async function waitForTxReceived(
timeoutReached = true;
}, timeoutPeriod);

let storageTx = await hWallet.getTx(txId);
let storageTx = (await hWallet.getTx(txId)) as IHistoryTx;

// We consider that the tx was received after it's in the storage
// and the history processing is finished
Expand All @@ -300,7 +299,7 @@ export async function waitForTxReceived(

// Tx not found, wait 1s before trying again
await delay(1000);
storageTx = await hWallet.getTx(txId);
storageTx = (await hWallet.getTx(txId)) as IHistoryTx;
}

// Clean timeout handler
Expand Down
10 changes: 3 additions & 7 deletions __tests__/integration/utils/core.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@

/**
* Simple way to wait asynchronously before continuing the funcion. Does not block the JS thread.
* @param {number} ms Amount of milliseconds to delay
* @returns {Promise<unknown>}
* @param ms Amount of milliseconds to delay
*/
export async function delay(ms) {
export async function delay(ms: number): Promise<unknown> {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
Expand All @@ -19,11 +18,8 @@ export async function delay(ms) {
/**
* Generates a random positive integer between the maximum and minimum values,
* with the default minimum equals zero
* @param {number} max
* @param {number} [min=0]
* @returns {number} Random number
*/
export function getRandomInt(max, min = 0) {
export function getRandomInt(max: number, min: number = 0): number {
const _min = Math.ceil(min);
const _max = Math.floor(max);
return Math.floor(Math.random() * (_max - _min + 1)) + _min;
Expand Down
31 changes: 30 additions & 1 deletion __tests__/utils/storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ import {
getSupportedSyncMode,
getHistorySyncMethod,
apiSyncHistory,
addCreatedTokenFromTx,
} from '../../src/utils/storage';
import { manualStreamSyncHistory, xpubStreamSyncHistory } from '../../src/sync/stream';
import CreateTokenTransaction from '../../src/models/create_token_transaction';
import Transaction from '../../src/models/transaction';

describe('scanning policy methods', () => {
it('start addresses', async () => {
Expand Down Expand Up @@ -356,8 +359,34 @@ test('getSupportedSyncMode', async () => {
await expect(getSupportedSyncMode(storage)).resolves.toEqual([]);
});

test('getHistorySyyncMethod', () => {
test('getHistorySyncMethod', () => {
expect(getHistorySyncMethod(HistorySyncMode.POLLING_HTTP_API)).toEqual(apiSyncHistory);
expect(getHistorySyncMethod(HistorySyncMode.MANUAL_STREAM_WS)).toEqual(manualStreamSyncHistory);
expect(getHistorySyncMethod(HistorySyncMode.XPUB_STREAM_WS)).toEqual(xpubStreamSyncHistory);
});

test('addCreatedTokenFromTx', async () => {
const store = new MemoryStore();
const storage = new Storage(store);
const spy = jest.spyOn(storage, 'addToken');
const tx = new CreateTokenTransaction('Token A', 'tkA', [], []);
const notCreateTokenTx = new Transaction([], []);

// If we force a transaction without the correct version it should do nothing.
await addCreatedTokenFromTx(notCreateTokenTx as CreateTokenTransaction, storage);
expect(spy).not.toHaveBeenCalled();

// Tx without hash means we do not know the UID.
await expect(addCreatedTokenFromTx(tx, storage)).rejects.toThrow();
expect(spy).not.toHaveBeenCalled();

// A working test
tx.hash = 'd00d';
await addCreatedTokenFromTx(tx, storage);
expect(spy).toHaveBeenCalledWith({
uid: 'd00d',
name: 'Token A',
symbol: 'tkA',
});
await expect(storage.getToken('d00d')).resolves.not.toBeNull();
});
165 changes: 165 additions & 0 deletions __tests__/utils/transaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@ import {
TOKEN_MELT_MASK,
TOKEN_MINT_MASK,
} from '../../src/constants';
import txApi from '../../src/api/txApi';
import { MemoryStore, Storage } from '../../src/storage';
import Input from '../../src/models/input';
import Transaction from '../../src/models/transaction';
import Output from '../../src/models/output';
import P2PKH from '../../src/models/p2pkh';
import Address from '../../src/models/address';

test('isAuthorityOutput', () => {
expect(transaction.isAuthorityOutput({ token_data: TOKEN_AUTHORITY_MASK })).toBe(true);
Expand Down Expand Up @@ -464,3 +468,164 @@ test('getTxType', () => {
expect(transaction.getTxType({ version: POA_BLOCK_VERSION })).toBe('Proof-of-Authority Block');
expect(transaction.getTxType({ version: 999 })).toBe('Unknown');
});

test('convertTransactionToHistoryTx', async () => {
const p2pkh = new P2PKH(new Address('WYBwT3xLpDnHNtYZiU52oanupVeDKhAvNp'));
const script = p2pkh.createScript();
const store = new MemoryStore();
await store.saveTx({
tx_id: 'from-storage-tx1',
inputs: [],
outputs: [
{
script: script.toString('hex'),
decoded: { address: 'WYBwT3xLpDnHNtYZiU52oanupVeDKhAvNp' },
spent_by: null,
token: 'token-C',
token_data: 2,
value: 123n,
selected_as_input: false,
},
{
script: script.toString('hex'),
decoded: { address: 'WYBwT3xLpDnHNtYZiU52oanupVeDKhAvNp' },
spent_by: null,
token: 'token-A',
token_data: 1,
value: 1n,
selected_as_input: false,
},
],
tokens: [],
is_voided: false,
nonce: 456,
parents: [],
timestamp: 123,
version: 1,
weight: 18,
});

const storage = new Storage(store);
storage.config.setNetwork('testnet');
const getTxSpy = jest.spyOn(txApi, 'getTransaction');
const tx = new Transaction([], [], {
hash: '',
parents: ['parent-1', 'parent-2'],
nonce: 123,
timestamp: 456,
tokens: ['token-A', 'token-B'],
version: 1,
signalBits: 5,
weight: 18,
});

getTxSpy.mockImplementation(async (txId, resolve) => {
switch (txId) {
case 'resolve-fail':
resolve({ success: false, message: 'failed api call' });
break;
case 'no-outputs':
resolve({
success: true,
tx: { outputs: [] },
});
break;
case 'fail':
throw new Error('Boom!');
case 'from-api-tx1':
resolve({
success: true,
tx: {
outputs: [
{
value: 2n,
token_data: 129,
script: script.toString('hex'),
token: 'token-B',
decoded: {
type: 'P2PKH',
address: 'WYBwT3xLpDnHNtYZiU52oanupVeDKhAvNp',
timelock: undefined,
},
},
],
},
});
break;
default:
throw new Error('Unknown test case');
}
});

try {
tx.hash = 'resolve-fail-case';
tx.inputs = [new Input('resolve-fail', 0)];
await expect(transaction.convertTransactionToHistoryTx(tx, storage)).rejects.toThrow(
'failed api call'
);
tx.hash = 'no-outputs-case';
tx.inputs = [new Input('no-outputs', 0)];
await expect(transaction.convertTransactionToHistoryTx(tx, storage)).rejects.toThrow(
'Index outsite of tx output array bounds'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix: minor typo

Suggested change
'Index outsite of tx output array bounds'
'Index outside of tx output array bounds'

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

);

tx.hash = 'fail-case';
tx.inputs = [new Input('fail', 0)];
await expect(transaction.convertTransactionToHistoryTx(tx, storage)).rejects.toThrow('Boom!');

tx.hash = 'success-case';
tx.inputs = [new Input('from-storage-tx1', 1), new Input('from-api-tx1', 0)];
tx.outputs = [new Output(3n, script)];
await expect(transaction.convertTransactionToHistoryTx(tx, storage)).resolves.toEqual({
tx_id: 'success-case',
parents: ['parent-1', 'parent-2'],
nonce: 123,
timestamp: 456,
tokens: ['token-A', 'token-B'],
version: 1,
signalBits: 5,
weight: 18,
is_voided: false,
inputs: [
{
tx_id: 'from-storage-tx1',
index: 1,
script: script.toString('hex'),
decoded: { address: 'WYBwT3xLpDnHNtYZiU52oanupVeDKhAvNp' },
token_data: 1,
token: 'token-A',
value: 1n,
},
{
tx_id: 'from-api-tx1',
index: 0,
script: script.toString('hex'),
decoded: {
type: 'P2PKH',
address: 'WYBwT3xLpDnHNtYZiU52oanupVeDKhAvNp',
timelock: undefined,
},
token_data: 129,
token: 'token-B',
value: 2n,
},
],
outputs: [
{
value: 3n,
token_data: 0,
script: script.toString('hex'),
decoded: {
type: 'P2PKH',
address: 'WYBwT3xLpDnHNtYZiU52oanupVeDKhAvNp',
timelock: null,
},
token: '00',
spent_by: null,
},
],
});
} finally {
getTxSpy.mockRestore();
}
});
5 changes: 2 additions & 3 deletions src/api/txApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
* LICENSE file in the root directory of this source tree.
*/

import { AxiosResponse } from 'axios';
import { createRequestInstance } from './axiosInstance';
import { transformJsonBigIntResponse } from '../utils/bigint';
import { TransactionSchema, transactionSchema } from './schemas/txApi';
import { transactionSchema } from './schemas/txApi';

/**
* Api calls for transaction
Expand Down Expand Up @@ -78,7 +77,7 @@ const txApi = {
* @memberof ApiTransaction
* @inner
*/
getTransaction(id, resolve): Promise<void | AxiosResponse<TransactionSchema>> {
getTransaction(id, resolve): Promise<void> {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getTransaction method does not return a TransactionSchema, it passes the TransactionSchema instance to the resolve argument.
We could type resolve: (a: TransactionSchema) => void which is correct, but some other parts of the lib break due to being reliant on the implicit typing.

const data = { id };
return this.getTransactionBase(data, resolve, transactionSchema);
},
Expand Down
12 changes: 12 additions & 0 deletions src/models/p2pkh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import { intToBytes } from '../utils/buffer';
import helpers from '../utils/helpers';
import Address from './address';
import { IHistoryOutputDecoded } from '../types';

type optionsType = {
timelock?: number | null | undefined;
Expand Down Expand Up @@ -82,6 +83,17 @@ class P2PKH {
return util.buffer.concat(arr);
}

/**
* Build the original decoded script
*/
toData(): IHistoryOutputDecoded {
return {
type: this.getType().toUpperCase(),
address: this.address.base58,
timelock: this.timelock,
};
}

/**
* Identify a script as P2PKH or not.
*
Expand Down
12 changes: 12 additions & 0 deletions src/models/p2sh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { OP_GREATERTHAN_TIMESTAMP, OP_HASH160, OP_EQUAL } from '../opcodes';
import helpers from '../utils/helpers';
import { intToBytes } from '../utils/buffer';
import Address from './address';
import { IHistoryOutputDecoded } from '../types';

type optionsType = {
timelock?: number | null | undefined;
Expand Down Expand Up @@ -49,6 +50,17 @@ class P2SH {
return 'p2sh';
}

/**
* Build the original decoded script
*/
toData(): IHistoryOutputDecoded {
return {
type: this.getType().toUpperCase(),
address: this.address.base58,
timelock: this.timelock,
};
}

/**
* Create a P2SH script
*
Expand Down
11 changes: 11 additions & 0 deletions src/models/script_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { util } from 'bitcore-lib';
import buffer from 'buffer';
import { OP_CHECKSIG } from '../opcodes';
import helpers from '../utils/helpers';
import { IHistoryOutputDecoded } from '../types';

class ScriptData {
// String of data to store on the script
Expand All @@ -34,6 +35,16 @@ class ScriptData {
return 'data';
}

/**
* Build the original decoded script
*/
toData(): IHistoryOutputDecoded {
return {
type: this.getType().toUpperCase(),
data: this.data,
};
}

/**
* Create an output script from data
*
Expand Down
Loading
Loading