diff --git a/src/components/OracleQueryList/ItemDetails.tsx b/src/components/OracleQueryList/ItemDetails.tsx index 3ed996bd..6f3ea659 100644 --- a/src/components/OracleQueryList/ItemDetails.tsx +++ b/src/components/OracleQueryList/ItemDetails.tsx @@ -27,10 +27,15 @@ export function ItemDetails({ proposeOptions, } = item; - const valueToShow = maybeGetValueTextFromOptions(valueText, proposeOptions); + const valuesToShow = Array.isArray(valueText) + ? valueText + : [maybeGetValueTextFromOptions(valueText, proposeOptions)]; + const hasBond = bond !== null; const hasReward = reward !== null; + // console.log({ valuesToShow }); + const verifyDetails = ( {hasBond && ( @@ -43,7 +48,9 @@ export function ItemDetails({ )} Proposal - {valueToShow} + {valuesToShow.map((value, index) => ( + {value} + ))} {livenessEndsMilliseconds !== undefined && timeMilliseconds !== undefined ? ( @@ -90,7 +97,7 @@ export function ItemDetails({ Settled As - {valueToShow} + {valuesToShow.join(",")} ); diff --git a/src/components/OracleQueryTable/SettledCells.tsx b/src/components/OracleQueryTable/SettledCells.tsx index 65e4f5c7..34270f51 100644 --- a/src/components/OracleQueryTable/SettledCells.tsx +++ b/src/components/OracleQueryTable/SettledCells.tsx @@ -7,7 +7,9 @@ export function SettledCells({ valueText, proposeOptions, }: OracleQueryUI) { - const valueToShow = maybeGetValueTextFromOptions(valueText, proposeOptions); + const valuesToShow = Array.isArray(valueText) + ? valueText + : [maybeGetValueTextFromOptions(valueText, proposeOptions)]; return ( <> @@ -15,7 +17,7 @@ export function SettledCells({ {oracleType} - {valueToShow} + {valuesToShow.join(", ")} ); diff --git a/src/components/OracleQueryTable/VerifyCells.tsx b/src/components/OracleQueryTable/VerifyCells.tsx index 9fdf5f3e..528eae0d 100644 --- a/src/components/OracleQueryTable/VerifyCells.tsx +++ b/src/components/OracleQueryTable/VerifyCells.tsx @@ -14,11 +14,13 @@ export function VerifyCells({ chainId, tokenAddress, }: OracleQueryUI) { - const valueToShow = maybeGetValueTextFromOptions(valueText, proposeOptions); + const valuesToShow = Array.isArray(valueText) + ? valueText + : [maybeGetValueTextFromOptions(valueText, proposeOptions)]; return ( <> - {valueToShow} + {valuesToShow.join(",")} diff --git a/src/components/Panel/Actions/Actions.tsx b/src/components/Panel/Actions/Actions.tsx index ea29e6e9..24fdbaf1 100644 --- a/src/components/Panel/Actions/Actions.tsx +++ b/src/components/Panel/Actions/Actions.tsx @@ -56,7 +56,9 @@ export function Actions({ query }: Props) { const errors = [inputError, ...(primaryAction?.errors || [])].filter(Boolean); const actionsTitle = getActionsTitle(); const actionsIcon = pageIsSettled ? : ; - const valueToShow = maybeGetValueTextFromOptions(valueText, proposeOptions); + const valuesToShow = Array.isArray(valueText) + ? valueText + : [maybeGetValueTextFromOptions(valueText, proposeOptions)]; function getActionsTitle() { if (pageIsSettled) return "Settled as"; @@ -92,7 +94,7 @@ export function Actions({ query }: Props) { marginBottom: !pageIsSettled ? "20px" : "0px", }} > -

{valueToShow}

+

{valuesToShow.join(", ")}

)} {!pageIsSettled &&
} diff --git a/src/data/approvedIdentifiersTable.json b/src/data/approvedIdentifiersTable.json index 850ef78b..87da5f85 100644 --- a/src/data/approvedIdentifiersTable.json +++ b/src/data/approvedIdentifiersTable.json @@ -1951,4 +1951,4 @@ "url": "" } } -} +} \ No newline at end of file diff --git a/src/helpers/converters.ts b/src/helpers/converters.ts index aed502a4..06339c91 100644 --- a/src/helpers/converters.ts +++ b/src/helpers/converters.ts @@ -39,6 +39,9 @@ import { erc20ABI } from "wagmi"; import { formatBytes32String } from "./ethers"; import { getQueryMetaData } from "./queryParsing"; +export const MIN_INT256 = -(BigInt(2) ** BigInt(255)); +export const MAX_INT256 = BigInt(2) ** BigInt(255) - BigInt(1); + export type RequiredRequest = Omit< Request, "currency" | "bond" | "customLiveness" @@ -677,6 +680,24 @@ export function requestToOracleQuery(request: Request): OracleQueryUI { project: "Unknown", }; + if (exists(ancillaryData)) { + result.queryTextHex = ancillaryData; + result.queryText = safeDecodeHexString(ancillaryData); + } + + if (exists(identifier) && exists(result.queryText) && exists(requester)) { + const { title, description, umipUrl, umipNumber, project, proposeOptions } = + getQueryMetaData(identifier, result.queryText, requester); + result.title = title; + result.description = description; + result.project = project; + result.proposeOptions = proposeOptions; + result.moreInformation = makeMoreInformationList( + request, + umipNumber, + umipUrl, + ); + } if (exists(state)) { result.state = state; } @@ -696,34 +717,32 @@ export function requestToOracleQuery(request: Request): OracleQueryUI { } else { result.valueText = ethers.utils.formatEther(price); } + // we need the raw price for multiple values, because we need to parse this into + // multiple possible price values + } else if ( + exists(identifier) && + parseIdentifier(identifier) === "MULTIPLE_VALUES" + ) { + const price = settlementPrice ?? proposedPrice; + if (price === null || price === undefined || !result.proposeOptions) { + result.valueText = "-"; + } else { + // this is an array of strings now representings scores as uints + result.valueText = decodeMultipleQuery( + price.toString(), + result.proposeOptions.length, + ).toString(); + } } else { result.valueText = getPriceRequestValueText(proposedPrice, settlementPrice); } - if (exists(ancillaryData)) { - result.queryTextHex = ancillaryData; - result.queryText = safeDecodeHexString(ancillaryData); - } - let bytes32Identifier = undefined; if (exists(identifier)) { result.identifier = identifier; bytes32Identifier = formatBytes32String(identifier); } - if (exists(identifier) && exists(result.queryText) && exists(requester)) { - const { title, description, umipUrl, umipNumber, project, proposeOptions } = - getQueryMetaData(identifier, result.queryText, requester); - result.title = title; - result.description = description; - result.project = project; - result.proposeOptions = proposeOptions; - result.moreInformation = makeMoreInformationList( - request, - umipNumber, - umipUrl, - ); - } const { bond, eventBased } = getOOV2SpecificValues(request); if (exists(bond)) { result.bond = bond; @@ -1009,3 +1028,58 @@ ${rulesRegex[1]}`; return result; } + +// input user values as regular numbers +export function decodeMultipleQueryPriceAtIndex( + encodedPrice: bigint, + index: number, +): number { + if (index < 0 || index > 6) { + throw new Error("Index out of range"); + } + // Shift the bits of encodedPrice to the right by (32 * index) positions. + // This operation effectively moves the desired 32-bit segment to the least significant position. + // The bitwise AND operation with 0xffffffff ensures that only the least significant 32 bits are retained, + // effectively extracting the 32-bit value at the specified index. + return Number((encodedPrice >> BigInt(32 * index)) & BigInt(0xffffffff)); +} +export function encodeMultipleQuery(values: number[]): bigint { + if (values.length > 7) { + throw new Error("Maximum of 7 values allowed"); + } + + let encodedPrice = BigInt(0); + + for (let i = 0; i < values.length; i++) { + if (!Number.isInteger(values[i])) { + throw new Error("All values must be integers"); + } + if (values[i] > 0xffffffff || values[i] < 0) { + throw new Error("Values must be uint32 (0 <= value <= 2^32 - 1)"); + } + // Shift the current value to its correct position in the 256-bit field. + // Each value is a 32-bit unsigned integer, so we shift it by 32 bits times its index. + // This places the first value at the least significant bits and subsequent values + // at increasingly higher bit positions. + encodedPrice |= BigInt(values[i]) << BigInt(32 * i); + } + + return encodedPrice; +} +export function decodeMultipleQuery(price: string, length: number): number[] { + const result: number[] = []; + const bigIntPrice = BigInt(price); + + for (let i = 0; i < length; i++) { + const value = decodeMultipleQueryPriceAtIndex(bigIntPrice, i); + result.push(value); + } + return result; +} +export function isTooEarly(price: bigint): boolean { + return price === MIN_INT256; +} + +export function isUnresolvable(price: bigint): boolean { + return price === MAX_INT256; +} diff --git a/src/helpers/queryParsing.ts b/src/helpers/queryParsing.ts index 6e7c1094..d02a286f 100644 --- a/src/helpers/queryParsing.ts +++ b/src/helpers/queryParsing.ts @@ -1,9 +1,11 @@ +import assert from "assert"; import { earlyRequestMagicNumber } from "@/constants"; import approvedIdentifiers from "@/data/approvedIdentifiersTable"; import type { DropdownItem, MetaData } from "@/types"; import { formatEther } from "@/helpers"; import { chunk } from "lodash"; import type { ProjectName } from "@shared/constants"; +import * as s from "superstruct"; // hard coded known poly addresses: // https://github.com/UMAprotocol/protocol/blob/master/packages/monitor-v2/src/monitor-polymarket/common.ts#L474 @@ -301,6 +303,9 @@ export function getQueryMetaData( return tryParseMultipleChoiceQuery(decodedQueryText, projectName); } + if (decodedIdentifier === "MULTIPLE_VALUES") { + return tryParseMultipleValuesQuery(decodedQueryText, "Polymarket"); + } const identifierDetails = approvedIdentifiers[decodedIdentifier]; const isApprovedIdentifier = Boolean(identifierDetails); @@ -334,6 +339,75 @@ export function getQueryMetaData( }; } +const MultipleValuesQuery = s.object({ + // The title of the request + title: s.string(), + // Description of the request + description: s.string(), + // Values will be encoded into the settled price in the same order as the provided labels. The oracle UI will display each Label along with an input field. 7 labels maximum. + labels: s.array(s.string()), +}); +type MultipleValuesQuery = s.Infer; + +const isMultipleValuesQueryFormat = (q: unknown) => + s.is(q, MultipleValuesQuery); + +function decodeMultipleValuesQuery(decodedAncillaryData: string) { + const endOfObjectIndex = decodedAncillaryData.lastIndexOf("}"); + const maybeJson = + endOfObjectIndex > 0 + ? decodedAncillaryData.slice(0, endOfObjectIndex + 1) + : decodedAncillaryData; + + const json = JSON.parse(maybeJson); + if (!isMultipleValuesQueryFormat(json)) + throw new Error("Not a valid multiple values request"); + const query = json as MultipleValuesQuery; + assert( + query.labels.length <= 7, + "MULTIPLE_VALUES only support up to 7 labels", + ); + + return { + title: query.title, + description: query.description, + proposeOptions: query.labels.map((opt: string) => ({ + label: opt, + value: 0, + secondaryLabel: undefined, + })), + }; +} +function tryParseMultipleValuesQuery( + decodedQueryText: string, + projectName: ProjectName, +): MetaData { + try { + const result = { + ...decodeMultipleValuesQuery(decodedQueryText), + umipUrl: + "https://github.com/UMAprotocol/UMIPs/blob/master/UMIPs/umip-183.md", + umipNumber: "UMIP-183", + project: projectName, + }; + return result; + } catch (err) { + let description = `Error decoding description`; + if (err instanceof Error) { + description += `: ${err.message}`; + } + console.error(`Error parsing MULTIPLE_VALUES`, decodedQueryText, err); + return { + title: `MULTIPLE_VALUES`, + description, + umipUrl: + "https://github.com/UMAprotocol/UMIPs/blob/master/UMIPs/umip-183.md", + umipNumber: "UMIP-183", + project: projectName, + proposeOptions: [], + }; + } +} type MultipleChoiceQuery = { // Require a title string title: string; @@ -391,7 +465,7 @@ function tryParseMultipleChoiceQuery( umipUrl: "https://github.com/UMAprotocol/UMIPs/blob/master/UMIPs/umip-181.md", umipNumber: "UMIP-181", - project: "Unknown", + project: projectName, proposeOptions: makeMultipleChoiceOptions( makeMultipleChoiceYesOrNoOptions(), ), diff --git a/src/types/ui.ts b/src/types/ui.ts index f33e6cf0..e88f8ba2 100644 --- a/src/types/ui.ts +++ b/src/types/ui.ts @@ -76,7 +76,8 @@ export type OracleQueryUI = { queryText?: string; // for price requests the value text is null until a price is proposed. Then it is the proposed price. After a price is settled it is the settled price. // for assertions the value text is null until settlement, after which it is the `settlementResolution` field - valueText?: string | null; + valueText?: string | string[] | null; + timeUTC?: string; timeUNIX?: number; timeMilliseconds?: number; @@ -423,5 +424,5 @@ export type MetaData = { export type DropdownItem = { label: string; value: string | number; - secondaryLabel?: string; + secondaryLabel?: string | undefined; };