test clone grpc api with new post offer config

This commit is contained in:
woodser 2025-04-04 16:25:09 -04:00 committed by woodser
parent 5764a1959f
commit 0cd3c54e37
2 changed files with 117 additions and 60 deletions

View file

@ -196,6 +196,7 @@ class TradeContext {
isPrivateOffer?: boolean; isPrivateOffer?: boolean;
buyerAsTakerWithoutDeposit?: boolean; // buyer as taker security deposit is optional for private offers buyerAsTakerWithoutDeposit?: boolean; // buyer as taker security deposit is optional for private offers
extraInfo?: string; extraInfo?: string;
sourceOfferId?: string;
// take offer // take offer
awaitFundsToTakeOffer?: boolean; awaitFundsToTakeOffer?: boolean;
@ -1553,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"); 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 // TODO: provide number of confirmations in offer status
test("Can schedule offers with locked funds (Test, CI)", async () => { test("Can schedule offers with locked funds (Test, CI)", async () => {
let user3: HavenoClient|undefined; let user3: HavenoClient|undefined;
@ -2919,21 +2954,31 @@ async function makeOffer(ctxP?: Partial<TradeContext>): Promise<OfferInfo> {
ctx.taker.balancesBeforeOffer = await ctx.taker.havenod?.getBalances(); ctx.taker.balancesBeforeOffer = await ctx.taker.havenod?.getBalances();
} }
// post offer // post or clone offer
const offer: OfferInfo = await ctx.maker.havenod!.postOffer( const offer: OfferInfo = await ctx.maker.havenod!.postOffer({
ctx.direction!, direction: ctx.direction,
ctx.offerAmount!, amount: ctx.offerAmount,
ctx.assetCode!, assetCode: ctx.assetCode,
ctx.makerPaymentAccountId!, paymentAccountId: ctx.makerPaymentAccountId,
ctx.securityDepositPct!, securityDepositPct: ctx.securityDepositPct,
ctx.price, price: ctx.price,
ctx.priceMargin, marketPriceMarginPct: ctx.priceMargin,
ctx.triggerPrice, triggerPrice: ctx.triggerPrice,
ctx.offerMinAmount, minAmount: ctx.offerMinAmount,
ctx.reserveExactAmount, reserveExactAmount: ctx.reserveExactAmount,
ctx.isPrivateOffer, isPrivateOffer: ctx.isPrivateOffer,
ctx.buyerAsTakerWithoutDeposit, buyerAsTakerWithoutDeposit: ctx.buyerAsTakerWithoutDeposit,
ctx.extraInfo); 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); testOffer(offer, ctx, true);
// offer is included in my offers only // offer is included in my offers only
@ -2962,13 +3007,13 @@ async function makeOffer(ctxP?: Partial<TradeContext>): Promise<OfferInfo> {
if (offer.getState() === "PENDING") { if (offer.getState() === "PENDING") {
if (!ctx.reserveExactAmount && unlockedBalanceAfter !== unlockedBalanceBefore) throw new Error("Unlocked balance should not change for scheduled offer " + offer.getId()); if (!ctx.reserveExactAmount && unlockedBalanceAfter !== unlockedBalanceBefore) throw new Error("Unlocked balance should not change for scheduled offer " + offer.getId());
} else if (offer.getState() === "AVAILABLE") { } 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"); console.warn("Unlocked balance did not change after posting offer, waiting a sync period");
await wait(ctx.walletSyncPeriodMs); await wait(ctx.walletSyncPeriodMs);
unlockedBalanceAfter = BigInt((await ctx.maker.havenod!.getBalances()).getAvailableBalance()); 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); 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()); throw new Error("Unexpected offer state after posting: " + offer.getState());
} }

View file

@ -24,6 +24,27 @@ import { GetTradeStatisticsClient, GetVersionClient, AccountClient, XmrConnectio
import { GetTradeStatisticsRequest, GetTradeStatisticsReply, GetVersionRequest, GetVersionReply, IsAppInitializedRequest, IsAppInitializedReply, RegisterDisputeAgentRequest, UnregisterDisputeAgentRequest, MarketPriceRequest, MarketPriceReply, MarketPricesRequest, MarketPricesReply, MarketPriceInfo, MarketDepthRequest, MarketDepthReply, MarketDepthInfo, GetBalancesRequest, GetBalancesReply, XmrBalanceInfo, GetMyOfferRequest, GetMyOfferReply, GetOffersRequest, GetOffersReply, OfferInfo, GetPaymentMethodsRequest, GetPaymentMethodsReply, GetPaymentAccountFormRequest, CreatePaymentAccountRequest, ValidateFormFieldRequest, CreatePaymentAccountReply, GetPaymentAccountFormReply, GetPaymentAccountsRequest, GetPaymentAccountsReply, CreateCryptoCurrencyPaymentAccountRequest, CreateCryptoCurrencyPaymentAccountReply, DeletePaymentAccountRequest, DeletePaymentAccountReply, PostOfferRequest, PostOfferReply, CancelOfferRequest, TakeOfferRequest, TakeOfferReply, TradeInfo, GetTradeRequest, GetTradeReply, GetTradesRequest, GetTradesReply, GetXmrSeedRequest, GetXmrSeedReply, GetXmrPrimaryAddressRequest, GetXmrPrimaryAddressReply, GetXmrNewSubaddressRequest, GetXmrNewSubaddressReply, ConfirmPaymentSentRequest, ConfirmPaymentReceivedRequest, CompleteTradeRequest, XmrTx, GetXmrTxsRequest, GetXmrTxsReply, XmrDestination, CreateXmrTxRequest, CreateXmrTxReply, RelayXmrTxRequest, RelayXmrTxReply, CreateAccountRequest, AccountExistsRequest, AccountExistsReply, DeleteAccountRequest, OpenAccountRequest, IsAccountOpenRequest, IsAccountOpenReply, CloseAccountRequest, ChangePasswordRequest, BackupAccountRequest, BackupAccountReply, RestoreAccountRequest, StopRequest, NotificationMessage, RegisterNotificationListenerRequest, SendNotificationRequest, UrlConnection, AddConnectionRequest, RemoveConnectionRequest, GetConnectionRequest, GetConnectionsRequest, SetConnectionRequest, CheckConnectionRequest, CheckConnectionsReply, CheckConnectionsRequest, StartCheckingConnectionRequest, StopCheckingConnectionRequest, GetBestConnectionRequest, SetAutoSwitchRequest, GetAutoSwitchRequest, CheckConnectionReply, GetConnectionsReply, GetConnectionReply, GetBestConnectionReply, GetDisputeRequest, GetDisputeReply, GetDisputesRequest, GetDisputesReply, OpenDisputeRequest, ResolveDisputeRequest, SendDisputeChatMessageRequest, SendChatMessageRequest, GetChatMessagesRequest, GetChatMessagesReply, StartXmrNodeRequest, StopXmrNodeRequest, IsXmrNodeOnlineRequest, IsXmrNodeOnlineReply, GetXmrNodeSettingsRequest, GetXmrNodeSettingsReply } from "./protobuf/grpc_pb"; import { GetTradeStatisticsRequest, GetTradeStatisticsReply, GetVersionRequest, GetVersionReply, IsAppInitializedRequest, IsAppInitializedReply, RegisterDisputeAgentRequest, UnregisterDisputeAgentRequest, MarketPriceRequest, MarketPriceReply, MarketPricesRequest, MarketPricesReply, MarketPriceInfo, MarketDepthRequest, MarketDepthReply, MarketDepthInfo, GetBalancesRequest, GetBalancesReply, XmrBalanceInfo, GetMyOfferRequest, GetMyOfferReply, GetOffersRequest, GetOffersReply, OfferInfo, GetPaymentMethodsRequest, GetPaymentMethodsReply, GetPaymentAccountFormRequest, CreatePaymentAccountRequest, ValidateFormFieldRequest, CreatePaymentAccountReply, GetPaymentAccountFormReply, GetPaymentAccountsRequest, GetPaymentAccountsReply, CreateCryptoCurrencyPaymentAccountRequest, CreateCryptoCurrencyPaymentAccountReply, DeletePaymentAccountRequest, DeletePaymentAccountReply, PostOfferRequest, PostOfferReply, CancelOfferRequest, TakeOfferRequest, TakeOfferReply, TradeInfo, GetTradeRequest, GetTradeReply, GetTradesRequest, GetTradesReply, GetXmrSeedRequest, GetXmrSeedReply, GetXmrPrimaryAddressRequest, GetXmrPrimaryAddressReply, GetXmrNewSubaddressRequest, GetXmrNewSubaddressReply, ConfirmPaymentSentRequest, ConfirmPaymentReceivedRequest, CompleteTradeRequest, XmrTx, GetXmrTxsRequest, GetXmrTxsReply, XmrDestination, CreateXmrTxRequest, CreateXmrTxReply, RelayXmrTxRequest, RelayXmrTxReply, CreateAccountRequest, AccountExistsRequest, AccountExistsReply, DeleteAccountRequest, OpenAccountRequest, IsAccountOpenRequest, IsAccountOpenReply, CloseAccountRequest, ChangePasswordRequest, BackupAccountRequest, BackupAccountReply, RestoreAccountRequest, StopRequest, NotificationMessage, RegisterNotificationListenerRequest, SendNotificationRequest, UrlConnection, AddConnectionRequest, RemoveConnectionRequest, GetConnectionRequest, GetConnectionsRequest, SetConnectionRequest, CheckConnectionRequest, CheckConnectionsReply, CheckConnectionsRequest, StartCheckingConnectionRequest, StopCheckingConnectionRequest, GetBestConnectionRequest, SetAutoSwitchRequest, GetAutoSwitchRequest, CheckConnectionReply, GetConnectionsReply, GetConnectionReply, GetBestConnectionReply, GetDisputeRequest, GetDisputeReply, GetDisputesRequest, GetDisputesReply, OpenDisputeRequest, ResolveDisputeRequest, SendDisputeChatMessageRequest, SendChatMessageRequest, GetChatMessagesRequest, GetChatMessagesReply, StartXmrNodeRequest, StopXmrNodeRequest, IsXmrNodeOnlineRequest, IsXmrNodeOnlineReply, GetXmrNodeSettingsRequest, GetXmrNodeSettingsReply } from "./protobuf/grpc_pb";
import { TradeStatistics3, OfferDirection, PaymentMethod, PaymentAccountForm, PaymentAccountFormField, PaymentAccount, PaymentAccountPayload, AvailabilityResult, Attachment, DisputeResult, Dispute, ChatMessage, XmrNodeSettings } from "./protobuf/pb_pb"; import { TradeStatistics3, OfferDirection, PaymentMethod, PaymentAccountForm, PaymentAccountFormField, PaymentAccount, PaymentAccountPayload, AvailabilityResult, Attachment, DisputeResult, Dispute, ChatMessage, XmrNodeSettings } from "./protobuf/pb_pb";
/**
* Configuration to post, clone, or edit an offer.
*/
export interface OfferConfig {
direction?: OfferDirection;
amount?: bigint;
minAmount?: bigint;
assetCode?: string;
paymentAccountId?: string;
securityDepositPct?: number;
price?: number;
marketPriceMarginPct?: number;
triggerPrice?: number;
reserveExactAmount?: boolean;
isPrivateOffer?: boolean;
buyerAsTakerWithoutDeposit?: boolean;
extraInfo?: string;
sourceOfferId?: string;
}
/** /**
* Haveno daemon client. * Haveno daemon client.
*/ */
@ -1035,53 +1056,44 @@ export default class HavenoClient {
} }
/** /**
* Post an offer. * Post or clone an offer.
* *
* @param {OfferDirection} direction - "buy" or "sell" XMR * @param {OfferConfig} config - configures the offer to post or clone
* @param {bigint} amount - amount of XMR to trade * @param {OfferDirection} [config.direction] - specifies to buy or sell xmr (default buy)
* @param {string} assetCode - asset code to trade for XMR * @param {bigint} [config.amount] - amount of XMR to trade
* @param {string} paymentAccountId - payment account id * @param {string} [config.assetCode] - asset code to trade for XMR
* @param {number} securityDepositPct - security deposit as % of trade amount for buyer and seller * @param {string} [config.paymentAccountId] - payment account id
* @param {number} price - trade price (optional, default to market price) * @param {number} [config.securityDepositPct] - security deposit as % of trade amount for buyer and seller
* @param {number} marketPriceMarginPct - if using market price, % from market price to accept (optional, default 0%) * @param {number} [config.price] - trade price (optional, default to market price)
* @param {number} triggerPrice - price to remove offer (optional) * @param {number} [config.marketPriceMarginPct] - if using market price, % from market price to accept (optional, default 0%)
* @param {bigint} minAmount - minimum amount to trade (optional, default to fixed amount) * @param {number} [config.triggerPrice] - price to remove offer (optional)
* @param {number} reserveExactAmount - reserve exact amount needed for offer, incurring on-chain transaction and 10 confirmations before the offer goes live (default = false) * @param {bigint} [config.minAmount] - minimum amount to trade (optional, default to fixed amount)
* @param {boolean} isPrivateOffer - whether the offer is private (default = false) * @param {number} [config.reserveExactAmount] - reserve exact amount needed for offer, incurring on-chain transaction and 10 confirmations before the offer goes live (default = false)
* @param {boolean} buyerAsTakerWithoutDeposit - waive buyer as taker deposit and fee (default false) * @param {boolean} [config.isPrivateOffer] - whether the offer is private (default = false)
* @param {string} extraInfo - extra information to include with the offer (optional) * @param {boolean} [config.buyerAsTakerWithoutDeposit] - waive buyer as taker deposit and fee (default false)
* @param {string} [config.extraInfo] - extra information to include with the offer (optional)
* @param {string} [config.sourceOfferId] - create a clone of a source offer which shares the same reserved funds. overrides other fields which are immutable or unspecified (optional)
* @return {OfferInfo} the posted offer * @return {OfferInfo} the posted offer
*/ */
async postOffer(direction: OfferDirection, async postOffer(config: OfferConfig): Promise<OfferInfo> {
amount: bigint, console.log("Posting offer with security deposit %: " + config.securityDepositPct)
assetCode: string,
paymentAccountId: string,
securityDepositPct: number,
price?: number,
marketPriceMarginPct?: number,
triggerPrice?: number,
minAmount?: bigint,
reserveExactAmount?: boolean,
isPrivateOffer?: boolean,
buyerAsTakerWithoutDeposit?: boolean,
extraInfo?: string): Promise<OfferInfo> {
console.log("Posting offer with security deposit %: " + securityDepositPct)
try { try {
const request = new PostOfferRequest() const request = new PostOfferRequest();
.setDirection(direction === OfferDirection.BUY ? "buy" : "sell") if (config.direction) request.setDirection(config.direction === OfferDirection.BUY ? "buy" : "sell");
.setAmount(amount.toString()) if (config.amount) request.setAmount(config.amount.toString());
.setCurrencyCode(assetCode) request.setMinAmount(config.minAmount ? config.minAmount.toString() : config.amount!.toString());
.setPaymentAccountId(paymentAccountId) if (config.assetCode) request.setCurrencyCode(config.assetCode);
.setSecurityDepositPct(securityDepositPct) if (config.paymentAccountId) request.setPaymentAccountId(config.paymentAccountId);
.setUseMarketBasedPrice(price === undefined) if (config.securityDepositPct) request.setSecurityDepositPct(config.securityDepositPct);
.setMinAmount(minAmount ? minAmount.toString() : amount.toString()); request.setUseMarketBasedPrice(config.price === undefined);
if (price) request.setPrice(price.toString()); if (config.price) request.setPrice(config.price?.toString())
if (marketPriceMarginPct) request.setMarketPriceMarginPct(marketPriceMarginPct); if (config.marketPriceMarginPct) request.setMarketPriceMarginPct(config.marketPriceMarginPct);
if (triggerPrice) request.setTriggerPrice(triggerPrice.toString()); if (config.triggerPrice) request.setTriggerPrice(config.triggerPrice.toString());
if (reserveExactAmount) request.setReserveExactAmount(true); if (config.reserveExactAmount) request.setReserveExactAmount(true);
if (isPrivateOffer) request.setIsPrivateOffer(true); if (config.isPrivateOffer) request.setIsPrivateOffer(true);
if (buyerAsTakerWithoutDeposit) request.setBuyerAsTakerWithoutDeposit(true); if (config.buyerAsTakerWithoutDeposit) request.setBuyerAsTakerWithoutDeposit(true);
if (extraInfo) request.setExtraInfo(extraInfo); if (config.extraInfo) request.setExtraInfo(config.extraInfo);
if (config.sourceOfferId) request.setSourceOfferId(config.sourceOfferId);
return (await this._offersClient.postOffer(request, {password: this._password})).getOffer()!; return (await this._offersClient.postOffer(request, {password: this._password})).getOffer()!;
} catch (e: any) { } catch (e: any) {
throw new HavenoError(e.message, e.code); throw new HavenoError(e.message, e.code);