test completed payout state

This commit is contained in:
woodser 2022-10-26 01:05:49 -04:00
parent a382b18a6d
commit 796f14c9ef
2 changed files with 100 additions and 34 deletions

View File

@ -54,7 +54,7 @@ const TestConfig = {
testDataDir: "./testdata", testDataDir: "./testdata",
haveno: { haveno: {
path: "../haveno", path: "../haveno",
version: "0.0.2" version: "1.0.0"
}, },
monerod: { monerod: {
url: "http://localhost:" + getNetworkStartPort() + "8081", // 18081, 28081, 38081 for mainnet, testnet, stagenet respectively url: "http://localhost:" + getNetworkStartPort() + "8081", // 18081, 28081, 38081 for mainnet, testnet, stagenet respectively
@ -170,7 +170,7 @@ const TestConfig = {
buyerSendsPayment: true, buyerSendsPayment: true,
sellerReceivesPayment: true, sellerReceivesPayment: true,
arbitrator: {} as HavenoClient, // test arbitrator state (does not choose arbitrator). assigned to default arbitrator before all tests arbitrator: {} as HavenoClient, // test arbitrator state (does not choose arbitrator). assigned to default arbitrator before all tests
resolveDispute: true, // resolve dispute after opening, resolveDispute: true, // resolve dispute after opening
disputeWinner: DisputeResult.Winner.SELLER, disputeWinner: DisputeResult.Winner.SELLER,
disputeReason: DisputeResult.Reason.PEER_WAS_LATE, disputeReason: DisputeResult.Reason.PEER_WAS_LATE,
disputeSummary: "Seller is winner" disputeSummary: "Seller is winner"
@ -214,6 +214,7 @@ interface TradeConfig {
testTraderChat?: boolean, testTraderChat?: boolean,
buyer?: HavenoClient, buyer?: HavenoClient,
seller?: HavenoClient, seller?: HavenoClient,
tradeStarted?: boolean,
// resolve dispute config // resolve dispute config
resolveDispute?: boolean resolveDispute?: boolean
@ -1266,47 +1267,42 @@ test("Can resolve disputes", async () => {
HavenoUtils.log(1, "Opening disputes"); HavenoUtils.log(1, "Opening disputes");
const trade2 = await user1.getTrade(tradeIds[2]); const trade2 = await user1.getTrade(tradeIds[2]);
const trade3 = await user1.getTrade(tradeIds[3]); const trade3 = await user1.getTrade(tradeIds[3]);
const disputeConfigs: TradeConfig[] = [{ Object.assign(configs[0], {
offerId: tradeIds[0],
takeOffer: false,
resolveDispute: false, resolveDispute: false,
sellerOpensDisputeAfterDepositsUnlock: true, sellerOpensDisputeAfterDepositsUnlock: true,
disputeWinner: DisputeResult.Winner.SELLER, disputeWinner: DisputeResult.Winner.SELLER,
disputeReason: DisputeResult.Reason.PEER_WAS_LATE, disputeReason: DisputeResult.Reason.PEER_WAS_LATE,
disputeSummary: "Seller is winner" disputeSummary: "Seller is winner"
}, { });
offerId: tradeIds[1], Object.assign(configs[1], {
takeOffer: false,
resolveDispute: false, resolveDispute: false,
buyerOpensDisputeAfterDepositsUnlock: true, buyerOpensDisputeAfterDepositsUnlock: true,
disputeWinner: DisputeResult.Winner.BUYER, disputeWinner: DisputeResult.Winner.BUYER,
disputeReason: DisputeResult.Reason.SELLER_NOT_RESPONDING, disputeReason: DisputeResult.Reason.SELLER_NOT_RESPONDING,
disputeSummary: "Buyer is winner" disputeSummary: "Buyer is winner"
}, { });
offerId: tradeIds[2], Object.assign(configs[2], {
takeOffer: false,
resolveDispute: false, resolveDispute: false,
sellerOpensDisputeAfterDepositsUnlock: true, sellerOpensDisputeAfterDepositsUnlock: true,
disputeWinner: DisputeResult.Winner.BUYER, disputeWinner: DisputeResult.Winner.BUYER,
disputeReason: DisputeResult.Reason.WRONG_SENDER_ACCOUNT, disputeReason: DisputeResult.Reason.WRONG_SENDER_ACCOUNT,
disputeSummary: "Split trade amount", disputeSummary: "Split trade amount",
disputeWinnerAmount: BigInt(trade2.getAmountAsLong()) / BigInt(2) + HavenoUtils.centinerosToAtomicUnits(trade2.getOffer()!.getBuyerSecurityDeposit()) disputeWinnerAmount: BigInt(trade2.getAmountAsLong()) / BigInt(2) + HavenoUtils.centinerosToAtomicUnits(trade2.getOffer()!.getBuyerSecurityDeposit())
}, { });
offerId: tradeIds[3], Object.assign(configs[3], {
takeOffer: false,
resolveDispute: false, resolveDispute: false,
buyerOpensDisputeAfterDepositsUnlock: true, buyerOpensDisputeAfterDepositsUnlock: true,
disputeWinner: DisputeResult.Winner.SELLER, disputeWinner: DisputeResult.Winner.SELLER,
disputeReason: DisputeResult.Reason.TRADE_ALREADY_SETTLED, disputeReason: DisputeResult.Reason.TRADE_ALREADY_SETTLED,
disputeSummary: "Seller gets everything", disputeSummary: "Seller gets everything",
disputeWinnerAmount: BigInt(trade3.getAmountAsLong()) + HavenoUtils.centinerosToAtomicUnits(trade3.getOffer()!.getBuyerSecurityDeposit() + trade3.getOffer()!.getSellerSecurityDeposit()) disputeWinnerAmount: BigInt(trade3.getAmountAsLong()) + HavenoUtils.centinerosToAtomicUnits(trade3.getOffer()!.getBuyerSecurityDeposit() + trade3.getOffer()!.getSellerSecurityDeposit())
}]; });
await executeTrades(disputeConfigs); await executeTrades(configs);
// resolve disputes // resolve disputes
HavenoUtils.log(1, "Resolving disputes"); HavenoUtils.log(1, "Resolving disputes");
for (const config of disputeConfigs) config.resolveDispute = true; for (const config of configs) config.resolveDispute = true;
await executeTrades(disputeConfigs, false); // resolve in sequence to test balances before and after await executeTrades(configs, false); // resolve in sequence to test balances before and after
}); });
test("Cannot make or take offer with insufficient unlocked funds", async () => { test("Cannot make or take offer with insufficient unlocked funds", async () => {
@ -1729,8 +1725,12 @@ async function executeTrade(config?: TradeConfig): Promise<string> {
// take offer or get existing trade // take offer or get existing trade
let trade: TradeInfo|undefined = undefined; let trade: TradeInfo|undefined = undefined;
if (config.takeOffer) trade = await takeOffer(config); if (config.tradeStarted) trade = await config.taker!.getTrade(config.offerId!);
else trade = await config.taker!.getTrade(config.offerId!); else {
if (!config.takeOffer) return config.offerId!;
trade = await takeOffer(config);
config.tradeStarted = true;
}
// test trader chat // test trader chat
if (config.testTraderChat) await testTradeChat(trade.getTradeId(), config.maker!, config.taker!); if (config.testTraderChat) await testTradeChat(trade.getTradeId(), config.maker!, config.taker!);
@ -1757,7 +1757,7 @@ async function executeTrade(config?: TradeConfig): Promise<string> {
await waitForUnlockedTxs(trade.getMakerDepositTxId(), trade.getTakerDepositTxId()); await waitForUnlockedTxs(trade.getMakerDepositTxId(), trade.getTakerDepositTxId());
// buyer comes online if offline // buyer comes online if offline
if (config.buyerOfflineAfterPaymentSent) { if (config.buyerOfflineAfterTake) {
config.buyer = await initHaveno({appName: buyerAppName}); config.buyer = await initHaveno({appName: buyerAppName});
if (isBuyerMaker) config.maker = config.buyer; if (isBuyerMaker) config.maker = config.buyer;
else config.taker = config.buyer; else config.taker = config.buyer;
@ -1824,13 +1824,20 @@ async function executeTrade(config?: TradeConfig): Promise<string> {
if (config.sellerOfflineAfterTake) await wait(TestConfig.walletSyncPeriodMs); // wait to process mailbox messages if (config.sellerOfflineAfterTake) await wait(TestConfig.walletSyncPeriodMs); // wait to process mailbox messages
fetchedTrade = await config.seller.getTrade(trade.getTradeId()); fetchedTrade = await config.seller.getTrade(trade.getTradeId());
expect(fetchedTrade.getPhase()).toEqual("PAYMENT_SENT"); expect(fetchedTrade.getPhase()).toEqual("PAYMENT_SENT");
expect(fetchedTrade.getPayoutState()).toEqual("UNPUBLISHED");
// seller confirms payment is received // seller confirms payment is received
if (!config.sellerReceivesPayment) return offer!.getId(); if (!config.sellerReceivesPayment) return offer!.getId();
HavenoUtils.log(1, "Seller confirming payment received"); HavenoUtils.log(1, "Seller confirming payment received");
await config.seller.confirmPaymentReceived(trade.getTradeId()); await config.seller.confirmPaymentReceived(trade.getTradeId());
fetchedTrade = await config.seller.getTrade(trade.getTradeId()); fetchedTrade = await config.seller.getTrade(trade.getTradeId());
expect(fetchedTrade.getPhase()).toEqual(config.sellerOfflineAfterTake ? "PAYMENT_RECEIVED" : "PAYOUT_PUBLISHED"); // payout can only be published if seller remained online after first confirmation to share updated multisig info expect(fetchedTrade.getPhase()).toEqual("PAYMENT_RECEIVED");
expect(fetchedTrade.getPayoutState()).toEqual(config.sellerOfflineAfterTake ? "UNPUBLISHED" : "PUBLISHED"); // payout published iff seller remained online after first confirmation to share updated multisig info
// payout tx is published by buyer (priority) or arbitrator
await wait(TestConfig.walletSyncPeriodMs);
await testTradeState(await config.seller!.getTrade(trade.getTradeId()), "PAYMENT_RECEIVED", ["PUBLISHED", "CONFIRMED", "UNLOCKED"], false, true);
await testTradeState(await config.arbitrator!.getTrade(trade.getTradeId()), "COMPLETED", ["PUBLISHED", "CONFIRMED", "UNLOCKED"], true, true); // arbitrator trade auto completes
// buyer comes online if offline // buyer comes online if offline
if (config.buyerOfflineAfterPaymentSent) { if (config.buyerOfflineAfterPaymentSent) {
@ -1838,18 +1845,19 @@ async function executeTrade(config?: TradeConfig): Promise<string> {
if (isBuyerMaker) config.maker = config.buyer; if (isBuyerMaker) config.maker = config.buyer;
else config.taker = config.buyer; else config.taker = config.buyer;
HavenoUtils.log(1, "Done starting buyer"); HavenoUtils.log(1, "Done starting buyer");
await wait(TestConfig.maxWalletStartupMs + TestConfig.walletSyncPeriodMs);
} }
// test notifications // test trade state
await wait(TestConfig.maxWalletStartupMs + TestConfig.walletSyncPeriodMs * 2); await testTradeState(await config.buyer!.getTrade(trade.getTradeId()), "PAYMENT_RECEIVED", ["PUBLISHED", "CONFIRMED", "UNLOCKED"], false, true);
fetchedTrade = await config.buyer!.getTrade(trade.getTradeId()); await testTradeState(await config.seller!.getTrade(trade.getTradeId()), "PAYMENT_RECEIVED", ["PUBLISHED", "CONFIRMED", "UNLOCKED"], false, true);
expect(fetchedTrade.getPhase()).toEqual("PAYOUT_PUBLISHED"); // TODO: this should be WITHDRAW_COMPLETED? await testTradeState(await config.arbitrator!.getTrade(trade.getTradeId()), "COMPLETED", ["PUBLISHED", "CONFIRMED", "UNLOCKED"], true, true);
fetchedTrade = await config.seller.getTrade(trade.getTradeId());
expect(fetchedTrade.getPhase()).toEqual("PAYOUT_PUBLISHED");
const arbitratorTrade = await config.arbitrator!.getTrade(trade.getTradeId());
expect(arbitratorTrade.getState()).toEqual("WITHDRAW_COMPLETED");
// TODO: traders mark trades as complete // test trade completion
await config.buyer!.completeTrade(trade.getTradeId());
await testTradeState(await config.buyer!.getTrade(trade.getTradeId()), "COMPLETED", ["PUBLISHED", "CONFIRMED", "UNLOCKED"], true, true);
await config.seller!.completeTrade(trade.getTradeId());
await testTradeState(await config.buyer!.getTrade(trade.getTradeId()), "COMPLETED", ["PUBLISHED", "CONFIRMED", "UNLOCKED"], true, true);
// test balances after payout tx unless other trades can interfere // test balances after payout tx unless other trades can interfere
if (!config.concurrentTrades) { if (!config.concurrentTrades) {
@ -1864,9 +1872,43 @@ async function executeTrade(config?: TradeConfig): Promise<string> {
expect(sellerFee).toBeGreaterThan(BigInt("0")); expect(sellerFee).toBeGreaterThan(BigInt("0"));
} }
// mine at least one block
const height = await monerod.getHeight();
await mineToHeight(height + 1);
await wait(TestConfig.maxWalletStartupMs + TestConfig.walletSyncPeriodMs * 2);
fetchedTrade = await config.buyer!.getTrade(trade.getTradeId());
assert(GenUtils.arrayContains(["CONFIRMED", "UNLOCKED"], fetchedTrade.getPayoutState()));
fetchedTrade = await config.seller!.getTrade(trade.getTradeId());
assert(GenUtils.arrayContains(["CONFIRMED", "UNLOCKED"], fetchedTrade.getPayoutState()));
// mine until unlock
await mineToHeight(height + 10);
await wait(TestConfig.walletSyncPeriodMs * 2);
fetchedTrade = await config.buyer!.getTrade(trade.getTradeId());
assert(GenUtils.arrayContains(["UNLOCKED"], fetchedTrade.getPayoutState()));
fetchedTrade = await config.seller!.getTrade(trade.getTradeId());
assert(GenUtils.arrayContains(["UNLOCKED"], fetchedTrade.getPayoutState()));
return offer!.getId(); return offer!.getId();
} }
async function mineToHeight(height: number) {
if (await monerod.getHeight() >= height) return;
const miningStarted = await startMining();
while (await monerod.getHeight() < height) {
await GenUtils.waitFor(TestConfig.walletSyncPeriodMs);
}
if (miningStarted) await stopMining();
}
async function testTradeState(trade: TradeInfo, phase: string, payoutStates: string[], isCompleted: boolean, isPayoutPublished: boolean) {
expect(trade.getPhase()).toEqual(phase);
assert(GenUtils.arrayContains(payoutStates, trade.getPayoutState()));
expect(trade.getIsCompleted()).toEqual(isCompleted);
expect(trade.getIsPayoutPublished()).toEqual(isPayoutPublished);
//expect(trade.getIsPayoutConfirmed()).toEqual(isPayoutPublished); // TODO
}
async function makeOffer(config?: TradeConfig): Promise<OfferInfo> { async function makeOffer(config?: TradeConfig): Promise<OfferInfo> {
// assign default config // assign default config
@ -2104,6 +2146,12 @@ async function resolveDispute(config: TradeConfig) {
dispute = await config.disputePeer!.getDispute(config.offerId!); dispute = await config.disputePeer!.getDispute(config.offerId!);
expect(dispute.getIsClosed()).toBe(true); expect(dispute.getIsClosed()).toBe(true);
// test trade state
await wait(TestConfig.maxWalletStartupMs + TestConfig.walletSyncPeriodMs * 2);
expect((await config.buyer!.getTrade(config.offerId!)).getPhase()).toEqual("COMPLETED");
expect((await config.seller!.getTrade(config.offerId!)).getPhase()).toEqual("COMPLETED");
expect((await config.arbitrator!.getTrade(config.offerId!)).getPhase()).toEqual("COMPLETED");
// check balances after payout tx unless concurrent trades // check balances after payout tx unless concurrent trades
if (config.concurrentTrades) return; if (config.concurrentTrades) return;
await wait(TestConfig.walletSyncPeriodMs * 2); await wait(TestConfig.walletSyncPeriodMs * 2);

View File

@ -4,7 +4,7 @@ import HavenoUtils from "./utils/HavenoUtils";
import TaskLooper from "./utils/TaskLooper"; import TaskLooper from "./utils/TaskLooper";
import type * as grpcWeb from "grpc-web"; import type * as grpcWeb from "grpc-web";
import { GetVersionClient, AccountClient, MoneroConnectionsClient, DisputesClient, DisputeAgentsClient, NotificationsClient, WalletsClient, PriceClient, OffersClient, PaymentAccountsClient, TradesClient, ShutdownServerClient, MoneroNodeClient } from './protobuf/GrpcServiceClientPb'; import { GetVersionClient, AccountClient, MoneroConnectionsClient, DisputesClient, DisputeAgentsClient, NotificationsClient, WalletsClient, PriceClient, OffersClient, PaymentAccountsClient, TradesClient, ShutdownServerClient, MoneroNodeClient } from './protobuf/GrpcServiceClientPb';
import { GetVersionRequest, GetVersionReply, IsAppInitializedRequest, IsAppInitializedReply, RegisterDisputeAgentRequest, UnregisterDisputeAgentRequest, MarketPriceRequest, MarketPriceReply, MarketPricesRequest, MarketPricesReply, MarketPriceInfo, MarketDepthRequest, MarketDepthReply, MarketDepthInfo, GetBalancesRequest, GetBalancesReply, XmrBalanceInfo, GetMyOfferRequest, GetMyOfferReply, GetOffersRequest, GetOffersReply, OfferInfo, GetPaymentMethodsRequest, GetPaymentMethodsReply, GetPaymentAccountFormRequest, CreatePaymentAccountRequest, ValidateFormFieldRequest, CreatePaymentAccountReply, GetPaymentAccountFormReply, GetPaymentAccountsRequest, GetPaymentAccountsReply, CreateCryptoCurrencyPaymentAccountRequest, CreateCryptoCurrencyPaymentAccountReply, CreateOfferRequest, CreateOfferReply, CancelOfferRequest, TakeOfferRequest, TakeOfferReply, TradeInfo, GetTradeRequest, GetTradeReply, GetTradesRequest, GetTradesReply, GetXmrSeedRequest, GetXmrSeedReply, GetXmrPrimaryAddressRequest, GetXmrPrimaryAddressReply, GetXmrNewSubaddressRequest, GetXmrNewSubaddressReply, ConfirmPaymentStartedRequest, ConfirmPaymentReceivedRequest, XmrTx, GetXmrTxsRequest, GetXmrTxsReply, XmrDestination, CreateXmrTxRequest, CreateXmrTxReply, RelayXmrTxRequest, RelayXmrTxReply, CreateAccountRequest, AccountExistsRequest, AccountExistsReply, DeleteAccountRequest, OpenAccountRequest, IsAccountOpenRequest, IsAccountOpenReply, CloseAccountRequest, ChangePasswordRequest, BackupAccountRequest, BackupAccountReply, RestoreAccountRequest, StopRequest, NotificationMessage, RegisterNotificationListenerRequest, SendNotificationRequest, UrlConnection, AddConnectionRequest, RemoveConnectionRequest, GetConnectionRequest, GetConnectionsRequest, SetConnectionRequest, CheckConnectionRequest, CheckConnectionsReply, CheckConnectionsRequest, StartCheckingConnectionsRequest, StopCheckingConnectionsRequest, GetBestAvailableConnectionRequest, SetAutoSwitchRequest, CheckConnectionReply, GetConnectionsReply, GetConnectionReply, GetBestAvailableConnectionReply, GetDisputeRequest, GetDisputeReply, GetDisputesRequest, GetDisputesReply, OpenDisputeRequest, ResolveDisputeRequest, SendDisputeChatMessageRequest, SendChatMessageRequest, GetChatMessagesRequest, GetChatMessagesReply, StartMoneroNodeRequest, StopMoneroNodeRequest, IsMoneroNodeOnlineRequest, IsMoneroNodeOnlineReply, GetMoneroNodeSettingsRequest, GetMoneroNodeSettingsReply } from "./protobuf/grpc_pb"; import { GetVersionRequest, GetVersionReply, IsAppInitializedRequest, IsAppInitializedReply, RegisterDisputeAgentRequest, UnregisterDisputeAgentRequest, MarketPriceRequest, MarketPriceReply, MarketPricesRequest, MarketPricesReply, MarketPriceInfo, MarketDepthRequest, MarketDepthReply, MarketDepthInfo, GetBalancesRequest, GetBalancesReply, XmrBalanceInfo, GetMyOfferRequest, GetMyOfferReply, GetOffersRequest, GetOffersReply, OfferInfo, GetPaymentMethodsRequest, GetPaymentMethodsReply, GetPaymentAccountFormRequest, CreatePaymentAccountRequest, ValidateFormFieldRequest, CreatePaymentAccountReply, GetPaymentAccountFormReply, GetPaymentAccountsRequest, GetPaymentAccountsReply, CreateCryptoCurrencyPaymentAccountRequest, CreateCryptoCurrencyPaymentAccountReply, CreateOfferRequest, CreateOfferReply, CancelOfferRequest, TakeOfferRequest, TakeOfferReply, TradeInfo, GetTradeRequest, GetTradeReply, GetTradesRequest, GetTradesReply, GetXmrSeedRequest, GetXmrSeedReply, GetXmrPrimaryAddressRequest, GetXmrPrimaryAddressReply, GetXmrNewSubaddressRequest, GetXmrNewSubaddressReply, ConfirmPaymentStartedRequest, ConfirmPaymentReceivedRequest, CompleteTradeRequest, XmrTx, GetXmrTxsRequest, GetXmrTxsReply, XmrDestination, CreateXmrTxRequest, CreateXmrTxReply, RelayXmrTxRequest, RelayXmrTxReply, CreateAccountRequest, AccountExistsRequest, AccountExistsReply, DeleteAccountRequest, OpenAccountRequest, IsAccountOpenRequest, IsAccountOpenReply, CloseAccountRequest, ChangePasswordRequest, BackupAccountRequest, BackupAccountReply, RestoreAccountRequest, StopRequest, NotificationMessage, RegisterNotificationListenerRequest, SendNotificationRequest, UrlConnection, AddConnectionRequest, RemoveConnectionRequest, GetConnectionRequest, GetConnectionsRequest, SetConnectionRequest, CheckConnectionRequest, CheckConnectionsReply, CheckConnectionsRequest, StartCheckingConnectionsRequest, StopCheckingConnectionsRequest, GetBestAvailableConnectionRequest, SetAutoSwitchRequest, CheckConnectionReply, GetConnectionsReply, GetConnectionReply, GetBestAvailableConnectionReply, GetDisputeRequest, GetDisputeReply, GetDisputesRequest, GetDisputesReply, OpenDisputeRequest, ResolveDisputeRequest, SendDisputeChatMessageRequest, SendChatMessageRequest, GetChatMessagesRequest, GetChatMessagesReply, StartMoneroNodeRequest, StopMoneroNodeRequest, IsMoneroNodeOnlineRequest, IsMoneroNodeOnlineReply, GetMoneroNodeSettingsRequest, GetMoneroNodeSettingsReply } from "./protobuf/grpc_pb";
import { PaymentMethod, PaymentAccountForm, PaymentAccountFormField, PaymentAccount, AvailabilityResult, Attachment, DisputeResult, Dispute, ChatMessage, MoneroNodeSettings } from "./protobuf/pb_pb"; import { PaymentMethod, PaymentAccountForm, PaymentAccountFormField, PaymentAccount, AvailabilityResult, Attachment, DisputeResult, Dispute, ChatMessage, MoneroNodeSettings } from "./protobuf/pb_pb";
/** /**
@ -1354,6 +1354,24 @@ export default class HavenoClient {
} }
} }
/**
* Acknowledge that a trade has completed.
*
* @param {string} tradeId - the id of the trade
*/
async completeTrade(tradeId: string): Promise<void> {
try {
await new Promise<void>((resolve, reject) => {
this._tradesClient.completeTrade(new CompleteTradeRequest().setTradeId(tradeId), {password: this._password}, function(err: grpcWeb.RpcError) {
if (err) reject(err);
else resolve();
});
});
} catch (e: any) {
throw new HavenoError(e.message, e.code);
}
}
/** /**
* Get all chat messages for a trade. * Get all chat messages for a trade.
* *