Skip to content

Commit 01f4b5e

Browse files
committed
pool+batches: update batchStore to support multiple lease durations
1 parent b36307a commit 01f4b5e

File tree

9 files changed

+280
-115
lines changed

9 files changed

+280
-115
lines changed

app/src/__tests__/components/pool/BatchRow.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ describe('BatchRow', () => {
1515
});
1616

1717
const render = () => {
18-
batch = new Batch(store, poolBatchSnapshot);
18+
batch = new Batch(store, 2016, poolBatchSnapshot);
1919
return renderWithProviders(<BatchRow batch={batch} />, store);
2020
};
2121

app/src/__tests__/components/pool/OrderFormSection.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ describe('OrderFormSection', () => {
153153

154154
it('should suggest the correct premium', async () => {
155155
const { getByText, getByLabelText, changeInput } = render();
156-
await store.batchStore.fetchLatestBatch();
156+
await store.batchStore.fetchBatches();
157157

158158
store.batchStore.sortedBatches[0].clearingPriceRate = 496;
159159
changeInput('Desired Inbound Liquidity', '1000000');

app/src/__tests__/store/batchStore.spec.ts

Lines changed: 64 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { runInAction, values } from 'mobx';
1+
import { keys, runInAction, values } from 'mobx';
22
import * as AUCT from 'types/generated/auctioneer_pb';
33
import { grpc } from '@improbable-eng/grpc-web';
44
import { waitFor } from '@testing-library/react';
55
import * as config from 'config';
6-
import { hex } from 'util/strings';
6+
import { b64, hex } from 'util/strings';
77
import { injectIntoGrpcUnary } from 'util/tests';
88
import {
99
poolBatchSnapshot,
@@ -30,8 +30,9 @@ describe('BatchStore', () => {
3030
grpcMock.unary.mockImplementation((desc, opts) => {
3131
let res: any;
3232
if (desc.methodName === 'BatchSnapshots') {
33+
const count = (opts.request.toObject() as any).numBatchesBack;
3334
res = {
34-
batchesList: [...Array(20)].map((_, i) => ({
35+
batchesList: [...Array(count)].map((_, i) => ({
3536
...poolBatchSnapshot,
3637
batchId: `${index + i}-${poolBatchSnapshot.batchId}`,
3738
prevBatchId: `${index + i}-${poolBatchSnapshot.prevBatchId}`,
@@ -51,38 +52,39 @@ describe('BatchStore', () => {
5152

5253
it('should fetch batches', async () => {
5354
expect(store.batches.size).toBe(0);
54-
expect(store.orderedIds.length).toBe(0);
5555

5656
await store.fetchBatches();
5757
expect(store.batches.size).toBe(BATCH_QUERY_LIMIT);
58-
expect(store.orderedIds.length).toBe(BATCH_QUERY_LIMIT);
5958
});
6059

6160
it('should append start from the oldest batch when fetching batches multiple times', async () => {
6261
expect(store.batches.size).toBe(0);
63-
expect(store.orderedIds.length).toBe(0);
6462

6563
await store.fetchBatches();
6664
expect(store.batches.size).toBe(BATCH_QUERY_LIMIT);
67-
expect(store.orderedIds.length).toBe(BATCH_QUERY_LIMIT);
6865

6966
// calling a second time should append new batches to the list
7067
await store.fetchBatches();
7168
expect(store.batches.size).toBe(BATCH_QUERY_LIMIT * 2);
72-
expect(store.orderedIds.length).toBe(BATCH_QUERY_LIMIT * 2);
7369
});
7470

7571
it('should handle a number of batches less than the query limit', async () => {
7672
// mock the BatchSnapshot response to return 5 batches with the last one having a
7773
// blank prevBatchId to signify that there are no more batches available
78-
grpcMock.unary.mockImplementation((_, opts) => {
79-
const res = {
80-
batchesList: [...Array(5)].map((_, i) => ({
81-
...poolBatchSnapshot,
82-
batchId: `${i}-${poolBatchSnapshot.batchId}`,
83-
prevBatchId: index < 4 ? `${i}-${poolBatchSnapshot.prevBatchId}` : '',
84-
})),
85-
};
74+
grpcMock.unary.mockImplementation((desc, opts) => {
75+
let res: any;
76+
if (desc.methodName === 'BatchSnapshots') {
77+
res = {
78+
batchesList: [...Array(5)].map((_, i) => ({
79+
...poolBatchSnapshot,
80+
batchId: b64(`${hex(poolBatchSnapshot.batchId)}0${i}`),
81+
prevBatchId: i < 4 ? b64(`${hex(poolBatchSnapshot.prevBatchId)}0${i}`) : '',
82+
})),
83+
};
84+
index += BATCH_QUERY_LIMIT;
85+
} else if (desc.methodName === 'LeaseDurations') {
86+
res = poolLeaseDurations;
87+
}
8688
opts.onEnd({
8789
status: grpc.Code.OK,
8890
message: { toObject: () => res },
@@ -94,11 +96,21 @@ describe('BatchStore', () => {
9496

9597
await store.fetchBatches();
9698
expect(store.batches.size).toBe(5);
99+
100+
await store.fetchBatches();
101+
expect(store.batches.size).toBe(5);
97102
});
98103

99104
it('should handle errors when fetching batches', async () => {
100-
grpcMock.unary.mockImplementationOnce(() => {
101-
throw new Error('test-err');
105+
grpcMock.unary.mockImplementation((desc, opts) => {
106+
if (desc.methodName === 'BatchSnapshots') {
107+
throw new Error('test-err');
108+
}
109+
opts.onEnd({
110+
status: grpc.Code.OK,
111+
message: { toObject: () => poolLeaseDurations },
112+
} as any);
113+
return undefined as any;
102114
});
103115
expect(rootStore.appView.alerts.size).toBe(0);
104116
await store.fetchBatches();
@@ -107,8 +119,15 @@ describe('BatchStore', () => {
107119
});
108120

109121
it('should not show error when last snapshot is not found', async () => {
110-
grpcMock.unary.mockImplementationOnce(() => {
111-
throw new Error('batch snapshot not found');
122+
grpcMock.unary.mockImplementation((desc, opts) => {
123+
if (desc.methodName === 'BatchSnapshots') {
124+
throw new Error('batch snapshot not found');
125+
}
126+
opts.onEnd({
127+
status: grpc.Code.OK,
128+
message: { toObject: () => poolLeaseDurations },
129+
} as any);
130+
return undefined as any;
112131
});
113132
expect(rootStore.appView.alerts.size).toBe(0);
114133
await store.fetchBatches();
@@ -117,20 +136,19 @@ describe('BatchStore', () => {
117136

118137
it('should fetch the latest batch', async () => {
119138
await store.fetchBatches();
120-
expect(store.orderedIds.length).toBe(BATCH_QUERY_LIMIT);
139+
expect(store.batches.size).toBe(BATCH_QUERY_LIMIT);
121140

122141
// return the same last batch to ensure no new data is added
123142
const lastBatchId = store.sortedBatches[0].batchId;
124143
index--;
125144
await store.fetchLatestBatch();
126-
expect(store.orderedIds.length).toBe(BATCH_QUERY_LIMIT);
145+
expect(store.batches.size).toBe(BATCH_QUERY_LIMIT);
127146
expect(store.sortedBatches[0].batchId).toBe(lastBatchId);
128147

129148
// return a new batch as the latest
130149
index = 100;
131150
await store.fetchLatestBatch();
132-
expect(store.orderedIds.length).toBe(BATCH_QUERY_LIMIT + 1);
133-
expect(store.orderedIds[0]).toBe(hex(`100-${poolBatchSnapshot.batchId}`));
151+
expect(store.batches.size).toBe(BATCH_QUERY_LIMIT + 1);
134152
});
135153

136154
it('should handle errors when fetching the latest batch', async () => {
@@ -205,6 +223,28 @@ describe('BatchStore', () => {
205223
expect(store.nodeTier).toBe(AUCT.NodeTier.TIER_0);
206224
});
207225

226+
it('should handle errors when fetching node tier', async () => {
227+
grpcMock.unary.mockImplementationOnce(() => {
228+
throw new Error('test-err');
229+
});
230+
expect(rootStore.appView.alerts.size).toBe(0);
231+
await store.fetchNodeTier();
232+
expect(rootStore.appView.alerts.size).toBe(1);
233+
expect(values(rootStore.appView.alerts)[0].message).toBe('test-err');
234+
});
235+
236+
it('should set the active market', async () => {
237+
expect(store.selectedLeaseDuration).toBe(0);
238+
await store.fetchBatches();
239+
expect(store.selectedLeaseDuration).toBe(2016);
240+
expect(keys(store.leaseDurations)).toEqual([2016, 4032, 6048, 8064]);
241+
store.setActiveMarket(4032);
242+
expect(store.selectedLeaseDuration).toBe(4032);
243+
store.setActiveMarket(5000);
244+
expect(store.selectedLeaseDuration).toBe(4032);
245+
expect(rootStore.appView.alerts.size).toBe(1);
246+
});
247+
208248
it('should start and stop polling', async () => {
209249
let callCount = 0;
210250
injectIntoGrpcUnary(desc => {

app/src/store/models/batch.ts

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { makeAutoObservable, observable } from 'mobx';
22
import * as AUCT from 'types/generated/auctioneer_pb';
3+
import { LeaseDuration } from 'types/state';
34
import Big from 'big.js';
45
import { toPercent } from 'util/bigmath';
56
import { ellipseInside, hex } from 'util/strings';
@@ -37,6 +38,9 @@ class MatchedOrder {
3738
}
3839
}
3940

41+
/**
42+
* Represents a batch with only orders for a specific lease duration
43+
*/
4044
export default class Batch {
4145
private _store: Store;
4246
// native values from the POOL api
@@ -47,14 +51,22 @@ export default class Batch {
4751
batchTxFeeRateSatPerKw = 0;
4852
matchedOrders: MatchedOrder[] = [];
4953

50-
constructor(store: Store, llmBatch: AUCT.BatchSnapshotResponse.AsObject) {
54+
// the provided lease duration to filter orders by
55+
leaseDuration: LeaseDuration;
56+
57+
constructor(
58+
store: Store,
59+
duration: LeaseDuration,
60+
llmBatch: AUCT.BatchSnapshotResponse.AsObject,
61+
) {
5162
makeAutoObservable(
5263
this,
5364
{ matchedOrders: observable },
5465
{ deep: false, autoBind: true },
5566
);
5667

5768
this._store = store;
69+
this.leaseDuration = duration;
5870
this.update(llmBatch);
5971
}
6072

@@ -153,18 +165,18 @@ export default class Batch {
153165
this.prevBatchId = hex(llmBatch.prevBatchId);
154166
this.batchTxId = llmBatch.batchTxId;
155167
this.batchTxFeeRateSatPerKw = llmBatch.batchTxFeeRateSatPerKw;
156-
// temporary fix to keep a batch level rate. Now that pool supports multiple
157-
// durations, the rate per matched order will vary. The rate for orders with
158-
// the same duration should be the same. In the future, we should group orders
159-
// by their duration so they can be displayed in separate charts
160-
if (llmBatch.matchedOrdersList.length > 0) {
161-
this.clearingPriceRate = llmBatch.matchedOrdersList[0].matchingRate;
162-
}
163-
this.matchedOrders = llmBatch.matchedOrdersList
164-
// there should never be a match that does not have both a bid and an ask, but
165-
// the proto -> TS compiler makes these objects optional. This filter is just
166-
// a sanity check to avoid unexpected errors
167-
.filter(m => m.ask && m.bid)
168-
.map(m => new MatchedOrder(m as Required<AUCT.MatchedOrderSnapshot.AsObject>));
168+
// loop over all markets to limit the orders of this batch to a specific lease duration
169+
llmBatch.matchedMarketsMap.forEach(([duration, market]) => {
170+
// ignore markets for other lease durations
171+
if (duration === this.leaseDuration) {
172+
this.clearingPriceRate = market.clearingPriceRate;
173+
this.matchedOrders = market.matchedOrdersList
174+
// there should never be a match that does not have both a bid and an ask, but
175+
// the proto -> TS compiler makes these objects optional. This filter is just
176+
// a sanity check to avoid unexpected errors
177+
.filter(m => m.ask && m.bid)
178+
.map(m => new MatchedOrder(m as Required<AUCT.MatchedOrderSnapshot.AsObject>));
179+
}
180+
});
169181
}
170182
}

app/src/store/models/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export { default as Account } from './account';
55
export { default as Order } from './order';
66
export { default as Lease } from './lease';
77
export { default as Batch } from './batch';
8+
export { default as Market } from './market';

app/src/store/models/market.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { makeAutoObservable, observable, ObservableMap } from 'mobx';
2+
import * as AUCT from 'types/generated/auctioneer_pb';
3+
import { hex } from 'util/strings';
4+
import { Store } from 'store';
5+
import { Batch } from './';
6+
7+
/**
8+
* Represents a list of batches for a specific lease duration.
9+
*/
10+
export default class Market {
11+
private _store: Store;
12+
13+
/** the lease duration of this market */
14+
leaseDuration: number;
15+
16+
/** the collection of batches in this market */
17+
batches: ObservableMap<string, Batch> = observable.map();
18+
19+
/**
20+
* store the order of batches so that new batches can be inserted at the front
21+
* and old batches appended at the end
22+
*/
23+
orderedIds: string[] = [];
24+
25+
constructor(store: Store, duration: number) {
26+
makeAutoObservable(this, {}, { deep: false, autoBind: true });
27+
28+
this._store = store;
29+
this.leaseDuration = duration;
30+
}
31+
32+
/**
33+
* all batches sorted by newest first
34+
*/
35+
get sortedBatches() {
36+
return (
37+
this.orderedIds
38+
.map(id => this.batches.get(id))
39+
// filter out empty batches
40+
.filter(b => !!b && b.clearingPriceRate > 0) as Batch[]
41+
);
42+
}
43+
44+
/**
45+
* the oldest batch that we have queried from the API
46+
*/
47+
get oldestBatch() {
48+
return this.sortedBatches[this.sortedBatches.length - 1];
49+
}
50+
51+
/**
52+
* Updates the collection of batches given an array of batches from the pool API.
53+
* In each batch, only the matched orders for this market will be included. Orders
54+
* for other markets will be filtered out.
55+
*/
56+
update(poolBatches: AUCT.BatchSnapshotResponse.AsObject[], appendToFront: boolean) {
57+
poolBatches.forEach(poolBatch => {
58+
const batchId = hex(poolBatch.batchId);
59+
const existing = this.batches.get(batchId);
60+
// add the batch if it's not already stored in state
61+
if (!existing) {
62+
const batch = new Batch(this._store, this.leaseDuration, poolBatch);
63+
this.batches.set(batch.batchId, batch);
64+
// add this batch's id to the orderedIds array
65+
this.orderedIds = appendToFront
66+
? [batch.batchId, ...this.orderedIds]
67+
: [...this.orderedIds, batch.batchId];
68+
} else {
69+
existing.update(poolBatch);
70+
}
71+
});
72+
}
73+
}

0 commit comments

Comments
 (0)