@flayerlabs/gamemode-client 0.4.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/live.ts CHANGED
@@ -26,6 +26,7 @@ import {
26
26
  type PresenceState,
27
27
  type Reaction,
28
28
  type RoundTerms,
29
+ type SpendToken,
29
30
  type WireEconomyBalance,
30
31
  type WireMarketState,
31
32
  type WireRoundTerms,
@@ -64,7 +65,7 @@ export interface Host {
64
65
  authorisation: Authorisation,
65
66
  progress?: (event: HostBuyProgress) => void,
66
67
  signal?: AbortSignal,
67
- ): Promise<{ spentWei: bigint } | { failed: BuyResult }>;
68
+ ): Promise<{ spent: bigint } | { failed: BuyResult }>;
68
69
  }
69
70
 
70
71
  /** The host reports only what it uniquely knows; LiveRoom owns the terminal result. */
@@ -377,7 +378,7 @@ class LiveRoom<PublicView, PlayerView, Action> implements Room<PublicView, Playe
377
378
  readonly economy: Economy = {
378
379
  current: () => this.economySignal.current(),
379
380
  subscribe: (listener) => this.economySignal.subscribe(listener),
380
- available: () => this.economySignal.current().availableWei,
381
+ available: () => this.economySignal.current().available,
381
382
  buy: (maxSpendWei) => this.buy(maxSpendWei),
382
383
  };
383
384
 
@@ -411,9 +412,9 @@ class LiveRoom<PublicView, PlayerView, Action> implements Room<PublicView, Playe
411
412
  return this.failBuy(attempt, 'try-again');
412
413
  }
413
414
 
414
- let spendWei: bigint;
415
+ let spend: bigint;
415
416
  try {
416
- spendWei = BigInt(authorisation.maxSpendWei);
417
+ spend = BigInt(authorisation.maxSpendWei);
417
418
  } catch {
418
419
  return this.failBuy(attempt, 'try-again');
419
420
  }
@@ -424,11 +425,11 @@ class LiveRoom<PublicView, PlayerView, Action> implements Room<PublicView, Playe
424
425
  ...before,
425
426
  ...(this.balanceRevision === balanceRevision
426
427
  ? {
427
- heldWei: before.heldWei + spendWei,
428
- availableWei: before.availableWei > spendWei ? before.availableWei - spendWei : 0n,
428
+ held: before.held + spend,
429
+ available: before.available > spend ? before.available - spend : 0n,
429
430
  }
430
431
  : {}),
431
- buy: { state: 'signing', spendWei },
432
+ buy: { state: 'signing', spend },
432
433
  });
433
434
 
434
435
  let transactionHash: string | undefined;
@@ -443,8 +444,8 @@ class LiveRoom<PublicView, PlayerView, Action> implements Room<PublicView, Playe
443
444
  this.economySignal.set({
444
445
  ...current,
445
446
  buy: transactionHash === undefined
446
- ? { state: 'pending', spendWei }
447
- : { state: 'pending', spendWei, transactionHash },
447
+ ? { state: 'pending', spend }
448
+ : { state: 'pending', spend, transactionHash },
448
449
  });
449
450
  });
450
451
  hostSettled = true;
@@ -466,11 +467,11 @@ class LiveRoom<PublicView, PlayerView, Action> implements Room<PublicView, Playe
466
467
 
467
468
  const current = this.economySignal.current();
468
469
  const confirmed: BuyProgress = transactionHash === undefined
469
- ? { state: 'confirmed', spentWei: result.spentWei }
470
- : { state: 'confirmed', spentWei: result.spentWei, transactionHash };
470
+ ? { state: 'confirmed', spent: result.spent }
471
+ : { state: 'confirmed', spent: result.spent, transactionHash };
471
472
  // The signed allowance remains held until the gate observes chain settlement or it expires.
472
473
  this.economySignal.set({ ...current, buy: confirmed });
473
- return { bought: true, spentWei: result.spentWei };
474
+ return { bought: true, spent: result.spent };
474
475
  }
475
476
 
