Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add multiple choice query parsing #207

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions src/components/OracleQueryList/ItemDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
<ItemDetailsWrapper>
{hasBond && (
Expand All @@ -43,7 +48,9 @@ export function ItemDetails({
)}
<ItemDetailsInnerWrapper>
<ItemDetailsText>Proposal</ItemDetailsText>
<ItemDetailsText>{valueToShow}</ItemDetailsText>
{valuesToShow.map((value, index) => (
<ItemDetailsText key={index}>{value}</ItemDetailsText>
))}
</ItemDetailsInnerWrapper>
{livenessEndsMilliseconds !== undefined &&
timeMilliseconds !== undefined ? (
Expand Down Expand Up @@ -90,7 +97,7 @@ export function ItemDetails({
<ItemDetailsWrapper>
<ItemDetailsInnerWrapper>
<ItemDetailsText>Settled As</ItemDetailsText>
<ItemDetailsText>{valueToShow}</ItemDetailsText>
<ItemDetailsText>{valuesToShow.join(",")}</ItemDetailsText>
</ItemDetailsInnerWrapper>
</ItemDetailsWrapper>
);
Expand Down
6 changes: 4 additions & 2 deletions src/components/OracleQueryTable/SettledCells.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ export function SettledCells({
valueText,
proposeOptions,
}: OracleQueryUI) {
const valueToShow = maybeGetValueTextFromOptions(valueText, proposeOptions);
const valuesToShow = Array.isArray(valueText)
? valueText
: [maybeGetValueTextFromOptions(valueText, proposeOptions)];

return (
<>
<TD>
<Text>{oracleType}</Text>
</TD>
<TD>
<Text>{valueToShow}</Text>
<Text>{valuesToShow.join(", ")}</Text>
</TD>
</>
);
Expand Down
6 changes: 4 additions & 2 deletions src/components/OracleQueryTable/VerifyCells.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<>
<TD>
<Text>{valueToShow}</Text>
<Text>{valuesToShow.join(",")}</Text>
</TD>
<TD>
<Text>
Expand Down
6 changes: 4 additions & 2 deletions src/components/Panel/Actions/Actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ export function Actions({ query }: Props) {
const errors = [inputError, ...(primaryAction?.errors || [])].filter(Boolean);
const actionsTitle = getActionsTitle();
const actionsIcon = pageIsSettled ? <Settled /> : <Pencil />;
const valueToShow = maybeGetValueTextFromOptions(valueText, proposeOptions);
const valuesToShow = Array.isArray(valueText)
? valueText
: [maybeGetValueTextFromOptions(valueText, proposeOptions)];

function getActionsTitle() {
if (pageIsSettled) return "Settled as";
Expand Down Expand Up @@ -92,7 +94,7 @@ export function Actions({ query }: Props) {
marginBottom: !pageIsSettled ? "20px" : "0px",
}}
>
<p className="sm:text-lg font-semibold">{valueToShow}</p>
<p className="sm:text-lg font-semibold">{valuesToShow.join(", ")}</p>
</div>
)}
{!pageIsSettled && <Details {...query} />}
Expand Down
2 changes: 1 addition & 1 deletion src/data/approvedIdentifiersTable.json
Original file line number Diff line number Diff line change
Expand Up @@ -1951,4 +1951,4 @@
"url": ""
}
}
}
}
110 changes: 92 additions & 18 deletions src/helpers/converters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
76 changes: 75 additions & 1 deletion src/helpers/queryParsing.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<typeof MultipleValuesQuery>;

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;
Expand Down Expand Up @@ -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(),
),
Expand Down
5 changes: 3 additions & 2 deletions src/types/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -423,5 +424,5 @@ export type MetaData = {
export type DropdownItem = {
label: string;
value: string | number;
secondaryLabel?: string;
secondaryLabel?: string | undefined;
};