test resolving dispute while traders go offline

remove duplicate state from TradeContext
This commit is contained in:
woodser 2023-01-23 16:17:00 -05:00
parent 3465eb5ce6
commit 83d2a9123d

View File

@ -204,6 +204,8 @@ interface TradeContext {
buyerOfflineAfterTake?: boolean,
sellerOfflineAfterTake?: boolean,
buyerOfflineAfterPaymentSent?: boolean
buyerOfflineAfterDisputeOpened?: boolean,
sellerOfflineAfterDisputeOpened?: boolean,
sellerDisputeContext?: DisputeContext,
buyerDisputeContext?: DisputeContext,
buyerSendsPayment?: boolean,
@ -232,16 +234,14 @@ interface TradeContext {
// resolve dispute
resolveDispute?: boolean
arbitrator?: HavenoClient,
disputeOpener?: HavenoClient
disputePeer?: HavenoClient
disputeOpener?: SaleRole,
disputeWinner?: DisputeResult.Winner,
disputeReason?: DisputeResult.Reason,
disputeSummary?: string,
disputeWinnerAmount?: bigint
// other context
buyer?: HavenoClient,
seller?: HavenoClient,
offer?: OfferInfo,
index?: number,
isOfferTaken?: boolean,
isPaymentSent?: boolean,
@ -258,12 +258,20 @@ interface TradeContext {
maxConcurrency?: number,
walletSyncPeriodMs?: number,
maxTimePeerNoticeMs?: number,
stopOnFailure?: boolean
stopOnFailure?: boolean,
buyerAppName?: string,
sellerAppName?: string,
usedPorts?: string[]
}
enum TradeRole {
MAKER = "MAKER",
TAKER = "TAKER",
MAKER = "MAKER",
TAKER = "TAKER",
}
enum SaleRole {
BUYER = "BUYER",
SELLER = "SELLER"
}
enum DisputeContext {
@ -1320,7 +1328,6 @@ test("Can go offline while completing a trade (CI, sanity check)", async () => {
let traders: HavenoClient[] = [];
let ctx: TradeContext = {};
let err: any;
let miningStarted = false;
try {
// start and fund 2 trader processes
@ -1339,19 +1346,55 @@ test("Can go offline while completing a trade (CI, sanity check)", async () => {
ctx.buyerOfflineAfterPaymentSent = true;
// execute trade
miningStarted = await startMining();
await executeTrade(ctx);
} catch (e) {
err = e;
}
// stop traders
if (miningStarted) await stopMining();
if (ctx.maker) await releaseHavenoProcess(ctx.maker, true);
if (ctx.taker) await releaseHavenoProcess(ctx.taker, true);
if (err) throw err;
});
test("Can go offline while resolving disputes", async () => {
let traders: HavenoClient[] = [];
let ctx: TradeContext = {};
let err: any;
try {
// start and fund 2 trader processes
HavenoUtils.log(1, "Starting trader processes");
traders = await initHavenos(2);
HavenoUtils.log(1, "Funding traders");
const tradeAmount = BigInt("250000000000");
await waitForAvailableBalance(tradeAmount * BigInt("2"), ...traders);
// create trade config
ctx = Object.assign(getTradeContexts(1)[0], {
maker: traders[0],
taker: traders[1],
buyerOfflineAfterTake: true,
sellerOfflineAfterDisputeOpened: true,
buyerOfflineAfterDisputeOpened: false,
sellerDisputeContext: DisputeContext.OPEN_AFTER_DEPOSITS_UNLOCK,
disputeWinner: DisputeResult.Winner.SELLER,
disputeReason: DisputeResult.Reason.NO_REPLY,
disputeSummary: "Seller wins dispute because buyer has not replied",
});
// execute trade
await executeTrade(ctx);
} catch (e) {
err = e;
}
// stop and delete traders
await releaseHavenoProcess(ctx.maker!, true);
deleteHavenoInstanceByAppName(ctx.sellerAppName!); // seller is offline
if (err) throw err;
});
test("Can resolve disputes (CI)", async () => {
// take trades but stop before sending payment
@ -1720,6 +1763,28 @@ test("Selects arbitrators which are online, registered, and least used", async (
// ----------------------------- TEST HELPERS ---------------------------------
function getBuyer(ctx: TradeContext) {
return ctx.direction?.toUpperCase() === "BUY" ? ctx.maker : ctx.taker;
}
function getSeller(ctx: TradeContext) {
return ctx.direction?.toUpperCase() === "SELL" ? ctx.maker : ctx.taker;
}
function isBuyerMaker(ctx: TradeContext) {
return ctx.direction?.toUpperCase() === "BUY";
}
function getDisputeOpener(ctx: TradeContext) {
if (!ctx.disputeOpener) return undefined;
return ctx.disputeOpener === SaleRole.BUYER ? getBuyer(ctx) : getSeller(ctx);
}
function getDisputePeer(ctx: TradeContext) {
if (!ctx.disputeOpener) return undefined;
return ctx.disputeOpener === SaleRole.BUYER ? getSeller(ctx) : getBuyer(ctx);
}
function getTradeContexts(numConfigs: number): TradeContext[] {
const configs: TradeContext[] = [];
for (let i = 0; i < numConfigs; i++) configs.push({});
@ -1734,10 +1799,10 @@ function tradeContextToString(ctx: TradeContext) {
arbitrator: ctx.arbitrator ? ctx.arbitrator.getUrl() : undefined,
maker: ctx.maker ? ctx.maker.getUrl() : undefined,
taker: ctx.taker ? ctx.taker.getUrl() : undefined,
buyer: ctx.buyer ? ctx.buyer.getUrl() : undefined,
seller: ctx.seller ? ctx.seller.getUrl() : undefined,
disputeOpener: ctx.disputeOpener ? ctx.disputeOpener.getUrl() : undefined,
disputePeer: ctx.disputePeer ? ctx.disputePeer.getUrl() : undefined,
buyer: getBuyer(ctx) ? getBuyer(ctx)?.getUrl() : undefined,
seller: getSeller(ctx) ? getSeller(ctx)?.getUrl() : undefined,
disputeOpener: ctx.maker ? getDisputeOpener(ctx)?.getUrl() : undefined,
disputePeer: ctx.maker ? getDisputePeer(ctx)?.getUrl() : undefined,
disputeWinner: ctx.disputeWinner === DisputeResult.Winner.BUYER ? "buyer" : "seller"
}));
}
@ -1833,32 +1898,22 @@ async function executeTrade(ctx?: TradeContext): Promise<string> {
await waitForAvailableBalance(ctx.amount! * BigInt("2"), ...clientsToFund);
}
// determine buyer and seller
let offer: OfferInfo|undefined = undefined;
let isBuyerMaker = false;
if (makingOffer) {
isBuyerMaker = "buy" === ctx.direction!.toLowerCase();
} else {
offer = getOffer(await ctx.maker!.getMyOffers(ctx.assetCode!, ctx.direction), ctx.offerId!);
if (!offer) {
const trade = await ctx.maker!.getTrade(ctx.offerId!);
offer = trade.getOffer();
}
isBuyerMaker = "buy" === offer!.getDirection().toLowerCase();
}
ctx.buyer = isBuyerMaker ? ctx.maker : ctx.taker;
ctx.seller = isBuyerMaker ? ctx.taker : ctx.maker;
// get info before trade
const buyerBalancesBefore = await ctx.buyer!.getBalances();
const sellerBalancesBefore = await ctx.seller!.getBalances();
const buyerBalancesBefore = await getBuyer(ctx)!.getBalances();
const sellerBalancesBefore = await getSeller(ctx)!.getBalances();
// make offer if configured
if (makingOffer) {
offer = await makeOffer(ctx);
expect(offer.getState()).toEqual("AVAILABLE");
ctx.offerId = offer.getId();
ctx.offer = await makeOffer(ctx);
expect(ctx.offer.getState()).toEqual("AVAILABLE");
ctx.offerId = ctx.offer.getId();
await wait(ctx.maxTimePeerNoticeMs! + ctx.walletSyncPeriodMs! * 2);
} else {
ctx.offer = getOffer(await ctx.maker!.getMyOffers(ctx.assetCode!, ctx.direction), ctx.offerId!);
if (!ctx.offer) {
const trade = await ctx.maker!.getTrade(ctx.offerId!);
ctx.offer = trade.getOffer();
}
}
// TODO (woodser): test error message taking offer before posted
@ -1876,31 +1931,29 @@ async function executeTrade(ctx?: TradeContext): Promise<string> {
if (ctx.testTraderChat) await testTradeChat(ctx);
// get expected payment account payloads
let expectedBuyerPaymentAccountPayload = (await ctx.buyer?.getPaymentAccount(ctx.maker == ctx.buyer ? ctx.makerPaymentAccountId! : ctx.takerPaymentAccountId!))?.getPaymentAccountPayload();
let expectedSellerPaymentAccountPayload = (await ctx.seller?.getPaymentAccount(ctx.maker == ctx.buyer ? ctx.takerPaymentAccountId! : ctx.makerPaymentAccountId!))?.getPaymentAccountPayload();
let expectedBuyerPaymentAccountPayload = (await getBuyer(ctx)?.getPaymentAccount(ctx.maker == getBuyer(ctx) ? ctx.makerPaymentAccountId! : ctx.takerPaymentAccountId!))?.getPaymentAccountPayload();
let expectedSellerPaymentAccountPayload = (await getSeller(ctx)?.getPaymentAccount(ctx.maker == getBuyer(ctx) ? ctx.takerPaymentAccountId! : ctx.makerPaymentAccountId!))?.getPaymentAccountPayload();
// seller does not have buyer's payment account payload until payment sent
let fetchedTrade = await ctx.seller!.getTrade(ctx.offerId!);
let fetchedTrade = await getSeller(ctx)!.getTrade(ctx.offerId!);
let contract = fetchedTrade.getContract()!;
let buyerPaymentAccountPayload = contract.getIsBuyerMakerAndSellerTaker() ? contract.getMakerPaymentAccountPayload() : contract.getTakerPaymentAccountPayload();
if (ctx.isPaymentSent) expect(buyerPaymentAccountPayload).toEqual(expectedBuyerPaymentAccountPayload);
else expect(buyerPaymentAccountPayload).toBeUndefined();
// shut down buyer and seller if configured
const usedPorts = [getPort(ctx.buyer!.getUrl()), getPort(ctx.seller!.getUrl())];
ctx.usedPorts = [getPort(getBuyer(ctx)!.getUrl()), getPort(getSeller(ctx)!.getUrl())];
const promises: Promise<void>[] = [];
const buyerAppName = ctx.buyer!.getAppName();
ctx.buyerAppName = getBuyer(ctx)!.getAppName();
if (ctx.buyerOfflineAfterTake) {
promises.push(releaseHavenoProcess(ctx.buyer!));
ctx.buyer = undefined; // TODO: don't track them separately?
if (isBuyerMaker) ctx.maker = undefined;
promises.push(releaseHavenoProcess(getBuyer(ctx)!));
if (isBuyerMaker(ctx)) ctx.maker = undefined;
else ctx.taker = undefined;
}
const sellerAppName = ctx.seller!.getAppName();
ctx.sellerAppName = getSeller(ctx)!.getAppName();
if (ctx.sellerOfflineAfterTake) {
promises.push(releaseHavenoProcess(ctx.seller!));
ctx.seller = undefined;
if (isBuyerMaker) ctx.taker = undefined;
promises.push(releaseHavenoProcess(getSeller(ctx)!));
if (isBuyerMaker(ctx)) ctx.taker = undefined;
else ctx.maker = undefined;
}
await Promise.all(promises);
@ -1908,115 +1961,127 @@ async function executeTrade(ctx?: TradeContext): Promise<string> {
// wait for deposit txs to unlock
await waitForUnlockedTxs(trade.getMakerDepositTxId(), trade.getTakerDepositTxId());
// buyer comes online if offline
if (ctx.buyerOfflineAfterTake) {
ctx.buyer = await initHaveno({appName: buyerAppName, excludePorts: usedPorts});
if (isBuyerMaker) ctx.maker = ctx.buyer;
else ctx.taker = ctx.buyer;
usedPorts.push(getPort(ctx.buyer!.getUrl()));
// buyer comes online if offline and used
if (ctx.buyerOfflineAfterTake && ((ctx.buyerSendsPayment && !ctx.isPaymentSent && ctx.sellerDisputeContext !== DisputeContext.OPEN_AFTER_DEPOSITS_UNLOCK) || (ctx.buyerDisputeContext === DisputeContext.OPEN_AFTER_DEPOSITS_UNLOCK && !ctx.buyerOpenedDispute))) {
const buyer = await initHaveno({appName: ctx.buyerAppName, excludePorts: ctx.usedPorts});
if (isBuyerMaker(ctx)) ctx.maker = buyer;
else ctx.taker = buyer;
ctx.usedPorts.push(getPort(buyer.getUrl()));
}
// test trade states
// wait for traders to observe
await wait(TestConfig.maxWalletStartupMs + ctx.walletSyncPeriodMs! * 2);
// test buyer trade state if online
const expectedState = ctx.isPaymentSent ? "PAYMENT_SENT" : "DEPOSITS_UNLOCKED" // TODO: test COMPLETED, PAYMENT_RECEIVED states?
expect((await ctx.buyer!.getTrade(offer!.getId())).getPhase()).toEqual(expectedState);
fetchedTrade = await ctx.buyer!.getTrade(ctx.offerId!);
expect(fetchedTrade.getIsDepositUnlocked()).toBe(true);
expect(fetchedTrade.getPhase()).toEqual(expectedState);
if (!ctx.sellerOfflineAfterTake) {
fetchedTrade = await ctx.seller!.getTrade(trade.getTradeId());
if (getBuyer(ctx)) {
expect((await getBuyer(ctx)!.getTrade(ctx.offer!.getId())).getPhase()).toEqual(expectedState);
fetchedTrade = await getBuyer(ctx)!.getTrade(ctx.offerId!);
expect(fetchedTrade.getIsDepositUnlocked()).toBe(true);
expect(fetchedTrade.getPhase()).toEqual(expectedState);
}
// test seller trade state if online
if (getSeller(ctx)) {
fetchedTrade = await getSeller(ctx)!.getTrade(trade.getTradeId());
expect(fetchedTrade.getIsDepositUnlocked()).toBe(true);
expect(fetchedTrade.getPhase()).toEqual(expectedState);
}
// buyer has seller's payment account payload after first confirmation
fetchedTrade = await ctx.buyer!.getTrade(ctx.offerId!);
contract = fetchedTrade.getContract()!;
let sellerPaymentAccountPayload = contract.getIsBuyerMakerAndSellerTaker() ? contract.getTakerPaymentAccountPayload() : contract.getMakerPaymentAccountPayload();
expect(sellerPaymentAccountPayload).toEqual(expectedSellerPaymentAccountPayload);
let form = await ctx.buyer!.getPaymentAccountPayloadForm(sellerPaymentAccountPayload!);
let expectedForm = await ctx.buyer!.getPaymentAccountPayloadForm(expectedSellerPaymentAccountPayload!);
expect(HavenoUtils.formToString(form)).toEqual(HavenoUtils.formToString(expectedForm));
let sellerPaymentAccountPayload;
let form;
let expectedForm;
if (getBuyer(ctx)) {
fetchedTrade = await getBuyer(ctx)!.getTrade(ctx.offerId!);
contract = fetchedTrade.getContract()!;
sellerPaymentAccountPayload = contract.getIsBuyerMakerAndSellerTaker() ? contract.getTakerPaymentAccountPayload() : contract.getMakerPaymentAccountPayload();
expect(sellerPaymentAccountPayload).toEqual(expectedSellerPaymentAccountPayload);
form = await getBuyer(ctx)!.getPaymentAccountPayloadForm(sellerPaymentAccountPayload!);
expectedForm = await getBuyer(ctx)!.getPaymentAccountPayloadForm(expectedSellerPaymentAccountPayload!);
expect(HavenoUtils.formToString(form)).toEqual(HavenoUtils.formToString(expectedForm));
}
// buyer notified to send payment TODO
// open dispute(s) if configured
if (ctx.buyerDisputeContext === DisputeContext.OPEN_AFTER_DEPOSITS_UNLOCK && !ctx.buyerOpenedDispute) {
await ctx.buyer!.openDispute(ctx.offerId!);
await getBuyer(ctx)!.openDispute(ctx.offerId!);
ctx.buyerOpenedDispute = true;
ctx.disputeOpener = ctx.buyer;
ctx.disputeOpener = SaleRole.BUYER;
}
if (ctx.sellerDisputeContext === DisputeContext.OPEN_AFTER_DEPOSITS_UNLOCK && !ctx.sellerOpenedDispute) {
await ctx.seller!.openDispute(ctx.offerId!);
await getSeller(ctx)!.openDispute(ctx.offerId!);
ctx.sellerOpenedDispute = true;
if (!ctx.disputeOpener) ctx.disputeOpener = ctx.seller;
}
if (ctx.disputeOpener) {
ctx.disputePeer = ctx.disputeOpener === ctx.buyer ? ctx.seller : ctx.buyer;
await testOpenDispute(ctx);
if (!ctx.disputeOpener) ctx.disputeOpener = SaleRole.SELLER;
}
// if dispute opened, resolve dispute if configured and return
// handle opened dispute
if (ctx.disputeOpener) {
// test open dispute
await testOpenDispute(ctx);
// resolve dispute if configured
if (ctx.resolveDispute) await resolveDispute(ctx);
// return offer id
return ctx.offerId!;
}
// buyer confirms payment is sent
if (!ctx.buyerSendsPayment) return offer!.getId();
if (!ctx.buyerSendsPayment) return ctx.offer!.getId();
else if (!ctx.isPaymentSent) {
HavenoUtils.log(1, "Buyer confirming payment sent");
await ctx.buyer!.confirmPaymentStarted(trade.getTradeId());
await getBuyer(ctx)!.confirmPaymentStarted(trade.getTradeId());
ctx.isPaymentSent = true;
fetchedTrade = await ctx.buyer!.getTrade(trade.getTradeId());
fetchedTrade = await getBuyer(ctx)!.getTrade(trade.getTradeId());
expect(fetchedTrade.getPhase()).toEqual("PAYMENT_SENT");
}
// buyer goes offline if configured
if (ctx.buyerOfflineAfterPaymentSent) {
await releaseHavenoProcess(ctx.buyer!);
ctx.buyer = undefined;
if (isBuyerMaker) ctx.maker = undefined;
await releaseHavenoProcess(getBuyer(ctx)!);
if (isBuyerMaker(ctx)) ctx.maker = undefined;
else ctx.taker = undefined;
}
// seller comes online if offline
if (!ctx.seller) {
ctx.seller = await initHaveno({appName: sellerAppName, excludePorts: usedPorts});
if (isBuyerMaker) ctx.taker = ctx.seller;
else ctx.maker = ctx.seller;
usedPorts.push(getPort(ctx.seller!.getUrl()))
if (!getSeller(ctx)) {
const seller = await initHaveno({appName: ctx.sellerAppName, excludePorts: ctx.usedPorts});
if (isBuyerMaker(ctx)) ctx.taker = seller;
else ctx.maker = seller;
ctx.usedPorts.push(getPort(getSeller(ctx)!.getUrl()))
}
// seller notified payment is sent
await wait(ctx.maxTimePeerNoticeMs! + TestConfig.maxWalletStartupMs); // TODO: test notification
if (ctx.sellerOfflineAfterTake) await wait(ctx.walletSyncPeriodMs!); // wait to process mailbox messages
fetchedTrade = await ctx.seller.getTrade(trade.getTradeId());
fetchedTrade = await getSeller(ctx)!.getTrade(trade.getTradeId());
expect(fetchedTrade.getPhase()).toEqual("PAYMENT_SENT");
expect(fetchedTrade.getPayoutState()).toEqual("PAYOUT_UNPUBLISHED");
// seller has buyer's payment account payload after payment sent
fetchedTrade = await ctx.seller!.getTrade(ctx.offerId!);
fetchedTrade = await getSeller(ctx)!.getTrade(ctx.offerId!);
contract = fetchedTrade.getContract()!;
buyerPaymentAccountPayload = contract.getIsBuyerMakerAndSellerTaker() ? contract.getMakerPaymentAccountPayload() : contract.getTakerPaymentAccountPayload();
expect(buyerPaymentAccountPayload).toEqual(expectedBuyerPaymentAccountPayload);
form = await ctx.seller!.getPaymentAccountPayloadForm(sellerPaymentAccountPayload!);
expectedForm = await ctx.seller!.getPaymentAccountPayloadForm(expectedSellerPaymentAccountPayload!);
form = await getSeller(ctx)!.getPaymentAccountPayloadForm(sellerPaymentAccountPayload!);
expectedForm = await getSeller(ctx)!.getPaymentAccountPayloadForm(expectedSellerPaymentAccountPayload!);
expect(HavenoUtils.formToString(form)).toEqual(HavenoUtils.formToString(expectedForm));
// open dispute(s) if configured
if (ctx.buyerDisputeContext === DisputeContext.OPEN_AFTER_PAYMENT_SENT && !ctx.buyerOpenedDispute) {
await ctx.buyer!.openDispute(ctx.offerId!);
await getBuyer(ctx)!.openDispute(ctx.offerId!);
ctx.buyerOpenedDispute = true;
if (!ctx.disputeOpener) ctx.disputeOpener = ctx.buyer;
if (!ctx.disputeOpener) ctx.disputeOpener = SaleRole.BUYER;
}
if (ctx.sellerDisputeContext === DisputeContext.OPEN_AFTER_PAYMENT_SENT && !ctx.sellerOpenedDispute) {
await ctx.seller!.openDispute(ctx.offerId!);
await getSeller(ctx)!.openDispute(ctx.offerId!);
ctx.sellerOpenedDispute = true;
if (!ctx.disputeOpener) ctx.disputeOpener = ctx.seller;
if (!ctx.disputeOpener) ctx.disputeOpener = SaleRole.SELLER;
}
if (ctx.disputeOpener) {
ctx.disputePeer = ctx.disputeOpener === ctx.buyer ? ctx.seller : ctx.buyer;
await testOpenDispute(ctx);
}
@ -2027,45 +2092,45 @@ async function executeTrade(ctx?: TradeContext): Promise<string> {
}
// seller confirms payment is received
if (!ctx.sellerReceivesPayment) return offer!.getId();
if (!ctx.sellerReceivesPayment) return ctx.offer!.getId();
else if (!ctx.isPaymentReceived) {
HavenoUtils.log(1, "Seller confirming payment received");
await ctx.seller.confirmPaymentReceived(trade.getTradeId());
await getSeller(ctx)!.confirmPaymentReceived(trade.getTradeId());
ctx.isPaymentReceived = true;
fetchedTrade = await ctx.seller.getTrade(trade.getTradeId());
fetchedTrade = await getSeller(ctx)!.getTrade(trade.getTradeId());
expect(fetchedTrade.getPhase()).toEqual("PAYMENT_RECEIVED");
await wait(ctx.walletSyncPeriodMs!); // buyer or arbitrator will sign and publish payout tx
await testTradeState(await ctx.seller!.getTrade(trade.getTradeId()), {phase: "PAYMENT_RECEIVED", payoutState: ["PAYOUT_PUBLISHED", "PAYOUT_CONFIRMED", "PAYOUT_UNLOCKED"], isCompleted: false, isPayoutPublished: true});
await testTradeState(await getSeller(ctx)!.getTrade(trade.getTradeId()), {phase: "PAYMENT_RECEIVED", payoutState: ["PAYOUT_PUBLISHED", "PAYOUT_CONFIRMED", "PAYOUT_UNLOCKED"], isCompleted: false, isPayoutPublished: true});
}
// payout tx is published by buyer (priority) or arbitrator
await wait(ctx.walletSyncPeriodMs!);
await testTradeState(await ctx.seller!.getTrade(trade.getTradeId()), {phase: "PAYMENT_RECEIVED", payoutState: ["PAYOUT_PUBLISHED", "PAYOUT_CONFIRMED", "PAYOUT_UNLOCKED"], isCompleted: false, isPayoutPublished: true});
await testTradeState(await getSeller(ctx)!.getTrade(trade.getTradeId()), {phase: "PAYMENT_RECEIVED", payoutState: ["PAYOUT_PUBLISHED", "PAYOUT_CONFIRMED", "PAYOUT_UNLOCKED"], isCompleted: false, isPayoutPublished: true});
await testTradeState(await ctx.arbitrator!.getTrade(trade.getTradeId()), {phase: "COMPLETED", payoutState: ["PAYOUT_PUBLISHED", "PAYOUT_CONFIRMED", "PAYOUT_UNLOCKED"], isCompleted: true, isPayoutPublished: true}); // arbitrator trade auto completes
// buyer comes online if offline
if (ctx.buyerOfflineAfterPaymentSent) {
ctx.buyer = await initHaveno({appName: buyerAppName, excludePorts: usedPorts});
if (isBuyerMaker) ctx.maker = ctx.buyer;
else ctx.taker = ctx.buyer;
usedPorts.push(getPort(ctx.buyer!.getUrl()));
const buyer = await initHaveno({appName: ctx.buyerAppName, excludePorts: ctx.usedPorts});
if (isBuyerMaker(ctx)) ctx.maker = buyer;
else ctx.taker = buyer;
ctx.usedPorts.push(getPort(buyer.getUrl()));
HavenoUtils.log(1, "Done starting buyer");
await wait(TestConfig.maxWalletStartupMs + ctx.walletSyncPeriodMs!);
}
await testTradeState(await ctx.buyer!.getTrade(trade.getTradeId()), {phase: "PAYMENT_RECEIVED", payoutState: ["PAYOUT_PUBLISHED", "PAYOUT_CONFIRMED", "PAYOUT_UNLOCKED"], isCompleted: false, isPayoutPublished: true});
await testTradeState(await getBuyer(ctx)!.getTrade(trade.getTradeId()), {phase: "PAYMENT_RECEIVED", payoutState: ["PAYOUT_PUBLISHED", "PAYOUT_CONFIRMED", "PAYOUT_UNLOCKED"], isCompleted: false, isPayoutPublished: true});
// test trade completion
await ctx.buyer!.completeTrade(trade.getTradeId());
await testTradeState(await ctx.buyer!.getTrade(trade.getTradeId()), {phase: "COMPLETED", payoutState: ["PAYOUT_PUBLISHED", "PAYOUT_CONFIRMED", "PAYOUT_UNLOCKED"], isCompleted: true, isPayoutPublished: true});
await ctx.seller!.completeTrade(trade.getTradeId());
await testTradeState(await ctx.seller!.getTrade(trade.getTradeId()), {phase: "COMPLETED", payoutState: ["PAYOUT_PUBLISHED", "PAYOUT_CONFIRMED", "PAYOUT_UNLOCKED"], isCompleted: true, isPayoutPublished: true});
await getBuyer(ctx)!.completeTrade(trade.getTradeId());
await testTradeState(await getBuyer(ctx)!.getTrade(trade.getTradeId()), {phase: "COMPLETED", payoutState: ["PAYOUT_PUBLISHED", "PAYOUT_CONFIRMED", "PAYOUT_UNLOCKED"], isCompleted: true, isPayoutPublished: true});
await getSeller(ctx)!.completeTrade(trade.getTradeId());
await testTradeState(await getSeller(ctx)!.getTrade(trade.getTradeId()), {phase: "COMPLETED", payoutState: ["PAYOUT_PUBLISHED", "PAYOUT_CONFIRMED", "PAYOUT_UNLOCKED"], isCompleted: true, isPayoutPublished: true});
// test balances after payout tx unless other trades can interfere
if (!ctx.concurrentTrades) {
const buyerBalancesAfter = await ctx.buyer!.getBalances();
const sellerBalancesAfter = await ctx.seller.getBalances();
const buyerFee = BigInt(buyerBalancesBefore.getBalance()) + BigInt(buyerBalancesBefore.getReservedOfferBalance()) + BigInt(offer!.getAmount()) - (BigInt(buyerBalancesAfter.getBalance()) + BigInt(buyerBalancesAfter.getReservedOfferBalance())); // buyer fee = total balance before + offer amount - total balance after
const sellerFee = BigInt(sellerBalancesBefore.getBalance()) + BigInt(sellerBalancesBefore.getReservedOfferBalance()) - BigInt(offer!.getAmount()) - (BigInt(sellerBalancesAfter.getBalance()) + BigInt(sellerBalancesAfter.getReservedOfferBalance())); // seller fee = total balance before - offer amount - total balance after
const buyerBalancesAfter = await getBuyer(ctx)!.getBalances();
const sellerBalancesAfter = await getSeller(ctx)!.getBalances();
const buyerFee = BigInt(buyerBalancesBefore.getBalance()) + BigInt(buyerBalancesBefore.getReservedOfferBalance()) + BigInt(ctx.offer!.getAmount()) - (BigInt(buyerBalancesAfter.getBalance()) + BigInt(buyerBalancesAfter.getReservedOfferBalance())); // buyer fee = total balance before + offer amount - total balance after
const sellerFee = BigInt(sellerBalancesBefore.getBalance()) + BigInt(sellerBalancesBefore.getReservedOfferBalance()) - BigInt(ctx.offer!.getAmount()) - (BigInt(sellerBalancesAfter.getBalance()) + BigInt(sellerBalancesAfter.getReservedOfferBalance())); // seller fee = total balance before - offer amount - total balance after
expect(buyerFee).toBeLessThanOrEqual(TestConfig.maxFee);
expect(buyerFee).toBeGreaterThan(BigInt("0"));
expect(sellerFee).toBeLessThanOrEqual(TestConfig.maxFee);
@ -2074,7 +2139,8 @@ async function executeTrade(ctx?: TradeContext): Promise<string> {
// test payout unlock
await testTradePayoutUnlock(ctx);
return offer!.getId();
if (ctx.offer!.getId() !== ctx.offerId) throw new Error("Expected offer ids to match");
return ctx.offer!.getId();
} catch (err) {
HavenoUtils.log(0, "Error executing trade " + ctx!.offerId + (ctx!.index === undefined ? "" : " at index " + ctx!.index) + ": " + err.message);
HavenoUtils.log(0, tradeContextToString(ctx!));
@ -2086,24 +2152,24 @@ async function testTradePayoutUnlock(ctx: TradeContext) {
const height = await monerod.getHeight();
// test after payout confirmed
const payoutTxId = (await ctx.buyer!.getTrade(ctx.offerId!)).getPayoutTxId();
let trade = await ctx.buyer!.getTrade(ctx.offerId!);
const payoutTxId = (await ctx.arbitrator!.getTrade(ctx.offerId!)).getPayoutTxId();
let trade = await ctx.arbitrator!.getTrade(ctx.offerId!);
if (trade.getPayoutState() !== "PAYOUT_CONFIRMED") await mineToHeight(height + 1);
await wait(TestConfig.maxWalletStartupMs + ctx.walletSyncPeriodMs! * 2);
await testTradeState(await ctx.buyer!.getTrade(ctx.offerId!), {phase: "COMPLETED", payoutState: ["PAYOUT_CONFIRMED", "PAYOUT_UNLOCKED"]});
await testTradeState(await ctx.seller!.getTrade(ctx.offerId!), {phase: "COMPLETED", payoutState: ["PAYOUT_CONFIRMED", "PAYOUT_UNLOCKED"]});
if (getBuyer(ctx)) await testTradeState(await getBuyer(ctx)!.getTrade(ctx.offerId!), {phase: "COMPLETED", payoutState: ["PAYOUT_CONFIRMED", "PAYOUT_UNLOCKED"]});
if (getSeller(ctx)) await testTradeState(await getSeller(ctx)!.getTrade(ctx.offerId!), {phase: "COMPLETED", payoutState: ["PAYOUT_CONFIRMED", "PAYOUT_UNLOCKED"]});
await testTradeState(await ctx.arbitrator!.getTrade(ctx.offerId!), {phase: "COMPLETED", payoutState: ["PAYOUT_PUBLISHED", "PAYOUT_CONFIRMED", "PAYOUT_UNLOCKED"]}); // arbitrator idles wallet
let payoutTx = await ctx.buyer?.getXmrTx(payoutTxId);
let payoutTx = getBuyer(ctx) ? await getBuyer(ctx)?.getXmrTx(payoutTxId) : await getSeller(ctx)?.getXmrTx(payoutTxId);
expect(payoutTx?.getIsConfirmed());
// test after payout unlocked
trade = await ctx.buyer!.getTrade(ctx.offerId!);
trade = await ctx.arbitrator!.getTrade(ctx.offerId!);
if (trade.getPayoutState() !== "PAYOUT_UNLOCKED") await mineToHeight(height + 10);
await wait(TestConfig.maxWalletStartupMs + ctx.walletSyncPeriodMs! * 2);
await testTradeState(await ctx.buyer!.getTrade(ctx.offerId!), {phase: "COMPLETED", payoutState: ["PAYOUT_UNLOCKED"]});
await testTradeState(await ctx.seller!.getTrade(ctx.offerId!), {phase: "COMPLETED", payoutState: ["PAYOUT_UNLOCKED"]});
if (await getBuyer(ctx)) await testTradeState(await getBuyer(ctx)!.getTrade(ctx.offerId!), {phase: "COMPLETED", payoutState: ["PAYOUT_UNLOCKED"]});
if (await getSeller(ctx)) await testTradeState(await getSeller(ctx)!.getTrade(ctx.offerId!), {phase: "COMPLETED", payoutState: ["PAYOUT_UNLOCKED"]});
await testTradeState(await ctx.arbitrator!.getTrade(ctx.offerId!), {phase: "COMPLETED", payoutState: ["PAYOUT_PUBLISHED", "PAYOUT_CONFIRMED", "PAYOUT_UNLOCKED"]}); // arbitrator idles wallet
payoutTx = await ctx.buyer?.getXmrTx(payoutTxId);
payoutTx = getBuyer(ctx) ? await getBuyer(ctx)?.getXmrTx(payoutTxId) : await getSeller(ctx)?.getXmrTx(payoutTxId);
expect(!payoutTx?.getIsLocked());
}
@ -2235,15 +2301,21 @@ async function takeOffer(ctx: TradeContext): Promise<TradeInfo> {
async function testOpenDispute(ctx: TradeContext) {
// TODO: test open dispute when buyer or seller offline
if (!getBuyer(ctx) || !getSeller(ctx)) {
HavenoUtils.log(0, "WARNING: skipping test open dispute tests because a trader is offline"); // TODO: update tests for offline trader
return;
}
// test dispute state
const openerDispute = await ctx.disputeOpener!.getDispute(ctx.offerId!);
const openerDispute = await getDisputeOpener(ctx)!.getDispute(ctx.offerId!);
expect(openerDispute.getTradeId()).toEqual(ctx.offerId);
expect(openerDispute.getIsOpener()).toBe(true);
expect(openerDispute.getDisputeOpenerIsBuyer()).toBe(ctx.disputeOpener === ctx.buyer);
expect(openerDispute.getDisputeOpenerIsBuyer()).toBe(getDisputeOpener(ctx) === getBuyer(ctx));
// get non-existing dispute should fail
try {
await ctx.disputeOpener!.getDispute("invalid");
await getDisputeOpener(ctx)!.getDispute("invalid");
throw new Error("get dispute with invalid id should fail");
} catch (err: any) {
assert.equal(err.message, "dispute for trade id 'invalid' not found");
@ -2251,7 +2323,7 @@ async function testOpenDispute(ctx: TradeContext) {
// peer sees the dispute
await wait(ctx.maxTimePeerNoticeMs! + TestConfig.maxWalletStartupMs);
const peerDispute = await ctx.disputePeer!.getDispute(ctx.offerId!);
const peerDispute = await getDisputePeer(ctx)!.getDispute(ctx.offerId!);
expect(peerDispute.getTradeId()).toEqual(ctx.offerId);
expect(peerDispute.getIsOpener()).toBe(false || ctx.buyerDisputeContext === ctx.sellerDisputeContext); // TODO: both peers think they're the opener if disputes opened at same time since not waiting for ack
@ -2265,7 +2337,7 @@ async function testOpenDispute(ctx: TradeContext) {
// arbitrator has seller's payment account info
let sellerPaymentAccountPayload = arbDisputeOpener.getContract()!.getIsBuyerMakerAndSellerTaker() ? arbDisputeOpener.getTakerPaymentAccountPayload() : arbDisputeOpener.getMakerPaymentAccountPayload();
let expectedSellerPaymentAccountPayload = (await ctx.seller?.getPaymentAccount(sellerPaymentAccountPayload?.getId()!))?.getPaymentAccountPayload();
let expectedSellerPaymentAccountPayload = (await getSeller(ctx)?.getPaymentAccount(sellerPaymentAccountPayload?.getId()!))?.getPaymentAccountPayload();
expect(sellerPaymentAccountPayload).toEqual(expectedSellerPaymentAccountPayload);
expect(await ctx.arbitrator?.getPaymentAccountPayloadForm(sellerPaymentAccountPayload!)).toEqual(await ctx.arbitrator?.getPaymentAccountPayloadForm(expectedSellerPaymentAccountPayload!));
sellerPaymentAccountPayload = arbDisputePeer.getContract()!.getIsBuyerMakerAndSellerTaker() ? arbDisputePeer.getTakerPaymentAccountPayload() : arbDisputeOpener.getMakerPaymentAccountPayload();
@ -2274,16 +2346,16 @@ async function testOpenDispute(ctx: TradeContext) {
// arbitrator has buyer's payment account info unless opener is seller and payment not sent
let buyerPaymentAccountPayload = arbDisputeOpener.getContract()!.getIsBuyerMakerAndSellerTaker() ? arbDisputeOpener.getMakerPaymentAccountPayload() : arbDisputeOpener.getTakerPaymentAccountPayload();
if (ctx.disputeOpener === ctx.seller && !ctx.isPaymentSent) expect(buyerPaymentAccountPayload).toBeUndefined();
if (getDisputeOpener(ctx) === getSeller(ctx) && !ctx.isPaymentSent) expect(buyerPaymentAccountPayload).toBeUndefined();
else {
let expectedBuyerPaymentAccountPayload = (await ctx.buyer?.getPaymentAccount(buyerPaymentAccountPayload?.getId()!))?.getPaymentAccountPayload();
let expectedBuyerPaymentAccountPayload = (await getBuyer(ctx)?.getPaymentAccount(buyerPaymentAccountPayload?.getId()!))?.getPaymentAccountPayload();
expect(buyerPaymentAccountPayload).toEqual(expectedBuyerPaymentAccountPayload);
expect(await ctx.arbitrator?.getPaymentAccountPayloadForm(buyerPaymentAccountPayload!)).toEqual(await ctx.arbitrator?.getPaymentAccountPayloadForm(expectedBuyerPaymentAccountPayload!));
}
buyerPaymentAccountPayload = arbDisputePeer.getContract()!.getIsBuyerMakerAndSellerTaker() ? arbDisputePeer.getMakerPaymentAccountPayload() : arbDisputePeer.getTakerPaymentAccountPayload();
if (ctx.disputeOpener === ctx.seller && !ctx.isPaymentSent) expect(buyerPaymentAccountPayload).toBeUndefined();
if (getDisputeOpener(ctx) === getSeller(ctx) && !ctx.isPaymentSent) expect(buyerPaymentAccountPayload).toBeUndefined();
else {
let expectedBuyerPaymentAccountPayload = (await ctx.buyer?.getPaymentAccount(buyerPaymentAccountPayload?.getId()!))?.getPaymentAccountPayload();
let expectedBuyerPaymentAccountPayload = (await getBuyer(ctx)?.getPaymentAccount(buyerPaymentAccountPayload?.getId()!))?.getPaymentAccountPayload();
expect(buyerPaymentAccountPayload).toEqual(expectedBuyerPaymentAccountPayload);
expect(await ctx.arbitrator?.getPaymentAccountPayloadForm(buyerPaymentAccountPayload!)).toEqual(await ctx.arbitrator?.getPaymentAccountPayloadForm(expectedBuyerPaymentAccountPayload!));
}
@ -2292,8 +2364,8 @@ async function testOpenDispute(ctx: TradeContext) {
const disputeOpenerNotifications: NotificationMessage[] = [];
const disputePeerNotifications: NotificationMessage[] = [];
const arbitratorNotifications: NotificationMessage[] = [];
await ctx.disputeOpener!.addNotificationListener(notification => { HavenoUtils.log(3, "Dispute opener received notification " + notification.getType() + " " + (notification.getChatMessage() ? notification.getChatMessage()?.getMessage() : "")); disputeOpenerNotifications.push(notification); });
await ctx.disputePeer!.addNotificationListener(notification => { HavenoUtils.log(3, "Dispute peer received notification " + notification.getType() + " " + (notification.getChatMessage() ? notification.getChatMessage()?.getMessage() : "")); disputePeerNotifications.push(notification); });
await getDisputeOpener(ctx)!.addNotificationListener(notification => { HavenoUtils.log(3, "Dispute opener received notification " + notification.getType() + " " + (notification.getChatMessage() ? notification.getChatMessage()?.getMessage() : "")); disputeOpenerNotifications.push(notification); });
await getDisputePeer(ctx)!.addNotificationListener(notification => { HavenoUtils.log(3, "Dispute peer received notification " + notification.getType() + " " + (notification.getChatMessage() ? notification.getChatMessage()?.getMessage() : "")); disputePeerNotifications.push(notification); });
await arbitrator.addNotificationListener(notification => { HavenoUtils.log(3, "Arbitrator received notification " + notification.getType() + " " + (notification.getChatMessage() ? notification.getChatMessage()?.getMessage() : "")); arbitratorNotifications.push(notification); });
// arbitrator sends chat messages to traders
@ -2312,14 +2384,14 @@ async function testOpenDispute(ctx: TradeContext) {
attachment2.setBytes(bytes2);
attachment2.setFileName("proof.png");
HavenoUtils.log(2, "Dispute opener sending chat message to arbitrator. tradeId=" + ctx.offerId + ", disputeId=" + openerDispute.getId());
await ctx.disputeOpener!.sendDisputeChatMessage(openerDispute.getId(), "Dispute opener chat message", [attachment, attachment2]);
await getDisputeOpener(ctx)!.sendDisputeChatMessage(openerDispute.getId(), "Dispute opener chat message", [attachment, attachment2]);
await wait(ctx.maxTimePeerNoticeMs!); // wait for user2's message to arrive
HavenoUtils.log(2, "Dispute peer sending chat message to arbitrator. tradeId=" + ctx.offerId + ", disputeId=" + peerDispute.getId());
await ctx.disputePeer!.sendDisputeChatMessage(peerDispute.getId(), "Dispute peer chat message", []);
await getDisputePeer(ctx)!.sendDisputeChatMessage(peerDispute.getId(), "Dispute peer chat message", []);
// test trader chat messages
await wait(ctx.maxTimePeerNoticeMs!);
let dispute = await ctx.disputeOpener!.getDispute(ctx.offerId!);
let dispute = await getDisputeOpener(ctx)!.getDispute(ctx.offerId!);
let messages = dispute.getChatMessageList();
expect(messages.length).toBeGreaterThanOrEqual(3); // last messages are chat, first messages are system message and possibly DisputeOpenedMessage acks
expect(messages[messages.length - 2].getMessage()).toEqual("Arbitrator chat message to dispute opener");
@ -2330,7 +2402,7 @@ async function testOpenDispute(ctx: TradeContext) {
expect(attachments[0].getBytes()).toEqual(bytes);
expect(attachments[1].getFileName()).toEqual("proof.png");
expect(attachments[1].getBytes()).toEqual(bytes2);
dispute = await ctx.disputePeer!.getDispute(ctx.offerId!);
dispute = await getDisputePeer(ctx)!.getDispute(ctx.offerId!);
messages = dispute.getChatMessageList();
expect(messages.length).toBeGreaterThanOrEqual(3);
expect(messages[messages.length - 2].getMessage()).toEqual("Arbitrator chat message to dispute peer");
@ -2359,10 +2431,24 @@ async function testOpenDispute(ctx: TradeContext) {
async function resolveDispute(ctx: TradeContext) {
// stop buyer or seller depending on configuration
const promises: Promise<void>[] = [];
if (getBuyer(ctx) && ctx.buyerOfflineAfterDisputeOpened) {
promises.push(releaseHavenoProcess(getBuyer(ctx)!)); // stop buyer
if (isBuyerMaker(ctx)) ctx.maker = undefined;
else ctx.taker = undefined;
}
if (getSeller(ctx) && ctx.sellerOfflineAfterDisputeOpened) {
promises.push(releaseHavenoProcess(getSeller(ctx)!)); // stop seller
if (isBuyerMaker(ctx)) ctx.taker = undefined;
else ctx.maker = undefined;
}
await Promise.all(promises);
// award too little to loser
const offer = (await ctx.maker!.getTrade(ctx.offerId!)).getOffer();
const tradeAmount: bigint = BigInt(offer!.getAmount());
const customWinnerAmount = tradeAmount + BigInt(offer!.getBuyerSecurityDeposit() + offer!.getSellerSecurityDeposit()) - BigInt("10000");
const tradeAmount: bigint = BigInt(ctx.offer!.getAmount());
const customWinnerAmount = tradeAmount + BigInt(ctx.offer!.getBuyerSecurityDeposit() + ctx.offer!.getSellerSecurityDeposit()) - BigInt("10000");
try {
await arbitrator.resolveDispute(ctx.offerId!, ctx.disputeWinner!, ctx.disputeReason!, "Loser gets too little", customWinnerAmount);
throw new Error("Should have failed resolving dispute with insufficient loser payout");
@ -2371,41 +2457,61 @@ async function resolveDispute(ctx: TradeContext) {
}
// resolve dispute according to configuration
const winner = ctx.disputeWinner === DisputeResult.Winner.BUYER ? ctx.buyer : ctx.seller;
const loser = ctx.disputeWinner === DisputeResult.Winner.BUYER ? ctx.seller : ctx.buyer;
const winnerBalancesBefore = await winner!.getBalances();
const loserBalancesBefore = await loser!.getBalances();
const winner = ctx.disputeWinner === DisputeResult.Winner.BUYER ? getBuyer(ctx) : getSeller(ctx);
const loser = ctx.disputeWinner === DisputeResult.Winner.BUYER ? getSeller(ctx) : getBuyer(ctx);
const winnerBalancesBefore = winner ? await winner!.getBalances() : undefined;
const loserBalancesBefore = loser ? await loser!.getBalances() : undefined;
HavenoUtils.log(1, "Resolving dispute for trade " + ctx.offerId);
const startTime = Date.now();
await arbitrator.resolveDispute(ctx.offerId!, ctx.disputeWinner!, ctx.disputeReason!, ctx.disputeSummary!, ctx.disputeWinnerAmount);
HavenoUtils.log(1, "Done resolving dispute (" + (Date.now() - startTime) + ")");
// start buyer or seller depending on configuration
if (!getBuyer(ctx) && ctx.buyerOfflineAfterDisputeOpened === false) {
const buyer = await initHaveno({appName: ctx.buyerAppName, excludePorts: ctx.usedPorts}); // start buyer
if (isBuyerMaker(ctx)) ctx.maker = buyer;
else ctx.taker = buyer;
ctx.usedPorts!.push(getPort(buyer.getUrl()));
}
if (!getSeller(ctx) && ctx.sellerOfflineAfterDisputeOpened === false) {
const seller = await initHaveno({appName: ctx.sellerAppName, excludePorts: ctx.usedPorts}); // start seller
if (isBuyerMaker(ctx)) ctx.taker = seller;
else ctx.maker = seller;
ctx.usedPorts!.push(getPort(getSeller(ctx)!.getUrl()))
}
// test resolved dispute
await wait(TestConfig.maxWalletStartupMs + ctx.walletSyncPeriodMs! * 2);
let dispute = await ctx.disputeOpener!.getDispute(ctx.offerId!);
assert(dispute.getIsClosed(), "Dispute is not closed for opener, trade " + ctx.offerId);
dispute = await ctx.disputePeer!.getDispute(ctx.offerId!);
assert(dispute.getIsClosed(), "Dispute is not closed for opener's peer, trade " + ctx.offerId);
if (getDisputeOpener(ctx)) {
const dispute = await getDisputeOpener(ctx)!.getDispute(ctx.offerId!);
assert(dispute.getIsClosed(), "Dispute is not closed for opener, trade " + ctx.offerId);
}
if (getDisputePeer(ctx)) {
const dispute = await getDisputePeer(ctx)!.getDispute(ctx.offerId!);
assert(dispute.getIsClosed(), "Dispute is not closed for opener, trade " + ctx.offerId);
}
// test trade state
await testTradeState(await ctx.buyer!.getTrade(ctx.offerId!), {phase: "COMPLETED", payoutState: ["PAYOUT_PUBLISHED", "PAYOUT_CONFIRMED", "PAYOUT_UNLOCKED"], disputeState: "DISPUTE_CLOSED", isCompleted: true, isPayoutPublished: true});
await testTradeState(await ctx.seller!.getTrade(ctx.offerId!), {phase: "COMPLETED", payoutState: ["PAYOUT_PUBLISHED", "PAYOUT_CONFIRMED", "PAYOUT_UNLOCKED"], disputeState: "DISPUTE_CLOSED", isCompleted: true, isPayoutPublished: true});
if (getBuyer(ctx)) await testTradeState(await getBuyer(ctx)!.getTrade(ctx.offerId!), {phase: "COMPLETED", payoutState: ["PAYOUT_PUBLISHED", "PAYOUT_CONFIRMED", "PAYOUT_UNLOCKED"], disputeState: "DISPUTE_CLOSED", isCompleted: true, isPayoutPublished: true});
if (getSeller(ctx)) await testTradeState(await getSeller(ctx)!.getTrade(ctx.offerId!), {phase: "COMPLETED", payoutState: ["PAYOUT_PUBLISHED", "PAYOUT_CONFIRMED", "PAYOUT_UNLOCKED"], disputeState: "DISPUTE_CLOSED", isCompleted: true, isPayoutPublished: true});
await testTradeState(await ctx.arbitrator!.getTrade(ctx.offerId!), {phase: "COMPLETED", payoutState: ["PAYOUT_PUBLISHED", "PAYOUT_CONFIRMED", "PAYOUT_UNLOCKED"], disputeState: "DISPUTE_CLOSED", isCompleted: true, isPayoutPublished: true});
// TODO: test trade state after txs confirm and unlock
// test balances after payout tx unless concurrent trades
if (!ctx.concurrentTrades) {
const winnerBalancesAfter = await winner!.getBalances();
const loserBalancesAfter = await loser!.getBalances();
const winnerDifference = BigInt(winnerBalancesAfter.getBalance()) - BigInt(winnerBalancesBefore.getBalance());
const loserDifference = BigInt(loserBalancesAfter.getBalance()) - BigInt(loserBalancesBefore.getBalance());
const winnerSecurityDeposit = BigInt(ctx.disputeWinner === DisputeResult.Winner.BUYER ? offer!.getBuyerSecurityDeposit() : offer!.getSellerSecurityDeposit())
const loserSecurityDeposit = BigInt(ctx.disputeWinner === DisputeResult.Winner.BUYER ? offer!.getSellerSecurityDeposit() : offer!.getBuyerSecurityDeposit());
const winnerPayout = ctx.disputeWinnerAmount ? ctx.disputeWinnerAmount : tradeAmount + winnerSecurityDeposit; // TODO: this assumes security deposit is returned to winner, but won't be the case if payment sent
const loserPayout = loserSecurityDeposit;
expect(winnerDifference).toEqual(winnerPayout);
expect(loserPayout - loserDifference).toBeLessThan(TestConfig.maxFee);
if (winner) {
const winnerBalancesAfter = await winner!.getBalances();
const winnerDifference = BigInt(winnerBalancesAfter.getBalance()) - BigInt(winnerBalancesBefore!.getBalance());
const winnerSecurityDeposit = BigInt(ctx.disputeWinner === DisputeResult.Winner.BUYER ? ctx.offer!.getBuyerSecurityDeposit() : ctx.offer!.getSellerSecurityDeposit())
const winnerPayout = ctx.disputeWinnerAmount ? ctx.disputeWinnerAmount : tradeAmount + winnerSecurityDeposit; // TODO: this assumes security deposit is returned to winner, but won't be the case if payment sent
expect(winnerDifference).toEqual(winnerPayout);
}
if (loser) {
const loserBalancesAfter = await loser!.getBalances();
const loserDifference = BigInt(loserBalancesAfter.getBalance()) - BigInt(loserBalancesBefore!.getBalance());
const loserSecurityDeposit = BigInt(ctx.disputeWinner === DisputeResult.Winner.BUYER ? ctx.offer!.getSellerSecurityDeposit() : ctx.offer!.getBuyerSecurityDeposit());
const loserPayout = loserSecurityDeposit;
expect(loserPayout - loserDifference).toBeLessThan(TestConfig.maxFee);
}
}
// test payout unlock
@ -2615,9 +2721,13 @@ async function releaseHavenoProcess(havenod: HavenoClient, deleteAppDir?: boolea
*/
function deleteHavenoInstance(havenod: HavenoClient) {
if (!havenod.getAppName()) throw new Error("Cannot delete Haveno instance owned by different process")
const userDataDir = process.env.APPDATA || (process.platform === 'darwin' ? process.env.HOME + '/Library/Application Support' : process.env.HOME + "/.local/share");
const appPath = path.normalize(userDataDir + "/" + havenod.getAppName()!);
fs.rmSync(appPath, { recursive: true, force: true });
deleteHavenoInstanceByAppName(havenod.getAppName()!);
}
function deleteHavenoInstanceByAppName(appName: string) {
const userDataDir = process.env.APPDATA || (process.platform === 'darwin' ? process.env.HOME + '/Library/Application Support' : process.env.HOME + "/.local/share");
const appPath = path.normalize(userDataDir + "/" + appName);
fs.rmSync(appPath, { recursive: true, force: true });
}
/**