-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
Copy pathtransaction-response.test.ts
359 lines (302 loc) · 11.4 KB
/
transaction-response.test.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
import { ErrorCode } from '@fuel-ts/errors';
import { TransactionResponse, Wallet, ScriptTransactionRequest } from 'fuels';
import { expectToThrowFuelError, launchTestNode } from 'fuels/test-utils';
import type { MockInstance } from 'vitest';
async function verifyKeepAliveMessageWasSent(subscriptionStream: ReadableStream<Uint8Array>) {
const decoder = new TextDecoder();
const reader = subscriptionStream.getReader();
let hasKeepAliveMessage = false;
do {
const { value, done } = await reader.read();
if (done) {
break;
}
const text = decoder.decode(value);
if (text === ':keep-alive-text\n\n') {
hasKeepAliveMessage = true;
}
} while (!hasKeepAliveMessage);
// The keep-alive message is sent every 15 seconds,
// and this assertion verifies that it was indeed sent.
// if this fails, check if the duration was changed on the fuel-core side.
// As of the time of writing, the latest permalink where this info can be found is:
// https://github.com/FuelLabs/fuel-core/blob/bf1b22f47c58a9d078676c5756c942d839f38916/crates/fuel-core/src/graphql_api/service.rs#L247
// To get the actual latest info you need to check out the master branch:
// https://github.com/FuelLabs/fuel-core/blob/master/crates/fuel-core/src/graphql_api/service.rs#L247
// This link can fail because master can change.
expect(hasKeepAliveMessage).toBe(true);
}
function getSubscriptionStreamFromFetch(streamHolder: { stream: ReadableStream<Uint8Array> }) {
function getFetchMock(fetchSpy: MockInstance) {
return async (...args: Parameters<typeof fetch>) => {
/**
* We need to restore the original fetch implementation so that fetching is possible
* We then get the response and mock the fetch implementation again
* So that the mock can be used for the next fetch call
*/
fetchSpy.mockRestore();
const r = await fetch(...args);
fetchSpy.mockImplementation(getFetchMock(fetchSpy));
const isSubscriptionCall = args[0].toString().endsWith('graphql-sub');
if (!isSubscriptionCall) {
return r;
}
/**
* This duplicates a stream and all writes happen to both streams.
* We can thus use one stream to verify the keep-alive message was sent
* and pass the other forward in place of the original stream,
* thereby not affecting the response at all.
* */
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const [stream1, stream2] = r.body!.tee();
// eslint-disable-next-line no-param-reassign
streamHolder.stream = stream1;
return new Response(stream2);
};
}
const fetchSpy = vi.spyOn(global, 'fetch');
fetchSpy.mockImplementation(getFetchMock(fetchSpy));
return streamHolder;
}
/**
* @group node
* @group browser
*/
describe('TransactionResponse', () => {
it('should ensure create method waits till a transaction response is given', async () => {
using launched = await launchTestNode();
const {
provider,
wallets: [adminWallet],
} = launched;
const destination = Wallet.generate({
provider,
});
const { id: transactionId } = await adminWallet.transfer(
destination.address,
100,
await provider.getBaseAssetId(),
{ gasLimit: 10_000 }
);
const response = await TransactionResponse.create(transactionId, provider);
const { id } = await response.assembleResult();
expect(id).toEqual(transactionId);
});
it('should ensure getTransactionSummary fetches a transaction and assembles transaction summary', async () => {
using launched = await launchTestNode({
nodeOptions: {
args: [
'--poa-instant',
'false',
'--poa-interval-period',
'1s',
'--tx-ttl-check-interval',
'1s',
],
},
});
const {
provider,
wallets: [adminWallet],
} = launched;
const destination = Wallet.generate({
provider,
});
const { id: transactionId } = await adminWallet.transfer(
destination.address,
100,
await provider.getBaseAssetId()
);
const response = await TransactionResponse.create(transactionId, provider);
const transactionSummary = await response.waitForResult();
expect(transactionSummary.id).toBeDefined();
expect(transactionSummary.fee).toBeDefined();
expect(transactionSummary.gasUsed).toBeDefined();
expect(transactionSummary.operations).toBeDefined();
expect(transactionSummary.type).toBeDefined();
expect(transactionSummary.blockId).toBeDefined();
expect(transactionSummary.time).toBeDefined();
expect(transactionSummary.status).toBeDefined();
expect(transactionSummary.receipts).toBeDefined();
expect(transactionSummary.mintedAssets).toBeDefined();
expect(transactionSummary.burnedAssets).toBeDefined();
expect(transactionSummary.isTypeMint).toBeDefined();
expect(transactionSummary.isTypeBlob).toBeDefined();
expect(transactionSummary.isTypeCreate).toBeDefined();
expect(transactionSummary.isTypeScript).toBeDefined();
expect(transactionSummary.isStatusFailure).toBeDefined();
expect(transactionSummary.isStatusSuccess).toBeDefined();
expect(transactionSummary.isStatusPending).toBeDefined();
expect(transactionSummary.transaction).toBeDefined();
});
it.skip(
'should ensure waitForResult always waits for the transaction to be processed',
{ timeout: 18_500 },
async () => {
using launched = await launchTestNode({
/**
* This is set to so long in order to test keep-alive message handling as well.
* Keep-alive messages are sent every 15s.
* It is very important to test this because the keep-alive messages are not sent in the same format as the other messages
* and it is reasonable to expect subscriptions lasting more than 15 seconds.
* We need a proper integration test for this
* because if the keep-alive message changed in any way between fuel-core versions and we missed it,
* all our subscriptions would break.
* We need at least one long test to ensure that the keep-alive messages are handled correctly.
* */
nodeOptions: {
args: [
'--poa-instant',
'false',
'--poa-interval-period',
'17sec',
'--tx-ttl-check-interval',
'1s',
],
},
});
const {
provider,
wallets: [genesisWallet, destination],
} = launched;
const { id: transactionId } = await genesisWallet.transfer(
destination.address,
100,
await provider.getBaseAssetId(),
{ gasLimit: 10_000 }
);
const response = await TransactionResponse.create(transactionId, provider);
const subscriptionStreamHolder = {
stream: new ReadableStream<Uint8Array>(),
};
getSubscriptionStreamFromFetch(subscriptionStreamHolder);
await response.waitForResult();
await verifyKeepAliveMessageWasSent(subscriptionStreamHolder.stream);
}
);
it(
'should throw error for a SqueezedOut status update [submitAndAwaitStatus]',
{ timeout: 10_000, retry: 10 },
async () => {
/**
* a larger --tx-pool-ttl 1s is necessary to ensure that the transaction doesn't get squeezed out
* before the waitForResult (provider.operations.statusChange) call is made
* */
using launched = await launchTestNode({
walletsConfig: {
amountPerCoin: 500_000,
},
nodeOptions: {
args: [
'--poa-instant',
'false',
'--poa-interval-period',
'2s',
'--tx-pool-ttl',
'1s',
'--tx-ttl-check-interval',
'1s',
],
loggingEnabled: false,
},
});
const {
provider,
wallets: [genesisWallet],
} = launched;
const request = new ScriptTransactionRequest();
request.addCoinOutput(Wallet.generate(), 100, await provider.getBaseAssetId());
await request.autoCost(genesisWallet);
request.updateWitnessByOwner(
genesisWallet.address,
await genesisWallet.signTransaction(request)
);
const response = await provider.sendTransaction(request);
await expectToThrowFuelError(
async () => {
await response.waitForResult();
},
{ code: ErrorCode.TRANSACTION_SQUEEZED_OUT }
);
}
);
it(
'should throw error for a SqueezedOut status update [statusChange]',
{ retry: 10 },
async () => {
using launched = await launchTestNode({
nodeOptions: {
args: [
'--poa-instant',
'false',
'--poa-interval-period',
'4s',
'--tx-pool-ttl',
'1s',
'--tx-ttl-check-interval',
'1s',
],
loggingEnabled: false,
},
});
const {
provider,
wallets: [genesisWallet],
} = launched;
const request = new ScriptTransactionRequest();
request.addCoinOutput(Wallet.generate(), 100, await provider.getBaseAssetId());
await request.autoCost(genesisWallet, {
signatureCallback: (tx) => tx.addAccountWitnesses(genesisWallet),
});
request.updateWitnessByOwner(
genesisWallet.address,
await genesisWallet.signTransaction(request)
);
const submit = await provider.sendTransaction(request);
const txResponse = new TransactionResponse(submit.id, provider, await provider.getChainId());
await expectToThrowFuelError(
async () => {
await txResponse.waitForResult();
},
{ code: ErrorCode.TRANSACTION_SQUEEZED_OUT }
);
}
);
it('builds response and awaits result [uses fee from status]', async () => {
using launched = await launchTestNode();
const {
provider,
wallets: [genesisWallet],
} = launched;
const getLatestGasPriceSpy = vi.spyOn(provider, 'getLatestGasPrice');
const request = new ScriptTransactionRequest();
request.addCoinOutput(Wallet.generate(), 100, await provider.getBaseAssetId());
await request.autoCost(genesisWallet);
const tx = await genesisWallet.sendTransaction(request);
const result = await tx.waitForResult();
// fee is used from the success status, latest gas price not needed
expect(getLatestGasPriceSpy).toHaveBeenCalledTimes(0);
expect(result.fee.toNumber()).toBeGreaterThan(0);
expect(result.id).toBe(tx.id);
});
it('builds response and assembles result [fetches gas price then uses fee]', async () => {
using launched = await launchTestNode();
const {
provider,
wallets: [genesisWallet],
} = launched;
const getLatestGasPriceSpy = vi.spyOn(provider, 'getLatestGasPrice');
const request = new ScriptTransactionRequest();
request.addCoinOutput(Wallet.generate(), 100, await provider.getBaseAssetId());
await request.autoCost(genesisWallet);
const tx = await genesisWallet.sendTransaction(request);
const result = await tx.assembleResult();
// tx has not settled so response will fetch the gas price
expect(getLatestGasPriceSpy).toHaveBeenCalledTimes(1);
expect(result.id).toBe(tx.id);
const finalisedResult = await tx.waitForResult();
expect(finalisedResult.fee.toNumber()).toBeGreaterThan(0);
expect(getLatestGasPriceSpy).toHaveBeenCalledTimes(1);
expect(finalisedResult.id).toBe(tx.id);
});
});