test support buying xmr without deposit or fee using passphrase

This commit is contained in:
woodser 2024-12-16 10:27:00 -05:00
parent 8153371cf2
commit 7b88f7d261
2 changed files with 170 additions and 60 deletions

View File

@ -63,7 +63,7 @@ enum BaseCurrencyNetwork {
}
// clients
const startupHavenods: HavenoClient[] = [];
const startupHavenodUrls: string[] = [];
let arbitrator: HavenoClient;
let user1: HavenoClient;
let user2: HavenoClient;
@ -148,7 +148,9 @@ const defaultTradeConfig: Partial<TradeContext> = {
stopOnFailure: false, // TODO: setting to true can cause error: Http response at 400 or 500 level, http status code: 503
testPayoutConfirmed: true,
testPayoutUnlocked: false,
maxConcurrency: getMaxConcurrency()
maxConcurrency: getMaxConcurrency(),
isPrivateOffer: false,
buyerAsTakerWithoutDeposit: undefined
}
/**
@ -188,11 +190,14 @@ class TradeContext {
priceMargin?: number;
triggerPrice?: number;
reserveExactAmount?: boolean;
isPrivateOffer?: boolean;
buyerAsTakerWithoutDeposit?: boolean; // buyer as taker security deposit is optional for private offers
// take offer
awaitFundsToTakeOffer?: boolean;
offerId?: string;
takerPaymentAccountId?: string;
challenge?: string;
testTraderChat?: boolean;
// resolve dispute
@ -287,6 +292,10 @@ class TradeContext {
return this.isPaymentReceived ? "PAYMENT_RECEIVED" : this.isPaymentSent ? "PAYMENT_SENT" : "DEPOSITS_UNLOCKED";
}
hasBuyerAsTakerWithoutDeposit() {
return this.getBuyer() === this.getTaker() && this.buyerAsTakerWithoutDeposit;
}
static init(ctxP: Partial<TradeContext> | undefined): TradeContext {
let ctx = ctxP instanceof TradeContext ? ctxP : new TradeContext(ctxP);
if (!ctx.offerAmount && ctx.tradeAmount) ctx.offerAmount = ctx.tradeAmount;
@ -487,7 +496,7 @@ interface TxContext {
}
// track started haveno processes
const HAVENO_PROCESSES: HavenoClient[] = [];
const HAVENO_CLIENTS: HavenoClient[] = [];
const HAVENO_PROCESS_PORTS: string[] = [];
const HAVENO_WALLETS: Map<HavenoClient, any> = new Map<HavenoClient, any>();
@ -506,6 +515,10 @@ function isGitHubActions() {
jest.setTimeout(TestConfig.testTimeout);
beforeEach(async () => {
HavenoUtils.log(0, "BEFORE TEST \"" + expect.getState().currentTestName + "\"");
});
beforeAll(async () => {
try {
@ -532,11 +545,15 @@ beforeAll(async () => {
}
// start configured haveno daemons
const startupHavenods: HavenoClient[] = [];
const promises: Promise<HavenoClient>[] = [];
let err;
for (const config of TestConfig.startupHavenods) promises.push(initHaveno(config));
for (const settledPromise of await Promise.allSettled(promises)) {
if (settledPromise.status === "fulfilled") startupHavenods.push((settledPromise as PromiseFulfilledResult<HavenoClient>).value);
if (settledPromise.status === "fulfilled") {
startupHavenods.push((settledPromise as PromiseFulfilledResult<HavenoClient>).value);
startupHavenodUrls.push(startupHavenods[startupHavenods.length - 1].getUrl());
}
else if (!err) err = new Error((settledPromise as PromiseRejectedResult).reason);
}
if (err) throw err;
@ -564,8 +581,27 @@ beforeAll(async () => {
}
});
beforeEach(async () => {
HavenoUtils.log(0, "BEFORE TEST \"" + expect.getState().currentTestName + "\"");
afterEach(async () => {
// collect unreleased clients
const unreleasedClients: HavenoClient[] = [];
for (const havenod of HAVENO_CLIENTS.slice()) {
if (!moneroTs.GenUtils.arrayContains(startupHavenodUrls, havenod.getUrl())) {
unreleasedClients.push(havenod);
}
}
// release clients
if (unreleasedClients.length > 0) {
const promises: Promise<void>[] = [];
HavenoUtils.log(0, unreleasedClients.length + " Haveno clients were not released after test \"" + expect.getState().currentTestName + "\", releasing...");
for (const client of unreleasedClients) {
HavenoUtils.log(0, "\tUnreleased Haveno client: " + client.getUrl());
promises.push(releaseHavenoClient(client));
}
await Promise.all(promises);
HavenoUtils.log(0, "Done releasing " + unreleasedClients.length + " unreleased Haveno clients");
}
});
afterAll(async () => {
@ -574,10 +610,10 @@ afterAll(async () => {
async function shutDown() {
// release haveno processes
// release all clients
const promises: Promise<void>[] = [];
for (const havenod of startupHavenods) {
promises.push(havenod.getProcess() ? releaseHavenoProcess(havenod) : havenod.disconnect());
for (const client of HAVENO_CLIENTS.slice()) {
promises.push(releaseHavenoClient(client));
}
await Promise.all(promises);
@ -1235,7 +1271,7 @@ test("Can get my offers (CI)", async () => {
for (const assetCode of TestConfig.assetCodes) {
const offers: OfferInfo[] = await user1.getMyOffers(assetCode);
for (const offer of offers) {
testOffer(offer);
testOffer(offer, undefined, true);
expect(assetCode).toEqual(isCrypto(assetCode) ? offer.getBaseCurrencyCode() : offer.getCounterCurrencyCode()); // crypto asset codes are base
}
}
@ -1659,6 +1695,24 @@ test("Cannot post offer exceeding trade limit (CI, sanity check)", async () => {
if (err.message.indexOf("amount is larger than") < 0) throw err;
}
// test posting sell offer above limit without buyer deposit
try {
await executeTrade({
offerAmount: moneroTs.MoneroUtils.xmrToAtomicUnits(1.1), // limit is 1 xmr without deposit or fee
offerMinAmount: moneroTs.MoneroUtils.xmrToAtomicUnits(0.25),
direction: OfferDirection.SELL,
assetCode: assetCode,
makerPaymentAccountId: account.getId(),
isPrivateOffer: true,
buyerAsTakerWithoutDeposit: true,
takeOffer: false,
price: 142.23
});
throw new Error("Should have rejected posting offer above trade limit")
} catch (err: any) {
if (err.message.indexOf("amount is larger than") < 0) throw err;
}
// test that sell limit is higher than buy limit
let offerId = await executeTrade({
offerAmount: 2100000000000n,
@ -1670,7 +1724,7 @@ test("Cannot post offer exceeding trade limit (CI, sanity check)", async () => {
await user1.removeOffer(offerId);
});
test("Can complete a trade within a range", async () => {
test("Can complete a trade within a range and without a buyer deposit", async () => {
// create payment accounts
let paymentMethodId = "cash_at_atm";
@ -1682,7 +1736,7 @@ test("Can complete a trade within a range", async () => {
const tradeStatisticsPre = await arbitrator.getTradeStatistics();
// execute trade
const offerAmount = HavenoUtils.xmrToAtomicUnits(2);
const offerAmount = HavenoUtils.xmrToAtomicUnits(1);
const offerMinAmount = HavenoUtils.xmrToAtomicUnits(.15);
const tradeAmount = getRandomBigIntWithinRange(offerMinAmount, offerAmount);
const ctx: Partial<TradeContext> = {
@ -1693,9 +1747,11 @@ test("Can complete a trade within a range", async () => {
testPayoutUnlocked: true, // override to test unlock
makerPaymentAccountId: makerPaymentAccount.getId(),
takerPaymentAccountId: takerPaymentAccount.getId(),
direction: OfferDirection.SELL,
assetCode: assetCode,
testBalanceChangeEndToEnd: true
testBalanceChangeEndToEnd: true,
direction: OfferDirection.SELL,
isPrivateOffer: true,
buyerAsTakerWithoutDeposit: true
}
await executeTrade(ctx);
@ -1751,7 +1807,7 @@ test("Can complete all trade combinations (stress)", async () => {
disputeSummary: "After much deliberation, " + (DISPUTE_WINNER_OPTS[m] === DisputeResult.Winner.BUYER ? "buyer" : "seller") + " is winner",
offerAmount: getRandomBigIntWithinPercent(TestConfig.trade.offerAmount!, 0.15)
};
ctxs.push(Object.assign({}, new TradeContext(TestConfig.trade), ctx));
ctxs.push(new TradeContext(Object.assign({}, new TradeContext(TestConfig.trade), ctx)));
}
}
}
@ -1760,7 +1816,12 @@ test("Can complete all trade combinations (stress)", async () => {
// execute trades
const ctxIdx = undefined; // run single index for debugging
if (ctxIdx !== undefined) ctxs = ctxs.slice(ctxIdx, ctxIdx + 1);
if (ctxIdx !== undefined) {
ctxs = ctxs.slice(ctxIdx, ctxIdx + 1);
HavenoUtils.log(0, "Executing single trade configuration");
console.log(ctxs[0]);
console.log(await ctxs[0].toSummary());
}
HavenoUtils.log(0, "Executing " + ctxs.length + " trade configurations");
await executeTrades(ctxs);
});
@ -1910,7 +1971,7 @@ test("Can go offline while resolving a dispute (CI)", async () => {
buyerOfflineAfterTake: true,
sellerOfflineAfterDisputeOpened: true,
buyerOfflineAfterDisputeOpened: false,
sellerDisputeContext: DisputeContext.OPEN_AFTER_DEPOSITS_UNLOCK,
buyerDisputeContext: DisputeContext.OPEN_AFTER_DEPOSITS_UNLOCK,
disputeWinner: DisputeResult.Winner.SELLER,
disputeReason: DisputeResult.Reason.NO_REPLY,
disputeSummary: "Seller wins dispute because buyer has not replied",
@ -2445,6 +2506,7 @@ async function executeTrade(ctxP: Partial<TradeContext>): Promise<string> {
ctx.buyerAppName = ctx.getBuyer().havenod!.getAppName();
if (ctx.buyerOfflineAfterTake) {
HavenoUtils.log(0, "Buyer going offline");
assertNotStaticClient(ctx.getBuyer().havenod!);
promises.push(releaseHavenoProcess(ctx.getBuyer().havenod!));
if (ctx.isBuyerMaker()) ctx.maker.havenod = undefined;
else ctx.taker.havenod = undefined;
@ -2452,6 +2514,7 @@ async function executeTrade(ctxP: Partial<TradeContext>): Promise<string> {
ctx.sellerAppName = ctx.getSeller().havenod!.getAppName();
if (ctx.sellerOfflineAfterTake) {
HavenoUtils.log(0, "Seller going offline");
assertNotStaticClient(ctx.getSeller().havenod!);
promises.push(releaseHavenoProcess(ctx.getSeller().havenod!));
if (ctx.isBuyerMaker()) ctx.taker.havenod = undefined;
else ctx.maker.havenod = undefined;
@ -2460,7 +2523,8 @@ async function executeTrade(ctxP: Partial<TradeContext>): Promise<string> {
// wait for deposit txs to unlock
if (ctx.isStopped) return ctx.offerId!;
await waitForUnlockedTxs(trade.getMakerDepositTxId(), trade.getTakerDepositTxId());
if (ctx.hasBuyerAsTakerWithoutDeposit()) await waitForUnlockedTxs(trade.getMakerDepositTxId());
else await waitForUnlockedTxs(trade.getMakerDepositTxId(), trade.getTakerDepositTxId());
// buyer comes online if offline and used
if (ctx.isStopped) return ctx.offerId!;
@ -2554,6 +2618,7 @@ async function executeTrade(ctxP: Partial<TradeContext>): Promise<string> {
if (ctx.isStopped) return ctx.offerId!;
if (ctx.buyerOfflineAfterPaymentSent) {
HavenoUtils.log(0, "Buyer going offline");
assertNotStaticClient(ctx.getBuyer().havenod!);
await releaseHavenoProcess(ctx.getBuyer().havenod!);
if (ctx.isBuyerMaker()) ctx.maker.havenod = undefined;
else ctx.taker.havenod = undefined;
@ -2757,8 +2822,10 @@ async function makeOffer(ctxP?: Partial<TradeContext>): Promise<OfferInfo> {
ctx.priceMargin,
ctx.triggerPrice,
ctx.offerMinAmount,
ctx.reserveExactAmount);
testOffer(offer, ctx);
ctx.reserveExactAmount,
ctx.isPrivateOffer,
ctx.buyerAsTakerWithoutDeposit);
testOffer(offer, ctx, true);
// offer is included in my offers only
if (!getOffer(await ctx.maker.havenod!.getMyOffers(ctx.assetCode!, ctx.direction), offer.getId())) {
@ -2771,6 +2838,7 @@ async function makeOffer(ctxP?: Partial<TradeContext>): Promise<OfferInfo> {
// collect context
ctx.maker.splitOutputTxFee = BigInt(offer.getSplitOutputTxFee());
ctx.taker.splitOutputTxFee = 0n;
ctx.challenge = offer.getChallenge();
// market-priced offer amounts are unadjusted, fixed-priced offer amounts are adjusted (e.g. cash at atm is $10 increments)
// TODO: adjustments should be based on currency and payment method, not fixed-price
@ -2835,12 +2903,12 @@ async function takeOffer(ctxP: Partial<TradeContext>): Promise<TradeInfo> {
const takerBalancesBefore: XmrBalanceInfo = await ctx.taker.havenod!.getBalances();
const startTime = Date.now();
HavenoUtils.log(1, "Taking offer " + ctx.offerId);
const trade = await ctx.taker.havenod!.takeOffer(ctx.offerId, ctx.takerPaymentAccountId!, ctx.tradeAmount);
const takerTrade = await ctx.taker.havenod!.takeOffer(ctx.offerId, ctx.takerPaymentAccountId!, ctx.tradeAmount, ctx.challenge);
HavenoUtils.log(1, "Done taking offer " + ctx.offerId + " in " + (Date.now() - startTime) + " ms");
// maker is notified that offer is taken
await wait(ctx.maxTimePeerNoticeMs);
const tradeNotifications = getNotifications(makerNotifications, NotificationMessage.NotificationType.TRADE_UPDATE, trade.getTradeId());
const tradeNotifications = getNotifications(makerNotifications, NotificationMessage.NotificationType.TRADE_UPDATE, takerTrade.getTradeId());
expect(tradeNotifications.length).toBe(1);
assert(moneroTs.GenUtils.arrayContains(["DEPOSITS_PUBLISHED", "DEPOSITS_CONFIRMED", "DEPOSITS_UNLOCKED"], tradeNotifications[0].getTrade()!.getPhase()), "Unexpected trade phase: " + tradeNotifications[0].getTrade()!.getPhase());
expect(tradeNotifications[0].getTitle()).toEqual("Offer Taken");
@ -2852,33 +2920,35 @@ async function takeOffer(ctxP: Partial<TradeContext>): Promise<TradeInfo> {
// wait to observe deposit txs
ctx.arbitrator.trade = await ctx.arbitrator.havenod!.getTrade(ctx.offerId!);
ctx.maker.depositTx = await monerod.getTx(ctx.arbitrator.trade!.getMakerDepositTxId());
ctx.taker.depositTx = await monerod.getTx(ctx.arbitrator.trade!.getTakerDepositTxId());
if (!ctx.maker.depositTx || !ctx.taker.depositTx) {
if (ctx.hasBuyerAsTakerWithoutDeposit()) assert(!ctx.arbitrator.trade!.getTakerDepositTxId());
else ctx.taker.depositTx = await monerod.getTx(ctx.arbitrator.trade!.getTakerDepositTxId());
if (!ctx.maker.depositTx || (!ctx.taker.depositTx && !ctx.hasBuyerAsTakerWithoutDeposit())) {
if (!ctx.maker.depositTx) HavenoUtils.log(0, "Maker deposit tx not found with id " + ctx.arbitrator.trade!.getMakerDepositTxId() + ", waiting...");
if (!ctx.taker.depositTx) HavenoUtils.log(0, "Taker deposit tx not found with id " + ctx.arbitrator.trade!.getTakerDepositTxId() + ", waiting...");
if (!ctx.taker.depositTx && !ctx.hasBuyerAsTakerWithoutDeposit()) HavenoUtils.log(0, "Taker deposit tx not found with id " + ctx.arbitrator.trade!.getTakerDepositTxId() + ", waiting...");
await wait(ctx.walletSyncPeriodMs);
ctx.maker.depositTx = await monerod.getTx(ctx.arbitrator.trade!.getMakerDepositTxId());
ctx.taker.depositTx = await monerod.getTx(ctx.arbitrator.trade!.getTakerDepositTxId());
if (!ctx.hasBuyerAsTakerWithoutDeposit()) ctx.taker.depositTx = await monerod.getTx(ctx.arbitrator.trade!.getTakerDepositTxId());
if (!ctx.maker.depositTx) throw new Error("Maker deposit tx not found with id " + ctx.arbitrator.trade!.getMakerDepositTxId());
if (!ctx.taker.depositTx) throw new Error("Taker deposit tx not found with id " + ctx.arbitrator.trade!.getTakerDepositTxId());
if (!ctx.taker.depositTx && !ctx.hasBuyerAsTakerWithoutDeposit()) throw new Error("Taker deposit tx not found with id " + ctx.arbitrator.trade!.getTakerDepositTxId());
}
// record context
ctx.tradeAmount = BigInt(trade.getAmount()); // re-assign trade amount which could be adjusted
ctx.tradeAmount = BigInt(takerTrade.getAmount()); // reassign trade amount which could be adjusted
ctx.maker.trade = await ctx.maker.havenod!.getTrade(ctx.offerId!);
ctx.taker.trade = await ctx.taker.havenod!.getTrade(ctx.offerId!);
ctx.maker.balancesAfterTake = await ctx.maker.havenod!.getBalances();
ctx.taker.balancesAfterTake = await ctx.taker.havenod!.getBalances();
ctx.maker.depositTxFee = BigInt(ctx.maker.depositTx!.getFee());
ctx.taker.depositTxFee = BigInt(ctx.taker.depositTx!.getFee());
ctx.maker.tradeFee = BigInt(trade.getMakerFee());
ctx.taker.tradeFee = BigInt(trade.getTakerFee());
ctx.getBuyer().securityDepositActual = BigInt(trade.getBuyerSecurityDeposit()!);
ctx.getSeller().securityDepositActual = BigInt(trade.getSellerSecurityDeposit()!);
ctx.taker.depositTxFee = BigInt(ctx.hasBuyerAsTakerWithoutDeposit() ? 0 : ctx.taker.depositTx!.getFee());
ctx.maker.tradeFee = BigInt(takerTrade.getMakerFee());
ctx.taker.tradeFee = BigInt(takerTrade.getTakerFee());
if (ctx.hasBuyerAsTakerWithoutDeposit()) assert.equal(ctx.taker.depositTxFee, 0n);
ctx.getBuyer().securityDepositActual = BigInt(takerTrade.getBuyerSecurityDeposit()!);
ctx.getSeller().securityDepositActual = BigInt(takerTrade.getSellerSecurityDeposit()!);
}
// test trade model
await testTrade(trade, ctx);
await testTrade(takerTrade, ctx);
// test buyer and seller balances after offer taken
if (!ctx.concurrentTrades) {
@ -2886,14 +2956,14 @@ async function takeOffer(ctxP: Partial<TradeContext>): Promise<TradeInfo> {
// test buyer balances after offer taken
const buyerBalanceDiffReservedTrade = BigInt(ctx.getBuyer().balancesAfterTake!.getReservedTradeBalance()) - BigInt(ctx.getBuyer().balancesBeforeTake!.getReservedTradeBalance());
expect(buyerBalanceDiffReservedTrade).toEqual(BigInt(trade.getBuyerSecurityDeposit()!));
expect(buyerBalanceDiffReservedTrade).toEqual(BigInt(takerTrade.getBuyerSecurityDeposit()!));
const buyerBalanceDiffReservedOffer = BigInt(ctx.getBuyer().balancesAfterTake!.getReservedOfferBalance()) - BigInt(ctx.getBuyer().balancesBeforeTake!.getReservedOfferBalance());
const buyerBalanceDiff = BigInt(ctx.getBuyer().balancesAfterTake!.getBalance()) - BigInt(ctx.getBuyer().balancesBeforeTake!.getBalance());
expect(buyerBalanceDiff).toEqual(-1n * buyerBalanceDiffReservedOffer - buyerBalanceDiffReservedTrade - ctx.getBuyer().depositTxFee! - ctx.getBuyer().tradeFee!);
// test seller balances after offer taken
const sellerBalanceDiffReservedTrade = BigInt(ctx.getSeller().balancesAfterTake!.getReservedTradeBalance()) - BigInt(ctx.getSeller().balancesBeforeTake!.getReservedTradeBalance());
expect(sellerBalanceDiffReservedTrade).toEqual(BigInt(trade.getAmount()) + BigInt(trade.getSellerSecurityDeposit()!));
expect(sellerBalanceDiffReservedTrade).toEqual(BigInt(takerTrade.getAmount()) + BigInt(takerTrade.getSellerSecurityDeposit()!));
const sellerBalanceDiffReservedOffer = BigInt(ctx.getSeller().balancesAfterTake!.getReservedOfferBalance()) - BigInt(ctx.getSeller().balancesBeforeTake!.getReservedOfferBalance());
const sellerBalanceDiff = BigInt(ctx.getSeller().balancesAfterTake!.getBalance()) - BigInt(ctx.getSeller().balancesBeforeTake!.getBalance());
expect(sellerBalanceDiff).toEqual(-1n * sellerBalanceDiffReservedOffer - sellerBalanceDiffReservedTrade - ctx.getSeller().depositTxFee! - ctx.getSeller().tradeFee!);
@ -2912,25 +2982,25 @@ async function takeOffer(ctxP: Partial<TradeContext>): Promise<TradeInfo> {
// market-priced offer amounts are unadjusted, fixed-priced offer amounts are adjusted (e.g. cash at atm is $10 increments)
// TODO: adjustments are based on payment method, not fixed-price
if (trade.getOffer()!.getUseMarketBasedPrice()) {
assert.equal(ctx.tradeAmount, BigInt(trade.getAmount()));
if (takerTrade.getOffer()!.getUseMarketBasedPrice()) {
assert.equal(ctx.tradeAmount, BigInt(takerTrade.getAmount()));
} else {
expect(Math.abs(HavenoUtils.percentageDiff(ctx.tradeAmount!, BigInt(trade.getAmount())))).toBeLessThan(TestConfig.maxAdjustmentPct);
expect(Math.abs(HavenoUtils.percentageDiff(ctx.tradeAmount!, BigInt(takerTrade.getAmount())))).toBeLessThan(TestConfig.maxAdjustmentPct);
}
// maker is notified of balance change
// taker is notified of balance change
return trade;
return takerTrade;
}
async function testTrade(trade: TradeInfo, ctx: TradeContext, havenod?: HavenoClient): Promise<void> {
expect(BigInt(trade.getAmount())).toEqual(ctx!.tradeAmount);
// test security deposit = max(.1, trade amount * security deposit pct)
// test security deposit = max(0.1, trade amount * security deposit pct)
const expectedSecurityDeposit = HavenoUtils.max(HavenoUtils.xmrToAtomicUnits(.1), HavenoUtils.multiply(ctx.tradeAmount!, ctx.securityDepositPct!));
expect(BigInt(trade.getBuyerSecurityDeposit())).toEqual(expectedSecurityDeposit - ctx.getBuyer().depositTxFee!);
expect(BigInt(trade.getBuyerSecurityDeposit())).toEqual(ctx.hasBuyerAsTakerWithoutDeposit() ? 0n : expectedSecurityDeposit - ctx.getBuyer().depositTxFee!);
expect(BigInt(trade.getSellerSecurityDeposit())).toEqual(expectedSecurityDeposit - ctx.getSeller().depositTxFee!);
// test phase
@ -3138,8 +3208,6 @@ async function resolveDispute(ctxP: Partial<TradeContext>) {
}
// award too little to loser (minority receiver)
let makerDepositTx = await monerod.getTx(trade.getMakerDepositTxId());
let takerDepositTx = await monerod.getTx(trade.getTakerDepositTxId());
customWinnerAmount = tradeAmount + BigInt(trade.getBuyerSecurityDeposit()) + BigInt(trade.getSellerSecurityDeposit()) - 10000n;
try {
await arbitrator.resolveDispute(ctx.offerId!, ctx.disputeWinner!, ctx.disputeReason!, "Loser gets too little", customWinnerAmount);
@ -3235,16 +3303,16 @@ async function testAmountsAfterComplete(tradeCtx: TradeContext) {
} else {
// get expected payouts for disputed trade
const winnerGetsAll = tradeCtx.disputeWinnerAmount === tradeCtx.maker.securityDepositActual! + tradeCtx.taker.securityDepositActual! + tradeCtx.tradeAmount!;
const winnerGetsAll = tradeCtx.disputeWinnerAmount === tradeCtx.maker.securityDepositActual! + tradeCtx.taker.securityDepositActual! + tradeCtx.tradeAmount! || (tradeCtx.hasBuyerAsTakerWithoutDeposit() && tradeCtx.disputeWinner === DisputeResult.Winner.SELLER && tradeCtx.disputeWinnerAmount === undefined);
if (tradeCtx.disputeWinnerAmount) {
tradeCtx.getDisputeWinner()!.payoutTxFee = winnerGetsAll ? payoutTxFee : 0n;
tradeCtx.getDisputeWinner()!.payoutAmount = tradeCtx.disputeWinnerAmount - tradeCtx.getDisputeWinner()!.payoutTxFee!;
tradeCtx.getDisputeLoser()!.payoutTxFee = winnerGetsAll ? 0n : payoutTxFee;
tradeCtx.getDisputeLoser()!.payoutAmount = tradeCtx.maker.securityDepositActual! + tradeCtx.taker.securityDepositActual! + tradeCtx.tradeAmount! - tradeCtx.disputeWinnerAmount - tradeCtx.getDisputeLoser()!.payoutTxFee!;
} else {
tradeCtx.getDisputeWinner()!.payoutTxFee = payoutTxFee / 2n;
tradeCtx.getDisputeWinner()!.payoutTxFee = winnerGetsAll ? payoutTxFee : payoutTxFee / 2n;
tradeCtx.getDisputeWinner()!.payoutAmount = tradeCtx.tradeAmount! + tradeCtx.getDisputeWinner()!.securityDepositActual! - tradeCtx.getDisputeWinner()!.payoutTxFee!;
tradeCtx.getDisputeLoser()!.payoutTxFee = payoutTxFee / 2n;
tradeCtx.getDisputeLoser()!.payoutTxFee = winnerGetsAll ? 0n : payoutTxFee / 2n;
tradeCtx.getDisputeLoser()!.payoutAmount = tradeCtx.getDisputeLoser()!.securityDepositActual! - tradeCtx.getDisputeLoser()!.payoutTxFee!;
}
}
@ -3457,18 +3525,20 @@ async function initHaveno(ctx?: HavenodContext): Promise<HavenoClient> {
"--logLevel", ctx.logLevel!
];
havenod = await HavenoClient.startProcess(TestConfig.haveno.path, cmd, "http://localhost:" + ctx.port, ctx.logProcessOutput!);
HAVENO_PROCESSES.push(havenod);
// wait to process network notifications
await wait(3000);
}
// add to list of clients
HAVENO_CLIENTS.push(havenod)
// open account if configured
if (ctx.autoLogin) {
try {
await initHavenoAccount(havenod, ctx.accountPassword!);
} catch (err) {
await releaseHavenoProcess(havenod);
await releaseHavenoClient(havenod);
throw err;
}
}
@ -3487,11 +3557,21 @@ async function initHaveno(ctx?: HavenodContext): Promise<HavenoClient> {
}
}
/**
* Release a Haveno client by shutting down its process or disconnecting.
*/
async function releaseHavenoClient(client: HavenoClient, deleteProcessAppDir?: boolean) {
if (client.getProcess()) return releaseHavenoProcess(client, deleteProcessAppDir);
else await client.disconnect();
}
/**
* Release a Haveno process for reuse and try to shutdown.
*/
async function releaseHavenoProcess(havenod: HavenoClient, deleteAppDir?: boolean) {
moneroTs.GenUtils.remove(HAVENO_PROCESSES, havenod);
if (!testsOwnProcess(havenod)) throw new Error("Cannot shut down havenod process which is not owned by test");
if (!moneroTs.GenUtils.arrayContains(HAVENO_CLIENTS, havenod)) throw new Error("Cannot release Haveno client which is not in list of clients");
moneroTs.GenUtils.remove(HAVENO_CLIENTS, havenod);
moneroTs.GenUtils.remove(HAVENO_PROCESS_PORTS, getPort(havenod.getUrl()));
try {
await havenod.shutdownServer();
@ -3501,6 +3581,14 @@ async function releaseHavenoProcess(havenod: HavenoClient, deleteAppDir?: boolea
if (deleteAppDir) deleteHavenoInstance(havenod);
}
function testsOwnProcess(havenod: HavenoClient) {
return havenod.getProcess();
}
function assertNotStaticClient(client: HavenoClient) {
if (client === user1 || client === user2 || client === arbitrator) throw new Error("Tests are not designed to shut down user1, user2, or arbitrator")
}
/**
* Delete a Haveno instance from disk.
*/
@ -4007,13 +4095,26 @@ function testCryptoPaymentAccountsEqual(acct1: PaymentAccount, acct2: PaymentAcc
expect(acct1.getPaymentAccountPayload()!.getCryptoCurrencyAccountPayload()!.getAddress()).toEqual(acct2.getPaymentAccountPayload()!.getCryptoCurrencyAccountPayload()!.getAddress());
}
function testOffer(offer: OfferInfo, ctx?: Partial<TradeContext>) {
function testOffer(offer: OfferInfo, ctxP?: Partial<TradeContext>, isMyOffer?: boolean) {
let ctx = TradeContext.init(ctxP);
expect(offer.getId().length).toBeGreaterThan(0);
if (ctx) {
expect(offer.getBuyerSecurityDepositPct()).toEqual(ctx?.securityDepositPct);
expect(offer.getSellerSecurityDepositPct()).toEqual(ctx?.securityDepositPct);
expect(offer.getIsPrivateOffer()).toEqual(ctx?.isPrivateOffer ? true : false); // TODO: update tests for security deposit
if (offer.getIsPrivateOffer()) {
if (isMyOffer) expect(offer.getChallenge().length).toBeGreaterThan(0);
else expect(offer.getChallenge()).toEqual("");
if (ctx.isBuyerMaker()) {
expect(offer.getBuyerSecurityDepositPct()).toEqual(ctx.securityDepositPct);
} else {
expect(offer.getBuyerSecurityDepositPct()).toEqual(ctx.buyerAsTakerWithoutDeposit ? 0 : ctx.securityDepositPct);
}
} else {
expect(offer.getBuyerSecurityDepositPct()).toEqual(ctx.securityDepositPct);
expect(offer.getChallenge()).toEqual("");
}
expect(offer.getSellerSecurityDepositPct()).toEqual(ctx.securityDepositPct);
expect(offer.getUseMarketBasedPrice()).toEqual(!ctx?.price);
expect(offer.getMarketPriceMarginPct()).toEqual(ctx?.priceMargin ? ctx?.priceMargin : 0);
expect(offer.getMarketPriceMarginPct()).toEqual(ctx?.priceMargin ? ctx.priceMargin : 0);
// TODO: test rest of offer
}

View File

@ -1046,6 +1046,8 @@ export default class HavenoClient {
* @param {number} triggerPrice - price to remove offer (optional)
* @param {bigint} minAmount - minimum amount to trade (optional, default to fixed amount)
* @param {number} reserveExactAmount - reserve exact amount needed for offer, incurring on-chain transaction and 10 confirmations before the offer goes live (default = false)
* @param {boolean} isPrivateOffer - whether the offer is private (default = false)
* @param {boolean} buyerAsTakerWithoutDeposit - waive buyer as taker deposit and fee (default false)
* @return {OfferInfo} the posted offer
*/
async postOffer(direction: OfferDirection,
@ -1057,7 +1059,9 @@ export default class HavenoClient {
marketPriceMarginPct?: number,
triggerPrice?: number,
minAmount?: bigint,
reserveExactAmount?: boolean): Promise<OfferInfo> {
reserveExactAmount?: boolean,
isPrivateOffer?: boolean,
buyerAsTakerWithoutDeposit?: boolean): Promise<OfferInfo> {
console.log("Posting offer with security deposit %: " + securityDepositPct)
try {
const request = new PostOfferRequest()
@ -1065,13 +1069,15 @@ export default class HavenoClient {
.setAmount(amount.toString())
.setCurrencyCode(assetCode)
.setPaymentAccountId(paymentAccountId)
.setBuyerSecurityDepositPct(securityDepositPct)
.setSecurityDepositPct(securityDepositPct)
.setUseMarketBasedPrice(price === undefined)
.setMinAmount(minAmount ? minAmount.toString() : amount.toString());
if (price) request.setPrice(price.toString());
if (marketPriceMarginPct) request.setMarketPriceMarginPct(marketPriceMarginPct);
if (triggerPrice) request.setTriggerPrice(triggerPrice.toString());
if (reserveExactAmount) request.setReserveExactAmount(reserveExactAmount);
if (reserveExactAmount) request.setReserveExactAmount(true);
if (isPrivateOffer) request.setIsPrivateOffer(true);
if (buyerAsTakerWithoutDeposit) request.setBuyerAsTakerWithoutDeposit(true);
return (await this._offersClient.postOffer(request, {password: this._password})).getOffer()!;
} catch (e: any) {
throw new HavenoError(e.message, e.code);
@ -1097,16 +1103,19 @@ export default class HavenoClient {
* @param {string} offerId - id of the offer to take
* @param {string} paymentAccountId - id of the payment account
* @param {bigint|undefined} amount - amount the taker chooses to buy or sell within the offer range (default is max offer amount)
* @param {string|undefined} challenge - the challenge to use for the private offer
* @return {TradeInfo} the initialized trade
*/
async takeOffer(offerId: string,
paymentAccountId: string,
amount?: bigint): Promise<TradeInfo> {
amount?: bigint,
challenge?: string): Promise<TradeInfo> {
try {
const request = new TakeOfferRequest()
.setOfferId(offerId)
.setPaymentAccountId(paymentAccountId);
if (amount) request.setAmount(amount.toString());
if (challenge) request.setChallenge(challenge);
const resp = await this._tradesClient.takeOffer(request, {password: this._password});
if (resp.getTrade()) return resp.getTrade()!;
throw new HavenoError(resp.getFailureReason()?.getDescription()!, resp.getFailureReason()?.getAvailabilityResult());
@ -1319,7 +1328,7 @@ export default class HavenoClient {
try {
await this.disconnect();
await this._shutdownServerClient.stop(new StopRequest(), {password: this._password}); // process receives 'exit' event
if (this._process) return HavenoUtils.kill(this._process);
if (this._process) await HavenoUtils.kill(this._process);
} catch (e: any) {
throw new HavenoError(e.message, e.code);
}