503 lines
25 KiB
TypeScript
503 lines
25 KiB
TypeScript
/*
|
|
* @project: TERA
|
|
* @version: Development (beta)
|
|
* @license: MIT (not for evil)
|
|
* @copyright: Yuriy Ivanov (Vtools) 2017-2019 [progr76@gmail.com]
|
|
* Web: https://terafoundation.org
|
|
* Twitter: https://twitter.com/terafoundation
|
|
* Telegram: https://t.me/terafoundation
|
|
*/
|
|
|
|
"use strict";
|
|
import CDB from './db/block-db'
|
|
import { TYPE_TRANSACTION } from '../constant/account';
|
|
import { DB_FORMAT } from '../constant/db-format';
|
|
import * as crypto from 'crypto'
|
|
import { LoadContext, ContextTask, TeraBlock, SocketSendInfo } from '../interfaces/server';
|
|
import CNode from './node';
|
|
var MIN_POW_CHAINS = 2;
|
|
global.COUNT_NODE_PROOF = 6;
|
|
if (global.TEST_NETWORK) {
|
|
MIN_POW_CHAINS = 1;
|
|
global.COUNT_NODE_PROOF = 1;
|
|
}
|
|
|
|
export default class CRest extends CDB {
|
|
LoadRestContext: LoadContext
|
|
LoadHistoryContext: LoadContext
|
|
BlockNumDB: number
|
|
BlockNumDBMin: number
|
|
UseTruncateBlockDB: number
|
|
ContextSendLoadToBegin: { Time: number; MapSend: {}; BlockNum?: number; }
|
|
constructor(SetKeyPair: crypto.ECDH, RunIP: string, RunPort: number, UseRNDHeader: boolean, bVirtual: boolean) {
|
|
super(SetKeyPair, RunIP, RunPort, UseRNDHeader, bVirtual)
|
|
}
|
|
CheckSyncRest() {
|
|
var BlockNumTime = global.GetCurrentBlockNumByTime();
|
|
var Delta = BlockNumTime - this.BlockNumDB;
|
|
if (Delta > global.REST_START_COUNT + global.DELTA_BLOCK_ACCOUNT_HASH + 500) {
|
|
var BlockNumRest = GetCurrentRestNum(global.REST_START_COUNT + global.DELTA_BLOCK_ACCOUNT_HASH + 500);
|
|
if (this.BlockNumDB >= this.BlockNumDBMin && this.BlockNumDB <= this.BlockNumDBMin + global.BLOCK_PROCESSING_LENGTH2) {
|
|
} else if (BlockNumRest > this.BlockNumDB) {
|
|
} else {
|
|
this.LoadRestContext = undefined
|
|
return;
|
|
}
|
|
this.LoadRestContext = {
|
|
Mode: 0,
|
|
BlockNum: BlockNumRest,
|
|
BlockNumRest: BlockNumRest,
|
|
WasDelta: Delta,
|
|
BlockNumProof: BlockNumRest + global.DELTA_BLOCK_ACCOUNT_HASH,
|
|
CountProof: global.COUNT_BLOCKS_FOR_LOAD,
|
|
StartTimeHistory: Date.now(),
|
|
MaxTimeOut: 3600 * 1000,
|
|
LoopSyncRest: 1,
|
|
SendGetHeaderCount: 0,
|
|
ReceiveHeaderCount: 0,
|
|
ArrProof: [],
|
|
MapSend: {}
|
|
}
|
|
for (var i = 0; i < this.NodesArr.length; i++) {
|
|
this.NodesArr[i].SendRestGetHeader = 0
|
|
}
|
|
global.ToLog("**********START REST MODE: " + this.LoadRestContext.BlockNumProof)
|
|
} else {
|
|
this.LoadRestContext = undefined
|
|
}
|
|
}
|
|
GetNextNode(task, keyid: string | number, checktime?: number | boolean, BlockNum?: number): any {
|
|
//defiend in block-loader.ts(CBlock)
|
|
}
|
|
DataFromF(Info: SocketSendInfo, bSendFormat?: boolean): any {
|
|
//defiend in block-loader.ts(CBlock)
|
|
}
|
|
AddToBan(Node: CNode, Str: string) {
|
|
//defiend in server.ts(CTransport)
|
|
}
|
|
LoopSyncRest() {
|
|
let Context = this.LoadRestContext;
|
|
switch (Context.Mode) {
|
|
case 0:
|
|
if (!global.TX_PROCESS) {
|
|
return;
|
|
}
|
|
var ArrNodes = this.GetActualNodes();
|
|
for (var i = 0; i < ArrNodes.length; i++) {
|
|
var Node = ArrNodes[i];
|
|
if (!Node || Node.SendRestGetHeader) {
|
|
continue;
|
|
}
|
|
Node.SendRestGetHeader = 1
|
|
global.ToLog("Send rest get header " + Context.BlockNumProof + " to " + global.NodeName(Node), 2)
|
|
this.SendF(Node, {
|
|
"Method": "GETBLOCKHEADER",
|
|
"Data": {
|
|
Foward: 1,
|
|
BlockNum: Context.BlockNumProof,
|
|
Hash: []
|
|
},
|
|
"Context": {
|
|
F: this.RETBLOCKHEADER_REST.bind(this)
|
|
},
|
|
})
|
|
Context.SendGetHeaderCount++
|
|
break;
|
|
}
|
|
if (Context.ReceiveHeaderCount >= global.COUNT_NODE_PROOF) {
|
|
Context.Mode = 2
|
|
global.ToLog("Next mode: " + Context.Mode + " Receive:" + Context.ReceiveHeaderCount + "/" + Context.SendGetHeaderCount, 2)
|
|
}
|
|
break;
|
|
case 1000:
|
|
break;
|
|
case 2:
|
|
var MapSumPower = {};
|
|
for (var i = 0; i < Context.ArrProof.length; i++) {
|
|
var Item = Context.ArrProof[i];
|
|
if (!MapSumPower[Item.SumPower])
|
|
MapSumPower[Item.SumPower] = 0
|
|
MapSumPower[Item.SumPower]++
|
|
}
|
|
var MaxCount = 0, MaxPow = 0;
|
|
for (var key in MapSumPower) {
|
|
if (MapSumPower[key] >= MaxCount) {
|
|
MaxCount = MapSumPower[key]
|
|
MaxPow = parseInt(key)
|
|
}
|
|
}
|
|
if (MaxCount < 2 || MaxPow === 0) {
|
|
global.ToLog("****************************************************************** Error MaxPow=" + MaxPow + " - reload.")
|
|
this.CheckSyncRest()
|
|
return;
|
|
}
|
|
for (var i = 0; i < Context.ArrProof.length; i++) {
|
|
var Item = Context.ArrProof[i];
|
|
if (Item.SumPower !== MaxPow) {
|
|
var Str = "BAD SumPower: " + Item.SumPower + "/" + MaxPow;
|
|
global.ToLog(Str + " from: " + global.NodeName(Item.Node), 2)
|
|
}
|
|
else
|
|
if (Item.SumPower && Item.arr.length >= Context.CountProof) {
|
|
Item.OK = 1
|
|
Context.BlockProof = Item.arr[0]
|
|
}
|
|
}
|
|
Context.Mode++
|
|
global.ToLog("Next mode: " + Context.Mode + " SumPower:" + MaxPow, 2)
|
|
break;
|
|
case 3:
|
|
if (global.TX_PROCESS && global.TX_PROCESS.RunRPC) {
|
|
Context.Mode++
|
|
global.ToLog("Next mode: " + Context.Mode, 2)
|
|
var Block = { BlockNum: Context.BlockNumRest } as TeraBlock;
|
|
this.BlockNumDB = Block.BlockNum
|
|
this.BlockNumDBMin = Block.BlockNum
|
|
this.WriteBlockHeaderDB(Block)
|
|
this.UseTruncateBlockDB = undefined
|
|
global.ToLog("Start run TXPrepareLoadRest", 2)
|
|
global.TX_PROCESS.RunRPC("TXPrepareLoadRest", Block.BlockNum, function(Err, Params) {
|
|
Context.Mode++
|
|
global.ToLog("Next mode: " + Context.Mode, 2)
|
|
})
|
|
}
|
|
break;
|
|
case 4:
|
|
break;
|
|
case 5:
|
|
let BlockProof = Context.BlockProof;
|
|
var SendCount = 0;
|
|
if (BlockProof)
|
|
for (var i = 0; i < Context.ArrProof.length; i++) {
|
|
let Item = Context.ArrProof[i];
|
|
if (Item.OK) {
|
|
SendCount++
|
|
global.ToLog("Send rest get block proof:" + BlockProof.BlockNum + " to " + global.NodeName(Item.Node), 2)
|
|
this.SendF(Item.Node, {
|
|
"Method": "GETBLOCK", "Data": { BlockNum: BlockProof.BlockNum, TreeHash: BlockProof.TreeHash }, "Context": {
|
|
F: function(Info) {
|
|
if (Context.TxProof)
|
|
return;
|
|
var Data = global.BufLib.GetObjectFromBuffer(Info.Data, DB_FORMAT.FORMAT_BLOCK_TRANSFER, global.WRK_BLOCK_TRANSFER);
|
|
Info.Data = undefined
|
|
if (Data.BlockNum !== BlockProof.BlockNum || global.CompareArr(Data.TreeHash, BlockProof.TreeHash) !== 0) {
|
|
global.ToLog("Error get proof block from " + global.NodeName(Item.Node), 2)
|
|
return;
|
|
}
|
|
var TreeHash = global.CalcTreeHashFromArrBody(Data.BlockNum, Data.arrContent);
|
|
if (global.CompareArr(BlockProof.TreeHash, TreeHash) !== 0) {
|
|
global.ToLog("Error TreeHash in proof block from " + global.NodeName(Item.Node), 2)
|
|
return;
|
|
}
|
|
global.ToLog("GET BLOCK proof from " + global.NodeName(Item.Node), 2)
|
|
var FindTx = undefined;
|
|
for (var n = 0; n < Data.arrContent.length; n++) {
|
|
var Body = Data.arrContent[n];
|
|
if (Body[0] === TYPE_TRANSACTION.TYPE_TRANSACTION_ACC_HASH) {
|
|
try {
|
|
FindTx = global.BufLib.GetObjectFromBuffer(Body, DB_FORMAT.FORMAT_ACCOUNT_HASH3, {})
|
|
}
|
|
catch (e) {
|
|
global.ToLog("Error parsing Body[" + n + "] block proof: " + e, 2)
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if (!FindTx)
|
|
return;
|
|
Context.TxProof = FindTx
|
|
Context.Mode++
|
|
global.ToLog("Next mode: " + Context.Mode, 2)
|
|
Context.AccTaskList = []
|
|
Context.AccTaskFinished = 0
|
|
var AccCount = FindTx.AccountMax + 1;
|
|
for (var n = 0; n < AccCount; n += global.MAX_ACCOUNTS_TRANSFER) {
|
|
var Task = { StartNum: n, Count: global.MAX_ACCOUNTS_TRANSFER, Time: 0, MapSend: {} };
|
|
if (Task.StartNum + Task.Count > AccCount)
|
|
Task.Count = AccCount - Task.StartNum
|
|
Context.AccTaskList.push(Task)
|
|
}
|
|
Context.SmartTaskList = []
|
|
Context.SmartTaskFinished = 0
|
|
for (var n = 0; n < FindTx.SmartCount; n += global.MAX_SMARTS_TRANSFER) {
|
|
var Task = { StartNum: n, Count: global.MAX_SMARTS_TRANSFER, Time: 0, MapSend: {} };
|
|
if (Task.StartNum + Task.Count > FindTx.SmartCount)
|
|
Task.Count = FindTx.SmartCount - Task.StartNum
|
|
Context.SmartTaskList.push(Task)
|
|
}
|
|
}
|
|
},
|
|
})
|
|
if (SendCount >= 5)
|
|
break;
|
|
}
|
|
}
|
|
Context.Mode++
|
|
global.ToLog("Next mode: " + Context.Mode, 2)
|
|
break;
|
|
case 6:
|
|
break;
|
|
case 7:
|
|
if (Context.AccTaskFinished === Context.AccTaskList.length) {
|
|
Context.Mode++
|
|
global.ToLog("Next mode: " + Context.Mode, 2)
|
|
break;
|
|
}
|
|
var CurTime = Date.now();
|
|
for (var i = 0; i < Context.AccTaskList.length; i++) {
|
|
let Task = Context.AccTaskList[i];
|
|
var Delta = CurTime - Task.Time;
|
|
if (Delta > 5 * 1000 && !Task.OK) {
|
|
var Ret = this.GetNextNode(Task, "", 1);
|
|
if (Ret.Result) {
|
|
global.ToLog("Send GETREST Num:" + Task.StartNum + "-" + Task.Count + " to " + global.NodeName(Ret.Node), 2)
|
|
var SELF = this;
|
|
this.SendF(Ret.Node, {
|
|
"Method": "GETREST",
|
|
"Data": {
|
|
BlockNum: Context.BlockNumRest,
|
|
AccNum: Task.StartNum,
|
|
Count: Task.Count,
|
|
AccHash: Context.TxProof.AccHash
|
|
},
|
|
"Context": {
|
|
F: function(Info) {
|
|
if (Task.OK)
|
|
return;
|
|
var Data = SELF.DataFromF(Info);
|
|
if (!Data.Result)
|
|
return;
|
|
if (Data.Version !== 1) {
|
|
global.ToLog("ERROR Version Result GETREST Num:" + Task.StartNum + " from " + global.NodeName(Info.Node), 2)
|
|
return;
|
|
}
|
|
if (global.CompareArrL(Data.ProofHash, Context.TxProof.AccHash) !== 0) {
|
|
global.ToLog("ERROR PROOF HASH Result GETREST Num:" + Task.StartNum + " Hash: " + global.GetHexFromArr(Data.ProofHash) + "/" + global.GetHexFromArr(Context.TxProof.AccHash) + " from " + global.NodeName(Info.Node),
|
|
2)
|
|
return;
|
|
}
|
|
var ArrM = [];
|
|
for (var i = 0; i < Data.Arr.length; i++) {
|
|
ArrM[i] = global.shaarr(Data.Arr[i])
|
|
}
|
|
var GetHash = global.CheckMerkleProof(Data.ProofArrL, ArrM, Data.ProofArrR);
|
|
if (global.CompareArrL(GetHash, Context.TxProof.AccHash) !== 0) {
|
|
global.ToLog("ERROR CALC PROOF HASH Result GETREST Num:" + Task.StartNum + " Hash: " + global.GetHexFromArr(GetHash) + "/" + global.GetHexFromArr(Context.TxProof.AccHash) + " from " + global.NodeName(Info.Node),
|
|
2)
|
|
return;
|
|
}
|
|
global.ToLog("OK Result GETREST Num:" + Task.StartNum + " arr=" + Data.Arr.length + " from " + global.NodeName(Info.Node), 2)
|
|
if (!global.TX_PROCESS || !global.TX_PROCESS.RunRPC) {
|
|
global.ToLog("ERROR global.TX_PROCESS")
|
|
return;
|
|
}
|
|
Task.OK = true
|
|
global.TX_PROCESS.RunRPC("TXWriteAccArr", { StartNum: Task.StartNum, Arr: Data.Arr }, function(Err, Params) {
|
|
Context.AccTaskFinished++
|
|
})
|
|
}
|
|
},
|
|
})
|
|
Task.Time = CurTime
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case 8:
|
|
if (Context.SmartTaskFinished === Context.SmartTaskList.length) {
|
|
Context.Mode++
|
|
global.ToLog("Next mode: " + Context.Mode, 2)
|
|
break;
|
|
}
|
|
var CurTime = Date.now();
|
|
for (var i = 0; i < Context.SmartTaskList.length; i++) {
|
|
let Task = Context.SmartTaskList[i];
|
|
var Delta = CurTime - Task.Time;
|
|
if (Delta > 3 * 1000 && !Task.OK) {
|
|
var Ret = this.GetNextNode(Task, "", 1);
|
|
if (Ret.Result) {
|
|
global.ToLog("Send GETSMART Num:" + Task.StartNum + "-" + Task.Count + " to " + global.NodeName(Ret.Node), 2)
|
|
var SELF = this;
|
|
this.SendF(Ret.Node, {
|
|
"Method": "GETSMART", "Data": { BlockNum: Context.BlockNumRest, SmartNum: Task.StartNum, Count: Task.Count },
|
|
"Context": {
|
|
F: function(Info) {
|
|
if (Task.OK)
|
|
return;
|
|
var Data = SELF.DataFromF(Info);
|
|
if (!Data.Result)
|
|
return;
|
|
global.ToLog("Result GETSMART Num:" + Task.StartNum + " arr=" + Data.Arr.length + " from " + global.NodeName(Info.Node), 2)
|
|
Task.Node = Info.Node
|
|
if (!global.TX_PROCESS || !global.TX_PROCESS.RunRPC)
|
|
return;
|
|
Task.OK = true
|
|
global.TX_PROCESS.RunRPC("TXWriteSmartArr", { StartNum: Task.StartNum, Arr: Data.Arr }, function(Err, Params) {
|
|
Context.SmartTaskFinished++
|
|
})
|
|
}
|
|
},
|
|
})
|
|
Task.Time = CurTime
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case 9:
|
|
if (!global.TX_PROCESS || !global.TX_PROCESS.RunRPC)
|
|
return;
|
|
var ErrSmartNum = CheckHashSmarts(Context.TxProof.SmartHash);
|
|
if (ErrSmartNum > 0) {
|
|
var Str = "Error hash in smart num: " + ErrSmartNum;
|
|
global.ToLog(Str, 2)
|
|
var t = Math.trunc(ErrSmartNum / global.MAX_SMARTS_TRANSFER);
|
|
var Task = Context.SmartTaskList[t];
|
|
if (!Task) {
|
|
global.ToLog("error task number: " + t)
|
|
Context.Mode = 100
|
|
} else {
|
|
Task.OK = false
|
|
Context.Mode--
|
|
Context.SmartTaskFinished--
|
|
this.AddToBan(Task.Node, Str)
|
|
}
|
|
break;
|
|
}
|
|
var SELF = this;
|
|
global.TX_PROCESS.RunRPC("TXWriteAccHash", {}, function(Err, Params) {
|
|
if (!Params)
|
|
return;
|
|
if (global.CompareArr(Context.TxProof.AccHash, Params.AccHash) === 0 && global.CompareArr(Context.TxProof.SmartHash, Params.SmartHash) === 0) {
|
|
Context.Mode++
|
|
global.ToLog("Next mode: " + Context.Mode, 2)
|
|
}
|
|
else {
|
|
global.ToLog("ERROR RESTS LOAD:")
|
|
global.ToLog("Must AccHash:" + global.GetHexFromArr(Context.TxProof.AccHash))
|
|
global.ToLog("Must SmartHash:" + global.GetHexFromArr(Context.TxProof.SmartHash))
|
|
global.ToLog("Write AccHash:" + global.GetHexFromArr(Params.AccHash))
|
|
global.ToLog("Write SmartHash:" + global.GetHexFromArr(Params.SmartHash))
|
|
SELF.BlockNumDB = 0
|
|
SELF.BlockNumDBMin = 0
|
|
SELF.UseTruncateBlockDB = undefined
|
|
global.TX_PROCESS.RunRPC("TXPrepareLoadRest", 0, function(Err, Params) {
|
|
})
|
|
Context.Mode = 100
|
|
}
|
|
})
|
|
Context.Mode++
|
|
global.ToLog("Next mode: " + Context.Mode, 2)
|
|
break;
|
|
case 10:
|
|
break;
|
|
case 11:
|
|
var Context2 = this.LoadHistoryContext;
|
|
Context2.BlockNum = this.LoadRestContext.BlockNumRest
|
|
Context2.StartTimeHistory = Date.now()
|
|
Context.Mode = 200
|
|
break;
|
|
case 200:
|
|
global.ToLog("Error state!")
|
|
break;
|
|
}
|
|
}
|
|
GetBlockArrFromBuffer_Load(BufRead: Buffer, Info?: SocketSendInfo): any {
|
|
//defiend in block-loader.ts(CBlock)
|
|
}
|
|
RETBLOCKHEADER_REST(Info: SocketSendInfo, CurTime: number) {
|
|
if (Info.Node.SendRestGetHeader === 2)
|
|
return;
|
|
Info.Node.SendRestGetHeader = 2
|
|
var Context = this.LoadRestContext;
|
|
var BufRead = global.BufLib.GetReadBuffer(Info.Data);
|
|
var arr = this.GetBlockArrFromBuffer_Load(BufRead, Info);
|
|
global.ToLog("RETBLOCKHEADER_FOWARD SyncRest from " + global.NodeName(Info.Node) + " arr=" + arr.length, 2)
|
|
Context.ReceiveHeaderCount++
|
|
var MinSumPow = 10 * Context.CountProof;
|
|
var SumPower = 0;
|
|
if (arr.length >= Context.CountProof)
|
|
for (var i = 0; i < Context.CountProof; i++) {
|
|
SumPower += arr[i].Power
|
|
}
|
|
if (SumPower <= MinSumPow)
|
|
SumPower = 0
|
|
Context.ArrProof.push({ Node: Info.Node, SumPower: SumPower, arr: arr, BufRead: BufRead })
|
|
}
|
|
static GETSMART_F() {
|
|
return "{\
|
|
SmartNum:uint,\
|
|
Count:uint,\
|
|
}";
|
|
}
|
|
static RETSMART_F() {
|
|
return DB_FORMAT.FORMAT_SMART_TRANSFER;
|
|
}
|
|
static GETREST_F() {
|
|
return "{\
|
|
BlockNum:uint,\
|
|
AccNum:uint,\
|
|
Count:uint,\
|
|
AccHash:hash,\
|
|
}";
|
|
}
|
|
static RETREST_F() {
|
|
return DB_FORMAT.FORMAT_REST_TRANSFER;
|
|
}
|
|
SendLoadToBegin() {
|
|
return;
|
|
if (!this.BlockNumDBMin)
|
|
return;
|
|
if (!this.ContextSendLoadToBegin)
|
|
this.ContextSendLoadToBegin = { Time: 0, MapSend: {} }
|
|
var Context = this.ContextSendLoadToBegin;
|
|
var CurTime = Date.now();
|
|
var Delta = CurTime - Context.Time;
|
|
if (Delta < 2 * 1000)
|
|
return;
|
|
var BlockDB = this.ReadBlockHeaderDB(this.BlockNumDBMin + 1);
|
|
if (!BlockDB)
|
|
return;
|
|
Context.BlockNum = BlockDB.BlockNum
|
|
var Ret = this.GetNextNode(Context, Context.BlockNum, 1);
|
|
if (Ret.Result) {
|
|
var Node = Ret.Node;
|
|
global.ToLog("LOAD_TO_BEGIN - from: " + BlockDB.BlockNum + " to " + global.NodeName(Node), 2)
|
|
Context.Time = CurTime
|
|
this.SendF(Node, {
|
|
"Method": "GETBLOCKHEADER", "Data": { Foward: 0, BlockNum: Context.BlockNum, Hash: BlockDB.Hash, IsSum: 0, Count: global.COUNT_HISTORY_BLOCKS_FOR_LOAD },
|
|
"Context": {
|
|
F: function(Info) {
|
|
global.ToLog("GET LOAD_TO_BEGIN from " + global.NodeName(Info.Node) + " Length=" + Info.Data.length, 2)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
};
|
|
|
|
function CheckHashSmarts(LastSumHash: Buffer) {
|
|
global.DApps.Smart.Close();
|
|
var MaxNum = global.DApps.Smart.GetMaxNum();
|
|
var Item = global.DApps.Smart.DBSmart.Read(MaxNum);
|
|
if (global.CompareArr(Item.SumHash, LastSumHash) !== 0)
|
|
return MaxNum;
|
|
var WorkStruct = {};
|
|
for (var Num = MaxNum; Num >= 1; Num--) {
|
|
var PrevItem = global.DApps.Smart.DBSmart.Read(Num - 1);
|
|
if (!PrevItem)
|
|
return Num;
|
|
var WasSumHash = Item.SumHash;
|
|
Item.SumHash = [];
|
|
var Buf = global.BufLib.GetBufferFromObject(Item, DB_FORMAT.FORMAT_SMART_ROW, 20000, WorkStruct);
|
|
var Hash = global.sha3(Buf);
|
|
var SumHash = global.sha3arr2(PrevItem.SumHash, Hash);
|
|
if (global.CompareArr(SumHash, WasSumHash) !== 0)
|
|
return Num;
|
|
Item = PrevItem;
|
|
}
|
|
return 0;
|
|
};
|