From 6d9fbd345c595b0ca27a2cced7fb1412d8156b1a Mon Sep 17 00:00:00 2001 From: Emanuele Cesena Date: Sun, 6 Oct 2024 18:12:25 -0700 Subject: [PATCH] risk: drift on chain policy --- anchor/src/client/drift.ts | 38 +++- playground/src/app/risk/page.tsx | 62 +++++- playground/src/app/trade/page.tsx | 87 ++------ playground/src/components/DynamicForm.tsx | 240 ++++++++++++++------- playground/src/components/ExplorerLink.tsx | 2 +- playground/src/constants/drift.tsx | 86 ++++++++ playground/src/constants/index.tsx | 1 + playground/src/data/glamRiskSchema.json | 89 ++++++++ 8 files changed, 452 insertions(+), 153 deletions(-) create mode 100644 playground/src/constants/drift.tsx create mode 100644 playground/src/constants/index.tsx create mode 100644 playground/src/data/glamRiskSchema.json diff --git a/anchor/src/client/drift.ts b/anchor/src/client/drift.ts index 45669e47..373e1678 100644 --- a/anchor/src/client/drift.ts +++ b/anchor/src/client/drift.ts @@ -15,6 +15,7 @@ import { initialize as _initialize, PositionDirection, BulkAccountLoader, + decodeUser, } from "@drift-labs/sdk"; import { BaseClient, ApiTxOptions } from "./base"; @@ -22,6 +23,7 @@ import { BaseClient, ApiTxOptions } from "./base"; const DRIFT_VAULT = new PublicKey( "JCNCMFXo5M5qwUPg2Utu1u6YWp3MbygxqBsBeXXJfrw" ); +const DRIFT_MARGIN_PRECISION = 10_000; const remainingAccountsForOrders = [ { @@ -204,6 +206,39 @@ export class DriftClient { return market; } + async fetchPolicyConfig(fund: any) { + let driftUserAccount; + if (fund) { + const [driftUserAddress] = this.getUser(fund.id); + const connection = this.base.provider.connection; + const info = await connection.getAccountInfo( + driftUserAddress, + connection.commitment + ); + if (info) { + driftUserAccount = decodeUser(info.data); + } + } + let delegate = driftUserAccount?.delegate; + if ( + delegate && + delegate.toBase58() === "11111111111111111111111111111111" + ) { + delegate = undefined; + } + return { + driftAccessControl: delegate ? 0 : 1, + driftDelegatedAccount: delegate || null, + driftMarketIndexesPerp: fund?.driftMarketIndexesPerp || [], + driftOrderTypes: fund?.driftOrderTypes || [], + driftMaxLeverage: driftUserAccount?.maxMarginRatio + ? DRIFT_MARGIN_PRECISION / driftUserAccount?.maxMarginRatio + : null, + driftEnableSpot: driftUserAccount?.isMarginTradingEnabled || false, + driftMarketIndexesSpot: fund?.driftMarketIndexesSpot || [], + }; + } + /* * API methods */ @@ -244,9 +279,8 @@ export class DriftClient { const manager = apiOptions.signer || this.base.getManager(); const [user] = this.getUser(fund, subAccountId); - const MARGIN_PRECISION = 10_000; // https://github.com/drift-labs/protocol-v2/blob/babed162b08b1fe34e49a81c5aa3e4ec0a88ecdf/programs/drift/src/math/constants.rs#L183-L184 - const marginRatio = MARGIN_PRECISION / maxLeverage; + const marginRatio = DRIFT_MARGIN_PRECISION / maxLeverage; const tx = await this.base.program.methods .driftUpdateUserCustomMarginRatio(subAccountId, marginRatio) diff --git a/playground/src/app/risk/page.tsx b/playground/src/app/risk/page.tsx index d87e1741..12ee69a2 100644 --- a/playground/src/app/risk/page.tsx +++ b/playground/src/app/risk/page.tsx @@ -11,16 +11,42 @@ import { IntegrationsList } from "./components/integrations-list"; import { integrations } from "./data"; import PageContentWrapper from "@/components/PageContentWrapper"; import { useGlam } from "@glam/anchor/react"; +import DynamicForm from "@/components/DynamicForm"; +import schema from "../../data/glamRiskSchema.json"; +import { useForm } from "react-hook-form"; -export default function Risk() { +export default async function Risk() { //@ts-ignore - const { allFunds, activeFund } = useGlam(); + const { allFunds, activeFund, glamClient } = useGlam(); const fundId = activeFund?.addressStr; const fund: any = fundId ? (allFunds || []).find((f: any) => f.idStr === fundId) : undefined; + const driftForm = useForm({ + defaultValues: { + driftAccessControl: 0, + driftDelegatedAccount: null, + driftMarketIndexesPerp: [], + driftOrderTypes: [], + driftMaxLeverage: null, + driftEnableSpot: false, + driftMarketIndexesSpot: [], + }, + }); + + useEffect(() => { + const fetchDriftData = async () => { + const driftPolicy = await glamClient.drift.fetchPolicyConfig(fund); + for (const [key, value] of Object.entries(driftPolicy)) { + driftForm.setValue(key, value); + } + }; + + fetchDriftData(); + }, [fund]); + //TODO: load on chain data and remove this whole useEffect const [rerender, setRerender] = useState(0); useEffect(() => { @@ -47,6 +73,23 @@ export default function Risk() { setRerender(rerender + 1); }, [fundId]); + // form behavior, change visible fields based on access control + const watchDriftAccessControl = driftForm.watch("driftAccessControl"); + useEffect(() => { + if (watchDriftAccessControl === 0) { + schema.drift.fields.driftDelegatedAccount["x-hidden"] = false; + schema.drift.fields.driftMarketIndexesPerp["x-hidden"] = true; + schema.drift.fields.driftOrderTypes["x-hidden"] = true; + schema.drift.fields.driftMarketIndexesSpot["x-hidden"] = true; + } else { + schema.drift.fields.driftDelegatedAccount["x-hidden"] = true; + schema.drift.fields.driftMarketIndexesPerp["x-hidden"] = false; + schema.drift.fields.driftOrderTypes["x-hidden"] = false; + schema.drift.fields.driftMarketIndexesSpot["x-hidden"] = false; + } + setRerender(rerender + 1); + }, [watchDriftAccessControl]); + return (
@@ -68,7 +111,20 @@ export default function Risk() {
-
+
+ { + console.log("submit", data); + }} + onWatch={(data: any) => { + console.log("watch", data); + }} + /> +
); diff --git a/playground/src/app/trade/page.tsx b/playground/src/app/trade/page.tsx index 08145fea..789ee91f 100644 --- a/playground/src/app/trade/page.tsx +++ b/playground/src/app/trade/page.tsx @@ -73,69 +73,14 @@ import { OrderType, PositionDirection, } from "@drift-labs/sdk"; +import { + DRIFT_ORDER_TYPES, + DRIFT_PERP_MARKETS, + DRIFT_SPOT_MARKETS, +} from "@/constants"; -const spotMarkets = [{ label: "SOL/USDC", value: "SOL-USDC" }] as const; - -const ORDER_TYPES: [string, ...string[]] = [ - "Market", - "Limit", - "Trigger Market", - "Trigger Limit", - "Oracle", -]; - -const PERP_MARKETS: [string, ...string[]] = [ - "SOL-PERP", - "BTC-PERP", - "ETH-PERP", - "APT-PERP", - "1MBONK-PERP", - "MATIC-PERP", - "ARB-PERP", - "DOGE-PERP", - "BNB-PERP", - "SUI-PERP", - "1MPEPE-PERP", - "OP-PERP", - "RENDER-PERP", - "XRP-PERP", - "HNT-PERP", - "INJ-PERP", - "LINK-PERP", - "RLB-PERP", - "PYTH-PERP", - "TIA-PERP", - "JTO-PERP", - "SEI-PERP", - "AVAX-PERP", - "WIF-PERP", - "JUP-PERP", - "DYM-PERP", - "TAO-PERP", - "W-PERP", - "KMNO-PERP", - "TNSR-PERP", - "DRIFT-PERP", - "CLOUD-PERP", - "IO-PERP", - "ZEX-PERP", - "POPCAT-PERP", - "1KWEN-PERP", - "TRUMP-WIN-2024-BET", - "KAMALA-POPULAR-VOTE-2024-BET", - "FED-CUT-50-SEPT-2024-BET", - "REPUBLICAN-POPULAR-AND-WIN-BET", - "BREAKPOINT-IGGYERIC-BET", - "DEMOCRATS-WIN-MICHIGAN-BET", - "TON-PERP", - "LANDO-F1-SGP-WIN-BET", - "MOTHER-PERP", - "MOODENG-PERP", - "WARWICK-FIGHT-WIN-BET", -]; - -const perpsMarkets = PERP_MARKETS.map((x) => ({ label: x, value: x })); -const orderTypes = ORDER_TYPES.map((x) => ({ label: x, value: x })); +const spotMarkets = DRIFT_SPOT_MARKETS.map((x) => ({ label: x, value: x })); +const perpsMarkets = DRIFT_PERP_MARKETS.map((x) => ({ label: x, value: x })); const swapSchema = z.object({ venue: z.enum(["Jupiter"]), @@ -157,14 +102,8 @@ const swapSchema = z.object({ const spotSchema = z.object({ venue: z.enum(["Jupiter", "Drift"]), - spotMarket: z.enum(["SOL-USDC"]), - spotType: z.enum([ - "Market", - "Limit", - "Trigger Market", - "Trigger Limit", - "Oracle", - ]), + spotMarket: z.enum(DRIFT_SPOT_MARKETS), + spotType: z.enum(DRIFT_ORDER_TYPES), side: z.enum(["Buy", "Sell"]), limitPrice: z.number().nonnegative(), size: z.number().nonnegative(), @@ -177,8 +116,8 @@ const spotSchema = z.object({ const perpsSchema = z.object({ venue: z.enum(["Drift"]), - perpsMarket: z.enum(PERP_MARKETS), - perpsType: z.enum(ORDER_TYPES), + perpsMarket: z.enum(DRIFT_PERP_MARKETS), + perpsType: z.enum(DRIFT_ORDER_TYPES), side: z.enum(["Buy", "Sell"]), limitPrice: z.number().nonnegative(), size: z.number().nonnegative(), @@ -441,7 +380,7 @@ export default function Trade() { values.side === "Buy" ? PositionDirection.LONG : PositionDirection.SHORT, - marketIndex: PERP_MARKETS.indexOf(values.perpsMarket), + marketIndex: DRIFT_PERP_MARKETS.indexOf(values.perpsMarket), baseAssetAmount: new anchor.BN(values.size * LAMPORTS_PER_SOL), price: new anchor.BN(values.limitPrice), // set a very low limit price }); @@ -1428,7 +1367,7 @@ export default function Trade() {
diff --git a/playground/src/components/DynamicForm.tsx b/playground/src/components/DynamicForm.tsx index 202df9de..3b43886e 100644 --- a/playground/src/components/DynamicForm.tsx +++ b/playground/src/components/DynamicForm.tsx @@ -45,10 +45,12 @@ import { CommandItem, CommandList, } from "@/components/ui/command"; +import { ScrollArea } from "@/components/ui/scroll-area"; import { format } from "date-fns"; import { cn } from "@/lib/utils"; import * as openfundsConstants from "@/utils/openfundsConstants"; +import * as constants from "@/constants"; import { Tooltip, TooltipContent, @@ -115,45 +117,83 @@ interface DynamicFormProps { schema: Schema; isNested?: boolean; groups?: string[]; + onSubmit?: (data: FormData) => void; + onWatch?: (data: FormData) => void; + formData?: any; } type FormData = { [key: string]: any; }; -const DynamicForm: React.FC = ({ schema, isNested = false, groups = [] }) => { +const defaultOnData = (data: FormData) => { + console.log("Form data:", data); +}; + +const DynamicForm: React.FC = ({ + schema, + isNested = false, + groups = [], + onSubmit = defaultOnData, + onWatch = defaultOnData, + formData = undefined, +}) => { const [formSchema] = useState(schema); - const [enumValues, setEnumValues] = useState>({}); + const [enumValues, setEnumValues] = useState< + Record + >({}); const validate = useMemo(() => ajv.compile(schema), [schema]); useEffect(() => { const fetchEnumValues = () => { - const values: Record = {}; + const values: Record< + string, + { label: string; value: string | number }[] + > = {}; if (isNested && groups.length) { - groups.forEach(group => { + groups.forEach((group) => { if (schema[group]?.fields) { const fields = schema[group].fields as Record; - for (const [key, field] of Object.entries(schema[group].fields as Record)) { + for (const [key, field] of Object.entries( + schema[group].fields as Record + )) { if (field["x-enumValues"]) { - const enumData = (openfundsConstants as any)[field["x-enumValues"]]; + const enumData = + (openfundsConstants as any)[field["x-enumValues"]] || + (constants as any)[field["x-enumValues"]]; if (Array.isArray(enumData)) { - values[key] = enumData.map((item: any) => ({ - label: item[field["x-enumValuesLabel"] as string] || item.label, - value: item[field["x-enumValuesValue"] as string] || item.value, - })); + if ( + field["x-enumValuesLabel"] && + field["x-enumValuesValue"] + ) { + values[key] = enumData.map((item: any) => ({ + label: + item[field["x-enumValuesLabel"] as string] || + item.label, + value: + item[field["x-enumValuesValue"] as string] || + item.value, + })); + } else { + values[key] = enumData.map((item: string, j) => ({ + label: item, + value: j, + })); + } } } else if (field.enum) { - values[key] = field.enum.map((item: string) => ({ + values[key] = field.enum.map((item: string, j) => ({ label: item, - value: item, + value: j, })); } } } }); - } else if (schema.fields) { // Add this check + } else if (schema.fields) { + // Add this check for (const [key, field] of Object.entries(schema.fields)) { if (field["x-enumValues"]) { const enumData = (openfundsConstants as any)[field["x-enumValues"]]; @@ -164,9 +204,9 @@ const DynamicForm: React.FC = ({ schema, isNested = false, gro })); } } else if (field.enum) { - values[key] = field.enum.map((item: string) => ({ + values[key] = field.enum.map((item: string, j) => ({ label: item, - value: item, + value: j, })); } } @@ -180,54 +220,53 @@ const DynamicForm: React.FC = ({ schema, isNested = false, gro fetchEnumValues(); }, [schema, isNested, groups]); - const form = useForm({ - resolver: async (values) => { - const valid = validate(values); - const errors: any = {}; + const form = formData + ? formData + : useForm({ + resolver: async (values) => { + const valid = validate(values); + const errors: any = {}; - if (!valid) { - validate.errors?.forEach((error) => { - let fieldKey = error.instancePath.substring(1); + if (!valid) { + validate.errors?.forEach((error) => { + let fieldKey = error.instancePath.substring(1); - // Handle required fields error where `instancePath` might be empty - if (error.keyword === "required") { - fieldKey = error.params.missingProperty; + // Handle required fields error where `instancePath` might be empty + if (error.keyword === "required") { + fieldKey = error.params.missingProperty; + } + + // @ts-ignore + const fieldSchema = schema.fields[fieldKey]; + const customError = fieldSchema?.["x-error"]; + const requiredError = + error.keyword === "required" + ? `${fieldSchema?.title || fieldKey} is required.` + : ""; + + errors[fieldKey] = { + type: error.keyword, + message: [customError, requiredError].filter(Boolean).join(" "), + }; + }); } - // @ts-ignore - const fieldSchema = schema.fields[fieldKey]; - const customError = fieldSchema?.["x-error"]; - const requiredError = - error.keyword === "required" - ? `${fieldSchema?.title || fieldKey} is required.` - : ""; - - errors[fieldKey] = { - type: error.keyword, - message: [customError, requiredError].filter(Boolean).join(" "), + return { + values: valid ? values : {}, + errors: valid ? {} : errors, }; - }); - } - - return { - values: valid ? values : {}, - errors: valid ? {} : errors, - }; - }, - defaultValues: {}, - }); - - const onSubmit = (data: FormData) => { - console.log("Form data:", data); - }; + }, + defaultValues: {}, + }); if (!formSchema) { return
Loading...
; } const sortedFields = (fields: Record) => - Object.entries(fields) - .sort(([, a], [, b]) => (a["x-order"] || 0) - (b["x-order"] || 0)); + Object.entries(fields).sort( + ([, a], [, b]) => (a["x-order"] || 0) - (b["x-order"] || 0) + ); const renderField = (key: string, field: SchemaField) => { const isRequired = schema.required?.includes(key); @@ -336,7 +375,7 @@ const DynamicForm: React.FC = ({ schema, isNested = false, gro const renderComponent = ( key: string, schemaField: SchemaField, - field: any, + field: any ) => { const options = enumValues[key] || []; const placeholder = schemaField["x-placeholder"] || ""; @@ -392,7 +431,7 @@ const DynamicForm: React.FC = ({ schema, isNested = false, gro {options.map((option) => ( - + {option.label} ))} @@ -414,8 +453,13 @@ const DynamicForm: React.FC = ({ schema, isNested = false, gro > {options.map((option) => (
- - {option.label} + + + {option.label} +
))} @@ -431,8 +475,8 @@ const DynamicForm: React.FC = ({ schema, isNested = false, gro const date = field.value ? new Date(field.value) : schemaField.default - ? new Date(schemaField.default) - : undefined; + ? new Date(schemaField.default) + : undefined; return ( @@ -442,7 +486,7 @@ const DynamicForm: React.FC = ({ schema, isNested = false, gro variant={"outline"} className={cn( "w-full pl-3 text-left font-normal", - !date && "text-muted-foreground", + !date && "text-muted-foreground" )} > {date ? format(date, "PPP") : placeholder || "Pick a date"} @@ -474,12 +518,12 @@ const DynamicForm: React.FC = ({ schema, isNested = false, gro role="combobox" className={cn( "w-full justify-between", - !field.value && "text-muted-foreground", + !field.value && "text-muted-foreground" )} > {field.value ? options.find((option) => option.value === field.value) - ?.label + ?.label : placeholder || "Select an option"}{" "} {/* Use placeholder */} @@ -506,7 +550,7 @@ const DynamicForm: React.FC = ({ schema, isNested = false, gro "mr-2 h-4 w-4", option.value === field.value ? "opacity-100" - : "opacity-0", + : "opacity-0" )} /> {option.label} @@ -519,6 +563,56 @@ const DynamicForm: React.FC = ({ schema, isNested = false, gro ); + case "checklist": + return ( + + + { + return ( + + {options.map((item) => ( + { + return ( + + + { + return checked + ? field.onChange( + [...field.value, item.value].sort() + ) + : field.onChange( + (field.value || []).filter( + (value: number) => + value !== item.value + ) + ); + }} + /> + + + {item.label} + + + ); + }} + /> + ))} + + ); + }} + /> + + + ); default: return ( = ({ schema, isNested = false, gro
{isNested && groups.length - ? groups.map(group => - schema[group]?.fields - ? sortedFields(schema[group].fields).map(([key, field]) => - renderField(key, field) - ) - : null - ) - : formSchema.fields // Add this check - ? sortedFields(formSchema.fields).map(([key, field]) => + ? groups.map((group) => + schema[group]?.fields + ? sortedFields(schema[group].fields).map(([key, field]) => + renderField(key, field) + ) + : null + ) + : formSchema.fields // Add this check + ? sortedFields(formSchema.fields).map(([key, field]) => renderField(key, field) ) - : null} -
diff --git a/playground/src/components/ExplorerLink.tsx b/playground/src/components/ExplorerLink.tsx index b8f7dc5c..e2006a15 100644 --- a/playground/src/components/ExplorerLink.tsx +++ b/playground/src/components/ExplorerLink.tsx @@ -23,7 +23,7 @@ export function ExplorerLink({ explorer?: string; }) { const cluster = useCluster(); - let href = cluster.getExplorerUrl(path); + let href = path.startsWith("http") ? path : cluster.getExplorerUrl(path); if (explorer == "solana.fm") { href = href .replace("explorer.solana.com", "solana.fm") diff --git a/playground/src/constants/drift.tsx b/playground/src/constants/drift.tsx new file mode 100644 index 00000000..68de85df --- /dev/null +++ b/playground/src/constants/drift.tsx @@ -0,0 +1,86 @@ +export const DRIFT_ORDER_TYPES: [string, ...string[]] = [ + "Market", + "Limit", + "Trigger Market", + "Trigger Limit", + "Oracle", +]; + +export const DRIFT_PERP_MARKETS: [string, ...string[]] = [ + "SOL-PERP", + "BTC-PERP", + "ETH-PERP", + "APT-PERP", + "1MBONK-PERP", + "MATIC-PERP", + "ARB-PERP", + "DOGE-PERP", + "BNB-PERP", + "SUI-PERP", + "1MPEPE-PERP", + "OP-PERP", + "RENDER-PERP", + "XRP-PERP", + "HNT-PERP", + "INJ-PERP", + "LINK-PERP", + "RLB-PERP", + "PYTH-PERP", + "TIA-PERP", + "JTO-PERP", + "SEI-PERP", + "AVAX-PERP", + "WIF-PERP", + "JUP-PERP", + "DYM-PERP", + "TAO-PERP", + "W-PERP", + "KMNO-PERP", + "TNSR-PERP", + "DRIFT-PERP", + "CLOUD-PERP", + "IO-PERP", + "ZEX-PERP", + "POPCAT-PERP", + "1KWEN-PERP", + "TRUMP-WIN-2024-BET", + "KAMALA-POPULAR-VOTE-2024-BET", + "FED-CUT-50-SEPT-2024-BET", + "REPUBLICAN-POPULAR-AND-WIN-BET", + "BREAKPOINT-IGGYERIC-BET", + "DEMOCRATS-WIN-MICHIGAN-BET", + "TON-PERP", + "LANDO-F1-SGP-WIN-BET", + "MOTHER-PERP", + "MOODENG-PERP", + "WARWICK-FIGHT-WIN-BET", +]; + +export const DRIFT_SPOT_MARKETS: [string, ...string[]] = [ + "SOL/USDC", + "mSOL/USDC", + "wBTC/USDC", + "wETH/USDC", + "USDT/USDC", + "jitoSOL/USDC", + "PYTH/USDC", + "bSOL/USDC", + "JTO/USDC", + "WIF/USDC", + "JUP/USDC", + "RENDER/USDC", + "W/USDC", + "TNSR/USDC", + "DRIFT/USDC", + "INF/USDC", + "dSOL/USDC", + "USDY/USDC", + "JLP/USDC", + "POPCAT/USDC", + "CLOUD/USDC", + "PYUSD/USDC", + "USDe/USDC", + "sUSDe/USDC", + "BNSOL/USDC", + "MOTHER/USDC", +]; diff --git a/playground/src/constants/index.tsx b/playground/src/constants/index.tsx new file mode 100644 index 00000000..5dffab21 --- /dev/null +++ b/playground/src/constants/index.tsx @@ -0,0 +1 @@ +export * from "./drift"; diff --git a/playground/src/data/glamRiskSchema.json b/playground/src/data/glamRiskSchema.json new file mode 100644 index 00000000..0ed6bcc0 --- /dev/null +++ b/playground/src/data/glamRiskSchema.json @@ -0,0 +1,89 @@ +{ + "$id": "http://glam.systems/playground/schemas/glamFormSchema.json", + "type": "object", + "drift": { + "type": "object", + "fields": { + "driftAccessControl": { + "x-id": "d1", + "type": "number", + "title": "Access Control Model", + "description": " ", + "x-order": 1, + "x-component": "radio", + "enum": ["Drift delegated account", "GLAM fine grain access control"], + "readOnly": false, + "x-hidden": false + }, + "driftDelegatedAccount": { + "x-id": "d2", + "type": "string", + "title": "Drift Delegated Account", + "description": "This account can sign into Drift app and trade on any market", + "x-order": 2, + "x-component": "input", + "readOnly": false, + "x-hidden": false + }, + "driftMarketIndexesPerp": { + "x-id": "d3", + "type": "array", + "title": "Perp Markets", + "description": "Enable only selected markets. This requires GLAM fine grain access control", + "x-order": 3, + "x-component": "checklist", + "x-enumSource": "constants/drift.tsx", + "x-enumValues": "DRIFT_PERP_MARKETS", + "readOnly": false, + "x-hidden": false, + "x-enforced": false + }, + "driftOrderTypes": { + "x-id": "d4", + "type": "array", + "title": "Order Types", + "description": "Enable only selected order types. This requires GLAM fine grain access control", + "x-order": 4, + "x-component": "checklist", + "x-enumSource": "constants/drift.tsx", + "x-enumValues": "DRIFT_ORDER_TYPES", + "readOnly": false, + "x-hidden": false, + "x-enforced": false + }, + "driftMaxLeverage": { + "x-id": "d5", + "type": "number", + "title": "Max Leverage", + "description": " ", + "x-order": 5, + "x-component": "input", + "readOnly": false, + "x-hidden": false + }, + "driftEnableSpot": { + "x-id": "d1", + "type": "boolean", + "title": "Enable Spot Trading", + "description": " ", + "x-order": 6, + "x-component": "switch", + "readOnly": false, + "x-hidden": false + }, + "driftMarketIndexesSpot": { + "x-id": "d7", + "type": "string", + "title": "Spot Markets", + "description": "Enable only selected markets. This requires GLAM fine grain access control", + "x-order": 7, + "x-component": "checklist", + "x-enumSource": "constants/drift.tsx", + "x-enumValues": "DRIFT_SPOT_MARKETS", + "readOnly": false, + "x-hidden": false, + "x-enforced": false + } + } + } +}