@tradelayerprotocol/tradelayer 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/.claude/settings.local.json +13 -0
  2. package/.claude/skills/tl-algo/SKILL.md +255 -0
  3. package/.gitattributes +2 -0
  4. package/.github/workflows/publish.yaml +26 -0
  5. package/4mm.js +163 -0
  6. package/LICENSE +21 -0
  7. package/NPMSwapRefactor.zip +0 -0
  8. package/README.md +217 -0
  9. package/address.sh +26 -0
  10. package/algoAPI.js +581 -0
  11. package/analyzepsbt.js +92 -0
  12. package/apiEx.js +99 -0
  13. package/bb_hyperscalper.js +290 -0
  14. package/bbo_demo.js +111 -0
  15. package/buyer.js +622 -0
  16. package/client.js +50 -0
  17. package/createTxTest.js +26 -0
  18. package/createWallet.js +75 -0
  19. package/daytrader.js +531 -0
  20. package/decodeTest.js +69 -0
  21. package/fundingManager.js +144 -0
  22. package/index.js +4 -0
  23. package/listener.js +27 -0
  24. package/litecoreTxBuilder.js +1128 -0
  25. package/mmEx.js +356 -0
  26. package/networks.js +51 -0
  27. package/orderbook.js +200 -0
  28. package/package.json +34 -0
  29. package/perTradeQueue.js +36 -0
  30. package/projectsTLNPMTLNPM/package-lock.json +162 -0
  31. package/projectsTLNPMTLNPM/package.json +5 -0
  32. package/quick.js +32 -0
  33. package/quickFut.js +37 -0
  34. package/quickSell.js +37 -0
  35. package/relayerClient.js +117 -0
  36. package/run4mm.js +80 -0
  37. package/run_bbo_tracker.js +241 -0
  38. package/seller.js +443 -0
  39. package/session.js +45 -0
  40. package/setup-lin-ltc.sh +139 -0
  41. package/setup-lin.sh +203 -0
  42. package/setup-win-ltc.bat +108 -0
  43. package/setup-win.bat +167 -0
  44. package/spam_screamer_futures.js +222 -0
  45. package/tradelayer.js/.gitattributes +2 -0
  46. package/tradelayer.js/README.md +2 -0
  47. package/tradelayer.js/oldTests/activationTest.js +6 -0
  48. package/tradelayer.js/oldTests/base58.test.js +23 -0
  49. package/tradelayer.js/oldTests/base64Decode.test.js +16 -0
  50. package/tradelayer.js/oldTests/blocksRefactor.js +140 -0
  51. package/tradelayer.js/oldTests/checkVestBalance.js +25 -0
  52. package/tradelayer.js/oldTests/consensusHashProto.js +151 -0
  53. package/tradelayer.js/oldTests/contractOrderbook.js +243 -0
  54. package/tradelayer.js/oldTests/createPayload.js +0 -0
  55. package/tradelayer.js/oldTests/createTestnetAddr.js +43 -0
  56. package/tradelayer.js/oldTests/decode.js +205 -0
  57. package/tradelayer.js/oldTests/decodeTest.js +50 -0
  58. package/tradelayer.js/oldTests/displayTallyMap.js +19 -0
  59. package/tradelayer.js/oldTests/encodeDecode.js +340 -0
  60. package/tradelayer.js/oldTests/expressTest.js +29 -0
  61. package/tradelayer.js/oldTests/extractBlocksVanilla.js +214 -0
  62. package/tradelayer.js/oldTests/extractBlocksVanillaa.js +179 -0
  63. package/tradelayer.js/oldTests/extractPubkeyTest.js +60 -0
  64. package/tradelayer.js/oldTests/fillInputCacheProto.js +111 -0
  65. package/tradelayer.js/oldTests/getRawTxTest.js +22 -0
  66. package/tradelayer.js/oldTests/indexTest.js +26 -0
  67. package/tradelayer.js/oldTests/initTokensTest.js +32 -0
  68. package/tradelayer.js/oldTests/interfaceChild.js +129 -0
  69. package/tradelayer.js/oldTests/listenerChild.js +112 -0
  70. package/tradelayer.js/oldTests/opdecode.js +26 -0
  71. package/tradelayer.js/oldTests/options.js +79 -0
  72. package/tradelayer.js/oldTests/optxtest.js +116 -0
  73. package/tradelayer.js/oldTests/optxtest1.js +64 -0
  74. package/tradelayer.js/oldTests/oracle.test.js +32 -0
  75. package/tradelayer.js/oldTests/orderbook.test.js +36 -0
  76. package/tradelayer.js/oldTests/parsing.js +93 -0
  77. package/tradelayer.js/oldTests/payload.js +13 -0
  78. package/tradelayer.js/oldTests/persistenceUnitTest.js +23 -0
  79. package/tradelayer.js/oldTests/property.test.js +53 -0
  80. package/tradelayer.js/oldTests/propertyLevel.js +75 -0
  81. package/tradelayer.js/oldTests/propertyTest.js +32 -0
  82. package/tradelayer.js/oldTests/queryAddressTest.js +17 -0
  83. package/tradelayer.js/oldTests/salter.js +14 -0
  84. package/tradelayer.js/oldTests/tally.js +81 -0
  85. package/tradelayer.js/oldTests/tally.test.js +48 -0
  86. package/tradelayer.js/oldTests/tally2.js +124 -0
  87. package/tradelayer.js/oldTests/tally3.js +142 -0
  88. package/tradelayer.js/oldTests/tallyDiag.js +38 -0
  89. package/tradelayer.js/oldTests/testGetRaw.js +40 -0
  90. package/tradelayer.js/oldTests/testHexConvert.js +47 -0
  91. package/tradelayer.js/oldTests/testNewEncoding.js +96 -0
  92. package/tradelayer.js/oldTests/testNewEncoding2.js +113 -0
  93. package/tradelayer.js/oldTests/testNewEncoding3 +112 -0
  94. package/tradelayer.js/oldTests/testNewEncoding3.js +168 -0
  95. package/tradelayer.js/oldTests/testOPReturn.js +102 -0
  96. package/tradelayer.js/oldTests/testPayload.js +23 -0
  97. package/tradelayer.js/oldTests/testRaw.js +50 -0
  98. package/tradelayer.js/oldTests/testSendTooMuch.js +20 -0
  99. package/tradelayer.js/oldTests/testTxBuild +28 -0
  100. package/tradelayer.js/oldTests/testTxBuild.js +42 -0
  101. package/tradelayer.js/oldTests/tokenOrderbook.js +243 -0
  102. package/tradelayer.js/oldTests/txUtilsA.js +515 -0
  103. package/tradelayer.js/oldTests/validityUnitTest.js +53 -0
  104. package/tradelayer.js/oldTests/vaults.js +72 -0
  105. package/tradelayer.js/oldTests/volumeIndex.js +117 -0
  106. package/tradelayer.js/oldTests/volumeIndex2.js +88 -0
  107. package/tradelayer.js/output_base64.txt +1 -0
  108. package/tradelayer.js/package-lock.json +9967 -0
  109. package/tradelayer.js/package.json +61 -0
  110. package/tradelayer.js/server/index.js +88 -0
  111. package/tradelayer.js/server/litecoind.exe +0 -0
  112. package/tradelayer.js/src/activation.js +303 -0
  113. package/tradelayer.js/src/adjuster.js +77 -0
  114. package/tradelayer.js/src/amm.js +400 -0
  115. package/tradelayer.js/src/base256.js +55 -0
  116. package/tradelayer.js/src/base94.js +79 -0
  117. package/tradelayer.js/src/channels.js +1163 -0
  118. package/tradelayer.js/src/clearing.js +3109 -0
  119. package/tradelayer.js/src/clearlist.js +364 -0
  120. package/tradelayer.js/src/client.js +295 -0
  121. package/tradelayer.js/src/consensus.js +613 -0
  122. package/tradelayer.js/src/contractRegistry.js +964 -0
  123. package/tradelayer.js/src/db.js +89 -0
  124. package/tradelayer.js/src/init.js +24 -0
  125. package/tradelayer.js/src/insurance.js +347 -0
  126. package/tradelayer.js/src/interface.js +218 -0
  127. package/tradelayer.js/src/interfaceExpress.js +178 -0
  128. package/tradelayer.js/src/iou.js +509 -0
  129. package/tradelayer.js/src/listener.js +226 -0
  130. package/tradelayer.js/src/logic.js +1702 -0
  131. package/tradelayer.js/src/main.js +927 -0
  132. package/tradelayer.js/src/marginMap.js +2165 -0
  133. package/tradelayer.js/src/options.js +126 -0
  134. package/tradelayer.js/src/oracle.js +394 -0
  135. package/tradelayer.js/src/orderbook.js +4123 -0
  136. package/tradelayer.js/src/persistence.js +554 -0
  137. package/tradelayer.js/src/property.js +411 -0
  138. package/tradelayer.js/src/reOrg.js +41 -0
  139. package/tradelayer.js/src/scaling.js +145 -0
  140. package/tradelayer.js/src/tally.js +1275 -0
  141. package/tradelayer.js/src/tradeHistoryManager.js +552 -0
  142. package/tradelayer.js/src/txDecoder.js +584 -0
  143. package/tradelayer.js/src/txEncoder.js +610 -0
  144. package/tradelayer.js/src/txIndex.js +502 -0
  145. package/tradelayer.js/src/txUtils.js +1392 -0
  146. package/tradelayer.js/src/types.js +429 -0
  147. package/tradelayer.js/src/validity.js +3077 -0
  148. package/tradelayer.js/src/vaults.js +430 -0
  149. package/tradelayer.js/src/vesting.js +491 -0
  150. package/tradelayer.js/src/volumeIndex.js +618 -0
  151. package/tradelayer.js/src/walletInterface.js +220 -0
  152. package/tradelayer.js/src/walletListener.js +665 -0
  153. package/tradelayer.js/tests/256decode.js +82 -0
  154. package/tradelayer.js/tests/UTXOracle.js +205 -0
  155. package/tradelayer.js/tests/base94test.js +23 -0
  156. package/tradelayer.js/tests/cancelTxTest.js +62 -0
  157. package/tradelayer.js/tests/contractInterfaceTest.js +48 -0
  158. package/tradelayer.js/tests/decimalTest.js +65 -0
  159. package/tradelayer.js/tests/decoderTest.js +100 -0
  160. package/tradelayer.js/tests/deltaCount.js +47 -0
  161. package/tradelayer.js/tests/deltaCount2.js +60 -0
  162. package/tradelayer.js/tests/interfaceTest.js +37 -0
  163. package/tradelayer.js/tests/mainTest.js +53 -0
  164. package/tradelayer.js/tests/makeActivationTest.js +24 -0
  165. package/tradelayer.js/tests/maxHeightTest.js +49 -0
  166. package/tradelayer.js/tests/reverseHash.js +72 -0
  167. package/tradelayer.js/tests/sensitiveConsoleOutput.txt +267 -0
  168. package/tradelayer.js/tests/tallyTest.js +40 -0
  169. package/tradelayer.js/tests/testBuybacks.js +46 -0
  170. package/tradelayer.js/tests/testCodeHash.js +49 -0
  171. package/tradelayer.js/tests/testConsensusHash.js +91 -0
  172. package/tradelayer.js/tests/testDecode.js +30 -0
  173. package/tradelayer.js/tests/testEncodingLengths.js +129 -0
  174. package/tradelayer.js/tests/testGetTx +32 -0
  175. package/tradelayer.js/tests/testGetTx.js +32 -0
  176. package/tradelayer.js/tests/testHexHash.js +32 -0
  177. package/tradelayer.js/tests/testIndexHash.js +35 -0
  178. package/tradelayer.js/tests/testInitContracts.js +38 -0
  179. package/tradelayer.js/tests/testMaxConsensus.js +12 -0
  180. package/tradelayer.js/tests/testMaxSynth.js +44 -0
  181. package/tradelayer.js/tests/testMint.js +21 -0
  182. package/tradelayer.js/tests/testNetwork.js +33 -0
  183. package/tradelayer.js/tests/testOrderbookLoad.js +62 -0
  184. package/tradelayer.js/tests/testRebates.js +32 -0
  185. package/tradelayer.js/tests/testRedeem.js +22 -0
  186. package/tradelayer.js/tests/testTokenTrade.js +39 -0
  187. package/tradelayer.js/tests/testTxBuild.js +42 -0
  188. package/tradelayer.js/tests/testUTXOTrade.js +27 -0
  189. package/tradelayer.js/tests/tokenTradeHistory.js +27 -0
  190. package/tradelayer.js/tests/tradeFutures.js +40 -0
  191. package/tradelayer.js/tests/tradeHistoryExample.js +35 -0
  192. package/tradelayer.js/tests/tradeHistoryLoad.js +15 -0
  193. package/tradelayer.js/tests/txScanTest.js +134 -0
  194. package/tradelayer.js/tests/validateTest.js +136 -0
  195. package/tradelayer.js/tests/vestingTest.js +37 -0
  196. package/tradelayer.js/utils/activateMainnet.js +59 -0
  197. package/tradelayer.js/utils/activateMainnetDoge.js +63 -0
  198. package/tradelayer.js/utils/autocompactdb.js +23 -0
  199. package/tradelayer.js/utils/base64toHex.js +32 -0
  200. package/tradelayer.js/utils/broadcastDoge.js +38 -0
  201. package/tradelayer.js/utils/calcRedeem.js +19 -0
  202. package/tradelayer.js/utils/checkNetwork.js +27 -0
  203. package/tradelayer.js/utils/createAddress.js +48 -0
  204. package/tradelayer.js/utils/createAttestation.js +133 -0
  205. package/tradelayer.js/utils/createContract.js +118 -0
  206. package/tradelayer.js/utils/createOracle.js +94 -0
  207. package/tradelayer.js/utils/createwallet.js +20 -0
  208. package/tradelayer.js/utils/crossFuturesTrades.js +57 -0
  209. package/tradelayer.js/utils/crossTokenTrades.js +62 -0
  210. package/tradelayer.js/utils/dumpPriv.js +29 -0
  211. package/tradelayer.js/utils/generateChannel.js +34 -0
  212. package/tradelayer.js/utils/getInfo.js +21 -0
  213. package/tradelayer.js/utils/hardWipe.js +20 -0
  214. package/tradelayer.js/utils/hexTo64.js +16 -0
  215. package/tradelayer.js/utils/importAddress.js +28 -0
  216. package/tradelayer.js/utils/importpriv.js +20 -0
  217. package/tradelayer.js/utils/issueOracleContract.js +67 -0
  218. package/tradelayer.js/utils/issueTokens.js +41 -0
  219. package/tradelayer.js/utils/listunspent.js +66 -0
  220. package/tradelayer.js/utils/litecoinClient.js +30 -0
  221. package/tradelayer.js/utils/loadwallet.js +20 -0
  222. package/tradelayer.js/utils/publishOracle.js +113 -0
  223. package/tradelayer.js/utils/sendActivation.js +21 -0
  224. package/tradelayer.js/utils/sendChannelContractTrade.js +34 -0
  225. package/tradelayer.js/utils/sendChannelTokenTrade.js +34 -0
  226. package/tradelayer.js/utils/sendCommit.js +24 -0
  227. package/tradelayer.js/utils/sendDoge.js +62 -0
  228. package/tradelayer.js/utils/sendDogeMain.js +67 -0
  229. package/tradelayer.js/utils/sendDogeTx.js +46 -0
  230. package/tradelayer.js/utils/sendLTC.js +63 -0
  231. package/tradelayer.js/utils/sendMainnet.js +62 -0
  232. package/tradelayer.js/utils/sendTransfer.js +19 -0
  233. package/tradelayer.js/utils/sendVestTest.js +88 -0
  234. package/tradelayer.js/utils/sendWithdrawal.js +26 -0
  235. package/tradelayer.js/utils/simpleStart.js +8 -0
  236. package/tradelayer.js/utils/startStop.js +27 -0
  237. package/tradelayer.js/utils/structuredTrades.js +136 -0
  238. package/tradelayer.js/utils/verifySignature.js +90 -0
  239. package/tradelayer.js/utils/verifyWitnessAndScriptPubkey.js +41 -0
  240. package/tradelayer.js/utils/walletCache.js +172 -0
  241. package/tradelayer.js/utils/walletContractInterface.js +48 -0
  242. package/tradelayer.js/utils/walletFetchTxs.js +66 -0
  243. package/tradelayer.js/utils/walletUtils.js +97 -0
  244. package/tradelayer.js/utils/wipeDB.js +55 -0
  245. package/tradelayer.js/utils/wipeDBNotTx.js +50 -0
  246. package/txEncoder.js +529 -0
  247. package/utility.js +28 -0
  248. package/verifymessage.js +38 -0
  249. package/ws-transport.js +311 -0
