diff --git a/src/HavenoDaemon.test.ts b/src/HavenoDaemon.test.ts index 51169ba3..5c292745 100644 --- a/src/HavenoDaemon.test.ts +++ b/src/HavenoDaemon.test.ts @@ -922,6 +922,216 @@ test("Can complete a trade", async () => { expect(bobFee).toBeGreaterThan(BigInt("0")); }); +test("Can resolve disputes", async () => { + + // wait for alice and bob to have unlocked balance for trade + let tradeAmount: bigint = BigInt("250000000000"); + await waitForUnlockedBalance(tradeAmount * BigInt("6"), alice, bob); + + // register to receive notifications + let aliceNotifications: NotificationMessage[] = []; + let bobNotifications: NotificationMessage[] = []; + let arbitratorNotifications: NotificationMessage[] = []; + await alice.addNotificationListener(notification => { aliceNotifications.push(notification); }); + await bob.addNotificationListener(notification => { bobNotifications.push(notification); }); + await arbitrator.addNotificationListener(notification => { arbitratorNotifications.push(notification); }); + + // alice posts offers to buy xmr + let numOffers = 4; + HavenoUtils.log(1, "Alice posting offers"); + let direction = "buy"; + let offers = []; + for (let i = 0; i < numOffers; i++) offers.push(postOffer(alice, {direction: direction, amount: tradeAmount})); + offers = await Promise.all(offers); + HavenoUtils.log(1, "Alice done posting offers"); + + // wait for offers to post + await wait(TestConfig.walletSyncPeriodMs * 2); + + // bob takes offers + let paymentAccount = await createPaymentAccount(bob, "eth"); + HavenoUtils.log(1, "Bob taking offers"); + let startTime = Date.now(); + let trades = []; + for (let i = 0; i < numOffers; i++) trades.push(await bob.takeOffer(offers[i].getId(), paymentAccount.getId())); + //trades = await Promise.all(trades); // TODO: take trades in parallel when they take less time + HavenoUtils.log(1, "Bob done taking offers in " + (Date.now() - startTime) + " ms") + + // test trades + let depositTxIds: string[] = []; + for (let trade of trades) { + expect(trade.getPhase()).toEqual("DEPOSIT_PUBLISHED"); + let fetchedTrade: TradeInfo = await bob.getTrade(trade.getTradeId()); + expect(fetchedTrade.getPhase()).toEqual("DEPOSIT_PUBLISHED"); + depositTxIds.push(fetchedTrade.getMakerDepositTxId()); + depositTxIds.push(fetchedTrade.getTakerDepositTxId()); + } + + // mine until deposit txs unlock + HavenoUtils.log(1, "Mining to unlock deposit txs"); + await waitForUnlockedTxs(...depositTxIds); + HavenoUtils.log(1, "Done mining to unlock deposit txs"); + + // open disputes + HavenoUtils.log(1, "Opening disputes"); + await bob.openDispute(trades[0].getTradeId()); + await alice.openDispute(trades[1].getTradeId()); + await bob.openDispute(trades[2].getTradeId()); + await alice.openDispute(trades[3].getTradeId()); + + // test dispute + let bobDispute = await bob.getDispute(trades[0].getTradeId()); + expect(bobDispute.getTradeId()).toEqual(trades[0].getTradeId()); + expect(bobDispute.getIsOpener()).toBe(true); + expect(bobDispute.getDisputeOpenerIsBuyer()).toBe(false); + + // get non-existing dispute should fail + try { + await bob.getDispute("invalid"); + throw new Error("get dispute with invalid id should fail"); + } catch (err) { + assert.equal(err.message, "dispute for trade id 'invalid' not found"); + } + + // alice sees the dispute + await wait(TestConfig.maxTimePeerNoticeMs * 2); + let aliceDispute = await alice.getDispute(trades[0].getTradeId()); + expect(aliceDispute.getTradeId()).toEqual(trades[0].getTradeId()); + expect(aliceDispute.getIsOpener()).toBe(false); + + // arbitrator sees both disputes + let disputes = await arbitrator.getDisputes(); + expect(disputes.length).toBeGreaterThanOrEqual(2); + let arbAliceDispute = disputes.find(d => d.getId() === aliceDispute.getId()); + assert(arbAliceDispute); + let arbBobDispute = disputes.find(d => d.getId() === bobDispute.getId()); + assert(arbBobDispute); + + // arbitrator sends chat messages to alice and bob + HavenoUtils.log(1, "Testing chat messages"); + await arbitrator.sendDisputeChatMessage(arbBobDispute!.getId(), "Arbitrator chat message to Bob", []); + await arbitrator.sendDisputeChatMessage(arbAliceDispute!.getId(), "Arbitrator chat message to Alice", []); + + // alice and bob reply to arbitrator chat messages + let attachment = new Attachment(); + let bytes = new Uint8Array(Buffer.from("Proof Bob was scammed", "utf8")); + attachment.setBytes(bytes); + attachment.setFileName("proof.txt"); + let attachment2 = new Attachment(); + let bytes2 = new Uint8Array(Buffer.from("picture bytes", "utf8")); + attachment2.setBytes(bytes2); + attachment2.setFileName("proof.png"); + await bob.sendDisputeChatMessage(bobDispute.getId(), "Bob chat message", [attachment, attachment2]); + await alice.sendDisputeChatMessage(aliceDispute.getId(), "Alice chat message", []); + await wait(TestConfig.maxTimePeerNoticeMs); + + // test alice and bob's chat messages + let updatedDispute = await bob.getDispute(trades[0].getTradeId()); + let messages = updatedDispute.getChatMessageList(); + expect(messages.length).toEqual(3); // 1st message is the system message + expect(messages[1].getMessage()).toEqual("Arbitrator chat message to Bob"); + expect(messages[2].getMessage()).toEqual("Bob chat message"); + let attachments = messages[2].getAttachmentsList(); + expect(attachments.length).toEqual(2); + expect(attachments[0].getFileName()).toEqual("proof.txt"); + expect(attachments[0].getBytes()).toEqual(bytes); + expect(attachments[1].getFileName()).toEqual("proof.png"); + expect(attachments[1].getBytes()).toEqual(bytes2); + updatedDispute = await alice.getDispute(trades[0].getTradeId()); + messages = updatedDispute.getChatMessageList(); + expect(messages.length).toEqual(3); + expect(messages[1].getMessage()).toEqual("Arbitrator chat message to Alice"); + expect(messages[2].getMessage()).toEqual("Alice chat message"); + + // test notifications of chat messages + let chatNotifications = getNotifications(aliceNotifications, NotificationMessage.NotificationType.CHAT_MESSAGE); + expect(chatNotifications.length).toBe(1); + expect(chatNotifications[0].getChatMessage()?.getMessage()).toEqual("Arbitrator chat message to Alice"); + chatNotifications = getNotifications(bobNotifications, NotificationMessage.NotificationType.CHAT_MESSAGE); + expect(chatNotifications.length).toBe(1); + expect(chatNotifications[0].getChatMessage()?.getMessage()).toEqual("Arbitrator chat message to Bob"); + + // arbitrator has 2 chat messages, one with attachments + chatNotifications = getNotifications(arbitratorNotifications, NotificationMessage.NotificationType.CHAT_MESSAGE); + expect(chatNotifications.length).toBe(2); + expect(chatNotifications[0].getChatMessage()?.getMessage()).toEqual("Bob chat message"); + assert(chatNotifications[0].getChatMessage()?.getAttachmentsList()); + attachments = chatNotifications[0].getChatMessage()?.getAttachmentsList()!; + expect(attachments[0].getFileName()).toEqual("proof.txt"); + expect(attachments[0].getBytes()).toEqual(bytes); + expect(attachments[1].getFileName()).toEqual("proof.png"); + expect(attachments[1].getBytes()).toEqual(bytes2); + expect(chatNotifications[1].getChatMessage()?.getMessage()).toEqual("Alice chat message"); + + // award trade amount to seller + HavenoUtils.log(1, "Awarding trade amount to seller"); + let bobBalancesBefore = await bob.getBalances(); + let aliceBalancesBefore = await alice.getBalances(); + await arbitrator.resolveDispute(trades[0].getTradeId(), DisputeResult.Winner.SELLER, DisputeResult.Reason.PEER_WAS_LATE, "Seller is winner"); + + // dispute is resolved + await wait(TestConfig.maxTimePeerNoticeMs); + updatedDispute = await alice.getDispute(trades[0].getTradeId()); + expect(updatedDispute.getIsClosed()).toBe(true); + updatedDispute = await bob.getDispute(trades[0].getTradeId()); + expect(updatedDispute.getIsClosed()).toBe(true); + + // check balances after payout tx + await wait(TestConfig.walletSyncPeriodMs * 2); + let aliceBalancesAfter = await alice.getBalances(); + let bobBalancesAfter = await bob.getBalances(); + let aliceDifference = BigInt(aliceBalancesAfter.getBalance()) - BigInt(aliceBalancesBefore.getBalance()); + let bobDifference = BigInt(bobBalancesAfter.getBalance()) - BigInt(bobBalancesBefore.getBalance()); + let winnerPayout = tradeAmount + HavenoUtils.centinerosToAtomicUnits(offers[0].getSellerSecurityDeposit()); + let loserPayout = HavenoUtils.centinerosToAtomicUnits(offers[0].getBuyerSecurityDeposit()); + expect(loserPayout - aliceDifference).toBeLessThan(TestConfig.maxFee); + expect(bobDifference).toEqual(winnerPayout); + + // award trade amount to buyer + HavenoUtils.log(1, "Awarding trade amount to buyer"); + aliceBalancesBefore = await alice.getBalances(); + bobBalancesBefore = await bob.getBalances(); + await arbitrator.resolveDispute(trades[1].getTradeId(), DisputeResult.Winner.BUYER, DisputeResult.Reason.SELLER_NOT_RESPONDING, "Buyer is winner"); + await wait(TestConfig.walletSyncPeriodMs * 2); + aliceBalancesAfter = await alice.getBalances(); + bobBalancesAfter = await bob.getBalances(); + aliceDifference = BigInt(aliceBalancesAfter.getBalance()) - BigInt(aliceBalancesBefore.getBalance()); + bobDifference = BigInt(bobBalancesAfter.getBalance()) - BigInt(bobBalancesBefore.getBalance()); + winnerPayout = tradeAmount + HavenoUtils.centinerosToAtomicUnits(offers[1].getBuyerSecurityDeposit()); + loserPayout = HavenoUtils.centinerosToAtomicUnits(offers[1].getSellerSecurityDeposit()); + expect(aliceDifference).toEqual(winnerPayout); + expect(loserPayout - bobDifference).toBeLessThan(TestConfig.maxFee); + + // award half of trade amount to buyer + HavenoUtils.log(1, "Awarding half of trade amount to buyer"); + let customWinnerAmount = tradeAmount / BigInt(2) + HavenoUtils.centinerosToAtomicUnits(offers[2].getBuyerSecurityDeposit()); + aliceBalancesBefore = await alice.getBalances(); + bobBalancesBefore = await bob.getBalances(); + await arbitrator.resolveDispute(trades[2].getTradeId(), DisputeResult.Winner.BUYER, DisputeResult.Reason.WRONG_SENDER_ACCOUNT, "Split trade amount", customWinnerAmount); + await wait(TestConfig.walletSyncPeriodMs * 2); + aliceBalancesAfter = await alice.getBalances(); + bobBalancesAfter = await bob.getBalances(); + aliceDifference = BigInt(aliceBalancesAfter.getBalance()) - BigInt(aliceBalancesBefore.getBalance()); + bobDifference = BigInt(bobBalancesAfter.getBalance()) - BigInt(bobBalancesBefore.getBalance()); + loserPayout = tradeAmount + HavenoUtils.centinerosToAtomicUnits(offers[2].getBuyerSecurityDeposit()) + HavenoUtils.centinerosToAtomicUnits(offers[2].getSellerSecurityDeposit()) - customWinnerAmount; + expect(aliceDifference).toEqual(customWinnerAmount); + expect(loserPayout - bobDifference).toBeLessThan(TestConfig.maxFee); + + // award full amount to seller + HavenoUtils.log(1, "Awarding full amount to seller"); + customWinnerAmount = tradeAmount + HavenoUtils.centinerosToAtomicUnits(offers[3].getBuyerSecurityDeposit()) + HavenoUtils.centinerosToAtomicUnits(offers[3].getSellerSecurityDeposit()); + aliceBalancesBefore = await alice.getBalances(); + bobBalancesBefore = await bob.getBalances(); + await arbitrator.resolveDispute(trades[3].getTradeId(), DisputeResult.Winner.SELLER, DisputeResult.Reason.TRADE_ALREADY_SETTLED, "Seller gets everything", customWinnerAmount); + await wait(TestConfig.walletSyncPeriodMs * 2); + aliceBalancesAfter = await alice.getBalances(); + bobBalancesAfter = await bob.getBalances(); + aliceDifference = BigInt(aliceBalancesAfter.getBalance()) - BigInt(aliceBalancesBefore.getBalance()); + bobDifference = BigInt(bobBalancesAfter.getBalance()) - BigInt(bobBalancesBefore.getBalance()); + expect(aliceDifference).toEqual(BigInt(0)); + expect(customWinnerAmount - bobDifference).toBeLessThan(TestConfig.maxFee); +}); + test("Cannot make or take offer with insufficient unlocked funds", async () => { let charlie: HavenoDaemon | undefined; let err: any; @@ -1137,211 +1347,6 @@ test("Handles unexpected errors during trade initialization", async () => { if (err) throw err; }); -test("Can resolve disputes", async () => { - - // wait for alice and bob to have unlocked balance for trade - let tradeAmount: bigint = BigInt("250000000000"); - await waitForUnlockedBalance(tradeAmount * BigInt("6"), alice, bob); - - // register to receive notifications - let aliceNotifications: NotificationMessage[] = []; - let bobNotifications: NotificationMessage[] = []; - let arbitratorNotifications: NotificationMessage[] = []; - await alice.addNotificationListener(notification => { aliceNotifications.push(notification); }); - await bob.addNotificationListener(notification => { bobNotifications.push(notification); }); - await arbitrator.addNotificationListener(notification => { arbitratorNotifications.push(notification); }); - - // alice posts offers to buy xmr - HavenoUtils.log(1, "Alice posting offers"); - let direction = "buy"; - let offer1: OfferInfo = await postOffer(alice, {direction: direction, amount: tradeAmount}); - let offer2: OfferInfo = await postOffer(alice, {direction: direction, amount: tradeAmount}); - let offer3: OfferInfo = await postOffer(alice, {direction: direction, amount: tradeAmount}); - HavenoUtils.log(1, "Alice done posting offers"); - - // takes awhile to sync deposit - await wait(TestConfig.walletSyncPeriodMs * 3); - - let paymentAccount = await createPaymentAccount(bob, "eth"); - - // bob takes offer - let startTime = Date.now(); - HavenoUtils.log(1, "Bob taking offers"); - let trade1: TradeInfo = await bob.takeOffer(offer1.getId(), paymentAccount.getId()); - expect(trade1.getPhase()).toEqual("DEPOSIT_PUBLISHED"); - HavenoUtils.log(1, "Bob done taking offer1 in " + (Date.now() - startTime) + " ms"); - let fetchedTrade1: TradeInfo = await bob.getTrade(trade1.getTradeId()); - expect(fetchedTrade1.getPhase()).toEqual("DEPOSIT_PUBLISHED"); - - let trade2: TradeInfo = await bob.takeOffer(offer2.getId(), paymentAccount.getId()); - expect(trade2.getPhase()).toEqual("DEPOSIT_PUBLISHED"); - HavenoUtils.log(1, "Bob done taking offer2 in " + (Date.now() - startTime) + " ms"); - let fetchedTrade2: TradeInfo = await bob.getTrade(trade2.getTradeId()); - expect(fetchedTrade2.getPhase()).toEqual("DEPOSIT_PUBLISHED"); - - let trade3: TradeInfo = await bob.takeOffer(offer3.getId(), paymentAccount.getId()); - expect(trade3.getPhase()).toEqual("DEPOSIT_PUBLISHED"); - HavenoUtils.log(1, "Bob done taking offer3 in " + (Date.now() - startTime) + " ms"); - let fetchedTrade3: TradeInfo = await bob.getTrade(trade3.getTradeId()); - expect(fetchedTrade3.getPhase()).toEqual("DEPOSIT_PUBLISHED"); - - // mine until deposit txs unlock - HavenoUtils.log(1, "Mining to unlock deposit txs"); - await waitForUnlockedTxs( - fetchedTrade1.getMakerDepositTxId(), fetchedTrade1.getTakerDepositTxId(), - fetchedTrade2.getMakerDepositTxId(), fetchedTrade2.getTakerDepositTxId(), - fetchedTrade3.getMakerDepositTxId(), fetchedTrade3.getTakerDepositTxId(), - ); - HavenoUtils.log(1, "Done mining to unlock deposit txs"); - - // bob does not recieve payment, open a dispute - HavenoUtils.log(1, "Opening disputes"); - await bob.openDispute(trade1.getTradeId()); - await alice.openDispute(trade2.getTradeId()); - await bob.openDispute(trade3.getTradeId()); - - let bobDispute = await bob.getDispute(trade1.getTradeId()); - expect(bobDispute.getTradeId()).toEqual(trade1.getTradeId()); - expect(bobDispute.getIsOpener()).toBe(true); - expect(bobDispute.getDisputeOpenerIsBuyer()).toBe(false); - - // get non-existing dispute should fail - try { - await bob.getDispute("invalid"); - throw new Error("get dispute with invalid id should fail"); - } catch (err) { - assert.equal(err.message, "dispute for trade id 'invalid' not found"); - } - - // alice should see the dispute - await wait(TestConfig.maxTimePeerNoticeMs*2); // wait 2x since the dispute propagates from bob->arbitrator->alice - let aliceDispute = await alice.getDispute(trade1.getTradeId()); - expect(aliceDispute.getTradeId()).toEqual(trade1.getTradeId()); - expect(aliceDispute.getIsOpener()).toBe(false); - - // arbitrator should see both disputes - let disputes = await arbitrator.getDisputes(); - expect(disputes.length).toBeGreaterThanOrEqual(2); - let arbAliceDispute = disputes.find(d => d.getId() === aliceDispute.getId()); - assert(arbAliceDispute); - let arbBobDispute = disputes.find(d => d.getId() === bobDispute.getId()); - assert(arbBobDispute); - - // send a message - HavenoUtils.log(1, "Arbitrator sending chat messages"); - await arbitrator.sendDisputeChatMessage(arbBobDispute!.getId(), "Arbitrator chat message to Bob", []); - await arbitrator.sendDisputeChatMessage(arbAliceDispute!.getId(), "Arbitrator chat message to Alice", []); - - HavenoUtils.log(1, "Alice and bob replying to chat messages"); - - let attachment = new Attachment(); - let bytes = new Uint8Array(Buffer.from("Proof Bob was scammed", "utf8")); - attachment.setBytes(bytes); - attachment.setFileName("proof.txt"); - let attachment2 = new Attachment(); - let bytes2 = new Uint8Array(Buffer.from("picture bytes", "utf8")); - attachment2.setBytes(bytes2); - attachment2.setFileName("proof.png"); - await bob.sendDisputeChatMessage(bobDispute.getId(), "Bob chat message", [attachment, attachment2]); - await wait(TestConfig.maxTimePeerNoticeMs); - await alice.sendDisputeChatMessage(aliceDispute.getId(), "Alice chat message", []); - await wait(TestConfig.maxTimePeerNoticeMs); - - // check messages on alice and bob - HavenoUtils.log(1, "Confirming chat messages"); - let updatedDispute = await bob.getDispute(trade1.getTradeId()); - let messages = updatedDispute.getChatMessageList(); - expect(messages.length).toEqual(3); // 1st message is the system message - expect(messages[1].getMessage()).toEqual("Arbitrator chat message to Bob"); - expect(messages[2].getMessage()).toEqual("Bob chat message"); - let attachments = messages[2].getAttachmentsList(); - expect(attachments.length).toEqual(2); - expect(attachments[0].getFileName()).toEqual("proof.txt"); - expect(attachments[0].getBytes()).toEqual(bytes); - expect(attachments[1].getFileName()).toEqual("proof.png"); - expect(attachments[1].getBytes()).toEqual(bytes2); - updatedDispute = await alice.getDispute(trade1.getTradeId()); - messages = updatedDispute.getChatMessageList(); - expect(messages.length).toEqual(3); - expect(messages[1].getMessage()).toEqual("Arbitrator chat message to Alice"); - expect(messages[2].getMessage()).toEqual("Alice chat message"); - - HavenoUtils.log(1, "Confirming chat messages via notifications"); - let chatNotifications = getNotifications(aliceNotifications, NotificationMessage.NotificationType.CHAT_MESSAGE); - expect(chatNotifications.length).toBe(1); - expect(chatNotifications[0].getChatMessage()?.getMessage()).toEqual("Arbitrator chat message to Alice"); - chatNotifications = getNotifications(bobNotifications, NotificationMessage.NotificationType.CHAT_MESSAGE); - expect(chatNotifications.length).toBe(1); - expect(chatNotifications[0].getChatMessage()?.getMessage()).toEqual("Arbitrator chat message to Bob"); - - // arbitrator expects 2 chat messages, one with attachments - chatNotifications = getNotifications(arbitratorNotifications, NotificationMessage.NotificationType.CHAT_MESSAGE); - expect(chatNotifications.length).toBe(2); - expect(chatNotifications[0].getChatMessage()?.getMessage()).toEqual("Bob chat message"); - assert(chatNotifications[0].getChatMessage()?.getAttachmentsList()); - attachments = chatNotifications[0].getChatMessage()?.getAttachmentsList()!; - expect(attachments[0].getFileName()).toEqual("proof.txt"); - expect(attachments[0].getBytes()).toEqual(bytes); - expect(attachments[1].getFileName()).toEqual("proof.png"); - expect(attachments[1].getBytes()).toEqual(bytes2); - expect(chatNotifications[1].getChatMessage()?.getMessage()).toEqual("Alice chat message"); - - // arbitrator resolves dispute, seller winner scenario - HavenoUtils.log(1, "Resolving dispute in favor of seller"); - let bobBalancesBefore = await bob.getBalances(); - let aliceBalancesBefore = await alice.getBalances(); - await arbitrator.resolveDispute(trade1.getTradeId(), DisputeResult.Winner.SELLER, DisputeResult.Reason.PEER_WAS_LATE, "Seller is winner", BigInt(0)); - - // Dispute is properly resolved - await wait(TestConfig.maxTimePeerNoticeMs); - updatedDispute = await alice.getDispute(trade1.getTradeId()); - expect(updatedDispute.getIsClosed()).toBe(true); - updatedDispute = await bob.getDispute(trade1.getTradeId()); - expect(updatedDispute.getIsClosed()).toBe(true); - - // check balances after payout tx - await wait(TestConfig.walletSyncPeriodMs*2); - let aliceBalancesAfter = await alice.getBalances(); - let bobBalancesAfter = await bob.getBalances(); - let aliceDifference = BigInt(aliceBalancesAfter.getBalance()) - BigInt(aliceBalancesBefore.getBalance()); - let bobDifference = BigInt(bobBalancesAfter.getBalance()) - BigInt(bobBalancesBefore.getBalance()); - let winnerPayout = tradeAmount + BigInt(offer1.getSellerSecurityDeposit()) - TestConfig.maxFee; - let loserPayout = BigInt(offer1.getBuyerSecurityDeposit()) - TestConfig.maxFee; - expect(aliceDifference).toBeGreaterThan(loserPayout); - expect(bobDifference).toBeGreaterThan(winnerPayout); - - // buyer winner scenario - HavenoUtils.log(1, "Resolved dispute in favor of buyer"); - aliceBalancesBefore = await alice.getBalances(); - bobBalancesBefore = await bob.getBalances(); - await arbitrator.resolveDispute(trade2.getTradeId(), DisputeResult.Winner.BUYER, DisputeResult.Reason.SELLER_NOT_RESPONDING, "Buyer is winner", BigInt(0)); - await wait(TestConfig.walletSyncPeriodMs*2); - aliceBalancesAfter = await alice.getBalances(); - bobBalancesAfter = await bob.getBalances(); - aliceDifference = BigInt(aliceBalancesAfter.getBalance()) - BigInt(aliceBalancesBefore.getBalance()); - bobDifference = BigInt(bobBalancesAfter.getBalance()) - BigInt(bobBalancesBefore.getBalance()); - winnerPayout = tradeAmount + BigInt(offer2.getBuyerSecurityDeposit()) - TestConfig.maxFee; - loserPayout = BigInt(offer2.getSellerSecurityDeposit()) - TestConfig.maxFee; - expect(aliceDifference).toBeGreaterThan(winnerPayout); - expect(bobDifference).toBeGreaterThan(loserPayout); - - // custom payout scenario - HavenoUtils.log(1, "Resolve with split payment in custom payout"); - aliceBalancesBefore = await alice.getBalances(); - bobBalancesBefore = await bob.getBalances(); - let splitAmount = tradeAmount / BigInt(2); - await arbitrator.resolveDispute(trade3.getTradeId(), DisputeResult.Winner.BUYER, DisputeResult.Reason.WRONG_SENDER_ACCOUNT, "Equal payout", splitAmount); - await wait(TestConfig.walletSyncPeriodMs*2); - aliceBalancesAfter = await alice.getBalances(); - bobBalancesAfter = await bob.getBalances(); - aliceDifference = BigInt(aliceBalancesAfter.getBalance()) - BigInt(aliceBalancesBefore.getBalance()); - bobDifference = BigInt(bobBalancesAfter.getBalance()) - BigInt(bobBalancesBefore.getBalance()); - winnerPayout = splitAmount + BigInt(offer3.getBuyerSecurityDeposit()) - TestConfig.maxFee; - loserPayout = splitAmount + BigInt(offer3.getSellerSecurityDeposit()) - TestConfig.maxFee; - expect(aliceDifference).toBeGreaterThan(winnerPayout); - expect(bobDifference).toBeGreaterThan(loserPayout); -}); - // ------------------------------- HELPERS ------------------------------------ async function initHavenoDaemons(numDaemons: number, config?: any) { @@ -1722,7 +1727,7 @@ async function postOffer(maker: HavenoDaemon, config?: any) { config.buyerSecurityDeposit, config.paymentAccountId, config.triggerPrice); - testOffer(offer); + testOffer(offer, config); // unlocked balance has decreased let unlockedBalanceAfter: bigint = BigInt((await maker.getBalances()).getUnlockedBalance()); @@ -1753,7 +1758,12 @@ function testCryptoPaymentAccount(paymentAccount: PaymentAccount) { expect(tradeCurrency.getCode()).toEqual(paymentAccount.getSelectedTradeCurrency()!.getCode()); } -function testOffer(offer: OfferInfo) { +function testOffer(offer: OfferInfo, config?: any) { expect(offer.getId().length).toBeGreaterThan(0); + if (config) { + expect(HavenoUtils.centinerosToAtomicUnits(offer.getAmount())).toEqual(config.amount); // TODO (woodser): use atomic units in offer instead of centineros? + expect(offer.getBuyerSecurityDeposit() / offer.getAmount()).toEqual(config.buyerSecurityDeposit); + expect(offer.getSellerSecurityDeposit() / offer.getAmount()).toEqual(config.buyerSecurityDeposit); // TODO: use same config.securityDeposit for buyer and seller? + } // TODO: test rest of offer -} +} \ No newline at end of file diff --git a/src/HavenoDaemon.ts b/src/HavenoDaemon.ts index 8b7fea46..22a527c9 100644 --- a/src/HavenoDaemon.ts +++ b/src/HavenoDaemon.ts @@ -1027,21 +1027,6 @@ class HavenoDaemon { }); } - /** - * Shutdown the Haveno daemon server and stop the process if applicable. - */ - async shutdownServer() { - if (this._keepAliveLooper) this._keepAliveLooper.stop(); - let that = this; - await new Promise(function(resolve, reject) { - that._shutdownServerClient.stop(new StopRequest(), {password: that._password}, function(err: grpcWeb.RpcError) { // process receives 'exit' event - if (err) reject(err); - else resolve(); - }); - }); - if (this._process) return HavenoUtils.kill(this._process); - } - /** * Get a dispute by trade id. * @@ -1056,7 +1041,7 @@ class HavenoDaemon { }); }); } - + /** * Get all disputes. */ @@ -1069,9 +1054,9 @@ class HavenoDaemon { }); }); } - + /** - * Opens a dispute for a trade. + * Open a dispute for a trade. * * @param {string} tradeId - the id of the trade */ @@ -1084,35 +1069,35 @@ class HavenoDaemon { }); }); } - + /** - * Resolves a dispute. The winner receives the trade amount and security deposites are returned. - * Custom amounts >= 0 will result in the winner receiving the custom amount. + * Resolve a dispute. By default, the winner receives the trade amount and the security deposits are returned, + * but the arbitrator may award a custom amount to the winner. * * @param {string} tradeId - the id of the trade * @param {DisputeResult.Winner} winner - the winner of the dispute * @param {DisputeResult.Reason} reason - the reason for the dispute * @param {string} summaryNotes - summary of the dispute - * @param {bigint} customPayoutAmount - optional custom amount winner receives + * @param {bigint} customWinnerAmount - custom amount to award the winner (optional) */ - async resolveDispute(tradeId: string, winner: DisputeResult.Winner, reason: DisputeResult.Reason, summaryNotes: string, customPayoutAmount: bigint): Promise { + async resolveDispute(tradeId: string, winner: DisputeResult.Winner, reason: DisputeResult.Reason, summaryNotes: string, customWinnerAmount?: bigint): Promise { let that = this; return new Promise(function(resolve, reject) { - let request = new ResolveDisputeRequest(); - request.setTradeId(tradeId); - request.setWinner(winner); - request.setReason(reason); - request.setSummaryNotes(summaryNotes); - request.setCustomPayoutAmount(customPayoutAmount.toString()); + let request = new ResolveDisputeRequest() + .setTradeId(tradeId) + .setWinner(winner) + .setReason(reason) + .setSummaryNotes(summaryNotes) + .setCustomPayoutAmount(customWinnerAmount ? customWinnerAmount.toString() : "0"); that._disputesClient.resolveDispute(request, {password: that._password}, function(err: grpcWeb.RpcError) { if (err) reject(err); else resolve(); }); }); } - + /** - * Sends a dispute chat message. + * Send a dispute chat message. * * @param {string} disputeId - the id of the dispute * @param {string} message - the message @@ -1121,10 +1106,10 @@ class HavenoDaemon { async sendDisputeChatMessage(disputeId: string, message: string, attachments: Attachment[]): Promise { let that = this; return new Promise(function(resolve, reject) { - let request = new SendDisputeChatMessageRequest(); - request.setDisputeId(disputeId); - request.setMessage(message); - request.setAttachmentsList(attachments); + let request = new SendDisputeChatMessageRequest() + .setDisputeId(disputeId) + .setMessage(message) + .setAttachmentsList(attachments); that._disputesClient.sendDisputeChatMessage(request, {password: that._password}, function(err: grpcWeb.RpcError) { if (err) reject(err); else resolve(); @@ -1132,6 +1117,21 @@ class HavenoDaemon { }); } + /** + * Shutdown the Haveno daemon server and stop the process if applicable. + */ + async shutdownServer() { + if (this._keepAliveLooper) this._keepAliveLooper.stop(); + let that = this; + await new Promise(function(resolve, reject) { + that._shutdownServerClient.stop(new StopRequest(), {password: that._password}, function(err: grpcWeb.RpcError) { // process receives 'exit' event + if (err) reject(err); + else resolve(); + }); + }); + if (this._process) return HavenoUtils.kill(this._process); + } + // ------------------------------- HELPERS ---------------------------------- /** diff --git a/src/utils/HavenoUtils.ts b/src/utils/HavenoUtils.ts index c87d1d66..6b6d45dd 100644 --- a/src/utils/HavenoUtils.ts +++ b/src/utils/HavenoUtils.ts @@ -7,6 +7,7 @@ const console = require('console'); class HavenoUtils { static LOG_LEVEL = 0; + static CENTINEROS_AU_MULTIPLIER = 10000; /** * Log a message. @@ -53,6 +54,16 @@ class HavenoUtils { process.kill(signal ? signal : "SIGINT"); }); } + + /** + * Convert centineros to atomic units. + * + * @param {number} centineros - denominates an amount of XMR in centineros + * @return {BigInt} the amount denominated in atomic units + */ + static centinerosToAtomicUnits(centineros: number): bigint { + return BigInt(centineros) * BigInt(HavenoUtils.CENTINEROS_AU_MULTIPLIER); + } } export {HavenoUtils};