mirror of
https://github.com/haveno-dex/haveno-ts.git
synced 2025-08-06 05:24:15 -04:00
convert from centineros to atomic units, test full custom payout
This commit is contained in:
parent
35769fdd6e
commit
882a6dcada
3 changed files with 264 additions and 243 deletions
|
@ -922,6 +922,216 @@ test("Can complete a trade", async () => {
|
||||||
expect(bobFee).toBeGreaterThan(BigInt("0"));
|
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 () => {
|
test("Cannot make or take offer with insufficient unlocked funds", async () => {
|
||||||
let charlie: HavenoDaemon | undefined;
|
let charlie: HavenoDaemon | undefined;
|
||||||
let err: any;
|
let err: any;
|
||||||
|
@ -1137,211 +1347,6 @@ test("Handles unexpected errors during trade initialization", async () => {
|
||||||
if (err) throw err;
|
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 ------------------------------------
|
// ------------------------------- HELPERS ------------------------------------
|
||||||
|
|
||||||
async function initHavenoDaemons(numDaemons: number, config?: any) {
|
async function initHavenoDaemons(numDaemons: number, config?: any) {
|
||||||
|
@ -1722,7 +1727,7 @@ async function postOffer(maker: HavenoDaemon, config?: any) {
|
||||||
config.buyerSecurityDeposit,
|
config.buyerSecurityDeposit,
|
||||||
config.paymentAccountId,
|
config.paymentAccountId,
|
||||||
config.triggerPrice);
|
config.triggerPrice);
|
||||||
testOffer(offer);
|
testOffer(offer, config);
|
||||||
|
|
||||||
// unlocked balance has decreased
|
// unlocked balance has decreased
|
||||||
let unlockedBalanceAfter: bigint = BigInt((await maker.getBalances()).getUnlockedBalance());
|
let unlockedBalanceAfter: bigint = BigInt((await maker.getBalances()).getUnlockedBalance());
|
||||||
|
@ -1753,7 +1758,12 @@ function testCryptoPaymentAccount(paymentAccount: PaymentAccount) {
|
||||||
expect(tradeCurrency.getCode()).toEqual(paymentAccount.getSelectedTradeCurrency()!.getCode());
|
expect(tradeCurrency.getCode()).toEqual(paymentAccount.getSelectedTradeCurrency()!.getCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
function testOffer(offer: OfferInfo) {
|
function testOffer(offer: OfferInfo, config?: any) {
|
||||||
expect(offer.getId().length).toBeGreaterThan(0);
|
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
|
// TODO: test rest of offer
|
||||||
}
|
}
|
|
@ -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.
|
* Get a dispute by trade id.
|
||||||
*
|
*
|
||||||
|
@ -1056,7 +1041,7 @@ class HavenoDaemon {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all disputes.
|
* 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
|
* @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.
|
* Resolve a dispute. By default, the winner receives the trade amount and the security deposits are returned,
|
||||||
* Custom amounts >= 0 will result in the winner receiving the custom amount.
|
* but the arbitrator may award a custom amount to the winner.
|
||||||
*
|
*
|
||||||
* @param {string} tradeId - the id of the trade
|
* @param {string} tradeId - the id of the trade
|
||||||
* @param {DisputeResult.Winner} winner - the winner of the dispute
|
* @param {DisputeResult.Winner} winner - the winner of the dispute
|
||||||
* @param {DisputeResult.Reason} reason - the reason for the dispute
|
* @param {DisputeResult.Reason} reason - the reason for the dispute
|
||||||
* @param {string} summaryNotes - summary of 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<void> {
|
async resolveDispute(tradeId: string, winner: DisputeResult.Winner, reason: DisputeResult.Reason, summaryNotes: string, customWinnerAmount?: bigint): Promise<void> {
|
||||||
let that = this;
|
let that = this;
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
let request = new ResolveDisputeRequest();
|
let request = new ResolveDisputeRequest()
|
||||||
request.setTradeId(tradeId);
|
.setTradeId(tradeId)
|
||||||
request.setWinner(winner);
|
.setWinner(winner)
|
||||||
request.setReason(reason);
|
.setReason(reason)
|
||||||
request.setSummaryNotes(summaryNotes);
|
.setSummaryNotes(summaryNotes)
|
||||||
request.setCustomPayoutAmount(customPayoutAmount.toString());
|
.setCustomPayoutAmount(customWinnerAmount ? customWinnerAmount.toString() : "0");
|
||||||
that._disputesClient.resolveDispute(request, {password: that._password}, function(err: grpcWeb.RpcError) {
|
that._disputesClient.resolveDispute(request, {password: that._password}, function(err: grpcWeb.RpcError) {
|
||||||
if (err) reject(err);
|
if (err) reject(err);
|
||||||
else resolve();
|
else resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a dispute chat message.
|
* Send a dispute chat message.
|
||||||
*
|
*
|
||||||
* @param {string} disputeId - the id of the dispute
|
* @param {string} disputeId - the id of the dispute
|
||||||
* @param {string} message - the message
|
* @param {string} message - the message
|
||||||
|
@ -1121,10 +1106,10 @@ class HavenoDaemon {
|
||||||
async sendDisputeChatMessage(disputeId: string, message: string, attachments: Attachment[]): Promise<void> {
|
async sendDisputeChatMessage(disputeId: string, message: string, attachments: Attachment[]): Promise<void> {
|
||||||
let that = this;
|
let that = this;
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
let request = new SendDisputeChatMessageRequest();
|
let request = new SendDisputeChatMessageRequest()
|
||||||
request.setDisputeId(disputeId);
|
.setDisputeId(disputeId)
|
||||||
request.setMessage(message);
|
.setMessage(message)
|
||||||
request.setAttachmentsList(attachments);
|
.setAttachmentsList(attachments);
|
||||||
that._disputesClient.sendDisputeChatMessage(request, {password: that._password}, function(err: grpcWeb.RpcError) {
|
that._disputesClient.sendDisputeChatMessage(request, {password: that._password}, function(err: grpcWeb.RpcError) {
|
||||||
if (err) reject(err);
|
if (err) reject(err);
|
||||||
else resolve();
|
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 ----------------------------------
|
// ------------------------------- HELPERS ----------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -7,6 +7,7 @@ const console = require('console');
|
||||||
class HavenoUtils {
|
class HavenoUtils {
|
||||||
|
|
||||||
static LOG_LEVEL = 0;
|
static LOG_LEVEL = 0;
|
||||||
|
static CENTINEROS_AU_MULTIPLIER = 10000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log a message.
|
* Log a message.
|
||||||
|
@ -53,6 +54,16 @@ class HavenoUtils {
|
||||||
process.kill(signal ? signal : "SIGINT");
|
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};
|
export {HavenoUtils};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue