From d33c3227e08f48c985e89984edf816ecdaeca4c3 Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 19 Nov 2021 17:25:49 -0500 Subject: [PATCH] test trade states throughout trade --- config/grpc.proto | 9 +-- src/HavenoDaemon.test.ts | 142 ++++++++++++++++++++++---------------- src/protobuf/grpc_pb.d.ts | 4 ++ src/protobuf/grpc_pb.js | 76 ++++++++++++++------ 4 files changed, 144 insertions(+), 87 deletions(-) diff --git a/config/grpc.proto b/config/grpc.proto index 0229cd52..49383129 100644 --- a/config/grpc.proto +++ b/config/grpc.proto @@ -570,10 +570,11 @@ message BtcBalanceInfo { } message XmrBalanceInfo { - uint64 unlocked_balance = 1 [jstype = JS_STRING]; - uint64 locked_balance = 2 [jstype = JS_STRING]; - uint64 reserved_offer_balance = 3 [jstype = JS_STRING]; - uint64 reserved_trade_balance = 4 [jstype = JS_STRING]; + uint64 balance = 1 [jstype = JS_STRING]; + uint64 unlocked_balance = 2 [jstype = JS_STRING]; + uint64 locked_balance = 3 [jstype = JS_STRING]; + uint64 reserved_offer_balance = 4 [jstype = JS_STRING]; + uint64 reserved_trade_balance = 5 [jstype = JS_STRING]; } message AddressBalanceInfo { diff --git a/src/HavenoDaemon.test.ts b/src/HavenoDaemon.test.ts index f9f88e31..3ac4ce4b 100644 --- a/src/HavenoDaemon.test.ts +++ b/src/HavenoDaemon.test.ts @@ -3,7 +3,7 @@ // import haveno types import {HavenoDaemon} from "./HavenoDaemon"; import {XmrBalanceInfo, OfferInfo, TradeInfo} from './protobuf/grpc_pb'; // TODO (woodser): better names; haveno_grpc_pb, haveno_pb -import {PaymentAccount} from './protobuf/pb_pb'; +import {PaymentAccount, Offer} from './protobuf/pb_pb'; // import monero-javascript const monerojs = require("monero-javascript"); // TODO (woodser): support typescript and `npm install @types/monero-javascript` in monero-javascript @@ -46,6 +46,7 @@ const moneroDaemonPassword = "abctesting123"; let monerod: any; // other test config +const MAX_FEE = BigInt("75000000000"); const WALLET_SYNC_PERIOD = 5000; const MAX_TIME_PEER_NOTICE = 3000; const TEST_CRYPTO_ACCOUNTS = [ // TODO (woodser): test other cryptos, fiat @@ -177,13 +178,13 @@ test("Can post and remove an offer", async () => { // wait for alice to have unlocked balance to post offer let tradeAmount: bigint = BigInt("250000000000"); - await waitForUnlockedBalance(tradeAmount * BigInt("5"), alice); + await waitForUnlockedBalance(tradeAmount * BigInt("2"), alice); // get unlocked balance before reserving funds for offer let unlockedBalanceBefore: bigint = BigInt((await alice.getBalances()).getUnlockedBalance()); // post offer - let offer: OfferInfo = await postOffer(); + let offer: OfferInfo = await postOffer(alice, "buy", BigInt("200000000000")); // cancel offer await alice.removeOffer(offer.getId()); @@ -199,7 +200,7 @@ test("Invalidates offers when reserved funds are spent", async () => { // wait for alice and bob to have unlocked balance for trade let tradeAmount: bigint = BigInt("250000000000"); - await waitForUnlockedBalance(tradeAmount * BigInt("5"), alice); + await waitForUnlockedBalance(tradeAmount * BigInt("2"), alice); // get frozen key images before posting offer let frozenKeyImagesBefore = []; @@ -207,7 +208,7 @@ test("Invalidates offers when reserved funds are spent", async () => { // post offer await wait(1000); - let offer: OfferInfo = await postOffer(); + let offer: OfferInfo = await postOffer(alice, "buy", tradeAmount); // get key images reserved by offer let reservedKeyImages = []; @@ -248,37 +249,45 @@ test("Invalidates offers when reserved funds are spent", async () => { await monerod.flushTxPool(tx.getHash()); }); +// TODO (woodser): test grpc notifications test("Can complete a trade", async () => { // wait for alice and bob to have unlocked balance for trade let tradeAmount: bigint = BigInt("250000000000"); - await waitForUnlockedBalance(tradeAmount * BigInt("5"), alice, bob); + await waitForUnlockedBalance(tradeAmount * BigInt("2"), alice, bob); + let aliceBalancesBefore = await alice.getBalances(); + let bobBalancesBefore: XmrBalanceInfo = await bob.getBalances(); - // create bob's ethereum payment account + // alice posts offer to buy xmr + console.log("Alice posting offer"); + let direction = "buy"; + let offer: OfferInfo = await postOffer(alice, direction, tradeAmount); + expect(offer.getState()).toEqual("AVAILABLE"); + console.log("Alice done posting offer"); + + // bob sees offer + await wait(WALLET_SYNC_PERIOD * 2); + let offerBob = getOffer(await bob.getOffers(direction), offer.getId()); + if (!offerBob) throw new Error("Offer " + offer.getId() + " was not found in peer's offers after posting"); + expect(offerBob.getState()).toEqual("UNKNOWN"); + + // bob creates ethereum payment account let testAccount = TEST_CRYPTO_ACCOUNTS[0]; let ethPaymentAccount: PaymentAccount = await bob.createCryptoPaymentAccount( testAccount.currencyCode + " " + testAccount.address.substr(0, 8) + "... " + GenUtils.getUUID(), testAccount.currencyCode, testAccount.address); - // alice posts offer to buy xmr - console.log("Alice posting offer"); - let offer: OfferInfo = await postOffer(); - console.log("Alice done posting offer"); - - // bob sees offer - await wait(WALLET_SYNC_PERIOD * 2); - if (!getOffer(await bob.getOffers("buy"), offer.getId())) throw new Error("Offer " + offer.getId() + " was not found in peer's offers after posting"); - // bob takes offer let startTime = Date.now(); - let bobBalancesBefore: XmrBalanceInfo = await bob.getBalances(); console.log("Bob taking offer"); let trade: TradeInfo = await bob.takeOffer(offer.getId(), ethPaymentAccount.getId()); // TODO (woodser): this returns before trade is fully initialized. this fails with bad error message if trade is not yet seen by peer + expect(trade.getPhase()).toEqual("DEPOSIT_PUBLISHED"); console.log("Bob done taking offer in " + (Date.now() - startTime) + " ms"); // bob can get trade let fetchedTrade: TradeInfo = await bob.getTrade(trade.getTradeId()); + expect(fetchedTrade.getPhase()).toEqual("DEPOSIT_PUBLISHED"); // TODO: test fetched trade // test bob's balances after taking trade @@ -293,31 +302,51 @@ test("Can complete a trade", async () => { // alice can get trade fetchedTrade = await alice.getTrade(trade.getTradeId()); + expect(fetchedTrade.getPhase()).toEqual("DEPOSIT_PUBLISHED"); // mine until deposit txs unlock console.log("Mining to unlock deposit txs"); await waitForUnlockedTxs(fetchedTrade.getMakerDepositTxId(), fetchedTrade.getTakerDepositTxId()); console.log("Done mining to unlock deposit txs"); - + // alice notified to send payment - await wait(WALLET_SYNC_PERIOD); + await wait(WALLET_SYNC_PERIOD * 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? + fetchedTrade = await bob.getTrade(trade.getTradeId()); + expect(fetchedTrade.getIsDepositConfirmed()).toBe(true); + expect(fetchedTrade.getPhase()).toEqual("DEPOSIT_CONFIRMED"); // alice indicates payment is sent await alice.confirmPaymentStarted(trade.getTradeId()); + fetchedTrade = await alice.getTrade(trade.getTradeId()); + expect(fetchedTrade.getPhase()).toEqual("FIAT_SENT"); // TODO (woodser): rename to PAYMENT_SENT // bob notified payment is sent await wait(MAX_TIME_PEER_NOTICE); + fetchedTrade = await bob.getTrade(trade.getTradeId()); + expect(fetchedTrade.getPhase()).toEqual("FIAT_SENT"); // TODO (woodser): rename to PAYMENT_SENT // bob confirms payment is received await bob.confirmPaymentReceived(trade.getTradeId()); - - // bob notified trade is complete fetchedTrade = await bob.getTrade(trade.getTradeId()); - console.log(fetchedTrade.getState()); // TODO (woodser): this should be complete state - - // test bob's balances after confirming payment + expect(fetchedTrade.getPhase()).toEqual("PAYOUT_PUBLISHED"); // alice notified trade is complete and of balance changes + await wait(WALLET_SYNC_PERIOD * 2); + fetchedTrade = await alice.getTrade(trade.getTradeId()); + expect(fetchedTrade.getPhase()).toEqual("PAYOUT_PUBLISHED"); + + // test balances after payout tx + let aliceBalancesAfter = await alice.getBalances(); + bobBalancesAfter = await bob.getBalances(); + let aliceFee = BigInt(aliceBalancesBefore.getBalance()) + tradeAmount - BigInt(aliceBalancesAfter.getBalance()); + let bobFee = BigInt(bobBalancesBefore.getBalance()) - tradeAmount - BigInt(bobBalancesAfter.getBalance()); + expect(aliceFee).toBeLessThanOrEqual(MAX_FEE); + expect(aliceFee).toBeGreaterThan(BigInt("0")); + expect(bobFee).toBeLessThanOrEqual(MAX_FEE); + expect(bobFee).toBeGreaterThan(BigInt("0")); }); // ------------------------------- HELPERS ------------------------------------ @@ -398,7 +427,7 @@ async function waitForUnlockedBalance(amount: bigint, ...wallets: any[]) { let unlockedBalance = await wallet.getUnlockedBalance(); if (unlockedBalance < amount) miningNeeded = true; let depositNeeded: bigint = amount - unlockedBalance - await wallet.getLockedBalance(); - if (depositNeeded > BigInt("0") && wallet._wallet !== fundingWallet) fundConfig.addDestination(await wallet.getDepositAddress(), depositNeeded); + if (depositNeeded > BigInt("0") && wallet._wallet !== fundingWallet) fundConfig.addDestination(await wallet.getDepositAddress(), depositNeeded * BigInt("10")); // deposit 10 times more than needed } if (fundConfig.getDestinations()) { await waitForUnlockedBalance(minimumFunding, fundingWallet); // TODO (woodser): wait for enough to cover tx amount + fee @@ -450,7 +479,7 @@ async function waitForUnlockedTxs(...txHashes: string[]) { async function startMining() { try { - await monerod.startMining(await fundingWallet.getPrimaryAddress(), 1); + await monerod.startMining(await fundingWallet.getPrimaryAddress(), 3); } catch (err) { if (err.message !== "Already mining") throw err; } @@ -460,56 +489,49 @@ async function wait(durationMs: number) { return new Promise(function(resolve) { setTimeout(resolve, durationMs); }); } -async function postOffer() { // TODO (woodser): postOffer(maker, peer) +async function postOffer(maker: HavenoDaemon, direction: string, amount: bigint) { - // test requires ethereum payment account - let ethPaymentAccount: PaymentAccount | undefined; - for (let paymentAccount of await alice.getPaymentAccounts()) { - if (paymentAccount.getSelectedTradeCurrency()?.getCode() === "ETH") { - ethPaymentAccount = paymentAccount; - break; - } - } - if (!ethPaymentAccount) throw new Error("Test requires ethereum payment account to post offer"); + // maker creates ethereum payment account + let testAccount = TEST_CRYPTO_ACCOUNTS[0]; + let ethPaymentAccount: PaymentAccount = await maker.createCryptoPaymentAccount( + testAccount.currencyCode + " " + testAccount.address.substr(0, 8) + "... " + GenUtils.getUUID(), + testAccount.currencyCode, + testAccount.address); // get unlocked balance before reserving offer - let unlockedBalanceBefore: bigint = BigInt((await alice.getBalances()).getUnlockedBalance()); + let unlockedBalanceBefore: bigint = BigInt((await maker.getBalances()).getUnlockedBalance()); // post offer - // TODO: don't define variables, just document in comments - let amount: bigint = BigInt("200000000000"); - let minAmount: bigint = BigInt("150000000000"); - let price: number = 12.378981; // TODO: price is optional? price string gets converted to long? - let useMarketBasedPrice: boolean = true; - let marketPriceMargin: number = 0.02; // within 2% - let buyerSecurityDeposit: number = 0.15; // 15% - let triggerPrice: number = 12; // TODO: fails if there is decimal, gets converted to long? - let paymentAccountId: string = ethPaymentAccount.getId(); - let offer: OfferInfo = await alice.postOffer("eth", - "buy", // buy xmr for eth - price, - useMarketBasedPrice, - marketPriceMargin, - amount, - minAmount, - buyerSecurityDeposit, - paymentAccountId, - triggerPrice); + let offer: OfferInfo = await maker.postOffer("eth", + direction, // buy or sell xmr for eth + 12.378981, // price TODO: price is optional? price string gets converted to long? + true, // use market price + 0.02, // market price margin, e.g. within 2% + amount, // amount + BigInt("150000000000"), // min amount + 0.15, // buyer security deposit, e.g. 15% + ethPaymentAccount.getId(), // payment account id + undefined); // trigger price // TODO: fails if there is a decimal, gets converted to long? testOffer(offer); // unlocked balance has decreased - let unlockedBalanceAfter: bigint = BigInt((await alice.getBalances()).getUnlockedBalance()); + 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 alice.getMyOffers("buy"), offer.getId())) throw new Error("Offer " + offer.getId() + " was not found in my offers"); - if (getOffer(await alice.getOffers("buy"), offer.getId())) throw new Error("My offer " + offer.getId() + " should not appear in available offers"); + if (!getOffer(await maker.getMyOffers(direction), offer.getId())) { + console.log("OK, we couldn't get the offer, let's wait"); + await wait(10000); + if (!getOffer(await maker.getMyOffers(direction), offer.getId())) throw new Error("Offer " + offer.getId() + " was not found in my offers"); + else console.log("The offer finally posted!"); + } + if (getOffer(await maker.getOffers(direction), offer.getId())) throw new Error("My offer " + offer.getId() + " should not appear in available offers"); return offer; } function getBalancesStr(balances: XmrBalanceInfo) { - return "[unlocked balance=" + balances.getUnlockedBalance() + ", locked balance=" + balances.getLockedBalance() + ", reserved offer balance=" + balances.getReservedOfferBalance() + ", reserved trade balance: " + balances.getReservedTradeBalance() + "]"; + return "[balance=" + balances.getBalance() + ", unlocked balance=" + balances.getUnlockedBalance() + ", locked balance=" + balances.getLockedBalance() + ", reserved offer balance=" + balances.getReservedOfferBalance() + ", reserved trade balance: " + balances.getReservedTradeBalance() + "]"; } function getOffer(offers: OfferInfo[], id: string): OfferInfo | undefined { diff --git a/src/protobuf/grpc_pb.d.ts b/src/protobuf/grpc_pb.d.ts index f4edf260..9a0bdbbb 100644 --- a/src/protobuf/grpc_pb.d.ts +++ b/src/protobuf/grpc_pb.d.ts @@ -1818,6 +1818,9 @@ export namespace BtcBalanceInfo { } export class XmrBalanceInfo extends jspb.Message { + getBalance(): string; + setBalance(value: string): XmrBalanceInfo; + getUnlockedBalance(): string; setUnlockedBalance(value: string): XmrBalanceInfo; @@ -1840,6 +1843,7 @@ export class XmrBalanceInfo extends jspb.Message { export namespace XmrBalanceInfo { export type AsObject = { + balance: string, unlockedBalance: string, lockedBalance: string, reservedOfferBalance: string, diff --git a/src/protobuf/grpc_pb.js b/src/protobuf/grpc_pb.js index df32af2f..34ed2ffa 100644 --- a/src/protobuf/grpc_pb.js +++ b/src/protobuf/grpc_pb.js @@ -15371,10 +15371,11 @@ proto.io.bisq.protobuffer.XmrBalanceInfo.prototype.toObject = function(opt_inclu */ proto.io.bisq.protobuffer.XmrBalanceInfo.toObject = function(includeInstance, msg) { var f, obj = { - unlockedBalance: jspb.Message.getFieldWithDefault(msg, 1, "0"), - lockedBalance: jspb.Message.getFieldWithDefault(msg, 2, "0"), - reservedOfferBalance: jspb.Message.getFieldWithDefault(msg, 3, "0"), - reservedTradeBalance: jspb.Message.getFieldWithDefault(msg, 4, "0") + balance: jspb.Message.getFieldWithDefault(msg, 1, "0"), + unlockedBalance: jspb.Message.getFieldWithDefault(msg, 2, "0"), + lockedBalance: jspb.Message.getFieldWithDefault(msg, 3, "0"), + reservedOfferBalance: jspb.Message.getFieldWithDefault(msg, 4, "0"), + reservedTradeBalance: jspb.Message.getFieldWithDefault(msg, 5, "0") }; if (includeInstance) { @@ -15413,17 +15414,21 @@ proto.io.bisq.protobuffer.XmrBalanceInfo.deserializeBinaryFromReader = function( switch (field) { case 1: var value = /** @type {string} */ (reader.readUint64String()); - msg.setUnlockedBalance(value); + msg.setBalance(value); break; case 2: var value = /** @type {string} */ (reader.readUint64String()); - msg.setLockedBalance(value); + msg.setUnlockedBalance(value); break; case 3: var value = /** @type {string} */ (reader.readUint64String()); - msg.setReservedOfferBalance(value); + msg.setLockedBalance(value); break; case 4: + var value = /** @type {string} */ (reader.readUint64String()); + msg.setReservedOfferBalance(value); + break; + case 5: var value = /** @type {string} */ (reader.readUint64String()); msg.setReservedTradeBalance(value); break; @@ -15456,42 +15461,49 @@ proto.io.bisq.protobuffer.XmrBalanceInfo.prototype.serializeBinary = function() */ proto.io.bisq.protobuffer.XmrBalanceInfo.serializeBinaryToWriter = function(message, writer) { var f = undefined; - f = message.getUnlockedBalance(); + f = message.getBalance(); if (parseInt(f, 10) !== 0) { writer.writeUint64String( 1, f ); } - f = message.getLockedBalance(); + f = message.getUnlockedBalance(); if (parseInt(f, 10) !== 0) { writer.writeUint64String( 2, f ); } - f = message.getReservedOfferBalance(); + f = message.getLockedBalance(); if (parseInt(f, 10) !== 0) { writer.writeUint64String( 3, f ); } - f = message.getReservedTradeBalance(); + f = message.getReservedOfferBalance(); if (parseInt(f, 10) !== 0) { writer.writeUint64String( 4, f ); } + f = message.getReservedTradeBalance(); + if (parseInt(f, 10) !== 0) { + writer.writeUint64String( + 5, + f + ); + } }; /** - * optional uint64 unlocked_balance = 1; + * optional uint64 balance = 1; * @return {string} */ -proto.io.bisq.protobuffer.XmrBalanceInfo.prototype.getUnlockedBalance = function() { +proto.io.bisq.protobuffer.XmrBalanceInfo.prototype.getBalance = function() { return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "0")); }; @@ -15500,16 +15512,16 @@ proto.io.bisq.protobuffer.XmrBalanceInfo.prototype.getUnlockedBalance = function * @param {string} value * @return {!proto.io.bisq.protobuffer.XmrBalanceInfo} returns this */ -proto.io.bisq.protobuffer.XmrBalanceInfo.prototype.setUnlockedBalance = function(value) { +proto.io.bisq.protobuffer.XmrBalanceInfo.prototype.setBalance = function(value) { return jspb.Message.setProto3StringIntField(this, 1, value); }; /** - * optional uint64 locked_balance = 2; + * optional uint64 unlocked_balance = 2; * @return {string} */ -proto.io.bisq.protobuffer.XmrBalanceInfo.prototype.getLockedBalance = function() { +proto.io.bisq.protobuffer.XmrBalanceInfo.prototype.getUnlockedBalance = function() { return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "0")); }; @@ -15518,16 +15530,16 @@ proto.io.bisq.protobuffer.XmrBalanceInfo.prototype.getLockedBalance = function() * @param {string} value * @return {!proto.io.bisq.protobuffer.XmrBalanceInfo} returns this */ -proto.io.bisq.protobuffer.XmrBalanceInfo.prototype.setLockedBalance = function(value) { +proto.io.bisq.protobuffer.XmrBalanceInfo.prototype.setUnlockedBalance = function(value) { return jspb.Message.setProto3StringIntField(this, 2, value); }; /** - * optional uint64 reserved_offer_balance = 3; + * optional uint64 locked_balance = 3; * @return {string} */ -proto.io.bisq.protobuffer.XmrBalanceInfo.prototype.getReservedOfferBalance = function() { +proto.io.bisq.protobuffer.XmrBalanceInfo.prototype.getLockedBalance = function() { return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, "0")); }; @@ -15536,16 +15548,16 @@ proto.io.bisq.protobuffer.XmrBalanceInfo.prototype.getReservedOfferBalance = fun * @param {string} value * @return {!proto.io.bisq.protobuffer.XmrBalanceInfo} returns this */ -proto.io.bisq.protobuffer.XmrBalanceInfo.prototype.setReservedOfferBalance = function(value) { +proto.io.bisq.protobuffer.XmrBalanceInfo.prototype.setLockedBalance = function(value) { return jspb.Message.setProto3StringIntField(this, 3, value); }; /** - * optional uint64 reserved_trade_balance = 4; + * optional uint64 reserved_offer_balance = 4; * @return {string} */ -proto.io.bisq.protobuffer.XmrBalanceInfo.prototype.getReservedTradeBalance = function() { +proto.io.bisq.protobuffer.XmrBalanceInfo.prototype.getReservedOfferBalance = function() { return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 4, "0")); }; @@ -15554,11 +15566,29 @@ proto.io.bisq.protobuffer.XmrBalanceInfo.prototype.getReservedTradeBalance = fun * @param {string} value * @return {!proto.io.bisq.protobuffer.XmrBalanceInfo} returns this */ -proto.io.bisq.protobuffer.XmrBalanceInfo.prototype.setReservedTradeBalance = function(value) { +proto.io.bisq.protobuffer.XmrBalanceInfo.prototype.setReservedOfferBalance = function(value) { return jspb.Message.setProto3StringIntField(this, 4, value); }; +/** + * optional uint64 reserved_trade_balance = 5; + * @return {string} + */ +proto.io.bisq.protobuffer.XmrBalanceInfo.prototype.getReservedTradeBalance = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 5, "0")); +}; + + +/** + * @param {string} value + * @return {!proto.io.bisq.protobuffer.XmrBalanceInfo} returns this + */ +proto.io.bisq.protobuffer.XmrBalanceInfo.prototype.setReservedTradeBalance = function(value) { + return jspb.Message.setProto3StringIntField(this, 5, value); +}; + +