@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.
- package/.claude/settings.local.json +13 -0
- package/.claude/skills/tl-algo/SKILL.md +255 -0
- package/.gitattributes +2 -0
- package/.github/workflows/publish.yaml +26 -0
- package/4mm.js +163 -0
- package/LICENSE +21 -0
- package/NPMSwapRefactor.zip +0 -0
- package/README.md +217 -0
- package/address.sh +26 -0
- package/algoAPI.js +581 -0
- package/analyzepsbt.js +92 -0
- package/apiEx.js +99 -0
- package/bb_hyperscalper.js +290 -0
- package/bbo_demo.js +111 -0
- package/buyer.js +622 -0
- package/client.js +50 -0
- package/createTxTest.js +26 -0
- package/createWallet.js +75 -0
- package/daytrader.js +531 -0
- package/decodeTest.js +69 -0
- package/fundingManager.js +144 -0
- package/index.js +4 -0
- package/listener.js +27 -0
- package/litecoreTxBuilder.js +1128 -0
- package/mmEx.js +356 -0
- package/networks.js +51 -0
- package/orderbook.js +200 -0
- package/package.json +34 -0
- package/perTradeQueue.js +36 -0
- package/projectsTLNPMTLNPM/package-lock.json +162 -0
- package/projectsTLNPMTLNPM/package.json +5 -0
- package/quick.js +32 -0
- package/quickFut.js +37 -0
- package/quickSell.js +37 -0
- package/relayerClient.js +117 -0
- package/run4mm.js +80 -0
- package/run_bbo_tracker.js +241 -0
- package/seller.js +443 -0
- package/session.js +45 -0
- package/setup-lin-ltc.sh +139 -0
- package/setup-lin.sh +203 -0
- package/setup-win-ltc.bat +108 -0
- package/setup-win.bat +167 -0
- package/spam_screamer_futures.js +222 -0
- package/tradelayer.js/.gitattributes +2 -0
- package/tradelayer.js/README.md +2 -0
- package/tradelayer.js/oldTests/activationTest.js +6 -0
- package/tradelayer.js/oldTests/base58.test.js +23 -0
- package/tradelayer.js/oldTests/base64Decode.test.js +16 -0
- package/tradelayer.js/oldTests/blocksRefactor.js +140 -0
- package/tradelayer.js/oldTests/checkVestBalance.js +25 -0
- package/tradelayer.js/oldTests/consensusHashProto.js +151 -0
- package/tradelayer.js/oldTests/contractOrderbook.js +243 -0
- package/tradelayer.js/oldTests/createPayload.js +0 -0
- package/tradelayer.js/oldTests/createTestnetAddr.js +43 -0
- package/tradelayer.js/oldTests/decode.js +205 -0
- package/tradelayer.js/oldTests/decodeTest.js +50 -0
- package/tradelayer.js/oldTests/displayTallyMap.js +19 -0
- package/tradelayer.js/oldTests/encodeDecode.js +340 -0
- package/tradelayer.js/oldTests/expressTest.js +29 -0
- package/tradelayer.js/oldTests/extractBlocksVanilla.js +214 -0
- package/tradelayer.js/oldTests/extractBlocksVanillaa.js +179 -0
- package/tradelayer.js/oldTests/extractPubkeyTest.js +60 -0
- package/tradelayer.js/oldTests/fillInputCacheProto.js +111 -0
- package/tradelayer.js/oldTests/getRawTxTest.js +22 -0
- package/tradelayer.js/oldTests/indexTest.js +26 -0
- package/tradelayer.js/oldTests/initTokensTest.js +32 -0
- package/tradelayer.js/oldTests/interfaceChild.js +129 -0
- package/tradelayer.js/oldTests/listenerChild.js +112 -0
- package/tradelayer.js/oldTests/opdecode.js +26 -0
- package/tradelayer.js/oldTests/options.js +79 -0
- package/tradelayer.js/oldTests/optxtest.js +116 -0
- package/tradelayer.js/oldTests/optxtest1.js +64 -0
- package/tradelayer.js/oldTests/oracle.test.js +32 -0
- package/tradelayer.js/oldTests/orderbook.test.js +36 -0
- package/tradelayer.js/oldTests/parsing.js +93 -0
- package/tradelayer.js/oldTests/payload.js +13 -0
- package/tradelayer.js/oldTests/persistenceUnitTest.js +23 -0
- package/tradelayer.js/oldTests/property.test.js +53 -0
- package/tradelayer.js/oldTests/propertyLevel.js +75 -0
- package/tradelayer.js/oldTests/propertyTest.js +32 -0
- package/tradelayer.js/oldTests/queryAddressTest.js +17 -0
- package/tradelayer.js/oldTests/salter.js +14 -0
- package/tradelayer.js/oldTests/tally.js +81 -0
- package/tradelayer.js/oldTests/tally.test.js +48 -0
- package/tradelayer.js/oldTests/tally2.js +124 -0
- package/tradelayer.js/oldTests/tally3.js +142 -0
- package/tradelayer.js/oldTests/tallyDiag.js +38 -0
- package/tradelayer.js/oldTests/testGetRaw.js +40 -0
- package/tradelayer.js/oldTests/testHexConvert.js +47 -0
- package/tradelayer.js/oldTests/testNewEncoding.js +96 -0
- package/tradelayer.js/oldTests/testNewEncoding2.js +113 -0
- package/tradelayer.js/oldTests/testNewEncoding3 +112 -0
- package/tradelayer.js/oldTests/testNewEncoding3.js +168 -0
- package/tradelayer.js/oldTests/testOPReturn.js +102 -0
- package/tradelayer.js/oldTests/testPayload.js +23 -0
- package/tradelayer.js/oldTests/testRaw.js +50 -0
- package/tradelayer.js/oldTests/testSendTooMuch.js +20 -0
- package/tradelayer.js/oldTests/testTxBuild +28 -0
- package/tradelayer.js/oldTests/testTxBuild.js +42 -0
- package/tradelayer.js/oldTests/tokenOrderbook.js +243 -0
- package/tradelayer.js/oldTests/txUtilsA.js +515 -0
- package/tradelayer.js/oldTests/validityUnitTest.js +53 -0
- package/tradelayer.js/oldTests/vaults.js +72 -0
- package/tradelayer.js/oldTests/volumeIndex.js +117 -0
- package/tradelayer.js/oldTests/volumeIndex2.js +88 -0
- package/tradelayer.js/output_base64.txt +1 -0
- package/tradelayer.js/package-lock.json +9967 -0
- package/tradelayer.js/package.json +61 -0
- package/tradelayer.js/server/index.js +88 -0
- package/tradelayer.js/server/litecoind.exe +0 -0
- package/tradelayer.js/src/activation.js +303 -0
- package/tradelayer.js/src/adjuster.js +77 -0
- package/tradelayer.js/src/amm.js +400 -0
- package/tradelayer.js/src/base256.js +55 -0
- package/tradelayer.js/src/base94.js +79 -0
- package/tradelayer.js/src/channels.js +1163 -0
- package/tradelayer.js/src/clearing.js +3109 -0
- package/tradelayer.js/src/clearlist.js +364 -0
- package/tradelayer.js/src/client.js +295 -0
- package/tradelayer.js/src/consensus.js +613 -0
- package/tradelayer.js/src/contractRegistry.js +964 -0
- package/tradelayer.js/src/db.js +89 -0
- package/tradelayer.js/src/init.js +24 -0
- package/tradelayer.js/src/insurance.js +347 -0
- package/tradelayer.js/src/interface.js +218 -0
- package/tradelayer.js/src/interfaceExpress.js +178 -0
- package/tradelayer.js/src/iou.js +509 -0
- package/tradelayer.js/src/listener.js +226 -0
- package/tradelayer.js/src/logic.js +1702 -0
- package/tradelayer.js/src/main.js +927 -0
- package/tradelayer.js/src/marginMap.js +2165 -0
- package/tradelayer.js/src/options.js +126 -0
- package/tradelayer.js/src/oracle.js +394 -0
- package/tradelayer.js/src/orderbook.js +4123 -0
- package/tradelayer.js/src/persistence.js +554 -0
- package/tradelayer.js/src/property.js +411 -0
- package/tradelayer.js/src/reOrg.js +41 -0
- package/tradelayer.js/src/scaling.js +145 -0
- package/tradelayer.js/src/tally.js +1275 -0
- package/tradelayer.js/src/tradeHistoryManager.js +552 -0
- package/tradelayer.js/src/txDecoder.js +584 -0
- package/tradelayer.js/src/txEncoder.js +610 -0
- package/tradelayer.js/src/txIndex.js +502 -0
- package/tradelayer.js/src/txUtils.js +1392 -0
- package/tradelayer.js/src/types.js +429 -0
- package/tradelayer.js/src/validity.js +3077 -0
- package/tradelayer.js/src/vaults.js +430 -0
- package/tradelayer.js/src/vesting.js +491 -0
- package/tradelayer.js/src/volumeIndex.js +618 -0
- package/tradelayer.js/src/walletInterface.js +220 -0
- package/tradelayer.js/src/walletListener.js +665 -0
- package/tradelayer.js/tests/256decode.js +82 -0
- package/tradelayer.js/tests/UTXOracle.js +205 -0
- package/tradelayer.js/tests/base94test.js +23 -0
- package/tradelayer.js/tests/cancelTxTest.js +62 -0
- package/tradelayer.js/tests/contractInterfaceTest.js +48 -0
- package/tradelayer.js/tests/decimalTest.js +65 -0
- package/tradelayer.js/tests/decoderTest.js +100 -0
- package/tradelayer.js/tests/deltaCount.js +47 -0
- package/tradelayer.js/tests/deltaCount2.js +60 -0
- package/tradelayer.js/tests/interfaceTest.js +37 -0
- package/tradelayer.js/tests/mainTest.js +53 -0
- package/tradelayer.js/tests/makeActivationTest.js +24 -0
- package/tradelayer.js/tests/maxHeightTest.js +49 -0
- package/tradelayer.js/tests/reverseHash.js +72 -0
- package/tradelayer.js/tests/sensitiveConsoleOutput.txt +267 -0
- package/tradelayer.js/tests/tallyTest.js +40 -0
- package/tradelayer.js/tests/testBuybacks.js +46 -0
- package/tradelayer.js/tests/testCodeHash.js +49 -0
- package/tradelayer.js/tests/testConsensusHash.js +91 -0
- package/tradelayer.js/tests/testDecode.js +30 -0
- package/tradelayer.js/tests/testEncodingLengths.js +129 -0
- package/tradelayer.js/tests/testGetTx +32 -0
- package/tradelayer.js/tests/testGetTx.js +32 -0
- package/tradelayer.js/tests/testHexHash.js +32 -0
- package/tradelayer.js/tests/testIndexHash.js +35 -0
- package/tradelayer.js/tests/testInitContracts.js +38 -0
- package/tradelayer.js/tests/testMaxConsensus.js +12 -0
- package/tradelayer.js/tests/testMaxSynth.js +44 -0
- package/tradelayer.js/tests/testMint.js +21 -0
- package/tradelayer.js/tests/testNetwork.js +33 -0
- package/tradelayer.js/tests/testOrderbookLoad.js +62 -0
- package/tradelayer.js/tests/testRebates.js +32 -0
- package/tradelayer.js/tests/testRedeem.js +22 -0
- package/tradelayer.js/tests/testTokenTrade.js +39 -0
- package/tradelayer.js/tests/testTxBuild.js +42 -0
- package/tradelayer.js/tests/testUTXOTrade.js +27 -0
- package/tradelayer.js/tests/tokenTradeHistory.js +27 -0
- package/tradelayer.js/tests/tradeFutures.js +40 -0
- package/tradelayer.js/tests/tradeHistoryExample.js +35 -0
- package/tradelayer.js/tests/tradeHistoryLoad.js +15 -0
- package/tradelayer.js/tests/txScanTest.js +134 -0
- package/tradelayer.js/tests/validateTest.js +136 -0
- package/tradelayer.js/tests/vestingTest.js +37 -0
- package/tradelayer.js/utils/activateMainnet.js +59 -0
- package/tradelayer.js/utils/activateMainnetDoge.js +63 -0
- package/tradelayer.js/utils/autocompactdb.js +23 -0
- package/tradelayer.js/utils/base64toHex.js +32 -0
- package/tradelayer.js/utils/broadcastDoge.js +38 -0
- package/tradelayer.js/utils/calcRedeem.js +19 -0
- package/tradelayer.js/utils/checkNetwork.js +27 -0
- package/tradelayer.js/utils/createAddress.js +48 -0
- package/tradelayer.js/utils/createAttestation.js +133 -0
- package/tradelayer.js/utils/createContract.js +118 -0
- package/tradelayer.js/utils/createOracle.js +94 -0
- package/tradelayer.js/utils/createwallet.js +20 -0
- package/tradelayer.js/utils/crossFuturesTrades.js +57 -0
- package/tradelayer.js/utils/crossTokenTrades.js +62 -0
- package/tradelayer.js/utils/dumpPriv.js +29 -0
- package/tradelayer.js/utils/generateChannel.js +34 -0
- package/tradelayer.js/utils/getInfo.js +21 -0
- package/tradelayer.js/utils/hardWipe.js +20 -0
- package/tradelayer.js/utils/hexTo64.js +16 -0
- package/tradelayer.js/utils/importAddress.js +28 -0
- package/tradelayer.js/utils/importpriv.js +20 -0
- package/tradelayer.js/utils/issueOracleContract.js +67 -0
- package/tradelayer.js/utils/issueTokens.js +41 -0
- package/tradelayer.js/utils/listunspent.js +66 -0
- package/tradelayer.js/utils/litecoinClient.js +30 -0
- package/tradelayer.js/utils/loadwallet.js +20 -0
- package/tradelayer.js/utils/publishOracle.js +113 -0
- package/tradelayer.js/utils/sendActivation.js +21 -0
- package/tradelayer.js/utils/sendChannelContractTrade.js +34 -0
- package/tradelayer.js/utils/sendChannelTokenTrade.js +34 -0
- package/tradelayer.js/utils/sendCommit.js +24 -0
- package/tradelayer.js/utils/sendDoge.js +62 -0
- package/tradelayer.js/utils/sendDogeMain.js +67 -0
- package/tradelayer.js/utils/sendDogeTx.js +46 -0
- package/tradelayer.js/utils/sendLTC.js +63 -0
- package/tradelayer.js/utils/sendMainnet.js +62 -0
- package/tradelayer.js/utils/sendTransfer.js +19 -0
- package/tradelayer.js/utils/sendVestTest.js +88 -0
- package/tradelayer.js/utils/sendWithdrawal.js +26 -0
- package/tradelayer.js/utils/simpleStart.js +8 -0
- package/tradelayer.js/utils/startStop.js +27 -0
- package/tradelayer.js/utils/structuredTrades.js +136 -0
- package/tradelayer.js/utils/verifySignature.js +90 -0
- package/tradelayer.js/utils/verifyWitnessAndScriptPubkey.js +41 -0
- package/tradelayer.js/utils/walletCache.js +172 -0
- package/tradelayer.js/utils/walletContractInterface.js +48 -0
- package/tradelayer.js/utils/walletFetchTxs.js +66 -0
- package/tradelayer.js/utils/walletUtils.js +97 -0
- package/tradelayer.js/utils/wipeDB.js +55 -0
- package/tradelayer.js/utils/wipeDBNotTx.js +50 -0
- package/txEncoder.js +529 -0
- package/utility.js +28 -0
- package/verifymessage.js +38 -0
- 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;
|