@reality.eth/reality-eth-lib 3.4.25 → 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/package.json CHANGED
@@ -1,9 +1,24 @@
1
1
  {
2
2
  "name": "@reality.eth/reality-eth-lib",
3
- "version": "3.4.25",
3
+ "version": "3.4.29",
4
4
  "description": "Tools for handling questions in the reality.eth fact verification platform",
5
5
  "scripts": {
6
- "test": "env TZ='Asia/Kabul' mocha ./test"
6
+ "build": "tsc",
7
+ "build:browser": "node build-browser.mjs",
8
+ "prepublishOnly": "npm run build && npm run build:browser",
9
+ "test": "npm run build && env TZ='Asia/Kabul' mocha ./test"
10
+ },
11
+ "exports": {
12
+ "./formatters/question.js": {
13
+ "require": "./dist/cjs/formatters/question.js",
14
+ "default": "./dist/cjs/formatters/question.js",
15
+ "types": "./dist/cjs/formatters/question.d.ts"
16
+ },
17
+ "./formatters/template.js": {
18
+ "require": "./dist/cjs/formatters/template.js",
19
+ "default": "./dist/cjs/formatters/template.js",
20
+ "types": "./dist/cjs/formatters/template.d.ts"
21
+ }
7
22
  },
8
23
  "keywords": [
9
24
  "Ethereum",
@@ -16,20 +31,19 @@
16
31
  "author": "Edmund Edgar (https://reality.eth.link)",
17
32
  "license": "GPL-3.0",
18
33
  "dependencies": {
19
- "@reality.eth/contracts": "^3.2.22",
20
- "bignumber.js": "^7.2.1",
21
- "bn.js": "^5.2.1",
22
- "ethereumjs-abi": "^0.6.5",
23
- "html-to-text": "^8.2.1",
34
+ "@reality.eth/contracts": "^3.2.26",
35
+ "ethers": "^6.0.0",
24
36
  "isomorphic-dompurify": "^0.23.0",
25
- "marked": "^4.1.1",
26
- "sprintf-js": "^1.1.1"
37
+ "marked": "^4.1.1"
27
38
  },
28
39
  "devDependencies": {
40
+ "bignumber.js": "^7.2.1",
29
41
  "chai": "^4.3.6",
30
42
  "chai-bignumber": "^2.0.2",
43
+ "esbuild": "0.28.2",
31
44
  "mocha": "^5.2.0",
32
- "truffle": "^4.0.1"
45
+ "truffle": "^4.0.1",
46
+ "typescript": "^6.0.3"
33
47
  },
34
48
  "repository": {
35
49
  "type": "git",
@@ -39,5 +53,5 @@
39
53
  "url": "https://github.com/RealityETH/monorepo/issues"
40
54
  },
41
55
  "homepage": "https://reality.eth.link",
42
- "gitHead": "8f89b31904d7c897a6e63fe9652c303a93d42ef0"
56
+ "gitHead": "1318f7f0cc58219bab1ee041431a0883f1b49b11"
43
57
  }
@@ -0,0 +1,574 @@
1
+ import { ethers } from 'ethers';
2
+ import { marked } from 'marked';
3
+ import DOMPurify from 'isomorphic-dompurify';
4
+
5
+ const QUESTION_MAX_OUTCOMES = 128;
6
+ const TEMPLATE_MAX_PLACEHOLDERS = 128;
7
+
8
+ (marked as any).setOptions({ headerIds: false });
9
+
10
+ export interface QuestionJSON {
11
+ title?: string;
12
+ title_text?: string;
13
+ title_html?: string;
14
+ type?: string;
15
+ category?: string;
16
+ lang?: string;
17
+ outcomes?: string[];
18
+ format?: string;
19
+ precision?: string;
20
+ decimals?: number;
21
+ has_invalid?: boolean;
22
+ errors?: Record<string, boolean>;
23
+ [key: string]: unknown;
24
+ }
25
+
26
+ export interface TemplatePart {
27
+ part: 'parameter' | 'array_parameter' | 'text';
28
+ part_index?: number;
29
+ label?: string;
30
+ value?: string;
31
+ }
32
+
33
+ export interface TemplateFieldDef {
34
+ label: string;
35
+ parts: TemplatePart[];
36
+ }
37
+
38
+ export interface TemplateConfig {
39
+ fields: Record<string, TemplateFieldDef>;
40
+ tags: Record<string, string>;
41
+ }
42
+
43
+ // Replace %s placeholders in order from args array
44
+ function vsprintf(template: string, args: string[]): string {
45
+ let i = 0;
46
+ return template.replace(/%s/g, () => (i < args.length ? args[i++] : ''));
47
+ }
48
+
49
+ // Convert marked.js HTML output to plain text.
50
+ // Handles headings, paragraphs, blockquotes, lists; strips inline tags; skips hr.
51
+ function htmlToText(html: string): string {
52
+ function decodeEntities(str: string): string {
53
+ return str.replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#39;|&apos;/g, "'");
54
+ }
55
+ function stripInline(h: string): string {
56
+ return decodeEntities(h.replace(/<[^>]+>/g, ''));
57
+ }
58
+
59
+ const blocks: { text: string; leading: number; trailing: number }[] = [];
60
+ const blockRe = /<(h[1-6]|p|blockquote|ul|ol)([^>]*)>([\s\S]*?)<\/\1>|<hr[^>]*\/?>/gi;
61
+ let match: RegExpExecArray | null;
62
+
63
+ while ((match = blockRe.exec(html)) !== null) {
64
+ const tag = match[1] ? match[1].toLowerCase() : 'hr';
65
+ const content = match[3] || '';
66
+ if (tag === 'hr') continue;
67
+
68
+ let text = '', leading = 2, trailing = 2;
69
+ if (/^h[1-6]$/.test(tag)) {
70
+ text = stripInline(content).trim();
71
+ leading = 3; trailing = 3;
72
+ } else if (tag === 'p') {
73
+ text = stripInline(content).trim();
74
+ } else if (tag === 'blockquote') {
75
+ const inner = content.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, (_m: string, pc: string) => stripInline(pc).trim()).trim();
76
+ text = '> ' + inner;
77
+ } else if (tag === 'ul') {
78
+ const items: string[] = [];
79
+ content.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_m: string, item: string) => {
80
+ items.push(' * ' + stripInline(item).trim());
81
+ return '';
82
+ });
83
+ text = items.join('\n');
84
+ } else if (tag === 'ol') {
85
+ const items: string[] = [];
86
+ let i = 1;
87
+ content.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_m: string, item: string) => {
88
+ items.push(' ' + i + '. ' + stripInline(item).trim());
89
+ i++;
90
+ return '';
91
+ });
92
+ text = items.join('\n');
93
+ }
94
+
95
+ if (text !== '') blocks.push({ text, leading, trailing });
96
+ }
97
+
98
+ if (blocks.length === 0) return '';
99
+ let result = blocks[0].text;
100
+ for (let i = 1; i < blocks.length; i++) {
101
+ result += '\n'.repeat(Math.max(blocks[i-1].trailing, blocks[i].leading)) + blocks[i].text;
102
+ }
103
+ return result;
104
+ }
105
+
106
+ export function delimiter(): string {
107
+ return '␟';
108
+ }
109
+
110
+ export function contentHash(template_id: number | string, opening_ts: number | string, content: string): string {
111
+ return ethers.solidityPackedKeccak256(
112
+ ['uint256', 'uint32', 'string'],
113
+ [template_id, opening_ts, content]
114
+ );
115
+ }
116
+
117
+ export function questionID(
118
+ template_id: number | string,
119
+ question: string,
120
+ arbitrator: string,
121
+ timeout: number | string,
122
+ opening_ts: number | string,
123
+ sender: string,
124
+ nonce: number | string,
125
+ min_bond?: string,
126
+ contract?: string,
127
+ version?: string
128
+ ): string {
129
+ if (typeof version === 'undefined') {
130
+ if (typeof min_bond === 'string' || typeof contract === 'string') {
131
+ throw Error('Version not defined');
132
+ }
133
+ }
134
+ if (!version) version = '2.0';
135
+ const vernum = parseInt(version);
136
+ if (isNaN(vernum) || vernum < 2 || vernum > 4) {
137
+ throw Error('Version not recognized');
138
+ }
139
+ if (vernum >= 3) {
140
+ if (typeof min_bond !== 'string') {
141
+ throw Error('min_bond not supplied or invalid. Required in v3. Pass "0x0" for a zero bond');
142
+ }
143
+ }
144
+
145
+ const content_hash = contentHash(template_id, opening_ts, question);
146
+
147
+ if (vernum < 3) {
148
+ return ethers.solidityPackedKeccak256(
149
+ ['uint256', 'address', 'uint32', 'address', 'uint256'],
150
+ [content_hash, arbitrator, timeout, sender, nonce]
151
+ );
152
+ } else {
153
+ const contractAddr = ('0x' + BigInt(contract!).toString(16).padStart(40, '0'));
154
+ return ethers.solidityPackedKeccak256(
155
+ ['uint256', 'address', 'uint32', 'uint256', 'address', 'address', 'uint256'],
156
+ [content_hash, arbitrator, timeout, min_bond, contractAddr, sender, nonce]
157
+ );
158
+ }
159
+ }
160
+
161
+ export function minNumber(qjson: QuestionJSON): bigint {
162
+ if (qjson['type'] !== 'int') return 0n;
163
+ return -maxNumber(qjson);
164
+ }
165
+
166
+ export function maxNumber(qjson: QuestionJSON): bigint {
167
+ const is_signed = qjson['type'] === 'int';
168
+ const uint256_max = (1n << 256n) - 1n;
169
+ let divby = 1n;
170
+ if (qjson['decimals']) {
171
+ divby = 10n ** BigInt(qjson['decimals']);
172
+ }
173
+ if (is_signed) divby *= 2n;
174
+ return uint256_max / divby;
175
+ }
176
+
177
+ export function arrayToBitmaskBigNumber(selections: boolean[]): bigint {
178
+ let bitstr = '';
179
+ for (let i = 0; i < selections.length; i++) {
180
+ bitstr = (selections[i] ? '1' : '0') + bitstr;
181
+ }
182
+ return bitstr ? BigInt('0b' + bitstr) : 0n;
183
+ }
184
+
185
+ export function padToBytes32(n: string, raw?: boolean): string {
186
+ while (n.length < 64) n = '0' + n;
187
+ return raw ? n : '0x' + n;
188
+ }
189
+
190
+ export function answerToBytes32(answer: any, qjson: QuestionJSON): string {
191
+ const qtype = qjson['type'];
192
+ if (qtype === 'hash') {
193
+ return bytes32ToString(answer, qjson);
194
+ }
195
+ if (qtype === 'multiple-select') {
196
+ answer = arrayToBitmaskBigNumber(answer as boolean[]);
197
+ }
198
+ let decimals = (qtype === 'uint') ? parseInt(String(qjson['decimals'])) : 0;
199
+ if (!decimals) decimals = 0;
200
+
201
+ let ans_hex: string;
202
+ if (qtype === 'int') {
203
+ const bi = BigInt(String(answer));
204
+ ans_hex = (bi < 0n ? bi + (1n << 256n) : bi).toString(16);
205
+ } else if (decimals > 0) {
206
+ ans_hex = ethers.parseUnits(String(answer), decimals).toString(16);
207
+ } else {
208
+ ans_hex = BigInt(answer).toString(16);
209
+ }
210
+ return padToBytes32(ans_hex);
211
+ }
212
+
213
+ export function bytes32ToString(bytes32str: string, qjson: QuestionJSON): string {
214
+ const qtype = qjson['type'];
215
+ let decimals = parseInt(String(qjson['decimals']));
216
+ if (!decimals) decimals = 0;
217
+ bytes32str = bytes32str.replace(/^0x/, '');
218
+
219
+ if (qtype === 'hash') {
220
+ return padToBytes32(bytes32str).toLowerCase();
221
+ }
222
+
223
+ const bi = BigInt('0x' + bytes32str);
224
+ let ans: string;
225
+ if (qtype === 'int') {
226
+ const threshold = 1n << 255n;
227
+ ans = (bi >= threshold ? bi - (1n << 256n) : bi).toString();
228
+ } else if (qtype === 'uint' || qtype === 'datetime') {
229
+ ans = bi.toString();
230
+ } else {
231
+ throw Error('Unrecognized answer type ' + qtype);
232
+ }
233
+
234
+ if (decimals > 0) {
235
+ const divisor = 10n ** BigInt(decimals);
236
+ const bigAns = BigInt(ans);
237
+ const whole = bigAns / divisor;
238
+ const frac = bigAns % divisor;
239
+ if (frac === 0n) {
240
+ ans = whole.toString();
241
+ } else {
242
+ const fracStr = frac.toString().padStart(decimals, '0').replace(/0+$/, '');
243
+ ans = whole.toString() + '.' + fracStr;
244
+ }
245
+ }
246
+ return ans;
247
+ }
248
+
249
+ export function convertTsToString(ts: number | { toNumber(): number }): string {
250
+ if (typeof (ts as any).toNumber === 'function') {
251
+ ts = (ts as any).toNumber();
252
+ }
253
+ const date = new Date();
254
+ date.setTime((ts as number) * 1000);
255
+ return date.toISOString();
256
+ }
257
+
258
+ export function secondsTodHms(sec: number | string): string {
259
+ const s = Number(sec);
260
+ const d = Math.floor(s / (3600 * 24));
261
+ const h = Math.floor(s % (3600 * 24) / 3600);
262
+ const m = Math.floor(s % (3600 * 24) % 3600 / 60);
263
+ const r = Math.floor(s % (3600 * 24) % 3600 % 60);
264
+
265
+ const dDisplay = d > 0 ? d + (d === 1 ? ' day ' : ' days ') : '';
266
+ const hDisplay = h > 0 ? h + (h === 1 ? ' hour ' : ' hours ') : '';
267
+ const mDisplay = m > 0 ? m + (m === 1 ? ' minute ' : ' minutes ') : '';
268
+ const sDisplay = r > 0 ? r + (r === 1 ? ' second' : ' seconds') : '';
269
+ return dDisplay + hDisplay + mDisplay + sDisplay;
270
+ }
271
+
272
+ export function parseQuestionJSON(data: string, errors_to_title?: boolean, vsprint_errors?: Record<string, boolean> | null): QuestionJSON {
273
+ data = data.replace(//g, '');
274
+
275
+ let question_json: QuestionJSON;
276
+ try {
277
+ question_json = JSON.parse(data);
278
+ } catch (e) {
279
+ question_json = {
280
+ title: '[Badly formatted question]: ' + data,
281
+ type: 'broken-question',
282
+ errors: { json_parse_failed: true },
283
+ };
284
+ }
285
+
286
+ if (question_json['outcomes'] && (question_json['outcomes'] as string[]).length > QUESTION_MAX_OUTCOMES) {
287
+ if (!question_json['errors']) question_json['errors'] = {};
288
+ question_json['errors']['too_many_outcomes'] = true;
289
+ }
290
+ if ('type' in question_json && question_json['type'] === 'datetime' && 'precision' in question_json) {
291
+ if (!(['Y', 'm', 'd', 'H', 'i', 's'].includes(String(question_json['precision'])))) {
292
+ if (!question_json['errors']) question_json['errors'] = {};
293
+ question_json['errors']['invalid_precision'] = true;
294
+ }
295
+ }
296
+
297
+ if (vsprint_errors && vsprint_errors['suspicious_extra_data']) {
298
+ if (!question_json['errors']) question_json['errors'] = {};
299
+ question_json['errors']['suspicious_extra_data'] = true;
300
+ }
301
+
302
+ if (errors_to_title) {
303
+ if ('errors' in question_json) {
304
+ const prependers: Record<string, string> = {
305
+ invalid_precision: 'Invalid date format',
306
+ too_many_outcomes: 'Too many outcomes',
307
+ };
308
+ for (const e in question_json['errors']) {
309
+ if (e in prependers) {
310
+ question_json['title'] = '[' + prependers[e] + '] ' + question_json['title'];
311
+ }
312
+ }
313
+ }
314
+ }
315
+
316
+ if (!question_json['format']) question_json['format'] = 'text/plain';
317
+
318
+ if (question_json['format'] === 'text/plain') {
319
+ question_json['title_text'] = question_json['title'];
320
+ } else if (question_json['format'] === 'text/markdown') {
321
+ try {
322
+ const safeMarkdown = DOMPurify.sanitize(String(question_json['title']), { USE_PROFILES: { html: false } });
323
+ if (safeMarkdown !== question_json['title']) {
324
+ if (!question_json['errors']) question_json['errors'] = {};
325
+ question_json['errors']['unsafe_markdown'] = true;
326
+ } else {
327
+ question_json['title_html'] = DOMPurify.sanitize((marked as any).parse(safeMarkdown).replace(/<img.*src="(.*?)".*alt="(.*?)".*\/?>/, '<a href="$1">$2</a>'));
328
+ question_json['title_text'] = htmlToText(String(question_json['title_html']));
329
+ }
330
+ } catch (e) {
331
+ if (!question_json['errors']) question_json['errors'] = {};
332
+ question_json['errors']['markdown_parse_failed'] = true;
333
+ }
334
+ } else {
335
+ if (!question_json['errors']) question_json['errors'] = {};
336
+ question_json['errors']['invalid_format'] = true;
337
+ }
338
+
339
+ if (errors_to_title) {
340
+ if ('errors' in question_json) {
341
+ const prependers: Record<string, string> = {
342
+ invalid_format: 'Invalid format',
343
+ unsafe_markdown: 'Unsafe markdown',
344
+ markdown_parse_failed: 'Bad markdown parse',
345
+ suspicious_extra_data: 'Suspicious Extra Data',
346
+ };
347
+ for (const e in question_json['errors']) {
348
+ if (e in prependers) {
349
+ question_json['title'] = '[' + prependers[e] + '] ' + question_json['title'];
350
+ }
351
+ }
352
+ }
353
+ }
354
+
355
+ return question_json;
356
+ }
357
+
358
+ export function populatedJSONForTemplate(template: string, question: string, errors_to_title?: boolean): QuestionJSON {
359
+ const qbits = question.split(delimiter());
360
+ const interpolated = vsprintf(template, qbits);
361
+
362
+ let vsprint_errors: Record<string, boolean> | null = null;
363
+ if (question !== '') {
364
+ const tweak_question = question + 'x';
365
+ const qbits2 = tweak_question.split(delimiter());
366
+ const interpolated2 = vsprintf(template, qbits2);
367
+ if (interpolated === interpolated2) {
368
+ vsprint_errors = { suspicious_extra_data: true };
369
+ }
370
+ }
371
+ return parseQuestionJSON(interpolated, errors_to_title, vsprint_errors);
372
+ }
373
+
374
+ export function encodeCustomText(params: Record<string, any>): string {
375
+ const items: string[] = [];
376
+ for (const p in params) {
377
+ let val = params[p];
378
+ if (typeof val === 'string') {
379
+ val = JSON.stringify(val).replace(/^"|"$/g, '');
380
+ } else if (typeof val === 'object' && val !== null) {
381
+ val = JSON.stringify(val).replace(/^\[/, '').replace(/\]$/, '');
382
+ } else {
383
+ val = null;
384
+ }
385
+ items.push(val);
386
+ }
387
+ return items.join(delimiter());
388
+ }
389
+
390
+ export function guessTemplateConfig(template: string): TemplateConfig {
391
+ const placeholder = ethers.solidityPackedKeccak256(['string'], [template]);
392
+ const arr_placeholder = ethers.solidityPackedKeccak256(['string'], [template + '_arr']);
393
+ const pl_arr = new Array(TEMPLATE_MAX_PLACEHOLDERS).fill(placeholder);
394
+ let interpolated = vsprintf(template, pl_arr);
395
+ interpolated = interpolated.replaceAll('[' + placeholder + ']', '"' + arr_placeholder + '"');
396
+
397
+ const fake_json = parseQuestionJSON(interpolated, false);
398
+
399
+ const meta = '__META' in fake_json ? (fake_json['__META'] as any) : {};
400
+ const labels: Record<string, string> = 'labels' in meta ? meta['labels'] : {};
401
+
402
+ const fields: Record<string, TemplateFieldDef> = {};
403
+ for (const k in fake_json) {
404
+ if (k === 'title_text' || k === 'title_html' || k === '__META') continue;
405
+
406
+ const fdef: TemplateFieldDef = {
407
+ label: k in labels ? labels[k] : k,
408
+ parts: [],
409
+ };
410
+ const regexp = new RegExp('(' + placeholder + '|' + arr_placeholder + ')');
411
+ const bits = String(fake_json[k]).split(regexp);
412
+ let part_i = 0;
413
+ for (const b of bits) {
414
+ if (b === '') continue;
415
+ if (b === placeholder || b === arr_placeholder) {
416
+ let part_label = k + '_' + part_i;
417
+ if (part_label in labels) part_label = labels[part_label];
418
+ fdef.parts.push(b === placeholder
419
+ ? { part: 'parameter', part_index: part_i, label: part_label }
420
+ : { part: 'array_parameter', part_index: part_i, label: part_label });
421
+ part_i++;
422
+ } else {
423
+ fdef.parts.push({ part: 'text', value: b });
424
+ }
425
+ }
426
+ fields[k] = fdef;
427
+ }
428
+
429
+ const tags: Record<string, string> = 'tags' in meta ? meta['tags'] : {};
430
+ return { fields, tags };
431
+ }
432
+
433
+ export function encodeText(qtype: string, txt: string, outcomes?: string[] | null, category?: string, lang?: string): string {
434
+ const def: Record<string, any> = { title: txt };
435
+ if (qtype === 'single-select' || qtype === 'multiple-select') {
436
+ def['outcomes'] = outcomes;
437
+ }
438
+ def['category'] = category;
439
+ def['lang'] = lang;
440
+ return encodeCustomText(def);
441
+ }
442
+
443
+ export function getInvalidValue(_question_json?: QuestionJSON): string {
444
+ return '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff';
445
+ }
446
+
447
+ export function getAnsweredTooSoonValue(_question_json?: QuestionJSON): string {
448
+ return '0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe';
449
+ }
450
+
451
+ export function getLanguage(question_json: QuestionJSON): string {
452
+ if (typeof question_json['lang'] === 'undefined' || question_json['lang'] === '') {
453
+ return 'en_US';
454
+ }
455
+ return String(question_json['lang']);
456
+ }
457
+
458
+ export function hasInvalidOption(question_json: QuestionJSON, _contract_version?: string): boolean {
459
+ return !('has_invalid' in question_json && !question_json['has_invalid']);
460
+ }
461
+
462
+ export function hasAnsweredTooSoonOption(_question_json: QuestionJSON, contract_version: string): boolean {
463
+ const bits = contract_version.split('.');
464
+ return parseInt(bits[0]) >= 3;
465
+ }
466
+
467
+ export function getAnswerString(question_json: QuestionJSON, answer: string | null): string {
468
+ if (answer === null) return 'null';
469
+ if (answer === getInvalidValue(question_json)) return 'Invalid';
470
+ if (answer === getAnsweredTooSoonValue(question_json)) return 'Answered too soon';
471
+
472
+ let label = '';
473
+ switch (question_json['type']) {
474
+ case 'uint':
475
+ case 'int':
476
+ case 'hash':
477
+ label = bytes32ToString(answer, question_json);
478
+ break;
479
+ case 'bool': {
480
+ const boolVal = BigInt(answer);
481
+ if (boolVal === 1n) label = 'Yes';
482
+ else if (boolVal === 0n) label = 'No';
483
+ break;
484
+ }
485
+ case 'single-select':
486
+ if (question_json['outcomes'] && (question_json['outcomes'] as string[]).length > 0) {
487
+ const idx = Number(BigInt(answer));
488
+ label = (question_json['outcomes'] as string[])[idx];
489
+ }
490
+ break;
491
+ case 'multiple-select':
492
+ if (question_json['outcomes'] && (question_json['outcomes'] as string[]).length > 0) {
493
+ const answer_bits = BigInt(answer).toString(2);
494
+ const entries: string[] = [];
495
+ for (let i = (question_json['outcomes'] as string[]).length - 1; i >= 0; i--) {
496
+ if (answer_bits[i] === '1') {
497
+ const idx = answer_bits.length - 1 - i;
498
+ entries.push((question_json['outcomes'] as string[])[idx]);
499
+ }
500
+ }
501
+ return entries.join(' / ');
502
+ }
503
+ break;
504
+ case 'datetime': {
505
+ let precision = 'd';
506
+ if ('precision' in question_json && ['Y', 'm', 'd', 'H', 'i', 's'].includes(String(question_json['precision']))) {
507
+ precision = String(question_json['precision']);
508
+ }
509
+ const ts = parseInt(bytes32ToString(answer, question_json));
510
+ const dateObj = new Date(ts * 1000);
511
+
512
+ const year = dateObj.getUTCFullYear();
513
+ const month = dateObj.getUTCMonth() + 1;
514
+ const date = dateObj.getUTCDate();
515
+ const hour = dateObj.getUTCHours();
516
+ const min = dateObj.getUTCMinutes();
517
+ const sec = dateObj.getUTCSeconds();
518
+
519
+ const needm = precision !== 'Y';
520
+ const needd = needm && precision !== 'm';
521
+ const needH = needd && precision !== 'd';
522
+ const needi = needH && precision !== 'H';
523
+ const needs = needi && precision !== 'i';
524
+
525
+ const hass = needs || sec > 0;
526
+ const hasi = needi || hass || min > 0;
527
+ const hasH = needH || hasi || hour > 0;
528
+ const hasd = needd || hasH || date > 1;
529
+ const hasm = needm || hasd || month > 1;
530
+
531
+ let invalid = false;
532
+ if (!needm && hasm) invalid = true;
533
+ if (!needd && hasd) invalid = true;
534
+ if (!needH && hasH) invalid = true;
535
+ if (!needi && hasi) invalid = true;
536
+ if (!needs && hass) invalid = true;
537
+
538
+ if (invalid) label = '[Invalid datetime]: ';
539
+
540
+ function pad2(n: number): string { return ('0' + n).slice(-2); }
541
+
542
+ label += year;
543
+ if (hasm) label += '-' + pad2(month);
544
+ if (hasd) label += '-' + pad2(date);
545
+ if (hasH) label += ' ' + pad2(hour);
546
+ if (hasi) label += ':' + pad2(min);
547
+ else if (hasH) label += 'hr';
548
+ if (hass) label += ':' + pad2(sec);
549
+ break;
550
+ }
551
+ }
552
+ return label;
553
+ }
554
+
555
+ export function commitmentID(question_id: string, answer_hash: string, bond: string | { toString(base: number): string }): string {
556
+ const bond_hex = (typeof bond === 'string') ? bond : ('0x' + bond.toString(16));
557
+ return ethers.solidityPackedKeccak256(
558
+ ['uint256', 'uint256', 'uint256'],
559
+ [question_id, answer_hash, bond_hex]
560
+ );
561
+ }
562
+
563
+ export function answerHash(answer_plaintext: string, nonce: string): string {
564
+ return ethers.solidityPackedKeccak256(
565
+ ['uint256', 'uint256'],
566
+ [answer_plaintext, nonce]
567
+ );
568
+ }
569
+
570
+ export function shortDisplayQuestionID(question_id: string): string {
571
+ const bits = question_id.split('-');
572
+ const qid = bits[bits.length - 1];
573
+ return qid.substring(2, 9) + '...' + qid.slice(-7);
574
+ }
@@ -0,0 +1,19 @@
1
+ import templateConfig from '@reality.eth/contracts/config/templates.json';
2
+ import templateConfig32 from '@reality.eth/contracts/config/templates_3.2.json';
3
+
4
+ export function defaultTemplateIDForType(template_type: string): number {
5
+ return (templateConfig.base_ids as Record<string, number>)[template_type];
6
+ }
7
+
8
+ export function defaultTemplateForType(template_type: string): string {
9
+ return (templateConfig.content as Record<string, string>)[String(defaultTemplateIDForType(template_type))];
10
+ }
11
+
12
+ export function preloadedTemplateContents(): Record<string, string> {
13
+ return templateConfig.content as Record<string, string>;
14
+ }
15
+
16
+ // v3.2+ contracts (and v2.2) use description instead of category and add template 5 (hash type).
17
+ export function preloadedTemplateContentsV32(): Record<string, string> {
18
+ return templateConfig32.content as Record<string, string>;
19
+ }