@energyweb/node-red-contrib-green-proof-worker
Version:
789 lines • 27.9 kB
JSON
[
{
"id": "8ee174a8a298654a",
"type": "tab",
"label": "Flow 1",
"disabled": false,
"info": ""
},
{
"id": "9318cc15f9afd75f",
"type": "group",
"z": "8ee174a8a298654a",
"name": "unitsChanged v1",
"style": {
"label": true
},
"nodes": [
"e1f6f9ae2e6910d1",
"27566d8948e738f9",
"c3c0627c1de5e349",
"caada626130afaf6",
"ac42d7e5c6032791",
"65d289142a86384f",
"6ca11a0d57bdc6cc",
"b8d668ba7a1a03ed",
"ca32fbaad3ee3347",
"e9eb48b870d28564",
"d4403c151c10fd53",
"98a4076a7346f77b"
],
"x": 2074,
"y": 119,
"w": 1032,
"h": 262
},
{
"id": "35ba395e82c0b1fd",
"type": "group",
"z": "8ee174a8a298654a",
"name": "No protocol (old message format)",
"style": {
"label": true
},
"nodes": [
"126dbca226762933",
"58dda468b6e90876",
"be6ed27e6faa08cc",
"56fd2cd0cc0812f8"
],
"x": 74,
"y": 399,
"w": 892,
"h": 82
},
{
"id": "04c09f0c2cd6afcc",
"type": "group",
"z": "8ee174a8a298654a",
"name": "Protocol version 1",
"style": {
"label": true
},
"nodes": [
"fb43395da3eb18ac",
"746c2dd32cd8fa7f",
"af129b9f346b5e93",
"0bdc014d7d7e836a",
"a986536a19a878b8",
"4d6a02a638fceb91"
],
"x": 74,
"y": 559,
"w": 832,
"h": 122
},
{
"id": "a407a49de3d419a0",
"type": "sqlite-config",
"name": "",
"dbLocation": ""
},
{
"id": "cbe7827e8b1ce3bd",
"type": "function",
"z": "8ee174a8a298654a",
"name": "Restart source",
"func": "node.warn('Force resetting source')\nreturn {\n ...msg,\n topic: 'force-reset'\n}",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 280,
"y": 220,
"wires": [
[
"8308f574539280d2"
]
]
},
{
"id": "39c8b0b71a53b058",
"type": "link in",
"z": "8ee174a8a298654a",
"name": "Handle error IN",
"links": [
"41758cdba3bbaddc",
"5c9c86e7123f5da9"
],
"x": 125,
"y": 220,
"wires": [
[
"cbe7827e8b1ce3bd"
]
]
},
{
"id": "e6aebb3547ebbd29",
"type": "link in",
"z": "8ee174a8a298654a",
"name": "Finished processing IN",
"links": [
"27566d8948e738f9",
"e1f6f9ae2e6910d1",
"e16ab80308b97075"
],
"x": 125,
"y": 280,
"wires": [
[
"a8f1dc8b2b511da8"
]
]
},
{
"id": "a8f1dc8b2b511da8",
"type": "function",
"z": "8ee174a8a298654a",
"name": "Finished processing",
"func": "node.warn('Finished processing')\nreturn {\n ...msg,\n topic: 'finished-processing'\n}",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 280,
"y": 280,
"wires": [
[
"8308f574539280d2"
]
]
},
{
"id": "e1f6f9ae2e6910d1",
"type": "link out",
"z": "8ee174a8a298654a",
"g": "9318cc15f9afd75f",
"name": "Finished processing correct OUT",
"mode": "link",
"links": [
"e6aebb3547ebbd29"
],
"x": 3065,
"y": 260,
"wires": []
},
{
"id": "27566d8948e738f9",
"type": "link out",
"z": "8ee174a8a298654a",
"g": "9318cc15f9afd75f",
"name": "Finished processing wrong tx OUT",
"mode": "link",
"links": [
"e6aebb3547ebbd29"
],
"x": 3065,
"y": 300,
"wires": []
},
{
"id": "c3c0627c1de5e349",
"type": "comment",
"z": "8ee174a8a298654a",
"g": "9318cc15f9afd75f",
"name": "Validation failed",
"info": "",
"x": 2480,
"y": 340,
"wires": []
},
{
"id": "caada626130afaf6",
"type": "comment",
"z": "8ee174a8a298654a",
"g": "9318cc15f9afd75f",
"name": "Validation succeeded",
"info": "",
"x": 2500,
"y": 220,
"wires": []
},
{
"id": "ac42d7e5c6032791",
"type": "voting-marketplace",
"z": "8ee174a8a298654a",
"g": "9318cc15f9afd75f",
"name": "",
"workerAddress": "",
"indexerUrl": "",
"solutionNamespace": "",
"x": 2500,
"y": 260,
"wires": [
[
"ca32fbaad3ee3347"
]
]
},
{
"id": "65d289142a86384f",
"type": "voting-marketplace",
"z": "8ee174a8a298654a",
"g": "9318cc15f9afd75f",
"name": "",
"workerAddress": "",
"indexerUrl": "",
"solutionNamespace": "",
"x": 2500,
"y": 300,
"wires": [
[
"27566d8948e738f9"
]
]
},
{
"id": "8308f574539280d2",
"type": "source-http-api",
"z": "8ee174a8a298654a",
"name": "",
"host": "",
"appId": "ggp",
"sqliteConfig": "a407a49de3d419a0",
"x": 520,
"y": 220,
"wires": [
[
"737cc9c7a540ab89"
]
]
},
{
"id": "737cc9c7a540ab89",
"type": "switch",
"z": "8ee174a8a298654a",
"name": "Protocol check",
"property": "payload.protocolVersion",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "1",
"vt": "str"
},
{
"t": "istype",
"v": "undefined",
"vt": "undefined"
},
{
"t": "else"
}
],
"checkall": "true",
"repair": false,
"outputs": 3,
"x": 760,
"y": 220,
"wires": [
[
"a487a21a1443f6b7"
],
[
"42fe81814bb0c41b"
],
[
"90f75c63b182bae9"
]
]
},
{
"id": "126dbca226762933",
"type": "json-schema-validator",
"z": "8ee174a8a298654a",
"g": "35ba395e82c0b1fd",
"name": "Validate old message type",
"jsonSchema": "{\n \"type\": \"object\",\n \"required\": [\"type\", \"txLog\"],\n \"properties\": {\n \"type\": { \"const\": \"unitsChanged\" },\n \"txLog\": {\n \"type\": \"object\",\n \"required\": [\"rootUnitId\", \"changes\"],\n \"properties\": {\n \"rootUnitId\": { \"type\": \"string\" },\n \"changes\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"unitId\", \"volume\", \"owner\", \"prevOwner\"],\n \"properties\": {\n \"unitId\": { \"type\": \"string\" },\n \"volume\": { \"type\": \"number\" },\n \"owner\": { \"type\": \"string\" },\n \"prevOwner\": { \"type\": [\"string\", \"null\"] }\n }\n }\n }\n }\n }\n }\n}",
"x": 510,
"y": 440,
"wires": [
[
"56fd2cd0cc0812f8"
]
]
},
{
"id": "6ca11a0d57bdc6cc",
"type": "sqlite-inject",
"z": "8ee174a8a298654a",
"g": "9318cc15f9afd75f",
"name": "",
"sqliteConfig": "a407a49de3d419a0",
"x": 2170,
"y": 160,
"wires": [
[
"e9eb48b870d28564"
]
]
},
{
"id": "b8d668ba7a1a03ed",
"type": "function",
"z": "8ee174a8a298654a",
"g": "9318cc15f9afd75f",
"name": "Query ledger for accounts",
"func": "const txLog = msg.payload.payload.txLog;\n\nconst accountIds = txLog.changes.flatMap((change) => {\n return [change.owner].concat(change.prevOwner ?? []);\n});\n\nconst accountIdsWithRootIds = accountIds.map(accountId => ({\n accountId,\n rootUnitId: txLog.rootUnitId,\n}));\n\nmsg.payload.sqlite.then(async db => {\n if (accountIdsWithRootIds.length === 0) {\n node.send({\n ...msg,\n payload: {\n ...msg.payload,\n ledger: [],\n }\n });\n\n return;\n }\n \n const result = await db.selectFrom('ledger')\n .selectAll()\n .where(({ eb, refTuple, tuple }) => eb(\n refTuple('account_id', 'root_unit_id'),\n 'in',\n accountIdsWithRootIds.flatMap((accountWithUnit) => {\n return tuple(accountWithUnit.accountId, accountWithUnit.rootUnitId)\n })\n ))\n .execute()\n \n node.send({\n ...msg,\n payload: {\n ...msg.payload,\n ledger: result.map(r => ({\n accountId: r.account_id,\n rootUnitId: r.root_unit_id,\n volume: r.volume\n }))\n }\n })\n}).catch(node.error)\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 2630,
"y": 160,
"wires": [
[
"98a4076a7346f77b"
]
]
},
{
"id": "6b974a17d0bf6e5a",
"type": "catch",
"z": "8ee174a8a298654a",
"name": "",
"scope": null,
"uncaught": false,
"x": 100,
"y": 160,
"wires": [
[
"75e8cba180af61bb"
]
]
},
{
"id": "6d927d07bd0f86c8",
"type": "comment",
"z": "8ee174a8a298654a",
"name": "No protocol (old message format)",
"info": "",
"x": 1090,
"y": 220,
"wires": []
},
{
"id": "42fe81814bb0c41b",
"type": "link out",
"z": "8ee174a8a298654a",
"name": "No protocol flow (OUT)",
"mode": "link",
"links": [
"58dda468b6e90876"
],
"x": 935,
"y": 220,
"wires": []
},
{
"id": "58dda468b6e90876",
"type": "link in",
"z": "8ee174a8a298654a",
"g": "35ba395e82c0b1fd",
"name": "No protocol flow (IN)",
"links": [
"42fe81814bb0c41b"
],
"x": 115,
"y": 440,
"wires": [
[
"be6ed27e6faa08cc"
]
]
},
{
"id": "ca32fbaad3ee3347",
"type": "function",
"z": "8ee174a8a298654a",
"g": "9318cc15f9afd75f",
"name": "Update ledger",
"func": "const txLog = msg.payload.payload.txLog;\nconst ledger = msg.payload.ledger;\n\nconst rootUnitId = txLog.rootUnitId;\nconst volumeMap = ledger.reduce((volumeMap, entry) => {\n const key = `${entry.accountId}.${entry.rootUnitId}`;\n const value = entry.volume;\n\n volumeMap[key] ||= value;\n\n return volumeMap;\n}, {});\n\nfor (const change of txLog.changes) {\n if (change.prevOwner) {\n const prevOwnerVolume = volumeMap[`${change.prevOwner}.${rootUnitId}`];\n\n if (prevOwnerVolume === undefined) {\n throw new Error(`Prev owner not found: prevOwnerId=${change.prevOwner}, rootUnitId=${rootUnitId}`);\n }\n\n volumeMap[`${change.prevOwner}.${rootUnitId}`] -= change.volume;\n }\n\n volumeMap[`${change.owner}.${rootUnitId}`] ||= 0;\n volumeMap[`${change.owner}.${rootUnitId}`] += change.volume;\n}\n\nconst ledgerUpdate = Object.entries(volumeMap).map(([key, volume]) => {\n const [accountId, rootUnitId] = key.split('.');\n\n return {\n accountId,\n rootUnitId,\n volume,\n };\n});\n\nif (ledgerUpdate.length === 0) {\n return;\n}\n\nmsg.payload.sqlite.then(async db => {\n await db\n .insertInto('ledger')\n .values(ledgerUpdate.map(e => ({\n account_id: e.accountId,\n root_unit_id: e.rootUnitId,\n volume: e.volume\n })))\n .onConflict(c => c.columns(['account_id', 'root_unit_id']).doUpdateSet((eb) => ({\n volume: eb.ref('excluded.volume'),\n })))\n .execute()\n\n node.send(msg);\n}).catch(node.error);",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 2720,
"y": 260,
"wires": [
[
"d4403c151c10fd53"
]
]
},
{
"id": "75e8cba180af61bb",
"type": "function",
"z": "8ee174a8a298654a",
"name": "Log error",
"func": "node.error(`[${msg.error.source.type}:${msg.error.source.name}] ${msg.error.message}`);\n\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 260,
"y": 160,
"wires": [
[
"cbe7827e8b1ce3bd"
]
]
},
{
"id": "be6ed27e6faa08cc",
"type": "function",
"z": "8ee174a8a298654a",
"g": "35ba395e82c0b1fd",
"name": "No \"type\" compatibility",
"func": "return {\n ...msg,\n payload: {\n ...msg.payload,\n type: 'unitsChanged'\n }\n}",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 260,
"y": 440,
"wires": [
[
"126dbca226762933"
]
]
},
{
"id": "e9eb48b870d28564",
"type": "function",
"z": "8ee174a8a298654a",
"g": "9318cc15f9afd75f",
"name": "Filter processed",
"func": "const txLog = msg.payload.payload.txLog;\n\nmsg.payload.sqlite.then(async db => {\n const unitIds = txLog.changes.map(c => c.unitId);\n\n if (unitIds.length === 0) {\n node.send(msg);\n return;\n }\n\n const existingUnits = await db\n .selectFrom('processed')\n .select('unit_id')\n .where('unit_id', 'in', unitIds)\n .execute()\n .then(result => result.map(row => row.unit_id));\n\n const existingUnitsSet = new Set(existingUnits);\n const notExistingUnits = unitIds.filter(unitId => !existingUnitsSet.has(unitId));\n\n if (existingUnits.length !== 0) {\n if (existingUnits.length === unitIds.length) {\n node.log(`Filtered ALL changes (unit ids: ${existingUnits.join(', ')})`);\n } else {\n node.log(`Filtered some changes (unit ids ${existingUnits.join(', ')})`);\n }\n }\n\n node.send({\n ...msg,\n payload: {\n ...msg.payload,\n payload: {\n ...msg.payload.payload,\n txLog: {\n ...txLog,\n changes: txLog.changes.filter(change => notExistingUnits.includes(change.unitId))\n }\n }\n }\n })\n}).catch(node.error);\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 2360,
"y": 160,
"wires": [
[
"b8d668ba7a1a03ed"
]
]
},
{
"id": "d4403c151c10fd53",
"type": "function",
"z": "8ee174a8a298654a",
"g": "9318cc15f9afd75f",
"name": "Update processed",
"func": "const txLog = msg.payload.payload.txLog;\n\nmsg.payload.sqlite.then(async db => {\n const changes = txLog.changes;\n\n if (changes.length === 0) {\n node.send(msg);\n return;\n }\n\n await db\n .insertInto('processed')\n .values(changes.map(c => ({\n owner: c.owner,\n prev_owner: c.prevOwner,\n root_unit_id: txLog.rootUnitId,\n unit_id: c.unitId,\n volume: c.volume,\n created_at: Date.now(),\n })))\n .execute();\n\n node.send(msg);\n}).catch(node.error);\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 2930,
"y": 260,
"wires": [
[
"e1f6f9ae2e6910d1"
]
]
},
{
"id": "98a4076a7346f77b",
"type": "function",
"z": "8ee174a8a298654a",
"g": "9318cc15f9afd75f",
"name": "Transaction validator",
"func": "const VOTE_APPROVE = 'approve';\nconst VOTE_REJECT = 'reject';\n\nconst txLog = msg.payload.payload.txLog;\nconst ledger = msg.payload.ledger;\n\nconst rootUnitId = txLog.rootUnitId;\nconst changes = txLog.changes;\nconst volumeMap = ledger.reduce((volumeMap, entry) => {\n const key = `${entry.accountId}.${entry.rootUnitId}`;\n const value = entry.volume;\n\n volumeMap[key] ||= value;\n\n return volumeMap;\n}, {});\n\nfor (const change of changes) {\n // Ensure entries to be modified exist\n if (change.prevOwner) {\n volumeMap[`${change.prevOwner}.${rootUnitId}`] ||= 0;\n }\n volumeMap[`${change.owner}.${rootUnitId}`] ||= 0;\n\n if (change.prevOwner) {\n const ownedVolume = volumeMap[`${change.prevOwner}.${rootUnitId}`] ?? 0;\n\n if (ownedVolume - change.volume < 0) {\n return [\n undefined,\n {\n ...msg,\n payload: {\n ...msg.payload,\n txFailedReason: `${change.prevOwner} has ${ownedVolume} volume in unit ${rootUnitId}, but ${change.volume} is required`,\n voting: txLog.changes.map(v => ({\n vote: VOTE_REJECT,\n votingId: v.votingId,\n }))\n }\n }\n ];\n\n }\n }\n\n if (change.prevOwner) {\n volumeMap[`${change.prevOwner}.${rootUnitId}`] -= change.volume;\n }\n\n volumeMap[`${change.owner}.${rootUnitId}`] += change.volume;\n}\n\n\nreturn [\n {\n ...msg,\n payload: {\n ...msg.payload,\n voting: txLog.changes.map(v => ({\n vote: VOTE_APPROVE,\n votingId: v.votingId,\n }))\n }\n },\n undefined\n]\n",
"outputs": 2,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 2220,
"y": 280,
"wires": [
[
"ac42d7e5c6032791"
],
[
"65d289142a86384f"
]
]
},
{
"id": "90f75c63b182bae9",
"type": "function",
"z": "8ee174a8a298654a",
"name": "Unknown protocol version",
"func": "throw new Error(`Protocol version ${msg.payload.protocolVersion} not supported`);\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1070,
"y": 260,
"wires": [
[]
]
},
{
"id": "a487a21a1443f6b7",
"type": "link out",
"z": "8ee174a8a298654a",
"name": "Protocol vresion 1 (OUT)",
"mode": "link",
"links": [
"fb43395da3eb18ac"
],
"x": 935,
"y": 180,
"wires": []
},
{
"id": "fb43395da3eb18ac",
"type": "link in",
"z": "8ee174a8a298654a",
"g": "04c09f0c2cd6afcc",
"name": "Protocol version 1 (IN)",
"links": [
"a487a21a1443f6b7"
],
"x": 115,
"y": 620,
"wires": [
[
"746c2dd32cd8fa7f"
]
]
},
{
"id": "8cf263f8817fe694",
"type": "comment",
"z": "8ee174a8a298654a",
"name": "Protocol version 1",
"info": "",
"x": 1050,
"y": 180,
"wires": []
},
{
"id": "746c2dd32cd8fa7f",
"type": "json-schema-validator",
"z": "8ee174a8a298654a",
"g": "04c09f0c2cd6afcc",
"name": "Protocol validator",
"jsonSchema": "{\n \"type\": \"object\",\n \"required\": [\"protocolVersion\", \"type\", \"version\", \"payload\"],\n \"properties\": {\n \"protocolVersion\": { \"const\": 1 },\n \"type\": { \"type\": \"string\" },\n \"version\": { \"type\": \"number\" },\n \"payload\": {}\n }\n}",
"x": 290,
"y": 620,
"wires": [
[
"af129b9f346b5e93"
]
]
},
{
"id": "af129b9f346b5e93",
"type": "switch",
"z": "8ee174a8a298654a",
"g": "04c09f0c2cd6afcc",
"name": "Message type check",
"property": "payload.type",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "unitsChanged",
"vt": "str"
},
{
"t": "else"
}
],
"checkall": "true",
"repair": false,
"outputs": 2,
"x": 520,
"y": 620,
"wires": [
[
"a986536a19a878b8"
],
[
"0bdc014d7d7e836a"
]
]
},
{
"id": "0bdc014d7d7e836a",
"type": "function",
"z": "8ee174a8a298654a",
"g": "04c09f0c2cd6afcc",
"name": "Unknown message type",
"func": "throw new Error(`Message type ${msg.payload.type} not supported`);\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 770,
"y": 640,
"wires": [
[]
]
},
{
"id": "04570ad1292f1568",
"type": "link in",
"z": "8ee174a8a298654a",
"name": "unitsChanged (IN)",
"links": [
"a986536a19a878b8"
],
"x": 1335,
"y": 200,
"wires": [
[
"7f0c15b61ca09b2f"
]
]
},
{
"id": "a986536a19a878b8",
"type": "link out",
"z": "8ee174a8a298654a",
"g": "04c09f0c2cd6afcc",
"name": "unitsChanged (OUT)",
"mode": "link",
"links": [
"04570ad1292f1568"
],
"x": 675,
"y": 600,
"wires": []
},
{
"id": "4d6a02a638fceb91",
"type": "comment",
"z": "8ee174a8a298654a",
"g": "04c09f0c2cd6afcc",
"name": "unitsChanged",
"info": "",
"x": 770,
"y": 600,
"wires": []
},
{
"id": "a7187e080554af45",
"type": "comment",
"z": "8ee174a8a298654a",
"name": "unitsChanged",
"info": "",
"x": 1390,
"y": 160,
"wires": []
},
{
"id": "56fd2cd0cc0812f8",
"type": "function",
"z": "8ee174a8a298654a",
"g": "35ba395e82c0b1fd",
"name": "Convert old message to new one",
"func": "const { txLog, type, ...payload } = msg.payload;\n\nreturn {\n ...msg,\n payload: {\n ...payload,\n protocolVersion: 1,\n type: 'unitsChanged',\n version: 1,\n payload: {\n txLog: {\n ...txLog,\n changes: txLog.changes.map(change => ({\n ...change,\n votingId: change.unitId\n }))\n }\n }\n }\n}",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 800,
"y": 440,
"wires": [
[
"746c2dd32cd8fa7f"
]
]
},
{
"id": "7f0c15b61ca09b2f",
"type": "switch",
"z": "8ee174a8a298654a",
"name": "unitsChanged version check",
"property": "payload.version",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "1",
"vt": "num"
},
{
"t": "else"
}
],
"checkall": "true",
"repair": false,
"outputs": 2,
"x": 1500,
"y": 200,
"wires": [
[
"7a9d1c1446786bd7"
],
[
"6e4d452be82e6fee"
]
]
},
{
"id": "6e4d452be82e6fee",
"type": "function",
"z": "8ee174a8a298654a",
"name": "Unknown message version",
"func": "throw new Error(`Message type \"unitsChanged\" in version ${msg.payload.version} is not supported`);\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1780,
"y": 240,
"wires": [
[]
]
},
{
"id": "7a9d1c1446786bd7",
"type": "json-schema-validator",
"z": "8ee174a8a298654a",
"name": "Validate unitsChanged v1 payload",
"jsonSchema": "{\n \"type\": \"object\",\n \"required\": [\"payload\"],\n \"properties\": {\n \"payload\": {\n \"type\": \"object\",\n \"required\": [\"txLog\"],\n \"properties\": {\n \"txLog\": {\n \"type\": \"object\",\n \"required\": [\"rootUnitId\", \"changes\"],\n \"properties\": {\n \"rootUnitId\": { \"type\": \"string\" },\n \"changes\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"required\": [\"unitId\", \"volume\", \"owner\", \"prevOwner\", \"votingId\"],\n \"properties\": {\n \"unitId\": { \"type\": \"string\" },\n \"votingId\": { \"type\": \"string\" },\n \"volume\": { \"type\": \"number\" },\n \"owner\": { \"type\": \"string\" },\n \"prevOwner\": { \"type\": [\"string\", \"null\"] }\n }\n }\n }\n }\n }\n }\n }\n }\n}",
"x": 1800,
"y": 160,
"wires": [
[
"6ca11a0d57bdc6cc"
]
]
}
]