@dust-dice/verifier 0.4.2 → 0.4.4
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/dist/contracts.d.ts +22 -4
- package/dist/contracts.js +34 -16
- package/dist/contracts.js.map +1 -1
- package/dist/indexer.d.ts +18 -13
- package/dist/indexer.js +110 -43
- package/dist/indexer.js.map +1 -1
- package/dist/verify.js +257 -68
- package/dist/verify.js.map +1 -1
- package/package.json +8 -8
- package/src/contracts.ts +41 -17
- package/src/indexer.ts +145 -55
- package/src/verify.ts +6 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dust-dice/verifier",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Independent verifier for Dust Dice: replays every roll of a finished table from the chain's own record \u2014 no operator, no trust \u2014 and the shared read-side helpers (compiled-contract handles, indexer queries).",
|
|
6
6
|
"exports": {
|
|
@@ -28,11 +28,11 @@
|
|
|
28
28
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@midnight-ntwrk/compact-runtime": "0.
|
|
32
|
-
"@midnight-ntwrk/midnight-js-indexer-public-data-provider": "
|
|
33
|
-
"@midnight-ntwrk/midnight-js-network-id": "
|
|
34
|
-
"@midnight-ntwrk/midnight-js-protocol": "
|
|
35
|
-
"@midnight-ntwrk/midnight-js-types": "
|
|
31
|
+
"@midnight-ntwrk/compact-runtime": "0.16.0",
|
|
32
|
+
"@midnight-ntwrk/midnight-js-indexer-public-data-provider": "4.1.1",
|
|
33
|
+
"@midnight-ntwrk/midnight-js-network-id": "4.1.1",
|
|
34
|
+
"@midnight-ntwrk/midnight-js-protocol": "4.1.1",
|
|
35
|
+
"@midnight-ntwrk/midnight-js-types": "4.1.1"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "^26.4.0",
|
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
"dust-dice-verify": "dist/verify.js"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
|
-
"@dust-dice/api": "
|
|
64
|
-
"@dust-dice/contract": "
|
|
63
|
+
"@dust-dice/api": "0.4.4",
|
|
64
|
+
"@dust-dice/contract": "0.4.4"
|
|
65
65
|
},
|
|
66
66
|
"//peerDependencies": "The verifier resolves the compiled contract artifacts from wherever @dust-dice/contract is installed next to it. As regular dependencies with exact pins, npm nested a second copy of the contract package under the verifier whenever the consumer's version differed \u2014 and that copy was the one its config found (dust-dice bug #33). Peers make the consumer's single copy the only one."
|
|
67
67
|
}
|
package/src/contracts.ts
CHANGED
|
@@ -15,17 +15,21 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { CompiledContract } from '@midnight-ntwrk/midnight-js-protocol/compact-js';
|
|
18
|
+
import { ContractState } from '@midnight-ntwrk/midnight-js-protocol/compact-runtime';
|
|
18
19
|
import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider';
|
|
19
20
|
import {
|
|
20
21
|
Table,
|
|
21
22
|
Lobby,
|
|
22
23
|
tableWitnesses,
|
|
23
24
|
lobbyWitnesses,
|
|
25
|
+
unshieldedBalances,
|
|
26
|
+
type UnshieldedBalances,
|
|
24
27
|
type TablePrivateState,
|
|
25
28
|
type LobbyPrivateState,
|
|
26
29
|
} from '@dust-dice/contract';
|
|
27
30
|
|
|
28
31
|
import { NETWORK } from './config.ts';
|
|
32
|
+
import { contractStateHexAt } from './indexer.ts';
|
|
29
33
|
|
|
30
34
|
export { Table, Lobby };
|
|
31
35
|
|
|
@@ -71,6 +75,9 @@ export const CompiledLobbyContract = CompiledContract.make<Lobby.Contract<LobbyP
|
|
|
71
75
|
export type TableLedger = Table.Ledger;
|
|
72
76
|
export type LobbyLedger = Lobby.Ledger;
|
|
73
77
|
|
|
78
|
+
/** Which state to read: the latest, or the one a specific transaction left behind. */
|
|
79
|
+
export type LedgerAt = { txHash: string };
|
|
80
|
+
|
|
74
81
|
/**
|
|
75
82
|
* Read a table's ledger with a ONE-SHOT query.
|
|
76
83
|
*
|
|
@@ -79,28 +86,45 @@ export type LobbyLedger = Lobby.Ledger;
|
|
|
79
86
|
* driver -- and there is one after almost every transaction -- has to use `queryContractState`.
|
|
80
87
|
* (bugs-found.md §0 #10; the same reason probes/gate0/src/step.ts reads this way.)
|
|
81
88
|
*
|
|
82
|
-
* `
|
|
83
|
-
* game with.
|
|
89
|
+
* `at.txHash` pins the read to the state a transaction left, which is what the chain-only
|
|
90
|
+
* verifier walks the game with. By TRANSACTION, not by block: on this indexer (4.3.x) a block
|
|
91
|
+
* offset means "the action in that block", and with simultaneous rounds several of a table's
|
|
92
|
+
* transactions routinely share one -- a block-keyed read cannot say which it returned. The SDK
|
|
93
|
+
* offers no transaction-hash offset, so that read goes straight to the indexer.
|
|
84
94
|
*/
|
|
85
|
-
export async function
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (!state) {
|
|
92
|
-
throw new Error(
|
|
93
|
-
`no contract state at ${address}${blockHeight === undefined ? '' : ` @ block ${blockHeight}`}`,
|
|
94
|
-
);
|
|
95
|
+
export async function readContractState(address: string, at?: LedgerAt): Promise<ContractState> {
|
|
96
|
+
if (at === undefined) {
|
|
97
|
+
const pdp = indexerPublicDataProvider(NETWORK.indexer, NETWORK.indexerWS);
|
|
98
|
+
const state = await pdp.queryContractState(address);
|
|
99
|
+
if (!state) throw new Error(`no contract state at ${address}`);
|
|
100
|
+
return state;
|
|
95
101
|
}
|
|
96
|
-
|
|
102
|
+
const hex = await contractStateHexAt(address, at.txHash);
|
|
103
|
+
if (!hex) throw new Error(`no contract state at ${address} after tx ${at.txHash}`);
|
|
104
|
+
return ContractState.deserialize(Buffer.from(hex.replace(/^0x/, ''), 'hex'));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function readTableLedger(address: string, at?: LedgerAt): Promise<TableLedger> {
|
|
108
|
+
return Table.ledger((await readContractState(address, at)).data);
|
|
97
109
|
}
|
|
98
110
|
|
|
99
111
|
export async function readLobbyLedger(address: string): Promise<LobbyLedger> {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
112
|
+
return Lobby.ledger((await readContractState(address)).data);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* What the LEDGER says the contract holds, for checking against the `pot` it claims.
|
|
117
|
+
*
|
|
118
|
+
* Read out of the contract's own state, NOT out of the indexer's
|
|
119
|
+
* `contractAction { unshieldedBalances }` -- that field answers `[]` on this indexer even for a
|
|
120
|
+
* table sitting on a pot, which reads as a solvency alarm instead of a balance. See
|
|
121
|
+
* `@dust-dice/contract`'s `custody.ts` and the test that pins it.
|
|
122
|
+
*/
|
|
123
|
+
export async function contractUnshieldedBalances(
|
|
124
|
+
address: string,
|
|
125
|
+
at?: LedgerAt,
|
|
126
|
+
): Promise<UnshieldedBalances> {
|
|
127
|
+
return unshieldedBalances(await readContractState(address, at));
|
|
104
128
|
}
|
|
105
129
|
|
|
106
130
|
/** `Dice` as the plain five-element array everything else in this repo speaks. */
|
package/src/indexer.ts
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
* 4,900,000 in exactly this situation (docs/bugs-found.md §9). PER-TRANSACTION UTXO MOVEMENT IS
|
|
11
11
|
* THE ONLY TRUSTWORTHY MEASURE, and it is what these queries read:
|
|
12
12
|
*
|
|
13
|
-
* - `contractAction(address){ unshieldedBalances }` what the LEDGER says the contract holds
|
|
14
13
|
* - `transactions(offset:{hash}){ unshieldedCreatedOutputs / unshieldedSpentOutputs }`
|
|
15
14
|
* who actually received or spent what, with
|
|
16
15
|
* `registeredForDustGeneration` per created
|
|
@@ -18,14 +17,17 @@
|
|
|
18
17
|
* - `transactions(offset:{hash}){ raw }` the transaction's byte size, which is what
|
|
19
18
|
* the `OutsideTimeToDismiss` admission check
|
|
20
19
|
* measures
|
|
21
|
-
* - `
|
|
20
|
+
* - `contractActions(address, offset)` subscription the table's whole public history, which is
|
|
22
21
|
* all the chain-only verifier is given
|
|
22
|
+
* - `contractAction(address, offset:{transactionOffset})` the state one transaction left
|
|
23
23
|
*
|
|
24
24
|
* There is no user-address balance query in this indexer's schema (checked by introspecting
|
|
25
25
|
* `__schema.queryType.fields`: only `bridgeBalance(address)` takes an address, and that is the
|
|
26
26
|
* Cardano bridge, not NIGHT), so per-address totals are derived from the UTXO sets above.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
+
import { NATIVE_TOKEN_HEX } from '@dust-dice/contract';
|
|
30
|
+
|
|
29
31
|
import { NETWORK } from './config.ts';
|
|
30
32
|
|
|
31
33
|
export interface GqlUnshieldedUtxo {
|
|
@@ -70,8 +72,11 @@ async function gql<T>(query: string, variables: Record<string, unknown> = {}): P
|
|
|
70
72
|
return body.data;
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
/**
|
|
74
|
-
|
|
75
|
+
/**
|
|
76
|
+
* The token-type constant and the balance decode live with the contract package, next to the
|
|
77
|
+
* ledger layout they read; re-exported here so callers of this module keep one import.
|
|
78
|
+
*/
|
|
79
|
+
export { NATIVE_TOKEN_HEX, nativeBalance } from '@dust-dice/contract';
|
|
75
80
|
|
|
76
81
|
const isNative = (u: { tokenType: string }): boolean =>
|
|
77
82
|
u.tokenType.replace(/^0x/, '').toLowerCase() === NATIVE_TOKEN_HEX;
|
|
@@ -84,35 +89,11 @@ export function sumNativeFor(utxos: GqlUnshieldedUtxo[], owner: string): bigint
|
|
|
84
89
|
return sumNative(utxos.filter((u) => u.owner === owner));
|
|
85
90
|
}
|
|
86
91
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
* `contractAction(address)` returns the latest action, which is what carries the running
|
|
93
|
-
* balances, and all three action variants declare the field so it selects on the interface.
|
|
94
|
-
*/
|
|
95
|
-
export async function contractUnshieldedBalances(address: string): Promise<Record<string, bigint>> {
|
|
96
|
-
const data = await gql<{
|
|
97
|
-
contractAction: { unshieldedBalances: { tokenType: string; amount: string }[] } | null;
|
|
98
|
-
}>(
|
|
99
|
-
`query ($address: HexEncoded!) {
|
|
100
|
-
contractAction(address: $address) { unshieldedBalances { tokenType amount } }
|
|
101
|
-
}`,
|
|
102
|
-
{ address },
|
|
103
|
-
);
|
|
104
|
-
const out: Record<string, bigint> = {};
|
|
105
|
-
for (const b of data.contractAction?.unshieldedBalances ?? [])
|
|
106
|
-
out[b.tokenType] = BigInt(b.amount);
|
|
107
|
-
return out;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function nativeBalance(balances: Record<string, bigint>): bigint {
|
|
111
|
-
for (const [k, v] of Object.entries(balances)) {
|
|
112
|
-
if (k.replace(/^0x/, '').toLowerCase() === NATIVE_TOKEN_HEX) return v;
|
|
113
|
-
}
|
|
114
|
-
return 0n;
|
|
115
|
-
}
|
|
92
|
+
// What the contract HOLDS is deliberately NOT read here. The schema does offer
|
|
93
|
+
// `contractAction { unshieldedBalances }`, and it answers `[]` for every action of every
|
|
94
|
+
// contract on this indexer -- tables sitting on a pot included -- so a custody check built on
|
|
95
|
+
// it reports a solvent table as empty, which is the shape of a solvency alarm. Custody comes
|
|
96
|
+
// from the contract's own state: `contracts.ts`'s `contractUnshieldedBalances`.
|
|
116
97
|
|
|
117
98
|
/**
|
|
118
99
|
* One transaction by hash, with its UTXO deltas and its raw bytes.
|
|
@@ -164,42 +145,151 @@ export const allowanceMs = (bytes: number): number => Math.max(15.0, 0.002 * byt
|
|
|
164
145
|
/** The floor a contract call must clear to be admitted at all, in bytes. */
|
|
165
146
|
export const ADMISSION_FLOOR_BYTES = 7984;
|
|
166
147
|
|
|
148
|
+
/**
|
|
149
|
+
* One graphql-transport-ws subscription, collected until `done` accepts a row (that row included)
|
|
150
|
+
* or `max` rows have arrived.
|
|
151
|
+
*
|
|
152
|
+
* A raw WebSocket on purpose: the protocol is four message types -- init, ack, subscribe, next --
|
|
153
|
+
* and a client library would be a second copy of what the SDK already bundles. Node's global
|
|
154
|
+
* WebSocket (22+, this package's floor) is all it needs.
|
|
155
|
+
*/
|
|
156
|
+
function subscribeUntil<T>(
|
|
157
|
+
query: string,
|
|
158
|
+
variables: Record<string, unknown>,
|
|
159
|
+
field: string,
|
|
160
|
+
done: (row: T) => boolean,
|
|
161
|
+
max: number,
|
|
162
|
+
timeoutMs = 60_000,
|
|
163
|
+
): Promise<T[]> {
|
|
164
|
+
return new Promise((resolve, reject) => {
|
|
165
|
+
const ws = new WebSocket(NETWORK.indexerWS, 'graphql-transport-ws');
|
|
166
|
+
const rows: T[] = [];
|
|
167
|
+
let settled = false;
|
|
168
|
+
const finish = (err?: Error): void => {
|
|
169
|
+
if (settled) return;
|
|
170
|
+
settled = true;
|
|
171
|
+
clearTimeout(timer);
|
|
172
|
+
ws.close();
|
|
173
|
+
if (err) reject(err);
|
|
174
|
+
else resolve(rows);
|
|
175
|
+
};
|
|
176
|
+
const timer = setTimeout(
|
|
177
|
+
() => finish(new Error(`indexer subscription ${field}: still open after ${timeoutMs} ms`)),
|
|
178
|
+
timeoutMs,
|
|
179
|
+
);
|
|
180
|
+
ws.onopen = () => ws.send(JSON.stringify({ type: 'connection_init' }));
|
|
181
|
+
ws.onerror = () => finish(new Error(`indexer subscription ${field}: websocket error`));
|
|
182
|
+
ws.onclose = () =>
|
|
183
|
+
finish(new Error(`indexer subscription ${field}: closed after ${rows.length} rows`));
|
|
184
|
+
ws.onmessage = (ev) => {
|
|
185
|
+
const m = JSON.parse(String(ev.data)) as { type: string; payload?: unknown };
|
|
186
|
+
if (m.type === 'connection_ack') {
|
|
187
|
+
ws.send(JSON.stringify({ id: '1', type: 'subscribe', payload: { query, variables } }));
|
|
188
|
+
} else if (m.type === 'next') {
|
|
189
|
+
const p = m.payload as { data?: Record<string, T>; errors?: { message: string }[] };
|
|
190
|
+
if (p.errors?.length) {
|
|
191
|
+
finish(new Error(`indexer GraphQL: ${p.errors.map((e) => e.message).join('; ')}`));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const row = p.data?.[field];
|
|
195
|
+
if (row === undefined) return;
|
|
196
|
+
rows.push(row);
|
|
197
|
+
if (done(row) || rows.length >= max) finish();
|
|
198
|
+
} else if (m.type === 'error') {
|
|
199
|
+
finish(
|
|
200
|
+
new Error(`indexer subscription ${field}: ${JSON.stringify(m.payload).slice(0, 300)}`),
|
|
201
|
+
);
|
|
202
|
+
} else if (m.type === 'complete') {
|
|
203
|
+
finish();
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const sameAddress = (a: string, b: string): boolean =>
|
|
210
|
+
a.replace(/^0x/, '').toLowerCase() === b.replace(/^0x/, '').toLowerCase();
|
|
211
|
+
|
|
167
212
|
/**
|
|
168
213
|
* A contract's whole public history, oldest first.
|
|
169
214
|
*
|
|
170
215
|
* This is the ONLY thing `verify.ts` is given beyond the table's address: every join, every
|
|
171
216
|
* turn and the settlement are recovered from these actions and the contract states they point
|
|
172
|
-
* at.
|
|
217
|
+
* at.
|
|
218
|
+
*
|
|
219
|
+
* The indexer this build talks to (4.3.x, the preprod line) has no query that LISTS a contract's
|
|
220
|
+
* actions -- the ledger-9 `contract(address){ actions }` is gone. It has the `contractActions`
|
|
221
|
+
* SUBSCRIPTION, which replays every action from a block offset and then stays open for new ones;
|
|
222
|
+
* it is what midnight-js's own provider uses. The stream has no "caught up" marker of its own,
|
|
223
|
+
* so the latest action is fetched first over plain HTTP and its transaction ends the collection
|
|
224
|
+
* -- after as many rows as that transaction has actions on this contract, because a fast turn is
|
|
225
|
+
* one transaction carrying up to seven calls and stopping at the first would drop the rest. The
|
|
226
|
+
* deploy block comes from the same query (`ContractCall.deploy`), so nothing from before the
|
|
227
|
+
* contract existed is streamed.
|
|
173
228
|
*/
|
|
174
229
|
export async function contractActions(address: string, limit = 1000): Promise<ContractAction[]> {
|
|
175
230
|
type Row = {
|
|
176
231
|
__typename: ContractAction['kind'];
|
|
177
232
|
entryPoint?: string;
|
|
178
|
-
transaction: { hash: string; block: { height: number; timestamp: string } };
|
|
233
|
+
transaction: { hash: string; block: { height: number; timestamp: string | number } };
|
|
179
234
|
};
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
235
|
+
const head = await gql<{
|
|
236
|
+
contractAction: (Row & { deploy?: { transaction: { block: { height: number } } } }) | null;
|
|
237
|
+
}>(
|
|
238
|
+
`query ($address: HexEncoded!) {
|
|
239
|
+
contractAction(address: $address) {
|
|
240
|
+
__typename
|
|
241
|
+
transaction { hash block { height timestamp } }
|
|
242
|
+
... on ContractCall { deploy { transaction { block { height } } } }
|
|
189
243
|
}
|
|
190
244
|
}`,
|
|
191
|
-
{ address
|
|
245
|
+
{ address },
|
|
246
|
+
);
|
|
247
|
+
if (!head.contractAction) throw new Error(`no contract at ${address}`);
|
|
248
|
+
const latest = head.contractAction;
|
|
249
|
+
const fromHeight = latest.deploy?.transaction.block.height ?? latest.transaction.block.height;
|
|
250
|
+
|
|
251
|
+
const lastTx = await gql<{ transactions: { contractActions: { address: string }[] }[] }>(
|
|
252
|
+
`query ($hash: HexEncoded!) {
|
|
253
|
+
transactions(offset: { hash: $hash }) { contractActions { address } }
|
|
254
|
+
}`,
|
|
255
|
+
{ hash: latest.transaction.hash },
|
|
256
|
+
);
|
|
257
|
+
const lastTxActions = (lastTx.transactions[0]?.contractActions ?? []).filter((a) =>
|
|
258
|
+
sameAddress(a.address, address),
|
|
259
|
+
).length;
|
|
260
|
+
|
|
261
|
+
let seenOfLast = 0;
|
|
262
|
+
const rows = await subscribeUntil<Row>(
|
|
263
|
+
`subscription ($address: HexEncoded!, $offset: BlockOffset) {
|
|
264
|
+
contractActions(address: $address, offset: $offset) {
|
|
265
|
+
__typename
|
|
266
|
+
... on ContractCall { entryPoint }
|
|
267
|
+
transaction { hash block { height timestamp } }
|
|
268
|
+
}
|
|
269
|
+
}`,
|
|
270
|
+
{ address, offset: { height: fromHeight } },
|
|
271
|
+
'contractActions',
|
|
272
|
+
(row) => row.transaction.hash === latest.transaction.hash && ++seenOfLast >= lastTxActions,
|
|
273
|
+
limit,
|
|
274
|
+
);
|
|
275
|
+
return rows.map((a) => ({
|
|
276
|
+
kind: a.__typename,
|
|
277
|
+
entryPoint: a.entryPoint,
|
|
278
|
+
txHash: a.transaction.hash,
|
|
279
|
+
blockHeight: a.transaction.block.height,
|
|
280
|
+
blockTimestamp: Number(a.transaction.block.timestamp),
|
|
281
|
+
}));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** The state a specific transaction left the contract in, as the indexer's hex; null if none. */
|
|
285
|
+
export async function contractStateHexAt(address: string, txHash: string): Promise<string | null> {
|
|
286
|
+
const data = await gql<{ contractAction: { state: string } | null }>(
|
|
287
|
+
`query ($address: HexEncoded!, $hash: HexEncoded!) {
|
|
288
|
+
contractAction(address: $address, offset: { transactionOffset: { hash: $hash } }) { state }
|
|
289
|
+
}`,
|
|
290
|
+
{ address, hash: txHash },
|
|
192
291
|
);
|
|
193
|
-
|
|
194
|
-
return data.contract.actions
|
|
195
|
-
.map((a) => ({
|
|
196
|
-
kind: a.__typename,
|
|
197
|
-
entryPoint: a.entryPoint,
|
|
198
|
-
txHash: a.transaction.hash,
|
|
199
|
-
blockHeight: a.transaction.block.height,
|
|
200
|
-
blockTimestamp: Number(a.transaction.block.timestamp),
|
|
201
|
-
}))
|
|
202
|
-
.reverse();
|
|
292
|
+
return data.contractAction?.state ?? null;
|
|
203
293
|
}
|
|
204
294
|
|
|
205
295
|
/** Current chain tip, for sanity-checking that blocks are being produced. */
|
package/src/verify.ts
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* having replayed every earlier round in order;
|
|
27
27
|
* 4. every score, recomputed from the dice with api/src/rules.ts -- the contract-canonical
|
|
28
28
|
* rules engine, not the circuit -- matches the scorecard the chain holds, box by box,
|
|
29
|
-
* including the upper bonus and the
|
|
29
|
+
* including the upper bonus and the five-of-a-kind bonuses;
|
|
30
30
|
* 5. the winner the tie-break selects, among SURVIVORS, is the seat the chain paid;
|
|
31
31
|
* 6. the settle transaction spent ZERO user inputs and created exactly the two expected
|
|
32
32
|
* outputs, to the addresses recorded at join and at construction.
|
|
@@ -195,9 +195,9 @@ const GROUP_RANK: Record<string, number> = {
|
|
|
195
195
|
/**
|
|
196
196
|
* Read the table's whole history: every transaction, and the state it left behind.
|
|
197
197
|
*
|
|
198
|
-
* One
|
|
199
|
-
*
|
|
200
|
-
*
|
|
198
|
+
* One state read per transaction, pinned to that transaction. `contractStateObservable` is not
|
|
199
|
+
* usable for this -- it misses rapid successive updates and its first emission may predate the
|
|
200
|
+
* write being read (bugs-found.md §0 #10).
|
|
201
201
|
*/
|
|
202
202
|
async function readHistory(address: string): Promise<LogGroup[]> {
|
|
203
203
|
const actions = (await contractActions(address)).filter((a) => a.kind !== 'ContractDeploy');
|
|
@@ -210,7 +210,7 @@ async function readHistory(address: string): Promise<LogGroup[]> {
|
|
|
210
210
|
blockHeight: calls[0]!.blockHeight,
|
|
211
211
|
txHash,
|
|
212
212
|
entryPoints: calls.map((x) => x.entryPoint ?? '?'),
|
|
213
|
-
led: await readTableLedger(address,
|
|
213
|
+
led: await readTableLedger(address, { txHash }),
|
|
214
214
|
sharesBlock: false,
|
|
215
215
|
});
|
|
216
216
|
}
|
|
@@ -241,7 +241,7 @@ function seatCard(led: TableLedger, seat: number): Scorecard {
|
|
|
241
241
|
const c = led.seatCard.lookup(BigInt(seat));
|
|
242
242
|
return {
|
|
243
243
|
scores: c.filled.map((f, i) => (f ? Number(c.scores[i]) : null)),
|
|
244
|
-
|
|
244
|
+
fiveOfAKindBonuses: Number(c.fiveOfAKindBonuses),
|
|
245
245
|
};
|
|
246
246
|
}
|
|
247
247
|
|