476
477
  readonly social: Social = {
@@ -567,11 +568,11 @@ class LiveRoom<PublicView, PlayerView, Action> implements Room<PublicView, Playe
567
568
 
568
569
  function emptyEconomy(): EconomyState {
569
570
  return {
570
- weiPerPoint: 0n,
571
- earnedWei: 0n,
572
- heldWei: 0n,
573
- spentWei: 0n,
574
- availableWei: 0n,
571
+ unitsPerPoint: 0n,
572
+ earned: 0n,
573
+ held: 0n,
574
+ spent: 0n,
575
+ available: 0n,
575
576
  holdExpiresAt: null,
576
577
  buy: null,
577
578
  };
@@ -582,13 +583,13 @@ function economyFrom(input: unknown): Omit<EconomyState, 'buy'> | null {
582
583
  const value = input as Partial<WireEconomyBalance>;
583
584
  try {
584
585
  const amounts = {
585
- weiPerPoint: BigInt(value.weiPerPoint ?? '0'),
586
- earnedWei: BigInt(value.earnedWei ?? '-1'),
587
- heldWei: BigInt(value.heldWei ?? '-1'),
588
- spentWei: BigInt(value.spentWei ?? '-1'),
589
- availableWei: BigInt(value.availableWei ?? '-1'),
586
+ unitsPerPoint: BigInt(value.unitsPerPoint ?? '0'),
587
+ earned: BigInt(value.earned ?? '-1'),
588
+ held: BigInt(value.held ?? '-1'),
589
+ spent: BigInt(value.spent ?? '-1'),
590
+ available: BigInt(value.available ?? '-1'),
590
591
  };
591
- if (amounts.weiPerPoint <= 0n || Object.values(amounts).some((amount) => amount < 0n)) return null;
592
+ if (amounts.unitsPerPoint <= 0n || Object.values(amounts).some((amount) => amount < 0n)) return null;
592
593
 
593
594
  // A deadline is either a whole millisecond timestamp or absent. Anything else — a string, a
594
595
  // fraction, Infinity — is a malformed frame, and a countdown drawn from one would run to a
@@ -614,38 +615,77 @@ export function termsFrom(input: unknown): RoundTerms | null {
614
615
  const value = input as Partial<WireRoundTerms>;
615
616
  const decimal = /^(0|[1-9][0-9]*)$/;
616
617
  if (
617
- typeof value.walletCapWei !== 'string' ||
618
- !decimal.test(value.walletCapWei) ||
619
- typeof value.weiPerPoint !== 'string' ||
620
- !decimal.test(value.weiPerPoint)
618
+ typeof value.walletCap !== 'string' ||
619
+ !decimal.test(value.walletCap) ||
620
+ typeof value.unitsPerPoint !== 'string' ||
621
+ !decimal.test(value.unitsPerPoint)
621
622
  ) return null;
622
623
  try {
623
- const walletCapWei = BigInt(value.walletCapWei);
624
- const weiPerPoint = BigInt(value.weiPerPoint);
625
- const { maxPointsPerPlayer, pointsPerDollar, usdPerEth } = value;
624
+ const walletCap = BigInt(value.walletCap);
625
+ const unitsPerPoint = BigInt(value.unitsPerPoint);
626
+ const { maxPointsPerPlayer, pointsPerDollar, usdPerSpendToken } = value;
626
627
  if (
627
- walletCapWei <= 0n ||
628
- weiPerPoint <= 0n ||
628
+ walletCap <= 0n ||
629
+ unitsPerPoint <= 0n ||
629
630
  typeof maxPointsPerPlayer !== 'number' ||
630
631
  !Number.isSafeInteger(maxPointsPerPlayer) ||
631
632
  maxPointsPerPlayer <= 0 ||
632
633
  // Prices, not counts, so these are finite rather than integral. Both are still positive: a
633
- // round priced at zero dollars an ETH is a round whose rate cannot mean anything.
634
+ // round priced at zero dollars a token is a round whose rate cannot mean anything.
634
635
  typeof pointsPerDollar !== 'number' ||
635
636
  !Number.isFinite(pointsPerDollar) ||
636
637
  pointsPerDollar <= 0 ||
637
- typeof usdPerEth !== 'number' ||
638
- !Number.isFinite(usdPerEth) ||
639
- usdPerEth <= 0
638
+ typeof usdPerSpendToken !== 'number' ||
639
+ !Number.isFinite(usdPerSpendToken) ||
640
+ usdPerSpendToken <= 0
640
641
  ) {
641
642
  return null;
642
643
  }
643
- return Object.freeze({ maxPointsPerPlayer, walletCapWei, weiPerPoint, pointsPerDollar, usdPerEth });
644
+ // The token is required: every amount in these terms is in its base units, so terms without it
645
+ // cannot be displayed or spent against. A gate that does not send one is not one this client
646
+ // can play against, and refusing here says so at the snapshot rather than in the arithmetic.
647
+ const spendToken = spendTokenFrom((value as { spendToken?: unknown }).spendToken);
648
+ if (!spendToken) return null;
649
+ return Object.freeze({
650
+ maxPointsPerPlayer,
651
+ walletCap,
652
+ unitsPerPoint,
653
+ pointsPerDollar,
654
+ usdPerSpendToken,
655
+ spendToken,
656
+ });
644
657
  } catch {
645
658
  return null;
646
659
  }
647
660
  }
648
661
 
662
+ const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
663
+
664
+ function spendTokenFrom(input: unknown): SpendToken | null {
665
+ if (typeof input !== 'object' || input === null) return null;
666
+ const value = input as Partial<SpendToken>;
667
+ if (
668
+ typeof value.address !== 'string' ||
669
+ !ADDRESS.test(value.address) ||
670
+ typeof value.symbol !== 'string' ||
671
+ value.symbol.length === 0 ||
672
+ value.symbol.length > 64 ||
673
+ typeof value.decimals !== 'number' ||
674
+ !Number.isSafeInteger(value.decimals) ||
675
+ value.decimals < 0 ||
676
+ value.decimals > 36 ||
677
+ typeof value.isNative !== 'boolean'
678
+ ) {
679
+ return null;
680
+ }
681
+ return Object.freeze({
682
+ address: value.address,
683
+ symbol: value.symbol,
684
+ decimals: value.decimals,
685
+ isNative: value.isNative,
686
+ });
687
+ }
688
+
649
689
  function presenceFrom(input: unknown): PresenceState | null {
650
690
  if (typeof input !== 'object' || input === null) return null;
651
691
  const value = input as Partial<PresenceState>;
@@ -707,14 +747,14 @@ export function marketFrom(input: unknown): MarketState | null {
707
747
  (trade.side !== 'buy' && trade.side !== 'sell') ||
708
748
  (trade.priceEth !== null && (typeof trade.priceEth !== 'number' || !Number.isFinite(trade.priceEth)))
709
749
  ) return [];
710
- const spendWei = BigInt(trade.spendWei);
711
- if (spendWei < 0n) return [];
750
+ const spend = BigInt(trade.spend);
751
+ if (spend < 0n) return [];
712
752
  return [{
713
753
  id: trade.id,
714
754
  at: trade.at,
715
755
  player: trade.player,
716
756
  side: trade.side,
717
- spendWei,
757
+ spend,
718
758
  priceEth: trade.priceEth,
719
759
  ...(typeof trade.transactionHash === 'string' ? { transactionHash: trade.transactionHash } : {}),
720
760
  }];
package/src/mock.ts CHANGED
@@ -2,6 +2,7 @@ import { Round } from '@flayerlabs/gamemode-spec/round';
2
2
  import { isRefusal, type GameModule, type PlayerId } from '@flayerlabs/gamemode-spec';
3
3
  import type { BuyProgress, BuyResult, Economy, EconomyState, MockOptions, Room, SendResult, Snapshot, Social } from './index.js';
4
4
  import {
5
+ ETH_SPEND_TOKEN,
5
6
  isReactionId,
6
7
  type ConnectionState,
7
8
  type LaunchContext,
@@ -16,8 +17,6 @@ import { CachedIdentityResolver } from './identity.js';
16
17
  /** How often the mock looks for a deadline that has fallen due. */
17
18
  const TICK_MS = 100;
18
19
 
19
- const WEI_PER_ETH = 1_000_000_000_000_000_000n;
20
-
21
20
  /** Long enough to see a spinner, short enough not to be annoying while building. */
22
21
  const FAKE_SIGN_MS = 150;
23
22
  const FAKE_PENDING_MS = 250;
@@ -29,13 +28,13 @@ export class MockRoom<Config, State, Event, Action, PublicView, PlayerView>
29
28
  {
30
29
  private readonly round: Round<Config, State, Event, Action, PublicView, PlayerView>;
31
30
  private readonly player: PlayerId;
32
- private readonly weiPerPoint: bigint;
31
+ private readonly unitsPerPoint: bigint;
33
32
  private readonly allowedReactions: ReadonlySet<string>;
34
33
  private readonly listeners = new Set<(s: Snapshot<PublicView, PlayerView>) => void>();
35
34
  private readonly reactionListeners = new Set<(r: Reaction) => void>();
36
35
  private readonly timer: ReturnType<typeof setInterval>;
37
- private spentWei = 0n;
38
- private heldWei = 0n;
36
+ private spent = 0n;
37
+ private held = 0n;
39
38
  /** Mirrors the gate's five-minute hold, so a game can build the countdown against the mock. */
40
39
  private holdExpiresAt: number | null = null;
41
40
  private buyProgress: BuyProgress | null = null;
@@ -53,7 +52,7 @@ export class MockRoom<Config, State, Event, Action, PublicView, PlayerView>
53
52
  ) {
54
53
  const opensAt = Date.now() + (options.lobbyMs ?? 3_000);
55
54
  this.player = options.player ?? '0xyou';
56
- this.weiPerPoint = options.weiPerPoint ?? 10_000_000_000_000n;
55
+ this.unitsPerPoint = options.unitsPerPoint ?? 10_000_000_000_000n;
57
56
  this.allowedReactions = new Set(options.reactionIds ?? []);
58
57
  if ([...this.allowedReactions].some((id) => !isReactionId(id))) {
59
58
  throw new RangeError('reactionIds must contain only valid reaction identifiers');
@@ -79,10 +78,10 @@ export class MockRoom<Config, State, Event, Action, PublicView, PlayerView>
79
78
  // may spend. Defaulting the cap to exactly that payout, and refusing anything under it, keeps
80
79
  // the mock from letting a game pass locally on numbers the gate would reject.
81
80
  const maxPointsPerPlayer = game.rewardBounds(options.config).maxPointsPerPlayer;
82
- const flawlessRoundWei = BigInt(maxPointsPerPlayer) * this.weiPerPoint;
83
- const walletCapWei = options.walletCapWei ?? flawlessRoundWei;
84
- if (walletCapWei < flawlessRoundWei) {
85
- throw new RangeError('walletCapWei must cover a flawless round, or the gate would refuse this launch');
81
+ const flawlessRoundWei = BigInt(maxPointsPerPlayer) * this.unitsPerPoint;
82
+ const walletCap = options.walletCap ?? flawlessRoundWei;
83
+ if (walletCap < flawlessRoundWei) {
84
+ throw new RangeError('walletCap must cover a flawless round, or the gate would refuse this launch');
86
85
  }
87
86
  // The gate derives its rate from these two, so the mock runs the identity backwards from
88
87
  // whatever rate it was given. Reporting three numbers that do not agree would let a menu built
@@ -91,17 +90,33 @@ export class MockRoom<Config, State, Event, Action, PublicView, PlayerView>
91
90
  if (!Number.isFinite(pointsPerDollar) || pointsPerDollar <= 0) {
92
91
  throw new RangeError('pointsPerDollar must be a positive finite number');
93
92
  }
94
- const pointsPerEth = WEI_PER_ETH / this.weiPerPoint;
95
- if (pointsPerEth <= 0n || pointsPerEth > BigInt(Number.MAX_SAFE_INTEGER)) {
96
- throw new RangeError('weiPerPoint does not produce representable round terms');
93
+ // In the spend token's own base units — wei unless a token was given. The gate's identity is
94
+ // `unitsPerPoint = units / round(usdPerSpendToken × pointsPerDollar)`, so the mock runs it
95
+ // backwards and reports exactly the three numbers a live round would agree on.
96
+ const spendToken = options.spendToken ?? ETH_SPEND_TOKEN;
97
+ const decimals = spendToken.decimals;
98
+ if (!Number.isSafeInteger(decimals) || decimals < 0 || decimals > 36) {
99
+ throw new RangeError('spendToken.decimals must be an integer between 0 and 36');
100
+ }
101
+ const units = 10n ** BigInt(decimals);
102
+ const pointsPerToken = units / this.unitsPerPoint;
103
+ if (pointsPerToken <= 0n || pointsPerToken > BigInt(Number.MAX_SAFE_INTEGER)) {
104
+ throw new RangeError('unitsPerPoint does not produce representable round terms');
97
105
  }
98
- const usdPerEth = Number(pointsPerEth) / pointsPerDollar;
99
- const roundTrip = WEI_PER_ETH / BigInt(Math.round(usdPerEth * pointsPerDollar));
100
- if (roundTrip !== this.weiPerPoint) {
101
- throw new RangeError('weiPerPoint must round-trip through gate pricing terms');
106
+ const usdPerSpendToken = Number(pointsPerToken) / pointsPerDollar;
107
+ const roundTrip = units / BigInt(Math.round(usdPerSpendToken * pointsPerDollar));
108
+ if (roundTrip !== this.unitsPerPoint) {
109
+ throw new RangeError('unitsPerPoint must round-trip through gate pricing terms');
102
110
  }
103
111
  this.termsSignal = new Signal<RoundTerms>(
104
- Object.freeze({ maxPointsPerPlayer, walletCapWei, weiPerPoint: this.weiPerPoint, pointsPerDollar, usdPerEth }),
112
+ Object.freeze({
113
+ maxPointsPerPlayer,
114
+ walletCap,
115
+ unitsPerPoint: this.unitsPerPoint,
116
+ pointsPerDollar,
117
+ usdPerSpendToken,
118
+ spendToken: Object.freeze({ ...spendToken }),
119
+ }),
105
120
  );
106
121
  this.marketSignal = new Signal(
107
122
  options.platform?.market?.current() ?? { status: 'unavailable', prices: [], trades: [], marketCapUsd: null },
@@ -180,25 +195,25 @@ export class MockRoom<Config, State, Event, Action, PublicView, PlayerView>
180
195
  };
181
196
 
182
197
  private economyState(): EconomyState {
183
- const earnedWei = BigInt(this.round.pointsFor(this.player)) * this.weiPerPoint;
184
- const availableWei = earnedWei - this.spentWei - this.heldWei;
198
+ const earned = BigInt(this.round.pointsFor(this.player)) * this.unitsPerPoint;
199
+ const available = earned - this.spent - this.held;
185
200
  return {
186
- weiPerPoint: this.weiPerPoint,
187
- earnedWei,
188
- heldWei: this.heldWei,
189
- spentWei: this.spentWei,
190
- availableWei: availableWei > 0n ? availableWei : 0n,
191
- holdExpiresAt: this.heldWei > 0n ? this.holdExpiresAt : null,
201
+ unitsPerPoint: this.unitsPerPoint,
202
+ earned,
203
+ held: this.held,
204
+ spent: this.spent,
205
+ available: available > 0n ? available : 0n,
206
+ holdExpiresAt: this.held > 0n ? this.holdExpiresAt : null,
192
207
  buy: this.buyProgress,
193
208
  };
194
209
  }
195
210
 
196
211
  private readonly economySignal = new Signal<EconomyState>({
197
- weiPerPoint: 0n,
198
- earnedWei: 0n,
199
- heldWei: 0n,
200
- spentWei: 0n,
201
- availableWei: 0n,
212
+ unitsPerPoint: 0n,
213
+ earned: 0n,
214
+ held: 0n,
215
+ spent: 0n,
216
+ available: 0n,
202
217
  holdExpiresAt: null,
203
218
  buy: null,
204
219
  });
@@ -210,7 +225,7 @@ export class MockRoom<Config, State, Event, Action, PublicView, PlayerView>
210
225
  readonly economy: Economy = {
211
226
  current: () => this.economyState(),
212
227
  subscribe: (listener) => this.economySignal.subscribe(listener),
213
- available: () => this.economyState().availableWei,
228
+ available: () => this.economyState().available,
214
229
  buy: async (maxSpendWei: bigint): Promise<BuyResult> => {
215
230
  if (maxSpendWei <= 0n) {
216
231
  this.buyProgress = { state: 'failed', reason: 'nothing-to-spend' };
@@ -228,34 +243,34 @@ export class MockRoom<Config, State, Event, Action, PublicView, PlayerView>
228
243
  }
229
244
 
230
245
  const spend = maxSpendWei < available ? maxSpendWei : available;
231
- this.heldWei += spend;
246
+ this.held += spend;
232
247
  this.holdExpiresAt = Date.now() + MOCK_HOLD_MS;
233
- this.buyProgress = { state: 'signing', spendWei: spend };
248
+ this.buyProgress = { state: 'signing', spend: spend };
234
249
  this.publishEconomy();
235
250
 
236
251
  await new Promise((resolve) => setTimeout(resolve, FAKE_SIGN_MS));
237
252
  if (this.disposed) {
238
- this.heldWei -= spend;
253
+ this.held -= spend;
239
254
  this.buyProgress = { state: 'failed', reason: 'try-again' };
240
255
  this.publishEconomy();
241
256
  return { bought: false, reason: 'try-again' };
242
257
  }
243
- this.buyProgress = { state: 'pending', spendWei: spend };
258
+ this.buyProgress = { state: 'pending', spend: spend };
244
259
  this.publishEconomy();
245
260
 
246
261
  await new Promise((resolve) => setTimeout(resolve, FAKE_PENDING_MS));
247
262
  if (this.disposed) {
248
- this.heldWei -= spend;
263
+ this.held -= spend;
249
264
  this.buyProgress = { state: 'failed', reason: 'try-again' };
250
265
  this.publishEconomy();
251
266
  return { bought: false, reason: 'try-again' };
252
267
  }
253
268
 
254
- this.heldWei -= spend;
255
- this.spentWei += spend;
256
- this.buyProgress = { state: 'confirmed', spentWei: spend };
269
+ this.held -= spend;
270
+ this.spent += spend;
271
+ this.buyProgress = { state: 'confirmed', spent: spend };
257
272
  this.emit();
258
- return { bought: true, spentWei: spend };
273
+ return { bought: true, spent: spend };
259
274
  },
260
275
  };
261
276
 
@@ -27,7 +27,7 @@ export interface MarketFixture {
27
27
  afterMs: number;
28
28
  player: string | null;
29
29
  side: 'buy' | 'sell';
30
- spendWei: bigint;
30
+ spend: bigint;
31
31
  priceEth: number | null;
32
32
  }[];
33
33
  marketCapUsd?: number | null;
@@ -91,7 +91,7 @@ export function replayMarket(fixture: MarketFixture, now: () => number = Date.no
91
91
  at: startedAt + trade.afterMs,
92
92
  player: trade.player,
93
93
  side: trade.side,
94
- spendWei: trade.spendWei,
94
+ spend: trade.spend,
95
95
  priceEth: trade.priceEth,
96
96
  }));
97
97
 
@@ -141,10 +141,10 @@ export const BUSY_LAUNCH: MarketFixture = {
141
141
  { afterMs: 18_000, priceEth: 0.000_049 },
142
142
  ],
143
143
  trades: [
144
- { afterMs: 1_500, player: '0x1111111111111111111111111111111111111111', side: 'buy', spendWei: 12_000_000_000_000_000n, priceEth: 0.000_013 },
145
- { afterMs: 5_200, player: '0x2222222222222222222222222222222222222222', side: 'buy', spendWei: 40_000_000_000_000_000n, priceEth: 0.000_021 },
146
- { afterMs: 6_100, player: null, side: 'buy', spendWei: 8_000_000_000_000_000n, priceEth: 0.000_041 },
147
- { afterMs: 9_400, player: '0x3333333333333333333333333333333333333333', side: 'sell', spendWei: 5_000_000_000_000_000n, priceEth: 0.000_039 },
148
- { afterMs: 12_800, player: '0x1111111111111111111111111111111111111111', side: 'buy', spendWei: 25_000_000_000_000_000n, priceEth: 0.000_050 },
144
+ { afterMs: 1_500, player: '0x1111111111111111111111111111111111111111', side: 'buy', spend: 12_000_000_000_000_000n, priceEth: 0.000_013 },
145
+ { afterMs: 5_200, player: '0x2222222222222222222222222222222222222222', side: 'buy', spend: 40_000_000_000_000_000n, priceEth: 0.000_021 },
146
+ { afterMs: 6_100, player: null, side: 'buy', spend: 8_000_000_000_000_000n, priceEth: 0.000_041 },
147
+ { afterMs: 9_400, player: '0x3333333333333333333333333333333333333333', side: 'sell', spend: 5_000_000_000_000_000n, priceEth: 0.000_039 },
148
+ { afterMs: 12_800, player: '0x1111111111111111111111111111111111111111', side: 'buy', spend: 25_000_000_000_000_000n, priceEth: 0.000_050 },
149
149
  ],
150
150
  };