Skip to content

Commit 7a0b839

Browse files
fix: invalid websocket schema (#850)
* fix: websocket transaction schema is different from what server is sending * tests: added test on checking for new address generation on websocket update * refactor: passing eslint
1 parent 5752bae commit 7a0b839

File tree

6 files changed

+359
-75
lines changed

6 files changed

+359
-75
lines changed

__tests__/wallet/connection.test.ts

+49-17
Original file line numberDiff line numberDiff line change
@@ -47,35 +47,67 @@ describe('WalletConnection', () => {
4747

4848
describe('websocket transaction events', () => {
4949
const validTransaction = {
50-
tx_id: '00003eeb2ce22e80e0fa72d8afb0b8b01f8919faac94cb3a3b4900782d0f399f',
51-
nonce: 123,
52-
timestamp: Date.now(),
53-
version: 1,
54-
weight: 1,
55-
parents: ['parent1', 'parent2'],
50+
tx_id: '00000e0e193894909fc85dad8a778a8e7904de30362f53b4839e93cc315648e6',
51+
nonce: 16488190,
52+
timestamp: 1745940424,
53+
version: 2,
54+
voided: false,
55+
weight: 17.43270481128759,
56+
parents: [
57+
'000000003a90c27be4093663ef1e1eb1564aa5462f282d86fc062add717b059a',
58+
'0000762414270482d75b7c759d1b8bc7341a884084004042fb70eafe11a01eb6',
59+
],
5660
inputs: [
5761
{
58-
address: 'HH5As5aLtzFkcbmbXZmE65wSd22GqPWq2T',
59-
timelock: null,
60-
type: 'P2PKH',
62+
tx_id: '0000762414270482d75b7c759d1b8bc7341a884084004042fb70eafe11a01eb6',
63+
index: 0,
64+
value: 1n,
65+
token_data: 0,
66+
script: {
67+
type: 'Buffer' as const,
68+
data: [
69+
118, 169, 20, 107, 97, 132, 123, 120, 0, 1, 243, 13, 222, 197, 107, 73, 138, 22, 138,
70+
241, 2, 209, 72, 136, 172,
71+
],
72+
},
73+
token: '00',
74+
decoded: {
75+
type: 'P2PKH',
76+
address: 'HGJuWWGgRQ2roCfcmt5MCBJZx3yMYRz8dq',
77+
timelock: null,
78+
},
6179
},
6280
],
6381
outputs: [
6482
{
65-
address: 'HH5As5aLtzFkcbmbXZmE65wSd22GqPWq2T',
66-
timelock: null,
67-
type: 'P2PKH',
83+
value: 100n,
84+
token_data: 1,
85+
script: {
86+
type: 'Buffer' as const,
87+
data: [
88+
118, 169, 20, 69, 227, 122, 171, 130, 223, 106, 158, 121, 173, 64, 26, 133, 156, 27,
89+
199, 10, 82, 191, 81, 136, 172,
90+
],
91+
},
92+
decodedScript: null,
93+
token: '00000e0e193894909fc85dad8a778a8e7904de30362f53b4839e93cc315648e6',
94+
locked: false,
95+
index: 0,
96+
decoded: {
97+
type: 'P2PKH',
98+
address: 'HCtfX7Pz98ihXjPKCEugFHduVeuHgSXRcy',
99+
timelock: null,
100+
},
68101
},
69102
],
70-
height: 100,
71-
token_name: 'Test Token',
103+
height: 0,
104+
token_name: 'Test',
72105
token_symbol: 'TST',
73-
signal_bits: 1,
74-
voided: false,
106+
signal_bits: 0,
75107
};
76108

77109
const invalidTransaction = {
78-
// Missing required fields
110+
// Missing required fields, this remains a simple invalid object
79111
tx_id: '00003eeb2ce22e80e0fa72d8afb0b8b01f8919faac94cb3a3b4900782d0f399f',
80112
};
81113

__tests__/wallet/wallet.test.ts

+106-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ import Mnemonic from 'bitcore-mnemonic';
1010
import { mockAxiosAdapter } from '../__mock_helpers__/axios-adapter.mock';
1111
import HathorWalletServiceWallet from '../../src/wallet/wallet';
1212
import Network from '../../src/models/network';
13-
import { GetAddressesObject, WsTransaction, CreateWalletAuthData } from '../../src/wallet/types';
13+
import {
14+
GetAddressesObject,
15+
WsTransaction,
16+
CreateWalletAuthData,
17+
AddressInfoObject,
18+
} from '../../src/wallet/types';
1419
import config from '../../src/config';
1520
import {
1621
buildSuccessTxByIdTokenDataResponse,
@@ -102,6 +107,106 @@ test('getAddressAtIndex', async () => {
102107
await expect(wallet.getAddressAtIndex(0)).rejects.toThrow('Error getting wallet addresses.');
103108
});
104109

110+
describe('onNewTx', () => {
111+
const requestPassword = jest.fn();
112+
const network = new Network('testnet');
113+
const seed = defaultWalletSeed;
114+
115+
it('should call getNewAddresses if an output address is in newAddresses', async () => {
116+
const wallet = new HathorWalletServiceWallet({
117+
requestPassword,
118+
seed,
119+
network,
120+
});
121+
122+
const testAddress = 'testAddress1';
123+
// @ts-expect-error: Monkey-patching wallet instance
124+
wallet.newAddresses = [
125+
{ address: testAddress, index: 0, addressPath: "m/0'/0/0" },
126+
] as AddressInfoObject[];
127+
128+
const getNewAddressesSpy = jest
129+
// @ts-expect-error: Monkey-patching wallet instance
130+
.spyOn(wallet, 'getNewAddresses')
131+
.mockResolvedValue(undefined);
132+
133+
const newTx: WsTransaction = {
134+
tx_id: 'tx1',
135+
nonce: 0,
136+
timestamp: 0,
137+
signal_bits: 0,
138+
version: 1,
139+
weight: 1,
140+
parents: [],
141+
inputs: [],
142+
outputs: [
143+
{
144+
value: 100n,
145+
token_data: 0,
146+
script: { type: 'Buffer', data: [] },
147+
token: 'HTR',
148+
decoded: {
149+
type: 'P2PKH',
150+
address: testAddress,
151+
timelock: null,
152+
},
153+
locked: false,
154+
index: 0,
155+
},
156+
],
157+
};
158+
159+
await wallet.onNewTx(newTx);
160+
161+
expect(getNewAddressesSpy).toHaveBeenCalled();
162+
});
163+
164+
it('should not call getNewAddresses if no output address is in newAddresses', async () => {
165+
const wallet = new HathorWalletServiceWallet({
166+
requestPassword,
167+
seed,
168+
network,
169+
});
170+
171+
// @ts-expect-error: Monkey-patching newAddresses
172+
wallet.newAddresses = [
173+
{ address: 'otherAddress', index: 0, addressPath: "m/0'/0/0" },
174+
] as AddressInfoObject[];
175+
176+
const getNewAddressesSpy = jest.spyOn(wallet, 'getNewAddresses').mockResolvedValue(undefined);
177+
178+
const newTx: WsTransaction = {
179+
tx_id: 'tx2',
180+
nonce: 0,
181+
timestamp: 0,
182+
signal_bits: 0,
183+
version: 1,
184+
weight: 1,
185+
parents: [],
186+
inputs: [],
187+
outputs: [
188+
{
189+
value: 100n,
190+
token_data: 0,
191+
script: { type: 'Buffer', data: [] },
192+
token: 'HTR',
193+
decoded: {
194+
type: 'P2PKH',
195+
address: 'someRandomAddress',
196+
timelock: null,
197+
},
198+
locked: false,
199+
index: 0,
200+
},
201+
],
202+
};
203+
204+
await wallet.onNewTx(newTx);
205+
206+
expect(getNewAddressesSpy).not.toHaveBeenCalled();
207+
});
208+
});
209+
105210
test('getTxBalance', async () => {
106211
const requestPassword = jest.fn();
107212
const network = new Network('testnet');

__tests__/wallet/walletSchemas.test.ts

+101-35
Original file line numberDiff line numberDiff line change
@@ -776,64 +776,130 @@ describe('Wallet API Schemas', () => {
776776
});
777777

778778
describe('wsTransactionSchema', () => {
779+
const validWsTxData = {
780+
tx_id: '00000e0e193894909fc85dad8a778a8e7904de30362f53b4839e93cc315648e6',
781+
nonce: 16488190,
782+
timestamp: 1745940424,
783+
version: 2,
784+
voided: false,
785+
weight: 17.43270481128759,
786+
parents: [
787+
'000000003a90c27be4093663ef1e1eb1564aa5462f282d86fc062add717b059a',
788+
'0000762414270482d75b7c759d1b8bc7341a884084004042fb70eafe11a01eb6',
789+
],
790+
inputs: [
791+
{
792+
tx_id: '0000762414270482d75b7c759d1b8bc7341a884084004042fb70eafe11a01eb6',
793+
index: 0,
794+
value: 1n, // Use BigInt notation
795+
token_data: 0,
796+
script: {
797+
type: 'Buffer',
798+
data: [
799+
118, 169, 20, 107, 97, 132, 123, 120, 0, 1, 243, 13, 222, 197, 107, 73, 138, 22, 138,
800+
241, 2, 209, 72, 136, 172,
801+
],
802+
},
803+
token: '00',
804+
decoded: {
805+
type: 'P2PKH',
806+
address: 'HGJuWWGgRQ2roCfcmt5MCBJZx3yMYRz8dq',
807+
timelock: null,
808+
},
809+
},
810+
],
811+
outputs: [
812+
{
813+
value: 100n, // Use BigInt notation
814+
token_data: 1,
815+
script: {
816+
type: 'Buffer',
817+
data: [
818+
118, 169, 20, 69, 227, 122, 171, 130, 223, 106, 158, 121, 173, 64, 26, 133, 156, 27,
819+
199, 10, 82, 191, 81, 136, 172,
820+
],
821+
},
822+
decodedScript: null,
823+
token: '00000e0e193894909fc85dad8a778a8e7904de30362f53b4839e93cc315648e6',
824+
locked: false,
825+
index: 0,
826+
decoded: {
827+
type: 'P2PKH',
828+
address: 'HCtfX7Pz98ihXjPKCEugFHduVeuHgSXRcy',
829+
timelock: null,
830+
},
831+
},
832+
],
833+
height: 0,
834+
token_name: 'Test',
835+
token_symbol: 'TST',
836+
signal_bits: 0,
837+
};
838+
779839
it('should validate valid websocket transaction', () => {
780-
const validData = {
781-
tx_id: tx1,
782-
nonce: 1,
783-
timestamp: 1234567890,
784-
version: 1,
785-
voided: false,
786-
weight: 1,
787-
parents: ['parent1'],
840+
expect(() => wsTransactionSchema.parse(validWsTxData)).not.toThrow();
841+
});
842+
843+
it('should validate websocket transaction with null height', () => {
844+
const dataWithNullHeight = {
845+
...validWsTxData,
846+
height: null,
847+
};
848+
expect(() => wsTransactionSchema.parse(dataWithNullHeight)).not.toThrow();
849+
});
850+
851+
it('should validate websocket transaction with omitted height', () => {
852+
const { height: _height, ...dataWithoutHeight } = validWsTxData;
853+
expect(() => wsTransactionSchema.parse(dataWithoutHeight)).not.toThrow();
854+
});
855+
856+
it('should reject invalid websocket transaction (bad input structure)', () => {
857+
const invalidData = {
858+
...validWsTxData,
788859
inputs: [
789860
{
861+
// Missing many required fields from wsTxInputSchema
790862
address: addr1,
791863
timelock: null,
792864
type: 'P2PKH',
793865
},
794866
],
867+
};
868+
expect(() => wsTransactionSchema.parse(invalidData)).toThrow();
869+
});
870+
871+
it('should reject invalid websocket transaction (bad output structure)', () => {
872+
const invalidData = {
873+
...validWsTxData,
795874
outputs: [
796875
{
876+
// Missing many required fields from wsTxOutputSchema
797877
address: addr2,
798878
timelock: null,
799879
type: 'P2PKH',
800880
},
801881
],
802-
height: 1,
803-
token_name: 'Token 1',
804-
token_symbol: 'T1',
805-
signal_bits: 0,
806882
};
807-
expect(() => wsTransactionSchema.parse(validData)).not.toThrow();
883+
expect(() => wsTransactionSchema.parse(invalidData)).toThrow();
808884
});
809885

810-
it('should reject invalid websocket transaction', () => {
886+
it('should reject invalid websocket transaction (bad nonce type)', () => {
811887
const invalidData = {
812-
tx_id: tx1,
813-
nonce: '1', // should be number
814-
timestamp: 1234567890,
815-
version: 1,
816-
voided: false,
817-
weight: 1,
818-
parents: ['parent1'],
819-
inputs: [
820-
{
821-
address: addr1,
822-
timelock: null,
823-
type: 'P2PKH',
824-
},
825-
],
888+
...validWsTxData,
889+
nonce: '16488190', // Nonce should be number
890+
};
891+
expect(() => wsTransactionSchema.parse(invalidData)).toThrow();
892+
});
893+
894+
it('should reject invalid websocket transaction (invalid token id in output)', () => {
895+
const invalidData = {
896+
...validWsTxData,
826897
outputs: [
827898
{
828-
address: addr2,
829-
timelock: null,
830-
type: 'P2PKH',
899+
...validWsTxData.outputs[0],
900+
token: 'invalid-token-id', // Invalid token ID format
831901
},
832902
],
833-
height: 1,
834-
token_name: 'Token 1',
835-
token_symbol: 'T1',
836-
signal_bits: 0,
837903
};
838904
expect(() => wsTransactionSchema.parse(invalidData)).toThrow();
839905
});

0 commit comments

Comments
 (0)