json schema
This commit is contained in:
@@ -1,15 +1,22 @@
|
|||||||
import Editor, { loader } from "@monaco-editor/react";
|
import Editor, { loader, useMonaco } from "@monaco-editor/react";
|
||||||
import { FC, useEffect, useState } from "react";
|
import { FC, useCallback, useEffect, useState } from "react";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
|
|
||||||
import dark from "../../theme/editor/amy.json";
|
import dark from "../../theme/editor/amy.json";
|
||||||
import light from "../../theme/editor/xcode_default.json";
|
import light from "../../theme/editor/xcode_default.json";
|
||||||
import { useSnapshot } from "valtio";
|
import { useSnapshot } from "valtio";
|
||||||
import state, { parseJSON, prepareState, TransactionState } from "../../state";
|
import state, {
|
||||||
|
prepareState,
|
||||||
|
transactionsData,
|
||||||
|
TransactionState,
|
||||||
|
} from "../../state";
|
||||||
import Text from "../Text";
|
import Text from "../Text";
|
||||||
import Flex from "../Flex";
|
import Flex from "../Flex";
|
||||||
import { Link } from "..";
|
import { Link } from "..";
|
||||||
import { showAlert } from "../../state/actions/showAlert";
|
import { showAlert } from "../../state/actions/showAlert";
|
||||||
|
import { parseJSON } from "../../utils/json";
|
||||||
|
import { extractSchemaProps } from "../../utils/schema";
|
||||||
|
import amountSchema from "../../content/amount-schema.json";
|
||||||
|
|
||||||
loader.config({
|
loader.config({
|
||||||
paths: {
|
paths: {
|
||||||
@@ -30,8 +37,8 @@ export const TxJson: FC<JsonProps> = ({
|
|||||||
header,
|
header,
|
||||||
setState,
|
setState,
|
||||||
}) => {
|
}) => {
|
||||||
const { editorSettings } = useSnapshot(state);
|
const { editorSettings, accounts } = useSnapshot(state);
|
||||||
const { editorValue = value } = txState;
|
const { editorValue = value, selectedTransaction } = txState;
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const [hasUnsaved, setHasUnsaved] = useState(false);
|
const [hasUnsaved, setHasUnsaved] = useState(false);
|
||||||
|
|
||||||
@@ -73,6 +80,82 @@ export const TxJson: FC<JsonProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const path = `file:///${header}`;
|
const path = `file:///${header}`;
|
||||||
|
const monaco = useMonaco();
|
||||||
|
|
||||||
|
const getSchemas = useCallback((): any[] => {
|
||||||
|
const tt = selectedTransaction?.value;
|
||||||
|
const txObj = transactionsData.find(td => td.TransactionType === tt);
|
||||||
|
|
||||||
|
let genericSchemaProps: any;
|
||||||
|
if (txObj) {
|
||||||
|
genericSchemaProps = extractSchemaProps(txObj);
|
||||||
|
} else {
|
||||||
|
genericSchemaProps = transactionsData.reduce(
|
||||||
|
(cumm, td) => ({
|
||||||
|
...cumm,
|
||||||
|
...extractSchemaProps(td),
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
uri: "file:///main-schema.json", // id of the first schema
|
||||||
|
fileMatch: ["**.json"], // associate with our model
|
||||||
|
schema: {
|
||||||
|
title: header,
|
||||||
|
type: "object",
|
||||||
|
required: ["TransactionType", "Account"],
|
||||||
|
properties: {
|
||||||
|
...genericSchemaProps,
|
||||||
|
TransactionType: {
|
||||||
|
title: "Transaction Type",
|
||||||
|
enum: transactionsData.map(td => td.TransactionType),
|
||||||
|
},
|
||||||
|
Account: {
|
||||||
|
$ref: "file:///account-schema.json",
|
||||||
|
},
|
||||||
|
Destination: {
|
||||||
|
anyOf: [
|
||||||
|
{
|
||||||
|
$ref: "file:///account-schema.json",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "string",
|
||||||
|
title: "Destination Account",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Amount: {
|
||||||
|
$ref: "file:///amount-schema.json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: "file:///account-schema.json",
|
||||||
|
schema: {
|
||||||
|
type: "string",
|
||||||
|
title: "Account type",
|
||||||
|
enum: accounts.map(acc => acc.address),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...amountSchema,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [accounts, header, selectedTransaction?.value]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!monaco) return;
|
||||||
|
console.log("monaco render");
|
||||||
|
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||||
|
validate: true,
|
||||||
|
schemas: getSchemas(),
|
||||||
|
});
|
||||||
|
}, [getSchemas, monaco]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
fluid
|
fluid
|
||||||
@@ -98,8 +181,16 @@ export const TxJson: FC<JsonProps> = ({
|
|||||||
dragAndDrop: true,
|
dragAndDrop: true,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// register onExit cb
|
||||||
const model = editor.getModel();
|
const model = editor.getModel();
|
||||||
model?.onWillDispose(() => onExit(model.getValue()));
|
model?.onWillDispose(() => onExit(model.getValue()));
|
||||||
|
|
||||||
|
// set json defaults
|
||||||
|
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||||
|
validate: true,
|
||||||
|
schemas: getSchemas(),
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
theme={theme === "dark" ? "dark" : "light"}
|
theme={theme === "dark" ? "dark" : "light"}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -164,16 +164,16 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
|
|||||||
|
|
||||||
let value: string | undefined;
|
let value: string | undefined;
|
||||||
if (typeof _value === "object") {
|
if (typeof _value === "object") {
|
||||||
if (_value.type === "json" && typeof _value.value === "object") {
|
if (_value.$type === "json" && typeof _value.$value === "object") {
|
||||||
value = JSON.stringify(_value.value);
|
value = JSON.stringify(_value.$value);
|
||||||
} else {
|
} else {
|
||||||
value = _value.value.toString();
|
value = _value.$value.toString();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
value = _value?.toString();
|
value = _value?.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
let isXrp = typeof _value === "object" && _value.type === "xrp";
|
let isXrp = typeof _value === "object" && _value.$type === "xrp";
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
key={field}
|
key={field}
|
||||||
@@ -197,7 +197,7 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
|
|||||||
...txFields,
|
...txFields,
|
||||||
[field]:
|
[field]:
|
||||||
typeof _value === "object"
|
typeof _value === "object"
|
||||||
? { ..._value, value: e.target.value }
|
? { ..._value, $value: e.target.value }
|
||||||
: e.target.value,
|
: e.target.value,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
50
content/amount-schema.json
Normal file
50
content/amount-schema.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"uri": "file:///amount-schema.json",
|
||||||
|
"title": "Amount",
|
||||||
|
"description": "Specify xrp in drops and tokens as objects.",
|
||||||
|
"schema": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": [
|
||||||
|
"number",
|
||||||
|
"string"
|
||||||
|
],
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"maximum": "100000000000000000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"currency": {
|
||||||
|
"description": "Arbitrary currency code for the token. Cannot be XRP."
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"number"
|
||||||
|
],
|
||||||
|
"description": "Quoted decimal representation of the amount of the token."
|
||||||
|
},
|
||||||
|
"issuer": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Generally, the account that issues this token. In special cases, this can refer to the account that holds the token instead."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"defaultSnippets": [
|
||||||
|
{
|
||||||
|
"label": "Xrp",
|
||||||
|
"body": "1000000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Token",
|
||||||
|
"body": {
|
||||||
|
"currency": "${1:13.1}",
|
||||||
|
"value": "${2:FOO}",
|
||||||
|
"description": "${3:rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpns}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,8 +27,8 @@
|
|||||||
"Account": "rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy",
|
"Account": "rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy",
|
||||||
"TransactionType": "CheckCash",
|
"TransactionType": "CheckCash",
|
||||||
"Amount": {
|
"Amount": {
|
||||||
"value": "100",
|
"$value": "100",
|
||||||
"type": "xrp"
|
"$type": "xrp"
|
||||||
},
|
},
|
||||||
"CheckID": "838766BA2B995C00744175F69A1B11E32C3DBC40E64801A4056FCBD657F57334",
|
"CheckID": "838766BA2B995C00744175F69A1B11E32C3DBC40E64801A4056FCBD657F57334",
|
||||||
"Fee": "12"
|
"Fee": "12"
|
||||||
@@ -61,8 +61,8 @@
|
|||||||
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
||||||
"TransactionType": "EscrowCreate",
|
"TransactionType": "EscrowCreate",
|
||||||
"Amount": {
|
"Amount": {
|
||||||
"value": "100",
|
"$value": "100",
|
||||||
"type": "xrp"
|
"$type": "xrp"
|
||||||
},
|
},
|
||||||
"Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
|
"Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
|
||||||
"CancelAfter": 533257958,
|
"CancelAfter": 533257958,
|
||||||
@@ -99,8 +99,8 @@
|
|||||||
"Account": "rs8jBmmfpwgmrSPgwMsh7CvKRmRt1JTVSX",
|
"Account": "rs8jBmmfpwgmrSPgwMsh7CvKRmRt1JTVSX",
|
||||||
"TokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007",
|
"TokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007",
|
||||||
"Amount": {
|
"Amount": {
|
||||||
"value": "100",
|
"$value": "100",
|
||||||
"type": "xrp"
|
"$type": "xrp"
|
||||||
},
|
},
|
||||||
"Flags": 1
|
"Flags": 1
|
||||||
},
|
},
|
||||||
@@ -122,8 +122,8 @@
|
|||||||
"Sequence": 8,
|
"Sequence": 8,
|
||||||
"TakerGets": "6000000",
|
"TakerGets": "6000000",
|
||||||
"Amount": {
|
"Amount": {
|
||||||
"value": "100",
|
"$value": "100",
|
||||||
"type": "xrp"
|
"$type": "xrp"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -131,8 +131,8 @@
|
|||||||
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
||||||
"Destination": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
|
"Destination": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
|
||||||
"Amount": {
|
"Amount": {
|
||||||
"value": "100",
|
"$value": "100",
|
||||||
"type": "xrp"
|
"$type": "xrp"
|
||||||
},
|
},
|
||||||
"Fee": "12",
|
"Fee": "12",
|
||||||
"Flags": 2147483648,
|
"Flags": 2147483648,
|
||||||
@@ -142,8 +142,8 @@
|
|||||||
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
|
||||||
"TransactionType": "PaymentChannelCreate",
|
"TransactionType": "PaymentChannelCreate",
|
||||||
"Amount": {
|
"Amount": {
|
||||||
"value": "100",
|
"$value": "100",
|
||||||
"type": "xrp"
|
"$type": "xrp"
|
||||||
},
|
},
|
||||||
"Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
|
"Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
|
||||||
"SettleDelay": 86400,
|
"SettleDelay": 86400,
|
||||||
@@ -157,8 +157,8 @@
|
|||||||
"TransactionType": "PaymentChannelFund",
|
"TransactionType": "PaymentChannelFund",
|
||||||
"Channel": "C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198",
|
"Channel": "C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198",
|
||||||
"Amount": {
|
"Amount": {
|
||||||
"value": "200",
|
"$value": "200",
|
||||||
"type": "xrp"
|
"$type": "xrp"
|
||||||
},
|
},
|
||||||
"Expiration": 543171558
|
"Expiration": 543171558
|
||||||
},
|
},
|
||||||
@@ -176,8 +176,8 @@
|
|||||||
"Fee": "12",
|
"Fee": "12",
|
||||||
"SignerQuorum": 3,
|
"SignerQuorum": 3,
|
||||||
"SignerEntries": {
|
"SignerEntries": {
|
||||||
"type": "json",
|
"$type": "json",
|
||||||
"value": [
|
"$value": [
|
||||||
{
|
{
|
||||||
"SignerEntry": {
|
"SignerEntry": {
|
||||||
"Account": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
|
"Account": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
|
||||||
@@ -213,8 +213,8 @@
|
|||||||
"Flags": 262144,
|
"Flags": 262144,
|
||||||
"LastLedgerSequence": 8007750,
|
"LastLedgerSequence": 8007750,
|
||||||
"Amount": {
|
"Amount": {
|
||||||
"value": "100",
|
"$value": "100",
|
||||||
"type": "xrp"
|
"$type": "xrp"
|
||||||
},
|
},
|
||||||
"Sequence": 12
|
"Sequence": 12
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { deepEqual } from '../utils/object';
|
|||||||
import transactionsData from "../content/transactions.json";
|
import transactionsData from "../content/transactions.json";
|
||||||
import state from '.';
|
import state from '.';
|
||||||
import { showAlert } from "../state/actions/showAlert";
|
import { showAlert } from "../state/actions/showAlert";
|
||||||
|
import { parseJSON } from '../utils/json';
|
||||||
|
|
||||||
export type SelectOption = {
|
export type SelectOption = {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -106,25 +107,25 @@ export const prepareTransaction = (data: any) => {
|
|||||||
(Object.keys(options)).forEach(field => {
|
(Object.keys(options)).forEach(field => {
|
||||||
let _value = options[field];
|
let _value = options[field];
|
||||||
// convert xrp
|
// convert xrp
|
||||||
if (_value && typeof _value === "object" && _value.type === "xrp") {
|
if (_value && typeof _value === "object" && _value.$type === "xrp") {
|
||||||
if (+_value.value) {
|
if (+_value.$value) {
|
||||||
options[field] = (+_value.value * 1000000 + "") as any;
|
options[field] = (+_value.$value * 1000000 + "") as any;
|
||||||
} else {
|
} else {
|
||||||
options[field] = undefined; // 👇 💀
|
options[field] = undefined; // 👇 💀
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// handle type: `json`
|
// handle type: `json`
|
||||||
if (_value && typeof _value === "object" && _value.type === "json") {
|
if (_value && typeof _value === "object" && _value.$type === "json") {
|
||||||
if (typeof _value.value === "object") {
|
if (typeof _value.$value === "object") {
|
||||||
options[field] = _value.value as any;
|
options[field] = _value.$value as any;
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
options[field] = JSON.parse(_value.value);
|
options[field] = JSON.parse(_value.$value);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = `Input error for json field '${field}': ${error instanceof Error ? error.message : ""
|
const message = `Input error for json field '${field}': ${error instanceof Error ? error.message : ""
|
||||||
}`;
|
}`;
|
||||||
console.error(message)
|
console.error(message)
|
||||||
options[field] = _value.value
|
options[field] = _value.$value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,16 +202,16 @@ export const prepareState = (value: string, txState: TransactionState) => {
|
|||||||
Object.keys(rest).forEach(field => {
|
Object.keys(rest).forEach(field => {
|
||||||
const value = rest[field];
|
const value = rest[field];
|
||||||
const origValue = txFields[field as keyof TxFields]
|
const origValue = txFields[field as keyof TxFields]
|
||||||
const isXrp = typeof value !== 'object' && origValue && typeof origValue === 'object' && origValue.type === 'xrp'
|
const isXrp = typeof value !== 'object' && origValue && typeof origValue === 'object' && origValue.$type === 'xrp'
|
||||||
if (isXrp) {
|
if (isXrp) {
|
||||||
rest[field] = {
|
rest[field] = {
|
||||||
type: "xrp",
|
$type: "xrp",
|
||||||
value: +value / 1000000, // TODO maybe use bigint?
|
$value: +value / 1000000, // TODO maybe use bigint?
|
||||||
};
|
};
|
||||||
} else if (typeof value === "object") {
|
} else if (typeof value === "object") {
|
||||||
rest[field] = {
|
rest[field] = {
|
||||||
type: "json",
|
$type: "json",
|
||||||
value,
|
$value: value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -221,14 +222,4 @@ export const prepareState = (value: string, txState: TransactionState) => {
|
|||||||
return tx
|
return tx
|
||||||
}
|
}
|
||||||
|
|
||||||
export const parseJSON = (str?: string | null): any | undefined => {
|
|
||||||
if (!str) return undefined
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(str);
|
|
||||||
return typeof parsed === "object" ? parsed : undefined;
|
|
||||||
} catch (error) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { transactionsData }
|
export { transactionsData }
|
||||||
|
|||||||
@@ -19,3 +19,13 @@ export const extractJSON = (str?: string) => {
|
|||||||
firstOpen = str.indexOf('{', firstOpen + 1);
|
firstOpen = str.indexOf('{', firstOpen + 1);
|
||||||
} while (firstOpen != -1);
|
} while (firstOpen != -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const parseJSON = (str?: string | null): any | undefined => {
|
||||||
|
if (!str) return undefined
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(str);
|
||||||
|
return typeof parsed === "object" ? parsed : undefined;
|
||||||
|
} catch (error) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
39
utils/schema.ts
Normal file
39
utils/schema.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
export const extractSchemaProps = <O extends object>(obj: O) =>
|
||||||
|
Object.entries(obj).reduce((prev, [key, val]) => {
|
||||||
|
const typeOf = <T>(arg: T) =>
|
||||||
|
arg instanceof Array
|
||||||
|
? "array"
|
||||||
|
: arg === null
|
||||||
|
? "undefined"
|
||||||
|
: typeof arg;
|
||||||
|
|
||||||
|
const value = (typeOf(val) === "object" && '$type' in val && '$value' in val) ? val?.$value : val;
|
||||||
|
const type = typeOf(value);
|
||||||
|
|
||||||
|
let schema: any = {
|
||||||
|
title: key,
|
||||||
|
type,
|
||||||
|
default: value,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeOf(value) === 'array') {
|
||||||
|
const item = value[0] // TODO merge other item schema's into one
|
||||||
|
if (typeOf(item) !== 'object') {
|
||||||
|
schema.items = {
|
||||||
|
type: 'object',
|
||||||
|
properties: extractSchemaProps(item),
|
||||||
|
default: item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO support primitive-value arrays
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeOf(value) === "object") {
|
||||||
|
schema.properties = extractSchemaProps(value)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[key]: schema,
|
||||||
|
};
|
||||||
|
}, {} as any);
|
||||||
|
|
||||||
Reference in New Issue
Block a user