@@ -0,0 +1,4123 @@
1
+ const BigNumber = require('bignumber.js')
2
+ const dbInstance = require('./db.js'); // Import your database instance
3
+ const { v4: uuidv4 } = require('uuid'); // Import the v4 function from the uuid library
4
+ const TradeHistory = require('./tradeHistoryManager.js')
5
+ const ContractRegistry = require('./contractRegistry.js')
6
+ const VolumeIndex= require('./volumeIndex.js')
7
+ const Channels = require('./channels.js')
8
+ const ClearList = require('./clearlist.js')
9
+ const Consensus = require('./consensus.js')
10
+ const PnlIou = require('./iou.js')
11
+ const Clearing = require('./clearing.js')
12
+
13
+ // Helper: rank a single character with "alphabetical then numerical"
14
+ function addressCharRank(ch) {
15
+ if (!ch) return { group: 2, char: '' }; // missing chars sort last
16
+ const isDigit = ch >= '0' && ch <= '9';
17
+ return {
18
+ group: isDigit ? 1 : 0, // 0 = letters, 1 = digits, 2 = missing
19
+ char: ch.toLowerCase()
20
+ };
21
+ }
22
+
23
+ // Helper: compare two sender addresses by last, then 2nd-last, then 3rd-last char
24
+ // Helper: compare two sender addresses by last, then 2nd-last, then 3rd-last char,
25
+ // with optional txid tie-break for full determinism
26
+ function compareSenderAddresses(a, b, txidA = null, txidB = null) {
27
+ const aLen = a.length;
28
+ const bLen = b.length;
29
+
30
+ const aChars = [a[aLen - 1], a[aLen - 2], a[aLen - 3]];
31
+ const bChars = [b[bLen - 1], b[bLen - 2], b[bLen - 3]];
32
+
33
+ for (let i = 0; i < 3; i++) {
34
+ const ra = addressCharRank(aChars[i]);
35
+ const rb = addressCharRank(bChars[i]);
36
+
37
+ if (ra.group !== rb.group) return ra.group - rb.group;
38
+ if (ra.char < rb.char) return -1;
39
+ if (ra.char > rb.char) return 1;
40
+ }
41
+
42
+ // fallback: full address lexicographically
43
+ const addrCmp = a.localeCompare(b);
44
+ if (addrCmp !== 0) return addrCmp;
45
+
46
+ // FINAL deterministic tie-breaker (optional)
47
+ if (txidA && txidB) {
48
+ return txidA.localeCompare(txidB);
49
+ }
50
+
51
+ return 0;
52
+ }
53
+
54
+
55
+ class Orderbook {
56
+ constructor(orderBookKey, tickSize = new BigNumber('0.00000001')) {
57
+ this.tickSize = tickSize;
58
+ this.orderBookKey = orderBookKey; // Unique identifier for each orderbook (contractId or propertyId pair)
59
+ this.orderBooks = {};
60
+ this.block = 1
61
+ //this.loadOrderBook(); // Load or create an order book based on the orderBookKey
62
+ }
63
+ // Static async method to get an instance of Orderbook
64
+ static async getOrderbookInstance(orderBookKey) {
65
+ const orderbook = new Orderbook(orderBookKey); // Create instance
66
+ orderbook.orderBooks[orderBookKey] = await orderbook.loadOrderBook(orderBookKey); // Load orderbook
67
+ console.log("Returning Orderbook instance:", orderbook);
68
+ return orderbook;
69
+ }
70
+
71
+ async loadOrderBook(key) {
72
+ const stringKey = typeof key === 'string' ? key : String(key);
73
+ const orderBooksDB = await dbInstance.getDatabase('orderBooks');
74
+
75
+ try {
76
+ const orderBookData = await orderBooksDB.findOneAsync({ _id: stringKey });
77
+ if (orderBookData && orderBookData.value) {
78
+ const parsedOrderBook = JSON.parse(orderBookData.value);
79
+ this.orderBooks[key] = parsedOrderBook;
80
+ //console.log('loading the orderbook in check from addr '+addr+' for ' + key + ' in the form of ' + JSON.stringify(parsedOrderBook.buy));
81
+ return parsedOrderBook; // Return the parsed order book
82
+ } else {
83
+ console.log('new orderbook for ' + key);
84
+ return { buy: [], sell: [] };
85
+ }
86
+ } catch (error) {
87
+ console.error('Error loading or parsing order book data:', error);
88
+ return { buy: [], sell: [] }; // Return an empty order book on error
89
+ }
90
+ }
91
+
92
+ async saveOrderBook(orderbookData, key) {
93
+ const stringKey = String(key); // 🔒 normalize always to string
94
+ console.log('saving orderbook with key ' + stringKey);
95
+
96
+ const orderBooksDB = await dbInstance.getDatabase('orderBooks');
97
+
98
+ await orderBooksDB.updateAsync(
99
+ { _id: stringKey },
100
+ { _id: stringKey, value: JSON.stringify(orderbookData) },
101
+ { upsert: true }
102
+ );
103
+
104
+ return;
105
+ }
106
+
107
+
108
+ async saveTrade(tradeRecord) {
109
+ const tradeDB =await dbInstance.getDatabase('tradeHistory');
110
+
111
+ const uuid = uuidv4();
112
+
113
+ // Use the key provided in the trade record for storage
114
+ const tradeId = `${tradeRecord.key}-${uuid}-${tradeRecord.blockHeight}`;
115
+
116
+ // Construct the document to be saved
117
+ const tradeDoc = {
118
+ _id: tradeId,
119
+ ...tradeRecord
120
+ };
121
+
122
+ // Save or update the trade record in the database
123
+ try {
124
+ await tradeDB.updateAsync(
125
+ { _id: tradeId },
126
+ tradeDoc,
127
+ { upsert: true }
128
+ );
129
+ //console.log(`Trade record saved successfully: ${tradeId}`);
130
+ } catch (error) {
131
+ //console.error(`Error saving trade record: ${tradeId}`, error);
132
+ throw error; // Rethrow the error for handling upstream
133
+ }
134
+ }
135
+
136
+ // Record a token trade with specific key identifiers
137
+ async recordTokenTrade(trade, blockHeight, txid) {
138
+ const tradeRecordKey = `token-${trade.offeredPropertyId}-${trade.desiredPropertyId}`;
139
+ const tradeRecord = {
140
+ key: tradeRecordKey,
141
+ type: 'token',
142
+ trade,
143
+ blockHeight,
144
+ txid
145
+ };
146
+ await this.saveTrade(tradeRecord);
147
+ }
148
+
149
+ // Record a contract trade with specific key identifiers
150
+ async recordContractTrade(trade, blockHeight, sellerTx, buyerTx) {
151
+ const tradeRecordKey = `contract-${trade.contractId}`;
152
+ const tradeRecord = {
153
+ key: tradeRecordKey,
154
+ type: 'contract',
155
+ trade,
156
+ blockHeight,
157
+ sellerTx,
158
+ buyerTx
159
+ };
160
+ //console.log('saving contract trade ' +JSON.stringify(trade))
161
+ await this.saveTrade(tradeRecord);
162
+ }
163
+ // Retrieve token trading history by propertyId pair
164
+ static async getTokenTradeHistoryByPropertyIdPair(propertyId1, propertyId2) {
165
+ const tradeDB = await dbInstance.getDatabase('tradeHistory');
166
+ const tradeRecordKey = `token-${propertyId1}-${propertyId2}`;
167
+ const trades = await tradeDB.findAsync({ key: tradeRecordKey });
168
+ return trades.map(doc => doc.trade);
169
+ }
170
+
171
+ // Retrieve contract trading history by contractId
172
+ static async getContractTradeHistoryByContractId(contractId) {
173
+ //console.log('loading trade history for '+contractId)
174
+ const tradeDB = await dbInstance.getDatabase('tradeHistory');
175
+ const tradeRecordKey = `contract-${contractId}`;
176
+ const trades = await tradeDB.findAsync({ key: tradeRecordKey });
177
+ return trades.map(doc => doc.trade);
178
+ }
179
+
180
+ // Retrieve trade history by address for both token and contract trades
181
+ static async getTradeHistoryByAddress(address) {
182
+ const tradeDB = await dbInstance.getDatabase('tradeHistory');
183
+ const trades = await tradeDB.findAsync({
184
+ $or: [{ 'trade.sender': address }, { 'trade.receiverAddress': address }]
185
+ });
186
+ return trades.map(doc => doc.trade);
187
+ }
188
+
189
+ // Function to divide two numbers with an option to round up or down to the nearest Satoshi
190
+ divideAndRound(number1, number2, roundUp = false) {
191
+ const result = new BigNumber(number1).dividedBy(new BigNumber(number2));
192
+ return roundUp
193
+ ? result.decimalPlaces(8, BigNumber.ROUND_UP).toString()
194
+ : result.decimalPlaces(8, BigNumber.ROUND_DOWN).toString();
195
+ }
196
+
197
+ // Ensure we have the global pending queue
198
+ // --- DB-backed on-chain order queue using the orderbook DB ---
199
+ static async _getOrderbookDB() {
200
+ // IMPORTANT: use the same name you already use for the orderbook collection
201
+ // e.g. dbInstance.getDatabase('orderbook') or dbInstance.getDatabase('orderBooks')
202
+ return dbInstance.getDatabase('orderBooks');
203
+ }
204
+
205
+ static async _addActivePair(pairKey) {
206
+ const db = await this._getOrderbookDB();
207
+ const doc = await db.findOneAsync({ _id: 'activePairs' });
208
+ let pairs = (doc && Array.isArray(doc.pairs)) ? doc.pairs : [];
209
+
210
+ if (!pairs.includes(pairKey)) {
211
+ pairs.push(pairKey);
212
+ await db.updateAsync(
213
+ { _id: 'activePairs' },
214
+ { _id: 'activePairs', pairs },
215
+ { upsert: true }
216
+ );
217
+ await db.loadDatabase();
218
+ }
219
+ }
220
+
221
+ static async _updateActivePairs(pairs) {
222
+ const db = await this._getOrderbookDB();
223
+ await db.updateAsync(
224
+ { _id: 'activePairs' },
225
+ { _id: 'activePairs', pairs },
226
+ { upsert: true }
227
+ );
228
+ await db.loadDatabase();
229
+ }
230
+
231
+ /**
232
+ * Queue a token:token on-chain order (tx type 5) under key "queue-<pairKey>".
233
+ */
234
+ static async queueOnChainTokenOrder(orderBookKey, sender, order, blockHeight, txid) {
235
+ const db = await this._getOrderbookDB();
236
+ const pairKey = String(orderBookKey);
237
+ const queueId = `queue-${pairKey}`;
238
+
239
+ const doc = await db.findOneAsync({ _id: queueId });
240
+ const entries = (doc && Array.isArray(doc.orders)) ? doc.orders : [];
241
+
242
+ entries.push({
243
+ kind: 'token',
244
+ orderBookKey: pairKey,
245
+ sender,
246
+ blockHeight: Number(blockHeight),
247
+ txid,
248
+ order
249
+ });
250
+
251
+ await db.updateAsync(
252
+ { _id: queueId },
253
+ { _id: queueId, orders: entries },
254
+ { upsert: true }
255
+ );
256
+ await db.loadDatabase();
257
+ await this._addActivePair(pairKey);
258
+ }
259
+
260
+ /**
261
+ * Queue a contract on-chain order (tx type 18) under key "queue-<contractId>".
262
+ */
263
+ static async queueOnChainContractOrder(contractId, sender, params, blockHeight, txid) {
264
+ const db = await this._getOrderbookDB();
265
+ const pairKey = String(contractId); // reuse the same pattern
266
+ const queueId = `queue-${pairKey}`;
267
+
268
+ const doc = await db.findOneAsync({ _id: queueId });
269
+ const entries = (doc && Array.isArray(doc.orders)) ? doc.orders : [];
270
+
271
+ entries.push({
272
+ kind: 'contract',
273
+ orderBookKey: pairKey,
274
+ sender,
275
+ blockHeight: Number(blockHeight),
276
+ txid,
277
+ params
278
+ });
279
+
280
+ await db.updateAsync(
281
+ { _id: queueId },
282
+ { _id: queueId, orders: entries },
283
+ { upsert: true }
284
+ );
285
+ await db.loadDatabase();
286
+ await this._addActivePair(pairKey);
287
+ }
288
+
289
+ /**
290
+ * For a given blockHeight:
291
+ * - read "activePairs"
292
+ * - for each pair, read "queue-<pair>"
293
+ * - split entries into thisBlock / remaining
294
+ * - sort thisBlock by canonical tail-char sender order
295
+ * - chaingun addTokenOrder / addContractOrder
296
+ * - write back remaining, clean up activePairs for empty queues
297
+ */
298
+ static async processQueuedOnChainOrdersForBlock(blockHeight) {
299
+ const height = Number(blockHeight);
300
+ const db = await this._getOrderbookDB();
301
+
302
+ const activeDoc = await db.findOneAsync({ _id: 'activePairs' });
303
+ if (!activeDoc || !Array.isArray(activeDoc.pairs) || activeDoc.pairs.length === 0) {
304
+ return;
305
+ }
306
+
307
+ let activePairs = activeDoc.pairs.slice();
308
+
309
+ for (const pairKey of activeDoc.pairs) {
310
+ const queueId = `queue-${pairKey}`;
311
+ const qDoc = await db.findOneAsync({ _id: queueId });
312
+
313
+ if (!qDoc || !Array.isArray(qDoc.orders) || qDoc.orders.length === 0) {
314
+ // Nothing queued; ensure the pair doesn't linger in activePairs
315
+ activePairs = activePairs.filter(k => k !== pairKey);
316
+ await db.updateAsync(
317
+ { _id: queueId },
318
+ { _id: queueId, orders: [] },
319
+ { upsert: true }
320
+ );
321
+ continue;
322
+ }
323
+
324
+ const entries = qDoc.orders;
325
+ const ready = [];
326
+ const future = [];
327
+
328
+ // ✅ process all orders whose blockHeight <= current height
329
+ for (const entry of entries) {
330
+ const entryHeight = Number(entry.blockHeight);
331
+ if (!isNaN(entryHeight) && entryHeight <= height) {
332
+ ready.push(entry);
333
+ } else {
334
+ future.push(entry);
335
+ }
336
+ }
337
+
338
+ if (ready.length === 0) {
339
+ // No work for this height for this pair; still persist shrunk queue if needed
340
+ if (future.length !== entries.length) {
341
+ await db.updateAsync(
342
+ { _id: queueId },
343
+ { _id: queueId, orders: future },
344
+ { upsert: true }
345
+ );
346
+ }
347
+ if (future.length === 0) {
348
+ activePairs = activePairs.filter(k => k !== pairKey);
349
+ }
350
+ continue;
351
+ }
352
+
353
+ // Deterministic ordering: first by blockHeight, then by sender
354
+ ready.sort((a, b) => {
355
+ const ha = Number(a.blockHeight);
356
+ const hb = Number(b.blockHeight);
357
+ if (ha !== hb) return ha - hb;
358
+ return compareSenderAddresses(a.sender, b.sender,a.txid,b.txid);
359
+ });
360
+
361
+ const orderbook = await Orderbook.getOrderbookInstance(pairKey);
362
+
363
+ for (const entry of ready) {
364
+ if (entry.kind === 'token') {
365
+ await orderbook.addTokenOrder(
366
+ entry.order,
367
+ entry.blockHeight,
368
+ entry.txid
369
+ );
370
+ } else if (entry.kind === 'contract') {
371
+ const p = entry.params;
372
+ const matchResult = await orderbook.addContractOrder(
373
+ p.contractId,
374
+ p.price,
375
+ p.amount,
376
+ p.sell,
377
+ p.insurance,
378
+ p.blockTime, // using blockTime as you had it
379
+ entry.txid,
380
+ entry.sender,
381
+ p.isLiq || false,
382
+ p.reduce,
383
+ p.post,
384
+ p.stop,
385
+ orderbook // ✅ pass the existing instance through
386
+ );
387
+ //console.log(' match result ' + JSON.stringify(matchResult));
388
+ }
389
+ }
390
+
391
+ // Debug: show loaded book & height, preserve your throw
392
+ const data = await orderbook.loadOrderBook(pairKey);
393
+
394
+ // ✅ Write back only truly future entries for this pair
395
+ await db.updateAsync(
396
+ { _id: queueId },
397
+ { _id: queueId, orders: future },
398
+ { upsert: true }
399
+ );
400
+
401
+ // If nothing left queued at all for this pair, drop it from activePairs
402
+ if (future.length === 0) {
403
+ activePairs = activePairs.filter(k => k !== pairKey);
404
+ }
405
+ }
406
+
407
+ // Persist the shrunk activePairs set
408
+ await this._updateActivePairs(activePairs);
409
+ }
410
+
411
+ /**
412
+ * Queue a CHANNEL trade (token or contract) for deterministic processing.
413
+ */
414
+ static async queueChannelTrade(kind, pairKey, sender, match, blockHeight, txid) {
415
+ const db = await this._getOrderbookDB();
416
+ const queueId = `channel-queue-${pairKey}`;
417
+
418
+ const doc = await db.findOneAsync({ _id: queueId });
419
+ const trades = (doc && Array.isArray(doc.trades)) ? doc.trades : [];
420
+
421
+ trades.push({
422
+ kind, // 'token' | 'contract'
423
+ pairKey: String(pairKey),
424
+ sender, // canonical ordering key (commit address / multisig)
425
+ blockHeight: Number(blockHeight),
426
+ txid,
427
+ match // ✅ already fully-formed
428
+ });
429
+
430
+ await db.updateAsync(
431
+ { _id: queueId },
432
+ { _id: queueId, trades },
433
+ { upsert: true }
434
+ );
435
+
436
+ await db.loadDatabase();
437
+ await this._addActivePair(`channel-${pairKey}`);
438
+ }
439
+
440
+
441
+ static async processQueuedChannelTradesForBlock(blockHeight) {
442
+ const height = Number(blockHeight);
443
+ const db = await this._getOrderbookDB();
444
+
445
+ const activeDoc = await db.findOneAsync({ _id: 'activePairs' });
446
+ if (!activeDoc?.pairs?.length) return;
447
+
448
+ let activePairs = activeDoc.pairs.slice();
449
+
450
+ for (const pair of activeDoc.pairs) {
451
+ if (!pair.startsWith('channel-')) continue;
452
+
453
+ const pairKey = pair.replace('channel-', '');
454
+ const queueId = `channel-queue-${pairKey}`;
455
+ const qDoc = await db.findOneAsync({ _id: queueId });
456
+
457
+ if (!qDoc?.trades?.length) {
458
+ activePairs = activePairs.filter(p => p !== pair);
459
+ continue;
460
+ }
461
+
462
+ const ready = [];
463
+ const future = [];
464
+
465
+ for (const t of qDoc.trades) {
466
+ if (Number(t.blockHeight) <= height) ready.push(t);
467
+ else future.push(t);
468
+ }
469
+
470
+ if (ready.length === 0) continue;
471
+
472
+ // ✅ CANONICAL SIEVE (same rule everywhere)
473
+ ready.sort((a, b) => {
474
+ if (a.blockHeight !== b.blockHeight) {
475
+ return a.blockHeight - b.blockHeight;
476
+ }
477
+ const s = compareSenderAddresses(a.sender, b.sender,a.txid,b.txid);
478
+ if (s !== 0) return s;
479
+ return a.txid.localeCompare(b.txid);
480
+ });
481
+
482
+ const orderbook = await Orderbook.getOrderbookInstance(pairKey);
483
+
484
+ for (const entry of ready) {
485
+ if (entry.kind === 'token') {
486
+ await orderbook.processTokenMatches(
487
+ [entry.payload.match],
488
+ entry.blockHeight,
489
+ entry.txid,
490
+ true
491
+ );
492
+ } else if (entry.kind === 'contract') {
493
+ await orderbook.processContractMatches(
494
+ [entry.payload.match],
495
+ entry.blockHeight,
496
+ true
497
+ );
498
+ }
499
+ }
500
+
501
+ await db.updateAsync(
502
+ { _id: queueId },
503
+ { _id: queueId, trades: future },
504
+ { upsert: true }
505
+ );
506
+
507
+ if (future.length === 0) {
508
+ activePairs = activePairs.filter(p => p !== pair);
509
+ }
510
+ }
511
+
512
+ await this._updateActivePairs(activePairs);
513
+ }
514
+
515
+ // Adds a token order to the order book
516
+ async addTokenOrder(order, blockHeight, txid) {
517
+
518
+ const TallyMap = require('./tally.js'); //lazy load so we can move available to reserved for this order
519
+ await TallyMap.updateBalance(order.sender, order.offeredPropertyId, -order.amountOffered, order.amountOffered, 0, 0,'tokenOrder',blockHeight);
520
+
521
+ // Determine the correct orderbook key
522
+ const normalizedOrderBookKey = this.normalizeOrderBookKey(order.offeredPropertyId, order.desiredPropertyId);
523
+ //console.log('Normalized Order Book Key:', normalizedOrderBookKey);
524
+
525
+ // Create an instance of Orderbook for the pair and load its data
526
+ const orderbook = new Orderbook(normalizedOrderBookKey);
527
+ var orderbookData = await orderbook.loadOrderBook(normalizedOrderBookKey,false);
528
+ //console.log('loaded orderbook' +JSON.stringify(orderbookData))
529
+ // Calculate the price for the order and round to the nearest tick interval
530
+ const calculatedPrice = this.calculatePrice(order.amountOffered, order.amountExpected);
531
+ console.log('Calculated Token Price:' + calculatedPrice+' '+txid);
532
+ order.price = calculatedPrice; // Append the calculated price to the order object
533
+ order.txid= txid.slice(0,3)+txid.slice(-4)
534
+
535
+ // Determine if the order is a sell order
536
+ const isSellOrder = Boolean(order.offeredPropertyId < order.desiredPropertyId);
537
+
538
+ // Add the order to the orderbook
539
+ orderbookData = await orderbook.insertOrder(order, orderbookData, isSellOrder,false);
540
+ //console.log('Order Insertion Confirmation:', orderbookData);
541
+
542
+ // Match orders in the orderbook
543
+ const matchResult = await orderbook.matchTokenOrders(orderbookData);
544
+ if (matchResult.matches && matchResult.matches.length > 0) {
545
+ //console.log('Match Result:', matchResult);
546
+ await orderbook.processTokenMatches(matchResult.matches, blockHeight, txid, false);
547
+ }else{console.log('No Matches for ' +txid)}
548
+ console.log('Normalized Order Book Key before saving:', normalizedOrderBookKey);
549
+ //console.log('getting ready to save orderbook update '+JSON.stringify(matchResult.orderBook))
550
+ // Save the updated orderbook back to the database
551
+ await orderbook.saveOrderBook(matchResult.orderBook,normalizedOrderBookKey);
552
+
553
+ return matchResult;
554
+ }
555
+
556
+ /**
557
+ * Get the total reserved margin for a specific address across buy and sell orders
558
+ * @param {string} address - The address whose reserved margin is being calculated
559
+ * @returns {BigNumber} - Total reserved margin for the address
560
+ */
561
+ getReserveByAddress(address,key) {
562
+ const stringKey = typeof key === 'string' ? key : String(key);
563
+
564
+ let totalReserved = new BigNumber(0);
565
+ for (const side of ["buy", "sell"]) {
566
+ console.log('inside get reserve by addr book '+JSON.stringify(this.orderBooks))
567
+ if (!this.orderBooks[stringKey][side]) continue;
568
+ for (const order of this.orderBooks[stringKey][side]) {
569
+ if ((order.sender || order.address) === address) {
570
+ console.log('in getReserveByAddr '+totalReserved.toNumber())
571
+ totalReserved = totalReserved.plus(order.initMargin || 0);
572
+ }
573
+ }
574
+ }
575
+ return totalReserved;
576
+ }
577
+
578
+ normalizeOrderBookKey(propertyId1, propertyId2) {
579
+ // Ensure lower property ID is first in the key
580
+ return propertyId1 < propertyId2 ? `${propertyId1}-${propertyId2}` : `${propertyId2}-${propertyId1}`;
581
+ }
582
+
583
+ async insertOrder(order, orderbookData, isSellOrder, isLiq) {
584
+
585
+ if (typeof orderbookData === 'string') {
586
+ try {
587
+ orderbookData = JSON.parse(orderbookData);
588
+ } catch (e) {
589
+ console.error('Failed to parse orderbook data:', orderbookData);
590
+ return; // Exit if parsing fails to prevent further issues
591
+ }
592
+ }
593
+
594
+ /*if (!this.isValidOrderbook(orderbookData,contract)) {
595
+
596
+
597
+ + console.error('Invalid orderbook data:', JSON.stringify(orderbookData));
598
+ return orderbookData; // Return early to avoid corrupting the orderbook
599
+ }*/
600
+ if (!orderbookData) {
601
+ orderbookData = { buy: [], sell: [] };
602
+ }
603
+
604
+ // Log the current state for debugging
605
+ //console.log('Order:', JSON.stringify(order));
606
+ //console.log('Orderbook data before:', JSON.stringify(orderbookData));
607
+ //console.log('Is sell order:', isSellOrder);
608
+
609
+ // Determine the side of the order
610
+ //console.log('is sell?'+isSellOrder)
611
+ const side = isSellOrder ? 'sell' : 'buy';
612
+ //console.log('side '+side)
613
+ let bookSide = orderbookData[side];
614
+ //console.log('book side '+JSON.stringify(bookSide))
615
+ // Ensure bookSide is initialized if undefined
616
+ if (!bookSide) {
617
+ bookSide = [];
618
+ }
619
+
620
+ // Log the state of bookSide for debugging
621
+ //console.log('Book side before:', JSON.stringify(bookSide));
622
+
623
+ // Find the appropriate index to insert the new order
624
+ const index = bookSide.findIndex((o) => o.time > order.time);
625
+ if (index === -1) {
626
+ bookSide.push(order); // Append to the end if no larger time is found
627
+ } else {
628
+ bookSide.splice(index, 0, order); // Insert at the found index
629
+ }
630
+
631
+ // Reintegrate bookSide back into orderbookData correctly
632
+ orderbookData[side] = bookSide;
633
+
634
+ // Log the updated orderbookData for debugging
635
+ //console.log('Updated orderbook data:', JSON.stringify(orderbookData));
636
+
637
+ return orderbookData;
638
+ }
639
+
640
+ isValidOrderbook(data,contract) {
641
+ if (typeof data !== 'object' || data === null) return false;
642
+
643
+ const hasBuySell = data.hasOwnProperty('buy') && data.hasOwnProperty('sell');
644
+ const isValidBuyArray = Array.isArray(data.buy) && (data.buy.length === 0 || data.buy.every(this.isValidOrder,contract));
645
+ const isValidSellArray = Array.isArray(data.sell) && (data.sell.length === 0 || data.sell.every(this.isValidOrder,contract));
646
+
647
+ console.log(isValidBuyArray, isValidSellArray)
648
+ console.log(data.buy.length===0, data.sell.length===0)
649
+
650
+
651
+ return hasBuySell && isValidBuyArray && isValidSellArray;
652
+ }
653
+
654
+ isValidOrder(order, contract) {
655
+ const hasRequiredFields = order.hasOwnProperty('offeredPropertyId') &&
656
+ order.hasOwnProperty('desiredPropertyId') &&
657
+ order.hasOwnProperty('amountOffered') &&
658
+ order.hasOwnProperty('amountExpected') &&
659
+ order.hasOwnProperty('blockTime') &&
660
+ order.hasOwnProperty('sender') &&
661
+ order.hasOwnProperty('price');
662
+
663
+ const hasValidTypes = typeof order.offeredPropertyId === 'number' &&
664
+ typeof order.desiredPropertyId === 'number' &&
665
+ typeof order.amountOffered === 'number' &&
666
+ typeof order.amountExpected === 'number' &&
667
+ typeof order.blockTime === 'number' &&
668
+ typeof order.sender === 'string' &&
669
+ typeof order.price === 'number';
670
+
671
+ if (contract==true||contract==null) {
672
+ const hasContractFields = order.hasOwnProperty('contractId') &&
673
+ order.hasOwnProperty('amount') &&
674
+ order.hasOwnProperty('side') &&
675
+ order.hasOwnProperty('initMargin') &&
676
+ order.hasOwnProperty('txid') &&
677
+ order.hasOwnProperty('isLiq') &&
678
+ order.hasOwnProperty('reduce') &&
679
+ order.hasOwnProperty('post');
680
+
681
+ const hasValidContractTypes = typeof order.contractId === 'number' &&
682
+ typeof order.amount === 'number' &&
683
+ typeof order.side === 'boolean' &&
684
+ typeof order.initMargin === 'number' &&
685
+ typeof order.txid === 'string' &&
686
+ typeof order.isLiq === 'boolean' &&
687
+ typeof order.reduce === 'boolean' &&
688
+ typeof order.post === 'boolean';
689
+
690
+ return hasContractFields && hasValidContractTypes;
691
+ }
692
+
693
+ return hasRequiredFields && hasValidTypes;
694
+ }
695
+
696
+
697
+ calculatePrice(amountOffered, amountExpected) {
698
+ const priceRatio = new BigNumber(amountOffered).dividedBy(amountExpected);
699
+ //console.log('price ratio '+priceRatio)
700
+ return priceRatio.decimalPlaces(8, BigNumber.ROUND_HALF_UP).toNumber();
701
+ }
702
+
703
+ async matchTokenOrders(orderbookData) {
704
+ if (!orderbookData || typeof orderbookData !== 'object') {
705
+ console.error("⚠️ Invalid orderbookData received:", orderbookData);
706
+ return { orderBook: { buy: [], sell: [] }, matches: [] };
707
+ }
708
+
709
+ let orderBookCopy = {
710
+ buy: Array.isArray(orderbookData.buy) ? [...orderbookData.buy] : [],
711
+ sell: Array.isArray(orderbookData.sell) ? [...orderbookData.sell] : []
712
+ };
713
+
714
+ console.log(`📊 Matching orders... Buy: ${orderBookCopy.buy.length}, Sell: ${orderBookCopy.sell.length}`);
715
+
716
+ let matches = [];
717
+
718
+ // Sort buy and sell orders
719
+ if (orderBookCopy.buy.length > 0) {
720
+ orderBookCopy.buy.sort((a, b) => new BigNumber(b.price).comparedTo(a.price) || a.blockTime - b.blockTime);
721
+ }
722
+ if (orderBookCopy.sell.length > 0) {
723
+ orderBookCopy.sell.sort((a, b) => new BigNumber(a.price).comparedTo(b.price) || a.blockTime - b.blockTime);
724
+ }
725
+
726
+ console.log(`📈 Orders sorted, beginning matching process...`);
727
+
728
+ let iterationLimit = 1000;
729
+ let iterationCount = 0;
730
+
731
+ for (; orderBookCopy.buy.length > 0 && orderBookCopy.sell.length > 0; iterationCount++) {
732
+ if (iterationCount >= iterationLimit) {
733
+ console.warn(`⚠️ Match execution limit reached! Exiting.`);
734
+ break;
735
+ }
736
+
737
+ let sellOrder = orderBookCopy.sell[0];
738
+ let buyOrder = orderBookCopy.buy[0];
739
+
740
+ // Ensure matching distinct property IDs
741
+ if (sellOrder.offeredPropertyId !== buyOrder.desiredPropertyId || sellOrder.desiredPropertyId !== buyOrder.offeredPropertyId) {
742
+ console.warn(`⚠️ Mismatched property IDs, skipping orders.`);
743
+ break;
744
+ }
745
+
746
+ let tradePrice;
747
+ let bumpTrade = false;
748
+ let post = false;
749
+ sellOrder.maker = false;
750
+ buyOrder.maker = false;
751
+
752
+ // Handle trades in the same block
753
+ if (sellOrder.blockTime === buyOrder.blockTime) {
754
+ tradePrice = buyOrder.price;
755
+ if (sellOrder.post) {
756
+ tradePrice = sellOrder.price;
757
+ post = true;
758
+ sellOrder.maker = true;
759
+ } else if (buyOrder.post) {
760
+ tradePrice = buyOrder.price;
761
+ post = true;
762
+ buyOrder.maker = true;
763
+ }
764
+ sellOrder.flat = true;
765
+ } else {
766
+ tradePrice = sellOrder.blockTime < buyOrder.blockTime ? sellOrder.price : buyOrder.price;
767
+ if ((sellOrder.blockTime < buyOrder.blockTime && buyOrder.post) ||
768
+ (buyOrder.blockTime < sellOrder.blockTime && sellOrder.post)) {
769
+ bumpTrade = true;
770
+ }
771
+ if (sellOrder.blockTime < buyOrder.blockTime && !bumpTrade) {
772
+ sellOrder.maker = true;
773
+ } else if (sellOrder.blockTime > buyOrder.blockTime && !bumpTrade) {
774
+ buyOrder.maker = true;
775
+ }
776
+ }
777
+
778
+ if (sellOrder.sender === buyOrder.sender) {
779
+ console.log(`🔄 Self-trade detected, removing maker order.`);
780
+ if (sellOrder.maker) {
781
+ orderBookCopy.sell.shift();
782
+ } else if (buyOrder.maker) {
783
+ orderBookCopy.buy.shift();
784
+ }
785
+ continue;
786
+ }
787
+
788
+ // Check for price match
789
+ if (new BigNumber(buyOrder.price).isGreaterThanOrEqualTo(sellOrder.price)) {
790
+ let sellAmountOffered = new BigNumber(sellOrder.amountOffered);
791
+ let sellAmountExpected = new BigNumber(sellOrder.amountExpected);
792
+ let buyAmountOffered = new BigNumber(buyOrder.amountOffered);
793
+ let buyAmountExpected = new BigNumber(buyOrder.amountExpected);
794
+
795
+ let tradeAmountA = BigNumber.min(sellAmountOffered, buyAmountExpected);
796
+ let tradeAmountB = tradeAmountA.times(tradePrice);
797
+
798
+ console.log(`🔄 Processing trade - Amount A: ${tradeAmountA}, Amount B: ${tradeAmountB}`);
799
+
800
+ if (!bumpTrade) {
801
+ sellOrder.amountOffered = sellAmountOffered.minus(tradeAmountA).toNumber();
802
+ buyOrder.amountOffered = buyAmountOffered.minus(tradeAmountB).toNumber();
803
+ sellOrder.amountExpected = sellAmountExpected.minus(tradeAmountB).toNumber();
804
+ buyOrder.amountExpected = buyAmountExpected.minus(tradeAmountA).toNumber();
805
+
806
+ matches.push({
807
+ sellOrder: {...sellOrder, amountOffered: tradeAmountA.toNumber()},
808
+ buyOrder: {...buyOrder, amountExpected: tradeAmountB.toNumber()},
809
+ amountOfTokenA: tradeAmountA.toNumber(),
810
+ amountOfTokenB: tradeAmountB.toNumber(),
811
+ tradePrice,
812
+ post,
813
+ bumpTrade
814
+ });
815
+
816
+ if (sellOrder.amountOffered === 0) {
817
+ orderBookCopy.sell.shift();
818
+ } else {
819
+ orderBookCopy.sell[0] = sellOrder;
820
+ }
821
+
822
+ if (buyOrder.amountExpected === 0) {
823
+ orderBookCopy.buy.shift();
824
+ } else {
825
+ orderBookCopy.buy[0] = buyOrder;
826
+ }
827
+ } else {
828
+ if (buyOrder.post) {
829
+ buyOrder.price = sellOrder.price - this.tickSize;
830
+ }
831
+ if (sellOrder.post) {
832
+ sellOrder.price = buyOrder.price + this.tickSize;
833
+ }
834
+ }
835
+ } else {
836
+ console.log(`❌ No price match, stopping execution.`);
837
+ break;
838
+ }
839
+ }
840
+
841
+ console.log(`✅ Matching complete. Trades executed: ${matches.length}`);
842
+ return { orderBook: orderBookCopy, matches: matches };
843
+ }
844
+
845
+ async processTokenMatches(matches, blockHeight, txid, channel) {
846
+ if (!Array.isArray(matches) || matches.length === 0) {
847
+ console.log('No valid matches to process');
848
+ return;
849
+ }
850
+
851
+ // If it’s a channel fill, divert to channel handler and return early
852
+ if (channel) {
853
+ await this.processTokenChannelTrades(matches, blockHeight, txid);
854
+ return;
855
+ }
856
+
857
+ for (const match of matches) {
858
+ if (!match.sellOrder || !match.buyOrder) {
859
+ console.error('Invalid match object:', match);
860
+ continue;
861
+ }
862
+
863
+ const sellOrderAddress = match.sellOrder.senderAddress;
864
+ const buyOrderAddress = match.buyOrder.senderAddress;
865
+ const sellOrderPropertyId = match.sellOrder.offeredPropertyId; // Token A
866
+ const buyOrderPropertyId = match.buyOrder.desiredPropertyId; // Token B
867
+
868
+ // Tag maker/taker by time (stable, deterministic)
869
+ if (match.sellOrder.blockTime < match.buyOrder.blockTime) {
870
+ match.sellOrder.orderRole = 'maker';
871
+ match.buyOrder.orderRole = 'taker';
872
+ } else if (match.buyOrder.blockTime < match.sellOrder.blockTime) {
873
+ match.buyOrder.orderRole = 'maker';
874
+ match.sellOrder.orderRole = 'taker';
875
+ } else {
876
+ match.buyOrder.orderRole = 'split';
877
+ match.sellOrder.orderRole = 'split';
878
+ }
879
+
880
+ const amountToTradeA = new BigNumber(match.amountOfTokenA); // seller gives A
881
+ const amountToTradeB = new BigNumber(match.amountOfTokenB); // buyer gives B
882
+
883
+ let takerFee = new BigNumber(0);
884
+ let makerRebate = new BigNumber(0);
885
+
886
+ // Fees: 2 bps taker; 50% rebate to maker (1 bp); 1 bp retained as exchange fee.
887
+ if (match.sellOrder.orderRole === 'maker' && match.buyOrder.orderRole === 'taker') {
888
+ // taker pays in the asset they are giving (Token B here)
889
+ takerFee = amountToTradeB.times(0.0002);
890
+ makerRebate = takerFee.div(2);
891
+ takerFee = takerFee.div(2); // actual paid net by taker after rebate to maker
892
+
893
+ // Spot-fee accrual in B (contractId = null → revenues to Property 1 investors)
894
+ await tallyMap.updateFeeCache(buyOrderPropertyId, takerFee.decimalPlaces(8, BigNumber.ROUND_FLOOR).toNumber(), null,blockHeight);
895
+
896
+ // Apply maker rebate to maker’s received asset (they receive B)
897
+ const sellOrderAmountChange = amountToTradeB.plus(makerRebate).decimalPlaces(8);
898
+ const buyOrderAmountChange = amountToTradeA.minus(new BigNumber(0)); // taker receives A without a fee on A side
899
+
900
+ // Seller (maker): -A reserve, +B available (+ rebate)
901
+ await tallyMap.updateBalance(
902
+ sellOrderAddress, sellOrderPropertyId,
903
+ 0, amountToTradeA.negated().toNumber(), 0, 0, true, false, false, txid
904
+ );
905
+ await tallyMap.updateBalance(
906
+ sellOrderAddress, buyOrderPropertyId,
907
+ sellOrderAmountChange.toNumber(), 0, 0, 0, true, false, false, txid
908
+ );
909
+
910
+ // Buyer (taker): -B reserve, +A available
911
+ await tallyMap.updateBalance(
912
+ buyOrderAddress, buyOrderPropertyId,
913
+ 0, amountToTradeB.negated().toNumber(), 0, 0, true, false, false, txid
914
+ );
915
+ await tallyMap.updateBalance(
916
+ buyOrderAddress, sellOrderPropertyId,
917
+ amountToTradeA.toNumber(), 0, 0, 0, true, false, false, txid
918
+ );
919
+
920
+ // Record trade
921
+ await this.recordTokenTrade({
922
+ offeredPropertyId: sellOrderPropertyId,
923
+ desiredPropertyId: buyOrderPropertyId,
924
+ amountOffered: amountToTradeA.toNumber(),
925
+ amountExpected: amountToTradeB.toNumber(),
926
+ price: match.tradePrice,
927
+ buyerRole: match.buyOrder.orderRole,
928
+ sellerRole: match.sellOrder.orderRole,
929
+ takerFee: takerFee.toNumber(),
930
+ makerRebate: makerRebate.toNumber(),
931
+ block: blockHeight,
932
+ buyer: buyOrderAddress,
933
+ seller: sellOrderAddress,
934
+ takerTxId: txid
935
+ }, blockHeight, txid);
936
+
937
+ } else if (match.buyOrder.orderRole === 'maker' && match.sellOrder.orderRole === 'taker') {
938
+ // taker pays in the asset they are giving (Token A here)
939
+ takerFee = amountToTradeA.times(0.0002);
940
+ makerRebate = takerFee.div(2);
941
+ takerFee = takerFee.div(2);
942
+
943
+ // Spot-fee accrual in A (contractId = null)
944
+ await tallyMap.updateFeeCache(sellOrderPropertyId, takerFee.decimalPlaces(8, BigNumber.ROUND_FLOOR).toNumber(), null,blockHeight);
945
+
946
+ // Apply maker rebate to maker’s received asset (maker receives A here)
947
+ const buyOrderAmountChange = amountToTradeA.plus(makerRebate).decimalPlaces(8);
948
+ const sellOrderAmountChange = amountToTradeB.minus(new BigNumber(0));
949
+
950
+ // Seller (taker): -A reserve, +B available
951
+ await tallyMap.updateBalance(
952
+ sellOrderAddress, sellOrderPropertyId,
953
+ 0, amountToTradeA.negated().toNumber(), 0, 0, true, false, false, txid
954
+ );
955
+ await tallyMap.updateBalance(
956
+ sellOrderAddress, buyOrderPropertyId,
957
+ amountToTradeB.toNumber(), 0, 0, 0, true, false, false, txid
958
+ );
959
+
960
+ // Buyer (maker): -B reserve, +A available (+ rebate)
961
+ await tallyMap.updateBalance(
962
+ buyOrderAddress, buyOrderPropertyId,
963
+ 0, amountToTradeB.negated().toNumber(), 0, 0, true, false, false, txid
964
+ );
965
+ await tallyMap.updateBalance(
966
+ buyOrderAddress, sellOrderPropertyId,
967
+ buyOrderAmountChange.toNumber(), 0, 0, 0, true, false, false, txid
968
+ );
969
+
970
+ await this.recordTokenTrade({
971
+ offeredPropertyId: sellOrderPropertyId,
972
+ desiredPropertyId: buyOrderPropertyId,
973
+ amountOffered: amountToTradeA.toNumber(),
974
+ amountExpected: amountToTradeB.toNumber(),
975
+ price: match.tradePrice,
976
+ buyerRole: match.buyOrder.orderRole,
977
+ sellerRole: match.sellOrder.orderRole,
978
+ takerFee: takerFee.toNumber(),
979
+ makerRebate: makerRebate.toNumber(),
980
+ block: blockHeight,
981
+ buyer: buyOrderAddress,
982
+ seller: sellOrderAddress,
983
+ takerTxId: txid
984
+ }, blockHeight, txid);
985
+
986
+ } else {
987
+ // split blockTime case: each pays 1bp on the asset they give
988
+ const takerFeeA = amountToTradeA.times(0.0001).decimalPlaces(8, BigNumber.ROUND_FLOOR);
989
+ const takerFeeB = amountToTradeB.times(0.0001).decimalPlaces(8, BigNumber.ROUND_FLOOR);
990
+
991
+ await tallyMap.updateFeeCache(buyOrderPropertyId, takerFeeA.toNumber(), null,blockHeight);
992
+ await tallyMap.updateFeeCache(sellOrderPropertyId, takerFeeB.toNumber(), null,blockHeight);
993
+
994
+ // Seller: -A reserve, +B available (minus its own fee on B? fee is in the asset they GIVE, so no)
995
+ await tallyMap.updateBalance(
996
+ sellOrderAddress, sellOrderPropertyId,
997
+ 0, amountToTradeA.negated().toNumber(), 0, 0, true, false, false, txid
998
+ );
999
+ await tallyMap.updateBalance(
1000
+ sellOrderAddress, buyOrderPropertyId,
1001
+ amountToTradeB.toNumber(), 0, 0, 0, true, false, false, txid
1002
+ );
1003
+
1004
+ // Buyer: -B reserve, +A available
1005
+ await tallyMap.updateBalance(
1006
+ buyOrderAddress, buyOrderPropertyId,
1007
+ 0, amountToTradeB.negated().toNumber(), 0, 0, true, false, false, txid
1008
+ );
1009
+ await tallyMap.updateBalance(
1010
+ buyOrderAddress, sellOrderPropertyId,
1011
+ amountToTradeA.toNumber(), 0, 0, 0, true, false, false, txid
1012
+ );
1013
+
1014
+ await this.recordTokenTrade({
1015
+ offeredPropertyId: sellOrderPropertyId,
1016
+ desiredPropertyId: buyOrderPropertyId,
1017
+ amountOffered: amountToTradeA.toNumber(),
1018
+ amountExpected: amountToTradeB.toNumber(),
1019
+ price: match.tradePrice,
1020
+ buyerRole: 'split',
1021
+ sellerRole: 'split',
1022
+ takerFee: new BigNumber(takerFeeA).plus(takerFeeB).toNumber(), // total fees collected
1023
+ makerRebate: 0,
1024
+ block: blockHeight,
1025
+ buyer: buyOrderAddress,
1026
+ seller: sellOrderAddress,
1027
+ takerTxId: txid
1028
+ }, blockHeight, txid);
1029
+ }
1030
+ }
1031
+ }
1032
+
1033
+ async processTokenChannelTrades(matches, blockHeight, txid) {
1034
+ console.log(`⚡ Processing ${matches.length} channel token matches at block ${blockHeight}`);
1035
+
1036
+ for (const match of matches) {
1037
+ if (!match.sellOrder || !match.buyOrder) continue;
1038
+
1039
+ const sellAddr = match.sellOrder.senderAddress;
1040
+ const buyAddr = match.buyOrder.senderAddress;
1041
+ const propA = match.sellOrder.offeredPropertyId; // A
1042
+ const propB = match.buyOrder.desiredPropertyId; // B
1043
+ const amtA = new BigNumber(match.amountOfTokenA);
1044
+ const amtB = new BigNumber(match.amountOfTokenB);
1045
+
1046
+ // TODO: replace below with true channel ledger updates (HTLC/commitment updates, etc.)
1047
+ // For now: same balance updates as spot path, so your economics and fee flow remain consistent.
1048
+
1049
+ // Debit seller reserve A, credit B available
1050
+ await tallyMap.updateBalance(sellAddr, propA, 0, amtA.negated().toNumber(), 0, 0, true, false, /*channel*/ true, txid);
1051
+ await tallyMap.updateBalance(sellAddr, propB, amtB.toNumber(), 0, 0, 0, true, false, /*channel*/ true, txid);
1052
+
1053
+ // Debit buyer reserve B, credit A available
1054
+ await tallyMap.updateBalance(buyAddr, propB, 0, amtB.negated().toNumber(), 0, 0, true, false, /*channel*/ true, txid);
1055
+ await tallyMap.updateBalance(buyAddr, propA, amtA.toNumber(), 0, 0, 0, true, false, /*channel*/ true, txid);
1056
+ }
1057
+ }
1058
+
1059
+ async addContractOrder(
1060
+ contractId,
1061
+ price,
1062
+ amount,
1063
+ sell,
1064
+ insurance,
1065
+ blockTime,
1066
+ txid,
1067
+ sender,
1068
+ isLiq,
1069
+ reduce,
1070
+ post,
1071
+ stop,
1072
+ orderbook
1073
+ ) {
1074
+ const ContractRegistry = require('./contractRegistry.js');
1075
+ const MarginMap = require('./marginMap.js');
1076
+
1077
+ // Ensure we have an orderbook instance
1078
+ const orderBookKey = `${contractId}`;
1079
+ if (!orderbook) {
1080
+ orderbook = await Orderbook.getOrderbookInstance(orderBookKey);
1081
+ }
1082
+
1083
+ const marginMap = await MarginMap.loadMarginMap(contractId);
1084
+ const existingPosition = await marginMap.getPositionForAddress(sender, contractId);
1085
+
1086
+ console.log(
1087
+ 'amount in add contract order ' +
1088
+ amount +
1089
+ ' ' +
1090
+ JSON.stringify(existingPosition)
1091
+ );
1092
+
1093
+ const contracts = Number(existingPosition.contracts || 0);
1094
+
1095
+ const isLong = contracts > 0;
1096
+ const isShort = contracts < 0;
1097
+
1098
+ // ✅ Correct: reduce only when the order is opposite to existing sign
1099
+ // - If you're SHORT (< 0), a BUY reduces/ flips.
1100
+ // - If you're LONG (> 0), a SELL reduces/ flips.
1101
+ const isBuyerReducingPosition = isShort && (sell === false); // buy against short
1102
+ const isSellerReducingPosition = isLong && (sell === true); // sell against long
1103
+
1104
+ let initialReduce = false;
1105
+ console.log(
1106
+ 'adding contract order... existingPosition? ' +
1107
+ JSON.stringify(existingPosition) +
1108
+ ' reducing position? ' +
1109
+ isBuyerReducingPosition +
1110
+ ' ' +
1111
+ isSellerReducingPosition
1112
+ );
1113
+
1114
+ let initMargin = 0;
1115
+
1116
+ // 🔹 Case 1: new or *increasing* exposure → must reserve init margin
1117
+ if (!isBuyerReducingPosition && !isSellerReducingPosition) {
1118
+ console.log('about to call moveCollateralToReserve ' + contractId, amount, sender);
1119
+
1120
+ initMargin = await ContractRegistry.moveCollateralToReserve(
1121
+ sender,
1122
+ contractId,
1123
+ amount,
1124
+ price,
1125
+ blockTime,
1126
+ txid
1127
+ );
1128
+
1129
+ // If we cannot reserve any margin, do NOT place the order.
1130
+ if (!initMargin || initMargin <= 0) {
1131
+ console.warn(
1132
+ `Insufficient collateral to open/increase position for ${sender} on contract ${contractId}; ` +
1133
+ `skipping order ${txid}.`
1134
+ );
1135
+ // Return the current book and no matches
1136
+ const currentBook = await orderbook.loadOrderBook(orderBookKey, false);
1137
+ return { orderBook: currentBook, matches: [] };
1138
+ }
1139
+
1140
+ // 🔹 Case 2: reduce or flip
1141
+ } else if (isBuyerReducingPosition || isSellerReducingPosition) {
1142
+ initialReduce = true;
1143
+
1144
+ let flipAmount = 0;
1145
+
1146
+ // If the order *over-shoots* the existing exposure, the excess is a flip that
1147
+ // needs *new* margin.
1148
+ if (
1149
+ (sell && contracts > 0 && amount > contracts) || // long -> bigger short
1150
+ (!sell && contracts < 0 && amount > Math.abs(contracts)) // short -> bigger long
1151
+ ) {
1152
+ flipAmount = sell
1153
+ ? amount - contracts
1154
+ : amount - Math.abs(contracts);
1155
+
1156
+ if (flipAmount > 0) {
1157
+ const extraInitMargin = await ContractRegistry.moveCollateralToReserve(
1158
+ sender,
1159
+ contractId,
1160
+ flipAmount,
1161
+ price,
1162
+ blockTime,
1163
+ txid
1164
+ );
1165
+
1166
+ if (!extraInitMargin || extraInitMargin <= 0) {
1167
+ console.warn(
1168
+ `Insufficient collateral to flip position for ${sender} on contract ${contractId}; ` +
1169
+ `skipping order ${txid}.`
1170
+ );
1171
+ const currentBook = await orderbook.loadOrderBook(orderBookKey, false);
1172
+ return { orderBook: currentBook, matches: [] };
1173
+ }
1174
+
1175
+ // We only lock extra for the flipped part; margin for the reduced leg
1176
+ // is already in margin/reserve from the existing position.
1177
+ initMargin = extraInitMargin;
1178
+ }
1179
+ }
1180
+ }
1181
+
1182
+ // Build the order object
1183
+ const contractOrder = {
1184
+ contractId,
1185
+ amount,
1186
+ price,
1187
+ blockTime,
1188
+ sell,
1189
+ initMargin,
1190
+ sender,
1191
+ txid,
1192
+ isLiq,
1193
+ reduce,
1194
+ post,
1195
+ stop,
1196
+ initialReduce
1197
+ };
1198
+
1199
+ // Load the orderbook snapshot for this contract
1200
+ let orderbookData = await orderbook.loadOrderBook(orderBookKey, false);
1201
+
1202
+ console.log('is sell? ' + sell);
1203
+
1204
+ // Insert into book
1205
+ orderbookData = await orderbook.insertOrder(
1206
+ contractOrder,
1207
+ orderbookData,
1208
+ sell,
1209
+ isLiq
1210
+ );
1211
+
1212
+ // Run matching
1213
+ const matchResult = await orderbook.matchContractOrders(orderbookData);
1214
+
1215
+ console.log('about to save orderbook in contract trade ' + orderBookKey);
1216
+ await orderbook.saveOrderBook(matchResult.orderBook, orderBookKey);
1217
+
1218
+ // If we got matches, clear/margin them
1219
+ if (matchResult.matches && matchResult.matches.length > 0) {
1220
+ await orderbook.processContractMatches(matchResult.matches, blockTime, false);
1221
+ }
1222
+
1223
+ return matchResult;
1224
+ }
1225
+
1226
+ async estimateLiquidation(
1227
+ liq, // 1st param: liquidation order (simulated incoming order)
1228
+ notional,
1229
+ liqPrice,
1230
+ trueLiqPrice,
1231
+ inverse,
1232
+ contractId // 7th param
1233
+ ) {
1234
+ const amount = Math.abs(Number(liq.amount || 0));
1235
+
1236
+ const result = {
1237
+ filled: false,
1238
+ filledSize: 0,
1239
+ goodFilledSize: 0,
1240
+ badFilledSize: 0,
1241
+ remainder: amount,
1242
+ avgFillPrice: null,
1243
+ fills: []
1244
+ };
1245
+
1246
+ console.log(
1247
+ "\n[EST_LIQ:start]",
1248
+ "contract", contractId,
1249
+ "amount", amount,
1250
+ "sell", liq.sell,
1251
+ "trueLiqPrice", trueLiqPrice,
1252
+ "inverse", inverse
1253
+ );
1254
+
1255
+ // --------------------------------------------------
1256
+ // Load orderbook for THIS contract
1257
+ // --------------------------------------------------
1258
+ const key = String(contractId);
1259
+ let ob = this.orderBooks[key];
1260
+ if (!ob) {
1261
+ console.log("[EST_LIQ] loading orderbook for contract", key);
1262
+ ob = await this.loadOrderBook(key);
1263
+ this.orderBooks[key] = ob;
1264
+ }
1265
+
1266
+ console.log(
1267
+ "[EST_LIQ] book sizes",
1268
+ "buy", ob.buy?.length,
1269
+ "sell", ob.sell?.length
1270
+ );
1271
+
1272
+ // Long liquidation → SELL into bids
1273
+ // Short liquidation → BUY into asks
1274
+ const orderbookSide = liq.sell ? ob.buy : ob.sell;
1275
+ if (!orderbookSide || orderbookSide.length === 0) {
1276
+ console.log("[EST_LIQ] no liquidity on side → ADL");
1277
+ return result;
1278
+ }
1279
+
1280
+ console.log(
1281
+ "[EST_LIQ] sweeping side",
1282
+ liq.sell ? "BUY (bids)" : "SELL (asks)",
1283
+ "levels", orderbookSide.length,
1284
+ "bestPx", orderbookSide[0]?.price,
1285
+ "bestSz", orderbookSide[0]?.amount
1286
+ );
1287
+
1288
+ let remaining = amount;
1289
+ let weightedPriceSum = 0;
1290
+
1291
+ // --------------------------------------------------
1292
+ // Sweep the book as-if `liq` were incoming
1293
+ // --------------------------------------------------
1294
+ for (let i = 0; i < orderbookSide.length; i++) {
1295
+ const level = orderbookSide[i];
1296
+ if (remaining <= 0) {
1297
+ console.log("[EST_LIQ] remaining exhausted");
1298
+ break;
1299
+ }
1300
+
1301
+ if (liq.address && level.sender === liq.address) {
1302
+ console.log("[EST_LIQ] skip self order @", level.price);
1303
+ continue;
1304
+ }
1305
+
1306
+ const px = Number(level.price);
1307
+ const sz = Number(level.amount);
1308
+
1309
+ if (!Number.isFinite(px) || !Number.isFinite(sz) || sz <= 0) {
1310
+ console.log("[EST_LIQ] skip invalid level", level);
1311
+ continue;
1312
+ }
1313
+
1314
+ const isGood = !inverse
1315
+ ? (liq.sell ? px >= trueLiqPrice : px <= trueLiqPrice)
1316
+ : (liq.sell ? px <= trueLiqPrice : px >= trueLiqPrice);
1317
+
1318
+ console.log(
1319
+ "[EST_LIQ] level",
1320
+ i,
1321
+ "px", px,
1322
+ "sz", sz,
1323
+ "remaining", remaining,
1324
+ "isGood", isGood
1325
+ );
1326
+
1327
+ if (!isGood) {
1328
+ console.log(
1329
+ "[EST_LIQ] price fails threshold → stop sweep",
1330
+ "px", px,
1331
+ "threshold", trueLiqPrice
1332
+ );
1333
+ break;
1334
+ }
1335
+
1336
+ const take = Math.min(remaining, sz);
1337
+
1338
+ console.log(
1339
+ "[EST_LIQ] TAKE",
1340
+ take,
1341
+ "@", px
1342
+ );
1343
+
1344
+ result.goodFilledSize += take;
1345
+ weightedPriceSum += take * px;
1346
+ result.fills.push({ price: px, size: take, good: true });
1347
+
1348
+ remaining -= take;
1349
+ }
1350
+
1351
+ // --------------------------------------------------
1352
+ // Finalize result
1353
+ // --------------------------------------------------
1354
+ result.filledSize = result.goodFilledSize;
1355
+ result.remainder = amount - result.goodFilledSize;
1356
+
1357
+ if (result.goodFilledSize > 0) {
1358
+ result.filled = true;
1359
+ result.avgFillPrice = weightedPriceSum / result.goodFilledSize;
1360
+ }
1361
+
1362
+ console.log(
1363
+ "[EST_LIQ:end]",
1364
+ "filled", result.filled,
1365
+ "filledSize", result.filledSize,
1366
+ "remainder", result.remainder,
1367
+ "avgPx", result.avgFillPrice
1368
+ );
1369
+
1370
+ return result;
1371
+ }
1372
+
1373
+ async matchContractOrders(orderBook) {
1374
+ // Base condition: if there are no buy or sell orders, return an empty match array.
1375
+ if (!orderBook || orderBook.buy.length === 0 || orderBook.sell.length === 0) {
1376
+ return { orderBook, matches: [] };
1377
+ }
1378
+
1379
+ let matches = [];
1380
+ const maxIterations = Math.min(orderBook.buy.length, orderBook.sell.length, 10000); // Safety guard
1381
+
1382
+ // Sort buy orders descending by price and ascending by blockTime,
1383
+ // sort sell orders ascending by price and ascending by blockTime.
1384
+ //
1385
+ // LITERAL PATCH: add a small priority bump for isMarket without touching existing logic
1386
+ orderBook.buy.sort((a, b) =>
1387
+ BigNumber(b.price).comparedTo(a.price) ||
1388
+ a.blockTime - b.blockTime
1389
+ );
1390
+ orderBook.sell.sort((a, b) =>
1391
+ BigNumber(a.price).comparedTo(b.price) ||
1392
+ a.blockTime - b.blockTime
1393
+ );
1394
+
1395
+ // Process a round of matching
1396
+ for (let i = 0; i < maxIterations; i++) {
1397
+ if (orderBook.sell.length === 0 || orderBook.buy.length === 0) break;
1398
+
1399
+ let sellOrder = orderBook.sell[0];
1400
+ let buyOrder = orderBook.buy[0];
1401
+
1402
+ console.log('sell order ' + JSON.stringify(sellOrder));
1403
+
1404
+ // Remove orders with zero amounts
1405
+ if (BigNumber(sellOrder.amount).isZero()) {
1406
+ orderBook.sell.splice(0, 1);
1407
+ continue;
1408
+ }
1409
+ if (BigNumber(buyOrder.amount).isZero()) {
1410
+ orderBook.buy.splice(0, 1);
1411
+ continue;
1412
+ }
1413
+
1414
+ // LITERAL PATCH: market flags
1415
+ const buyIsMkt = !!buyOrder.isMarket;
1416
+ const sellIsMkt = !!sellOrder.isMarket;
1417
+
1418
+ // Check for price match: if the best buy price is below the best sell price, no trade can occur.
1419
+ // LITERAL PATCH: skip this break when either side is market
1420
+ if (!buyIsMkt && !sellIsMkt) {
1421
+ if (BigNumber(buyOrder.price).isLessThan(sellOrder.price)) break;
1422
+ }
1423
+
1424
+ // Determine trade price (using the order with the earlier blockTime)
1425
+ // LITERAL PATCH: market order always takes the resting price
1426
+ let tradePrice;
1427
+ if (buyIsMkt && !sellIsMkt) {
1428
+ tradePrice = sellOrder.price;
1429
+ } else if (sellIsMkt && !buyIsMkt) {
1430
+ tradePrice = buyOrder.price;
1431
+ } else if (buyIsMkt && sellIsMkt) {
1432
+ // Should not happen in normal flow; conservative fallback
1433
+ tradePrice = buyOrder.price;
1434
+ } else {
1435
+ tradePrice =
1436
+ sellOrder.blockTime < buyOrder.blockTime ? sellOrder.price : buyOrder.price;
1437
+ }
1438
+
1439
+ // LITERAL PATCH: maker/taker rules with market support
1440
+ if (buyIsMkt && !sellIsMkt) {
1441
+ buyOrder.maker = false;
1442
+ sellOrder.maker = true;
1443
+ } else if (sellIsMkt && !buyIsMkt) {
1444
+ sellOrder.maker = false;
1445
+ buyOrder.maker = true;
1446
+ } else {
1447
+ sellOrder.maker = sellOrder.blockTime < buyOrder.blockTime;
1448
+ buyOrder.maker = buyOrder.blockTime < sellOrder.blockTime;
1449
+ }
1450
+
1451
+ // Prevent self-trading
1452
+ const sellSender = sellOrder.sender || sellOrder.address;
1453
+ const buySender = buyOrder.sender || buyOrder.address;
1454
+ if (sellSender === buySender) {
1455
+ console.log("Self-trade detected, removing the maker (resting) order.");
1456
+ if (sellOrder.maker) {
1457
+ orderBook.sell.splice(0, 1);
1458
+ } else {
1459
+ orderBook.buy.splice(0, 1);
1460
+ }
1461
+ continue;
1462
+ }
1463
+
1464
+ // For orders in the same block, decide based on the post-only flag.
1465
+ // LITERAL PATCH: only apply same-block post-only logic when neither side is market
1466
+ if (!buyIsMkt && !sellIsMkt && sellOrder.blockTime === buyOrder.blockTime) {
1467
+ console.log("Trades in the same block, defaulting to buy order");
1468
+ tradePrice = buyOrder.price;
1469
+ if (sellOrder.post) {
1470
+ tradePrice = sellOrder.price;
1471
+ sellOrder.maker = true;
1472
+ buyOrder.maker = false;
1473
+ } else if (buyOrder.post) {
1474
+ tradePrice = buyOrder.price;
1475
+ buyOrder.maker = true;
1476
+ sellOrder.maker = false;
1477
+ } else {
1478
+ sellOrder.maker = false;
1479
+ buyOrder.maker = false;
1480
+ }
1481
+ }
1482
+
1483
+ // Execute trade: match the minimum of the two orders’ amounts.
1484
+ let tradeAmount = BigNumber.min(sellOrder.amount, buyOrder.amount);
1485
+
1486
+ // Compute initial margin per contract (and marginUsed)
1487
+ const ContractRegistry = require('./contractRegistry.js');
1488
+ let initialMarginPerContract = await ContractRegistry.getInitialMargin(
1489
+ buyOrder.contractId,
1490
+ tradePrice
1491
+ );
1492
+ if (!initialMarginPerContract || isNaN(initialMarginPerContract)) {
1493
+ console.error(
1494
+ `Invalid initialMarginPerContract: ${initialMarginPerContract} for contract ${buyOrder.contractId} at price ${tradePrice}`
1495
+ );
1496
+ initialMarginPerContract = 0;
1497
+ }
1498
+ let marginUsed = BigNumber(initialMarginPerContract)
1499
+ .times(tradeAmount)
1500
+ .decimalPlaces(8)
1501
+ .toNumber();
1502
+ if (isNaN(marginUsed)) {
1503
+ console.error(`NaN detected in marginUsed: ${marginUsed}, using default 0`);
1504
+ marginUsed = 0;
1505
+ }
1506
+
1507
+ // Choose a txid based on maker flag
1508
+ let txid = sellOrder.maker ? sellOrder.txid : buyOrder.txid;
1509
+
1510
+ // Construct the match object
1511
+ matches.push({
1512
+ sellOrder: {
1513
+ ...sellOrder,
1514
+ contractId: sellOrder.contractId,
1515
+ amount: tradeAmount.toNumber(),
1516
+ sellerAddress: sellOrder.sender || sellOrder.address,
1517
+ txid: sellOrder.txid,
1518
+ maker: sellOrder.maker,
1519
+ liq: sellOrder.isLiq || false,
1520
+ marginUsed: marginUsed,
1521
+ initialReduce: sellOrder.initialReduce
1522
+ },
1523
+ buyOrder: {
1524
+ ...buyOrder,
1525
+ contractId: buyOrder.contractId,
1526
+ amount: tradeAmount.toNumber(),
1527
+ buyerAddress: buyOrder.sender || buyOrder.address,
1528
+ txid: buyOrder.txid,
1529
+ liq: buyOrder.isLiq || false,
1530
+ maker: buyOrder.maker,
1531
+ marginUsed: marginUsed,
1532
+ initialReduce: buyOrder.initialReduce
1533
+ },
1534
+ tradePrice,
1535
+ txid: txid
1536
+ });
1537
+
1538
+ // Update order amounts after the match
1539
+ sellOrder.amount = BigNumber(sellOrder.amount).minus(tradeAmount).toNumber();
1540
+ buyOrder.amount = BigNumber(buyOrder.amount).minus(tradeAmount).toNumber();
1541
+
1542
+ // initMargin shrinking
1543
+ if (sellOrder.amount > 0) {
1544
+ sellOrder.initMargin = (
1545
+ initialMarginPerContract * sellOrder.amount
1546
+ ).toFixed(8);
1547
+ }
1548
+
1549
+ if (buyOrder.amount > 0) {
1550
+ buyOrder.initMargin = (
1551
+ initialMarginPerContract * buyOrder.amount
1552
+ ).toFixed(8);
1553
+ }
1554
+
1555
+ // Remove fully filled orders from the front of the arrays
1556
+ if (sellOrder.amount === 0) {
1557
+ orderBook.sell.splice(0, 1);
1558
+ } else {
1559
+ orderBook.sell[0] = sellOrder;
1560
+ }
1561
+ if (buyOrder.amount === 0) {
1562
+ orderBook.buy.splice(0, 1);
1563
+ } else {
1564
+ orderBook.buy[0] = buyOrder;
1565
+ }
1566
+ }
1567
+
1568
+ // After this round, if there are still orders and the best buy price is at or above the best sell price,
1569
+ // recursively match the remaining orders.
1570
+ //
1571
+ // LITERAL PATCH: allow recursion to continue when a market order is at the top
1572
+ const topBuy = orderBook.buy[0];
1573
+ const topSell = orderBook.sell[0];
1574
+ const topBuyIsMkt = topBuy ? !!topBuy.isMarket : false;
1575
+ const topSellIsMkt = topSell ? !!topSell.isMarket : false;
1576
+
1577
+ if (
1578
+ orderBook.buy.length > 0 &&
1579
+ orderBook.sell.length > 0 &&
1580
+ (
1581
+ topBuyIsMkt ||
1582
+ topSellIsMkt ||
1583
+ BigNumber(topBuy.price).isGreaterThanOrEqualTo(topSell.price)
1584
+ )
1585
+ ) {
1586
+ const recResult = await this.matchContractOrders(orderBook);
1587
+ matches = matches.concat(recResult.matches);
1588
+ orderBook = recResult.orderBook;
1589
+ }
1590
+
1591
+ return { orderBook, matches };
1592
+ }
1593
+
1594
+
1595
+ async getAddressOrders(address, sell) {
1596
+ // Load the order book for the current instance's contractId
1597
+ const orderBookKey = `${this.orderBookKey}`;
1598
+ const orderbookData = await this.loadOrderBook(orderBookKey, false);
1599
+
1600
+ if(!orderbookData){
1601
+ console.error(`No order book found for contract ${this.orderBookKey}`);
1602
+ return [];
1603
+ }
1604
+
1605
+ // Determine whether to check buy or sell orders
1606
+ let orders = sell ? orderbookData.sell : orderbookData.buy;
1607
+
1608
+ // Filter orders by matching the given address
1609
+ return orders.filter(order => order.sender === address);
1610
+ }
1611
+
1612
+ async cancelContractOrdersForSize(address, contractId, blockHeight, sell, size) {
1613
+ // Load the order book for the current instance's contractId
1614
+ const orderBookKey = `${this.orderBookKey}`;
1615
+ const orderbookData = await this.loadOrderBook(orderBookKey, false);
1616
+
1617
+ if (!orderbookData) {
1618
+ console.error(`No order book found for contract ${this.orderBookKey}`);
1619
+ return [];
1620
+ }
1621
+
1622
+ // Determine the order side (buy or sell)
1623
+ let orders = sell ? orderbookData.sell : orderbookData.buy;
1624
+
1625
+ // Sort orders based on distance from market:
1626
+ // - Buy orders: Sort ascending (lowest price first)
1627
+ // - Sell orders: Sort descending (highest price first)
1628
+ orders = sell
1629
+ ? orders.sort((a, b) => a.price - b.price) // Buy side (cancel lowest first)
1630
+ : orders.sort((a, b) => b.price - a.price); // Sell side (cancel highest first)
1631
+
1632
+ let remainingSize = new BigNumber(size);
1633
+ let cancelledOrders = [];
1634
+
1635
+ for (let i = 0; i < orders.length; i++) {
1636
+ const order = orders[i];
1637
+
1638
+ // Only process orders belonging to the given address
1639
+ if (order.sender !== address) {
1640
+ continue;
1641
+ }
1642
+
1643
+ let orderSizeBN = new BigNumber(order.amount);
1644
+ let cancelSize = BigNumber.minimum(orderSizeBN, remainingSize);
1645
+
1646
+ // Cancel the order
1647
+ cancelledOrders.push({
1648
+ txid: order.txid, // Transaction ID of the order being cancelled
1649
+ amountCancelled: cancelSize.toNumber(),
1650
+ price: order.price,
1651
+ });
1652
+
1653
+ // Reduce remaining size to satisfy cancellation
1654
+ remainingSize = remainingSize.minus(cancelSize);
1655
+
1656
+ // Remove order from orderbook if fully cancelled
1657
+ if (orderSizeBN.isLessThanOrEqualTo(cancelSize)) {
1658
+ orders.splice(i, 1);
1659
+ i--; // Adjust index since we removed an element
1660
+ } else {
1661
+ // Update order amount in the order book
1662
+ order.amount = orderSizeBN.minus(cancelSize).toNumber();
1663
+ }
1664
+
1665
+ // If we have fully satisfied the requested size, stop processing
1666
+ if (remainingSize.isLessThanOrEqualTo(0)) {
1667
+ break;
1668
+ }
1669
+ }
1670
+
1671
+ // Save the updated order book
1672
+ //await this.saveOrderBook(orderBookKey, orderbookData);
1673
+
1674
+ return cancelledOrders;
1675
+ }
1676
+
1677
+ async evaluateBasicLiquidityReward(match, channel, contract) {
1678
+ var accepted = false
1679
+
1680
+ var contractOrPropertyIds = []
1681
+ if(!contract){
1682
+ contractOrPropertyIds=[match.propertyId1, match.propertyId2];
1683
+ }else{
1684
+ contractOrPropertyIds=[match.sellOrder.contractId]
1685
+ }
1686
+ let issuerAddresses = [];
1687
+
1688
+ if(contract){
1689
+ const ContractRegistry1 = require('./contractRegistry.js')
1690
+
1691
+ for (const id of contractOrPropertyIds) {
1692
+ const contractData = await ContractRegistry1.getContractInfo(id); // Assuming you have a similar method for contracts
1693
+ if (contractData && contractData.issuerAddress) {
1694
+ issuerAddresses.push(contractData.issuerAddress);
1695
+ }
1696
+ }
1697
+ }else{
1698
+ const PropertyManager1 = require('./property.js')
1699
+ for (const id of contractOrPropertyIds) {
1700
+ const propertyData = await PropertyManager1.getPropertyData(id);
1701
+ if (propertyData && propertyData.issuerAddress) {
1702
+ issuerAddresses.push(propertyData.issuerAddress);
1703
+ }
1704
+ }
1705
+
1706
+ }
1707
+ for (const address of issuerAddresses) {
1708
+ const isWhitelisted = await ClearList.isAddressInClearlist(1, address);
1709
+ if (isWhitelisted) {
1710
+ accepted=true
1711
+ }
1712
+ }
1713
+ return accepted;
1714
+ }
1715
+
1716
+ async evaluateEnhancedLiquidityReward(match, channel) {
1717
+ var accepted = false
1718
+
1719
+ let addressesToCheck = [];
1720
+
1721
+ if (match.type === 'channel') {
1722
+ const { commitAddressA, commitAddressB } = await Channels.getCommitAddresses(match.address);
1723
+ addressesToCheck = [channel.A.address, channel.B.address];
1724
+ } else {
1725
+ addressesToCheck = [match.buyerAddress, match.sellerAddress];
1726
+ }
1727
+
1728
+ for (const address of addressesToCheck) {
1729
+ const isWhitelisted = await ClearList.isAddressInClearlist(2, address);
1730
+ if (isWhitelisted) {
1731
+ accepted=true;
1732
+ }
1733
+ }
1734
+
1735
+ return accepted;
1736
+ }
1737
+
1738
+ // In Orderbooks.js
1739
+ async adjustOrdersForAddress(address, contractId, tally, pos) {
1740
+ const orderBookKey = `${this.orderBookKey}`;
1741
+ const orderbook = await this.loadOrderBook(orderBookKey, false);
1742
+ const obForContract = orderbook.orderBooks[contractId] || { buy: [], sell: [] };
1743
+
1744
+ console.log(`🔄 Adjusting orders for ${address} on contract ${contractId} with position: ${JSON.stringify(pos)}`);
1745
+
1746
+ let totalInitMarginForAddress = new BigNumber(0);
1747
+ let changedOrders = false;
1748
+ let requiredMarginChange = new BigNumber(0);
1749
+
1750
+ // Loop through buy & sell sides
1751
+ for (const side of ['buy', 'sell']) {
1752
+ for (let i = obForContract[side].length - 1; i >= 0; i--) {
1753
+ const order = obForContract[side][i];
1754
+ const orderAddress = order.sender || order.address;
1755
+ if (orderAddress !== address) continue;
1756
+
1757
+ const orderSide = order.side || (order.sell ? 'sell' : 'buy');
1758
+ const shouldBeReduce = (orderSide === 'buy' && pos.contracts < 0) ||
1759
+ (orderSide === 'sell' && pos.contracts > 0);
1760
+
1761
+ if (order.initialReduce !== shouldBeReduce) {
1762
+ console.log(`🔄 Order ${order.txid}: initialReduce flipped (${order.initialReduce} → ${shouldBeReduce})`);
1763
+ order.initialReduce = shouldBeReduce;
1764
+ changedOrders = true;
1765
+
1766
+ if (shouldBeReduce) {
1767
+ // ✅ **Return `initMargin` to `available` since it's now a take-profit order**
1768
+ console.log(`📉 Order ${order.txid} converted to take-profit. Returning ${order.initMargin} to available.`);
1769
+ await TallyMap.updateBalance(address, tally.propertyId, order.initMargin, -order.initMargin, 0, 0, 'takeProfitMarginReturn', tally.block);
1770
+ } else {
1771
+ // ❌ **Pull `initMargin` from `available` for new entry orders**
1772
+ console.log(`📈 Order ${order.txid} requires fresh margin allocation.`);
1773
+ requiredMarginChange = requiredMarginChange.plus(order.initMargin);
1774
+ }
1775
+ }
1776
+
1777
+ // Update margin usage
1778
+ const newInitialMargin = await ContractRegistry.getInitialMargin(contractId, pos.avgPrice);
1779
+ const expectedMarginUsed = new BigNumber(newInitialMargin).times(order.amount).decimalPlaces(8).toNumber();
1780
+
1781
+ if (order.marginUsed !== expectedMarginUsed) {
1782
+ console.log(`🔧 Updating marginUsed ${order.marginUsed} → ${expectedMarginUsed} for Order ${order.txid}`);
1783
+ order.marginUsed = expectedMarginUsed;
1784
+ changedOrders = true;
1785
+ }
1786
+
1787
+ // Track total reserved margin for address
1788
+ totalInitMarginForAddress = totalInitMarginForAddress.plus(expectedMarginUsed);
1789
+ }
1790
+ }
1791
+
1792
+ // **Step 2: Ensure sufficient balance before applying margin changes**
1793
+ if (requiredMarginChange.gt(0)) {
1794
+ const hasSufficient = await TallyMap.hasSufficientBalance(address, tally.propertyId, requiredMarginChange.toNumber());
1795
+
1796
+ if (!hasSufficient) {
1797
+ console.log(`⚠️ Insufficient balance for new entry orders. Cancelling lower-priority orders.`);
1798
+ await this.cancelExcessOrders(address, contractId, obForContract, requiredMarginChange);
1799
+ } else {
1800
+ console.log(`✅ Sufficient balance. Allocating ${requiredMarginChange.toFixed(8)} to margin.`);
1801
+ await TallyMap.updateBalance(address, tally.propertyId, -requiredMarginChange.toNumber(), requiredMarginChange.toNumber(), 0, 0, 'reduceFlagReallocation', tally.block);
1802
+ }
1803
+ }
1804
+
1805
+ // Save updated orderbook
1806
+ orderbook.orderBooks[contractId] = obForContract;
1807
+ await Orderbooks.saveOrderbook(orderbook);
1808
+ console.log(`✅ Finished adjusting orders for ${address} on contract ${contractId}.`);
1809
+
1810
+ return orderbook;
1811
+ }
1812
+
1813
+ /**
1814
+ * Attempt to source remaining loss from reserves tied up in *other* contract orderbooks.
1815
+ *
1816
+ * @param {string} address
1817
+ * @param {number} propertyId
1818
+ * @param {BigNumber} remaining
1819
+ * @param {number} skipContractId
1820
+ * @param {number} blockHeight
1821
+ * @returns {Object} { remaining: BigNumber, breakdown: {...} }
1822
+ */
1823
+ static async sourceCrossContractReserve(address, propertyId, remaining, skipContractId, blockHeight) {
1824
+ const TallyMap = require('./tally.js');
1825
+ const ContractRegistry = require('./contractRegistry.js');
1826
+ const Orderbook = require('./orderbook.js');
1827
+
1828
+ const breakdown = { fromCrossReserve: 0 };
1829
+
1830
+ // Load *all* contract IDs
1831
+ let allContracts = [];
1832
+ try {
1833
+ allContracts = await ContractRegistry.getAllContracts();
1834
+ console.log('all contracts? '+JSON.stringify(allContracts))
1835
+ } catch (e) {
1836
+ console.error("⚠️ Could not list all contracts, cross-contract reserve fallback skipped.", e);
1837
+ return { remaining, breakdown };
1838
+ }
1839
+
1840
+ // Snapshot available before cross-contract scavenging
1841
+ const initialTally = await TallyMap.getTally(address, propertyId);
1842
+ let baselineAvail = new BigNumber(initialTally.available || 0);
1843
+
1844
+ console.log(`🔁 Cross-contract scavenging for ${address}, need ${remaining.toFixed(8)}`);
1845
+
1846
+ for (const c of allContracts) {
1847
+ const otherCid = c.id; // ← extract numeric ID
1848
+ console.log('id and skip '+otherCid +' '+skipContractId)
1849
+ if (!otherCid) continue;
1850
+ if (otherCid === skipContractId) continue;
1851
+
1852
+ console.log(`➡️ Scanning contract ${otherCid} for cancellable orders...`);
1853
+
1854
+ await Orderbook.cancelExcessOrders(
1855
+ address,
1856
+ otherCid, // now a real number, not [object Object]
1857
+ remaining,
1858
+ propertyId,
1859
+ blockHeight
1860
+ );
1861
+
1862
+ // Tally AFTER cancellation
1863
+ const after = await TallyMap.getTally(address, propertyId);
1864
+ const afterAvail = new BigNumber(after.available || 0);
1865
+
1866
+ // Freed = increase in available
1867
+ let freed = afterAvail.minus(baselineAvail);
1868
+ if (freed.lt(0)) freed = new BigNumber(0);
1869
+
1870
+ const useX = BigNumber.min(remaining, freed);
1871
+
1872
+ if (useX.gt(0)) {
1873
+ console.log(` ✔ Freed ${useX.toFixed(8)} on contract ${otherCid}`);
1874
+
1875
+ // Debit available to pay the loss
1876
+ await TallyMap.updateBalance(
1877
+ address,
1878
+ propertyId,
1879
+ -useX,
1880
+ 0,
1881
+ 0,
1882
+ 0,
1883
+ 'loss_from_cross_contract_reserve',
1884
+ block
1885
+ );
1886
+
1887
+ breakdown.fromCrossReserve += useX.toNumber();
1888
+ remaining = remaining.minus(useX);
1889
+
1890
+ // Update baseline for next iteration
1891
+ baselineAvail = afterAvail.minus(useX);
1892
+ }
1893
+
1894
+ if (remaining.lte(0)) {
1895
+ console.log("🎉 Cross-contract reserve fully covers loss.");
1896
+ break;
1897
+ }
1898
+ }
1899
+
1900
+ return { remaining, breakdown };
1901
+ }
1902
+
1903
+
1904
+ static async cancelExcessOrders(address, contractId, requiredMargin,collateralId,block) {
1905
+ const TallyMap = require('./tally.js')
1906
+ let freedMargin = new BigNumber(0);
1907
+ const orderBookKey = `${contractId}`;
1908
+ const orderbook = new Orderbook(contractId);
1909
+ var obForContract = await orderbook.loadOrderBook(orderBookKey,false);
1910
+
1911
+ console.log(`🚨 Cancelling excess orders for ${address} on contract ${contractId} to free up ${requiredMargin.toFixed(8)} margin.`);
1912
+
1913
+ // Sort sell orders by highest price first (worst price for seller)
1914
+ obForContract.sell.sort((a, b) => new BigNumber(b.price).comparedTo(a.price));
1915
+
1916
+ // Sort buy orders by lowest price first (worst price for buyer)
1917
+ obForContract.buy.sort((a, b) => new BigNumber(a.price).comparedTo(b.price));
1918
+
1919
+ for (const side of ['buy', 'sell']) {
1920
+ for (let i = obForContract[side].length - 1; i >= 0; i--) {
1921
+ const order = obForContract[side][i];
1922
+ if ((order.sender || order.address) !== address) continue;
1923
+
1924
+ console.log(`❌ Cancelling Order ${order.txid}, freeing ${order.initMargin} margin.`);
1925
+ freedMargin = freedMargin.plus(order.initMargin);
1926
+ obForContract[side].splice(i, 1); // Remove order
1927
+
1928
+ await TallyMap.updateBalance(address, collateralId, order.initMargin, -order.initMargin, 0, 0, 'excessOrderCancellation', block);
1929
+
1930
+ if (freedMargin.gte(requiredMargin)) {
1931
+ console.log(`✅ Enough margin freed. Stopping cancellations.`);
1932
+ return;
1933
+ }
1934
+ }
1935
+ }
1936
+
1937
+ await orderbook.saveOrderBook(obForContract, orderBookKey);
1938
+
1939
+ console.log(`⚠️ Could not free all required margin. User may still be undercollateralized.`);
1940
+ }
1941
+
1942
+ static decomposePositionChange(oldPos, tradeAmount, isBuyerSide) {
1943
+ // Buyers are "long" side (+), sellers "short" side (-)
1944
+ const dir = isBuyerSide ? +1 : -1;
1945
+ const incoming = dir * tradeAmount; // signed change
1946
+ const absOld = Math.abs(oldPos);
1947
+ const sameDir = (Math.sign(oldPos) === Math.sign(incoming)) || oldPos === 0;
1948
+ console.log('inside decomp position '+JSON.stringify(oldPos)+' '+tradeAmount+' '+isBuyerSide)
1949
+ let closed = 0;
1950
+ let flipped = 0;
1951
+ let newPos = oldPos;
1952
+
1953
+ if (!sameDir && oldPos.amount !== 0) {
1954
+ // Trade goes against our existing position
1955
+ closed = Math.min(absOld, Math.abs(incoming)); // <= |oldPos|
1956
+ const remaining = Math.abs(incoming) - closed; // what's left after closing
1957
+ flipped = remaining; // always >= 0
1958
+
1959
+ if (remaining === 0) {
1960
+ newPos = oldPos + incoming; // ends up at 0
1961
+ } else {
1962
+ newPos = Math.sign(incoming) * remaining; // flipped to opposite side
1963
+ }
1964
+ console.log('inside the key block '+sameDir+' '+JSON.stringify(newPos)+' '+closed+' '+flipped+' '+remaining)
1965
+ } else {
1966
+ // Purely adding to existing direction (or opening from flat)
1967
+ closed = 0;
1968
+ flipped = 0;
1969
+ newPos = oldPos + incoming;
1970
+ }
1971
+
1972
+ return { closed, flipped, newPos };
1973
+ }
1974
+
1975
+ async sourceFundsForLoss(address, propertyId, lossAmount, block, contractId,tie) {
1976
+ const TallyMap = require('./tally.js')
1977
+ const tally = await TallyMap.getTally(address, propertyId);
1978
+ if (!tally) {
1979
+ return { hasSufficient: false, reason: 'undefined tally', remaining: lossAmount };
1980
+ }
1981
+
1982
+ let remaining = new BigNumber(lossAmount);
1983
+ const breakdown = { fromAvailable: 0, fromMarginCap: 0, fromReserve: 0, fromMarginFinal: 0 };
1984
+
1985
+ console.log(`🔍 Starting loss sourcing for ${address}, need ${remaining.toFixed(8)}`);
1986
+
1987
+ // 1️⃣ Available balance
1988
+ const availUse = BigNumber.min(remaining, tally.available || 0);
1989
+ let type = 'loss_from_available'
1990
+ if(tie){type+='_tieOff'}
1991
+ if (availUse.gt(0)) {
1992
+ await TallyMap.updateBalance(address, propertyId, -availUse, 0, 0, 0, type,block);
1993
+ breakdown.fromAvailable = availUse.toNumber();
1994
+ remaining = remaining.minus(availUse);
1995
+ }
1996
+
1997
+ type = 'loss_from_margin'
1998
+ if(tie){type+='_tieOff'}
1999
+ // 2️⃣ 49% of margin
2000
+ if (remaining.gt(0)) {
2001
+ const marginCap = new BigNumber(tally.margin || 0).multipliedBy(0.499);
2002
+ const marginUse = BigNumber.min(remaining, marginCap);
2003
+ if (marginUse.gt(0)) {
2004
+ await TallyMap.updateBalance(address, propertyId, 0, 0, -marginUse, 0, type,block);
2005
+ breakdown.fromMarginCap = marginUse.toNumber();
2006
+ remaining = remaining.minus(marginUse);
2007
+ }
2008
+ }
2009
+
2010
+ // 3️⃣ Try freeing reserve first if needed
2011
+ if (remaining.gt(0)) {
2012
+ // Snapshot before we mess with anything
2013
+ const before = await TallyMap.getTally(address, propertyId);
2014
+ const beforeAvail = new BigNumber(before.available || 0);
2015
+ const beforeReserved = new BigNumber(before.reserved || 0);
2016
+ console.log('before reserved '+ beforeAvail +' ' +beforeReserved)
2017
+ // Always try to cancel up to the shortfall.
2018
+ // cancelExcessOrders should internally clamp to available reserved.
2019
+ if (beforeReserved.gt(0)) {
2020
+ console.log(
2021
+ `⚠️ Attempting to free up to ${remaining.toFixed(8)} from cancelled orders (reserved=${beforeReserved.toFixed(8)})`
2022
+ );
2023
+ await Orderbook.cancelExcessOrders(address, contractId, remaining, propertyId, block);
2024
+ }
2025
+
2026
+ const after = await TallyMap.getTally(address, propertyId);
2027
+ const afterAvail = new BigNumber(after.available || 0);
2028
+ const afterReserved = new BigNumber(after.reserved || 0);
2029
+ console.log('after cancel '+afterAvail+' '+afterReserved)
2030
+ // Tokens freed from reserve are the *increase* in available
2031
+ // (assuming cancelExcessOrders moves reserved -> available without touching supply)
2032
+ let freedFromReserve = afterAvail.minus(beforeAvail);
2033
+ if (freedFromReserve.lt(0)) {
2034
+ // defensive: shouldn't happen, but don't let it blow things up
2035
+ freedFromReserve = new BigNumber(0);
2036
+ }
2037
+
2038
+ // Only use what we actually freed, and cap by remaining shortfall
2039
+ const reserveUse = BigNumber.min(remaining, freedFromReserve);
2040
+ console.log('reserve use '+reserveUse)
2041
+ if (reserveUse.gt(0)) {
2042
+ // This call should *not* create or destroy global supply:
2043
+ // losers lose reserveUse, winners gain reserveUse elsewhere.
2044
+ await TallyMap.updateBalance(
2045
+ address,
2046
+ propertyId,
2047
+ -reserveUse, // reduce available to pay the loss
2048
+ 0,
2049
+ 0,
2050
+ 0,
2051
+ 'loss_from_reserve',
2052
+ block
2053
+ );
2054
+ breakdown.fromReserve = (breakdown.fromReserve || 0) + reserveUse.toNumber();
2055
+ remaining = remaining.minus(reserveUse);
2056
+ }
2057
+ console.log('special check '+remaining.toNumber())
2058
+
2059
+ // --- after primary reserve sourcing ---
2060
+ if (remaining.gt(0)) {
2061
+ const x = await Orderbook.sourceCrossContractReserve(
2062
+ address,
2063
+ propertyId,
2064
+ remaining,
2065
+ contractId, // skip same contract
2066
+ block
2067
+ );
2068
+
2069
+ remaining = x.remaining;
2070
+ breakdown.fromCrossReserve = x.breakdown.fromCrossReserve || 0;
2071
+ }
2072
+
2073
+
2074
+ // At this point:
2075
+ // - available has been reduced by exactly the amount we just freed
2076
+ // - reserved has dropped by cancelExcessOrders
2077
+ // - global (amount+reserved+margin+vesting) stays invariant, except for the
2078
+ // separate credit to counterparties which should be balancing this debit.
2079
+ }
2080
+
2081
+ const success = remaining.lte(0);
2082
+ remaining.decimalPlaces(8).toNumber();
2083
+ const reason = success ? '' : 'Insufficient total balance after all buckets';
2084
+
2085
+ console.log(`📊 Loss sourcing result for ${address}:`, { success, remaining, breakdown });
2086
+
2087
+ return {
2088
+ hasSufficient: success,
2089
+ reason,
2090
+ remaining,
2091
+ totalUsed: lossAmount - remaining,
2092
+ breakdown
2093
+ };
2094
+ }
2095
+
2096
+ async _pruneInstaLiqOrdersFromFreshBook(
2097
+ thisPrice,
2098
+ blockHeight,
2099
+ contractId,
2100
+ notional,
2101
+ inverse
2102
+ ) {
2103
+ const Tally = require('./tally.js');
2104
+ const ContractRegistry = require('./contractRegistry.js');
2105
+
2106
+ const key = String(contractId);
2107
+ const collateralId = await ContractRegistry.getCollateralId(contractId);
2108
+
2109
+ // Use an instance so we can call loadOrderBook/saveOrderBook exactly like the rest of the file
2110
+ const obInst = new Orderbook(key);
2111
+
2112
+ // ✅ always load fresh snapshot
2113
+ const ob = await obInst.loadOrderBook(key);
2114
+ if (!ob || (!Array.isArray(ob.buy) && !Array.isArray(ob.sell))) return 0;
2115
+
2116
+ let pruned = 0;
2117
+
2118
+ // Sweep both sides (fresh snapshot)
2119
+ for (const sideName of ['buy', 'sell']) {
2120
+ const side = Array.isArray(ob[sideName]) ? ob[sideName] : [];
2121
+
2122
+ // iterate backwards because we splice
2123
+ for (let i = side.length - 1; i >= 0; i--) {
2124
+ const order = side[i];
2125
+ if (!order) continue;
2126
+
2127
+ const address = order.sender || order.address;
2128
+ if (!address) continue;
2129
+
2130
+ const qty = Math.abs(Number(order.amount || 0));
2131
+ if (!Number.isFinite(qty) || qty <= 0) continue;
2132
+
2133
+ // Lazy-load tally
2134
+ const tally = await Tally.getTally(address, collateralId);
2135
+ if (!tally) continue;
2136
+
2137
+ const avail = Number(tally.available || 0);
2138
+ const margin = Number(tally.margin || 0);
2139
+ const totalCollateral = avail + margin;
2140
+
2141
+ // no collateral => prune
2142
+ if (!Number.isFinite(totalCollateral) || totalCollateral <= 0) {
2143
+ // splice out order
2144
+ side.splice(i, 1);
2145
+ pruned++;
2146
+
2147
+ // refund reserve if present
2148
+ const reserve = Number(order.initMargin || 0);
2149
+ if (reserve > 0) {
2150
+ await Tally.updateBalance(
2151
+ address,
2152
+ collateralId,
2153
+ +reserve,
2154
+ -reserve,
2155
+ 0,
2156
+ 0,
2157
+ 'contractCancel',
2158
+ blockHeight
2159
+ );
2160
+ }
2161
+ continue;
2162
+ }
2163
+
2164
+ const px = Number(order.price);
2165
+ if (!Number.isFinite(px) || px <= 0) continue;
2166
+
2167
+ // sideName decides direction (don’t rely on order.isSell)
2168
+ const isSell = (sideName === 'sell');
2169
+
2170
+ let worstLoss = 0;
2171
+
2172
+ if (!inverse) {
2173
+ // Linear
2174
+ worstLoss = isSell
2175
+ ? qty * notional * Math.max(0, thisPrice - px) // short loses on price ↑
2176
+ : qty * notional * Math.max(0, px - thisPrice); // long loses on price ↓
2177
+ } else {
2178
+ // Inverse
2179
+ worstLoss = isSell
2180
+ ? qty * notional * Math.max(0, (1 / px) - (1 / thisPrice))
2181
+ : qty * notional * Math.max(0, (1 / thisPrice) - (1 / px));
2182
+ }
2183
+
2184
+ if (worstLoss > totalCollateral) {
2185
+ // prune the order
2186
+ side.splice(i, 1);
2187
+ pruned++;
2188
+
2189
+ // refund reserved collateral (prefer stored initMargin)
2190
+ let reserve = Number(order.initMargin || 0);
2191
+ if (!Number.isFinite(reserve) || reserve <= 0) {
2192
+ // fallback: compute from registry if needed
2193
+ const imPer = await ContractRegistry.getInitialMargin(contractId, px);
2194
+ reserve = Number(imPer || 0) * qty;
2195
+ }
2196
+
2197
+ if (reserve > 0) {
2198
+ await Tally.updateBalance(
2199
+ address,
2200
+ collateralId,
2201
+ +reserve,
2202
+ -reserve,
2203
+ 0,
2204
+ 0,
2205
+ 'contractCancel',
2206
+ blockHeight
2207
+ );
2208
+ }
2209
+ }
2210
+ }
2211
+ }
2212
+
2213
+ if (pruned > 0) {
2214
+ obInst.orderBooks[key] = ob;
2215
+ await obInst.saveOrderBook(ob, key);
2216
+ }
2217
+
2218
+ return pruned;
2219
+ }
2220
+
2221
+ async processContractMatches(matches, currentBlockHeight, channel, last=null){
2222
+ const TallyMap = require('./tally.js');
2223
+ const ContractRegistry = require('./contractRegistry.js')
2224
+ if (!Array.isArray(matches)) {
2225
+ // Handle the non-iterable case, e.g., log an error, initialize as an empty array, etc.
2226
+ console.error('Matches is not an array:', matches);
2227
+ matches = []; // Initialize as an empty array if that's appropriate
2228
+ }
2229
+
2230
+ const MarginMap = require('./marginMap.js')
2231
+ const tradeHistoryManager = new TradeHistory()
2232
+ const trades= []
2233
+ //console.log('processing contract mathces '+JSON.stringify(matches))
2234
+ let counter = 0
2235
+ for (const match of matches) {
2236
+ counter+=1
2237
+ console.log('counter 🛑 '+counter+' '+JSON.stringify(matches))
2238
+ console.log('🛑 JSON.stringify match '+JSON.stringify(match))
2239
+
2240
+ let isLiquidation = false
2241
+ if(match.buyOrder.isLiq||match.sellOrder.isLiq){
2242
+ isLiquidation=true
2243
+ }
2244
+ if(match.buyOrder.buyerAddress == match.sellOrder.sellerAddress){
2245
+ console.log('self trade nullified '+match.buyOrder.buyerAddress)
2246
+ continue
2247
+ }
2248
+
2249
+ let debugFlag = false
2250
+ // Load the margin map for the given series ID and block height
2251
+ const marginMap = await MarginMap.loadMarginMap(match.sellOrder.contractId);
2252
+ const isInverse = await ContractRegistry.isInverse(match.sellOrder.contractId)
2253
+ const priceInfo = await Clearing.isPriceUpdatedForBlockHeight(match.sellOrder.contractId,currentBlockHeight);
2254
+ let lastPrice = priceInfo.lastPrice
2255
+ let thisPrice = priceInfo.thisPrice
2256
+ if(isLiquidation&&last!=null){lastPrice=last}
2257
+ console.log('last price in process contracts '+lastPrice)
2258
+ match.inverse = isInverse
2259
+ let collateralPropertyId = await ContractRegistry.getCollateralId(match.buyOrder.contractId)
2260
+ const blob = await ContractRegistry.getNotionalValue(match.sellOrder.contractId,match.tradePrice)
2261
+ const notionalValue = blob.notionalValue
2262
+ const perContractNotional = blob.notionalPerContract;
2263
+ console.log('returned notionalValue '+notionalValue+' '+perContractNotional)
2264
+ let reserveBalanceA = await TallyMap.getTally(match.sellOrder.sellerAddress,collateralPropertyId)
2265
+ let reserveBalanceB = await TallyMap.getTally(match.buyOrder.buyerAddress,collateralPropertyId)
2266
+ if(debugFlag){
2267
+ console.log('checking reserves in process contract matches '+JSON.stringify(reserveBalanceA)+' '+JSON.stringify(reserveBalanceB))
2268
+ }
2269
+ //console.log('checking the marginMap for contractId '+ marginMap )
2270
+ // Get the existing position sizes for buyer and seller
2271
+ match.buyerPosition = await marginMap.getPositionForAddress(match.buyOrder.buyerAddress, match.buyOrder.contractId);
2272
+ match.sellerPosition = await marginMap.getPositionForAddress(match.sellOrder.sellerAddress, match.buyOrder.contractId);
2273
+ if(match.buyerPosition.address==undefined){
2274
+ match.buyerPosition.address=match.buyOrder.buyerAddress
2275
+ }
2276
+ if(match.sellerPosition.address==undefined){
2277
+ match.sellerPosition.address=match.sellOrder.sellerAddress
2278
+ }
2279
+
2280
+ console.log('checking positions '+JSON.stringify(match.buyerPosition)+' '+JSON.stringify(match.sellerPosition))
2281
+ const isBuyerReducingPosition = Boolean(match.buyerPosition.contracts < 0);
2282
+ const isSellerReducingPosition = Boolean(match.sellerPosition.contracts > 0);
2283
+
2284
+ console.log('about to calc fee '+match.buyOrder.amount+' '+match.sellOrder.maker+' '+match.buyOrder.maker+' '+isInverse+' '+match.tradePrice+' '+notionalValue+' '+channel)
2285
+ const { buyerFee, sellerFee } = this.calculateFee({
2286
+ amountBuy: match.buyOrder.amount,
2287
+ amountSell: match.sellOrder.amount,
2288
+ buyMaker: match.buyOrder.maker,
2289
+ sellMaker: match.sellOrder.maker,
2290
+ isInverse,
2291
+ lastMark: match.tradePrice,
2292
+ notionalValue,
2293
+ channel
2294
+ });
2295
+ console.log('seller/buyer fee '+sellerFee+' '+buyerFee)
2296
+ // Buyer side: only push taker/on-chain positive fees
2297
+ if (buyerFee.isGreaterThan(0)&&sellerFee.isLessThan(0)) {
2298
+ const feeToCache = buyerFee.div(2).toNumber();
2299
+ console.log('buyer fee to cache '+feeToCache)
2300
+ await TallyMap.updateFeeCache(collateralPropertyId, feeToCache, match.buyOrder.contractId,currentBlockHeight,true);
2301
+ }
2302
+
2303
+ // Seller side: same treatment
2304
+ if (sellerFee.isGreaterThan(0)&&buyerFee.isLessThan(0)) {
2305
+ const feeToCache = sellerFee.div(2).toNumber();
2306
+ console.log('seller fee to cache '+feeToCache)
2307
+ await TallyMap.updateFeeCache(collateralPropertyId, feeToCache, match.sellOrder.contractId,currentBlockHeight,true);
2308
+ }
2309
+
2310
+ if(buyerFee.isGreaterThan(0)&&sellerFee.isGreaterThan(0)){
2311
+ await TallyMap.updateFeeCache(collateralPropertyId, sellerFee.toNumber(), match.sellOrder.contractId,currentBlockHeight);
2312
+ await TallyMap.updateFeeCache(collateralPropertyId, buyerFee.toNumber(), match.sellOrder.contractId,currentBlockHeight);
2313
+ }
2314
+
2315
+ //console.log('reducing? buyer '+isBuyerReducingPosition +' seller '+isSellerReducingPosition+ ' buyer fee '+buyerFee +' seller fee '+sellerFee)
2316
+
2317
+ let feeInfo = await this.locateFee(match, reserveBalanceA, reserveBalanceB,collateralPropertyId,buyerFee, sellerFee, isBuyerReducingPosition, isSellerReducingPosition,currentBlockHeight)
2318
+
2319
+ const buyerPos = match.buyerPosition.contracts || 0;
2320
+ const sellerPos = match.sellerPosition.contracts || 0;
2321
+
2322
+ const buyerMove = Orderbook.decomposePositionChange(buyerPos, match.buyOrder.amount, /* isBuyerSide */ true);
2323
+ const sellerMove = Orderbook.decomposePositionChange(sellerPos, match.sellOrder.amount, /* isBuyerSide */ false);
2324
+ let initialMarginPerContract = await ContractRegistry.getInitialMargin(match.buyOrder.contractId, match.tradePrice);
2325
+ const buyerClosed = buyerMove.closed;
2326
+ let flipLong = buyerMove.flipped;
2327
+ const sellerClosed = sellerMove.closed;
2328
+ let flipShort = sellerMove.flipped;
2329
+ console.log('flip long and short '+flipLong+' '+flipShort)
2330
+ console.log('buyerClosed and sellerClosed '+buyerClosed+' '+sellerClosed)
2331
+ const isBuyerFlippingPosition = buyerMove.flipped > 0;
2332
+ const isSellerFlippingPosition = sellerMove.flipped > 0;
2333
+
2334
+ const buyerFullyClosed = (buyerMove.newPos === 0 && buyerClosed > 0);
2335
+ const sellerFullyClosed = (sellerMove.newPos === 0 && sellerClosed > 0);
2336
+
2337
+ console.log('debug flag flags '+isBuyerFlippingPosition+isSellerFlippingPosition+isBuyerReducingPosition+isSellerReducingPosition)
2338
+
2339
+ if (isBuyerFlippingPosition) {
2340
+ let closedContracts = buyerClosed // The contracts being closed
2341
+
2342
+ if (feeInfo.buyFeeFromMargin) {
2343
+ match.buyOrder.marginUsed = BigNumber(match.buyOrder.marginUsed).minus(buyerFee).decimalPlaces(8).toNumber();
2344
+ }
2345
+
2346
+ console.log(`Checking flip logic: ${match.buyOrder.buyerAddress} closing ${closedContracts}, flipping ${flipLong}`);
2347
+ let newMarginRequired = BigNumber(initialMarginPerContract).times(flipLong)
2348
+ console.log('newMargin flip '+newMarginRequired+' '+initialMarginPerContract+' '+flipLong)
2349
+ if(!channel){
2350
+ // Release margin for closed contracts
2351
+ let marginToRelease = BigNumber(initialMarginPerContract).times(closedContracts)
2352
+ //so in the event that this is not a channel trade we will deduct this as it matches the book
2353
+ let diff = marginToRelease.minus(newMarginRequired).decimalPlaces(8).toNumber();
2354
+ if(diff>0){
2355
+ await TallyMap.updateBalance(
2356
+ match.buyOrder.buyerAddress, collateralPropertyId, marginToRelease, -marginToRelease, 0, 0,
2357
+ 'contractMarginRelease', currentBlockHeight
2358
+ );
2359
+ }else{
2360
+ diff*=-1
2361
+ newMarginRequired-=diff
2362
+ }
2363
+
2364
+ }else if(channel){
2365
+ let diff = BigNumber(newMarginRequired).minus(match.buyerPosition.margin || 0).decimalPlaces(8).toNumber();
2366
+ if (diff !== 0) await TallyMap.updateBalance(match.buyOrder.buyerAddress, collateralPropertyId, -diff, 0, diff, 0, 'contractTradeInitMargin_channelFlip', currentBlockHeight);
2367
+
2368
+ }
2369
+
2370
+ // Ensure there is enough margin for the new contracts beyond closing
2371
+ let hasSufficientReserve = await TallyMap.hasSufficientBalance(match.buyOrder.buyerAddress, collateralPropertyId, newMarginRequired);
2372
+
2373
+ if (!hasSufficientReserve.hasSufficient) {
2374
+ console.log(`Shortfall detected: ${JSON.stringify(hasSufficientBalance)}`);
2375
+ console.log('hasSuf '+hasSufficientBalance.shortfall+' '+initialMarginPerContract )
2376
+ let contractUndo = BigNumber(hasSufficientBalance.shortfall)
2377
+ .dividedBy(initialMarginPerContract)
2378
+ .decimalPlaces(0, BigNumber.ROUND_CEIL)
2379
+ .toNumber();
2380
+
2381
+ flipLong -= contractUndo;
2382
+ newMarginRequired = BigNumber(initialMarginPerContract).times(new BigNumber(flipLong)).decimalPlaces(8).toNumber();
2383
+ console.log('contract undo investigate '+newMarginRequired+' '+flipLong+' '+contractUndo+' '+BigNumber(hasSufficientBalance.shortfall)
2384
+ .dividedBy(initialMarginPerContract)+' '+BigNumber(hasSufficientBalance.shortfall)
2385
+ .dividedBy(initialMarginPerContract)
2386
+ .decimalPlaces(0, BigNumber.ROUND_CEIL))
2387
+ }
2388
+
2389
+ await TallyMap.updateBalance(
2390
+ match.buyOrder.buyerAddress, collateralPropertyId, -newMarginRequired, 0, newMarginRequired, 0,
2391
+ 'contractTradeInitMargin', currentBlockHeight
2392
+ );
2393
+
2394
+ await marginMap.setInitialMargin(match.buyOrder.buyerAddress, match.buyOrder.contractId, newMarginRequired);
2395
+ await marginMap.recordMarginMapDelta(match.buyOrder.buyerAddress, match.buyOrder.contractId,
2396
+ match.buyerPosition.contracts + match.buyOrder.amount, match.buyOrder.amount, 0, 0, 0,
2397
+ 'updateContractBalancesFlip',currentBlockHeight
2398
+ );
2399
+
2400
+ let refreshedBalance = await TallyMap.getTally(match.buyOrder.buyerAddress,collateralPropertyId)
2401
+ //this.adjustOrdersForAddress(match.buyOrder.buyerAddress, match.buyOrder.contractId, refreshedBalance, match.buyerPosition)
2402
+
2403
+ console.log(`Flip logic updated: closed=${closedContracts}, flipped=${flipLong}`);
2404
+ }
2405
+
2406
+ if(isSellerFlippingPosition){
2407
+ let closedContracts = sellerClosed // The contracts being closed
2408
+
2409
+ console.log(`Checking sell flip logic: ${match.sellOrder.sellerAddress} closing ${closedContracts}, flipping ${flipShort}`);
2410
+
2411
+ console.log(`Checking flip logic: ${match.buyOrder.buyerAddress} closing ${closedContracts}, flipping ${flipLong}`);
2412
+ let newMarginRequired = BigNumber(initialMarginPerContract).times(flipShort).decimalPlaces(8).toNumber();
2413
+ console.log('newMargin flip '+newMarginRequired+' '+initialMarginPerContract+' '+flipLong)
2414
+ if(!channel){
2415
+ // Release margin for closed contracts
2416
+ let marginToRelease = BigNumber(initialMarginPerContract).times(closedContracts)
2417
+ //so in the event that this is not a channel trade we will deduct this as it matches the book
2418
+ let diff = marginToRelease.minus(newMarginRequired).decimalPlaces(8).toNumber();
2419
+ if(diff>0){
2420
+ await TallyMap.updateBalance(
2421
+ match.sellOrder.sellerAddress, collateralPropertyId, marginToRelease, -marginToRelease, 0, 0,
2422
+ 'contractMarginRelease', currentBlockHeight
2423
+ );
2424
+ }else{
2425
+ diff*=-1
2426
+ newMarginRequired-=diff
2427
+ }
2428
+ }else if(channel){
2429
+ let diff = BigNumber(newMarginRequired).minus(match.sellerPosition.margin || 0).decimalPlaces(8).toNumber();
2430
+ if (diff !== 0) await TallyMap.updateBalance(match.sellOrder.sellerAddress, collateralPropertyId, -diff, 0, diff, 0, 'contractTradeInitMargin_channelFlip', currentBlockHeight);
2431
+ }
2432
+
2433
+ if (feeInfo.sellFeeFromMargin) {
2434
+ newMarginRequired = BigNumber(newMarginRequired).minus(sellerFee).decimalPlaces(8).toNumber();
2435
+ }
2436
+
2437
+ let hasSufficientReserve = await TallyMap.hasSufficientBalance(match.sellOrder.sellerAddress, collateralPropertyId, newMarginRequired);
2438
+
2439
+ if (!hasSufficientReserve.hasSufficient) {
2440
+ console.log(`Sell flip shortfall detected: ${JSON.stringify(hasSufficientBalance)}`);
2441
+ let contractUndo = BigNumber(hasSufficientBalance.shortfall)
2442
+ .dividedBy(initialMarginPerContract)
2443
+ .decimalPlaces(0, BigNumber.ROUND_CEIL)
2444
+ .toNumber();
2445
+
2446
+ flipShort -= contractUndo;
2447
+ newMarginRequired = BigNumber(initialMarginPerContract).times(new BigNumber(flipShort)).decimalPlaces(8).toNumber();
2448
+ }
2449
+
2450
+ await TallyMap.updateBalance(
2451
+ match.sellOrder.sellerAddress, collateralPropertyId, -newMarginRequired, 0, newMarginRequired, 0,
2452
+ 'contractTradeInitMargin', currentBlockHeight
2453
+ );
2454
+
2455
+ await marginMap.setInitialMargin(match.sellOrder.sellerAddress, match.sellOrder.contractId, newMarginRequired);
2456
+ await marginMap.recordMarginMapDelta(match.sellOrder.sellerAddress, match.sellOrder.contractId,
2457
+ match.sellerPosition.contracts - match.sellOrder.amount, match.sellOrder.amount, 0, 0, 0,
2458
+ 'updateContractBalancesFlip',currentBlockHeight
2459
+ );
2460
+
2461
+ let refreshedBalanceB = await TallyMap.getTally(match.sellOrder.sellerAddress,collateralPropertyId)
2462
+ //this.adjustOrdersForAddress(match.sellOrder.sellerAddress, match.sellOrder.contractId, refreshedBalanceB, match.sellerPosition)
2463
+
2464
+ console.log(`Sell flip logic updated: closed=${closedContracts}, flipped=${flipShort}`);
2465
+ }
2466
+
2467
+ console.log('about to go into logic brackets for init margin '+isBuyerReducingPosition + ' seller reduce? '+ isSellerReducingPosition+ ' channel? '+channel)
2468
+
2469
+ console.log('looking at feeInfo obj '+JSON.stringify(feeInfo))
2470
+ if(!isBuyerReducingPosition&&!match.buyOrder.liq){
2471
+ if(channel==false){
2472
+ // Use the instance method to set the initial margin
2473
+ console.log('moving margin buyer not channel not reducing '+counter+' '+match.buyOrder.buyerAddress+' '+match.buyOrder.contractId+' '+match.buyOrder.amount+' '+match.buyOrder.marginUsed)
2474
+ const txid = match?.txid || '';
2475
+ match.buyerPosition = await ContractRegistry.moveCollateralToMargin(match.buyOrder.buyerAddress, match.buyOrder.contractId,match.buyOrder.amount, match.tradePrice, match.buyOrder.price,false,match.buyOrder.marginUsed,channel,null,currentBlockHeight,feeInfo,match.buyOrder.maker,debugFlag,txid,match.buyerPosition)
2476
+ console.log('looking at feeInfo obj '+JSON.stringify(feeInfo))
2477
+ }else if(channel==true){
2478
+ console.log('moving margin buyer channel not reducing '+counter+' '+match.buyOrder.buyerAddress+' '+match.buyOrder.contractId+' '+match.buyOrder.amount+' '+match.buyOrder.marginUsed)
2479
+ const txid = match?.txid || '';
2480
+ match.buyerPosition = await ContractRegistry.moveCollateralToMargin(match.buyOrder.buyerAddress, match.buyOrder.contractId,match.buyOrder.amount, match.buyOrder.price, match.buyOrder.price,false,match.buyOrder.marginUsed,channel, match.channelAddress,currentBlockHeight,feeInfo,match.buyOrder.maker,debugFlag,txid,match.buyerPosition)
2481
+ }
2482
+ //console.log('buyer position after moveCollat '+match.buyerPosition)
2483
+ }
2484
+ // Update MarginMap for the contract series
2485
+ console.log(' addresses in match '+match.buyOrder.buyerAddress+' '+match.sellOrder.sellerAddress)
2486
+ if(!isSellerReducingPosition&&!match.sellOrder.liq){
2487
+ if(channel==false){
2488
+ // Use the instance method to set the initial margin
2489
+ console.log('moving margin seller not channel not reducing '+counter+' '+match.sellOrder.sellerAddress+' '+match.sellOrder.contractId+' '+match.sellOrder.amount+' '+match.sellOrder.initMargin)
2490
+ match.sellerPosition = await ContractRegistry.moveCollateralToMargin(match.sellOrder.sellerAddress, match.sellOrder.contractId,match.sellOrder.amount, match.tradePrice,match.sellOrder.price, true, match.sellOrder.marginUsed,channel,null,currentBlockHeight,feeInfo,match.buyOrder.maker,match.sellerPosition)
2491
+ }else if(channel==true){
2492
+ console.log('moving margin seller channel not reducing '+counter+' '+match.sellOrder.sellerAddress+' '+match.sellOrder.contractId+' '+match.sellOrder.amount+' '+match.sellOrder.initMargin)
2493
+ match.sellerPosition = await ContractRegistry.moveCollateralToMargin(match.sellOrder.sellerAddress, match.sellOrder.contractId,match.sellOrder.amount, match.sellOrder.price,match.sellOrder.price, true, match.sellOrder.marginUsed,channel, match.channelAddress,currentBlockHeight,feeInfo,match.buyOrder.maker,match.sellerPosition)
2494
+ }
2495
+ console.log('sellerPosition after moveCollat '+match.sellerPosition)
2496
+ }
2497
+
2498
+
2499
+ //console.log('checking position for trade processing '+JSON.stringify(match.buyerPosition) +' buyer size '+' seller size '+JSON.stringify(match.sellerPosition))
2500
+ //console.log('reviewing Match object before processing '+JSON.stringify(match))
2501
+ // Update contract balances for the buyer and seller
2502
+ let close = false
2503
+ let flip = false
2504
+ if((isBuyerReducingPosition||isSellerReducingPosition)&&(isBuyerFlippingPosition==false||isSellerFlippingPosition==false)){
2505
+ close = true
2506
+ }else if(isBuyerFlippingPosition==true||isSellerFlippingPosition==true){
2507
+ flip=true
2508
+ }
2509
+ if(channel==true){
2510
+ console.log('checking match obj before calling update contract balances '+JSON.stringify(match))
2511
+ }
2512
+
2513
+ console.log('close? flip? '+close+' '+flip)
2514
+
2515
+ // ========== FIX: Capture avgPrice BEFORE position update ==========
2516
+ // This is critical for same-block closes where the position may flip
2517
+ // and then close, causing avgPrice to be set to null before we can use it
2518
+ const buyerAvgPriceBeforeUpdate = match.buyerPosition.avgPrice;
2519
+ const sellerAvgPriceBeforeUpdate = match.sellerPosition.avgPrice;
2520
+ // ===================================================================
2521
+
2522
+ let positions = await marginMap.updateContractBalancesWithMatch(match, channel, buyerClosed,flipLong,sellerClosed,flipShort,currentBlockHeight)
2523
+
2524
+ const isLiq = Boolean(match.sellOrder.liq||match.buyOrder.liq)
2525
+
2526
+ const trade = {
2527
+ buyerPosition: positions.bp,
2528
+ sellerPosition: positions.sp,
2529
+ buyerFee: buyerFee.decimalPlaces(8, BigNumber.ROUND_DOWN).toNumber(),
2530
+ sellerFee: sellerFee.decimalPlaces(8, BigNumber.ROUND_DOWN).toNumber(),
2531
+ contractId: match.sellOrder.contractId,
2532
+ amount: match.sellOrder.amount,
2533
+ price: match.tradePrice,
2534
+ buyerAddress: match.buyOrder.buyerAddress,
2535
+ sellerAddress: match.sellOrder.sellerAddress,
2536
+ sellerTx: match.sellOrder.sellerTx,
2537
+ buyerTx: match.buyOrder.buyerTx,
2538
+ buyerClose: buyerClosed,
2539
+ sellerClose: sellerClosed,
2540
+ block: currentBlockHeight,
2541
+ buyerFullClose: buyerFullyClosed,
2542
+ sellerFullClose: sellerFullyClosed,
2543
+ flipLong: flipLong,
2544
+ flipShort: flipShort,
2545
+ channel: channel,
2546
+ liquidation: isLiq,
2547
+ remainderLiq: 0
2548
+ // other relevant trade details...
2549
+ };
2550
+
2551
+ const deltas = this.deriveTradeDelta(
2552
+ match,
2553
+ buyerClosed,
2554
+ sellerClosed,
2555
+ flipLong,
2556
+ flipShort
2557
+ );
2558
+
2559
+ const buyerTradeRecord = Clearing.recordTrade(
2560
+ trade.contractId,
2561
+ trade.buyerAddress,
2562
+ deltas.buyer.opened, // opened
2563
+ buyerClosed, // closed
2564
+ trade.price,
2565
+ match.buyOrder.txid,
2566
+ true
2567
+ );
2568
+
2569
+ const sellerTradeRecord = Clearing.recordTrade(
2570
+ trade.contractId,
2571
+ trade.sellerAddress,
2572
+ deltas.seller.opened, // opened
2573
+ sellerClosed, // closed
2574
+ trade.price,
2575
+ match.sellOrder.txid,
2576
+ false
2577
+ );
2578
+
2579
+ const closesBuyer = buyerClosed;
2580
+ // how many of these closes belong to same-block opens?
2581
+ const buyerClosesAgainstAvg = buyerTradeRecord.consumedFromOpened;
2582
+ const sellerClosesAgainstAvg = sellerTradeRecord.consumedFromOpened
2583
+ // settlement prices
2584
+ const buyerAvg = match.buyerPosition.avgPrice;
2585
+ let sellerClosesAgainstLast = sellerClosed- sellerClosesAgainstAvg;
2586
+ let buyerClosesAgainstLast = buyerClosed- buyerClosesAgainstAvg
2587
+ console.log('trade '+JSON.stringify(trade))
2588
+ match.buyerPosition = positions.bp
2589
+ match.sellerPosition = positions.sp
2590
+ console.log('checking positions based on mMap vs. return of object in contract update '+JSON.stringify(positions)+' '+JSON.stringify(match.buyerPosition) + ' '+JSON.stringify(match.sellerPosition))
2591
+
2592
+ console.log('checking positions after contract adjustment, seller '+JSON.stringify(match.sellerPosition) + ' buyer '+JSON.stringify(match.buyerPosition))
2593
+ let buyerOpenMarkPNL = 0
2594
+ let sellerOpenMarkPNL = 0
2595
+ // ========== MARK BASIS (A): realize any OPENED portion to lastPrice ==========
2596
+ // This rebases same-block opens to mark immediately, so later closes can use lastPrice.
2597
+ const amount = Number(trade.amount || 0);
2598
+
2599
+ const buyerClose = Number(trade.buyerClose || 0);
2600
+ const sellerClose = Number(trade.sellerClose || 0);
2601
+
2602
+ // “Opened” is what’s left after close + flip allocation
2603
+ const buyerOpened = Math.max(0, amount - buyerClose );
2604
+ const sellerOpened = Math.max(0, amount - sellerClose);
2605
+ console.log('buyer opened '+buyerOpened+' '+amount+ ' '+buyerClose+' '+flipLong)
2606
+ console.log('seller opened '+sellerOpened+' '+amount+ ' '+sellerClose+' '+flipShort)
2607
+
2608
+ if (buyerOpened > 0) {
2609
+ let exit = lastPrice
2610
+ let type = 'buyerNewContractTieOff'
2611
+ if(isLiquidation){
2612
+ type+= 'Liq'
2613
+ exit=thisPrice}
2614
+ console.log('tie off in liquidation? '+isLiquidation+' '+exit+' '+lastPrice+' '+thisPrice)
2615
+ buyerOpenMarkPNL = await marginMap.settlePNL(
2616
+ trade.buyerAddress,
2617
+ buyerOpened, // long opened
2618
+ exit, // exit = mark
2619
+ trade.price, // entry = fill
2620
+ trade.contractId,
2621
+ currentBlockHeight,
2622
+ isInverse,
2623
+ perContractNotional
2624
+ );
2625
+
2626
+ if(buyerOpenMarkPNL>0){
2627
+ await TallyMap.updateBalance(
2628
+ trade.buyerAddress,
2629
+ collateralPropertyId,
2630
+ buyerOpenMarkPNL,
2631
+ 0, 0, 0,
2632
+ 'buyerNewContractTieOff',
2633
+ currentBlockHeight
2634
+ );
2635
+ }else{
2636
+ await this.sourceFundsForLoss(
2637
+ trade.buyerAddress,
2638
+ collateralPropertyId,
2639
+ Math.abs(buyerOpenMarkPNL),
2640
+ currentBlockHeight,
2641
+ trade.contractId,
2642
+ true)
2643
+ }
2644
+ }
2645
+
2646
+ if (sellerOpened > 0) {
2647
+ let exit = lastPrice
2648
+ let type = 'sellerNewContractTieOff'
2649
+ if(isLiquidation){
2650
+ type+= 'Liq'
2651
+ exit=thisPrice}
2652
+ console.log('tie off in liquidation? '+isLiquidation+' '+exit+' '+lastPrice+' '+thisPrice)
2653
+
2654
+
2655
+ sellerOpenMarkPNL = await marginMap.settlePNL(
2656
+ trade.sellerAddress,
2657
+ -sellerOpened, // short opened
2658
+ exit, // exit = mark
2659
+ trade.price, // entry = fill
2660
+ trade.contractId,
2661
+ currentBlockHeight,
2662
+ isInverse,
2663
+ perContractNotional
2664
+ );
2665
+
2666
+ if(sellerOpenMarkPNL>0){
2667
+ await TallyMap.updateBalance(
2668
+ trade.sellerAddress,
2669
+ collateralPropertyId,
2670
+ sellerOpenMarkPNL,
2671
+ 0, 0, 0,
2672
+ type,
2673
+ currentBlockHeight
2674
+ );
2675
+ }else{
2676
+ await this.sourceFundsForLoss(
2677
+ trade.sellerAddress,
2678
+ collateralPropertyId,
2679
+ Math.abs(sellerOpenMarkPNL),
2680
+ currentBlockHeight,
2681
+ trade.contractId,
2682
+ true)
2683
+ }
2684
+
2685
+
2686
+ }
2687
+
2688
+ console.log('seller and buyer tie offs'+sellerOpenMarkPNL+' '+buyerOpenMarkPNL+' '+sellerOpened+' '+buyerOpened)
2689
+
2690
+ // Record the contract trade
2691
+ await this.recordContractTrade(trade, currentBlockHeight);
2692
+
2693
+ // Realize PnL if the trade reduces the position size
2694
+ let buyerPnl = new BigNumber(0), sellerPnl = new BigNumber(0);
2695
+ console.log('do we realize PNL? '+isBuyerReducingPosition+' '+isBuyerFlippingPosition+' '+match.buyOrder.liq+' '+isSellerReducingPosition+' '+isSellerFlippingPosition+' '+match.sellOrder.liq)
2696
+ let closedShorts=0
2697
+ let realizedBuyerLoss = 0
2698
+ let realizedSellerLoss = 0
2699
+ let realizedBuyerProfit = 0
2700
+ let realizedSellerProfit = 0
2701
+
2702
+ if((isBuyerReducingPosition||isBuyerFlippingPosition)/*&&!match.buyOrder.liq*/){
2703
+ closedShorts = match.buyOrder.amount
2704
+
2705
+ if(isBuyerFlippingPosition){
2706
+ closedShorts-=flipLong
2707
+ }
2708
+ console.log('closed contracts '+match.buyOrder.amount+' '+closedShorts)
2709
+ //this loops through our position history and closed/open trades in that history to figure a precise entry price for the trades
2710
+ //on a LIFO basis that are being retroactively 'closed' by reference here
2711
+ //console.log('about to call trade history manager '+match.buyOrder.contractId)
2712
+ //const LIFO = tradeHistoryManager.calculateLIFOEntry(match.buyOrder.buyerAddress, closedContracts, match.buyOrder.contractId)
2713
+ //{AvgEntry,blockTimes}
2714
+
2715
+ // ========== FIX: Use pre-update avgPrice for settlement ==========
2716
+ // After updateContractBalancesWithMatch, avgPrice may be null if the
2717
+ // position closed. Use the captured value from before the update.
2718
+ let avgEntry = buyerAvgPriceBeforeUpdate || match.buyerPosition.avgPrice;
2719
+ // ===================================================================
2720
+
2721
+ //then we take that avg. entry price, not for the whole position but for the chunk that is being closed
2722
+ //and we figure what is the PNL that one would show on their taxes, to save a record.
2723
+
2724
+ match.buyerPosition = await marginMap.realizePnl(match.buyOrder.buyerAddress, closedShorts, match.tradePrice, avgEntry, isInverse, perContractNotional, match.buyerPosition, true,match.buyOrder.contractId);
2725
+ //then we will look at the last settlement mark price for this contract or default to the LIFO Avg. Entry if
2726
+ //the closing trade and the opening trades reference happened in the same block (exceptional, will add later)
2727
+
2728
+ let settlementPNL =
2729
+ await marginMap.settlePNL(
2730
+ trade.buyerAddress,
2731
+ -buyerClosed,
2732
+ trade.price,
2733
+ lastPrice,
2734
+ trade.contractId,
2735
+ currentBlockHeight,
2736
+ isInverse,
2737
+ perContractNotional
2738
+ );
2739
+ console.log('settlementPNL for buyer '+settlementPNL)
2740
+
2741
+ //then we figure out the aggregate position's margin situation and liberate margin on a pro-rata basis
2742
+ const reduction = await marginMap.reduceMargin(match.buyerPosition, closedShorts, initialMarginPerContract, match.buyOrder.contractId, match.buyOrder.buyerAddress, false, feeInfo.buyFeeFromMargin,buyerFee)
2743
+ //{netMargin,mode}
2744
+ const sufficientMargin = await TallyMap.hasSufficientMargin(match.buyOrder.buyerAddress,collateralPropertyId,reduction)
2745
+
2746
+ if(reduction!==0&&sufficientMargin.hasSufficient){
2747
+ //console.log('reduction about to pass to TallyMap' +reduction)
2748
+ await TallyMap.updateBalance(match.buyOrder.buyerAddress, collateralPropertyId, reduction, 0, -reduction, 0, 'contractTradeMarginReturn',currentBlockHeight)
2749
+ }
2750
+
2751
+ let debit = settlementPNL < 0 ? Math.abs(settlementPNL) : 0;
2752
+ if (debit > 0) {
2753
+ const recovery = await this.sourceFundsForLoss(
2754
+ match.buyOrder.buyerAddress,
2755
+ collateralPropertyId,
2756
+ debit,
2757
+ currentBlockHeight,
2758
+ trade.contractId
2759
+ );
2760
+
2761
+ realizedBuyerLoss=recovery.totalUsed
2762
+
2763
+ if (recovery.remaining > 0) {
2764
+ console.log(`⚠️ Buyer still short ${recovery.remaining}`);
2765
+ // optional: escalate to insurance/liquidation path
2766
+ trade.remainderLiq = recovery.remainder
2767
+
2768
+ }
2769
+ } else {
2770
+ realizedBuyerProfit= settlementPNL
2771
+ }
2772
+
2773
+
2774
+
2775
+ buyerPnl=new BigNumber(settlementPNL)
2776
+ const savePNLParams = {height:currentBlockHeight, contractId:match.buyOrder.contractId, accountingPNL: match.buyerPosition.realizedPNL, isBuyer: true,
2777
+ address: match.buyOrder.buyerAddress, amount: closedShorts, tradePrice: match.tradePrice, collateralPropertyId: collateralPropertyId,
2778
+ timestamp: new Date().toISOString(), txid: match.buyOrder.buyerTx, settlementPNL: settlementPNL, marginReduction:reduction, avgEntry: avgEntry}
2779
+ //console.log('preparing to call savePNL with params '+JSON.stringify(savePNLParams))
2780
+ tradeHistoryManager.savePNL(savePNLParams)
2781
+ }
2782
+
2783
+ if ((isSellerReducingPosition||isSellerFlippingPosition)/*&&!match.sellOrder.liq*/){
2784
+ let closedContracts = match.sellOrder.amount
2785
+
2786
+ if(isSellerFlippingPosition){
2787
+ closedContracts-=flipShort
2788
+ }
2789
+
2790
+ // ========== FIX: Use pre-update avgPrice for settlement ==========
2791
+ // After updateContractBalancesWithMatch, avgPrice may be null if the
2792
+ // position closed. Use the captured value from before the update.
2793
+ let avgEntry = sellerAvgPriceBeforeUpdate || match.sellerPosition.avgPrice;
2794
+ // ===================================================================
2795
+
2796
+ console.log('position before realizePnl '+JSON.stringify(match.sellerPosition))
2797
+ match.sellerPosition = await marginMap.realizePnl(match.sellOrder.sellerAddress, closedContracts, match.tradePrice, avgEntry, isInverse, perContractNotional, match.sellerPosition, false,match.sellOrder.contractId);
2798
+ //then we will look at the last settlement mark price for this contract or default to the LIFO Avg. Entry if
2799
+ //the closing trade and the opening trades reference happened in the same block (exceptional, will add later)
2800
+
2801
+ console.log('position before settlePNL '+JSON.stringify(match.sellerPosition))
2802
+
2803
+ let settlementPNL =
2804
+ await marginMap.settlePNL(
2805
+ trade.sellerAddress,
2806
+ sellerClosed,
2807
+ trade.price,
2808
+ lastPrice,
2809
+ trade.contractId,
2810
+ currentBlockHeight,
2811
+ isInverse,
2812
+ perContractNotional
2813
+ )
2814
+
2815
+
2816
+ console.log('settlementPNL for seller '+settlementPNL)
2817
+
2818
+ //then we figure out the aggregate position's margin situation and liberate margin on a pro-rata basis
2819
+ console.log('position before going into reduce Margin '+closedContracts+' '+flipShort+' '+match.sellOrder.amount/*JSON.stringify(match.sellerPosition)*/)
2820
+ const reduction = await marginMap.reduceMargin(match.sellerPosition, closedContracts, initialMarginPerContract, match.sellOrder.contractId, match.sellOrder.sellerAddress, false, feeInfo.sellFeeFromMargin, sellerFee)
2821
+ console.log('sell reduction '+JSON.stringify(reduction))
2822
+ //{netMargin,mode}
2823
+ const sufficientMargin = await TallyMap.hasSufficientMargin(match.sellOrder.sellerAddress,collateralPropertyId,reduction)
2824
+
2825
+ if(reduction !==0&&sufficientMargin.hasSufficient){
2826
+ await TallyMap.updateBalance(match.sellOrder.sellerAddress, collateralPropertyId, reduction, 0, -reduction, 0, 'contractTradeMarginReturn',currentBlockHeight)
2827
+ }
2828
+
2829
+ let debit = settlementPNL < 0 ? Math.abs(settlementPNL) : 0;
2830
+ if (debit > 0) {
2831
+ const recovery = await this.sourceFundsForLoss(
2832
+ match.sellOrder.sellerAddress,
2833
+ collateralPropertyId,
2834
+ debit,
2835
+ currentBlockHeight,
2836
+ trade.contractId
2837
+ );
2838
+ realizedSellerLoss=recovery.totalUsed
2839
+ if (recovery.remaining > 0) {
2840
+ console.log(`⚠️ Seller still short ${recovery.remaining}`);
2841
+ trade.remainderLiq = recovery.remainder
2842
+ }
2843
+ } else {
2844
+ realizedSellerProfit= settlementPNL
2845
+ }
2846
+
2847
+ sellerPnl=new BigNumber(settlementPNL)
2848
+ const savePNLParams = {height:currentBlockHeight, contractId:match.sellOrder.contractId, accountingPNL: match.sellerPosition.realizedPNL, isBuyer:false,
2849
+ address: match.sellOrder.sellerAddress, amount: closedContracts, tradePrice: match.tradePrice, collateralPropertyId: collateralPropertyId,
2850
+ timestamp: new Date().toISOString(), txid: match.sellOrder.sellerTx, settlementPNL: settlementPNL, marginReduction:reduction, avgEntry: avgEntry}
2851
+ //console.log('preparing to call savePNL with params '+JSON.stringify(savePNLParams))
2852
+ tradeHistoryManager.savePNL(savePNLParams)
2853
+ }
2854
+
2855
+ if(realizedSellerProfit > 0) {
2856
+ const realizedSellerProfitBN = new BigNumber(realizedSellerProfit)
2857
+ }
2858
+
2859
+ const realizedSellerProfitBN = new BigNumber(realizedSellerProfit)
2860
+ if (isLiquidation) {
2861
+ // Liquidation: credit full profit - funded by counterparty loss or tie-off
2862
+ await TallyMap.updateBalance(
2863
+ match.sellOrder.sellerAddress,
2864
+ collateralPropertyId,
2865
+ realizedSellerProfitBN.toNumber(),
2866
+ 0, 0, 0,
2867
+ 'liquidationProfit',
2868
+ currentBlockHeight
2869
+ );
2870
+ } else {
2871
+ // Normal trade: cap immediate payout by realized loss funding
2872
+ console.log('real profit '+realizedSellerProfit+' '+realizedBuyerLoss)
2873
+ let immediate = BigNumber.min(realizedSellerProfitBN, realizedBuyerLoss);
2874
+ const deferred = realizedSellerProfitBN.minus(immediate);
2875
+ if(!isBuyerReducingPosition||isBuyerFlippingPosition){
2876
+ immediate = realizedSellerProfitBN
2877
+ }
2878
+ console.log("BASDFSDF deffered and immediate in seller contract profit settlement "+deferred+' '+immediate)
2879
+
2880
+ if (immediate.gt(0)) {
2881
+ await TallyMap.updateBalance(
2882
+ match.sellOrder.sellerAddress,
2883
+ collateralPropertyId,
2884
+ immediate.toNumber(),
2885
+ 0, 0, 0,
2886
+ 'contractTradeSettlement',
2887
+ currentBlockHeight
2888
+ );
2889
+ }
2890
+
2891
+ if (deferred.gt(0)) {
2892
+ trade.sellerDeferredPnl = deferred.toNumber();
2893
+ }
2894
+ }
2895
+
2896
+ if(realizedBuyerProfit > 0) {
2897
+ const realizedBuyerProfitBN = new BigNumber(realizedBuyerProfit)
2898
+ console.log('realizing buyer profit maybe '+realizedBuyerProfit)
2899
+ if (isLiquidation) {
2900
+ // Liquidation: credit full profit
2901
+ await TallyMap.updateBalance(
2902
+ match.buyOrder.buyerAddress,
2903
+ collateralPropertyId,
2904
+ realizedBuyerProfitBN.toNumber(),
2905
+ 0, 0, 0,
2906
+ 'liquidationProfit',
2907
+ currentBlockHeight
2908
+ );
2909
+ } else {
2910
+ // Normal trade: cap immediate payout by realized loss funding
2911
+ let immediate = BigNumber.min(realizedBuyerProfitBN, realizedSellerLoss);
2912
+ const deferred = realizedBuyerProfitBN.minus(immediate);
2913
+ if(!isSellerReducingPosition||isSellerFlippingPosition){
2914
+ immediate = realizedBuyerProfitBN
2915
+ if(isSellerFlippingPosition){
2916
+
2917
+ }
2918
+ }
2919
+ if (immediate.gt(0)) {
2920
+ await TallyMap.updateBalance(
2921
+ match.buyOrder.buyerAddress,
2922
+ collateralPropertyId,
2923
+ immediate.toNumber(),
2924
+ 0, 0, 0,
2925
+ 'contractTradeSettlement',
2926
+ currentBlockHeight
2927
+ );
2928
+ }
2929
+
2930
+ if (deferred.gt(0)) {
2931
+ trade.buyerDeferredPnl = deferred.toNumber();
2932
+ }
2933
+ }
2934
+ }
2935
+
2936
+ const contractLTCValue = await VolumeIndex.getContractUnitLTCValue(trade.contractId)
2937
+ const totalContractsLTCValue = new BigNumber(contractLTCValue).times(trade.amount).decimalPlaces(8).toNumber()
2938
+ if (!Number.isFinite(Number(totalContractsLTCValue))) {
2939
+ throw new Error(`${contractLTCValue} ${trade.amount}`);
2940
+ }
2941
+ console.log('contract LTC Value '+contractLTCValue)
2942
+ if(contractLTCValue==0){throw new Error()}
2943
+ await VolumeIndex.saveVolumeDataById(
2944
+ trade.contractId,
2945
+ trade.amount,
2946
+ totalContractsLTCValue,
2947
+ trade.price,
2948
+ trade.block,
2949
+ 'contract')
2950
+
2951
+ //see if the trade qualifies for increased Liquidity Reward
2952
+ var qualifiesBasicLiqReward = await this.evaluateBasicLiquidityReward(match,channel,true)
2953
+ var qualifiesEnhancedLiqReward = await this.evaluateEnhancedLiquidityReward(match,channel)
2954
+ if(qualifiesBasicLiqReward){
2955
+ var notionalTokens = notionalValue*trade.amount
2956
+ const liqRewardBaseline = await VolumeIndex.baselineLiquidityReward(notionalTokens,0.000025,collateralPropertyId)
2957
+ TallyMap.updateBalance(match.sellOrder.sellerAddress,3,liqRewardBaseline,0,0,0,'baselineLiquidityReward')
2958
+ TallyMap.updateBalance(match.buyOrder.buyerAddress,3,liqRewardBaseline,0,0,0,'baselineLiquidityReward')
2959
+ }
2960
+
2961
+ if(qualifiesEnhancedLiqReward){
2962
+ var notionalTokens = notionalValue*trade.amount
2963
+ const liqRewardBaseline= await VolumeIndex.calculateLiquidityReward(notionalTokens)
2964
+ TallyMap.updateBalance(match.sellOrder.sellerAddress,3,liqRewardBaseline,0,0,0,'enhancedLiquidityReward')
2965
+ TallyMap.updateBalance(match.buyOrder.buyerAddress,3,liqRewardBaseline,0,0,0,'enhancedLiquidityReward')
2966
+ }
2967
+ // Save the updated margin map
2968
+ await marginMap.saveMarginMap(currentBlockHeight);
2969
+
2970
+ const delta = buyerPnl.plus(sellerPnl);
2971
+
2972
+ if (!isLiquidation&&(isSellerReducingPosition||isSellerFlippingPosition)&&(isBuyerReducingPosition||isBuyerFlippingPosition)) {
2973
+ if (delta.gt(0)) {
2974
+ // Net profit in this trade (winner with no/partial loser to fund)
2975
+ // Creates IOU claims for the unfunded portion
2976
+ await PnlIou.addIouClaims(
2977
+ trade.contractId,
2978
+ collateralPropertyId,
2979
+ currentBlockHeight,
2980
+ trade.buyerAddress,
2981
+ trade.sellerAddress,
2982
+ buyerPnl,
2983
+ sellerPnl,
2984
+ delta
2985
+ );
2986
+
2987
+ // Record as profit (system owes more)
2988
+ await PnlIou.addProfit(
2989
+ trade.contractId,
2990
+ collateralPropertyId,
2991
+ delta,
2992
+ currentBlockHeight
2993
+ );
2994
+ } else if (delta.lt(0)) {
2995
+ // Net loss in this trade (loser with no/partial winner)
2996
+ // Real tokens were debited - available for IOU payout
2997
+ await PnlIou.addLoss(
2998
+ trade.contractId,
2999
+ collateralPropertyId,
3000
+ delta.abs(),
3001
+ currentBlockHeight
3002
+ );
3003
+ }
3004
+ // If delta === 0, trade was fully offset internally, nothing to track
3005
+ }
3006
+
3007
+ trade.delta = delta;
3008
+ trades.push(trade);
3009
+ }
3010
+ return trades
3011
+ }
3012
+
3013
+
3014
+ /**
3015
+ * calculateFee
3016
+ * - Positive result => taker fee (debit)
3017
+ * - Negative result => maker rebate (credit)
3018
+ *
3019
+ * Inputs:
3020
+ * amount: trade size (contracts or units)
3021
+ * columnAIsSeller: bool
3022
+ * columnAIsMaker: bool | undefined (legacy tx may omit)
3023
+ * isInverse: bool
3024
+ * isBuyer: bool (this side is the buyer?)
3025
+ * lastMark: price used to value notional
3026
+ * notionalValue: contract notional
3027
+ * channel: bool (true = off-chain channel => fees ÷ 10)
3028
+ */
3029
+ calculateFee({
3030
+ amountBuy,
3031
+ amountSell,
3032
+ buyMaker,
3033
+ sellMaker,
3034
+ isInverse,
3035
+ lastMark,
3036
+ notionalValue,
3037
+ channel
3038
+ }) {
3039
+ const BNnotionalValue = new BigNumber(notionalValue);
3040
+ const BNlastMark = new BigNumber(lastMark);
3041
+ const BNamountBuy = new BigNumber(amountBuy);
3042
+ const BNamountSell = new BigNumber(amountSell);
3043
+
3044
+ let takerRate = new BigNumber(0.0005); // +5 bps
3045
+ let makerRate = new BigNumber(-0.00025); // –2.5 bps rebate
3046
+
3047
+ if (channel === true) {
3048
+ takerRate = takerRate.div(10); // +0.5 bps
3049
+ makerRate = makerRate.div(10); // –0.25 bps
3050
+ }
3051
+
3052
+ const baseFee = (bps, amt) =>
3053
+ isInverse
3054
+ ? new BigNumber(bps).times(BNnotionalValue).div(BNlastMark).times(amt)
3055
+ : new BigNumber(bps).times(BNlastMark).div(BNnotionalValue).times(amt);
3056
+
3057
+ // ----------------------------------------------------------
3058
+ // CASE 1 — Neither side is maker → same-block on-chain match
3059
+ // → apply “half taker” to both: 1.25bps each (split of 2.5bps)
3060
+ // ----------------------------------------------------------
3061
+ if (!buyMaker && !sellMaker) {
3062
+ // full taker fee on buy side
3063
+ let raw = baseFee(takerRate, BNamountBuy).abs();
3064
+
3065
+ let sats = raw.times(1e8).integerValue(BigNumber.ROUND_FLOOR);
3066
+
3067
+ // ensure final total fee is EVEN sats
3068
+ if (!sats.mod(2).isZero()) sats = sats.plus(1);
3069
+
3070
+ // split evenly
3071
+ const half = sats.idiv(2);
3072
+
3073
+ return {
3074
+ buyerFee: half.div(1e8),
3075
+ sellerFee: half.div(1e8)
3076
+ };
3077
+ }
3078
+
3079
+ // ----------------------------------------------------------
3080
+ // CASE 2 — Exactly one maker → normal match
3081
+ // ----------------------------------------------------------
3082
+ const buyerIsTaker = (buyMaker === false);
3083
+ const sellIsTaker = (sellMaker === false);
3084
+
3085
+ // exactly one of these is taker
3086
+ const takerSide = buyerIsTaker ? 'buyer' : 'seller';
3087
+ const makerSide = buyerIsTaker ? 'seller' : 'buyer';
3088
+
3089
+ const takerAmt = buyerIsTaker ? BNamountBuy : BNamountSell;
3090
+
3091
+ // compute taker fee once
3092
+ let rawTaker = baseFee(takerRate, takerAmt).abs();
3093
+
3094
+ let sats = rawTaker.times(1e8).integerValue(BigNumber.ROUND_FLOOR);
3095
+
3096
+ // make sure sats is EVEN → avoids downstream mint / burn
3097
+ if (!sats.mod(2).isZero()) sats = sats.plus(1);
3098
+
3099
+ const makerRebate = sats.negated().div(2);
3100
+
3101
+ // package results
3102
+ let buyerFee, sellerFee;
3103
+
3104
+ if (takerSide === 'buyer') {
3105
+ buyerFee = sats.div(1e8)
3106
+ sellerFee = makerRebate.div(1e8)
3107
+ } else {
3108
+ buyerFee = makerRebate.div(1e8)
3109
+ sellerFee = sats.div(1e8)
3110
+ }
3111
+
3112
+ return { buyerFee, sellerFee };
3113
+ }
3114
+
3115
+ resolveMaker(columnAIsSeller, columnAIsMaker) {
3116
+ const makerIsA = (columnAIsMaker === true)
3117
+ ? true
3118
+ : (columnAIsMaker === false)
3119
+ ? false
3120
+ : !columnAIsSeller; // inference for legacy tx
3121
+ return {
3122
+ sellerMaker: columnAIsSeller && makerIsA,
3123
+ buyerMaker: !columnAIsSeller && makerIsA,
3124
+ };
3125
+ }
3126
+
3127
+ deriveTradeDelta(match, buyerClosed, sellerClosed, flipLong, flipShort) {
3128
+ const beforeBuyer = match.buyerPosition.contracts - match.buyOrder.amount + buyerClosed;
3129
+ const afterBuyer = match.buyerPosition.contracts;
3130
+
3131
+ const beforeSeller = match.sellerPosition.contracts + match.sellOrder.amount - sellerClosed;
3132
+ const afterSeller = match.sellerPosition.contracts;
3133
+
3134
+ // ------------------------------------------------------------
3135
+ // Compute opened INDEPENDENTLY for buyer and seller
3136
+ // A party "opens" when they're not closing existing positions
3137
+ // ------------------------------------------------------------
3138
+ const tradeAmount = match.buyOrder.amount;
3139
+
3140
+ // Buyer opens = trade amount minus what they closed
3141
+ // If buyer closed 3 of 5, they opened 2 new longs
3142
+ let buyerOpened = tradeAmount - buyerClosed;
3143
+ if (flipLong > 0) {
3144
+ // Flip case: they closed all shorts and opened new longs
3145
+ buyerOpened = flipLong;
3146
+ }
3147
+
3148
+ // Seller opens = trade amount minus what they closed
3149
+ // If seller closed 3 of 5, they opened 2 new shorts
3150
+ let sellerOpened = tradeAmount - sellerClosed;
3151
+ if (flipShort > 0) {
3152
+ // Flip case: they closed all longs and opened new shorts
3153
+ sellerOpened = flipShort;
3154
+ }
3155
+
3156
+ return {
3157
+ buyer: {
3158
+ delta: match.buyOrder.amount,
3159
+ opened: buyerOpened,
3160
+ wasLong: beforeBuyer > 0,
3161
+ isLong: afterBuyer > 0
3162
+ },
3163
+ seller: {
3164
+ delta: -match.sellOrder.amount,
3165
+ opened: sellerOpened,
3166
+ wasLong: beforeSeller > 0,
3167
+ isLong: afterSeller > 0
3168
+ }
3169
+ };
3170
+ }
3171
+
3172
+
3173
+ async locateFee(
3174
+ match,
3175
+ reserveBalanceA,
3176
+ reserveBalanceB,
3177
+ collateralPropertyId,
3178
+ buyerFee, // signed: >0 taker debit, <0 maker rebate
3179
+ sellerFee, // signed: >0 taker debit, <0 maker rebate
3180
+ isBuyerReducingPosition,
3181
+ isSellerReducingPosition,
3182
+ block,
3183
+ isLiq,
3184
+ cacheAdd = 0 // ⬅️ sum of feeCache writes for this match (Number), default 0
3185
+ ) {
3186
+ const TallyMap = require('./tally.js');
3187
+ const MarginMap = require('./marginMap.js');
3188
+ const marginMap = await MarginMap.loadMarginMap(match.sellOrder.contractId);
3189
+
3190
+ const RD = BigNumber.ROUND_DOWN;
3191
+ buyerFee = new BigNumber(buyerFee).decimalPlaces(8, RD).toNumber();
3192
+ sellerFee = new BigNumber(sellerFee).decimalPlaces(8, RD).toNumber();
3193
+ cacheAdd = new BigNumber(cacheAdd).decimalPlaces(8, RD).toNumber();
3194
+
3195
+ let buyFeeFromMargin = false;
3196
+ let buyFeeFromReserve = false;
3197
+ let buyFeeFromAvailable = false;
3198
+ let sellFeeFromMargin = false;
3199
+ let sellFeeFromReserve = false;
3200
+ let sellFeeFromAvailable = false;
3201
+
3202
+ const feeInfo = {
3203
+ sellFeeFromAvailable,
3204
+ sellFeeFromReserve,
3205
+ sellFeeFromMargin,
3206
+ buyFeeFromAvailable,
3207
+ buyFeeFromReserve,
3208
+ buyFeeFromMargin,
3209
+ sellerFee,
3210
+ buyerFee,
3211
+ };
3212
+
3213
+ console.log('🔍 [locateFee] Checking balances to apply fees...');
3214
+ console.log('🧾 Buyer fee:', buyerFee, ', Seller fee:', sellerFee, ', Property:', collateralPropertyId);
3215
+
3216
+ const buyerAddr = match.buyOrder.buyerAddress;
3217
+ const sellerAddr = match.sellOrder.sellerAddress;
3218
+ const txid = match.txid || `contract-fee-${block}`;
3219
+
3220
+ // -------- BUYER SIDE --------
3221
+ if (buyerFee < 0) {
3222
+ // Negative = rebate → always credit
3223
+ await TallyMap.updateBalance(
3224
+ buyerAddr, collateralPropertyId,
3225
+ -buyerFee, 0, 0, 0, 'contractFeeRebate', block, txid
3226
+ );
3227
+ feeInfo.buyFeeFromAvailable = true;
3228
+ console.log('💚 Credited buyer rebate (available):', new BigNumber(-buyerFee).toFixed(8));
3229
+ } else if (buyerFee > 0) {
3230
+ // Positive = debit → sufficiency gates
3231
+ let buyerAvail = (await TallyMap.hasSufficientBalance(buyerAddr, collateralPropertyId, buyerFee)).hasSufficient;
3232
+ let buyerReserve = (await TallyMap.hasSufficientReserve(buyerAddr, collateralPropertyId, buyerFee)).hasSufficient;
3233
+ let buyerMargin = (await TallyMap.hasSufficientMargin(buyerAddr, collateralPropertyId, buyerFee)).hasSufficient;
3234
+
3235
+ console.log(`🧾 Buyer available: ${buyerAvail}, reserve: ${buyerReserve}, margin: ${buyerMargin}`);
3236
+
3237
+ if (buyerAvail) {
3238
+ await TallyMap.updateBalance(buyerAddr, collateralPropertyId, -buyerFee, 0, 0, 0, 'contractFee', block, txid);
3239
+ feeInfo.buyFeeFromAvailable = true;
3240
+ console.log('💰 Buyer fee from available');
3241
+ } else if (buyerReserve) {
3242
+ await TallyMap.updateBalance(buyerAddr, collateralPropertyId, 0, -buyerFee, 0, 0, 'contractFee', block, txid);
3243
+ feeInfo.buyFeeFromReserve = true;
3244
+ console.log('💰 Buyer fee from reserve');
3245
+ } else if (buyerMargin) {
3246
+ await TallyMap.updateBalance(buyerAddr, collateralPropertyId, 0, 0, -buyerFee, 0, 'contractFee', block, txid);
3247
+ feeInfo.buyFeeFromMargin = true;
3248
+ console.log('💰 Buyer fee from margin');
3249
+ } else {
3250
+ console.warn('⚠️ Buyer fee could not be debited from any source.');
3251
+ }
3252
+
3253
+ }
3254
+
3255
+ // -------- SELLER SIDE --------
3256
+ if (sellerFee < 0) {
3257
+ await TallyMap.updateBalance(
3258
+ sellerAddr, collateralPropertyId,
3259
+ -sellerFee, 0, 0, 0, 'contractFeeRebate', block, txid
3260
+ );
3261
+ feeInfo.sellFeeFromAvailable = true;
3262
+ console.log('💚 Credited seller rebate (available):', new BigNumber(-sellerFee).toFixed(8));
3263
+ } else if (sellerFee > 0) {
3264
+ let sellerAvail = (await TallyMap.hasSufficientBalance(sellerAddr, collateralPropertyId, sellerFee)).hasSufficient;
3265
+ let sellerReserve = (await TallyMap.hasSufficientReserve(sellerAddr, collateralPropertyId, sellerFee)).hasSufficient;
3266
+ let sellerMargin = (await TallyMap.hasSufficientMargin(sellerAddr, collateralPropertyId, sellerFee)).hasSufficient;
3267
+
3268
+ console.log(`🧾 Seller available: ${sellerAvail}, reserve: ${sellerReserve}, margin: ${sellerMargin}`);
3269
+
3270
+ if (sellerAvail) {
3271
+ await TallyMap.updateBalance(sellerAddr, collateralPropertyId, -sellerFee, 0, 0, 0, 'contractFee', block, txid);
3272
+ feeInfo.sellFeeFromAvailable = true;
3273
+ console.log('💰 Seller fee from available');
3274
+ } else if (sellerReserve) {
3275
+ await TallyMap.updateBalance(sellerAddr, collateralPropertyId, 0, -sellerFee, 0, 0, 'contractFee', block, txid);
3276
+ feeInfo.sellFeeFromReserve = true;
3277
+ console.log('💰 Seller fee from reserve');
3278
+ } else if (sellerMargin) {
3279
+ await TallyMap.updateBalance(sellerAddr, collateralPropertyId, 0, 0, -sellerFee, 0, 'contractFee', block, txid);
3280
+ feeInfo.sellFeeFromMargin = true;
3281
+ console.log('💰 Seller fee from margin');
3282
+ } else {
3283
+ console.warn('⚠️ Seller fee could not be debited from any source.');
3284
+ }
3285
+ }
3286
+
3287
+ // -------- Reconciliation log (per match) --------
3288
+ const takerPos = new BigNumber(Math.max(buyerFee, 0)).plus(Math.max(sellerFee, 0)).decimalPlaces(8, RD);
3289
+ const makerNeg = new BigNumber(Math.max(-buyerFee, 0)).plus(Math.max(-sellerFee, 0)).decimalPlaces(8, RD);
3290
+ const cacheBN = new BigNumber(cacheAdd).decimalPlaces(8, RD);
3291
+ const net = takerPos.negated().plus(makerNeg).plus(cacheBN).decimalPlaces(8, RD);
3292
+
3293
+ console.log('[recon]',
3294
+ 'takerPos=', takerPos.toFixed(),
3295
+ 'makerNeg=', makerNeg.toFixed(),
3296
+ 'cacheAdd=', cacheBN.toFixed(),
3297
+ 'net=', net.toFixed()
3298
+ );
3299
+
3300
+ console.log('✅ [locateFee] Fee sources determined:', JSON.stringify(feeInfo, null, 2));
3301
+ return feeInfo;
3302
+ }
3303
+
3304
+ /**
3305
+ * Route and cache a matched trade fee (buyer or seller).
3306
+ *
3307
+ * - Does NOT divide fee (full sats only)
3308
+ * - Correctly handles taker-only or dual-taker cases
3309
+ * - No double-splits (updateFeeCache does the only split)
3310
+ * - No misrouting to wrong contract ID
3311
+ *
3312
+ * @param {BigNumber} buyerFeeBN
3313
+ * @param {BigNumber} sellerFeeBN
3314
+ * @param {number} collateralPropertyId
3315
+ * @param {Object} match
3316
+ * @param {number} currentBlockHeight
3317
+ */
3318
+ async routeMatchFees(
3319
+ buyerFeeBN,
3320
+ sellerFeeBN,
3321
+ collateralPropertyId,
3322
+ match,
3323
+ currentBlockHeight
3324
+ ) {
3325
+ // Buyer taker fee (positive buyer, negative seller)
3326
+ if (buyerFeeBN.isGreaterThan(0) && sellerFeeBN.isLessThan(0)) {
3327
+ const feeToCache = buyerFeeBN.decimalPlaces(8, BigNumber.ROUND_DOWN).toNumber();
3328
+
3329
+ console.log("route: buyer taker fee → cache", feeToCache);
3330
+
3331
+ await TallyMap.updateFeeCache(
3332
+ collateralPropertyId,
3333
+ feeToCache,
3334
+ match.buyOrder.contractId,
3335
+ currentBlockHeight,
3336
+ true
3337
+ );
3338
+ }
3339
+
3340
+ // Seller taker fee (positive seller, negative buyer)
3341
+ if (sellerFeeBN.isGreaterThan(0) && buyerFeeBN.isLessThan(0)) {
3342
+ const feeToCache = sellerFeeBN.decimalPlaces(8, BigNumber.ROUND_DOWN).toNumber();
3343
+
3344
+ console.log("route: seller taker fee → cache", feeToCache);
3345
+
3346
+ await TallyMap.updateFeeCache(
3347
+ collateralPropertyId,
3348
+ feeToCache,
3349
+ match.sellOrder.contractId,
3350
+ currentBlockHeight,
3351
+ true
3352
+ );
3353
+ }
3354
+
3355
+ // Both positive → dual taker (rare but valid)
3356
+ if (buyerFeeBN.isGreaterThan(0) && sellerFeeBN.isGreaterThan(0)) {
3357
+ const buyerFeeToCache = buyerFeeBN.decimalPlaces(8, BigNumber.ROUND_DOWN).toNumber();
3358
+ const sellerFeeToCache = sellerFeeBN.decimalPlaces(8, BigNumber.ROUND_DOWN).toNumber();
3359
+
3360
+ console.log("route: dual taker fee → buyer:", buyerFeeToCache, "seller:", sellerFeeToCache);
3361
+
3362
+ await TallyMap.updateFeeCache(
3363
+ collateralPropertyId,
3364
+ buyerFeeToCache,
3365
+ match.buyOrder.contractId,
3366
+ currentBlockHeight,
3367
+ true
3368
+ );
3369
+
3370
+ await TallyMap.updateFeeCache(
3371
+ collateralPropertyId,
3372
+ sellerFeeToCache,
3373
+ match.sellOrder.contractId,
3374
+ currentBlockHeight,
3375
+ true
3376
+ );
3377
+ }
3378
+
3379
+ // If both negative or both zero → no fee handling required
3380
+ }
3381
+
3382
+
3383
+
3384
+ async processContractMatchesShort(matches, currentBlockHeight, channel) {
3385
+ const TallyMap = require('./tally.js');
3386
+ const ContractRegistry = require('./contractRegistry.js');
3387
+ const MarginMap = require('./marginMap.js');
3388
+ const tradeHistoryManager = new TradeHistory();
3389
+
3390
+ if (!Array.isArray(matches)) {
3391
+ console.error('Matches is not an array:', matches);
3392
+ matches = [];
3393
+ }
3394
+
3395
+ let counter = 0;
3396
+ for (const match of matches) {
3397
+ counter++;
3398
+ console.log(`Processing match ${counter}: ${JSON.stringify(match)}`);
3399
+
3400
+ // 1. Validate match and load up-to-date state.
3401
+ if (match.buyOrder.buyerAddress === match.sellOrder.sellerAddress) {
3402
+ console.log(`Self-trade nullified for ${match.buyOrder.buyerAddress}`);
3403
+ continue;
3404
+ }
3405
+ await validateMatch(match);
3406
+
3407
+ // 2. Calculate fees & update fee caches.
3408
+ const feeInfo = await calculateFees(match, channel,currentBlockHeight);
3409
+ // 3. Determine if flip logic applies and update collateral.
3410
+ const flipData = await handleFlipLogic(match, feeInfo, currentBlockHeight);
3411
+
3412
+ // 4. Adjust collateral for non-reducing orders.
3413
+ await moveCollateral(match, feeInfo, channel, currentBlockHeight);
3414
+
3415
+ // 5. Update contract balances (positions) using the match.
3416
+ const updatedPositions = await updateContractBalances(match, channel, flipData);
3417
+ match.buyerPosition = updatedPositions.bp;
3418
+ match.sellerPosition = updatedPositions.sp;
3419
+
3420
+ // 6. Settle PnL if the trade reduces the position.
3421
+ await realizePnLAndSettle(match, currentBlockHeight);
3422
+
3423
+ // 7. Record the trade.
3424
+ const trade = buildTradeObject(match, currentBlockHeight, flipData);
3425
+ await recordTrade(trade, currentBlockHeight);
3426
+
3427
+ // 8. Update volume data and liquidity rewards.
3428
+ await updateVolumeAndRewards(match, currentBlockHeight);
3429
+
3430
+ // Save the updated margin map after processing the match.
3431
+ await MarginMap.saveMarginMap(currentBlockHeight);
3432
+ }
3433
+ // Return something if needed.
3434
+ return;
3435
+ }
3436
+
3437
+ // 1. Validate the match and load up-to-date state (positions, collateral, etc.)
3438
+ async validateMatch(match) {
3439
+ // Check for self-trade
3440
+ if (match.buyOrder.buyerAddress === match.sellOrder.sellerAddress) {
3441
+ throw new Error(`Self-trade detected for ${match.buyOrder.buyerAddress}`);
3442
+ }
3443
+ // Load the margin map for this contract
3444
+ const marginMap = await MarginMap.loadMarginMap(match.sellOrder.contractId);
3445
+ match.buyerPosition = await marginMap.getPositionForAddress(match.buyOrder.buyerAddress, match.buyOrder.contractId);
3446
+ match.sellerPosition = await marginMap.getPositionForAddress(match.sellOrder.sellerAddress, match.buyOrder.contractId);
3447
+ if (!match.buyerPosition.address) match.buyerPosition.address = match.buyOrder.buyerAddress;
3448
+ if (!match.sellerPosition.address) match.sellerPosition.address = match.sellOrder.sellerAddress;
3449
+
3450
+ // Attach collateral and notional info
3451
+ match.collateralPropertyId = await ContractRegistry.getCollateralId(match.buyOrder.contractId);
3452
+ const blob = await ContractRegistry.getNotionalValue(match.sellOrder.contractId, match.tradePrice);
3453
+ match.notionalValue = blob.notionalValue;
3454
+ match.perContractNotional = blob.notionalPerContract;
3455
+ // Also fetch tally (reserve/available) for each side if needed later.
3456
+ match.reserveA = await TallyMap.getTally(match.sellOrder.sellerAddress, match.collateralPropertyId);
3457
+ match.reserveB = await TallyMap.getTally(match.buyOrder.buyerAddress, match.collateralPropertyId);
3458
+
3459
+ // Determine if contract is inverse
3460
+ match.inverse = await ContractRegistry.isInverse(match.sellOrder.contractId);
3461
+
3462
+ return match;
3463
+ }
3464
+
3465
+ // 2. Calculate fees for the match and update fee caches
3466
+ async calculateFees(match, channel,block) {
3467
+ // (Assume you have a calculateFee function available.)
3468
+ const buyerFee = calculateFee(
3469
+ match.buyOrder.amount,
3470
+ match.sellOrder.maker,
3471
+ match.buyOrder.maker,
3472
+ match.inverse,
3473
+ true,
3474
+ match.tradePrice,
3475
+ match.notionalValue,
3476
+ channel
3477
+ );
3478
+ const sellerFee = calculateFee(
3479
+ match.sellOrder.amount,
3480
+ match.sellOrder.maker,
3481
+ match.buyOrder.maker,
3482
+ match.inverse,
3483
+ false,
3484
+ match.tradePrice,
3485
+ match.notionalValue,
3486
+ channel
3487
+ );
3488
+ await TallyMap.updateFeeCache(match.collateralPropertyId, buyerFee, match.buyOrder.contractId,block);
3489
+ await TallyMap.updateFeeCache(match.collateralPropertyId, sellerFee, match.buyOrder.contractId,block);
3490
+
3491
+ // Return fee info object. (You can add more properties as needed.)
3492
+ return { buyerFee, sellerFee, buyFeeFromMargin: false, sellFeeFromMargin: false };
3493
+ }
3494
+
3495
+ // 3. Handle flip logic: check if buyer/seller are “flipping” their positions and adjust margin accordingly.
3496
+ async handleFlipLogic(match, feeInfo, currentBlockHeight) {
3497
+ const flipData = { flipLong: 0, flipShort: 0, buyerFullyClosed: false, sellerFullyClosed: false };
3498
+ const initialMarginPerContract = await ContractRegistry.getInitialMargin(match.buyOrder.contractId, match.tradePrice);
3499
+
3500
+ // Buyer flip: if buyer's order amount exceeds the absolute value of a negative (short) position.
3501
+ const isBuyerFlipping = (match.buyOrder.amount > Math.abs(match.buyerPosition.contracts)) && (match.buyerPosition.contracts < 0);
3502
+ // Seller flip: if seller's order amount exceeds a positive (long) position.
3503
+ const isSellerFlipping = (match.sellOrder.amount > match.sellerPosition.contracts) && (match.sellerPosition.contracts > 0);
3504
+
3505
+ if (isBuyerFlipping) {
3506
+ const closedContracts = Math.abs(match.buyerPosition.contracts);
3507
+ flipData.flipLong = match.buyOrder.amount - closedContracts;
3508
+ // Release margin for closed contracts.
3509
+ const marginToRelease = new BigNumber(initialMarginPerContract).times(closedContracts).decimalPlaces(8).toNumber();
3510
+ await TallyMap.updateBalance(
3511
+ match.buyOrder.buyerAddress,
3512
+ match.collateralPropertyId,
3513
+ marginToRelease,
3514
+ -marginToRelease,
3515
+ 0,
3516
+ 0,
3517
+ 'contractMarginRelease',
3518
+ currentBlockHeight
3519
+ );
3520
+ flipData.buyerFullyClosed = true;
3521
+ }
3522
+
3523
+ if (isSellerFlipping) {
3524
+ const closedContracts = Math.abs(match.sellerPosition.contracts);
3525
+ flipData.flipShort = match.sellOrder.amount - closedContracts;
3526
+ const marginToRelease = new BigNumber(initialMarginPerContract).times(closedContracts).decimalPlaces(8).toNumber();
3527
+ await TallyMap.updateBalance(
3528
+ match.sellOrder.sellerAddress,
3529
+ match.collateralPropertyId,
3530
+ marginToRelease,
3531
+ -marginToRelease,
3532
+ 0,
3533
+ 0,
3534
+ 'contractMarginRelease',
3535
+ currentBlockHeight
3536
+ );
3537
+ flipData.sellerFullyClosed = true;
3538
+ }
3539
+
3540
+ return flipData;
3541
+ }
3542
+
3543
+ // 4. Move collateral for non-reducing orders (for buyer and seller).
3544
+ async moveCollateral(match, feeInfo, channel, currentBlockHeight) {
3545
+ // Only move collateral if the order is not marked as liquidation and not reducing.
3546
+ if (!match.buyOrder.liq && !match.buyerReducing) {
3547
+ match.buyerPosition = await ContractRegistry.moveCollateralToMargin(
3548
+ match.buyOrder.buyerAddress,
3549
+ match.buyOrder.contractId,
3550
+ match.buyOrder.amount,
3551
+ match.tradePrice,
3552
+ match.buyOrder.price,
3553
+ false,
3554
+ match.buyOrder.marginUsed,
3555
+ channel,
3556
+ channel ? match.channelAddress : null,
3557
+ currentBlockHeight,
3558
+ feeInfo,
3559
+ match.buyOrder.maker
3560
+ );
3561
+ }
3562
+ if (!match.sellOrder.liq && !match.sellerReducing) {
3563
+ match.sellerPosition = await ContractRegistry.moveCollateralToMargin(
3564
+ match.sellOrder.sellerAddress,
3565
+ match.sellOrder.contractId,
3566
+ match.sellOrder.amount,
3567
+ match.tradePrice,
3568
+ match.sellOrder.price,
3569
+ true,
3570
+ match.sellOrder.marginUsed,
3571
+ channel,
3572
+ channel ? match.channelAddress : null,
3573
+ currentBlockHeight,
3574
+ feeInfo,
3575
+ match.buyOrder.maker
3576
+ );
3577
+ }
3578
+ return match;
3579
+ }
3580
+
3581
+ // 5. Update contract balances using the match.
3582
+ async updateContractBalances(match, channel, flipData) {
3583
+ const marginMap = await MarginMap.loadMarginMap(match.buyOrder.contractId);
3584
+ // Assume updateContractBalancesWithMatch is defined on marginMap.
3585
+ const positions = await marginMap.updateContractBalancesWithMatch(match, channel,
3586
+ (match.buyerReducing || match.sellerReducing),
3587
+ (flipData.flipLong > 0 || flipData.flipShort > 0)
3588
+ );
3589
+ return positions; // e.g., { bp: updated buyerPosition, sp: updated sellerPosition }
3590
+ }
3591
+
3592
+ // 6. Realize PnL and settle for reducing trades.
3593
+ async realizePnLAndSettle(match, currentBlockHeight, flipData) {
3594
+ const marginMap = await MarginMap.loadMarginMap(match.buyOrder.contractId);
3595
+ const lastMark = await ContractRegistry.getPriceAtBlock(match.buyOrder.contractId, currentBlockHeight) || match.tradePrice;
3596
+ // For buyer
3597
+ if ((match.buyerReducing || match.buyerFlipping) && !match.buyOrder.liq) {
3598
+ let closedContracts = match.buyOrder.amount;
3599
+ if (match.buyerFlipping) {
3600
+ closedContracts -= flipData.flipLong;
3601
+ }
3602
+ const avgEntry = match.buyerPosition.avgPrice;
3603
+ match.buyerPosition = await marginMap.realizePnl(
3604
+ match.buyOrder.buyerAddress,
3605
+ closedContracts,
3606
+ match.tradePrice,
3607
+ avgEntry,
3608
+ match.inverse,
3609
+ match.perContractNotional,
3610
+ match.buyerPosition,
3611
+ true,
3612
+ match.buyOrder.contractId
3613
+ );
3614
+ const settlementPNL = await marginMap.settlePNL(
3615
+ match.buyOrder.buyerAddress,
3616
+ closedContracts,
3617
+ match.tradePrice,
3618
+ lastMark,
3619
+ match.buyOrder.contractId,
3620
+ currentBlockHeight
3621
+ );
3622
+ await TallyMap.updateBalance(
3623
+ match.buyOrder.buyerAddress,
3624
+ match.collateralPropertyId,
3625
+ settlementPNL,
3626
+ 0,
3627
+ 0,
3628
+ 0,
3629
+ 'contractTradeSettlement',
3630
+ currentBlockHeight
3631
+ );
3632
+ }
3633
+ // For seller
3634
+ if ((match.sellerReducing || match.sellerFlipping) && !match.sellOrder.liq) {
3635
+ let closedContracts = match.sellOrder.amount;
3636
+ if (match.sellerFlipping) {
3637
+ closedContracts -= flipData.flipShort;
3638
+ }
3639
+ const avgEntry = match.sellerPosition.avgPrice;
3640
+ match.sellerPosition = await marginMap.realizePnl(
3641
+ match.sellOrder.sellerAddress,
3642
+ closedContracts,
3643
+ match.tradePrice,
3644
+ avgEntry,
3645
+ match.inverse,
3646
+ match.perContractNotional,
3647
+ match.sellerPosition,
3648
+ false,
3649
+ match.sellOrder.contractId
3650
+ );
3651
+ const settlementPNL = await marginMap.settlePNL(
3652
+ match.sellOrder.sellerAddress,
3653
+ closedContracts,
3654
+ match.tradePrice,
3655
+ lastMark,
3656
+ match.sellOrder.contractId,
3657
+ currentBlockHeight
3658
+ );
3659
+ await TallyMap.updateBalance(
3660
+ match.sellOrder.sellerAddress,
3661
+ match.collateralPropertyId,
3662
+ settlementPNL,
3663
+ 0,
3664
+ 0,
3665
+ 0,
3666
+ 'contractTradeSettlement',
3667
+ currentBlockHeight
3668
+ );
3669
+ }
3670
+ return match;
3671
+ }
3672
+
3673
+ // 7. Build a trade object from the match data.
3674
+ buildTradeObject(match, currentBlockHeight, flipData) {
3675
+ return {
3676
+ contractId: match.sellOrder.contractId,
3677
+ amount: match.sellOrder.amount,
3678
+ price: match.tradePrice,
3679
+ buyerAddress: match.buyOrder.buyerAddress,
3680
+ sellerAddress: match.sellOrder.sellerAddress,
3681
+ sellerTx: match.sellOrder.sellerTx,
3682
+ buyerTx: match.buyOrder.buyerTx,
3683
+ buyerClose: match.buyOrder.amount - (flipData.flipLong || 0),
3684
+ sellerClose: match.sellOrder.amount - (flipData.flipShort || 0),
3685
+ block: currentBlockHeight,
3686
+ buyerFullClose: (match.buyerPosition.contracts === match.buyOrder.amount),
3687
+ sellerFullClose: (match.sellerPosition.contracts === match.sellOrder.amount),
3688
+ flipLong: flipData.flipLong,
3689
+ flipShort: flipData.flipShort,
3690
+ channel: match.channel,
3691
+ liquidation: Boolean(match.sellOrder.liq || match.buyOrder.liq)
3692
+ };
3693
+ }
3694
+
3695
+ // 8. Record the trade in trade history.
3696
+ async recordTrade(trade, currentBlockHeight) {
3697
+ const tradeHistoryManager = new TradeHistory();
3698
+ await tradeHistoryManager.recordContractTrade(trade, currentBlockHeight);
3699
+ }
3700
+
3701
+ // 9. Update volume data and liquidity rewards.
3702
+ async updateVolumeAndRewards(match, currentBlockHeight) {
3703
+ // Calculate volume (UTXOEquivalentVolume) and update volume data.
3704
+ const UTXOEquivalentVolume = await VolumeIndex.getUTXOEquivalentVolume(
3705
+ match.sellOrder.contractId,
3706
+ match.sellOrder.amount,
3707
+ 'contract',
3708
+ match.collateralPropertyId,
3709
+ match.perContractNotional,
3710
+ match.inverse,
3711
+ match.tradePrice
3712
+ );
3713
+ if (match.channel === false) {
3714
+ await VolumeIndex.saveVolumeDataById(
3715
+ match.sellOrder.contractId,
3716
+ match.sellOrder.amount,
3717
+ UTXOEquivalentVolume,
3718
+ match.tradePrice,
3719
+ currentBlockHeight,
3720
+ 'onChainContract'
3721
+ );
3722
+ } else {
3723
+ await VolumeIndex.saveVolumeDataById(
3724
+ match.sellOrder.contractId,
3725
+ match.sellOrder.amount,
3726
+ UTXOEquivalentVolume,
3727
+ match.tradePrice,
3728
+ currentBlockHeight,
3729
+ 'channelContract'
3730
+ );
3731
+ }
3732
+ // Evaluate and update liquidity rewards if applicable.
3733
+ const qualifiesBasicLiqReward = await evaluateBasicLiquidityReward(match, match.channel, true);
3734
+ const qualifiesEnhancedLiqReward = await evaluateEnhancedLiquidityReward(match, match.channel);
3735
+ if (qualifiesBasicLiqReward) {
3736
+ const notionalTokens = match.notionalValue * match.sellOrder.amount;
3737
+ const liqRewardBaseline = await VolumeIndex.baselineLiquidityReward(notionalTokens, 0.000025, match.collateralPropertyId);
3738
+ await TallyMap.updateBalance(match.sellOrder.sellerAddress, 3, liqRewardBaseline, 0, 0, 0, 'baselineLiquidityReward');
3739
+ await TallyMap.updateBalance(match.buyOrder.buyerAddress, 3, liqRewardBaseline, 0, 0, 0, 'baselineLiquidityReward');
3740
+ }
3741
+ if (qualifiesEnhancedLiqReward) {
3742
+ const notionalTokens = match.notionalValue * match.sellOrder.amount;
3743
+ const liqRewardBaseline = await VolumeIndex.calculateLiquidityReward(notionalTokens);
3744
+ await TallyMap.updateBalance(match.sellOrder.sellerAddress, 3, liqRewardBaseline, 0, 0, 0, 'enhancedLiquidityReward');
3745
+ await TallyMap.updateBalance(match.buyOrder.buyerAddress, 3, liqRewardBaseline, 0, 0, 0, 'enhancedLiquidityReward');
3746
+ }
3747
+ }
3748
+
3749
+ async cancelOrdersByCriteria(fromAddress, orderBookKey, criteria, token, amm) {
3750
+
3751
+ let orderBook = await this.loadOrderBook(orderBookKey); // Assuming this is the correct reference
3752
+ const cancelledOrders = [];
3753
+ let returnFromReserve = 0
3754
+ console.log('orderbook object in cancel ' +JSON.stringify(orderBook))
3755
+ if(!token){
3756
+ //console.log('showing orderbook before cancel '+JSON.stringify(orderBook))
3757
+ }
3758
+ if(orderBook==undefined){
3759
+ // console.log('orderbook undefined, maybe empty ')
3760
+ return []
3761
+ }
3762
+
3763
+ // Check if the cancellation criteria are for AMM orders
3764
+ if (amm) {
3765
+ for (let i = orderBook.buy.length - 1; i >= 0; i--) {
3766
+ const order = orderBook.buy[i];
3767
+ // Check if the order is an AMM order (marked by "amm" sender)
3768
+ if (order.sender === "amm") {
3769
+ cancelledOrders.push(order);
3770
+ orderBook.buy.splice(i, 1);
3771
+ // Adjust return from reserve based on the cancelled order
3772
+ if (token === true) {
3773
+ returnFromReserve += order.amountOffered;
3774
+ } else {
3775
+ returnFromReserve += order.initMargin;
3776
+ }
3777
+ }
3778
+ }
3779
+
3780
+ // Loop through the sell side book as well
3781
+ for (let i = orderBook.sell.length - 1; i >= 0; i--) {
3782
+ const order = orderBook.sell[i];
3783
+ // Check if the order is an AMM order (marked by "amm" sender)
3784
+ if (order.sender === "amm") {
3785
+ cancelledOrders.push(order);
3786
+ orderBook.sell.splice(i, 1);
3787
+ // Adjust return from reserve based on the cancelled order
3788
+ if (token === true) {
3789
+ returnFromReserve += order.amountOffered;
3790
+ } else {
3791
+ returnFromReserve += order.initMargin;
3792
+ }
3793
+ }
3794
+ }
3795
+ } else {
3796
+
3797
+ if(criteria.txid!=undefined){
3798
+ //console.log('cancelling by txid '+criteria.txid)
3799
+ if(criteria.buy==true){
3800
+ for (let i = orderBook.buy.length - 1; i >= 0; i--) {
3801
+ const ord = orderBook.buy[i]
3802
+ if(ord.txid === criteria.txid){
3803
+ cancelledOrders.push(ord);
3804
+
3805
+ //console.log('splicing order '+JSON.stringify(ord))
3806
+ orderBook.buy.splice(i, 1);
3807
+ }
3808
+ }
3809
+ }
3810
+
3811
+ if(criteria.buy==false){
3812
+ for (let i = orderBook.sell.length - 1; i >= 0; i--) {
3813
+ const ordi = orderBook.sell[i]
3814
+ if(ordi.txid === criteria.txid){
3815
+ //console.log('splicing orders out for cancel by txid '+JSON.stringify(ordi))
3816
+ cancelledOrders.push(ordi);
3817
+
3818
+ //console.log('splicing order '+JSON.stringify(ordi))
3819
+ orderBook.buy.splice(i, 1);
3820
+ }
3821
+ }
3822
+
3823
+ }else{
3824
+ console.log('orderbook prior to cancelling '+JSON.stringify(orderBook))
3825
+ for (let i = orderBook.buy.length - 1; i >= 0; i--) {
3826
+
3827
+ const order = orderBook.buy[i];
3828
+
3829
+ if(this.shouldCancelOrder(order,criteria)){
3830
+ // Logic to cancel the order
3831
+ cancelledOrders.push(order);
3832
+
3833
+ //console.log('splicing order '+JSON.stringify(order))
3834
+ orderBook.buy.splice(i, 1);
3835
+
3836
+ if(token==true){
3837
+ returnFromReserve+=order.amountOffered
3838
+ }else{
3839
+ returnFromReserve+=order.initMargin
3840
+ }
3841
+ }
3842
+ }
3843
+
3844
+ //console.log('orderbook sellside '+JSON.stringify(orderBook.sell))
3845
+ for (let i = orderBook.sell.length - 1; i >= 0; i--) {
3846
+ const order = orderBook.sell[i];
3847
+
3848
+ if(this.shouldCancelOrder(order,criteria)){
3849
+ //if(criteria.address=="tltc1qa0kd2d39nmeph3hvcx8ytv65ztcywg5sazhtw8"){console.log('canceling all')}
3850
+ // Logic to cancel the order
3851
+ cancelledOrders.push(order);
3852
+ //console.log('splicing order '+JSON.stringify(order))
3853
+ orderBook.sell.splice(i, 1);
3854
+
3855
+ if(token==true){
3856
+ returnFromReserve+=order.amountOffered
3857
+ }else{
3858
+ returnFromReserve+=order.initMargin
3859
+ }
3860
+ }
3861
+ }
3862
+ }
3863
+ }
3864
+ }
3865
+
3866
+ console.log('returning tokens from reserve '+returnFromReserve)
3867
+ cancelledOrders.returnFromReserve=returnFromReserve
3868
+ // Save the updated order book to the database
3869
+
3870
+ this.orderBooks[orderBookKey] = orderBook
3871
+ console.log('orderbook after cancel operation '+orderBookKey+' '+JSON.stringify(orderBook))
3872
+ await this.saveOrderBook(orderBook, orderBookKey);
3873
+
3874
+ // Log the cancellation for record-keeping
3875
+ //console.log(`Cancelled orders: ${JSON.stringify(cancelledOrders)}`);
3876
+
3877
+ // Return the details of the cancelled orders
3878
+ return cancelledOrders;
3879
+ }
3880
+
3881
+ shouldCancelOrder(order, criteria) {
3882
+ /*if (!order || !order.senderAddess) {
3883
+ console.error('Invalid order:', order);
3884
+ return false;
3885
+ }*/
3886
+ //console.log('should cancel order? '+JSON.stringify(order)+' '+JSON.stringify(criteria))
3887
+ //console.log('cancel all criteria '+JSON.stringify(criteria.address!=undefined)+' '+JSON.stringify(order.sender===criteria.address))
3888
+ if(criteria.price!=undefined&&(criteria.buy ? order.price <= criteria.price : order.price >= criteria.price)){
3889
+ return true
3890
+ }
3891
+ if (criteria.address!=undefined && order.sender === criteria.address) {
3892
+ return true;
3893
+ }
3894
+
3895
+ return false;
3896
+ }
3897
+
3898
+ async cancelAllContractOrders(fromAddress, offeredPropertyId,block) {
3899
+ const TallyMap = require('./tally.js')
3900
+ const ContractRegistry = require('./contractRegistry.js')
3901
+ // Logic to cancel all contract orders
3902
+ // Retrieve relevant order details and calculate margin reserved amounts
3903
+ const criteria = { address: fromAddress }; // Criteria to cancel all orders for a specific address
3904
+ const key = offeredPropertyId
3905
+ console.log('about to call cancelOrdersByCriteria in cancelAllContractOrders '+fromAddress, key, criteria)
3906
+ const cancelledOrders = await this.cancelOrdersByCriteria(fromAddress, key, criteria);
3907
+ const collateralPropertyId = await ContractRegistry.getCollateralId(offeredPropertyId);
3908
+ console.log('returning from reserve '+cancelledOrders.returnFromReserve)
3909
+ for (const order of cancelledOrders) {
3910
+ //console.log('applying reserve changes for cancelled order '+JSON.stringify(order))
3911
+ const reserveAmount = parseFloat(order.initMargin)
3912
+ //console.log('about to apply changes '+reserveAmount+typeof reserveAmount)
3913
+ await TallyMap.updateBalance(fromAddress, collateralPropertyId, +reserveAmount, -reserveAmount,0,0,'contractCancel',block);
3914
+ }
3915
+
3916
+ // Return the details of the cancelled orders
3917
+ return cancelledOrders;
3918
+ }
3919
+
3920
+ async cancelContractOrderByTxid (fromAddress, offeredPropertyId, txid,block) {
3921
+ const TallyMap = require('./tally.js')
3922
+ // Logic to cancel a specific contract order by txid
3923
+ // Retrieve order details and calculate margin reserved amount
3924
+ const criteria = { txid: txid }; // Criteria to cancel orders by txid
3925
+ const key = offeredPropertyId
3926
+ const cancelledOrder = await this.cancelOrdersByCriteria(fromAddress, key, criteria);
3927
+ //console.log('cancelling order '+JSON.stringify(cancelledOrder)+' cancelled order price '+cancelledOrder[0].price)
3928
+ const initMarginPerContract = await ContractRegistry.getInitialMargin(offeredPropertyId, cancelledOrder[0].price);
3929
+ //console.log('about to calculate reserveAmount '+cancelledOrder[0].amount + ' '+initMarginPerContract)
3930
+ const reserveAmount = cancelledOrder[0].initMargin
3931
+ const collateralPropertyId = await ContractRegistry.getCollateralId(offeredPropertyId)
3932
+ //console.log('about to move reserve back to available cancelling contract order by txid '+reserveAmount +' '+collateralPropertyId)
3933
+ await TallyMap.updateBalance(fromAddress, collateralPropertyId, reserveAmount, -reserveAmount,0,0,'contractCancel',block);
3934
+
3935
+ // Return the details of the cancelled order
3936
+ return cancelledOrder;
3937
+ }
3938
+
3939
+ async cancelContractBuyOrdersByPrice(fromAddress, offeredPropertyId, price, buy,block) {
3940
+ const TallyMap = require('./tally.js')
3941
+ const criteria = { price: price, buy: false }; // Criteria to cancel sell orders by price
3942
+ const key = offeredPropertyId
3943
+ const cancelledOrders = await this.cancelOrdersByCriteria(fromAddress, key, criteria);
3944
+
3945
+ const collateralPropertyId = await ContractRegistry.getCollateralId(offeredPropertyId);
3946
+
3947
+ for (const order of cancelledOrders) {
3948
+ const reserveAmount = order.initMargin
3949
+ await TallyMap.updateBalance(fromAddress, collateralPropertyId, reserveAmount, -reserveAmount,0,0,'contractCancel',block);
3950
+ }
3951
+
3952
+ // Return the details of the cancelled orders
3953
+ return cancelledOrders;
3954
+ }
3955
+
3956
+ async cancelAllTokenOrders(fromAddress, offeredPropertyId, desiredPropertyId,block) {
3957
+ const TallyMap = require('./tally.js')
3958
+ // Logic to cancel all token orders
3959
+ // Retrieve relevant order details and calculate margin reserved amounts
3960
+
3961
+ const key = this.normalizeOrderBookKey(offeredPropertyId,desiredPropertyId)
3962
+ console.log('cancelAllTokenOrders key'+key)
3963
+ let buy = false
3964
+ if(offeredPropertyId>desiredPropertyId){
3965
+ buy=true
3966
+ }
3967
+ const criteria = { address: fromAddress, buy: buy }; // Criteria to cancel all orders for a specific address
3968
+ const cancelledOrders = await this.cancelOrdersByCriteria(fromAddress, key, criteria);
3969
+
3970
+ for (const order of cancelledOrders) {
3971
+ const reserveAmount = order.amountOffered;
3972
+ console.log('cancelling orders in cancelAll token orders '+JSON.stringify(order)+' '+reserveAmount)
3973
+ await TallyMap.updateBalance(fromAddress, offeredPropertyId, reserveAmount, -reserveAmount,0,0,'tokenCancel',block);
3974
+ }
3975
+
3976
+ // Return the details of the cancelled orders
3977
+ return cancelledOrders;
3978
+ }
3979
+
3980
+ async cancelTokenOrderByTxid(fromAddress, offeredPropertyId, desiredPropertyId, txid,block) {
3981
+ const TallyMap = require('./tally.js')
3982
+ // Logic to cancel a specific token order by txid
3983
+ // Retrieve order details and calculate margin reserved amount
3984
+ const key = this.normalizeOrderBookKey(offeredPropertyId,desiredPropertyId)
3985
+ let buy = false
3986
+ if(offeredPropertyId>desiredPropertyId){
3987
+ buy=true
3988
+ }
3989
+ const cancelledOrder = await this.cancelOrdersByCriteria(fromAddress, key, {txid:txid});
3990
+ const reserveAmount = order.amountOffered;
3991
+ await TallyMap.updateBalance(fromAddress, offeredPropertyId, reserveAmount, -reserveAmount,0,0,'tokenCancel',block);
3992
+
3993
+ // Return the details of the cancelled order
3994
+ return cancelledOrder;
3995
+ }
3996
+
3997
+ async cancelTokenBuyOrdersByPrice(fromAddress, offeredPropertyId, desiredPropertyId, price,block) {
3998
+ const TallyMap = require('./tally.js')
3999
+ // Logic to cancel token buy orders by price
4000
+ // Retrieve relevant buy orders and calculate margin reserved amounts
4001
+ const key = this.normalizeOrderBookKey(offeredPropertyId,desiredPropertyId)
4002
+ let buy = false
4003
+ if(offeredPropertyId>desiredPropertyId){
4004
+ buy=true
4005
+ }
4006
+ const cancelledOrders = await this.cancelOrdersByCriteria(fromAddress, key, {price:price, buy:true});
4007
+
4008
+ for (const order of cancelledOrders) {
4009
+ const reserveAmount = order.amountOffered;
4010
+ await TallyMap.updateBalance(fromAddress, offeredPropertyId, reserveAmount, -reserveAmount,0,0,'tokenCancel',block);
4011
+ }
4012
+
4013
+ // Return the details of the cancelled orders
4014
+ return cancelledOrders;
4015
+ }
4016
+
4017
+ async cancelTokenSellOrdersByPrice(fromAddress, offeredPropertyId, desiredPropertyId, price,block) {
4018
+ const TallyMap = require('./tally.js')
4019
+ // Logic to cancel token sell orders by price
4020
+ // Retrieve relevant sell orders and calculate margin reserved amounts
4021
+ const key = this.normalizeOrderBookKey(offeredPropertyId,desiredPropertyId)
4022
+ const cancelledOrders = await this.cancelOrdersByCriteria(fromAddress, key, {price:price, buy:false});
4023
+ let buy = false
4024
+ if(offeredPropertyId>desiredPropertyId){
4025
+ buy=true
4026
+ }
4027
+ for (const order of cancelledOrders) {
4028
+ const reserveAmount = order.amountOffered;
4029
+ await TallyMap.updateBalance(fromAddress, offeredPropertyId, reserveAmount, -reserveAmount,0,0,'tokenCancel',block);
4030
+ }
4031
+
4032
+ // Return the details of the cancelled orders
4033
+ return cancelledOrders;
4034
+ }
4035
+ async cancelAllOrdersForAddress(fromAddress, key, block, collateralPropertyId) {
4036
+ const TallyMap = require('./tally.js');
4037
+ const ContractRegistry = require('./contractRegistry.js');
4038
+
4039
+ console.log(`\n🛑 Cancelling all contract ${key} orders for ${fromAddress}`);
4040
+
4041
+ let orderBook = await this.loadOrderBook(key, fromAddress);
4042
+ if (!Array.isArray(orderBook.buy)) orderBook.buy = [];
4043
+ if (!Array.isArray(orderBook.sell)) orderBook.sell = [];
4044
+
4045
+ let cancelledOrders = [];
4046
+ let returnFromReserve = 0;
4047
+
4048
+ // Helper for shared cancel logic
4049
+ const filterFn = (side) => (order) => {
4050
+ const isMine = order.sender === fromAddress;
4051
+ const isReduce = !order.initMargin || order.initMargin <= 0;
4052
+
4053
+ if (isMine) {
4054
+ if (isReduce) {
4055
+ console.log(`⚠️ Skipping reduce-only ${side} order ${order.txid}`);
4056
+ return true; // Keep reduce orders
4057
+ }
4058
+ cancelledOrders.push(order);
4059
+ returnFromReserve += order.initMargin;
4060
+ return false; // Remove non-reduce order
4061
+ }
4062
+ return true; // Keep others
4063
+ };
4064
+
4065
+ orderBook.buy = orderBook.buy.filter(filterFn('buy'));
4066
+ orderBook.sell = orderBook.sell.filter(filterFn('sell'));
4067
+
4068
+ console.log(`✅ Cancelled ${cancelledOrders.length} non-reduce orders for ${fromAddress}. Returning ${returnFromReserve} to reserve.`);
4069
+ console.log(JSON.stringify(cancelledOrders, null, 2));
4070
+
4071
+ this.orderBooks[key] = orderBook;
4072
+ await this.saveOrderBook(orderBook, key);
4073
+
4074
+ if (returnFromReserve > 0) {
4075
+ await TallyMap.updateBalance(
4076
+ fromAddress,
4077
+ collateralPropertyId,
4078
+ returnFromReserve,
4079
+ -returnFromReserve,
4080
+ 0,
4081
+ 0,
4082
+ 'contractCancel',
4083
+ block
4084
+ );
4085
+ }
4086
+
4087
+ return cancelledOrders;
4088
+ }
4089
+
4090
+
4091
+ async getOrdersForAddress(address, contractId, offeredPropertyId, desiredPropertyId) {
4092
+ const orderbookId = contractId ? contractId.toString() : `${offeredPropertyId}-${desiredPropertyId}`;
4093
+
4094
+ try {
4095
+ // Load or create order book data
4096
+ const orderbookData = await this.loadOrderBook(orderbookId);
4097
+ const { buy, sell } = orderbookData;
4098
+
4099
+ if (!buy || !sell) {
4100
+ return []; // Return an empty array if buy or sell data is missing
4101
+ }
4102
+
4103
+ // Filter buy orders for the given address
4104
+ const buyOrders = buy.filter(order => order.sender === address);
4105
+
4106
+ // Filter sell orders for the given address
4107
+ const sellOrders = sell.filter(order => order.sender === address);
4108
+
4109
+ // Concatenate buy and sell orders and return the result
4110
+ return buyOrders.concat(sellOrders);
4111
+ } catch (error) {
4112
+ console.error('Error getting orders for address:', error);
4113
+ return []; // Return an empty array in case of an error
4114
+ }
4115
+ }
4116
+
4117
+ // Function to return the current state of the order book for the given key
4118
+ getOrderBookData() {
4119
+ return this.orderBooks[this.orderBookKey];
4120
+ }
4121
+ }
4122
+
4123
+ module.exports = Orderbook;