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, buyerOfflineAfterTake?: boolean,
sellerOfflineAfterTake?: boolean, sellerOfflineAfterTake?: boolean,
buyerOfflineAfterPaymentSent?: boolean buyerOfflineAfterPaymentSent?: boolean
buyerOfflineAfterDisputeOpened?: boolean,
sellerOfflineAfterDisputeOpened?: boolean,
sellerDisputeContext?: DisputeContext, sellerDisputeContext?: DisputeContext,
buyerDisputeContext?: DisputeContext, buyerDisputeContext?: DisputeContext,
buyerSendsPayment?: boolean, buyerSendsPayment?: boolean,
@ -232,16 +234,14 @@ interface TradeContext {
// resolve dispute // resolve dispute
resolveDispute?: boolean resolveDispute?: boolean
arbitrator?: HavenoClient, arbitrator?: HavenoClient,
disputeOpener?: HavenoClient disputeOpener?: SaleRole,
disputePeer?: HavenoClient
disputeWinner?: DisputeResult.Winner, disputeWinner?: DisputeResult.Winner,
disputeReason?: DisputeResult.Reason, disputeReason?: DisputeResult.Reason,
disputeSummary?: string, disputeSummary?: string,
disputeWinnerAmount?: bigint disputeWinnerAmount?: bigint
// other context // other context
buyer?: HavenoClient, offer?: OfferInfo,
seller?: HavenoClient,
index?: number, index?: number,
isOfferTaken?: boolean, isOfferTaken?: boolean,
isPaymentSent?: boolean, isPaymentSent?: boolean,
@ -258,7 +258,10 @@ interface TradeContext {
maxConcurrency?: number, maxConcurrency?: number,
walletSyncPeriodMs?: number, walletSyncPeriodMs?: number,
maxTimePeerNoticeMs?: number, maxTimePeerNoticeMs?: number,
stopOnFailure?: boolean stopOnFailure?: boolean,
buyerAppName?: string,
sellerAppName?: string,
usedPorts?: string[]
} }
enum TradeRole { enum TradeRole {
@ -266,6 +269,11 @@ enum TradeRole {
TAKER = "TAKER", TAKER = "TAKER",
} }
enum SaleRole {
BUYER = "BUYER",
SELLER = "SELLER"
}
enum DisputeContext { enum DisputeContext {
NONE = "NONE", NONE = "NONE",
OPEN_AFTER_DEPOSITS_UNLOCK = "OPEN_AFTER_DEPOSITS_UNLOCK", OPEN_AFTER_DEPOSITS_UNLOCK = "OPEN_AFTER_DEPOSITS_UNLOCK",
@ -1320,7 +1328,6 @@ test("Can go offline while completing a trade (CI, sanity check)", async () => {
let traders: HavenoClient[] = []; let traders: HavenoClient[] = [];
let ctx: TradeContext = {}; let ctx: TradeContext = {};
let err: any; let err: any;
let miningStarted = false;
try { try {
// start and fund 2 trader processes // 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; ctx.buyerOfflineAfterPaymentSent = true;
// execute trade // execute trade
miningStarted = await startMining();
await executeTrade(ctx); await executeTrade(ctx);
} catch (e) { } catch (e) {
err = e; err = e;
} }
// stop traders // stop traders
if (miningStarted) await stopMining();
if (ctx.maker) await releaseHavenoProcess(ctx.maker, true); if (ctx.maker) await releaseHavenoProcess(ctx.maker, true);
if (ctx.taker) await releaseHavenoProcess(ctx.taker, true); if (ctx.taker) await releaseHavenoProcess(ctx.taker, true);
if (err) throw err; 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 () => { test("Can resolve disputes (CI)", async () => {
// take trades but stop before sending payment // take trades but stop before sending payment
@ -1720,6 +1763,28 @@ test("Selects arbitrators which are online, registered, and least used", async (
// ----------------------------- TEST HELPERS --------------------------------- // ----------------------------- 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[] { function getTradeContexts(numConfigs: number): TradeContext[] {
const configs: TradeContext[] = []; const configs: TradeContext[] = [];
for (let i = 0; i < numConfigs; i++) configs.push({}); for (let i = 0; i < numConfigs; i++) configs.push({});
@ -1734,10 +1799,10 @@ function tradeContextToString(ctx: TradeContext) {
arbitrator: ctx.arbitrator ? ctx.arbitrator.getUrl() : undefined, arbitrator: ctx.arbitrator ? ctx.arbitrator.getUrl() : undefined,
maker: ctx.maker ? ctx.maker.getUrl() : undefined, maker: ctx.maker ? ctx.maker.getUrl() : undefined,
taker: ctx.taker ? ctx.taker.getUrl() : undefined, taker: ctx.taker ? ctx.taker.getUrl() : undefined,
buyer: ctx.buyer ? ctx.buyer.getUrl() : undefined, buyer: getBuyer(ctx) ? getBuyer(ctx)?.getUrl() : undefined,
seller: ctx.seller ? ctx.seller.getUrl() : undefined, seller: getSeller(ctx) ? getSeller(ctx)?.getUrl() : undefined,
disputeOpener: ctx.disputeOpener ? ctx.disputeOpener.getUrl() : undefined, disputeOpener: ctx.maker ? getDisputeOpener(ctx)?.getUrl() : undefined,
disputePeer: ctx.disputePeer ? ctx.disputePeer.getUrl() : undefined, disputePeer: ctx.maker ? getDisputePeer(ctx)?.getUrl() : undefined,
disputeWinner: ctx.disputeWinner === DisputeResult.Winner.BUYER ? "buyer" : "seller" 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); 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 // get info before trade
const buyerBalancesBefore = await ctx.buyer!.getBalances(); const buyerBalancesBefore = await getBuyer(ctx)!.getBalances();
const sellerBalancesBefore = await ctx.seller!.getBalances(); const sellerBalancesBefore = await getSeller(ctx)!.getBalances();
// make offer if configured // make offer if configured
if (makingOffer) { if (makingOffer) {
offer = await makeOffer(ctx); ctx.offer = await makeOffer(ctx);
expect(offer.getState()).toEqual("AVAILABLE"); expect(ctx.offer.getState()).toEqual("AVAILABLE");
ctx.offerId = offer.getId(); ctx.offerId = ctx.offer.getId();
await wait(ctx.maxTimePeerNoticeMs! + ctx.walletSyncPeriodMs! * 2); 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 // 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); if (ctx.testTraderChat) await testTradeChat(ctx);
// get expected payment account payloads // get expected payment account payloads
let expectedBuyerPaymentAccountPayload = (await ctx.buyer?.getPaymentAccount(ctx.maker == ctx.buyer ? ctx.makerPaymentAccountId! : ctx.takerPaymentAccountId!))?.getPaymentAccountPayload(); let expectedBuyerPaymentAccountPayload = (await getBuyer(ctx)?.getPaymentAccount(ctx.maker == getBuyer(ctx) ? ctx.makerPaymentAccountId! : ctx.takerPaymentAccountId!))?.getPaymentAccountPayload();
let expectedSellerPaymentAccountPayload = (await ctx.seller?.getPaymentAccount(ctx.maker == ctx.buyer ? ctx.takerPaymentAccountId! : ctx.makerPaymentAccountId!))?.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 // 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 contract = fetchedTrade.getContract()!;
let buyerPaymentAccountPayload = contract.getIsBuyerMakerAndSellerTaker() ? contract.getMakerPaymentAccountPayload() : contract.getTakerPaymentAccountPayload(); let buyerPaymentAccountPayload = contract.getIsBuyerMakerAndSellerTaker() ? contract.getMakerPaymentAccountPayload() : contract.getTakerPaymentAccountPayload();
if (ctx.isPaymentSent) expect(buyerPaymentAccountPayload).toEqual(expectedBuyerPaymentAccountPayload); if (ctx.isPaymentSent) expect(buyerPaymentAccountPayload).toEqual(expectedBuyerPaymentAccountPayload);
else expect(buyerPaymentAccountPayload).toBeUndefined(); else expect(buyerPaymentAccountPayload).toBeUndefined();
// shut down buyer and seller if configured // 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 promises: Promise<void>[] = [];
const buyerAppName = ctx.buyer!.getAppName(); ctx.buyerAppName = getBuyer(ctx)!.getAppName();
if (ctx.buyerOfflineAfterTake) { if (ctx.buyerOfflineAfterTake) {
promises.push(releaseHavenoProcess(ctx.buyer!)); promises.push(releaseHavenoProcess(getBuyer(ctx)!));
ctx.buyer = undefined; // TODO: don't track them separately? if (isBuyerMaker(ctx)) ctx.maker = undefined;
if (isBuyerMaker) ctx.maker = undefined;
else ctx.taker = undefined; else ctx.taker = undefined;
} }
const sellerAppName = ctx.seller!.getAppName(); ctx.sellerAppName = getSeller(ctx)!.getAppName();
if (ctx.sellerOfflineAfterTake) { if (ctx.sellerOfflineAfterTake) {
promises.push(releaseHavenoProcess(ctx.seller!)); promises.push(releaseHavenoProcess(getSeller(ctx)!));
ctx.seller = undefined; if (isBuyerMaker(ctx)) ctx.taker = undefined;
if (isBuyerMaker) ctx.taker = undefined;
else ctx.maker = undefined; else ctx.maker = undefined;
} }
await Promise.all(promises); await Promise.all(promises);
@ -1908,115 +1961,127 @@ async function executeTrade(ctx?: TradeContext): Promise<string> {
// wait for deposit txs to unlock // wait for deposit txs to unlock
await waitForUnlockedTxs(trade.getMakerDepositTxId(), trade.getTakerDepositTxId()); await waitForUnlockedTxs(trade.getMakerDepositTxId(), trade.getTakerDepositTxId());
// buyer comes online if offline // buyer comes online if offline and used
if (ctx.buyerOfflineAfterTake) { if (ctx.buyerOfflineAfterTake && ((ctx.buyerSendsPayment && !ctx.isPaymentSent && ctx.sellerDisputeContext !== DisputeContext.OPEN_AFTER_DEPOSITS_UNLOCK) || (ctx.buyerDisputeContext === DisputeContext.OPEN_AFTER_DEPOSITS_UNLOCK && !ctx.buyerOpenedDispute))) {
ctx.buyer = await initHaveno({appName: buyerAppName, excludePorts: usedPorts}); const buyer = await initHaveno({appName: ctx.buyerAppName, excludePorts: ctx.usedPorts});
if (isBuyerMaker) ctx.maker = ctx.buyer; if (isBuyerMaker(ctx)) ctx.maker = buyer;
else ctx.taker = ctx.buyer; else ctx.taker = buyer;
usedPorts.push(getPort(ctx.buyer!.getUrl())); ctx.usedPorts.push(getPort(buyer.getUrl()));
} }
// test trade states // wait for traders to observe
await wait(TestConfig.maxWalletStartupMs + ctx.walletSyncPeriodMs! * 2); 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? const expectedState = ctx.isPaymentSent ? "PAYMENT_SENT" : "DEPOSITS_UNLOCKED" // TODO: test COMPLETED, PAYMENT_RECEIVED states?
expect((await ctx.buyer!.getTrade(offer!.getId())).getPhase()).toEqual(expectedState); if (getBuyer(ctx)) {
fetchedTrade = await ctx.buyer!.getTrade(ctx.offerId!); 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.getIsDepositUnlocked()).toBe(true);
expect(fetchedTrade.getPhase()).toEqual(expectedState); expect(fetchedTrade.getPhase()).toEqual(expectedState);
if (!ctx.sellerOfflineAfterTake) { }
fetchedTrade = await ctx.seller!.getTrade(trade.getTradeId());
// test seller trade state if online
if (getSeller(ctx)) {
fetchedTrade = await getSeller(ctx)!.getTrade(trade.getTradeId());
expect(fetchedTrade.getIsDepositUnlocked()).toBe(true); expect(fetchedTrade.getIsDepositUnlocked()).toBe(true);
expect(fetchedTrade.getPhase()).toEqual(expectedState); expect(fetchedTrade.getPhase()).toEqual(expectedState);
} }
// buyer has seller's payment account payload after first confirmation // buyer has seller's payment account payload after first confirmation
fetchedTrade = await ctx.buyer!.getTrade(ctx.offerId!); let sellerPaymentAccountPayload;
let form;
let expectedForm;
if (getBuyer(ctx)) {
fetchedTrade = await getBuyer(ctx)!.getTrade(ctx.offerId!);
contract = fetchedTrade.getContract()!; contract = fetchedTrade.getContract()!;
let sellerPaymentAccountPayload = contract.getIsBuyerMakerAndSellerTaker() ? contract.getTakerPaymentAccountPayload() : contract.getMakerPaymentAccountPayload(); sellerPaymentAccountPayload = contract.getIsBuyerMakerAndSellerTaker() ? contract.getTakerPaymentAccountPayload() : contract.getMakerPaymentAccountPayload();
expect(sellerPaymentAccountPayload).toEqual(expectedSellerPaymentAccountPayload); expect(sellerPaymentAccountPayload).toEqual(expectedSellerPaymentAccountPayload);
let form = await ctx.buyer!.getPaymentAccountPayloadForm(sellerPaymentAccountPayload!); form = await getBuyer(ctx)!.getPaymentAccountPayloadForm(sellerPaymentAccountPayload!);
let expectedForm = await ctx.buyer!.getPaymentAccountPayloadForm(expectedSellerPaymentAccountPayload!); expectedForm = await getBuyer(ctx)!.getPaymentAccountPayloadForm(expectedSellerPaymentAccountPayload!);
expect(HavenoUtils.formToString(form)).toEqual(HavenoUtils.formToString(expectedForm)); expect(HavenoUtils.formToString(form)).toEqual(HavenoUtils.formToString(expectedForm));
}
// buyer notified to send payment TODO // buyer notified to send payment TODO
// open dispute(s) if configured // open dispute(s) if configured
if (ctx.buyerDisputeContext === DisputeContext.OPEN_AFTER_DEPOSITS_UNLOCK && !ctx.buyerOpenedDispute) { 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.buyerOpenedDispute = true;
ctx.disputeOpener = ctx.buyer; ctx.disputeOpener = SaleRole.BUYER;
} }
if (ctx.sellerDisputeContext === DisputeContext.OPEN_AFTER_DEPOSITS_UNLOCK && !ctx.sellerOpenedDispute) { 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; 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);
} }
// if dispute opened, resolve dispute if configured and return // handle opened dispute
if (ctx.disputeOpener) { if (ctx.disputeOpener) {
// test open dispute
await testOpenDispute(ctx);
// resolve dispute if configured
if (ctx.resolveDispute) await resolveDispute(ctx); if (ctx.resolveDispute) await resolveDispute(ctx);
// return offer id
return ctx.offerId!; return ctx.offerId!;
} }
// buyer confirms payment is sent // buyer confirms payment is sent
if (!ctx.buyerSendsPayment) return offer!.getId(); if (!ctx.buyerSendsPayment) return ctx.offer!.getId();
else if (!ctx.isPaymentSent) { else if (!ctx.isPaymentSent) {
HavenoUtils.log(1, "Buyer confirming payment sent"); HavenoUtils.log(1, "Buyer confirming payment sent");
await ctx.buyer!.confirmPaymentStarted(trade.getTradeId()); await getBuyer(ctx)!.confirmPaymentStarted(trade.getTradeId());
ctx.isPaymentSent = true; ctx.isPaymentSent = true;
fetchedTrade = await ctx.buyer!.getTrade(trade.getTradeId()); fetchedTrade = await getBuyer(ctx)!.getTrade(trade.getTradeId());
expect(fetchedTrade.getPhase()).toEqual("PAYMENT_SENT"); expect(fetchedTrade.getPhase()).toEqual("PAYMENT_SENT");
} }
// buyer goes offline if configured // buyer goes offline if configured
if (ctx.buyerOfflineAfterPaymentSent) { if (ctx.buyerOfflineAfterPaymentSent) {
await releaseHavenoProcess(ctx.buyer!); await releaseHavenoProcess(getBuyer(ctx)!);
ctx.buyer = undefined; if (isBuyerMaker(ctx)) ctx.maker = undefined;
if (isBuyerMaker) ctx.maker = undefined;
else ctx.taker = undefined; else ctx.taker = undefined;
} }
// seller comes online if offline // seller comes online if offline
if (!ctx.seller) { if (!getSeller(ctx)) {
ctx.seller = await initHaveno({appName: sellerAppName, excludePorts: usedPorts}); const seller = await initHaveno({appName: ctx.sellerAppName, excludePorts: ctx.usedPorts});
if (isBuyerMaker) ctx.taker = ctx.seller; if (isBuyerMaker(ctx)) ctx.taker = seller;
else ctx.maker = ctx.seller; else ctx.maker = seller;
usedPorts.push(getPort(ctx.seller!.getUrl())) ctx.usedPorts.push(getPort(getSeller(ctx)!.getUrl()))
} }
// seller notified payment is sent // seller notified payment is sent
await wait(ctx.maxTimePeerNoticeMs! + TestConfig.maxWalletStartupMs); // TODO: test notification await wait(ctx.maxTimePeerNoticeMs! + TestConfig.maxWalletStartupMs); // TODO: test notification
if (ctx.sellerOfflineAfterTake) await wait(ctx.walletSyncPeriodMs!); // wait to process mailbox messages 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.getPhase()).toEqual("PAYMENT_SENT");
expect(fetchedTrade.getPayoutState()).toEqual("PAYOUT_UNPUBLISHED"); expect(fetchedTrade.getPayoutState()).toEqual("PAYOUT_UNPUBLISHED");
// seller has buyer's payment account payload after payment sent // 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()!; contract = fetchedTrade.getContract()!;
buyerPaymentAccountPayload = contract.getIsBuyerMakerAndSellerTaker() ? contract.getMakerPaymentAccountPayload() : contract.getTakerPaymentAccountPayload(); buyerPaymentAccountPayload = contract.getIsBuyerMakerAndSellerTaker() ? contract.getMakerPaymentAccountPayload() : contract.getTakerPaymentAccountPayload();
expect(buyerPaymentAccountPayload).toEqual(expectedBuyerPaymentAccountPayload); expect(buyerPaymentAccountPayload).toEqual(expectedBuyerPaymentAccountPayload);
form = await ctx.seller!.getPaymentAccountPayloadForm(sellerPaymentAccountPayload!); form = await getSeller(ctx)!.getPaymentAccountPayloadForm(sellerPaymentAccountPayload!);
expectedForm = await ctx.seller!.getPaymentAccountPayloadForm(expectedSellerPaymentAccountPayload!); expectedForm = await getSeller(ctx)!.getPaymentAccountPayloadForm(expectedSellerPaymentAccountPayload!);
expect(HavenoUtils.formToString(form)).toEqual(HavenoUtils.formToString(expectedForm)); expect(HavenoUtils.formToString(form)).toEqual(HavenoUtils.formToString(expectedForm));
// open dispute(s) if configured // open dispute(s) if configured
if (ctx.buyerDisputeContext === DisputeContext.OPEN_AFTER_PAYMENT_SENT && !ctx.buyerOpenedDispute) { 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; 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) { 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; ctx.sellerOpenedDispute = true;
if (!ctx.disputeOpener) ctx.disputeOpener = ctx.seller; if (!ctx.disputeOpener) ctx.disputeOpener = SaleRole.SELLER;
} }
if (ctx.disputeOpener) { if (ctx.disputeOpener) {
ctx.disputePeer = ctx.disputeOpener === ctx.buyer ? ctx.seller : ctx.buyer;
await testOpenDispute(ctx); await testOpenDispute(ctx);
} }
@ -2027,45 +2092,45 @@ async function executeTrade(ctx?: TradeContext): Promise<string> {
} }
// seller confirms payment is received // seller confirms payment is received
if (!ctx.sellerReceivesPayment) return offer!.getId(); if (!ctx.sellerReceivesPayment) return ctx.offer!.getId();
else if (!ctx.isPaymentReceived) { else if (!ctx.isPaymentReceived) {
HavenoUtils.log(1, "Seller confirming payment received"); HavenoUtils.log(1, "Seller confirming payment received");
await ctx.seller.confirmPaymentReceived(trade.getTradeId()); await getSeller(ctx)!.confirmPaymentReceived(trade.getTradeId());
ctx.isPaymentReceived = true; ctx.isPaymentReceived = true;
fetchedTrade = await ctx.seller.getTrade(trade.getTradeId()); fetchedTrade = await getSeller(ctx)!.getTrade(trade.getTradeId());
expect(fetchedTrade.getPhase()).toEqual("PAYMENT_RECEIVED"); expect(fetchedTrade.getPhase()).toEqual("PAYMENT_RECEIVED");
await wait(ctx.walletSyncPeriodMs!); // buyer or arbitrator will sign and publish payout tx 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 // payout tx is published by buyer (priority) or arbitrator
await wait(ctx.walletSyncPeriodMs!); 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 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 // buyer comes online if offline
if (ctx.buyerOfflineAfterPaymentSent) { if (ctx.buyerOfflineAfterPaymentSent) {
ctx.buyer = await initHaveno({appName: buyerAppName, excludePorts: usedPorts}); const buyer = await initHaveno({appName: ctx.buyerAppName, excludePorts: ctx.usedPorts});
if (isBuyerMaker) ctx.maker = ctx.buyer; if (isBuyerMaker(ctx)) ctx.maker = buyer;
else ctx.taker = ctx.buyer; else ctx.taker = buyer;
usedPorts.push(getPort(ctx.buyer!.getUrl())); ctx.usedPorts.push(getPort(buyer.getUrl()));
HavenoUtils.log(1, "Done starting buyer"); HavenoUtils.log(1, "Done starting buyer");
await wait(TestConfig.maxWalletStartupMs + ctx.walletSyncPeriodMs!); 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 // test trade completion
await ctx.buyer!.completeTrade(trade.getTradeId()); await getBuyer(ctx)!.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 testTradeState(await getBuyer(ctx)!.getTrade(trade.getTradeId()), {phase: "COMPLETED", payoutState: ["PAYOUT_PUBLISHED", "PAYOUT_CONFIRMED", "PAYOUT_UNLOCKED"], isCompleted: true, isPayoutPublished: true});
await ctx.seller!.completeTrade(trade.getTradeId()); await getSeller(ctx)!.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 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 // test balances after payout tx unless other trades can interfere
if (!ctx.concurrentTrades) { if (!ctx.concurrentTrades) {
const buyerBalancesAfter = await ctx.buyer!.getBalances(); const buyerBalancesAfter = await getBuyer(ctx)!.getBalances();
const sellerBalancesAfter = await ctx.seller.getBalances(); const sellerBalancesAfter = await getSeller(ctx)!.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 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(offer!.getAmount()) - (BigInt(sellerBalancesAfter.getBalance()) + BigInt(sellerBalancesAfter.getReservedOfferBalance())); // seller 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).toBeLessThanOrEqual(TestConfig.maxFee);
expect(buyerFee).toBeGreaterThan(BigInt("0")); expect(buyerFee).toBeGreaterThan(BigInt("0"));
expect(sellerFee).toBeLessThanOrEqual(TestConfig.maxFee); expect(sellerFee).toBeLessThanOrEqual(TestConfig.maxFee);
@ -2074,7 +2139,8 @@ async function executeTrade(ctx?: TradeContext): Promise<string> {
// test payout unlock // test payout unlock
await testTradePayoutUnlock(ctx); 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) { } catch (err) {
HavenoUtils.log(0, "Error executing trade " + ctx!.offerId + (ctx!.index === undefined ? "" : " at index " + ctx!.index) + ": " + err.message); HavenoUtils.log(0, "Error executing trade " + ctx!.offerId + (ctx!.index === undefined ? "" : " at index " + ctx!.index) + ": " + err.message);
HavenoUtils.log(0, tradeContextToString(ctx!)); HavenoUtils.log(0, tradeContextToString(ctx!));
@ -2086,24 +2152,24 @@ async function testTradePayoutUnlock(ctx: TradeContext) {
const height = await monerod.getHeight(); const height = await monerod.getHeight();
// test after payout confirmed // test after payout confirmed
const payoutTxId = (await ctx.buyer!.getTrade(ctx.offerId!)).getPayoutTxId(); const payoutTxId = (await ctx.arbitrator!.getTrade(ctx.offerId!)).getPayoutTxId();
let trade = await ctx.buyer!.getTrade(ctx.offerId!); let trade = await ctx.arbitrator!.getTrade(ctx.offerId!);
if (trade.getPayoutState() !== "PAYOUT_CONFIRMED") await mineToHeight(height + 1); if (trade.getPayoutState() !== "PAYOUT_CONFIRMED") await mineToHeight(height + 1);
await wait(TestConfig.maxWalletStartupMs + ctx.walletSyncPeriodMs! * 2); await wait(TestConfig.maxWalletStartupMs + ctx.walletSyncPeriodMs! * 2);
await testTradeState(await ctx.buyer!.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"]});
await testTradeState(await ctx.seller!.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 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()); expect(payoutTx?.getIsConfirmed());
// test after payout unlocked // 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); if (trade.getPayoutState() !== "PAYOUT_UNLOCKED") await mineToHeight(height + 10);
await wait(TestConfig.maxWalletStartupMs + ctx.walletSyncPeriodMs! * 2); await wait(TestConfig.maxWalletStartupMs + ctx.walletSyncPeriodMs! * 2);
await testTradeState(await ctx.buyer!.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"]});
await testTradeState(await ctx.seller!.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 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()); expect(!payoutTx?.getIsLocked());
} }
@ -2235,15 +2301,21 @@ async function takeOffer(ctx: TradeContext): Promise<TradeInfo> {
async function testOpenDispute(ctx: TradeContext) { 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 // 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.getTradeId()).toEqual(ctx.offerId);
expect(openerDispute.getIsOpener()).toBe(true); 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 // get non-existing dispute should fail
try { try {
await ctx.disputeOpener!.getDispute("invalid"); await getDisputeOpener(ctx)!.getDispute("invalid");
throw new Error("get dispute with invalid id should fail"); throw new Error("get dispute with invalid id should fail");
} catch (err: any) { } catch (err: any) {
assert.equal(err.message, "dispute for trade id 'invalid' not found"); assert.equal(err.message, "dispute for trade id 'invalid' not found");
@ -2251,7 +2323,7 @@ async function testOpenDispute(ctx: TradeContext) {
// peer sees the dispute // peer sees the dispute
await wait(ctx.maxTimePeerNoticeMs! + TestConfig.maxWalletStartupMs); 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.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 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 // arbitrator has seller's payment account info
let sellerPaymentAccountPayload = arbDisputeOpener.getContract()!.getIsBuyerMakerAndSellerTaker() ? arbDisputeOpener.getTakerPaymentAccountPayload() : arbDisputeOpener.getMakerPaymentAccountPayload(); 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(sellerPaymentAccountPayload).toEqual(expectedSellerPaymentAccountPayload);
expect(await ctx.arbitrator?.getPaymentAccountPayloadForm(sellerPaymentAccountPayload!)).toEqual(await ctx.arbitrator?.getPaymentAccountPayloadForm(expectedSellerPaymentAccountPayload!)); expect(await ctx.arbitrator?.getPaymentAccountPayloadForm(sellerPaymentAccountPayload!)).toEqual(await ctx.arbitrator?.getPaymentAccountPayloadForm(expectedSellerPaymentAccountPayload!));
sellerPaymentAccountPayload = arbDisputePeer.getContract()!.getIsBuyerMakerAndSellerTaker() ? arbDisputePeer.getTakerPaymentAccountPayload() : arbDisputeOpener.getMakerPaymentAccountPayload(); 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 // arbitrator has buyer's payment account info unless opener is seller and payment not sent
let buyerPaymentAccountPayload = arbDisputeOpener.getContract()!.getIsBuyerMakerAndSellerTaker() ? arbDisputeOpener.getMakerPaymentAccountPayload() : arbDisputeOpener.getTakerPaymentAccountPayload(); 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 { else {
let expectedBuyerPaymentAccountPayload = (await ctx.buyer?.getPaymentAccount(buyerPaymentAccountPayload?.getId()!))?.getPaymentAccountPayload(); let expectedBuyerPaymentAccountPayload = (await getBuyer(ctx)?.getPaymentAccount(buyerPaymentAccountPayload?.getId()!))?.getPaymentAccountPayload();
expect(buyerPaymentAccountPayload).toEqual(expectedBuyerPaymentAccountPayload); expect(buyerPaymentAccountPayload).toEqual(expectedBuyerPaymentAccountPayload);
expect(await ctx.arbitrator?.getPaymentAccountPayloadForm(buyerPaymentAccountPayload!)).toEqual(await ctx.arbitrator?.getPaymentAccountPayloadForm(expectedBuyerPaymentAccountPayload!)); expect(await ctx.arbitrator?.getPaymentAccountPayloadForm(buyerPaymentAccountPayload!)).toEqual(await ctx.arbitrator?.getPaymentAccountPayloadForm(expectedBuyerPaymentAccountPayload!));
} }
buyerPaymentAccountPayload = arbDisputePeer.getContract()!.getIsBuyerMakerAndSellerTaker() ? arbDisputePeer.getMakerPaymentAccountPayload() : arbDisputePeer.getTakerPaymentAccountPayload(); 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 { else {
let expectedBuyerPaymentAccountPayload = (await ctx.buyer?.getPaymentAccount(buyerPaymentAccountPayload?.getId()!))?.getPaymentAccountPayload(); let expectedBuyerPaymentAccountPayload = (await getBuyer(ctx)?.getPaymentAccount(buyerPaymentAccountPayload?.getId()!))?.getPaymentAccountPayload();
expect(buyerPaymentAccountPayload).toEqual(expectedBuyerPaymentAccountPayload); expect(buyerPaymentAccountPayload).toEqual(expectedBuyerPaymentAccountPayload);
expect(await ctx.arbitrator?.getPaymentAccountPayloadForm(buyerPaymentAccountPayload!)).toEqual(await ctx.arbitrator?.getPaymentAccountPayloadForm(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 disputeOpenerNotifications: NotificationMessage[] = [];
const disputePeerNotifications: NotificationMessage[] = []; const disputePeerNotifications: NotificationMessage[] = [];
const arbitratorNotifications: 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 getDisputeOpener(ctx)!.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 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); }); 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 // arbitrator sends chat messages to traders
@ -2312,14 +2384,14 @@ async function testOpenDispute(ctx: TradeContext) {
attachment2.setBytes(bytes2); attachment2.setBytes(bytes2);
attachment2.setFileName("proof.png"); attachment2.setFileName("proof.png");
HavenoUtils.log(2, "Dispute opener sending chat message to arbitrator. tradeId=" + ctx.offerId + ", disputeId=" + openerDispute.getId()); 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 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()); 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 // test trader chat messages
await wait(ctx.maxTimePeerNoticeMs!); await wait(ctx.maxTimePeerNoticeMs!);
let dispute = await ctx.disputeOpener!.getDispute(ctx.offerId!); let dispute = await getDisputeOpener(ctx)!.getDispute(ctx.offerId!);
let messages = dispute.getChatMessageList(); let messages = dispute.getChatMessageList();
expect(messages.length).toBeGreaterThanOrEqual(3); // last messages are chat, first messages are system message and possibly DisputeOpenedMessage acks 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"); 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[0].getBytes()).toEqual(bytes);
expect(attachments[1].getFileName()).toEqual("proof.png"); expect(attachments[1].getFileName()).toEqual("proof.png");
expect(attachments[1].getBytes()).toEqual(bytes2); expect(attachments[1].getBytes()).toEqual(bytes2);
dispute = await ctx.disputePeer!.getDispute(ctx.offerId!); dispute = await getDisputePeer(ctx)!.getDispute(ctx.offerId!);
messages = dispute.getChatMessageList(); messages = dispute.getChatMessageList();
expect(messages.length).toBeGreaterThanOrEqual(3); expect(messages.length).toBeGreaterThanOrEqual(3);
expect(messages[messages.length - 2].getMessage()).toEqual("Arbitrator chat message to dispute peer"); 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) { 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 // award too little to loser
const offer = (await ctx.maker!.getTrade(ctx.offerId!)).getOffer(); const tradeAmount: bigint = BigInt(ctx.offer!.getAmount());
const tradeAmount: bigint = BigInt(offer!.getAmount()); const customWinnerAmount = tradeAmount + BigInt(ctx.offer!.getBuyerSecurityDeposit() + ctx.offer!.getSellerSecurityDeposit()) - BigInt("10000");
const customWinnerAmount = tradeAmount + BigInt(offer!.getBuyerSecurityDeposit() + offer!.getSellerSecurityDeposit()) - BigInt("10000");
try { try {
await arbitrator.resolveDispute(ctx.offerId!, ctx.disputeWinner!, ctx.disputeReason!, "Loser gets too little", customWinnerAmount); 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"); throw new Error("Should have failed resolving dispute with insufficient loser payout");
@ -2371,42 +2457,62 @@ async function resolveDispute(ctx: TradeContext) {
} }
// resolve dispute according to configuration // resolve dispute according to configuration
const winner = ctx.disputeWinner === DisputeResult.Winner.BUYER ? ctx.buyer : ctx.seller; const winner = ctx.disputeWinner === DisputeResult.Winner.BUYER ? getBuyer(ctx) : getSeller(ctx);
const loser = ctx.disputeWinner === DisputeResult.Winner.BUYER ? ctx.seller : ctx.buyer; const loser = ctx.disputeWinner === DisputeResult.Winner.BUYER ? getSeller(ctx) : getBuyer(ctx);
const winnerBalancesBefore = await winner!.getBalances(); const winnerBalancesBefore = winner ? await winner!.getBalances() : undefined;
const loserBalancesBefore = await loser!.getBalances(); const loserBalancesBefore = loser ? await loser!.getBalances() : undefined;
HavenoUtils.log(1, "Resolving dispute for trade " + ctx.offerId); HavenoUtils.log(1, "Resolving dispute for trade " + ctx.offerId);
const startTime = Date.now(); const startTime = Date.now();
await arbitrator.resolveDispute(ctx.offerId!, ctx.disputeWinner!, ctx.disputeReason!, ctx.disputeSummary!, ctx.disputeWinnerAmount); await arbitrator.resolveDispute(ctx.offerId!, ctx.disputeWinner!, ctx.disputeReason!, ctx.disputeSummary!, ctx.disputeWinnerAmount);
HavenoUtils.log(1, "Done resolving dispute (" + (Date.now() - startTime) + ")"); 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 // test resolved dispute
await wait(TestConfig.maxWalletStartupMs + ctx.walletSyncPeriodMs! * 2); await wait(TestConfig.maxWalletStartupMs + ctx.walletSyncPeriodMs! * 2);
let dispute = await ctx.disputeOpener!.getDispute(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); 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 (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 // 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}); 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});
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 (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}); 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 // test balances after payout tx unless concurrent trades
if (!ctx.concurrentTrades) { if (!ctx.concurrentTrades) {
if (winner) {
const winnerBalancesAfter = await winner!.getBalances(); const winnerBalancesAfter = await winner!.getBalances();
const loserBalancesAfter = await loser!.getBalances(); const winnerDifference = BigInt(winnerBalancesAfter.getBalance()) - BigInt(winnerBalancesBefore!.getBalance());
const winnerDifference = BigInt(winnerBalancesAfter.getBalance()) - BigInt(winnerBalancesBefore.getBalance()); const winnerSecurityDeposit = BigInt(ctx.disputeWinner === DisputeResult.Winner.BUYER ? ctx.offer!.getBuyerSecurityDeposit() : ctx.offer!.getSellerSecurityDeposit())
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 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(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); expect(loserPayout - loserDifference).toBeLessThan(TestConfig.maxFee);
} }
}
// test payout unlock // test payout unlock
await testTradePayoutUnlock(ctx); await testTradePayoutUnlock(ctx);
@ -2615,8 +2721,12 @@ async function releaseHavenoProcess(havenod: HavenoClient, deleteAppDir?: boolea
*/ */
function deleteHavenoInstance(havenod: HavenoClient) { function deleteHavenoInstance(havenod: HavenoClient) {
if (!havenod.getAppName()) throw new Error("Cannot delete Haveno instance owned by different process") if (!havenod.getAppName()) throw new Error("Cannot delete Haveno instance owned by different process")
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 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()!); const appPath = path.normalize(userDataDir + "/" + appName);
fs.rmSync(appPath, { recursive: true, force: true }); fs.rmSync(appPath, { recursive: true, force: true });
} }