From 17aab624780b23f8dbd312c50233fd936bdf9316 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sun, 6 Jul 2025 19:53:29 -0400 Subject: [PATCH 1/7] test crypto prices without inversion --- src/HavenoClient.test.ts | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/src/HavenoClient.test.ts b/src/HavenoClient.test.ts index 0232670b..4abd99ba 100644 --- a/src/HavenoClient.test.ts +++ b/src/HavenoClient.test.ts @@ -1194,7 +1194,7 @@ test("Can get market depth (Test, CI, sanity check)", async () => { await clearOffers(user2, assetCode); async function clearOffers(havenod: HavenoClient, assetCode: string) { for (const offer of await havenod.getMyOffers(assetCode)) { - if (offer.getBaseCurrencyCode().toLowerCase() === assetCode.toLowerCase()) { + if (offer.getCounterCurrencyCode().toLowerCase() === assetCode.toLowerCase()) { await havenod.removeOffer(offer.getId()); } } @@ -1229,18 +1229,18 @@ test("Can get market depth (Test, CI, sanity check)", async () => { // test buy prices and depths const buyOffers = (await user2.getOffers(assetCode, OfferDirection.BUY)).concat(await user2.getMyOffers(assetCode, OfferDirection.BUY)).sort(function(a, b) { return parseFloat(a.getPrice()) - parseFloat(b.getPrice()) }); - expect(marketDepth.getBuyPricesList()[0]).toEqual(1 / parseFloat(buyOffers[0].getPrice())); // TODO: price when posting offer is reversed. this assumes crypto counter currency - expect(marketDepth.getBuyPricesList()[1]).toEqual(1 / parseFloat(buyOffers[1].getPrice())); - expect(marketDepth.getBuyPricesList()[2]).toEqual(1 / parseFloat(buyOffers[2].getPrice())); - expect(marketDepth.getBuyDepthList()[0]).toEqual(0.15); - expect(marketDepth.getBuyDepthList()[1]).toEqual(0.30); + expect(marketDepth.getBuyPricesList()[0]).toEqual(parseFloat(buyOffers[2].getPrice())); + expect(marketDepth.getBuyPricesList()[1]).toEqual(parseFloat(buyOffers[1].getPrice())); + expect(marketDepth.getBuyPricesList()[2]).toEqual(parseFloat(buyOffers[0].getPrice())); + expect(marketDepth.getBuyDepthList()[0]).toEqual(0.35); + expect(marketDepth.getBuyDepthList()[1]).toEqual(0.5); expect(marketDepth.getBuyDepthList()[2]).toEqual(0.65); // test sell prices and depths const sellOffers = (await user2.getOffers(assetCode, OfferDirection.SELL)).concat(await user2.getMyOffers(assetCode, OfferDirection.SELL)).sort(function(a, b) { return parseFloat(b.getPrice()) - parseFloat(a.getPrice()) }); - expect(marketDepth.getSellPricesList()[0]).toEqual(1 / parseFloat(sellOffers[0].getPrice())); - expect(marketDepth.getSellPricesList()[1]).toEqual(1 / parseFloat(sellOffers[1].getPrice())); - expect(marketDepth.getSellPricesList()[2]).toEqual(1 / parseFloat(sellOffers[2].getPrice())); + expect(marketDepth.getSellPricesList()[0]).toEqual(parseFloat(sellOffers[2].getPrice())); + expect(marketDepth.getSellPricesList()[1]).toEqual(parseFloat(sellOffers[1].getPrice())); + expect(marketDepth.getSellPricesList()[2]).toEqual(parseFloat(sellOffers[0].getPrice())); expect(marketDepth.getSellDepthList()[0]).toEqual(0.3); expect(marketDepth.getSellDepthList()[1]).toEqual(0.6); expect(marketDepth.getSellDepthList()[2]).toEqual(1); @@ -1295,7 +1295,7 @@ test("Can get my offers (Test, CI)", async () => { const offers: OfferInfo[] = await user1.getMyOffers(assetCode); for (const offer of offers) { testOffer(offer, undefined, true); - expect(assetCode).toEqual(isCrypto(assetCode) ? offer.getBaseCurrencyCode() : offer.getCounterCurrencyCode()); // crypto asset codes are base + expect(assetCode).toEqual(offer.getCounterCurrencyCode()); } } }); @@ -1509,13 +1509,12 @@ test("Can post and remove an offer (Test, CI, sanity check)", async () => { // post crypto offer let assetCode = "BCH"; let price = 1 / 17; - price = 1 / price; // TODO: price in crypto offer is inverted let ctx: Partial = {maker: {havenod: user1}, assetCode: assetCode, price: price, extraInfo: "My extra info"}; let offer: OfferInfo = await makeOffer(ctx);; assert.equal(offer.getState(), "AVAILABLE"); - assert.equal(offer.getBaseCurrencyCode(), assetCode); // TODO: base and counter currencies inverted in crypto offer - assert.equal(offer.getCounterCurrencyCode(), "XMR"); - assert.equal(parseFloat(offer.getPrice()), price); + assert.equal(offer.getCounterCurrencyCode(), assetCode); + assert.equal(offer.getBaseCurrencyCode(), "XMR"); + assert.equal(parseFloat(offer.getPrice()), price.toFixed(8)); // has offer offer = await user1.getMyOffer(offer.getId()); @@ -1595,8 +1594,8 @@ test("Can clone offers (Test, CI, sanity check)", async () => { }); assert.notEqual(clonedOffer.getId(), offer.getId()); assert.equal(clonedOffer.getState(), "DEACTIVATED"); // deactivated if same payment method and currency - assert.equal(clonedOffer.getBaseCurrencyCode(), assetCode); - assert.equal(clonedOffer.getCounterCurrencyCode(), "XMR"); + assert.equal(clonedOffer.getCounterCurrencyCode(), assetCode); + assert.equal(clonedOffer.getBaseCurrencyCode(), "XMR"); assert.equal(clonedOffer.getAmount(), offer.getAmount()); assert.equal(clonedOffer.getMinAmount(), offer.getMinAmount()); assert.equal(clonedOffer.getIsPrivateOffer(), offer.getIsPrivateOffer()); @@ -3020,7 +3019,6 @@ async function makeOffer(ctxP?: Partial): Promise { sourceOfferId: ctx.sourceOfferId }); - // test offer testOffer(offer, ctx, true); @@ -4332,8 +4330,8 @@ function testOffer(offer: OfferInfo, ctxP?: Partial, isMyOffer?: b } if (ctx.extraInfo) expect(offer.getExtraInfo().indexOf(ctx.extraInfo)).toBeGreaterThanOrEqual(0); // may contain extra info from payment account expect(offer.getSellerSecurityDepositPct()).toEqual(ctx.securityDepositPct); - expect(offer.getUseMarketBasedPrice()).toEqual(!ctx?.price); - expect(offer.getMarketPriceMarginPct()).toEqual(ctx?.priceMargin ? ctx.priceMargin : 0); + expect(offer.getUseMarketBasedPrice()).toEqual(!ctx.price); + expect(offer.getMarketPriceMarginPct()).toEqual(ctx.priceMargin ? ctx.priceMargin : 0); // TODO: test rest of offer } From eab5b797612a792ce2293c4f418ddea98c94eee7 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sun, 13 Jul 2025 16:13:08 -0400 Subject: [PATCH 2/7] test that maker, taker, arbitrator trades have same date --- src/HavenoClient.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/HavenoClient.test.ts b/src/HavenoClient.test.ts index 4abd99ba..d7bdd69f 100644 --- a/src/HavenoClient.test.ts +++ b/src/HavenoClient.test.ts @@ -3140,6 +3140,8 @@ async function takeOffer(ctxP: Partial): Promise { } // test trade model + assert.equal(ctx.maker.trade!.getDate(), ctx.taker.trade!.getDate(), "Expected trade date to match"); + assert.equal(ctx.taker.trade!.getDate(), ctx.arbitrator.trade!.getDate(), "Expected trade date to match"); await testTrade(takerTrade, ctx); // test buyer and seller balances after offer taken From 6c6f5f243c549088552a846be068a5029e4ab3fc Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 18 Jul 2025 07:29:15 -0400 Subject: [PATCH 3/7] test minimum and maximum offer limits --- src/HavenoClient.test.ts | 103 +++++++++++++++++++++++++++++---------- 1 file changed, 77 insertions(+), 26 deletions(-) diff --git a/src/HavenoClient.test.ts b/src/HavenoClient.test.ts index d7bdd69f..96051dd1 100644 --- a/src/HavenoClient.test.ts +++ b/src/HavenoClient.test.ts @@ -441,7 +441,6 @@ const TestConfig = { } ], maxFee: HavenoUtils.xmrToAtomicUnits(0.5), // local testnet fees can be relatively high - minSecurityDeposit: moneroTs.MoneroUtils.xmrToAtomicUnits(0.1), maxAdjustmentPct: 0.2, daemonPollPeriodMs: 5000, maxWalletStartupMs: 10000, // TODO (woodser): make shorter by switching to jni @@ -493,7 +492,12 @@ const TestConfig = { }, tradeStepTimeoutMs: getBaseCurrencyNetwork() === BaseCurrencyNetwork.XMR_LOCAL ? 60000 : 180000, testTimeout: getBaseCurrencyNetwork() === BaseCurrencyNetwork.XMR_LOCAL ? 2400000 : 5400000, // timeout in ms for each test to complete (40 minutes for private network, 90 minutes for public network) - trade: new TradeContext(defaultTradeConfig) + trade: new TradeContext(defaultTradeConfig), + minAmount: moneroTs.MoneroUtils.xmrToAtomicUnits(0.05), + minSecurityDeposit: moneroTs.MoneroUtils.xmrToAtomicUnits(0.1), + maxAmountNoDeposit: moneroTs.MoneroUtils.xmrToAtomicUnits(1.5), + maxBuyAmountWithChargeback: moneroTs.MoneroUtils.xmrToAtomicUnits(3), + maxSellAmountWithChargeback: moneroTs.MoneroUtils.xmrToAtomicUnits(12) }; interface HavenodContext { @@ -1742,14 +1746,15 @@ test("Can reserve exact amount needed for offer (Test, CI)", async () => { }); }); -test("Cannot post offer exceeding trade limit (Test, CI, sanity check)", async () => { +test("Cannot post offer outside of trade limits (Test, CI, sanity check)", async () => { let assetCode = "USD"; const account = await createPaymentAccount(user1, assetCode, "zelle"); + const diff = 10000000000n; // test posting buy offer above limit try { await executeTrade({ - offerAmount: moneroTs.MoneroUtils.xmrToAtomicUnits(3.1), + offerAmount: TestConfig.maxBuyAmountWithChargeback + diff, direction: OfferDirection.BUY, assetCode: assetCode, makerPaymentAccountId: account.getId(), @@ -1763,7 +1768,7 @@ test("Cannot post offer exceeding trade limit (Test, CI, sanity check)", async ( // test posting sell offer above limit try { await executeTrade({ - offerAmount: moneroTs.MoneroUtils.xmrToAtomicUnits(12.1), + offerAmount: TestConfig.maxSellAmountWithChargeback + diff, direction: OfferDirection.SELL, assetCode: assetCode, makerPaymentAccountId: account.getId(), @@ -1774,27 +1779,73 @@ test("Cannot post offer exceeding trade limit (Test, CI, sanity check)", async ( if (err.message.indexOf("amount is larger than") < 0) throw err; } - // test posting sell offer above limit without buyer deposit - try { - await executeTrade({ - offerAmount: moneroTs.MoneroUtils.xmrToAtomicUnits(1.6), // limit is 1.5 xmr without deposit or fee - offerMinAmount: moneroTs.MoneroUtils.xmrToAtomicUnits(0.25), - direction: OfferDirection.SELL, - assetCode: assetCode, - makerPaymentAccountId: account.getId(), - isPrivateOffer: true, - buyerAsTakerWithoutDeposit: true, - takeOffer: false, - price: 142.23 - }); - throw new Error("Should have rejected posting offer above trade limit") - } catch (err: any) { - if (err.message.indexOf("amount is larger than") < 0) throw err; - } + // test posting sell offer below limit + try { + await executeTrade({ + offerAmount: moneroTs.MoneroUtils.xmrToAtomicUnits(1.6), + offerMinAmount: TestConfig.minAmount - diff, + direction: OfferDirection.SELL, + assetCode: assetCode, + makerPaymentAccountId: account.getId(), + isPrivateOffer: false, + buyerAsTakerWithoutDeposit: false, + takeOffer: false, + price: 142.23 + }); + throw new Error("Should have rejected posting offer above trade limit") + } catch (err: any) { + if (err.message.indexOf("must be above minimum") < 0) throw err; + } + + // test posting sell offer above limit without buyer deposit + try { + await executeTrade({ + offerAmount: TestConfig.maxAmountNoDeposit + diff, + offerMinAmount: moneroTs.MoneroUtils.xmrToAtomicUnits(0.25), + direction: OfferDirection.SELL, + assetCode: assetCode, + makerPaymentAccountId: account.getId(), + isPrivateOffer: true, + buyerAsTakerWithoutDeposit: true, + takeOffer: false, + price: 142.23 + }); + throw new Error("Should have rejected posting offer above trade limit") + } catch (err: any) { + if (err.message.indexOf("must be below maximum") < 0) throw err; + } + + // test posting sell offer below limit without buyer deposit + try { + await executeTrade({ + offerAmount: moneroTs.MoneroUtils.xmrToAtomicUnits(1.4), + offerMinAmount: TestConfig.minAmount - diff, + direction: OfferDirection.SELL, + assetCode: assetCode, + makerPaymentAccountId: account.getId(), + isPrivateOffer: true, + buyerAsTakerWithoutDeposit: true, + takeOffer: false, + price: 142.23 + }); + throw new Error("Should have rejected posting offer above trade limit") + } catch (err: any) { + if (err.message.indexOf("must be above minimum") < 0) throw err; + } + + // test minimum offer limit + let offerId = await executeTrade({ + offerAmount: TestConfig.minAmount, + direction: OfferDirection.SELL, + assetCode: assetCode, + makerPaymentAccountId: account.getId(), + takeOffer: false + }); + await user1.removeOffer(offerId); // test that sell limit is higher than buy limit - let offerId = await executeTrade({ - offerAmount: 2100000000000n, + offerId = await executeTrade({ + offerAmount: HavenoUtils.xmrToAtomicUnits(2.1), direction: OfferDirection.SELL, assetCode: assetCode, makerPaymentAccountId: account.getId(), @@ -3187,8 +3238,8 @@ async function takeOffer(ctxP: Partial): Promise { async function testTrade(trade: TradeInfo, ctx: TradeContext, havenod?: HavenoClient): Promise { expect(BigInt(trade.getAmount())).toEqual(ctx!.tradeAmount); - // test security deposit = max(0.1, trade amount * security deposit pct) - const expectedSecurityDeposit = HavenoUtils.max(HavenoUtils.xmrToAtomicUnits(.1), HavenoUtils.multiply(ctx.tradeAmount!, ctx.securityDepositPct!)); + // test security deposit = max(min security deposit, trade amount * security deposit pct) + const expectedSecurityDeposit = HavenoUtils.max(TestConfig.minSecurityDeposit, HavenoUtils.multiply(ctx.tradeAmount!, ctx.securityDepositPct!)); expect(BigInt(trade.getBuyerSecurityDeposit())).toEqual(ctx.hasBuyerAsTakerWithoutDeposit() ? 0n : expectedSecurityDeposit - ctx.getBuyer().depositTxFee!); expect(BigInt(trade.getSellerSecurityDeposit())).toEqual(expectedSecurityDeposit - ctx.getSeller().depositTxFee!); From 79c56f56d6240215d8382cc25797444f03434dd8 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Tue, 15 Jul 2025 14:10:22 -0400 Subject: [PATCH 4/7] update and enable chat message tests --- src/HavenoClient.test.ts | 95 ++++++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 38 deletions(-) diff --git a/src/HavenoClient.test.ts b/src/HavenoClient.test.ts index 96051dd1..144919de 100644 --- a/src/HavenoClient.test.ts +++ b/src/HavenoClient.test.ts @@ -146,7 +146,8 @@ const defaultTradeConfig: Partial = { disputeSummary: "Seller is winner", walletSyncPeriodMs: 5000, maxTimePeerNoticeMs: 6000, - testChatMessages: true, + testTradeChatMessages: true, + testDisputeChatMessages: true, stopOnFailure: false, // TODO: setting to true can cause error: Http response at 400 or 500 level, http status code: 503 testPayoutConfirmed: true, testPayoutUnlocked: false, @@ -203,7 +204,8 @@ class TradeContext { offerId?: string; takerPaymentAccountId?: string; challenge?: string; - testTraderChat?: boolean; + testTradeChatMessages?: boolean; + tradeChatMessagesTested?: boolean; // resolve dispute resolveDispute?: boolean @@ -230,7 +232,8 @@ class TradeContext { sellerOpenedDispute?: boolean; walletSyncPeriodMs!: number; maxTimePeerNoticeMs!: number; - testChatMessages!: boolean; + testDisputeChatMessages!: boolean; + disputeChatMessagesTested!: boolean; stopOnFailure?: boolean; buyerAppName?: string; sellerAppName?: string; @@ -2706,7 +2709,7 @@ async function executeTrade(ctxP: Partial): Promise { // test trader chat if (ctx.isStopped) return ctx.offerId!; - if (ctx.testTraderChat) await testTradeChat(ctx); + if (ctx.testTradeChatMessages && !ctx.tradeChatMessagesTested) await testTradeChat(ctx); // test trader chat once // get expected payment account payloads if (ctx.isStopped) return ctx.offerId!; @@ -3345,7 +3348,7 @@ async function testOpenDispute(ctxP: Partial) { await arbitrator.addNotificationListener(notification => { HavenoUtils.log(3, "Arbitrator received notification " + notification.getType() + " " + (notification.getChatMessage() ? notification.getChatMessage()?.getMessage() : "")); arbitratorNotifications.push(notification); }); // test chat messages - if (ctx.testChatMessages) { + if (ctx.testDisputeChatMessages && !ctx.disputeChatMessagesTested) { // arbitrator sends chat messages to traders HavenoUtils.log(1, "Arbitrator sending chat messages to traders. tradeId=" + ctx.offerId + ", disputeId=" + openerDispute.getId()); @@ -3419,6 +3422,8 @@ async function testOpenDispute(ctxP: Partial) { expect(attachments[1].getFileName()).toEqual("proof.png"); expect(attachments[1].getBytes()).toEqual(bytes2); expect(chatNotifications[1].getChatMessage()?.getMessage()).toEqual("Dispute peer chat message"); + + ctx.disputeChatMessagesTested = true; // mark chat messages as tested } } @@ -3612,74 +3617,88 @@ async function testTradeChat(ctxP: Partial) { // invalid trade should throw error try { - await user1.getChatMessages("invalid"); + await ctx.maker.havenod!.getChatMessages("invalid"); throw new Error("get chat messages with invalid id should fail"); } catch (err: any) { assert.equal(err.message, "trade with id 'invalid' not found"); } // trade chat should be in initial state - let messages = await user1.getChatMessages(ctx.offerId!); - assert(messages.length === 0); - messages = await user2.getChatMessages(ctx.offerId!); - assert(messages.length === 0); + let messages = await ctx.maker.havenod!.getChatMessages(ctx.offerId!); + expect(messages.length).toEqual(0); + messages = await ctx.taker.havenod!.getChatMessages(ctx.offerId!); + expect(messages.length).toEqual(0); // add notification handlers and send some messages - const user1Notifications: NotificationMessage[] = []; - const user2Notifications: NotificationMessage[] = []; - await user1.addNotificationListener(notification => { user1Notifications.push(notification); }); - await user2.addNotificationListener(notification => { user2Notifications.push(notification); }); + const makerNotifications: NotificationMessage[] = []; + const takerNotifications: NotificationMessage[] = []; + await ctx.maker.havenod!.addNotificationListener(notification => { makerNotifications.push(notification); }); + await ctx.taker.havenod!.addNotificationListener(notification => { takerNotifications.push(notification); }); // send simple conversation and verify the list of messages - const user1Msg = "Hi I'm user1"; - await user1.sendChatMessage(ctx.offerId!, user1Msg); + const makerMsg = "Hi I'm the maker"; + await await ctx.maker.havenod!.sendChatMessage(ctx.offerId!, makerMsg); await wait(ctx.maxTimePeerNoticeMs); - messages = await user2.getChatMessages(ctx.offerId!); + messages = await ctx.taker.havenod!.getChatMessages(ctx.offerId!); expect(messages.length).toEqual(2); expect(messages[0].getIsSystemMessage()).toEqual(true); // first message is system - expect(messages[1].getMessage()).toEqual(user1Msg); + expect(messages[1].getMessage()).toEqual(makerMsg); - const user2Msg = "Hello I'm user2"; - await user2.sendChatMessage(ctx.offerId!, user2Msg); + const takerMsg = "Hello I'm the taker"; + await ctx.taker.havenod!.sendChatMessage(ctx.offerId!, takerMsg); await wait(ctx.maxTimePeerNoticeMs); - messages = await user1.getChatMessages(ctx.offerId!); + messages = await ctx.maker.havenod!.getChatMessages(ctx.offerId!); expect(messages.length).toEqual(3); expect(messages[0].getIsSystemMessage()).toEqual(true); - expect(messages[1].getMessage()).toEqual(user1Msg); - expect(messages[2].getMessage()).toEqual(user2Msg); + expect(messages[1].getMessage()).toEqual(makerMsg); + expect(messages[2].getMessage()).toEqual(takerMsg); // verify notifications - let chatNotifications = getNotifications(user1Notifications, NotificationMessage.NotificationType.CHAT_MESSAGE); - expect(chatNotifications.length).toBe(1); - expect(chatNotifications[0].getChatMessage()?.getMessage()).toEqual(user2Msg); - chatNotifications = getNotifications(user2Notifications, NotificationMessage.NotificationType.CHAT_MESSAGE); - expect(chatNotifications.length).toBe(1); - expect(chatNotifications[0].getChatMessage()?.getMessage()).toEqual(user1Msg); + let chatNotifications = getNotifications(makerNotifications, NotificationMessage.NotificationType.CHAT_MESSAGE); + if (ctx.concurrentTrades) { + expect(chatNotifications.length).toBeGreaterThanOrEqual(1); + } else { + expect(chatNotifications.length).toBe(1); + expect(chatNotifications[0].getChatMessage()?.getMessage()).toEqual(takerMsg); + } + chatNotifications = getNotifications(takerNotifications, NotificationMessage.NotificationType.CHAT_MESSAGE); + if (ctx.concurrentTrades) { + expect(chatNotifications.length).toBeGreaterThanOrEqual(1); + } else { + expect(chatNotifications.length).toBe(1); + expect(chatNotifications[0].getChatMessage()?.getMessage()).toEqual(makerMsg); + } // additional msgs const msgs = ["", " ", "", "さようなら"]; for(const msg of msgs) { - await user1.sendChatMessage(ctx.offerId!, msg); + await ctx.maker.havenod!.sendChatMessage(ctx.offerId!, msg); await wait(1000); // the async operation can result in out of order messages } await wait(ctx.maxTimePeerNoticeMs); - messages = await user2.getChatMessages(ctx.offerId!); + messages = await ctx.taker.havenod!.getChatMessages(ctx.offerId!); let offset = 3; // 3 existing messages expect(messages.length).toEqual(offset + msgs.length); expect(messages[0].getIsSystemMessage()).toEqual(true); - expect(messages[1].getMessage()).toEqual(user1Msg); - expect(messages[2].getMessage()).toEqual(user2Msg); + expect(messages[1].getMessage()).toEqual(makerMsg); + expect(messages[2].getMessage()).toEqual(takerMsg); for (let i = 0; i < msgs.length; i++) { expect(messages[i + offset].getMessage()).toEqual(msgs[i]); } - chatNotifications = getNotifications(user2Notifications, NotificationMessage.NotificationType.CHAT_MESSAGE); + chatNotifications = getNotifications(takerNotifications, NotificationMessage.NotificationType.CHAT_MESSAGE); offset = 1; // 1 existing notification - expect(chatNotifications.length).toBe(offset + msgs.length); - expect(chatNotifications[0].getChatMessage()?.getMessage()).toEqual(user1Msg); - for (let i = 0; i < msgs.length; i++) { - expect(chatNotifications[i + offset].getChatMessage()?.getMessage()).toEqual(msgs[i]); + if (ctx.concurrentTrades) { + expect(chatNotifications.length).toBeGreaterThanOrEqual(offset + msgs.length); + } else { + expect(chatNotifications.length).toBe(offset + msgs.length); + expect(chatNotifications[0].getChatMessage()?.getMessage()).toEqual(makerMsg); + for (let i = 0; i < msgs.length; i++) { + expect(chatNotifications[i + offset].getChatMessage()?.getMessage()).toEqual(msgs[i]); + } } + + ctx.tradeChatMessagesTested = true; // mark trade chat as tested } // ---------------------------- OTHER HELPERS --------------------------------- From 87e02c9181bd9233caec3e58e23dcd2b76f4ca77 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 18 Jul 2025 07:54:45 -0400 Subject: [PATCH 5/7] test setting trigger price for fixed price offers --- src/HavenoClient.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/HavenoClient.test.ts b/src/HavenoClient.test.ts index 144919de..820afe63 100644 --- a/src/HavenoClient.test.ts +++ b/src/HavenoClient.test.ts @@ -1836,6 +1836,22 @@ test("Cannot post offer outside of trade limits (Test, CI, sanity check)", async if (err.message.indexOf("must be above minimum") < 0) throw err; } + // test setting trigger price for fixed price offer + try { + await executeTrade({ + price: 142.23, + triggerPrice: 150.0, + offerAmount: TestConfig.minAmount, + direction: OfferDirection.SELL, + assetCode: assetCode, + makerPaymentAccountId: account.getId(), + takeOffer: false + }); + throw new Error("Should have rejected posting offer with fixed price and trigger price") + } catch (err: any) { + if (err.message.toLowerCase().indexOf("cannot set trigger price for fixed price offers.") < 0) throw err; + } + // test minimum offer limit let offerId = await executeTrade({ offerAmount: TestConfig.minAmount, From 841853d4850e173c984e7cf44e057075c4d15e9f Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 18 Jul 2025 13:10:13 -0400 Subject: [PATCH 6/7] simulate common currency by preferring index 0 --- src/HavenoClient.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/HavenoClient.test.ts b/src/HavenoClient.test.ts index 820afe63..e4d8ee2d 100644 --- a/src/HavenoClient.test.ts +++ b/src/HavenoClient.test.ts @@ -2547,7 +2547,7 @@ test("Can bootstrap a network", async () => { if (await isFixedPrice(ctxP)) ctxP.price = ctxP.direction === OfferDirection.BUY ? getRandomFloat(125, 155) : getRandomFloat(160, 190); // randomize trade config - if (ctxP.takeOffer === undefined) ctxP.takeOffer = getRandomOutcome(3/5); + if (ctxP.takeOffer === undefined) ctxP.takeOffer = getRandomOutcome(1/2); if (ctxP.tradeAmount === undefined) ctxP.tradeAmount = isRangeOffer ? getRandomBigIntWithinRange(ctxP.offerMinAmount!, ctxP.offerAmount) : ctxP.offerAmount; if (ctxP.buyerSendsPayment === undefined) ctxP.buyerSendsPayment = completeAllTrades || getRandomOutcome(1/2); if (ctxP.priceMargin === undefined && ctxP.price === undefined) ctxP.priceMargin = parseFloat(getRandomFloat(0, .3).toFixed(10)); @@ -4561,8 +4561,8 @@ function getValidFormInputAux(form: PaymentAccountForm, fieldId: PaymentAccountF if (field.getComponent() === PaymentAccountFormField.Component.SELECT_ONE) { if (form.getId() === PaymentAccountForm.FormId.F2F) return "XAU"; if (form.getId() === PaymentAccountForm.FormId.PAY_BY_MAIL) return "XGB"; - let randomIndex = moneroTs.GenUtils.getRandomInt(0, field.getSupportedCurrenciesList().length - 1); - return field.getSupportedCurrenciesList()[randomIndex]!.getCode(); + let currencyIdx = getRandomOutcome(2/3) ? 0 : moneroTs.GenUtils.getRandomInt(0, field.getSupportedCurrenciesList().length - 1); // prefer index 0 to simulate common currency + return field.getSupportedCurrenciesList()[currencyIdx]!.getCode(); } else return field.getSupportedCurrenciesList().map(currency => currency.getCode()).join(','); case PaymentAccountFormField.FieldId.USERNAME: From 4b3cf051637eec07a57a967c01b6c0f2869371cb Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sat, 19 Jul 2025 08:47:52 -0400 Subject: [PATCH 7/7] set trigger price in bootstrap test --- src/HavenoClient.test.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/HavenoClient.test.ts b/src/HavenoClient.test.ts index e4d8ee2d..99299550 100644 --- a/src/HavenoClient.test.ts +++ b/src/HavenoClient.test.ts @@ -2516,12 +2516,12 @@ test("Can bootstrap a network", async () => { async function getRandomBootstrapConfig(ctxP?: Partial): Promise { if (!ctxP) ctxP = {}; - // customize configs + // customize config //ctxP.paymentMethodId = "BLOCK_CHAINS"; //ctxP.assetCode = "BTC"; const completeAllTrades = false; - // randomize offer config + // randomize basic offer config const user1AsMaker = getRandomOutcome(1/2); if (ctxP.maker === undefined) ctxP.maker = {}; if (ctxP.taker === undefined) ctxP.taker = {}; @@ -2544,13 +2544,19 @@ test("Can bootstrap a network", async () => { if (!ctxP.makerPaymentAccountId) ctxP.makerPaymentAccountId = (await createPaymentAccount2(ctxP.maker.havenod!, ctxP.paymentMethodId, ctxP.assetCode)).getId(); if (!ctxP.takerPaymentAccountId) ctxP.takerPaymentAccountId = (await createPaymentAccount2(ctxP.taker.havenod!, ctxP.paymentMethodId, ctxP.assetCode)).getId(); if (!ctxP.assetCode) ctxP.assetCode = getRandomAssetCodeForPaymentAccount(await ctxP.maker.havenod.getPaymentAccount(ctxP.makerPaymentAccountId)); + + // randomize offer price if (await isFixedPrice(ctxP)) ctxP.price = ctxP.direction === OfferDirection.BUY ? getRandomFloat(125, 155) : getRandomFloat(160, 190); + if (ctxP.price === undefined) { + if (ctxP.priceMargin === undefined) ctxP.priceMargin = parseFloat(getRandomFloat(0, .3).toFixed(10)); + const currentPrice = await ctxP.maker.havenod.getPrice(ctxP.assetCode!) + if (getRandomOutcome(1/2)) ctxP.triggerPrice = ctxP.direction === OfferDirection.BUY ? currentPrice! * (1 + getRandomFloat(0, .1)) : currentPrice! * (1 - getRandomFloat(0, .1)); + } // randomize trade config if (ctxP.takeOffer === undefined) ctxP.takeOffer = getRandomOutcome(1/2); if (ctxP.tradeAmount === undefined) ctxP.tradeAmount = isRangeOffer ? getRandomBigIntWithinRange(ctxP.offerMinAmount!, ctxP.offerAmount) : ctxP.offerAmount; if (ctxP.buyerSendsPayment === undefined) ctxP.buyerSendsPayment = completeAllTrades || getRandomOutcome(1/2); - if (ctxP.priceMargin === undefined && ctxP.price === undefined) ctxP.priceMargin = parseFloat(getRandomFloat(0, .3).toFixed(10)); if (ctxP.sellerReceivesPayment === undefined) ctxP.sellerReceivesPayment = completeAllTrades || getRandomOutcome(1/2); if (ctxP.buyerDisputeContext === undefined) ctxP.buyerDisputeContext = getRandomOutcome(1/14) ? DisputeContext.OPEN_AFTER_DEPOSITS_UNLOCK : undefined; if (ctxP.buyerDisputeContext === undefined) ctxP.buyerDisputeContext = getRandomOutcome(1/14) ? DisputeContext.OPEN_AFTER_PAYMENT_SENT : undefined;