mirror of
https://github.com/haveno-dex/haveno-ts.git
synced 2025-01-12 07:49:52 -05:00
test posting offers, taking trades, and opening disputes in parallel (#78)
tests can run repeatedly since backend closes wallets when unused support funding a wallet with X outputs of amount support checking if wallet funded with X outputs of amount
This commit is contained in:
parent
ad26aae4e6
commit
91710b2bcf
@ -13,8 +13,10 @@ import OnlineStatus = UrlConnection.OnlineStatus;
|
||||
// import monero-javascript
|
||||
const monerojs = require("monero-javascript"); // TODO (woodser): support typescript and `npm install @types/monero-javascript` in monero-javascript
|
||||
const GenUtils = monerojs.GenUtils;
|
||||
const BigInteger = monerojs.BigInteger;
|
||||
const MoneroNetworkType = monerojs.MoneroNetworkType;
|
||||
const MoneroTxConfig = monerojs.MoneroTxConfig;
|
||||
const MoneroDestination = monerojs.MoneroDestination;
|
||||
const MoneroUtils = monerojs.MoneroUtils;
|
||||
const TaskLooper = monerojs.TaskLooper;
|
||||
|
||||
@ -80,11 +82,13 @@ const TestConfig = {
|
||||
url: "http://localhost:8081",
|
||||
accountPasswordRequired: false,
|
||||
accountPassword: "abctesting789",
|
||||
walletUrl: "http://127.0.0.1:38092",
|
||||
}
|
||||
],
|
||||
maxFee: BigInt("75000000000"),
|
||||
walletSyncPeriodMs: 5000, // TODO (woodser): auto adjust higher if using remote connection
|
||||
daemonPollPeriodMs: 15000,
|
||||
maxWalletStartupMs: 10000, // TODO (woodser): make shorter by switching to jni
|
||||
maxTimePeerNoticeMs: 3000,
|
||||
assetCodes: ["USD", "GBP", "EUR", "ETH", "BTC", "BCH", "LTC", "ZEC"], // primary asset codes
|
||||
cryptoAddresses: [{
|
||||
@ -95,7 +99,7 @@ const TestConfig = {
|
||||
address: "bcrt1q6j90vywv8x7eyevcnn2tn2wrlg3vsjlsvt46qz"
|
||||
}, {
|
||||
currencyCode: "BCH",
|
||||
address: "1JRjBNKi4ZgJpKPeoL4149Q7ZZD3VvVgk9" // TODO: support Cash Address format also
|
||||
address: "1JRjBNKi4ZgJpKPeoL4149Q7ZZD3VvVgk9" // TODO: support CashAddr format only
|
||||
}, {
|
||||
currencyCode: "LTC",
|
||||
address: "LXUTUN5mTPc2LsS7cEjkyjTRcfYyJGoUuQ"
|
||||
@ -115,8 +119,9 @@ const TestConfig = {
|
||||
["8086", ["10005", "7781"]],
|
||||
]),
|
||||
devPrivilegePrivKey: "6ac43ea1df2a290c1c8391736aa42e4339c5cb4f110ff0257a13b63211977b7a", // from DEV_PRIVILEGE_PRIV_KEY
|
||||
tradeInitTimeout: 60000,
|
||||
timeout: 900000, // timeout in ms for all tests to complete (15 minutes)
|
||||
postOffer: {
|
||||
postOffer: { // TODO (woodser): use typed config
|
||||
direction: "buy", // buy or sell xmr
|
||||
amount: BigInt("200000000000"), // amount of xmr to trade
|
||||
assetCode: "eth", // counter asset to trade
|
||||
@ -142,6 +147,7 @@ let bob: HavenoDaemon;
|
||||
let monerod: any;
|
||||
let fundingWallet: any;
|
||||
let aliceWallet: any;
|
||||
let bobWallet: any;
|
||||
|
||||
// track started haveno processes
|
||||
const HAVENO_PROCESSES: HavenoDaemon[] = [];
|
||||
@ -179,13 +185,14 @@ beforeAll(async () => {
|
||||
// connect monero clients
|
||||
monerod = await monerojs.connectToDaemonRpc(TestConfig.monerod.url, TestConfig.monerod.username, TestConfig.monerod.password);
|
||||
aliceWallet = await monerojs.connectToWalletRpc(TestConfig.startupHavenods[1].walletUrl, TestConfig.defaultHavenod.walletUsername, TestConfig.startupHavenods[1].accountPasswordRequired ? TestConfig.startupHavenods[1].accountPassword : TestConfig.defaultHavenod.walletDefaultPassword);
|
||||
bobWallet = await monerojs.connectToWalletRpc(TestConfig.startupHavenods[2].walletUrl, TestConfig.defaultHavenod.walletUsername, TestConfig.startupHavenods[2].accountPasswordRequired ? TestConfig.startupHavenods[2].accountPassword : TestConfig.defaultHavenod.walletDefaultPassword);
|
||||
|
||||
// initialize funding wallet
|
||||
await initFundingWallet();
|
||||
});
|
||||
|
||||
beforeEach(async() => {
|
||||
console.log("Before test \"" + expect.getState().currentTestName + "\"");
|
||||
HavenoUtils.log(0, "BEFORE TEST \"" + expect.getState().currentTestName + "\"");
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@ -889,7 +896,7 @@ test("Can post and remove an offer", async () => {
|
||||
if (getOffer(await alice.getMyOffers(assetCode, "buy"), offer.getId())) throw new Error("Offer " + offer.getId() + " was found in my offers after removal");
|
||||
|
||||
// reserved balance released
|
||||
expect(unlockedBalanceBefore).toEqual(BigInt((await alice.getBalances()).getUnlockedBalance()));
|
||||
expect(BigInt((await alice.getBalances()).getUnlockedBalance())).toEqual(unlockedBalanceBefore);
|
||||
});
|
||||
|
||||
// TODO (woodser): test grpc notifications
|
||||
@ -913,7 +920,7 @@ test("Can complete a trade", async () => {
|
||||
HavenoUtils.log(1, "Alice posting offer to " + direction + " XMR for " + assetCode);
|
||||
let offer: OfferInfo = await postOffer(alice, {direction: direction, amount: tradeAmount, assetCode: assetCode});
|
||||
expect(offer.getState()).toEqual("AVAILABLE");
|
||||
HavenoUtils.log(1, "Alice done posting offer");
|
||||
HavenoUtils.log(1, "Alice done posting offer " + offer.getId());
|
||||
|
||||
// TODO (woodser): test error message taking offer before posted
|
||||
|
||||
@ -946,6 +953,7 @@ test("Can complete a trade", async () => {
|
||||
HavenoUtils.log(1, "Bob done taking offer in " + (Date.now() - startTime) + " ms");
|
||||
|
||||
// alice is notified that offer is taken
|
||||
await wait(1000);
|
||||
let tradeNotifications = getNotifications(aliceNotifications, NotificationMessage.NotificationType.TRADE_UPDATE);
|
||||
expect(tradeNotifications.length).toBe(1);
|
||||
expect(tradeNotifications[0].getTrade()!.getPhase()).toEqual("DEPOSIT_PUBLISHED");
|
||||
@ -974,12 +982,10 @@ test("Can complete a trade", async () => {
|
||||
await testTradeChat(trade.getTradeId(), alice, bob);
|
||||
|
||||
// mine until deposit txs unlock
|
||||
HavenoUtils.log(1, "Mining to unlock deposit txs");
|
||||
await waitForUnlockedTxs(fetchedTrade.getMakerDepositTxId(), fetchedTrade.getTakerDepositTxId());
|
||||
HavenoUtils.log(1, "Done mining to unlock deposit txs");
|
||||
|
||||
// alice notified to send payment
|
||||
await wait(TestConfig.walletSyncPeriodMs * 2);
|
||||
await wait(TestConfig.maxWalletStartupMs + TestConfig.walletSyncPeriodMs * 2);
|
||||
fetchedTrade = await alice.getTrade(trade.getTradeId());
|
||||
expect(fetchedTrade.getIsDepositConfirmed()).toBe(true);
|
||||
expect(fetchedTrade.getPhase()).toEqual("DEPOSIT_CONFIRMED"); // TODO (woodser): rename to DEPOSIT_UNLOCKED, have phase for when deposit txs confirm?
|
||||
@ -988,22 +994,24 @@ test("Can complete a trade", async () => {
|
||||
expect(fetchedTrade.getPhase()).toEqual("DEPOSIT_CONFIRMED");
|
||||
|
||||
// alice indicates payment is sent
|
||||
HavenoUtils.log(1, "Alice confirming payment started");
|
||||
await alice.confirmPaymentStarted(trade.getTradeId());
|
||||
fetchedTrade = await alice.getTrade(trade.getTradeId());
|
||||
expect(fetchedTrade.getPhase()).toEqual("FIAT_SENT"); // TODO (woodser): rename to PAYMENT_SENT
|
||||
expect(fetchedTrade.getPhase()).toEqual("PAYMENT_SENT");
|
||||
|
||||
// bob notified payment is sent
|
||||
await wait(TestConfig.maxTimePeerNoticeMs);
|
||||
await wait(TestConfig.maxTimePeerNoticeMs + TestConfig.maxWalletStartupMs);
|
||||
fetchedTrade = await bob.getTrade(trade.getTradeId());
|
||||
expect(fetchedTrade.getPhase()).toEqual("FIAT_SENT"); // TODO (woodser): rename to PAYMENT_SENT
|
||||
expect(fetchedTrade.getPhase()).toEqual("PAYMENT_SENT");
|
||||
|
||||
// bob confirms payment is received
|
||||
HavenoUtils.log(1, "Bob confirming payment received");
|
||||
await bob.confirmPaymentReceived(trade.getTradeId());
|
||||
fetchedTrade = await bob.getTrade(trade.getTradeId());
|
||||
expect(fetchedTrade.getPhase()).toEqual("PAYOUT_PUBLISHED");
|
||||
expect(fetchedTrade.getPhase()).toEqual("PAYOUT_PUBLISHED"); // TODO (woodser): payout is not necessarily published, buyer might need to sign
|
||||
|
||||
// alice notified trade is complete and of balance changes
|
||||
await wait(TestConfig.walletSyncPeriodMs * 2);
|
||||
await wait(TestConfig.maxWalletStartupMs + TestConfig.walletSyncPeriodMs * 2);
|
||||
fetchedTrade = await alice.getTrade(trade.getTradeId());
|
||||
expect(fetchedTrade.getPhase()).toEqual("PAYOUT_PUBLISHED");
|
||||
|
||||
@ -1022,24 +1030,27 @@ 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);
|
||||
await fundWallets([aliceWallet, bobWallet], tradeAmount * BigInt("6"), 4);
|
||||
|
||||
// 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); });
|
||||
await alice.addNotificationListener(notification => { HavenoUtils.log(3, "Alice received notification " + notification.getType() + " " + (notification.getChatMessage() ? notification.getChatMessage()?.getMessage() : "")); aliceNotifications.push(notification); });
|
||||
await bob.addNotificationListener(notification => { HavenoUtils.log(3, "Bob received notification " + notification.getType() + " " + (notification.getChatMessage() ? notification.getChatMessage()?.getMessage() : "")); bobNotifications.push(notification); });
|
||||
await arbitrator.addNotificationListener(notification => { HavenoUtils.log(3, "Arbitrator received notification " + notification.getType() + " " + (notification.getChatMessage() ? notification.getChatMessage()?.getMessage() : "")); arbitratorNotifications.push(notification); });
|
||||
|
||||
// TODO: notification collector with logging
|
||||
|
||||
// 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}));
|
||||
for (let i = 0; i < numOffers; i++) offers.push(postOffer(alice, {direction: direction, amount: tradeAmount, awaitUnlockedBalance: true}));
|
||||
offers = await Promise.all(offers);
|
||||
HavenoUtils.log(1, "Alice done posting offers");
|
||||
for (let i = 0; i < offers.length; i++) HavenoUtils.log(2, "Offer " + i + ": " + (await alice.getMyOffer(offers[i].getId())).getId());
|
||||
|
||||
// wait for offers to post
|
||||
await wait(TestConfig.walletSyncPeriodMs * 2);
|
||||
@ -1047,35 +1058,37 @@ test("Can resolve disputes", async () => {
|
||||
// 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")
|
||||
for (let i = 0; i < numOffers; i++) trades.push(bob.takeOffer(offers[i].getId(), paymentAccount.getId()));
|
||||
trades = await Promise.all(trades);
|
||||
HavenoUtils.log(1, "Bob done taking offers");
|
||||
|
||||
// test trades
|
||||
let depositTxIds: string[] = [];
|
||||
for (let trade of trades) {
|
||||
if (trade.getPhase() !== "DEPOSIT_PUBLISHED") throw new Error("Trade phase expected to be DEPOSIT_PUBLISHED but was " + trade.getPhase() + " for trade " + trade.getTradeId());
|
||||
expect(trade.getPhase()).toEqual("DEPOSIT_PUBLISHED");
|
||||
let fetchedTrade: TradeInfo = await bob.getTrade(trade.getTradeId());
|
||||
if (fetchedTrade.getPhase() !== "DEPOSIT_PUBLISHED") throw new Error("Fetched phase expected to be DEPOSIT_PUBLISHED but was " + fetchedTrade.getPhase() + " for trade " + fetchedTrade.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());
|
||||
await Promise.all([
|
||||
bob.openDispute(trades[0].getTradeId()),
|
||||
alice.openDispute(trades[1].getTradeId()),
|
||||
bob.openDispute(trades[2].getTradeId()),
|
||||
alice.openDispute(trades[3].getTradeId())
|
||||
]);
|
||||
HavenoUtils.log(1, "Done opening disputes");
|
||||
|
||||
// test dispute
|
||||
// test dispute state
|
||||
let bobDispute = await bob.getDispute(trades[0].getTradeId());
|
||||
expect(bobDispute.getTradeId()).toEqual(trades[0].getTradeId());
|
||||
expect(bobDispute.getIsOpener()).toBe(true);
|
||||
@ -1090,7 +1103,7 @@ test("Can resolve disputes", async () => {
|
||||
}
|
||||
|
||||
// alice sees the dispute
|
||||
await wait(TestConfig.maxTimePeerNoticeMs * 2);
|
||||
await wait(TestConfig.maxTimePeerNoticeMs + TestConfig.maxWalletStartupMs);
|
||||
let aliceDispute = await alice.getDispute(trades[0].getTradeId());
|
||||
expect(aliceDispute.getTradeId()).toEqual(trades[0].getTradeId());
|
||||
expect(aliceDispute.getIsOpener()).toBe(false);
|
||||
@ -1109,6 +1122,7 @@ test("Can resolve disputes", async () => {
|
||||
await arbitrator.sendDisputeChatMessage(arbAliceDispute!.getId(), "Arbitrator chat message to Alice", []);
|
||||
|
||||
// alice and bob reply to arbitrator chat messages
|
||||
await wait(TestConfig.maxTimePeerNoticeMs); // wait for arbitrator's message to arrive
|
||||
let attachment = new Attachment();
|
||||
let bytes = new Uint8Array(Buffer.from("Proof Bob was scammed", "utf8"));
|
||||
attachment.setBytes(bytes);
|
||||
@ -1117,8 +1131,10 @@ test("Can resolve disputes", async () => {
|
||||
let bytes2 = new Uint8Array(Buffer.from("picture bytes", "utf8"));
|
||||
attachment2.setBytes(bytes2);
|
||||
attachment2.setFileName("proof.png");
|
||||
HavenoUtils.log(2, "Bob sending chat message to arbitrator. tradeId=" + trades[0].getTradeId() + ", disputeId=" + bobDispute.getId());
|
||||
await bob.sendDisputeChatMessage(bobDispute.getId(), "Bob chat message", [attachment, attachment2]);
|
||||
await wait(1000); // make sure messages are sent in order
|
||||
await wait(TestConfig.maxTimePeerNoticeMs); // wait for bob's message to arrive
|
||||
HavenoUtils.log(2, "Alice sending chat message to arbitrator. tradeId=" + trades[0].getTradeId() + ", disputeId=" + aliceDispute.getId());
|
||||
await alice.sendDisputeChatMessage(aliceDispute.getId(), "Alice chat message", []);
|
||||
|
||||
// test alice and bob's chat messages
|
||||
@ -1161,13 +1177,14 @@ test("Can resolve disputes", async () => {
|
||||
expect(chatNotifications[1].getChatMessage()?.getMessage()).toEqual("Alice chat message");
|
||||
|
||||
// award trade amount to seller
|
||||
HavenoUtils.log(1, "Awarding trade amount to seller");
|
||||
HavenoUtils.log(1, "Awarding trade amount to seller, trade " + trades[0].getTradeId());
|
||||
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");
|
||||
HavenoUtils.log(1, "Done resolving dispute");
|
||||
|
||||
// dispute is resolved
|
||||
await wait(TestConfig.maxTimePeerNoticeMs);
|
||||
await wait(TestConfig.maxWalletStartupMs);
|
||||
updatedDispute = await alice.getDispute(trades[0].getTradeId());
|
||||
expect(updatedDispute.getIsClosed()).toBe(true);
|
||||
updatedDispute = await bob.getDispute(trades[0].getTradeId());
|
||||
@ -1185,34 +1202,52 @@ test("Can resolve disputes", async () => {
|
||||
expect(bobDifference).toEqual(winnerPayout);
|
||||
|
||||
// award trade amount to buyer
|
||||
HavenoUtils.log(1, "Awarding trade amount to buyer");
|
||||
HavenoUtils.log(1, "Awarding trade amount to buyer, trade " + trades[1].getTradeId());
|
||||
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);
|
||||
HavenoUtils.log(1, "Done resolving dispute");
|
||||
await wait(TestConfig.maxTimePeerNoticeMs + TestConfig.maxWalletStartupMs * 2 + TestConfig.walletSyncPeriodMs); // TODO (woodser): arbitrator sends mailbox message to trader -> trader opens and syncs multisig wallet and sends updated multisig hex to arbitrator -> arbitrator opens and syncs multisig wallet, signs payout tx and sends to trader -> trader finishes signing payout tx and broadcasts. more efficient way? traders can verify payout tx without syncing multisig wallet again
|
||||
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());
|
||||
if (aliceDifference !== winnerPayout || loserPayout - bobDifference > TestConfig.maxFee) {
|
||||
HavenoUtils.log(0, "WARNING: payout not observed. waiting longer"); // TODO (woodser): refactor dispute resolution
|
||||
await wait(TestConfig.maxWalletStartupMs + TestConfig.walletSyncPeriodMs);
|
||||
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(winnerPayout);
|
||||
expect(loserPayout - bobDifference).toBeLessThan(TestConfig.maxFee);
|
||||
|
||||
// award half of trade amount to buyer
|
||||
HavenoUtils.log(1, "Awarding half of trade amount to buyer");
|
||||
HavenoUtils.log(1, "Awarding half of trade amount to buyer, trade " + trades[2].getTradeId());
|
||||
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);
|
||||
HavenoUtils.log(1, "Done resolving dispute");
|
||||
await wait(TestConfig.maxTimePeerNoticeMs + TestConfig.maxWalletStartupMs * 2 + TestConfig.walletSyncPeriodMs);
|
||||
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;
|
||||
if (aliceDifference !== customWinnerAmount || loserPayout - bobDifference > TestConfig.maxFee) {
|
||||
HavenoUtils.log(0, "WARNING: payout not observed. waiting longer");
|
||||
await wait(TestConfig.maxWalletStartupMs + TestConfig.walletSyncPeriodMs);
|
||||
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(customWinnerAmount);
|
||||
expect(loserPayout - bobDifference).toBeLessThan(TestConfig.maxFee);
|
||||
expect(loserPayout - bobDifference).toBeLessThanOrEqual(TestConfig.maxFee);
|
||||
|
||||
// award too little to loser
|
||||
customWinnerAmount = tradeAmount + HavenoUtils.centinerosToAtomicUnits(offers[3].getBuyerSecurityDeposit()) + HavenoUtils.centinerosToAtomicUnits(offers[3].getSellerSecurityDeposit()) - BigInt("10000");
|
||||
@ -1224,18 +1259,26 @@ test("Can resolve disputes", async () => {
|
||||
}
|
||||
|
||||
// award full amount to seller
|
||||
HavenoUtils.log(1, "Awarding full amount to seller");
|
||||
HavenoUtils.log(1, "Awarding full amount to seller, trade " + trades[3].getTradeId());
|
||||
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);
|
||||
await wait(TestConfig.maxTimePeerNoticeMs + TestConfig.maxWalletStartupMs * 2 + TestConfig.walletSyncPeriodMs);
|
||||
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);
|
||||
if (customWinnerAmount - bobDifference > TestConfig.maxFee) {
|
||||
HavenoUtils.log(0, "WARNING: payout not observed. waiting longer");
|
||||
await wait(TestConfig.maxWalletStartupMs + TestConfig.walletSyncPeriodMs);
|
||||
aliceBalancesAfter = await alice.getBalances();
|
||||
bobBalancesAfter = await bob.getBalances();
|
||||
aliceDifference = BigInt(aliceBalancesAfter.getBalance()) - BigInt(aliceBalancesBefore.getBalance());
|
||||
bobDifference = BigInt(bobBalancesAfter.getBalance()) - BigInt(bobBalancesBefore.getBalance());
|
||||
}
|
||||
expect(customWinnerAmount - bobDifference).toBeLessThanOrEqual(TestConfig.maxFee);
|
||||
});
|
||||
|
||||
test("Cannot make or take offer with insufficient unlocked funds", async () => {
|
||||
@ -1398,7 +1441,7 @@ test("Handles unexpected errors during trade initialization", async () => {
|
||||
|
||||
// trader 1 tries to take offer
|
||||
try {
|
||||
HavenoUtils.log(1, "Trader 1 taking offer");
|
||||
HavenoUtils.log(1, "Trader 1 taking offer " + offer.getId());
|
||||
await traders[1].takeOffer(offer.getId(), paymentAccount.getId());
|
||||
throw new Error("Should have failed taking offer because taker trade funds spent")
|
||||
} catch (err) {
|
||||
@ -1411,8 +1454,8 @@ test("Handles unexpected errors during trade initialization", async () => {
|
||||
await wait(10000); // give time for trade initialization to fail and offer to become available
|
||||
offer = await traders[0].getMyOffer(offer.getId());
|
||||
if (offer.getState() !== "AVAILABLE") {
|
||||
HavenoUtils.log(1, "Offer is not yet available, waiting to become available after timeout..."); // there is no error notice if peer stops responding
|
||||
await wait(25000); // give another 25 seconds to become available
|
||||
HavenoUtils.log(1, "Offer is not yet available, waiting to become available after timeout..."); // TODO (woodser): fail trade on nack during initialization to save a bunch of time
|
||||
await wait(TestConfig.tradeInitTimeout - 10000); // wait remaining time for offer to become available after timeout
|
||||
offer = await traders[0].getMyOffer(offer.getId());
|
||||
assert.equal(offer.getState(), "AVAILABLE");
|
||||
}
|
||||
@ -1583,6 +1626,14 @@ async function initFundingWallet() {
|
||||
}
|
||||
}
|
||||
|
||||
async function startMining() {
|
||||
try {
|
||||
await monerod.startMining(await fundingWallet.getPrimaryAddress(), 3);
|
||||
} catch (err) {
|
||||
if (err.message !== "Already mining") throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for unlocked balance in wallet or Haveno daemon.
|
||||
*/
|
||||
@ -1639,7 +1690,7 @@ async function waitForUnlockedBalance(amount: bigint, ...wallets: any[]) {
|
||||
if (!miningNeeded) return;
|
||||
|
||||
// wait for funds to unlock
|
||||
console.log("Mining for unlocked balance of " + amount);
|
||||
HavenoUtils.log(0, "Mining for unlocked balance of " + amount);
|
||||
await startMining();
|
||||
let promises: Promise<void>[] = [];
|
||||
for (let wallet of wallets) {
|
||||
@ -1655,10 +1706,12 @@ async function waitForUnlockedBalance(amount: bigint, ...wallets: any[]) {
|
||||
}
|
||||
await Promise.all(promises);
|
||||
await monerod.stopMining();
|
||||
console.log("Funds unlocked, done mining");
|
||||
HavenoUtils.log(0, "Funds unlocked, done mining");
|
||||
};
|
||||
|
||||
async function waitForUnlockedTxs(...txHashes: string[]) {
|
||||
if (txHashes.length === 0) return;
|
||||
HavenoUtils.log(1, "Mining to unlock txs");
|
||||
await startMining();
|
||||
let promises: Promise<void>[] = [];
|
||||
for (let txHash of txHashes) {
|
||||
@ -1675,15 +1728,68 @@ async function waitForUnlockedTxs(...txHashes: string[]) {
|
||||
}));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
HavenoUtils.log(1, "Done mining to unlock txs");
|
||||
await monerod.stopMining();
|
||||
}
|
||||
|
||||
async function startMining() {
|
||||
try {
|
||||
await monerod.startMining(await fundingWallet.getPrimaryAddress(), 3);
|
||||
} catch (err) {
|
||||
if (err.message !== "Already mining") throw err;
|
||||
/**
|
||||
* Indicates if the wallet has an unlocked amount.
|
||||
*
|
||||
* @param {MoneroWallet} wallet - wallet to check
|
||||
* @param {BigInt} amt - amount to check
|
||||
* @param {number?} numOutputs - number of outputs of the given amount (default 1)
|
||||
*/
|
||||
async function hasUnlockedOutputs(wallet: any, amt: BigInt, numOutputs?: number): Promise<boolean> {
|
||||
if (numOutputs === undefined) numOutputs = 1;
|
||||
let availableOutputs = await wallet.getOutputs({isSpent: false, isFrozen: false, minAmount: monerojs.BigInteger(amt.toString()), txQuery: {isLocked: false}});
|
||||
return availableOutputs.length >= numOutputs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fund the given wallets.
|
||||
*
|
||||
* @param {MoneroWallet} wallets - monerojs wallets
|
||||
* @param {BigInt} amt - the amount to fund
|
||||
* @param {number?} numOutputs - the number of outputs of the given amount (default 1)
|
||||
*/
|
||||
async function fundWallets(wallets: any[], amt: BigInt, numOutputs?: number): Promise<void> {
|
||||
if (numOutputs === undefined) numOutputs = 1;
|
||||
|
||||
// collect destinations
|
||||
let destinations = [];
|
||||
for (let wallet of wallets) {
|
||||
if (await hasUnlockedOutputs(wallet, amt, numOutputs)) continue;
|
||||
for (let i = 0; i < numOutputs; i++) {
|
||||
destinations.push(new MoneroDestination((await wallet.createSubaddress()).getAddress(), monerojs.BigInteger(amt.toString())));
|
||||
}
|
||||
}
|
||||
|
||||
// fund destinations
|
||||
let txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(true);
|
||||
let txHashes: string[] = [];
|
||||
let sendAmt = BigInteger("0");
|
||||
for (let i = 0; i < destinations.length; i++) {
|
||||
txConfig.addDestination(destinations[i]);
|
||||
sendAmt = sendAmt.add(destinations[i].getAmount());
|
||||
if (i === destinations.length - 1 || (i > 0 && i % 15 === 0)) {
|
||||
await waitForUnlockedBalance(toBigInt(sendAmt), fundingWallet);
|
||||
txHashes.push((await fundingWallet.createTx(txConfig)).getHash());
|
||||
txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(true);
|
||||
sendAmt = BigInteger("0");
|
||||
}
|
||||
}
|
||||
|
||||
// wait for txs to unlock
|
||||
if (txHashes.length > 0) {
|
||||
await waitForUnlockedTxs(...txHashes);
|
||||
await wait(1000);
|
||||
for (let wallet of wallets) await wallet.sync();
|
||||
}
|
||||
}
|
||||
|
||||
// convert monero-javascript BigInteger to typescript BigInt
|
||||
function toBigInt(mjsBigInt: any) {
|
||||
return BigInt(mjsBigInt.toString())
|
||||
}
|
||||
|
||||
async function wait(durationMs: number) {
|
||||
@ -1836,10 +1942,6 @@ async function postOffer(maker: HavenoDaemon, config?: any) {
|
||||
config.triggerPrice);
|
||||
testOffer(offer, config);
|
||||
|
||||
// unlocked balance has decreased
|
||||
let unlockedBalanceAfter: bigint = BigInt((await maker.getBalances()).getUnlockedBalance());
|
||||
if (unlockedBalanceAfter === unlockedBalanceBefore) throw new Error("unlocked balance did not change after posting offer");
|
||||
|
||||
// offer is included in my offers only
|
||||
if (!getOffer(await maker.getMyOffers(config.assetCode, config.direction), offer.getId())) {
|
||||
await wait(10000);
|
||||
@ -1847,6 +1949,10 @@ async function postOffer(maker: HavenoDaemon, config?: any) {
|
||||
}
|
||||
if (getOffer(await maker.getOffers(config.assetCode, config.direction), offer.getId())) throw new Error("My offer " + offer.getId() + " should not appear in available offers");
|
||||
|
||||
// unlocked balance has decreased
|
||||
let unlockedBalanceAfter: bigint = BigInt((await maker.getBalances()).getUnlockedBalance());
|
||||
if (unlockedBalanceAfter === unlockedBalanceBefore) throw new Error("unlocked balance did not change after posting offer");
|
||||
|
||||
return offer;
|
||||
}
|
||||
|
||||
@ -1948,7 +2054,7 @@ async function testTradeChat(tradeId: string, alice: HavenoDaemon, bob: HavenoDa
|
||||
offset = 1; // 1 existing notification
|
||||
expect(chatNotifications.length).toBe(offset + msgs.length);
|
||||
expect(chatNotifications[0].getChatMessage()?.getMessage()).toEqual(aliceMsg);
|
||||
for (var i = 0; i < msgs.length; i++) {
|
||||
for (let i = 0; i < msgs.length; i++) {
|
||||
expect(chatNotifications[i + offset].getChatMessage()?.getMessage()).toEqual(msgs[i]);
|
||||
}
|
||||
}
|
||||
|
@ -679,7 +679,7 @@ class HavenoDaemon {
|
||||
async getBalances(): Promise<XmrBalanceInfo> {
|
||||
let that = this;
|
||||
return new Promise(function(resolve, reject) {
|
||||
that._walletsClient.getBalances(new GetBalancesRequest(), {password: that._password}, function(err: grpcWeb.RpcError, response: GetBalancesReply) {
|
||||
that._walletsClient.getBalances(new GetBalancesRequest().setCurrencyCode("XMR"), {password: that._password}, function(err: grpcWeb.RpcError, response: GetBalancesReply) {
|
||||
if (err) reject(err);
|
||||
else resolve(response.getBalances()!.getXmr()!);
|
||||
});
|
||||
@ -1018,7 +1018,7 @@ class HavenoDaemon {
|
||||
return new Promise(function(resolve, reject) {
|
||||
that._tradesClient.takeOffer(request, {password: that._password}, function(err: grpcWeb.RpcError, response: TakeOfferReply) {
|
||||
if (err) reject(err);
|
||||
else if (response.getFailureReason() && response.getFailureReason()!.getAvailabilityResult() !== AvailabilityResult.AVAILABLE) reject(response.getFailureReason()!.getDescription());
|
||||
else if (response.getFailureReason() && response.getFailureReason()!.getAvailabilityResult() !== AvailabilityResult.AVAILABLE) reject(new Error(response.getFailureReason()!.getDescription())); // TODO: api should throw grpcWeb.RpcError
|
||||
else resolve(response.getTrade()!);
|
||||
});
|
||||
});
|
||||
|
@ -6,19 +6,10 @@ const console = require('console');
|
||||
*/
|
||||
class HavenoUtils {
|
||||
|
||||
static LOG_LEVEL = 0;
|
||||
static CENTINEROS_AU_MULTIPLIER = 10000;
|
||||
|
||||
/**
|
||||
* Log a message.
|
||||
*
|
||||
* @param {int} level - log level of the message
|
||||
* @param {string} msg - message to log
|
||||
*/
|
||||
static log(level: number, msg: string) {
|
||||
assert(level === parseInt(level + "", 10) && level >= 0, "Log level must be an integer >= 0");
|
||||
if (HavenoUtils.LOG_LEVEL >= level) console.log(msg);
|
||||
}
|
||||
static logLevel = 0;
|
||||
static centinerosToAUMultiplier = 10000;
|
||||
static months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
||||
static lastLogTimeMs = 0;
|
||||
|
||||
/**
|
||||
* Set the log level with 0 being least verbose.
|
||||
@ -27,7 +18,7 @@ class HavenoUtils {
|
||||
*/
|
||||
static async setLogLevel(level: number) {
|
||||
assert(level === parseInt(level + "", 10) && level >= 0, "Log level must be an integer >= 0");
|
||||
HavenoUtils.LOG_LEVEL = level;
|
||||
HavenoUtils.logLevel = level;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -36,7 +27,35 @@ class HavenoUtils {
|
||||
* @return {int} the current log level
|
||||
*/
|
||||
static getLogLevel(): number {
|
||||
return HavenoUtils.LOG_LEVEL;
|
||||
return HavenoUtils.logLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message. // TODO (woodser): switch to log library?
|
||||
*
|
||||
* @param {int} level - log level of the message
|
||||
* @param {string} msg - message to log
|
||||
* @param {boolean?} warn - log the message as a warning if true
|
||||
*/
|
||||
static log(level: number, msg: string) {
|
||||
assert(level === parseInt(level + "", 10) && level >= 0, "Log level must be an integer >= 0");
|
||||
if (HavenoUtils.logLevel >= level) {
|
||||
let now = Date.now();
|
||||
let formattedTimeSinceLastLog = HavenoUtils.lastLogTimeMs ? " (+" + (now - HavenoUtils.lastLogTimeMs) + " ms)" : "\t";
|
||||
HavenoUtils.lastLogTimeMs = now;
|
||||
console.log(HavenoUtils.formatTimestamp(now) + formattedTimeSinceLastLog + "\t[L" + level + "] " + msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a timestamp as e.g. Jul-07 hh:mm:ss:ms. // TODO: move to GenUtils?
|
||||
*
|
||||
* @param {number} timestamp - the timestamp in milliseconds to format
|
||||
* @return {string} the formatted timestamp
|
||||
*/
|
||||
static formatTimestamp(timestamp: number): string {
|
||||
let date = new Date(timestamp);
|
||||
return HavenoUtils.months[date.getMonth()] + "-" + date.getDate() + " " + date.getHours() + ':' + ("0" + date.getMinutes()).substr(-2) + ':' + ("0" + date.getSeconds()).substr(-2) + ':' + ("0" + date.getMilliseconds()).substr(-2);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -62,7 +81,7 @@ class HavenoUtils {
|
||||
* @return {BigInt} the amount denominated in atomic units
|
||||
*/
|
||||
static centinerosToAtomicUnits(centineros: number): bigint {
|
||||
return BigInt(centineros) * BigInt(HavenoUtils.CENTINEROS_AU_MULTIPLIER);
|
||||
return BigInt(centineros) * BigInt(HavenoUtils.centinerosToAUMultiplier);
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user