|
|
|
@ -145,7 +145,7 @@ const defaultTradeConfig: Partial<TradeContext> = {
|
|
|
|
|
disputeReason: DisputeResult.Reason.PEER_WAS_LATE,
|
|
|
|
|
disputeSummary: "Seller is winner",
|
|
|
|
|
walletSyncPeriodMs: 5000,
|
|
|
|
|
maxTimePeerNoticeMs: 5000,
|
|
|
|
|
maxTimePeerNoticeMs: 6000,
|
|
|
|
|
testChatMessages: true,
|
|
|
|
|
stopOnFailure: false, // TODO: setting to true can cause error: Http response at 400 or 500 level, http status code: 503
|
|
|
|
|
testPayoutConfirmed: true,
|
|
|
|
@ -195,6 +195,8 @@ class TradeContext {
|
|
|
|
|
reserveExactAmount?: boolean;
|
|
|
|
|
isPrivateOffer?: boolean;
|
|
|
|
|
buyerAsTakerWithoutDeposit?: boolean; // buyer as taker security deposit is optional for private offers
|
|
|
|
|
extraInfo?: string;
|
|
|
|
|
sourceOfferId?: string;
|
|
|
|
|
|
|
|
|
|
// take offer
|
|
|
|
|
awaitFundsToTakeOffer?: boolean;
|
|
|
|
@ -267,6 +269,10 @@ class TradeContext {
|
|
|
|
|
return this.direction === OfferDirection.BUY;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
wasDisputeOpened() {
|
|
|
|
|
return this.buyerOpenedDispute || this.sellerOpenedDispute;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getDisputeOpener(): PeerContext | undefined {
|
|
|
|
|
if (this.disputeOpener === undefined) return undefined;
|
|
|
|
|
return this.disputeOpener === SaleRole.BUYER ? this.getBuyer() : this.getSeller();
|
|
|
|
@ -376,7 +382,7 @@ const TestConfig = {
|
|
|
|
|
deferralMs: 25000,
|
|
|
|
|
haveno: {
|
|
|
|
|
path: "../haveno",
|
|
|
|
|
version: "1.0.18"
|
|
|
|
|
version: "1.1.0"
|
|
|
|
|
},
|
|
|
|
|
monerod: {
|
|
|
|
|
url: "http://127.0.0.1:" + getNetworkStartPort() + "8081", // 18081, 28081, 38081 for mainnet, testnet, and stagenet, respectively
|
|
|
|
@ -441,9 +447,9 @@ const TestConfig = {
|
|
|
|
|
maxWalletStartupMs: 10000, // TODO (woodser): make shorter by switching to jni
|
|
|
|
|
maxCpuPct: 0.25,
|
|
|
|
|
paymentMethods: Object.keys(PaymentAccountForm.FormId), // all supported payment methods
|
|
|
|
|
assetCodes: ["USD", "GBP", "EUR", "ETH", "BTC", "BCH", "LTC", "USDT-ERC20", "USDT-TRC20", "USDC-ERC20"], // crypto asset codes
|
|
|
|
|
assetCodes: ["USD", "GBP", "EUR", "ETH", "BTC", "BCH", "LTC", "USDT-ERC20", "USDT-TRC20", "USDC-ERC20", "DAI-ERC20"],
|
|
|
|
|
fixedPriceAssetCodes: ["XAG", "XAU", "XGB"],
|
|
|
|
|
fixedPricePaymentMethods: [HavenoUtils.getPaymentMethodId(PaymentAccountForm.FormId.CASH_AT_ATM)],
|
|
|
|
|
fixedPricePaymentMethods: [],
|
|
|
|
|
cryptoAddresses: [{
|
|
|
|
|
currencyCode: "ETH",
|
|
|
|
|
address: "0xdBdAb835Acd6fC84cF5F9aDD3c0B5a1E25fbd99f"
|
|
|
|
@ -465,6 +471,9 @@ const TestConfig = {
|
|
|
|
|
}, {
|
|
|
|
|
currencyCode: "USDC-ERC20",
|
|
|
|
|
address: "0x1165cE9056620C9012D098103a08267e3c48B86B"
|
|
|
|
|
}, {
|
|
|
|
|
currencyCode: "DAI-ERC20",
|
|
|
|
|
address: "0x3fDBeb7b074a42D7B5810488d4aAD5992097DF93"
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
ports: new Map<string, string[]>([ // map http ports to havenod api and p2p ports
|
|
|
|
@ -832,7 +841,9 @@ test("Can manage Monero daemon connections (Test, CI)", async () => {
|
|
|
|
|
"--confirm-external-bind",
|
|
|
|
|
"--rpc-access-control-origins", "http://127.0.0.1:8080",
|
|
|
|
|
"--fixed-difficulty", "500",
|
|
|
|
|
"--disable-rpc-ban"
|
|
|
|
|
"--disable-rpc-ban",
|
|
|
|
|
"--rpc-max-connections-per-private-ip", "100",
|
|
|
|
|
"--max-connections-per-ip", "10"
|
|
|
|
|
];
|
|
|
|
|
if (getBaseCurrencyNetwork() !== BaseCurrencyNetwork.XMR_MAINNET) cmd.push("--" + moneroTs.MoneroNetworkType.toString(TestConfig.networkType).toLowerCase());
|
|
|
|
|
if (TestConfig.monerod3.username) cmd.push("--rpc-login", TestConfig.monerod3.username + ":" + TestConfig.monerod3.password);
|
|
|
|
@ -1479,7 +1490,8 @@ test("Can post and remove an offer (Test, CI, sanity check)", async () => {
|
|
|
|
|
let assetCode = "BCH";
|
|
|
|
|
let price = 1 / 17;
|
|
|
|
|
price = 1 / price; // TODO: price in crypto offer is inverted
|
|
|
|
|
let offer: OfferInfo = await makeOffer({maker: {havenod: user1}, assetCode: assetCode, price: price});
|
|
|
|
|
let ctx: Partial<TradeContext> = {maker: {havenod: user1}, assetCode: assetCode, price: price, extraInfo: "My extra info"};
|
|
|
|
|
let offer: OfferInfo = await makeOffer(ctx);;
|
|
|
|
|
assert.equal(offer.getState(), "AVAILABLE");
|
|
|
|
|
assert.equal(offer.getBaseCurrencyCode(), assetCode); // TODO: base and counter currencies inverted in crypto offer
|
|
|
|
|
assert.equal(offer.getCounterCurrencyCode(), "XMR");
|
|
|
|
@ -1491,7 +1503,9 @@ test("Can post and remove an offer (Test, CI, sanity check)", async () => {
|
|
|
|
|
|
|
|
|
|
// peer sees offer
|
|
|
|
|
await wait(TestConfig.trade.maxTimePeerNoticeMs);
|
|
|
|
|
if (!getOffer(await user2.getOffers(assetCode, TestConfig.trade.direction), offer.getId())) throw new Error("Offer " + offer.getId() + " was not found in peer's offers after posted");
|
|
|
|
|
let peerOffer = getOffer(await user2.getOffers(assetCode, TestConfig.trade.direction), offer.getId());
|
|
|
|
|
if (!peerOffer) throw new Error("Offer " + offer.getId() + " was not found in peer's offers after posted");
|
|
|
|
|
testOffer(peerOffer, ctx, false);
|
|
|
|
|
|
|
|
|
|
// cancel offer
|
|
|
|
|
await user1.removeOffer(offer.getId());
|
|
|
|
@ -1509,7 +1523,8 @@ test("Can post and remove an offer (Test, CI, sanity check)", async () => {
|
|
|
|
|
// post fiat offer
|
|
|
|
|
assetCode = "USD";
|
|
|
|
|
price = 180.0;
|
|
|
|
|
offer = await makeOffer({maker: {havenod: user1}, assetCode: assetCode, price: price});
|
|
|
|
|
ctx = {maker: {havenod: user1}, assetCode: assetCode, price: price, extraInfo: "My extra info 2"};
|
|
|
|
|
offer = await makeOffer(ctx);
|
|
|
|
|
assert.equal(offer.getState(), "AVAILABLE");
|
|
|
|
|
assert.equal(offer.getBaseCurrencyCode(), "XMR");
|
|
|
|
|
assert.equal(offer.getCounterCurrencyCode(), "USD");
|
|
|
|
@ -1521,7 +1536,9 @@ test("Can post and remove an offer (Test, CI, sanity check)", async () => {
|
|
|
|
|
|
|
|
|
|
// peer sees offer
|
|
|
|
|
await wait(TestConfig.trade.maxTimePeerNoticeMs);
|
|
|
|
|
if (!getOffer(await user2.getOffers(assetCode, TestConfig.trade.direction), offer.getId())) throw new Error("Offer " + offer.getId() + " was not found in peer's offers after posted");
|
|
|
|
|
peerOffer = getOffer(await user2.getOffers(assetCode, TestConfig.trade.direction), offer.getId());
|
|
|
|
|
if (!peerOffer) throw new Error("Offer " + offer.getId() + " was not found in peer's offers after posted");
|
|
|
|
|
testOffer(peerOffer, ctx, false);
|
|
|
|
|
|
|
|
|
|
// cancel offer
|
|
|
|
|
await user1.removeOffer(offer.getId());
|
|
|
|
@ -1537,6 +1554,40 @@ test("Can post and remove an offer (Test, CI, sanity check)", async () => {
|
|
|
|
|
if (getOffer(await user2.getOffers(assetCode, TestConfig.trade.direction), offer.getId())) throw new Error("Offer " + offer.getId() + " was found in peer's offers after removed");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("Can clone offers (Test, CI, sanity check)", async () => {
|
|
|
|
|
|
|
|
|
|
// wait for user1 to have unlocked balance to post offer
|
|
|
|
|
await waitForAvailableBalance(250000000000n * 2n, user1);
|
|
|
|
|
|
|
|
|
|
// get unlocked balance before reserving funds for offer
|
|
|
|
|
const availableBalanceBefore = BigInt((await user1.getBalances()).getAvailableBalance());
|
|
|
|
|
|
|
|
|
|
// post offer
|
|
|
|
|
let assetCode = "BCH";
|
|
|
|
|
let ctx: Partial<TradeContext> = {maker: {havenod: user1}, isPrivateOffer: true, buyerAsTakerWithoutDeposit: true, assetCode: assetCode, extraInfo: "My extra info"};
|
|
|
|
|
let offer: OfferInfo = await makeOffer(ctx);;
|
|
|
|
|
assert.equal(offer.getState(), "AVAILABLE");
|
|
|
|
|
|
|
|
|
|
// clone offer
|
|
|
|
|
const clonedOffer = await makeOffer({
|
|
|
|
|
sourceOfferId: offer.getId(),
|
|
|
|
|
assetCode: "BCH"
|
|
|
|
|
});
|
|
|
|
|
assert.notEqual(clonedOffer.getId(), offer.getId());
|
|
|
|
|
assert.equal(clonedOffer.getState(), "DEACTIVATED"); // deactivated if same payment method and currency
|
|
|
|
|
assert.equal(clonedOffer.getBaseCurrencyCode(), assetCode);
|
|
|
|
|
assert.equal(clonedOffer.getCounterCurrencyCode(), "XMR");
|
|
|
|
|
assert.equal(clonedOffer.getAmount(), offer.getAmount());
|
|
|
|
|
assert.equal(clonedOffer.getMinAmount(), offer.getMinAmount());
|
|
|
|
|
assert.equal(clonedOffer.getIsPrivateOffer(), offer.getIsPrivateOffer());
|
|
|
|
|
|
|
|
|
|
// TODO: test edited fields on clone, etc
|
|
|
|
|
|
|
|
|
|
// remove offers
|
|
|
|
|
await user1.removeOffer(offer.getId());
|
|
|
|
|
await user1.removeOffer(clonedOffer.getId());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// TODO: provide number of confirmations in offer status
|
|
|
|
|
test("Can schedule offers with locked funds (Test, CI)", async () => {
|
|
|
|
|
let user3: HavenoClient|undefined;
|
|
|
|
@ -1759,7 +1810,8 @@ test("Can complete a trade within a range and without a buyer deposit (Test, CI)
|
|
|
|
|
testBalanceChangeEndToEnd: true,
|
|
|
|
|
direction: OfferDirection.SELL,
|
|
|
|
|
isPrivateOffer: true,
|
|
|
|
|
buyerAsTakerWithoutDeposit: true
|
|
|
|
|
buyerAsTakerWithoutDeposit: true,
|
|
|
|
|
extraInfo: "My extra info"
|
|
|
|
|
}
|
|
|
|
|
await executeTrade(ctx);
|
|
|
|
|
|
|
|
|
@ -1797,25 +1849,30 @@ test("Can complete all trade combinations (Test, stress)", async () => {
|
|
|
|
|
const BUYER_DISPUTE_OPTS = [DisputeContext.NONE, DisputeContext.OPEN_AFTER_DEPOSITS_UNLOCK, DisputeContext.OPEN_AFTER_PAYMENT_SENT];
|
|
|
|
|
const SELLER_DISPUTE_OPTS = [DisputeContext.NONE, DisputeContext.OPEN_AFTER_DEPOSITS_UNLOCK, DisputeContext.OPEN_AFTER_PAYMENT_SENT];
|
|
|
|
|
const DISPUTE_WINNER_OPTS = [DisputeResult.Winner.BUYER, DisputeResult.Winner.SELLER];
|
|
|
|
|
const RESOLVE_DISPUTE_OPTS = [false, true];
|
|
|
|
|
for (let i = 0; i < MAKER_OPTS.length; i++) {
|
|
|
|
|
for (let j = 0; j < DIRECTION_OPTS.length; j++) {
|
|
|
|
|
for (let k = 0; k < BUYER_DISPUTE_OPTS.length; k++) {
|
|
|
|
|
for (let l = 0; l < SELLER_DISPUTE_OPTS.length; l++) {
|
|
|
|
|
for (let m = 0; m < DISPUTE_WINNER_OPTS.length; m++) {
|
|
|
|
|
if (BUYER_DISPUTE_OPTS[k] !== DisputeContext.NONE && SELLER_DISPUTE_OPTS[l] !== DisputeContext.NONE) continue; // skip both opening a dispute
|
|
|
|
|
const ctx: Partial<TradeContext> = {
|
|
|
|
|
walletSyncPeriodMs: 8000, // increase for stress test
|
|
|
|
|
maxTimePeerNoticeMs: 8000,
|
|
|
|
|
maker: { havenod: MAKER_OPTS[i] === TradeRole.MAKER ? user1 : user2 },
|
|
|
|
|
taker: { havenod: MAKER_OPTS[i] === TradeRole.MAKER ? user2 : user1 },
|
|
|
|
|
direction: DIRECTION_OPTS[j],
|
|
|
|
|
buyerDisputeContext: BUYER_DISPUTE_OPTS[k],
|
|
|
|
|
sellerDisputeContext: SELLER_DISPUTE_OPTS[l],
|
|
|
|
|
disputeWinner: DISPUTE_WINNER_OPTS[m],
|
|
|
|
|
disputeSummary: "After much deliberation, " + (DISPUTE_WINNER_OPTS[m] === DisputeResult.Winner.BUYER ? "buyer" : "seller") + " is winner",
|
|
|
|
|
offerAmount: getRandomBigIntWithinPercent(TestConfig.trade.offerAmount!, 0.15)
|
|
|
|
|
};
|
|
|
|
|
ctxs.push(new TradeContext(Object.assign({}, new TradeContext(TestConfig.trade), ctx)));
|
|
|
|
|
for (let n = 0; n < RESOLVE_DISPUTE_OPTS.length; n++) {
|
|
|
|
|
if (BUYER_DISPUTE_OPTS[k] !== DisputeContext.NONE && SELLER_DISPUTE_OPTS[l] !== DisputeContext.NONE) continue; // skip both opening a dispute
|
|
|
|
|
if (BUYER_DISPUTE_OPTS[k] === DisputeContext.NONE && SELLER_DISPUTE_OPTS[l] === DisputeContext.NONE && RESOLVE_DISPUTE_OPTS[n]) continue; // skip permutations to resolve dispute when no dispute
|
|
|
|
|
const ctx: Partial<TradeContext> = {
|
|
|
|
|
walletSyncPeriodMs: 8000, // increase for stress test
|
|
|
|
|
maxTimePeerNoticeMs: 8000,
|
|
|
|
|
maker: { havenod: MAKER_OPTS[i] === TradeRole.MAKER ? user1 : user2 },
|
|
|
|
|
taker: { havenod: MAKER_OPTS[i] === TradeRole.MAKER ? user2 : user1 },
|
|
|
|
|
direction: DIRECTION_OPTS[j],
|
|
|
|
|
buyerDisputeContext: BUYER_DISPUTE_OPTS[k],
|
|
|
|
|
sellerDisputeContext: SELLER_DISPUTE_OPTS[l],
|
|
|
|
|
disputeWinner: DISPUTE_WINNER_OPTS[m],
|
|
|
|
|
resolveDispute: RESOLVE_DISPUTE_OPTS[n],
|
|
|
|
|
disputeSummary: "After much deliberation, " + (DISPUTE_WINNER_OPTS[m] === DisputeResult.Winner.BUYER ? "buyer" : "seller") + " is winner",
|
|
|
|
|
offerAmount: getRandomBigIntWithinPercent(TestConfig.trade.offerAmount!, 0.15)
|
|
|
|
|
};
|
|
|
|
|
ctxs.push(new TradeContext(Object.assign({}, new TradeContext(TestConfig.trade), ctx)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -1920,6 +1977,7 @@ test("Can resolve disputes (Test, CI)", async () => {
|
|
|
|
|
const trade2 = await user1.getTrade(tradeIds[configIdx === undefined ? 2 : 0]);
|
|
|
|
|
Object.assign(ctxs[0], {
|
|
|
|
|
resolveDispute: false,
|
|
|
|
|
buyerSendsPayment: false,
|
|
|
|
|
sellerDisputeContext: DisputeContext.OPEN_AFTER_DEPOSITS_UNLOCK,
|
|
|
|
|
disputeWinner: DisputeResult.Winner.SELLER,
|
|
|
|
|
disputeReason: DisputeResult.Reason.PEER_WAS_LATE,
|
|
|
|
@ -1927,6 +1985,7 @@ test("Can resolve disputes (Test, CI)", async () => {
|
|
|
|
|
});
|
|
|
|
|
Object.assign(ctxs[1], {
|
|
|
|
|
resolveDispute: false,
|
|
|
|
|
buyerSendsPayment: false,
|
|
|
|
|
buyerDisputeContext: DisputeContext.OPEN_AFTER_DEPOSITS_UNLOCK,
|
|
|
|
|
disputeWinner: DisputeResult.Winner.BUYER,
|
|
|
|
|
disputeReason: DisputeResult.Reason.SELLER_NOT_RESPONDING,
|
|
|
|
@ -1935,6 +1994,7 @@ test("Can resolve disputes (Test, CI)", async () => {
|
|
|
|
|
});
|
|
|
|
|
Object.assign(ctxs[2], {
|
|
|
|
|
resolveDispute: false,
|
|
|
|
|
buyerSendsPayment: false,
|
|
|
|
|
buyerDisputeContext: DisputeContext.OPEN_AFTER_DEPOSITS_UNLOCK,
|
|
|
|
|
disputeWinner: DisputeResult.Winner.SELLER,
|
|
|
|
|
disputeReason: DisputeResult.Reason.TRADE_ALREADY_SETTLED,
|
|
|
|
@ -1944,6 +2004,7 @@ test("Can resolve disputes (Test, CI)", async () => {
|
|
|
|
|
Object.assign(ctxs[3], {
|
|
|
|
|
resolveDispute: false,
|
|
|
|
|
buyerSendsPayment: true,
|
|
|
|
|
sellerReceivesPayment: false,
|
|
|
|
|
sellerDisputeContext: DisputeContext.OPEN_AFTER_PAYMENT_SENT,
|
|
|
|
|
disputeWinner: DisputeResult.Winner.BUYER,
|
|
|
|
|
disputeReason: DisputeResult.Reason.TRADE_ALREADY_SETTLED,
|
|
|
|
@ -2209,7 +2270,7 @@ test("Can handle unexpected errors during trade initialization (Test)", async ()
|
|
|
|
|
|
|
|
|
|
// determine if error is expected
|
|
|
|
|
let expected = false;
|
|
|
|
|
const expectedErrMsgs = ["not enough unlocked money", "timeout reached. protocol did not complete", "trade is already taken"];
|
|
|
|
|
const expectedErrMsgs = ["not enough unlocked money", "timeout reached. protocol did not complete", "trade is already taken", "open offer has been removed"];
|
|
|
|
|
for (const expectedErrMsg of expectedErrMsgs) {
|
|
|
|
|
if (err.message.indexOf(expectedErrMsg) >= 0) {
|
|
|
|
|
expected = true;
|
|
|
|
@ -2249,8 +2310,8 @@ test("Selects arbitrators which are online, registered, and least used (Test)",
|
|
|
|
|
await wait(TestConfig.trade.walletSyncPeriodMs * 2);
|
|
|
|
|
|
|
|
|
|
// get internal api addresses
|
|
|
|
|
const arbitrator1ApiUrl = "127.0.0.1:" + TestConfig.ports.get(getPort(arbitrator.getUrl()))![1]; // TODO: havenod.getApiUrl()?
|
|
|
|
|
const arbitrator2ApiUrl = "127.0.0.1:" + TestConfig.ports.get(getPort(arbitrator2.getUrl()))![1];
|
|
|
|
|
const arbitrator1ApiUrl = "localhost:" + TestConfig.ports.get(getPort(arbitrator.getUrl()))![1]; // TODO: havenod.getApiUrl()?
|
|
|
|
|
const arbitrator2ApiUrl = "localhost:" + TestConfig.ports.get(getPort(arbitrator2.getUrl()))![1];
|
|
|
|
|
|
|
|
|
|
let err = undefined;
|
|
|
|
|
try {
|
|
|
|
@ -2384,10 +2445,10 @@ test("Can bootstrap a network", async () => {
|
|
|
|
|
if (await isFixedPrice(ctxP)) ctxP.price = 142.23;
|
|
|
|
|
|
|
|
|
|
// randomize trade config
|
|
|
|
|
if (ctxP.takeOffer === undefined) ctxP.takeOffer = getRandomOutcome(4/5);
|
|
|
|
|
if (ctxP.takeOffer === undefined) ctxP.takeOffer = getRandomOutcome(3/4);
|
|
|
|
|
if (ctxP.tradeAmount === undefined) ctxP.tradeAmount = isRangeOffer ? getRandomBigIntWithinRange(ctxP.offerMinAmount!, ctxP.offerAmount) : ctxP.offerAmount;
|
|
|
|
|
if (ctxP.buyerSendsPayment === undefined) ctxP.buyerSendsPayment = getRandomOutcome(5/7);
|
|
|
|
|
if (ctxP.sellerReceivesPayment === undefined) ctxP.sellerReceivesPayment = getRandomOutcome(6/7);
|
|
|
|
|
if (ctxP.buyerSendsPayment === undefined) ctxP.buyerSendsPayment = getRandomOutcome(1/2);
|
|
|
|
|
if (ctxP.sellerReceivesPayment === undefined) ctxP.sellerReceivesPayment = getRandomOutcome(1/2);
|
|
|
|
|
if (ctxP.buyerDisputeContext === undefined) ctxP.buyerDisputeContext = getRandomOutcome(1/14) ? DisputeContext.OPEN_AFTER_DEPOSITS_UNLOCK : undefined;
|
|
|
|
|
if (ctxP.buyerDisputeContext === undefined) ctxP.buyerDisputeContext = getRandomOutcome(1/14) ? DisputeContext.OPEN_AFTER_PAYMENT_SENT : undefined;
|
|
|
|
|
if (ctxP.sellerDisputeContext === undefined) ctxP.sellerDisputeContext = getRandomOutcome(1/14) ? DisputeContext.OPEN_AFTER_DEPOSITS_UNLOCK : undefined;
|
|
|
|
@ -2673,16 +2734,16 @@ async function executeTrade(ctxP: Partial<TradeContext>): Promise<string> {
|
|
|
|
|
|
|
|
|
|
// handle opened dispute
|
|
|
|
|
if (ctx.isStopped) return ctx.offerId!;
|
|
|
|
|
if (ctx.disputeOpener) {
|
|
|
|
|
if (ctx.wasDisputeOpened()) {
|
|
|
|
|
|
|
|
|
|
// test open dispute
|
|
|
|
|
await testOpenDispute(ctx);
|
|
|
|
|
|
|
|
|
|
// resolve dispute if configured
|
|
|
|
|
if (ctx.resolveDispute) await resolveDispute(ctx);
|
|
|
|
|
|
|
|
|
|
// return offer id
|
|
|
|
|
return ctx.offerId!;
|
|
|
|
|
if (ctx.resolveDispute) {
|
|
|
|
|
await resolveDispute(ctx);
|
|
|
|
|
return ctx.offerId!;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// buyer confirms payment is sent
|
|
|
|
@ -2748,12 +2809,12 @@ async function executeTrade(ctxP: Partial<TradeContext>): Promise<string> {
|
|
|
|
|
if (!ctx.disputeOpener) ctx.disputeOpener = SaleRole.SELLER;
|
|
|
|
|
}
|
|
|
|
|
if (ctx.isStopped) return ctx.offerId!;
|
|
|
|
|
if (ctx.disputeOpener) await testOpenDispute(ctx);
|
|
|
|
|
if (ctx.wasDisputeOpened()) await testOpenDispute(ctx);
|
|
|
|
|
|
|
|
|
|
// if dispute opened, resolve dispute if configured and return
|
|
|
|
|
// if dispute opened, resolve dispute if configured
|
|
|
|
|
if (ctx.isStopped) return ctx.offerId!;
|
|
|
|
|
if (ctx.disputeOpener) {
|
|
|
|
|
if (ctx.resolveDispute) await resolveDispute(ctx);
|
|
|
|
|
if (ctx.wasDisputeOpened() && ctx.resolveDispute) {
|
|
|
|
|
await resolveDispute(ctx);
|
|
|
|
|
return ctx.offerId!;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -2835,7 +2896,7 @@ async function testTradePayoutUnlock(ctxP: Partial<TradeContext>) {
|
|
|
|
|
let trade = await ctx.arbitrator.havenod!.getTrade(ctx.offerId!);
|
|
|
|
|
if (trade.getPayoutState() !== "PAYOUT_CONFIRMED") await mineToHeight(height + 1);
|
|
|
|
|
await wait(TestConfig.maxWalletStartupMs + ctx.walletSyncPeriodMs * 2);
|
|
|
|
|
const disputeState = ctx.isPaymentReceived ? "NO_DISPUTE" : "DISPUTE_CLOSED";
|
|
|
|
|
const disputeState = ctx.wasDisputeOpened() ? "DISPUTE_CLOSED" : "NO_DISPUTE";
|
|
|
|
|
if (ctx.getBuyer().havenod) await testTradeState(await ctx.getBuyer().havenod!.getTrade(ctx.offerId!), {phase: ctx.getPhase(), disputeState: disputeState, payoutState: ["PAYOUT_CONFIRMED", "PAYOUT_UNLOCKED"]});
|
|
|
|
|
if (ctx.getSeller().havenod) await testTradeState(await ctx.getSeller().havenod!.getTrade(ctx.offerId!), {phase: ctx.getPhase(), disputeState: disputeState, payoutState: ["PAYOUT_CONFIRMED", "PAYOUT_UNLOCKED"]});
|
|
|
|
|
await testTradeState(await ctx.arbitrator.havenod!.getTrade(ctx.offerId!), {phase: ctx.getPhase(), payoutState: ["PAYOUT_CONFIRMED", "PAYOUT_UNLOCKED"]});
|
|
|
|
@ -2893,20 +2954,31 @@ async function makeOffer(ctxP?: Partial<TradeContext>): Promise<OfferInfo> {
|
|
|
|
|
ctx.taker.balancesBeforeOffer = await ctx.taker.havenod?.getBalances();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// post offer
|
|
|
|
|
const offer: OfferInfo = await ctx.maker.havenod!.postOffer(
|
|
|
|
|
ctx.direction!,
|
|
|
|
|
ctx.offerAmount!,
|
|
|
|
|
ctx.assetCode!,
|
|
|
|
|
ctx.makerPaymentAccountId!,
|
|
|
|
|
ctx.securityDepositPct!,
|
|
|
|
|
ctx.price,
|
|
|
|
|
ctx.priceMargin,
|
|
|
|
|
ctx.triggerPrice,
|
|
|
|
|
ctx.offerMinAmount,
|
|
|
|
|
ctx.reserveExactAmount,
|
|
|
|
|
ctx.isPrivateOffer,
|
|
|
|
|
ctx.buyerAsTakerWithoutDeposit);
|
|
|
|
|
// post or clone offer
|
|
|
|
|
const offer: OfferInfo = await ctx.maker.havenod!.postOffer({
|
|
|
|
|
direction: ctx.direction,
|
|
|
|
|
amount: ctx.offerAmount,
|
|
|
|
|
assetCode: ctx.assetCode,
|
|
|
|
|
paymentAccountId: ctx.makerPaymentAccountId,
|
|
|
|
|
securityDepositPct: ctx.securityDepositPct,
|
|
|
|
|
price: ctx.price,
|
|
|
|
|
marketPriceMarginPct: ctx.priceMargin,
|
|
|
|
|
triggerPrice: ctx.triggerPrice,
|
|
|
|
|
minAmount: ctx.offerMinAmount,
|
|
|
|
|
reserveExactAmount: ctx.reserveExactAmount,
|
|
|
|
|
isPrivateOffer: ctx.isPrivateOffer,
|
|
|
|
|
buyerAsTakerWithoutDeposit: ctx.buyerAsTakerWithoutDeposit,
|
|
|
|
|
extraInfo: ctx.extraInfo,
|
|
|
|
|
sourceOfferId: ctx.sourceOfferId
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// transfer context from clone source
|
|
|
|
|
if (ctx.sourceOfferId) {
|
|
|
|
|
const sourceOffer = await ctx.maker.havenod!.getMyOffer(ctx.sourceOfferId);
|
|
|
|
|
ctx.isPrivateOffer = sourceOffer.getIsPrivateOffer();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// test offer
|
|
|
|
|
testOffer(offer, ctx, true);
|
|
|
|
|
|
|
|
|
|
// offer is included in my offers only
|
|
|
|
@ -2935,13 +3007,13 @@ async function makeOffer(ctxP?: Partial<TradeContext>): Promise<OfferInfo> {
|
|
|
|
|
if (offer.getState() === "PENDING") {
|
|
|
|
|
if (!ctx.reserveExactAmount && unlockedBalanceAfter !== unlockedBalanceBefore) throw new Error("Unlocked balance should not change for scheduled offer " + offer.getId());
|
|
|
|
|
} else if (offer.getState() === "AVAILABLE") {
|
|
|
|
|
if (unlockedBalanceAfter === unlockedBalanceBefore) {
|
|
|
|
|
if (!ctx.sourceOfferId && unlockedBalanceAfter === unlockedBalanceBefore) {
|
|
|
|
|
console.warn("Unlocked balance did not change after posting offer, waiting a sync period");
|
|
|
|
|
await wait(ctx.walletSyncPeriodMs);
|
|
|
|
|
unlockedBalanceAfter = BigInt((await ctx.maker.havenod!.getBalances()).getAvailableBalance());
|
|
|
|
|
if (unlockedBalanceAfter === unlockedBalanceBefore) throw new Error("Unlocked balance did not change after posting offer " + offer.getId() + ", before=" + unlockedBalanceBefore + ", after=" + unlockedBalanceAfter);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
} else if (!ctx.sourceOfferId) { // cloned offers can be deactivated after creating
|
|
|
|
|
throw new Error("Unexpected offer state after posting: " + offer.getState());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -3151,16 +3223,17 @@ async function testOpenDispute(ctxP: Partial<TradeContext>) {
|
|
|
|
|
expect(sellerPaymentAccountPayload).toEqual(expectedSellerPaymentAccountPayload);
|
|
|
|
|
expect(await ctx.arbitrator.havenod?.getPaymentAccountPayloadForm(sellerPaymentAccountPayload!)).toEqual(await ctx.arbitrator.havenod?.getPaymentAccountPayloadForm(expectedSellerPaymentAccountPayload!));
|
|
|
|
|
|
|
|
|
|
// arbitrator has buyer's payment account info unless opener is seller and payment not sent
|
|
|
|
|
// arbitrator has buyer's payment account info unless seller opens dispute before payment sent
|
|
|
|
|
// TODO: should arbitrator receive buyer's payment account info if seller opens dispute before payment sent?
|
|
|
|
|
let buyerPaymentAccountPayload = arbDisputeOpener.getContract()!.getIsBuyerMakerAndSellerTaker() ? arbDisputeOpener.getMakerPaymentAccountPayload() : arbDisputeOpener.getTakerPaymentAccountPayload();
|
|
|
|
|
if (ctx.getDisputeOpener()!.havenod === ctx.getSeller().havenod && !ctx.isPaymentSent) expect(buyerPaymentAccountPayload).toBeUndefined();
|
|
|
|
|
if (ctx.getDisputeOpener()!.havenod === ctx.getSeller().havenod && ctx.sellerDisputeContext === DisputeContext.OPEN_AFTER_DEPOSITS_UNLOCK) expect(buyerPaymentAccountPayload).toBeUndefined();
|
|
|
|
|
else {
|
|
|
|
|
let expectedBuyerPaymentAccountPayload = (await ctx.getBuyer().havenod?.getPaymentAccount(buyerPaymentAccountPayload?.getId()!))?.getPaymentAccountPayload();
|
|
|
|
|
expect(buyerPaymentAccountPayload).toEqual(expectedBuyerPaymentAccountPayload);
|
|
|
|
|
expect(await ctx.arbitrator.havenod?.getPaymentAccountPayloadForm(buyerPaymentAccountPayload!)).toEqual(await ctx.arbitrator.havenod?.getPaymentAccountPayloadForm(expectedBuyerPaymentAccountPayload!));
|
|
|
|
|
}
|
|
|
|
|
buyerPaymentAccountPayload = arbDisputePeer.getContract()!.getIsBuyerMakerAndSellerTaker() ? arbDisputePeer.getMakerPaymentAccountPayload() : arbDisputePeer.getTakerPaymentAccountPayload();
|
|
|
|
|
if (ctx.getDisputeOpener()!.havenod === ctx.getSeller().havenod && !ctx.isPaymentSent) expect(buyerPaymentAccountPayload).toBeUndefined();
|
|
|
|
|
if (ctx.getDisputeOpener()!.havenod === ctx.getSeller().havenod && ctx.sellerDisputeContext === DisputeContext.OPEN_AFTER_DEPOSITS_UNLOCK) expect(buyerPaymentAccountPayload).toBeUndefined();
|
|
|
|
|
else {
|
|
|
|
|
let expectedBuyerPaymentAccountPayload = (await ctx.getBuyer().havenod?.getPaymentAccount(buyerPaymentAccountPayload?.getId()!))?.getPaymentAccountPayload();
|
|
|
|
|
expect(buyerPaymentAccountPayload).toEqual(expectedBuyerPaymentAccountPayload);
|
|
|
|
@ -3368,8 +3441,8 @@ async function testAmountsAfterComplete(tradeCtx: TradeContext) {
|
|
|
|
|
const payoutTxFee = BigInt(payoutTx!.getFee());
|
|
|
|
|
|
|
|
|
|
// get expected payouts for normal trade
|
|
|
|
|
const isDisputedTrade = tradeCtx.getDisputeOpener() !== undefined;
|
|
|
|
|
if (!isDisputedTrade) {
|
|
|
|
|
const isResolvedByDispute = tradeCtx.wasDisputeOpened() && tradeCtx.resolveDispute;
|
|
|
|
|
if (!isResolvedByDispute) {
|
|
|
|
|
tradeCtx.getBuyer().payoutTxFee = payoutTxFee / 2n;
|
|
|
|
|
tradeCtx.getBuyer().payoutAmount = tradeCtx.getBuyer().securityDepositActual! + tradeCtx.tradeAmount! - tradeCtx.getBuyer().payoutTxFee!;
|
|
|
|
|
tradeCtx.getSeller().payoutTxFee = payoutTxFee / 2n;
|
|
|
|
@ -3392,7 +3465,7 @@ async function testAmountsAfterComplete(tradeCtx: TradeContext) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: payout tx is unknown to offline non-signer until confirmed
|
|
|
|
|
if (isDisputedTrade || tradeCtx.isOfflineFlow()) {
|
|
|
|
|
if (isResolvedByDispute || tradeCtx.isOfflineFlow()) {
|
|
|
|
|
await mineToHeight(await monerod.getHeight() + 1);
|
|
|
|
|
await wait(TestConfig.maxWalletStartupMs + tradeCtx.walletSyncPeriodMs * 2);
|
|
|
|
|
}
|
|
|
|
@ -4203,6 +4276,7 @@ function testOffer(offer: OfferInfo, ctxP?: Partial<TradeContext>, isMyOffer?: b
|
|
|
|
|
expect(offer.getBuyerSecurityDepositPct()).toEqual(ctx.securityDepositPct);
|
|
|
|
|
expect(offer.getChallenge()).toEqual("");
|
|
|
|
|
}
|
|
|
|
|
if (ctx.extraInfo) expect(offer.getExtraInfo().indexOf(ctx.extraInfo)).toBeGreaterThanOrEqual(0); // may contain extra info from payment account
|
|
|
|
|
expect(offer.getSellerSecurityDepositPct()).toEqual(ctx.securityDepositPct);
|
|
|
|
|
expect(offer.getUseMarketBasedPrice()).toEqual(!ctx?.price);
|
|
|
|
|
expect(offer.getMarketPriceMarginPct()).toEqual(ctx?.priceMargin ? ctx.priceMargin : 0);
|
|
|
|
@ -4315,7 +4389,7 @@ function getValidFormInput(form: PaymentAccountForm, fieldId: PaymentAccountForm
|
|
|
|
|
case PaymentAccountFormField.FieldId.INTERMEDIARY_SWIFT_CODE:
|
|
|
|
|
return "10987654321"; // TODO: use real swift code
|
|
|
|
|
case PaymentAccountFormField.FieldId.MOBILE_NR:
|
|
|
|
|
throw new Error("Not implemented");
|
|
|
|
|
return "876-512-7813";
|
|
|
|
|
case PaymentAccountFormField.FieldId.NATIONAL_ACCOUNT_ID:
|
|
|
|
|
throw new Error("Not implemented");
|
|
|
|
|
case PaymentAccountFormField.FieldId.PAYID:
|
|
|
|
@ -4454,7 +4528,7 @@ function getInvalidFormInput(form: PaymentAccountForm, fieldId: PaymentAccountFo
|
|
|
|
|
case PaymentAccountFormField.FieldId.INTERMEDIARY_SWIFT_CODE:
|
|
|
|
|
return "A";
|
|
|
|
|
case PaymentAccountFormField.FieldId.MOBILE_NR:
|
|
|
|
|
throw new Error("Not implemented");
|
|
|
|
|
return "A";
|
|
|
|
|
case PaymentAccountFormField.FieldId.NATIONAL_ACCOUNT_ID:
|
|
|
|
|
throw new Error("Not implemented");
|
|
|
|
|
case PaymentAccountFormField.FieldId.PAYID:
|
|
|
|
@ -4603,6 +4677,10 @@ function testPaymentAccount(account: PaymentAccount, form: PaymentAccountForm) {
|
|
|
|
|
expect(account.getTradeCurrenciesList().length).toEqual(1);
|
|
|
|
|
expect(account.getTradeCurrenciesList()[0].getCode()).toEqual("USD");
|
|
|
|
|
break;
|
|
|
|
|
case PaymentAccountForm.FormId.PAYSAFE:
|
|
|
|
|
expect(account.getPaymentAccountPayload()!.getPaysafeAccountPayload()!.getEmail()).toEqual(getFormField(form, PaymentAccountFormField.FieldId.EMAIL).getValue());
|
|
|
|
|
expect(account.getTradeCurrenciesList().map(currency => currency.getCode()).join(",")).toEqual(getFormField(form, PaymentAccountFormField.FieldId.TRADE_CURRENCIES).getValue());
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
throw new Error("Unhandled payment method type: " + form.getId());
|
|
|
|
|
}
|
|
|
|
|