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:
woodser 2022-04-06 11:28:56 -04:00 committed by GitHub
parent ad26aae4e6
commit 91710b2bcf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 199 additions and 74 deletions

View File

@ -13,8 +13,10 @@ import OnlineStatus = UrlConnection.OnlineStatus;
// import monero-javascript // import monero-javascript
const monerojs = require("monero-javascript"); // TODO (woodser): support typescript and `npm install @types/monero-javascript` in 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 GenUtils = monerojs.GenUtils;
const BigInteger = monerojs.BigInteger;
const MoneroNetworkType = monerojs.MoneroNetworkType; const MoneroNetworkType = monerojs.MoneroNetworkType;
const MoneroTxConfig = monerojs.MoneroTxConfig; const MoneroTxConfig = monerojs.MoneroTxConfig;
const MoneroDestination = monerojs.MoneroDestination;
const MoneroUtils = monerojs.MoneroUtils; const MoneroUtils = monerojs.MoneroUtils;
const TaskLooper = monerojs.TaskLooper; const TaskLooper = monerojs.TaskLooper;
@ -80,11 +82,13 @@ const TestConfig = {
url: "http://localhost:8081", url: "http://localhost:8081",
accountPasswordRequired: false, accountPasswordRequired: false,
accountPassword: "abctesting789", accountPassword: "abctesting789",
walletUrl: "http://127.0.0.1:38092",
} }
], ],
maxFee: BigInt("75000000000"), maxFee: BigInt("75000000000"),
walletSyncPeriodMs: 5000, // TODO (woodser): auto adjust higher if using remote connection walletSyncPeriodMs: 5000, // TODO (woodser): auto adjust higher if using remote connection
daemonPollPeriodMs: 15000, daemonPollPeriodMs: 15000,
maxWalletStartupMs: 10000, // TODO (woodser): make shorter by switching to jni
maxTimePeerNoticeMs: 3000, maxTimePeerNoticeMs: 3000,
assetCodes: ["USD", "GBP", "EUR", "ETH", "BTC", "BCH", "LTC", "ZEC"], // primary asset codes assetCodes: ["USD", "GBP", "EUR", "ETH", "BTC", "BCH", "LTC", "ZEC"], // primary asset codes
cryptoAddresses: [{ cryptoAddresses: [{
@ -95,7 +99,7 @@ const TestConfig = {
address: "bcrt1q6j90vywv8x7eyevcnn2tn2wrlg3vsjlsvt46qz" address: "bcrt1q6j90vywv8x7eyevcnn2tn2wrlg3vsjlsvt46qz"
}, { }, {
currencyCode: "BCH", currencyCode: "BCH",
address: "1JRjBNKi4ZgJpKPeoL4149Q7ZZD3VvVgk9" // TODO: support Cash Address format also address: "1JRjBNKi4ZgJpKPeoL4149Q7ZZD3VvVgk9" // TODO: support CashAddr format only
}, { }, {
currencyCode: "LTC", currencyCode: "LTC",
address: "LXUTUN5mTPc2LsS7cEjkyjTRcfYyJGoUuQ" address: "LXUTUN5mTPc2LsS7cEjkyjTRcfYyJGoUuQ"
@ -115,8 +119,9 @@ const TestConfig = {
["8086", ["10005", "7781"]], ["8086", ["10005", "7781"]],
]), ]),
devPrivilegePrivKey: "6ac43ea1df2a290c1c8391736aa42e4339c5cb4f110ff0257a13b63211977b7a", // from DEV_PRIVILEGE_PRIV_KEY devPrivilegePrivKey: "6ac43ea1df2a290c1c8391736aa42e4339c5cb4f110ff0257a13b63211977b7a", // from DEV_PRIVILEGE_PRIV_KEY
tradeInitTimeout: 60000,
timeout: 900000, // timeout in ms for all tests to complete (15 minutes) 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 direction: "buy", // buy or sell xmr
amount: BigInt("200000000000"), // amount of xmr to trade amount: BigInt("200000000000"), // amount of xmr to trade
assetCode: "eth", // counter asset to trade assetCode: "eth", // counter asset to trade
@ -142,6 +147,7 @@ let bob: HavenoDaemon;
let monerod: any; let monerod: any;
let fundingWallet: any; let fundingWallet: any;
let aliceWallet: any; let aliceWallet: any;
let bobWallet: any;
// track started haveno processes // track started haveno processes
const HAVENO_PROCESSES: HavenoDaemon[] = []; const HAVENO_PROCESSES: HavenoDaemon[] = [];
@ -179,13 +185,14 @@ beforeAll(async () => {
// connect monero clients // connect monero clients
monerod = await monerojs.connectToDaemonRpc(TestConfig.monerod.url, TestConfig.monerod.username, TestConfig.monerod.password); 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); 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 // initialize funding wallet
await initFundingWallet(); await initFundingWallet();
}); });
beforeEach(async() => { beforeEach(async() => {
console.log("Before test \"" + expect.getState().currentTestName + "\""); HavenoUtils.log(0, "BEFORE TEST \"" + expect.getState().currentTestName + "\"");
}); });
afterAll(async () => { 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"); 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 // reserved balance released
expect(unlockedBalanceBefore).toEqual(BigInt((await alice.getBalances()).getUnlockedBalance())); expect(BigInt((await alice.getBalances()).getUnlockedBalance())).toEqual(unlockedBalanceBefore);
}); });
// TODO (woodser): test grpc notifications // 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); HavenoUtils.log(1, "Alice posting offer to " + direction + " XMR for " + assetCode);
let offer: OfferInfo = await postOffer(alice, {direction: direction, amount: tradeAmount, assetCode: assetCode}); let offer: OfferInfo = await postOffer(alice, {direction: direction, amount: tradeAmount, assetCode: assetCode});
expect(offer.getState()).toEqual("AVAILABLE"); 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 // 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"); HavenoUtils.log(1, "Bob done taking offer in " + (Date.now() - startTime) + " ms");
// alice is notified that offer is taken // alice is notified that offer is taken
await wait(1000);
let tradeNotifications = getNotifications(aliceNotifications, NotificationMessage.NotificationType.TRADE_UPDATE); let tradeNotifications = getNotifications(aliceNotifications, NotificationMessage.NotificationType.TRADE_UPDATE);
expect(tradeNotifications.length).toBe(1); expect(tradeNotifications.length).toBe(1);
expect(tradeNotifications[0].getTrade()!.getPhase()).toEqual("DEPOSIT_PUBLISHED"); expect(tradeNotifications[0].getTrade()!.getPhase()).toEqual("DEPOSIT_PUBLISHED");
@ -974,12 +982,10 @@ test("Can complete a trade", async () => {
await testTradeChat(trade.getTradeId(), alice, bob); await testTradeChat(trade.getTradeId(), alice, bob);
// mine until deposit txs unlock // mine until deposit txs unlock
HavenoUtils.log(1, "Mining to unlock deposit txs");
await waitForUnlockedTxs(fetchedTrade.getMakerDepositTxId(), fetchedTrade.getTakerDepositTxId()); await waitForUnlockedTxs(fetchedTrade.getMakerDepositTxId(), fetchedTrade.getTakerDepositTxId());
HavenoUtils.log(1, "Done mining to unlock deposit txs");
// alice notified to send payment // alice notified to send payment
await wait(TestConfig.walletSyncPeriodMs * 2); await wait(TestConfig.maxWalletStartupMs + TestConfig.walletSyncPeriodMs * 2);
fetchedTrade = await alice.getTrade(trade.getTradeId()); fetchedTrade = await alice.getTrade(trade.getTradeId());
expect(fetchedTrade.getIsDepositConfirmed()).toBe(true); expect(fetchedTrade.getIsDepositConfirmed()).toBe(true);
expect(fetchedTrade.getPhase()).toEqual("DEPOSIT_CONFIRMED"); // TODO (woodser): rename to DEPOSIT_UNLOCKED, have phase for when deposit txs confirm? 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"); expect(fetchedTrade.getPhase()).toEqual("DEPOSIT_CONFIRMED");
// alice indicates payment is sent // alice indicates payment is sent
HavenoUtils.log(1, "Alice confirming payment started");
await alice.confirmPaymentStarted(trade.getTradeId()); await alice.confirmPaymentStarted(trade.getTradeId());
fetchedTrade = await alice.getTrade(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 // bob notified payment is sent
await wait(TestConfig.maxTimePeerNoticeMs); await wait(TestConfig.maxTimePeerNoticeMs + TestConfig.maxWalletStartupMs);
fetchedTrade = await bob.getTrade(trade.getTradeId()); 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 // bob confirms payment is received
HavenoUtils.log(1, "Bob confirming payment received");
await bob.confirmPaymentReceived(trade.getTradeId()); await bob.confirmPaymentReceived(trade.getTradeId());
fetchedTrade = await bob.getTrade(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 // 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()); fetchedTrade = await alice.getTrade(trade.getTradeId());
expect(fetchedTrade.getPhase()).toEqual("PAYOUT_PUBLISHED"); 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 // wait for alice and bob to have unlocked balance for trade
let tradeAmount: bigint = BigInt("250000000000"); let tradeAmount: bigint = BigInt("250000000000");
await waitForUnlockedBalance(tradeAmount * BigInt("6"), alice, bob); await fundWallets([aliceWallet, bobWallet], tradeAmount * BigInt("6"), 4);
// register to receive notifications // register to receive notifications
let aliceNotifications: NotificationMessage[] = []; let aliceNotifications: NotificationMessage[] = [];
let bobNotifications: NotificationMessage[] = []; let bobNotifications: NotificationMessage[] = [];
let arbitratorNotifications: NotificationMessage[] = []; let arbitratorNotifications: NotificationMessage[] = [];
await alice.addNotificationListener(notification => { aliceNotifications.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 => { bobNotifications.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 => { arbitratorNotifications.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 // alice posts offers to buy xmr
let numOffers = 4; let numOffers = 4;
HavenoUtils.log(1, "Alice posting offers"); HavenoUtils.log(1, "Alice posting offers");
let direction = "buy"; let direction = "buy";
let offers = []; 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); offers = await Promise.all(offers);
HavenoUtils.log(1, "Alice done posting 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 // wait for offers to post
await wait(TestConfig.walletSyncPeriodMs * 2); await wait(TestConfig.walletSyncPeriodMs * 2);
@ -1047,35 +1058,37 @@ test("Can resolve disputes", async () => {
// bob takes offers // bob takes offers
let paymentAccount = await createPaymentAccount(bob, "eth"); let paymentAccount = await createPaymentAccount(bob, "eth");
HavenoUtils.log(1, "Bob taking offers"); HavenoUtils.log(1, "Bob taking offers");
let startTime = Date.now();
let trades = []; let trades = [];
for (let i = 0; i < numOffers; i++) trades.push(await bob.takeOffer(offers[i].getId(), paymentAccount.getId())); for (let i = 0; i < numOffers; i++) trades.push(bob.takeOffer(offers[i].getId(), paymentAccount.getId()));
//trades = await Promise.all(trades); // TODO: take trades in parallel when they take less time trades = await Promise.all(trades);
HavenoUtils.log(1, "Bob done taking offers in " + (Date.now() - startTime) + " ms") HavenoUtils.log(1, "Bob done taking offers");
// test trades // test trades
let depositTxIds: string[] = []; let depositTxIds: string[] = [];
for (let trade of trades) { 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"); expect(trade.getPhase()).toEqual("DEPOSIT_PUBLISHED");
let fetchedTrade: TradeInfo = await bob.getTrade(trade.getTradeId()); 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"); expect(fetchedTrade.getPhase()).toEqual("DEPOSIT_PUBLISHED");
depositTxIds.push(fetchedTrade.getMakerDepositTxId()); depositTxIds.push(fetchedTrade.getMakerDepositTxId());
depositTxIds.push(fetchedTrade.getTakerDepositTxId()); depositTxIds.push(fetchedTrade.getTakerDepositTxId());
} }
// mine until deposit txs unlock // mine until deposit txs unlock
HavenoUtils.log(1, "Mining to unlock deposit txs");
await waitForUnlockedTxs(...depositTxIds); await waitForUnlockedTxs(...depositTxIds);
HavenoUtils.log(1, "Done mining to unlock deposit txs");
// open disputes // open disputes
HavenoUtils.log(1, "Opening disputes"); HavenoUtils.log(1, "Opening disputes");
await bob.openDispute(trades[0].getTradeId()); await Promise.all([
await alice.openDispute(trades[1].getTradeId()); bob.openDispute(trades[0].getTradeId()),
await bob.openDispute(trades[2].getTradeId()); alice.openDispute(trades[1].getTradeId()),
await alice.openDispute(trades[3].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()); let bobDispute = await bob.getDispute(trades[0].getTradeId());
expect(bobDispute.getTradeId()).toEqual(trades[0].getTradeId()); expect(bobDispute.getTradeId()).toEqual(trades[0].getTradeId());
expect(bobDispute.getIsOpener()).toBe(true); expect(bobDispute.getIsOpener()).toBe(true);
@ -1090,7 +1103,7 @@ test("Can resolve disputes", async () => {
} }
// alice sees the dispute // alice sees the dispute
await wait(TestConfig.maxTimePeerNoticeMs * 2); await wait(TestConfig.maxTimePeerNoticeMs + TestConfig.maxWalletStartupMs);
let aliceDispute = await alice.getDispute(trades[0].getTradeId()); let aliceDispute = await alice.getDispute(trades[0].getTradeId());
expect(aliceDispute.getTradeId()).toEqual(trades[0].getTradeId()); expect(aliceDispute.getTradeId()).toEqual(trades[0].getTradeId());
expect(aliceDispute.getIsOpener()).toBe(false); expect(aliceDispute.getIsOpener()).toBe(false);
@ -1109,6 +1122,7 @@ test("Can resolve disputes", async () => {
await arbitrator.sendDisputeChatMessage(arbAliceDispute!.getId(), "Arbitrator chat message to Alice", []); await arbitrator.sendDisputeChatMessage(arbAliceDispute!.getId(), "Arbitrator chat message to Alice", []);
// alice and bob reply to arbitrator chat messages // alice and bob reply to arbitrator chat messages
await wait(TestConfig.maxTimePeerNoticeMs); // wait for arbitrator's message to arrive
let attachment = new Attachment(); let attachment = new Attachment();
let bytes = new Uint8Array(Buffer.from("Proof Bob was scammed", "utf8")); let bytes = new Uint8Array(Buffer.from("Proof Bob was scammed", "utf8"));
attachment.setBytes(bytes); attachment.setBytes(bytes);
@ -1117,8 +1131,10 @@ test("Can resolve disputes", async () => {
let bytes2 = new Uint8Array(Buffer.from("picture bytes", "utf8")); let bytes2 = new Uint8Array(Buffer.from("picture bytes", "utf8"));
attachment2.setBytes(bytes2); attachment2.setBytes(bytes2);
attachment2.setFileName("proof.png"); 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 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", []); await alice.sendDisputeChatMessage(aliceDispute.getId(), "Alice chat message", []);
// test alice and bob's chat messages // 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"); expect(chatNotifications[1].getChatMessage()?.getMessage()).toEqual("Alice chat message");
// award trade amount to seller // 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 bobBalancesBefore = await bob.getBalances();
let aliceBalancesBefore = await alice.getBalances(); let aliceBalancesBefore = await alice.getBalances();
await arbitrator.resolveDispute(trades[0].getTradeId(), DisputeResult.Winner.SELLER, DisputeResult.Reason.PEER_WAS_LATE, "Seller is winner"); 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 // dispute is resolved
await wait(TestConfig.maxTimePeerNoticeMs); await wait(TestConfig.maxWalletStartupMs);
updatedDispute = await alice.getDispute(trades[0].getTradeId()); updatedDispute = await alice.getDispute(trades[0].getTradeId());
expect(updatedDispute.getIsClosed()).toBe(true); expect(updatedDispute.getIsClosed()).toBe(true);
updatedDispute = await bob.getDispute(trades[0].getTradeId()); updatedDispute = await bob.getDispute(trades[0].getTradeId());
@ -1185,34 +1202,52 @@ test("Can resolve disputes", async () => {
expect(bobDifference).toEqual(winnerPayout); expect(bobDifference).toEqual(winnerPayout);
// award trade amount to buyer // 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(); aliceBalancesBefore = await alice.getBalances();
bobBalancesBefore = await bob.getBalances(); bobBalancesBefore = await bob.getBalances();
await arbitrator.resolveDispute(trades[1].getTradeId(), DisputeResult.Winner.BUYER, DisputeResult.Reason.SELLER_NOT_RESPONDING, "Buyer is winner"); 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(); aliceBalancesAfter = await alice.getBalances();
bobBalancesAfter = await bob.getBalances(); bobBalancesAfter = await bob.getBalances();
aliceDifference = BigInt(aliceBalancesAfter.getBalance()) - BigInt(aliceBalancesBefore.getBalance()); aliceDifference = BigInt(aliceBalancesAfter.getBalance()) - BigInt(aliceBalancesBefore.getBalance());
bobDifference = BigInt(bobBalancesAfter.getBalance()) - BigInt(bobBalancesBefore.getBalance()); bobDifference = BigInt(bobBalancesAfter.getBalance()) - BigInt(bobBalancesBefore.getBalance());
winnerPayout = tradeAmount + HavenoUtils.centinerosToAtomicUnits(offers[1].getBuyerSecurityDeposit()); winnerPayout = tradeAmount + HavenoUtils.centinerosToAtomicUnits(offers[1].getBuyerSecurityDeposit());
loserPayout = HavenoUtils.centinerosToAtomicUnits(offers[1].getSellerSecurityDeposit()); 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(aliceDifference).toEqual(winnerPayout);
expect(loserPayout - bobDifference).toBeLessThan(TestConfig.maxFee); expect(loserPayout - bobDifference).toBeLessThan(TestConfig.maxFee);
// award half of trade amount to buyer // 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()); let customWinnerAmount = tradeAmount / BigInt(2) + HavenoUtils.centinerosToAtomicUnits(offers[2].getBuyerSecurityDeposit());
aliceBalancesBefore = await alice.getBalances(); aliceBalancesBefore = await alice.getBalances();
bobBalancesBefore = await bob.getBalances(); bobBalancesBefore = await bob.getBalances();
await arbitrator.resolveDispute(trades[2].getTradeId(), DisputeResult.Winner.BUYER, DisputeResult.Reason.WRONG_SENDER_ACCOUNT, "Split trade amount", customWinnerAmount); 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(); aliceBalancesAfter = await alice.getBalances();
bobBalancesAfter = await bob.getBalances(); bobBalancesAfter = await bob.getBalances();
aliceDifference = BigInt(aliceBalancesAfter.getBalance()) - BigInt(aliceBalancesBefore.getBalance()); aliceDifference = BigInt(aliceBalancesAfter.getBalance()) - BigInt(aliceBalancesBefore.getBalance());
bobDifference = BigInt(bobBalancesAfter.getBalance()) - BigInt(bobBalancesBefore.getBalance()); bobDifference = BigInt(bobBalancesAfter.getBalance()) - BigInt(bobBalancesBefore.getBalance());
loserPayout = tradeAmount + HavenoUtils.centinerosToAtomicUnits(offers[2].getBuyerSecurityDeposit()) + HavenoUtils.centinerosToAtomicUnits(offers[2].getSellerSecurityDeposit()) - customWinnerAmount; 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(aliceDifference).toEqual(customWinnerAmount);
expect(loserPayout - bobDifference).toBeLessThan(TestConfig.maxFee); expect(loserPayout - bobDifference).toBeLessThanOrEqual(TestConfig.maxFee);
// award too little to loser // award too little to loser
customWinnerAmount = tradeAmount + HavenoUtils.centinerosToAtomicUnits(offers[3].getBuyerSecurityDeposit()) + HavenoUtils.centinerosToAtomicUnits(offers[3].getSellerSecurityDeposit()) - BigInt("10000"); 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 // 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()); customWinnerAmount = tradeAmount + HavenoUtils.centinerosToAtomicUnits(offers[3].getBuyerSecurityDeposit()) + HavenoUtils.centinerosToAtomicUnits(offers[3].getSellerSecurityDeposit());
aliceBalancesBefore = await alice.getBalances(); aliceBalancesBefore = await alice.getBalances();
bobBalancesBefore = await bob.getBalances(); bobBalancesBefore = await bob.getBalances();
await arbitrator.resolveDispute(trades[3].getTradeId(), DisputeResult.Winner.SELLER, DisputeResult.Reason.TRADE_ALREADY_SETTLED, "Seller gets everything", customWinnerAmount); 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(); aliceBalancesAfter = await alice.getBalances();
bobBalancesAfter = await bob.getBalances(); bobBalancesAfter = await bob.getBalances();
aliceDifference = BigInt(aliceBalancesAfter.getBalance()) - BigInt(aliceBalancesBefore.getBalance()); aliceDifference = BigInt(aliceBalancesAfter.getBalance()) - BigInt(aliceBalancesBefore.getBalance());
bobDifference = BigInt(bobBalancesAfter.getBalance()) - BigInt(bobBalancesBefore.getBalance()); bobDifference = BigInt(bobBalancesAfter.getBalance()) - BigInt(bobBalancesBefore.getBalance());
expect(aliceDifference).toEqual(BigInt(0)); 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 () => { 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 // trader 1 tries to take offer
try { 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()); await traders[1].takeOffer(offer.getId(), paymentAccount.getId());
throw new Error("Should have failed taking offer because taker trade funds spent") throw new Error("Should have failed taking offer because taker trade funds spent")
} catch (err) { } 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 await wait(10000); // give time for trade initialization to fail and offer to become available
offer = await traders[0].getMyOffer(offer.getId()); offer = await traders[0].getMyOffer(offer.getId());
if (offer.getState() !== "AVAILABLE") { 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 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(25000); // give another 25 seconds to become available await wait(TestConfig.tradeInitTimeout - 10000); // wait remaining time for offer to become available after timeout
offer = await traders[0].getMyOffer(offer.getId()); offer = await traders[0].getMyOffer(offer.getId());
assert.equal(offer.getState(), "AVAILABLE"); 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. * Wait for unlocked balance in wallet or Haveno daemon.
*/ */
@ -1639,7 +1690,7 @@ async function waitForUnlockedBalance(amount: bigint, ...wallets: any[]) {
if (!miningNeeded) return; if (!miningNeeded) return;
// wait for funds to unlock // wait for funds to unlock
console.log("Mining for unlocked balance of " + amount); HavenoUtils.log(0, "Mining for unlocked balance of " + amount);
await startMining(); await startMining();
let promises: Promise<void>[] = []; let promises: Promise<void>[] = [];
for (let wallet of wallets) { for (let wallet of wallets) {
@ -1655,10 +1706,12 @@ async function waitForUnlockedBalance(amount: bigint, ...wallets: any[]) {
} }
await Promise.all(promises); await Promise.all(promises);
await monerod.stopMining(); await monerod.stopMining();
console.log("Funds unlocked, done mining"); HavenoUtils.log(0, "Funds unlocked, done mining");
}; };
async function waitForUnlockedTxs(...txHashes: string[]) { async function waitForUnlockedTxs(...txHashes: string[]) {
if (txHashes.length === 0) return;
HavenoUtils.log(1, "Mining to unlock txs");
await startMining(); await startMining();
let promises: Promise<void>[] = []; let promises: Promise<void>[] = [];
for (let txHash of txHashes) { for (let txHash of txHashes) {
@ -1675,15 +1728,68 @@ async function waitForUnlockedTxs(...txHashes: string[]) {
})); }));
} }
await Promise.all(promises); await Promise.all(promises);
HavenoUtils.log(1, "Done mining to unlock txs");
await monerod.stopMining(); await monerod.stopMining();
} }
async function startMining() { /**
try { * Indicates if the wallet has an unlocked amount.
await monerod.startMining(await fundingWallet.getPrimaryAddress(), 3); *
} catch (err) { * @param {MoneroWallet} wallet - wallet to check
if (err.message !== "Already mining") throw err; * @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) { async function wait(durationMs: number) {
@ -1836,10 +1942,6 @@ async function postOffer(maker: HavenoDaemon, config?: any) {
config.triggerPrice); config.triggerPrice);
testOffer(offer, config); 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 // offer is included in my offers only
if (!getOffer(await maker.getMyOffers(config.assetCode, config.direction), offer.getId())) { if (!getOffer(await maker.getMyOffers(config.assetCode, config.direction), offer.getId())) {
await wait(10000); 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"); 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; return offer;
} }
@ -1948,7 +2054,7 @@ async function testTradeChat(tradeId: string, alice: HavenoDaemon, bob: HavenoDa
offset = 1; // 1 existing notification offset = 1; // 1 existing notification
expect(chatNotifications.length).toBe(offset + msgs.length); expect(chatNotifications.length).toBe(offset + msgs.length);
expect(chatNotifications[0].getChatMessage()?.getMessage()).toEqual(aliceMsg); 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]); expect(chatNotifications[i + offset].getChatMessage()?.getMessage()).toEqual(msgs[i]);
} }
} }

View File

@ -679,7 +679,7 @@ class HavenoDaemon {
async getBalances(): Promise<XmrBalanceInfo> { async getBalances(): Promise<XmrBalanceInfo> {
let that = this; let that = this;
return new Promise(function(resolve, reject) { 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); if (err) reject(err);
else resolve(response.getBalances()!.getXmr()!); else resolve(response.getBalances()!.getXmr()!);
}); });
@ -1018,7 +1018,7 @@ class HavenoDaemon {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
that._tradesClient.takeOffer(request, {password: that._password}, function(err: grpcWeb.RpcError, response: TakeOfferReply) { that._tradesClient.takeOffer(request, {password: that._password}, function(err: grpcWeb.RpcError, response: TakeOfferReply) {
if (err) reject(err); 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()!); else resolve(response.getTrade()!);
}); });
}); });

View File

@ -6,19 +6,10 @@ const console = require('console');
*/ */
class HavenoUtils { class HavenoUtils {
static LOG_LEVEL = 0; static logLevel = 0;
static CENTINEROS_AU_MULTIPLIER = 10000; static centinerosToAUMultiplier = 10000;
static months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
/** static lastLogTimeMs = 0;
* 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);
}
/** /**
* Set the log level with 0 being least verbose. * Set the log level with 0 being least verbose.
@ -27,7 +18,7 @@ class HavenoUtils {
*/ */
static async setLogLevel(level: number) { static async setLogLevel(level: number) {
assert(level === parseInt(level + "", 10) && level >= 0, "Log level must be an integer >= 0"); 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 * @return {int} the current log level
*/ */
static getLogLevel(): number { 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 * @return {BigInt} the amount denominated in atomic units
*/ */
static centinerosToAtomicUnits(centineros: number): bigint { static centinerosToAtomicUnits(centineros: number): bigint {
return BigInt(centineros) * BigInt(HavenoUtils.CENTINEROS_AU_MULTIPLIER); return BigInt(centineros) * BigInt(HavenoUtils.centinerosToAUMultiplier);
} }
} }