@reality.eth/reality-eth-lib 3.4.28 → 3.4.29
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/README.md +34 -0
- package/browser-entry.ts +34 -0
- package/build-browser.mjs +24 -0
- package/dist/cjs/formatters/question.d.ts +58 -0
- package/dist/cjs/formatters/question.js +537 -0
- package/dist/cjs/formatters/template.d.ts +4 -0
- package/dist/cjs/formatters/template.js +24 -0
- package/formatters/question.js +3 -601
- package/formatters/template.js +2 -27
- package/package.json +25 -11
- package/src/formatters/question.ts +574 -0
- package/src/formatters/template.ts +19 -0
- package/test/formatters.js +49 -1
- package/test-ponder.mjs +265 -0
- package/tsconfig.json +17 -0
package/test/formatters.js
CHANGED
|
@@ -27,6 +27,14 @@ describe('Default template types', function() {
|
|
|
27
27
|
expect(q.type).to.equal(t);
|
|
28
28
|
}
|
|
29
29
|
});
|
|
30
|
+
it('round-trips a title containing double quotes without breaking JSON', function() {
|
|
31
|
+
const title = 'Will Trump say "economics" in his speech?';
|
|
32
|
+
const qtext = rc_question.encodeText('bool', title, null, 'politics');
|
|
33
|
+
const q = rc_question.populatedJSONForTemplate(rc_template.defaultTemplateForType('bool'), qtext);
|
|
34
|
+
expect(q.title).to.equal(title);
|
|
35
|
+
expect(q.type).to.equal('bool');
|
|
36
|
+
expect(q.errors).to.be.undefined;
|
|
37
|
+
});
|
|
30
38
|
it('marks the question if it has extra unused data', function() {
|
|
31
39
|
const outcomes = ["oink", "oink2"];
|
|
32
40
|
for (var i=0; i<option_types.length; i++) {
|
|
@@ -60,6 +68,31 @@ describe('Answer formatting', function() {
|
|
|
60
68
|
expect(rc_question.answerToBytes32("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffFD", q)).to.equal('0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffd');
|
|
61
69
|
expect(rc_question.answerToBytes32("1", q)).to.equal('0x0000000000000000000000000000000000000000000000000000000000000001');
|
|
62
70
|
});
|
|
71
|
+
it('Encodes datetime timestamps correctly, including those whose hex representation contains digits a-f', function() {
|
|
72
|
+
var q = rc_question.populatedJSONForTemplate(rc_template.defaultTemplateForType('datetime'), '');
|
|
73
|
+
// Trivial: 0 and 1 are the same in hex and decimal, so they pass even with the double-parse bug.
|
|
74
|
+
expect(rc_question.answerToBytes32(0, q)).to.equal('0x0000000000000000000000000000000000000000000000000000000000000000');
|
|
75
|
+
expect(rc_question.answerToBytes32(1, q)).to.equal('0x0000000000000000000000000000000000000000000000000000000000000001');
|
|
76
|
+
// 16 = 0x10: the hex string '10' re-parses as decimal 10 (= 0xa), producing the wrong value.
|
|
77
|
+
expect(rc_question.answerToBytes32(16, q)).to.equal('0x0000000000000000000000000000000000000000000000000000000000000010');
|
|
78
|
+
// 1527638400 = 0x5b0de980 (2018-05-30 UTC): hex contains 'b', 'd', 'e' → NaN without fix.
|
|
79
|
+
expect(rc_question.answerToBytes32(1527638400, q)).to.equal('0x000000000000000000000000000000000000000000000000000000005b0de980');
|
|
80
|
+
// 1767225600 = 0x6955b900 (2026-01-01 UTC): hex contains 'b' → NaN without fix.
|
|
81
|
+
expect(rc_question.answerToBytes32(1767225600, q)).to.equal('0x000000000000000000000000000000000000000000000000000000006955b900');
|
|
82
|
+
});
|
|
83
|
+
it('Encodes single-select option indices correctly, including indices >= 10', function() {
|
|
84
|
+
var outcomes = [];
|
|
85
|
+
for (var i = 0; i < 20; i++) outcomes.push('option' + i);
|
|
86
|
+
var qtext = rc_question.encodeText('single-select', 'Which?', outcomes, 'misc');
|
|
87
|
+
var q = rc_question.populatedJSONForTemplate(rc_template.defaultTemplateForType('single-select'), qtext);
|
|
88
|
+
// Indices 0-9 have the same single-character hex and decimal representation: pass without fix.
|
|
89
|
+
expect(rc_question.answerToBytes32('0', q)).to.equal('0x0000000000000000000000000000000000000000000000000000000000000000');
|
|
90
|
+
expect(rc_question.answerToBytes32('9', q)).to.equal('0x0000000000000000000000000000000000000000000000000000000000000009');
|
|
91
|
+
// Index 10 = 0xa: hex string 'a' is invalid base-10 → NaN without fix.
|
|
92
|
+
expect(rc_question.answerToBytes32('10', q)).to.equal('0x000000000000000000000000000000000000000000000000000000000000000a');
|
|
93
|
+
// Index 16 = 0x10: hex string '10' re-parses as decimal 10 (= 0xa), silently wrong without fix.
|
|
94
|
+
expect(rc_question.answerToBytes32('16', q)).to.equal('0x0000000000000000000000000000000000000000000000000000000000000010');
|
|
95
|
+
});
|
|
63
96
|
it('Turns options into hex', function() {
|
|
64
97
|
var outcomes = ['thing1', 'thing2', 'thing3'];
|
|
65
98
|
var qtext = rc_question.encodeText('multiple-select', 'oink', outcomes, 'my-category');
|
|
@@ -123,6 +156,21 @@ describe('Answer strings', function() {
|
|
|
123
156
|
expect(rc_question.getAnswerString(q, '0x0000000000000000000000000000000000000000000000001BC16D674EC80000')).to.equal('2');
|
|
124
157
|
});
|
|
125
158
|
|
|
159
|
+
it('Handles uints with 2 decimals: fractional, whole number, small fraction, zero', function() {
|
|
160
|
+
var q = rc_question.populatedJSONForTemplate(rc_template.defaultTemplateForType('uint'), '');
|
|
161
|
+
q.decimals = 2;
|
|
162
|
+
// 350 / 100 = 3.5 — the bug case: page was displaying 350 / 10^18
|
|
163
|
+
expect(rc_question.getAnswerString(q, '0x000000000000000000000000000000000000000000000000000000000000015e')).to.equal('3.5');
|
|
164
|
+
// 300 / 100 = 3.0 — whole number, trailing .00 must be stripped
|
|
165
|
+
expect(rc_question.getAnswerString(q, '0x000000000000000000000000000000000000000000000000000000000000012c')).to.equal('3');
|
|
166
|
+
// 1 / 100 = 0.01 — small fraction where fracStr must be zero-padded before strip
|
|
167
|
+
expect(rc_question.getAnswerString(q, '0x0000000000000000000000000000000000000000000000000000000000000001')).to.equal('0.01');
|
|
168
|
+
// 0 — zero is a whole number, no decimal point
|
|
169
|
+
expect(rc_question.getAnswerString(q, '0x0000000000000000000000000000000000000000000000000000000000000000')).to.equal('0');
|
|
170
|
+
// 105 / 100 = 1.05 — internal zero in fraction, only trailing zeros stripped
|
|
171
|
+
expect(rc_question.getAnswerString(q, '0x0000000000000000000000000000000000000000000000000000000000000069')).to.equal('1.05');
|
|
172
|
+
});
|
|
173
|
+
|
|
126
174
|
it('Leaves bytes32 strings unchanged except forced to lower case', function() {
|
|
127
175
|
// We don't have a built-in type for this yet so just switch out the uint one
|
|
128
176
|
var q = rc_question.populatedJSONForTemplate(rc_template.defaultTemplateForType('uint'), '');
|
|
@@ -335,7 +383,7 @@ describe('Markdown questions', function() {
|
|
|
335
383
|
expect(q.title).to.equal("`Inline code` with backticks\n\n```# code block\nprint '3 backticks or'\nprint 'indent 4 spaces'```");
|
|
336
384
|
expect(q.title_html).to.equal(
|
|
337
385
|
`<p><code>Inline code</code> with backticks</p>
|
|
338
|
-
<p><code># code block print
|
|
386
|
+
<p><code># code block print '3 backticks or' print 'indent 4 spaces'</code></p>`+ "\n");
|
|
339
387
|
});
|
|
340
388
|
});
|
|
341
389
|
|
package/test-ponder.mjs
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verify reality-eth-lib functions against Ponder's indexed question data.
|
|
3
|
+
*
|
|
4
|
+
* Tests:
|
|
5
|
+
* 1. contentHash(templateId, openingTimestamp, data) === ponder.contentHash
|
|
6
|
+
* 2. populatedJSONForTemplate(templateText, data).title === ponder.title
|
|
7
|
+
* 3. populatedJSONForTemplate(templateText, data).type === ponder.type
|
|
8
|
+
* 4. questionID v3 round-trip (when min_bond=0 and contract is known)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as lib from './dist/cjs/formatters/question.js';
|
|
12
|
+
import * as tmpl from './dist/cjs/formatters/template.js';
|
|
13
|
+
|
|
14
|
+
const PONDER_URL = 'http://localhost:42069/graphql';
|
|
15
|
+
const PAGE_SIZE = 100;
|
|
16
|
+
|
|
17
|
+
// --- GraphQL helpers --------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
async function gql(query) {
|
|
20
|
+
const res = await fetch(PONDER_URL, {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: { 'Content-Type': 'application/json' },
|
|
23
|
+
body: JSON.stringify({ query }),
|
|
24
|
+
});
|
|
25
|
+
const json = await res.json();
|
|
26
|
+
if (json.errors) throw new Error(JSON.stringify(json.errors));
|
|
27
|
+
return json.data;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function* paginate(queryFn, pageSize = PAGE_SIZE) {
|
|
31
|
+
let cursor = null;
|
|
32
|
+
while (true) {
|
|
33
|
+
const after = cursor ? `, after: "${cursor}"` : '';
|
|
34
|
+
const data = await gql(queryFn(pageSize, after));
|
|
35
|
+
const result = data[Object.keys(data)[0]];
|
|
36
|
+
yield* result.items;
|
|
37
|
+
if (!result.pageInfo.hasNextPage) break;
|
|
38
|
+
cursor = result.pageInfo.endCursor;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// --- Template cache ---------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
const templateCache = new Map(); // key: `${chainId}-${contract}-${templateId}` -> questionText
|
|
45
|
+
|
|
46
|
+
async function loadAllTemplates() {
|
|
47
|
+
const gen = paginate((limit, after) => `{
|
|
48
|
+
templates(limit: ${limit}${after}) {
|
|
49
|
+
pageInfo { hasNextPage endCursor }
|
|
50
|
+
items { id templateId chainId contract questionText }
|
|
51
|
+
}
|
|
52
|
+
}`);
|
|
53
|
+
for await (const t of gen) {
|
|
54
|
+
templateCache.set(t.id, t.questionText);
|
|
55
|
+
}
|
|
56
|
+
console.log(`Loaded ${templateCache.size} templates from Ponder.`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Get template text: prefer Ponder DB, fall back to built-in (ids 0-4)
|
|
60
|
+
function getTemplateText(chainId, contract, templateId) {
|
|
61
|
+
const key = `${chainId}-${contract}-${templateId}`;
|
|
62
|
+
if (templateCache.has(key)) return templateCache.get(key);
|
|
63
|
+
// Fall back to built-in templates (ids 0-4 same across all contracts)
|
|
64
|
+
const builtin = tmpl.preloadedTemplateContents();
|
|
65
|
+
return builtin[String(templateId)] ?? null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --- Counters ---------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
const stats = {
|
|
71
|
+
total: 0,
|
|
72
|
+
contentHashOk: 0,
|
|
73
|
+
contentHashFail: 0,
|
|
74
|
+
parseOk: 0,
|
|
75
|
+
parseTitleMismatch: 0,
|
|
76
|
+
parseTypeMismatch: 0,
|
|
77
|
+
parseNullType: 0,
|
|
78
|
+
qidOk: 0,
|
|
79
|
+
qidFail: 0,
|
|
80
|
+
qidSkip: 0,
|
|
81
|
+
templateMissing: 0,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const failures = {
|
|
85
|
+
contentHash: [],
|
|
86
|
+
title: [],
|
|
87
|
+
type: [],
|
|
88
|
+
qid: [],
|
|
89
|
+
};
|
|
90
|
+
const MAX_FAILURES = 5; // cap how many we print per category
|
|
91
|
+
|
|
92
|
+
// --- Main test logic --------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
async function testQuestion(q) {
|
|
95
|
+
stats.total++;
|
|
96
|
+
|
|
97
|
+
const templateId = Number(q.templateId);
|
|
98
|
+
const templateText = getTemplateText(q.chainId, q.contract, templateId);
|
|
99
|
+
|
|
100
|
+
// 1. contentHash
|
|
101
|
+
try {
|
|
102
|
+
const computed = lib.contentHash(templateId, q.openingTimestamp, q.data);
|
|
103
|
+
if (computed.toLowerCase() === q.contentHash.toLowerCase()) {
|
|
104
|
+
stats.contentHashOk++;
|
|
105
|
+
} else {
|
|
106
|
+
stats.contentHashFail++;
|
|
107
|
+
if (failures.contentHash.length < MAX_FAILURES) {
|
|
108
|
+
failures.contentHash.push({
|
|
109
|
+
id: q.id, computed, stored: q.contentHash,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} catch (e) {
|
|
114
|
+
stats.contentHashFail++;
|
|
115
|
+
if (failures.contentHash.length < MAX_FAILURES) {
|
|
116
|
+
failures.contentHash.push({ id: q.id, error: e.message });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 2. populatedJSONForTemplate vs Ponder's parsed title/type
|
|
121
|
+
if (templateText) {
|
|
122
|
+
try {
|
|
123
|
+
const parsed = lib.populatedJSONForTemplate(templateText, q.data);
|
|
124
|
+
|
|
125
|
+
// Title comparison (only if Ponder has one)
|
|
126
|
+
if (q.title != null) {
|
|
127
|
+
const libTitle = parsed.title ?? '';
|
|
128
|
+
if (libTitle === q.title) {
|
|
129
|
+
stats.parseOk++;
|
|
130
|
+
} else {
|
|
131
|
+
stats.parseTitleMismatch++;
|
|
132
|
+
if (failures.title.length < MAX_FAILURES) {
|
|
133
|
+
failures.title.push({
|
|
134
|
+
id: q.id,
|
|
135
|
+
libTitle,
|
|
136
|
+
ponderTitle: q.title,
|
|
137
|
+
templateId,
|
|
138
|
+
data: q.data.substring(0, 80),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Type comparison
|
|
145
|
+
if (q.type != null) {
|
|
146
|
+
if (parsed.type === q.type) {
|
|
147
|
+
// ok (counted above)
|
|
148
|
+
} else {
|
|
149
|
+
stats.parseTypeMismatch++;
|
|
150
|
+
if (failures.type.length < MAX_FAILURES) {
|
|
151
|
+
failures.type.push({
|
|
152
|
+
id: q.id,
|
|
153
|
+
libType: parsed.type,
|
|
154
|
+
ponderType: q.type,
|
|
155
|
+
templateId,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
stats.parseNullType++;
|
|
161
|
+
}
|
|
162
|
+
} catch (e) {
|
|
163
|
+
// parsing threw — count as title mismatch
|
|
164
|
+
stats.parseTitleMismatch++;
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
stats.templateMissing++;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 3. questionID v3 round-trip (use min_bond=0x0 as default)
|
|
171
|
+
// We know contract and creator from Ponder, and nonce is now stored.
|
|
172
|
+
const contract = q.contract;
|
|
173
|
+
const version = contract.toLowerCase().includes('3_2') ? '3.2' : '3.0';
|
|
174
|
+
// Only test if nonce is available and it's a v3 question
|
|
175
|
+
if (q.nonce != null) {
|
|
176
|
+
try {
|
|
177
|
+
const computed_qid = lib.questionID(
|
|
178
|
+
templateId,
|
|
179
|
+
q.data,
|
|
180
|
+
q.arbitrator,
|
|
181
|
+
q.timeout,
|
|
182
|
+
q.openingTimestamp,
|
|
183
|
+
q.creator,
|
|
184
|
+
q.nonce,
|
|
185
|
+
'0x0', // min_bond default (0 for most questions)
|
|
186
|
+
contract,
|
|
187
|
+
version
|
|
188
|
+
);
|
|
189
|
+
if (computed_qid.toLowerCase() === q.questionId.toLowerCase()) {
|
|
190
|
+
stats.qidOk++;
|
|
191
|
+
} else {
|
|
192
|
+
stats.qidFail++;
|
|
193
|
+
if (failures.qid.length < MAX_FAILURES) {
|
|
194
|
+
failures.qid.push({
|
|
195
|
+
id: q.id,
|
|
196
|
+
computed: computed_qid,
|
|
197
|
+
stored: q.questionId,
|
|
198
|
+
note: 'may have non-zero min_bond',
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} catch (e) {
|
|
203
|
+
stats.qidFail++;
|
|
204
|
+
if (failures.qid.length < MAX_FAILURES) {
|
|
205
|
+
failures.qid.push({ id: q.id, error: e.message });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
stats.qidSkip++;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// --- Entry point ------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
async function main() {
|
|
216
|
+
console.log('Loading templates...');
|
|
217
|
+
await loadAllTemplates();
|
|
218
|
+
|
|
219
|
+
console.log('Testing questions...');
|
|
220
|
+
let count = 0;
|
|
221
|
+
|
|
222
|
+
const gen = paginate((limit, after) => `{
|
|
223
|
+
questions(limit: ${limit}${after}, orderBy: "createdTimestamp", orderDirection: "asc") {
|
|
224
|
+
pageInfo { hasNextPage endCursor }
|
|
225
|
+
items {
|
|
226
|
+
id questionId nonce data title type
|
|
227
|
+
templateId contentHash contract chainId
|
|
228
|
+
creator arbitrator openingTimestamp timeout
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}`);
|
|
232
|
+
|
|
233
|
+
for await (const q of gen) {
|
|
234
|
+
await testQuestion(q);
|
|
235
|
+
count++;
|
|
236
|
+
if (count % 500 === 0) {
|
|
237
|
+
process.stdout.write(` processed ${count}...\r`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
console.log(`\n\n=== Results (${stats.total} questions) ===`);
|
|
242
|
+
console.log(`contentHash: ${stats.contentHashOk} ok, ${stats.contentHashFail} fail`);
|
|
243
|
+
console.log(`title parse: ${stats.parseOk} ok, ${stats.parseTitleMismatch} mismatch, ${stats.parseNullType} null-type (malformed template), ${stats.templateMissing} template-missing`);
|
|
244
|
+
console.log(`type parse: ${stats.parseTypeMismatch} mismatch`);
|
|
245
|
+
console.log(`questionID: ${stats.qidOk} ok, ${stats.qidFail} fail (may have non-zero min_bond), ${stats.qidSkip} skipped`);
|
|
246
|
+
|
|
247
|
+
if (failures.contentHash.length) {
|
|
248
|
+
console.log('\ncontentHash failures (sample):');
|
|
249
|
+
failures.contentHash.forEach(f => console.log(' ', JSON.stringify(f)));
|
|
250
|
+
}
|
|
251
|
+
if (failures.title.length) {
|
|
252
|
+
console.log('\ntitle mismatches (sample):');
|
|
253
|
+
failures.title.forEach(f => console.log(' ', JSON.stringify(f)));
|
|
254
|
+
}
|
|
255
|
+
if (failures.type.length) {
|
|
256
|
+
console.log('\ntype mismatches (sample):');
|
|
257
|
+
failures.type.forEach(f => console.log(' ', JSON.stringify(f)));
|
|
258
|
+
}
|
|
259
|
+
if (failures.qid.length) {
|
|
260
|
+
console.log('\nquestionID failures (sample):');
|
|
261
|
+
failures.qid.forEach(f => console.log(' ', JSON.stringify(f)));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
main().catch(e => { console.error(e); process.exit(1); });
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"lib": ["ES2021"],
|
|
6
|
+
"rootDir": "src",
|
|
7
|
+
"outDir": "dist/cjs",
|
|
8
|
+
"strict": false,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"allowSyntheticDefaultImports": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"declarationMap": false,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"resolveJsonModule": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*.ts"]
|
|
17
|
+
}
|